Merge branch 'develop' into migrate-location-app
# Conflicts: # apps/gallery/models.py # requirements/base.txt
This commit is contained in:
commit
95b5d4ad99
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -21,4 +21,6 @@ logs/
|
|||
/geoip_db/
|
||||
|
||||
# dev
|
||||
./docker-compose.override.yml
|
||||
./docker-compose.override.yml
|
||||
|
||||
celerybeat-schedule
|
||||
|
|
|
|||
40
.gitlab-ci.yml
Normal file
40
.gitlab-ci.yml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
image: docker:latest
|
||||
|
||||
stages:
|
||||
- build
|
||||
- test
|
||||
- deploy
|
||||
- clean
|
||||
|
||||
|
||||
clean:
|
||||
stage: clean
|
||||
script:
|
||||
- docker-compose -f compose-ci.yml stop
|
||||
- docker-compose -f compose-ci.yml rm --force gm_app
|
||||
when: always
|
||||
|
||||
|
||||
buid:
|
||||
stage: build
|
||||
script:
|
||||
- docker-compose -f compose-ci.yml build gm_app
|
||||
when: always
|
||||
|
||||
|
||||
test:
|
||||
stage: test
|
||||
script:
|
||||
- docker-compose -f compose-ci.yml run gm_app python manage.py test -v 3 --noinput
|
||||
when: always
|
||||
|
||||
|
||||
|
||||
deploy-develop:
|
||||
stage: deploy
|
||||
only:
|
||||
- develop
|
||||
script:
|
||||
- fab --roles=develop deploy
|
||||
environment:
|
||||
name: Develop
|
||||
18
apps/account/migrations/0009_auto_20191002_0648.py
Normal file
18
apps/account/migrations/0009_auto_20191002_0648.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-02 06:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('account', '0008_auto_20190912_1325'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='email',
|
||||
field=models.EmailField(default=None, max_length=254, null=True, unique=True, verbose_name='email address'),
|
||||
),
|
||||
]
|
||||
14
apps/account/migrations/0011_merge_20191011_1336.py
Normal file
14
apps/account/migrations/0011_merge_20191011_1336.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-11 13:36
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('account', '0009_auto_20191002_0648'),
|
||||
('account', '0010_user_password_confirmed'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
14
apps/account/migrations/0012_merge_20191015_0912.py
Normal file
14
apps/account/migrations/0012_merge_20191015_0912.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-15 09:12
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('account', '0011_merge_20191014_1258'),
|
||||
('account', '0011_merge_20191011_1336'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
14
apps/account/migrations/0014_merge_20191023_0959.py
Normal file
14
apps/account/migrations/0014_merge_20191023_0959.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-23 09:59
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('account', '0009_auto_20191002_0648'),
|
||||
('account', '0013_auto_20191016_0810'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
14
apps/account/migrations/0015_merge_20191023_1317.py
Normal file
14
apps/account/migrations/0015_merge_20191023_1317.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-23 13:17
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('account', '0014_merge_20191023_0959'),
|
||||
('account', '0012_merge_20191015_0912'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
24
apps/account/migrations/0016_auto_20191024_0830.py
Normal file
24
apps/account/migrations/0016_auto_20191024_0830.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-24 08:30
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('account', '0015_merge_20191023_1317'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='role',
|
||||
name='role',
|
||||
field=models.PositiveIntegerField(choices=[(1, 'Standard user'), (2, 'Comments moderator'), (3, 'Country admin'), (4, 'Content page manager'), (5, 'Establishment manager'), (6, 'Reviewer manager'), (7, 'Restaurant reviewer')], verbose_name='Role'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userrole',
|
||||
name='establishment',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='establishment.Establishment', verbose_name='Establishment'),
|
||||
),
|
||||
]
|
||||
24
apps/account/migrations/0016_auto_20191024_0833.py
Normal file
24
apps/account/migrations/0016_auto_20191024_0833.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-24 08:33
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('account', '0015_merge_20191023_1317'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='role',
|
||||
name='role',
|
||||
field=models.PositiveIntegerField(choices=[(1, 'Standard user'), (2, 'Comments moderator'), (3, 'Country admin'), (4, 'Content page manager'), (5, 'Establishment manager'), (6, 'Reviewer manager'), (7, 'Restaurant reviewer')], verbose_name='Role'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userrole',
|
||||
name='establishment',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='establishment.Establishment', verbose_name='Establishment'),
|
||||
),
|
||||
]
|
||||
14
apps/account/migrations/0017_merge_20191024_1233.py
Normal file
14
apps/account/migrations/0017_merge_20191024_1233.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-24 12:33
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('account', '0016_auto_20191024_0830'),
|
||||
('account', '0016_auto_20191024_0833'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
|
|
@ -89,7 +89,7 @@ class User(AbstractUser):
|
|||
blank=True, null=True, default=None)
|
||||
cropped_image_url = models.URLField(verbose_name=_('Cropped image URL path'),
|
||||
blank=True, null=True, default=None)
|
||||
email = models.EmailField(_('email address'), blank=True,
|
||||
email = models.EmailField(_('email address'), unique=True,
|
||||
null=True, default=None)
|
||||
unconfirmed_email = models.EmailField(_('unconfirmed email'), blank=True, null=True, default=None)
|
||||
email_confirmed = models.BooleanField(_('email status'), default=False)
|
||||
|
|
@ -214,6 +214,15 @@ class User(AbstractUser):
|
|||
template_name=settings.RESETTING_TOKEN_TEMPLATE,
|
||||
context=context)
|
||||
|
||||
def notify_password_changed_template(self, country_code):
|
||||
"""Get notification email template"""
|
||||
context = {'contry_code': country_code}
|
||||
context.update(self.base_template)
|
||||
return render_to_string(
|
||||
template_name=settings.NOTIFICATION_PASSWORD_TEMPLATE,
|
||||
context=context,
|
||||
)
|
||||
|
||||
def confirm_email_template(self, country_code):
|
||||
"""Get confirm email template"""
|
||||
context = {'token': self.confirm_email_token,
|
||||
|
|
|
|||
|
|
@ -127,6 +127,14 @@ class ChangePasswordSerializer(serializers.ModelSerializer):
|
|||
except serializers.ValidationError as e:
|
||||
raise serializers.ValidationError({'detail': e.detail})
|
||||
else:
|
||||
if settings.USE_CELERY:
|
||||
tasks.send_password_changed_email(
|
||||
user_id=self.instance.id,
|
||||
country_code=self.context.get('request').country_code)
|
||||
else:
|
||||
tasks.send_password_changed_email(
|
||||
user_id=self.instance.id,
|
||||
country_code=self.context.get('request').country_code)
|
||||
return attrs
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
"""Serializers for account web"""
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import password_validation as password_validators
|
||||
from rest_framework import serializers
|
||||
|
||||
from account import models
|
||||
from account import models, tasks
|
||||
from utils import exceptions as utils_exceptions
|
||||
from utils.methods import username_validator
|
||||
|
||||
|
|
@ -68,4 +69,12 @@ class PasswordResetConfirmSerializer(serializers.ModelSerializer):
|
|||
# Update user password from instance
|
||||
instance.set_password(validated_data.get('password'))
|
||||
instance.save()
|
||||
if settings.USE_CELERY:
|
||||
tasks.send_password_changed_email(
|
||||
user_id=instance.id,
|
||||
country_code=self.context.get('request').country_code)
|
||||
else:
|
||||
tasks.send_password_changed_email(
|
||||
user_id=instance.id,
|
||||
country_code=self.context.get('request').country_code)
|
||||
return instance
|
||||
|
|
|
|||
|
|
@ -1,47 +1,46 @@
|
|||
"""Account app celery tasks."""
|
||||
import inspect
|
||||
import logging
|
||||
|
||||
from celery import shared_task
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from . import models
|
||||
from account.models import User
|
||||
|
||||
logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def send_email(user_id: int, subject: str, message_prop: str, country_code: str):
|
||||
try:
|
||||
user = User.objects.get(id=user_id)
|
||||
user.send_email(subject=_(subject),
|
||||
message=getattr(user, message_prop, lambda _: '')(country_code))
|
||||
except:
|
||||
cur_frame = inspect.currentframe()
|
||||
cal_frame = inspect.getouterframes(cur_frame, 2)
|
||||
logger.error(f'METHOD_NAME: {cal_frame[1][3]}\n'
|
||||
f'DETAIL: Exception occurred for user: {user_id}')
|
||||
|
||||
|
||||
@shared_task
|
||||
def send_reset_password_email(user_id, country_code):
|
||||
"""Send email to user for reset password."""
|
||||
try:
|
||||
user = models.User.objects.get(id=user_id)
|
||||
user.send_email(subject=_('Password resetting'),
|
||||
message=user.reset_password_template(country_code))
|
||||
except:
|
||||
logger.error(f'METHOD_NAME: {send_reset_password_email.__name__}\n'
|
||||
f'DETAIL: Exception occurred for reset password: '
|
||||
f'{user_id}')
|
||||
send_email(user_id, 'Password_resetting', 'reset_password_template', country_code)
|
||||
|
||||
|
||||
@shared_task
|
||||
def confirm_new_email_address(user_id, country_code):
|
||||
"""Send email to user new email."""
|
||||
try:
|
||||
user = models.User.objects.get(id=user_id)
|
||||
user.send_email(subject=_('Validate new email address'),
|
||||
message=user.confirm_email_template(country_code))
|
||||
except:
|
||||
logger.error(f'METHOD_NAME: {confirm_new_email_address.__name__}\n'
|
||||
f'DETAIL: Exception occurred for user: {user_id}')
|
||||
send_email(user_id, 'Confirm new email address', 'confirm_email_template', country_code)
|
||||
|
||||
|
||||
@shared_task
|
||||
def change_email_address(user_id, country_code):
|
||||
"""Send email to user new email."""
|
||||
try:
|
||||
user = models.User.objects.get(id=user_id)
|
||||
user.send_email(subject=_('Validate new email address'),
|
||||
message=user.change_email_template(country_code))
|
||||
except:
|
||||
logger.error(f'METHOD_NAME: {change_email_address.__name__}\n'
|
||||
f'DETAIL: Exception occurred for user: {user_id}')
|
||||
send_email(user_id, 'Validate new email address', 'change_email_template', country_code)
|
||||
|
||||
|
||||
@shared_task
|
||||
def send_password_changed_email(user_id, country_code):
|
||||
"""Send email which notifies user that his password had changed"""
|
||||
send_email(user_id, 'Notify password changed', 'notify_password_changed_template', country_code)
|
||||
|
|
|
|||
|
|
@ -18,12 +18,6 @@ from utils.tokens import GMRefreshToken
|
|||
# Serializers
|
||||
class SignupSerializer(serializers.ModelSerializer):
|
||||
"""Signup serializer serializer mixin"""
|
||||
# REQUEST
|
||||
username = serializers.CharField(write_only=True)
|
||||
password = serializers.CharField(write_only=True)
|
||||
email = serializers.EmailField(write_only=True)
|
||||
newsletter = serializers.BooleanField(write_only=True)
|
||||
|
||||
class Meta:
|
||||
model = account_models.User
|
||||
fields = (
|
||||
|
|
@ -32,6 +26,12 @@ class SignupSerializer(serializers.ModelSerializer):
|
|||
'email',
|
||||
'newsletter'
|
||||
)
|
||||
extra_kwargs = {
|
||||
'username': {'write_only': True},
|
||||
'password': {'write_only': True},
|
||||
'email': {'write_only': True},
|
||||
'newsletter': {'write_only': True}
|
||||
}
|
||||
|
||||
def validate_username(self, value):
|
||||
"""Custom username validation"""
|
||||
|
|
|
|||
44
apps/collection/migrations/0015_auto_20191023_0715.py
Normal file
44
apps/collection/migrations/0015_auto_20191023_0715.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-23 07:15
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
import utils.models
|
||||
|
||||
|
||||
def fill_title_json_from_title(apps, schema_editor):
|
||||
# We can't import the Person model directly as it may be a newer
|
||||
# version than this migration expects. We use the historical version.
|
||||
Collection = apps.get_model('collection', 'Collection')
|
||||
for collection in Collection.objects.all():
|
||||
collection.name_json = {'en-GB': collection.name}
|
||||
collection.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('collection', '0014_auto_20191022_1242'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='collection',
|
||||
name='name_json',
|
||||
field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='name'),
|
||||
),
|
||||
migrations.RunPython(fill_title_json_from_title, migrations.RunPython.noop),
|
||||
migrations.RemoveField(
|
||||
model_name='collection',
|
||||
name='name',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='collection',
|
||||
old_name='name_json',
|
||||
new_name='name',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='collection',
|
||||
name='name',
|
||||
field=utils.models.TJSONField(help_text='{"en-GB":"some text"}', verbose_name='name'),
|
||||
),
|
||||
]
|
||||
19
apps/collection/migrations/0016_auto_20191024_1334.py
Normal file
19
apps/collection/migrations/0016_auto_20191024_1334.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-24 13:34
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('collection', '0015_auto_20191023_0715'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='guide',
|
||||
name='collection',
|
||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='collection.Collection', verbose_name='collection'),
|
||||
),
|
||||
]
|
||||
|
|
@ -43,9 +43,11 @@ class CollectionQuerySet(RelatedObjectsCountMixin):
|
|||
return self.filter(is_publish=True)
|
||||
|
||||
|
||||
class Collection(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin,
|
||||
class Collection(ProjectBaseMixin, CollectionDateMixin,
|
||||
TranslatedFieldsMixin, URLImageMixin):
|
||||
"""Collection model."""
|
||||
STR_FIELD_NAME = 'name'
|
||||
|
||||
ORDINARY = 0 # Ordinary collection
|
||||
POP = 1 # POP collection
|
||||
|
||||
|
|
@ -54,6 +56,8 @@ class Collection(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin,
|
|||
(POP, _('Pop')),
|
||||
)
|
||||
|
||||
name = TJSONField(verbose_name=_('name'),
|
||||
help_text='{"en-GB":"some text"}')
|
||||
collection_type = models.PositiveSmallIntegerField(choices=COLLECTION_TYPES,
|
||||
default=ORDINARY,
|
||||
verbose_name=_('Collection type'))
|
||||
|
|
@ -79,10 +83,6 @@ class Collection(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin,
|
|||
verbose_name = _('collection')
|
||||
verbose_name_plural = _('collections')
|
||||
|
||||
def __str__(self):
|
||||
"""String method."""
|
||||
return f'{self.name}'
|
||||
|
||||
|
||||
class GuideQuerySet(models.QuerySet):
|
||||
"""QuerySet for Guide."""
|
||||
|
|
@ -101,8 +101,9 @@ class Guide(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin):
|
|||
advertorials = JSONField(
|
||||
_('advertorials'), null=True, blank=True,
|
||||
default=None, help_text='{"key":"value"}')
|
||||
collection = models.ForeignKey(
|
||||
Collection, verbose_name=_('collection'), on_delete=models.CASCADE)
|
||||
collection = models.ForeignKey(Collection, on_delete=models.CASCADE,
|
||||
null=True, blank=True, default=None,
|
||||
verbose_name=_('collection'))
|
||||
|
||||
objects = GuideQuerySet.as_manager()
|
||||
|
||||
|
|
|
|||
|
|
@ -2,18 +2,19 @@ from rest_framework import serializers
|
|||
|
||||
from collection import models
|
||||
from location import models as location_models
|
||||
from utils.serializers import TranslatedField
|
||||
|
||||
|
||||
class CollectionBaseSerializer(serializers.ModelSerializer):
|
||||
"""Collection base serializer"""
|
||||
# RESPONSE
|
||||
description_translated = serializers.CharField(read_only=True, allow_null=True)
|
||||
name_translated = TranslatedField()
|
||||
description_translated = TranslatedField()
|
||||
|
||||
class Meta:
|
||||
model = models.Collection
|
||||
fields = [
|
||||
'id',
|
||||
'name',
|
||||
'name_translated',
|
||||
'description_translated',
|
||||
'image_url',
|
||||
'slug',
|
||||
|
|
@ -35,8 +36,7 @@ class CollectionSerializer(CollectionBaseSerializer):
|
|||
queryset=location_models.Country.objects.all(),
|
||||
write_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Collection
|
||||
class Meta(CollectionBaseSerializer.Meta):
|
||||
fields = CollectionBaseSerializer.Meta.fields + [
|
||||
'start',
|
||||
'end',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
from pytz import timezone as py_tz
|
||||
from timezonefinder import TimezoneFinder
|
||||
from establishment.models import Establishment
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Attach correct timestamps according to coordinates to existing establishments'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.tf = TimezoneFinder(in_memory=True)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
for establishment in Establishment.objects.prefetch_related('address').all():
|
||||
if establishment.address and establishment.address.latitude and establishment.address.longitude:
|
||||
establishment.tz = py_tz(self.tf.certain_timezone_at(lng=establishment.address.longitude,
|
||||
lat=establishment.address.latitude))
|
||||
establishment.save()
|
||||
self.stdout.write(self.style.SUCCESS(f'Attached timezone {establishment.tz} to {establishment}'))
|
||||
else:
|
||||
self.stdout.write(self.style.WARNING(f'Establishment {establishment} has no coordinates'
|
||||
f'passing...'))
|
||||
|
|
@ -8,7 +8,7 @@ def fill_establishment_subtype(apps, schema_editor):
|
|||
# version than this migration expects. We use the historical version.
|
||||
EstablishmentSubType = apps.get_model('establishment', 'EstablishmentSubType')
|
||||
for n, et in enumerate(EstablishmentSubType.objects.all()):
|
||||
et.index_name = f'Type {n}'
|
||||
et.index_name = 'Type %s' % n
|
||||
et.save()
|
||||
|
||||
|
||||
|
|
|
|||
18
apps/establishment/migrations/0041_auto_20191023_0920.py
Normal file
18
apps/establishment/migrations/0041_auto_20191023_0920.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-23 09:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('establishment', '0040_employee_tags'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='establishment',
|
||||
name='slug',
|
||||
field=models.SlugField(max_length=255, null=True, unique=True, verbose_name='Establishment slug'),
|
||||
),
|
||||
]
|
||||
21
apps/establishment/migrations/0042_establishment_tz.py
Normal file
21
apps/establishment/migrations/0042_establishment_tz.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-21 17:33
|
||||
|
||||
import timezone_field.fields
|
||||
from django.db import migrations
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
|
||||
dependencies = [
|
||||
('establishment', '0041_auto_20191023_0920'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='establishment',
|
||||
name='tz',
|
||||
field=timezone_field.fields.TimeZoneField(default=settings.TIME_ZONE),
|
||||
),
|
||||
]
|
||||
20
apps/establishment/migrations/0043_establishment_currency.py
Normal file
20
apps/establishment/migrations/0043_establishment_currency.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-24 13:13
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0022_auto_20191023_1113'),
|
||||
('establishment', '0042_establishment_tz'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='establishment',
|
||||
name='currency',
|
||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='main.Currency', verbose_name='currency'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
"""Establishment models."""
|
||||
from datetime import datetime
|
||||
from functools import reduce
|
||||
|
||||
import elasticsearch_dsl
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes import fields as generic
|
||||
from django.contrib.gis.db.models.functions import Distance
|
||||
|
|
@ -15,10 +17,11 @@ from phonenumber_field.modelfields import PhoneNumberField
|
|||
|
||||
from collection.models import Collection
|
||||
from location.models import Address
|
||||
from main.models import Award
|
||||
from main.models import Award, Currency
|
||||
from review.models import Review
|
||||
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
|
||||
TranslatedFieldsMixin, BaseAttributes)
|
||||
from timezone_field import TimeZoneField
|
||||
|
||||
|
||||
# todo: establishment type&subtypes check
|
||||
|
|
@ -128,15 +131,15 @@ class EstablishmentQuerySet(models.QuerySet):
|
|||
else:
|
||||
return self.none()
|
||||
|
||||
# def es_search(self, value, locale=None):
|
||||
# """Search text via ElasticSearch."""
|
||||
# from search_indexes.documents import EstablishmentDocument
|
||||
# search = EstablishmentDocument.search().filter(
|
||||
# Elastic_Q('match', name=value) |
|
||||
# Elastic_Q('match', **{f'description.{locale}': value})
|
||||
# ).execute()
|
||||
# ids = [result.meta.id for result in search]
|
||||
# return self.filter(id__in=ids)
|
||||
def es_search(self, value, locale=None):
|
||||
"""Search text via ElasticSearch."""
|
||||
from search_indexes.documents import EstablishmentDocument
|
||||
search = EstablishmentDocument.search().filter(
|
||||
elasticsearch_dsl.Q('match', name=value) |
|
||||
elasticsearch_dsl.Q('match', **{f'description.{locale}': value})
|
||||
).execute()
|
||||
ids = [result.meta.id for result in search]
|
||||
return self.filter(id__in=ids)
|
||||
|
||||
def by_country_code(self, code):
|
||||
"""Return establishments by country code"""
|
||||
|
|
@ -204,7 +207,7 @@ class EstablishmentQuerySet(models.QuerySet):
|
|||
.filter(image_url__isnull=False, public_mark__gte=10)
|
||||
.has_published_reviews()
|
||||
.annotate_distance(point=establishment.location)
|
||||
.order_by('distance')[:settings.LIMITING_QUERY_NUMBER]
|
||||
.order_by('distance')[:settings.LIMITING_QUERY_OBJECTS]
|
||||
.values('id')
|
||||
)
|
||||
return self.filter(id__in=subquery_filter_by_distance) \
|
||||
|
|
@ -224,7 +227,7 @@ class EstablishmentQuerySet(models.QuerySet):
|
|||
self.filter(image_url__isnull=False, public_mark__gte=10)
|
||||
.has_published_reviews()
|
||||
.annotate_distance(point=point)
|
||||
.order_by('distance')[:settings.LIMITING_QUERY_NUMBER]
|
||||
.order_by('distance')[:settings.LIMITING_QUERY_OBJECTS]
|
||||
.values('id')
|
||||
)
|
||||
return self.filter(id__in=subquery_filter_by_distance) \
|
||||
|
|
@ -349,8 +352,9 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
|
|||
verbose_name=_('Collections'))
|
||||
preview_image_url = models.URLField(verbose_name=_('Preview image URL path'),
|
||||
blank=True, null=True, default=None)
|
||||
slug = models.SlugField(unique=True, max_length=50, null=True,
|
||||
verbose_name=_('Establishment slug'), editable=True)
|
||||
slug = models.SlugField(unique=True, max_length=255, null=True,
|
||||
verbose_name=_('Establishment slug'))
|
||||
tz = TimeZoneField(default=settings.TIME_ZONE)
|
||||
|
||||
awards = generic.GenericRelation(to='main.Award', related_query_name='establishment')
|
||||
tags = models.ManyToManyField('tag.Tag', related_name='establishments',
|
||||
|
|
@ -358,6 +362,9 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
|
|||
reviews = generic.GenericRelation(to='review.Review')
|
||||
comments = generic.GenericRelation(to='comment.Comment')
|
||||
favorites = generic.GenericRelation(to='favorites.Favorites')
|
||||
currency = models.ForeignKey(Currency, blank=True, null=True, default=None,
|
||||
on_delete=models.PROTECT,
|
||||
verbose_name=_('currency'))
|
||||
|
||||
objects = EstablishmentQuerySet.as_manager()
|
||||
|
||||
|
|
@ -416,6 +423,32 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
|
|||
def best_price_carte(self):
|
||||
return 200
|
||||
|
||||
@property
|
||||
def works_noon(self):
|
||||
""" Used for indexing working by day """
|
||||
return [ret.weekday for ret in self.schedule.all() if ret.works_at_noon]
|
||||
|
||||
@property
|
||||
def works_evening(self):
|
||||
""" Used for indexing working by day """
|
||||
return [ret.weekday for ret in self.schedule.all() if ret.works_at_afternoon]
|
||||
|
||||
@property
|
||||
def works_now(self):
|
||||
""" Is establishment working now """
|
||||
now_at_est_tz = datetime.now(tz=self.tz)
|
||||
current_week = now_at_est_tz.weekday()
|
||||
schedule_for_today = self.schedule.filter(weekday=current_week).first()
|
||||
if schedule_for_today is None or schedule_for_today.closed_at is None or schedule_for_today.opening_at is None:
|
||||
return False
|
||||
time_at_est_tz = now_at_est_tz.time()
|
||||
return schedule_for_today.closed_at > time_at_est_tz > schedule_for_today.opening_at
|
||||
|
||||
@property
|
||||
def tags_indexing(self):
|
||||
return [{'id': tag.metadata.id,
|
||||
'label': tag.metadata.label} for tag in self.tags.all()]
|
||||
|
||||
@property
|
||||
def last_published_review(self):
|
||||
"""Return last published review"""
|
||||
|
|
@ -431,8 +464,8 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
|
|||
|
||||
@property
|
||||
def the_most_recent_award(self):
|
||||
return Award.objects.filter(Q(establishment=self) | Q(employees__establishments=self)).latest(
|
||||
field_name='vintage_year')
|
||||
return Award.objects.filter(Q(establishment=self) | Q(employees__establishments=self)) \
|
||||
.latest(field_name='vintage_year')
|
||||
|
||||
@property
|
||||
def country_id(self):
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from establishment.serializers import (
|
|||
EstablishmentTypeBaseSerializer)
|
||||
from main.models import Currency
|
||||
from utils.decorators import with_base_attributes
|
||||
from utils.serializers import TimeZoneChoiceField
|
||||
|
||||
|
||||
class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
|
||||
|
|
@ -19,8 +20,8 @@ class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
|
|||
phones = ContactPhonesSerializer(read_only=True, many=True, )
|
||||
emails = ContactEmailsSerializer(read_only=True, many=True, )
|
||||
socials = SocialNetworkRelatedSerializers(read_only=True, many=True, )
|
||||
slug = serializers.SlugField(required=True, allow_blank=False, max_length=50)
|
||||
type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True)
|
||||
tz = TimeZoneChoiceField()
|
||||
|
||||
class Meta:
|
||||
model = models.Establishment
|
||||
|
|
@ -41,6 +42,7 @@ class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
|
|||
'is_publish',
|
||||
'guestonline_id',
|
||||
'lastable_id',
|
||||
'tz',
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -159,10 +159,10 @@ class EstablishmentBaseSerializer(ProjectModelSerializer):
|
|||
"""Base serializer for Establishment model."""
|
||||
|
||||
preview_image = serializers.URLField(source='preview_image_url')
|
||||
slug = serializers.SlugField(allow_blank=False, required=True, max_length=50)
|
||||
address = AddressBaseSerializer()
|
||||
in_favorites = serializers.BooleanField(allow_null=True)
|
||||
tags = TagBaseSerializer(read_only=True, many=True)
|
||||
currency = CurrencySerializer()
|
||||
|
||||
class Meta:
|
||||
"""Meta class."""
|
||||
|
|
@ -180,6 +180,7 @@ class EstablishmentBaseSerializer(ProjectModelSerializer):
|
|||
'in_favorites',
|
||||
'address',
|
||||
'tags',
|
||||
'currency'
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from establishment.models import Establishment, EstablishmentType, Menu
|
|||
from translation.models import Language
|
||||
from account.models import Role, UserRole
|
||||
from location.models import Country, Address, City, Region
|
||||
from pytz import timezone as py_tz
|
||||
|
||||
|
||||
class BaseTestCase(APITestCase):
|
||||
|
|
@ -77,7 +78,8 @@ class EstablishmentBTests(BaseTestCase):
|
|||
'name': 'Test establishment',
|
||||
'type_id': self.establishment_type.id,
|
||||
'is_publish': True,
|
||||
'slug': 'test-establishment-slug'
|
||||
'slug': 'test-establishment-slug',
|
||||
'tz': py_tz('Europe/Moscow').zone
|
||||
}
|
||||
|
||||
response = self.client.post('/api/back/establishments/', data=data, format='json')
|
||||
|
|
|
|||
|
|
@ -19,9 +19,12 @@ class EstablishmentMixinView:
|
|||
|
||||
def get_queryset(self):
|
||||
"""Overridden method 'get_queryset'."""
|
||||
return models.Establishment.objects.published() \
|
||||
.with_base_related() \
|
||||
.annotate_in_favorites(self.request.user)
|
||||
qs = models.Establishment.objects.published() \
|
||||
.with_base_related() \
|
||||
.annotate_in_favorites(self.request.user)
|
||||
if self.request.country_code:
|
||||
qs = qs.by_country_code(self.request.country_code)
|
||||
return qs
|
||||
|
||||
|
||||
class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
|
||||
|
|
@ -30,13 +33,6 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
|
|||
filter_class = filters.EstablishmentFilter
|
||||
serializer_class = serializers.EstablishmentBaseSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""Overridden method 'get_queryset'."""
|
||||
qs = super(EstablishmentListView, self).get_queryset()
|
||||
if self.request.country_code:
|
||||
qs = qs.by_country_code(self.request.country_code)
|
||||
return qs
|
||||
|
||||
|
||||
class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView):
|
||||
"""Resource for getting a establishment."""
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
# Create your tests here.
|
||||
from http.cookies import SimpleCookie
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from rest_framework.test import APITestCase
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from account.models import User
|
||||
from establishment.models import Establishment, EstablishmentType
|
||||
from favorites.models import Favorites
|
||||
from establishment.models import Establishment, EstablishmentType, EstablishmentSubType
|
||||
from news.models import NewsType, News
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class BaseTestCase(APITestCase):
|
||||
|
|
@ -17,38 +19,57 @@ class BaseTestCase(APITestCase):
|
|||
self.username = 'sedragurda'
|
||||
self.password = 'sedragurdaredips19'
|
||||
self.email = 'sedragurda@desoz.com'
|
||||
self.user = User.objects.create_user(username=self.username, email=self.email, password=self.password)
|
||||
tokkens = User.create_jwt_tokens(self.user)
|
||||
self.client.cookies = SimpleCookie({'access_token': tokkens.get('access_token'),
|
||||
'refresh_token': tokkens.get('refresh_token')})
|
||||
self.user = User.objects.create_user(
|
||||
username=self.username,
|
||||
email=self.email,
|
||||
password=self.password
|
||||
)
|
||||
|
||||
self.test_news_type = NewsType.objects.create(name="Test news type")
|
||||
self.test_news = News.objects.create(created_by=self.user, modified_by=self.user, title={"en-GB": "Test news"},
|
||||
news_type=self.test_news_type,
|
||||
description={"en-GB": "Description test news"},
|
||||
playlist=1, start="2020-12-03 12:00:00", end="2020-12-13 12:00:00",
|
||||
state=News.PUBLISHED, slug='test-news')
|
||||
tokens = User.create_jwt_tokens(self.user)
|
||||
self.client.cookies = SimpleCookie(
|
||||
{'access_token': tokens.get('access_token'),
|
||||
'refresh_token': tokens.get('refresh_token')})
|
||||
|
||||
self.test_content_type = ContentType.objects.get(app_label="news", model="news")
|
||||
self.test_news_type = NewsType.objects.create(
|
||||
name="Test news type",
|
||||
)
|
||||
self.test_news = News.objects.create(
|
||||
created_by=self.user,
|
||||
modified_by=self.user,
|
||||
title={"en-GB": "Test news"},
|
||||
news_type=self.test_news_type,
|
||||
description={"en-GB": "Description test news"},
|
||||
start=datetime.fromisoformat("2020-12-03 12:00:00"),
|
||||
end=datetime.fromisoformat("2020-12-03 12:00:00"),
|
||||
state=News.PUBLISHED,
|
||||
slug='test-news'
|
||||
)
|
||||
|
||||
self.test_favorites = Favorites.objects.create(user=self.user, content_type=self.test_content_type,
|
||||
object_id=self.test_news.id)
|
||||
self.test_content_type = ContentType.objects.get(
|
||||
app_label="news", model="news")
|
||||
|
||||
self.test_establishment_type = EstablishmentType.objects.create(name={"en-GB": "test establishment type"},
|
||||
use_subtypes=False)
|
||||
self.test_favorites = Favorites.objects.create(
|
||||
user=self.user, content_type=self.test_content_type,
|
||||
object_id=self.test_news.id)
|
||||
|
||||
self.test_establishment = Establishment.objects.create(name="test establishment",
|
||||
description={"en-GB": "description of test establishment"},
|
||||
establishment_type=self.test_establishment_type,
|
||||
is_publish=True)
|
||||
self.test_establishment_type = EstablishmentType.objects.create(
|
||||
name={"en-GB": "test establishment type"},
|
||||
use_subtypes=False)
|
||||
|
||||
self.test_establishment = Establishment.objects.create(
|
||||
name="test establishment",
|
||||
description={"en-GB": "description of test establishment"},
|
||||
establishment_type=self.test_establishment_type,
|
||||
is_publish=True)
|
||||
# value for GenericRelation(reverse side) field must be iterable
|
||||
# value for GenericRelation(reverse side) field must be assigned through "set" method of field
|
||||
# value for GenericRelation(reverse side) field must be assigned through
|
||||
# "set" method of field
|
||||
self.test_establishment.favorites.set([self.test_favorites])
|
||||
|
||||
|
||||
class FavoritesTestCase(BaseTestCase):
|
||||
|
||||
def test_func(self):
|
||||
response = self.client.get("/api/web/favorites/establishments/")
|
||||
print(response.json())
|
||||
url = reverse('web:favorites:establishment-list')
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
|
@ -5,4 +5,9 @@ from gallery.models import Image
|
|||
|
||||
@admin.register(Image)
|
||||
class ImageModelAdmin(admin.ModelAdmin):
|
||||
"""Image model admin"""
|
||||
"""Image model admin."""
|
||||
list_display = ['id', 'title', 'orientation_display', 'image_tag', ]
|
||||
|
||||
def orientation_display(self, obj):
|
||||
"""Get image orientation name."""
|
||||
return obj.get_orientation_display() if obj.orientation else None
|
||||
|
|
|
|||
41
apps/gallery/migrations/0002_auto_20190930_0714.py
Normal file
41
apps/gallery/migrations/0002_auto_20190930_0714.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Generated by Django 2.2.4 on 2019-09-30 07:14
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import easy_thumbnails.fields
|
||||
import utils.methods
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('gallery', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='image',
|
||||
name='orientation',
|
||||
field=models.PositiveSmallIntegerField(blank=True, choices=[(0, 'Horizontal'), (1, 'Vertical')], default=None, null=True, verbose_name='image orientation'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='image',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='parent_image', to='gallery.Image', verbose_name='parent image'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='image',
|
||||
name='source',
|
||||
field=models.PositiveSmallIntegerField(choices=[(0, 'Mobile'), (1, 'Web'), (2, 'All')], default=0, verbose_name='Source'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='image',
|
||||
name='title',
|
||||
field=models.CharField(default='', max_length=255, verbose_name='title'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='image',
|
||||
name='image',
|
||||
field=easy_thumbnails.fields.ThumbnailerImageField(upload_to=utils.methods.image_path, verbose_name='image file'),
|
||||
),
|
||||
]
|
||||
24
apps/gallery/migrations/0003_auto_20191003_1228.py
Normal file
24
apps/gallery/migrations/0003_auto_20191003_1228.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-03 12:28
|
||||
|
||||
from django.db import migrations
|
||||
import sorl.thumbnail.fields
|
||||
import utils.methods
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('gallery', '0002_auto_20190930_0714'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='image',
|
||||
name='parent',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='image',
|
||||
name='image',
|
||||
field=sorl.thumbnail.fields.ImageField(upload_to=utils.methods.image_path, verbose_name='image file'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,15 +1,39 @@
|
|||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from easy_thumbnails.fields import ThumbnailerImageField
|
||||
from sorl.thumbnail import delete
|
||||
from sorl.thumbnail.fields import ImageField as SORLImageField
|
||||
|
||||
from utils.methods import image_path
|
||||
from utils.models import ProjectBaseMixin, ImageMixin
|
||||
from utils.models import ProjectBaseMixin, SORLImageMixin, PlatformMixin
|
||||
|
||||
|
||||
class Image(ProjectBaseMixin, ImageMixin):
|
||||
class ImageQuerySet(models.QuerySet):
|
||||
"""QuerySet for model Image."""
|
||||
|
||||
|
||||
class Image(ProjectBaseMixin, SORLImageMixin, PlatformMixin):
|
||||
"""Image model."""
|
||||
HORIZONTAL = 0
|
||||
VERTICAL = 1
|
||||
|
||||
ORIENTATIONS = (
|
||||
(HORIZONTAL, _('Horizontal')),
|
||||
(VERTICAL, _('Vertical')),
|
||||
)
|
||||
|
||||
image = SORLImageField(upload_to=image_path,
|
||||
verbose_name=_('image file'))
|
||||
orientation = models.PositiveSmallIntegerField(choices=ORIENTATIONS,
|
||||
blank=True, null=True, default=None,
|
||||
verbose_name=_('image orientation'))
|
||||
title = models.CharField(_('title'), max_length=255, default='')
|
||||
|
||||
<<<<<<< HEAD
|
||||
image = ThumbnailerImageField(upload_to=image_path,
|
||||
verbose_name=_('Image file'), max_length=255)
|
||||
=======
|
||||
objects = ImageQuerySet.as_manager()
|
||||
>>>>>>> develop
|
||||
|
||||
class Meta:
|
||||
"""Meta class."""
|
||||
|
|
@ -18,4 +42,22 @@ class Image(ProjectBaseMixin, ImageMixin):
|
|||
|
||||
def __str__(self):
|
||||
"""String representation"""
|
||||
return str(self.id)
|
||||
return f'{self.id}'
|
||||
|
||||
def delete_image(self, completely: bool = True):
|
||||
"""
|
||||
Deletes an instance and crops of instance from media storage.
|
||||
:param completely: if set to False then removed only crop neither original image.
|
||||
"""
|
||||
try:
|
||||
# Delete from remote storage
|
||||
delete(file_=self.image.file, delete_file=completely)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
finally:
|
||||
if completely:
|
||||
# Delete an instance of image
|
||||
super().delete()
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -12,13 +12,20 @@ class ImageSerializer(serializers.ModelSerializer):
|
|||
# RESPONSE
|
||||
url = serializers.ImageField(source='image',
|
||||
read_only=True)
|
||||
orientation_display = serializers.CharField(source='get_orientation_display',
|
||||
read_only=True)
|
||||
|
||||
class Meta:
|
||||
"""Meta class"""
|
||||
model = models.Image
|
||||
fields = (
|
||||
fields = [
|
||||
'id',
|
||||
'file',
|
||||
'url'
|
||||
)
|
||||
|
||||
'url',
|
||||
'orientation',
|
||||
'orientation_display',
|
||||
'title',
|
||||
]
|
||||
extra_kwargs = {
|
||||
'orientation': {'write_only': True}
|
||||
}
|
||||
|
|
|
|||
23
apps/gallery/tasks.py
Normal file
23
apps/gallery/tasks.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
"""Gallery app celery tasks."""
|
||||
import logging
|
||||
|
||||
from celery import shared_task
|
||||
|
||||
from . import models
|
||||
|
||||
logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@shared_task
|
||||
def delete_image(image_id: int, completely: bool = True):
|
||||
"""Delete an image from remote storage."""
|
||||
image_qs = models.Image.objects.filter(id=image_id)
|
||||
if image_qs.exists():
|
||||
try:
|
||||
image = image_qs.first()
|
||||
image.delete_image(completely=completely)
|
||||
except:
|
||||
logger.error(f'TASK_NAME: delete_image\n'
|
||||
f'DETAIL: Exception occurred while deleting an image '
|
||||
f'and related crops from remote storage: image_id - {image_id}')
|
||||
|
|
@ -6,5 +6,6 @@ from . import views
|
|||
app_name = 'gallery'
|
||||
|
||||
urlpatterns = [
|
||||
path('upload/', views.ImageUploadView.as_view(), name='upload-image')
|
||||
path('', views.ImageListCreateView.as_view(), name='list-create-image'),
|
||||
path('<int:pk>/', views.ImageRetrieveDestroyView.as_view(), name='retrieve-destroy-image'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,10 +1,30 @@
|
|||
from rest_framework import generics
|
||||
from django.conf import settings
|
||||
from django.db.transaction import on_commit
|
||||
from rest_framework import generics, status
|
||||
from rest_framework.response import Response
|
||||
|
||||
from . import models, serializers
|
||||
from . import tasks, models, serializers
|
||||
|
||||
|
||||
class ImageUploadView(generics.CreateAPIView):
|
||||
"""Upload image to gallery"""
|
||||
class ImageBaseView(generics.GenericAPIView):
|
||||
"""Base Image view."""
|
||||
model = models.Image
|
||||
queryset = models.Image.objects.all()
|
||||
serializer_class = serializers.ImageSerializer
|
||||
|
||||
|
||||
class ImageListCreateView(ImageBaseView, generics.ListCreateAPIView):
|
||||
"""List/Create Image view."""
|
||||
|
||||
|
||||
class ImageRetrieveDestroyView(ImageBaseView, generics.RetrieveDestroyAPIView):
|
||||
"""Destroy view for model Image"""
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
"""Override destroy view"""
|
||||
instance = self.get_object()
|
||||
if settings.USE_CELERY:
|
||||
on_commit(lambda: tasks.delete_image.delay(image_id=instance.id))
|
||||
else:
|
||||
on_commit(lambda: tasks.delete_image(image_id=instance.id))
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@ from django.db.models.signals import post_save
|
|||
from django.db.transaction import on_commit
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from utils.models import ProjectBaseMixin, SVGImageMixin, TranslatedFieldsMixin, TJSONField
|
||||
|
||||
from translation.models import Language
|
||||
from utils.models import ProjectBaseMixin, SVGImageMixin, TranslatedFieldsMixin, TJSONField
|
||||
|
||||
|
||||
class Country(TranslatedFieldsMixin, SVGImageMixin, ProjectBaseMixin):
|
||||
|
|
|
|||
14
apps/main/migrations/0020_merge_20191023_0750.py
Normal file
14
apps/main/migrations/0020_merge_20191023_0750.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-23 07:50
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0019_award_image_url'),
|
||||
('main', '0019_auto_20191022_1359'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
14
apps/main/migrations/0020_merge_20191025_0423.py
Normal file
14
apps/main/migrations/0020_merge_20191025_0423.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-25 04:23
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0019_auto_20191022_1359'),
|
||||
('main', '0019_award_image_url'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
18
apps/main/migrations/0021_auto_20191023_0924.py
Normal file
18
apps/main/migrations/0021_auto_20191023_0924.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-23 09:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0020_merge_20191023_0750'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='feature',
|
||||
name='slug',
|
||||
field=models.SlugField(max_length=255, unique=True),
|
||||
),
|
||||
]
|
||||
57
apps/main/migrations/0022_auto_20191023_1113.py
Normal file
57
apps/main/migrations/0022_auto_20191023_1113.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-23 11:13
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import utils.models
|
||||
|
||||
|
||||
def fill_currency_name(apps, schema_editor):
|
||||
# We can't import the Person model directly as it may be a newer
|
||||
# version than this migration expects. We use the historical version.
|
||||
Currency = apps.get_model('main', 'Currency')
|
||||
for currency in Currency.objects.all():
|
||||
currency.name_json = {'en-GB': currency.name}
|
||||
currency.save()
|
||||
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0021_auto_20191023_0924'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='currency',
|
||||
name='sign',
|
||||
field=models.CharField(default='?', max_length=1, verbose_name='sign'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='currency',
|
||||
name='slug',
|
||||
field=models.SlugField(default='?', max_length=255, unique=True),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitesettings',
|
||||
name='currency',
|
||||
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='main.Currency'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='currency',
|
||||
name='name_json',
|
||||
field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='name'),
|
||||
),
|
||||
migrations.RunPython(fill_currency_name, migrations.RunPython.noop),
|
||||
migrations.RemoveField(
|
||||
model_name='currency',
|
||||
name='name',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='currency',
|
||||
old_name='name_json',
|
||||
new_name='name',
|
||||
),
|
||||
]
|
||||
|
|
@ -106,6 +106,22 @@ from utils.querysets import ContentTypeQuerySetMixin
|
|||
#
|
||||
|
||||
|
||||
class Currency(TranslatedFieldsMixin, models.Model):
|
||||
"""Currency model."""
|
||||
name = TJSONField(
|
||||
_('name'), null=True, blank=True,
|
||||
default=None, help_text='{"en-GB":"some text"}')
|
||||
sign = models.CharField(_('sign'), max_length=1)
|
||||
slug = models.SlugField(max_length=255, unique=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('currency')
|
||||
verbose_name_plural = _('currencies')
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.name}'
|
||||
|
||||
|
||||
class SiteSettingsQuerySet(models.QuerySet):
|
||||
"""Extended queryset for SiteSettings model."""
|
||||
|
||||
|
|
@ -114,6 +130,7 @@ class SiteSettingsQuerySet(models.QuerySet):
|
|||
|
||||
|
||||
class SiteSettings(ProjectBaseMixin):
|
||||
|
||||
subdomain = models.CharField(max_length=255, db_index=True, unique=True,
|
||||
verbose_name=_('Subdomain'))
|
||||
country = models.OneToOneField(Country, on_delete=models.PROTECT,
|
||||
|
|
@ -135,6 +152,7 @@ class SiteSettings(ProjectBaseMixin):
|
|||
verbose_name=_('Config'))
|
||||
ad_config = models.TextField(blank=True, null=True, default=None,
|
||||
verbose_name=_('AD config'))
|
||||
currency = models.ForeignKey(Currency, on_delete=models.PROTECT, null=True, default=None)
|
||||
|
||||
objects = SiteSettingsQuerySet.as_manager()
|
||||
|
||||
|
|
@ -182,7 +200,7 @@ class Page(models.Model):
|
|||
class Feature(ProjectBaseMixin, PlatformMixin):
|
||||
"""Feature model."""
|
||||
|
||||
slug = models.CharField(max_length=255, unique=True)
|
||||
slug = models.SlugField(max_length=255, unique=True)
|
||||
priority = models.IntegerField(unique=True, null=True, default=None)
|
||||
route = models.ForeignKey(Page, on_delete=models.PROTECT, null=True, default=None)
|
||||
site_settings = models.ManyToManyField(SiteSettings, through='SiteFeature')
|
||||
|
|
@ -257,18 +275,6 @@ class AwardType(models.Model):
|
|||
return self.name
|
||||
|
||||
|
||||
class Currency(models.Model):
|
||||
"""Currency model."""
|
||||
name = models.CharField(_('name'), max_length=50)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('currency')
|
||||
verbose_name_plural = _('currencies')
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.name}'
|
||||
|
||||
|
||||
class CarouselQuerySet(models.QuerySet):
|
||||
"""Carousel QuerySet."""
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from rest_framework import serializers
|
|||
from advertisement.serializers.web import AdvertisementSerializer
|
||||
from location.serializers import CountrySerializer
|
||||
from main import models
|
||||
from utils.serializers import TranslatedField
|
||||
from utils.serializers import ProjectModelSerializer, TranslatedField
|
||||
|
||||
|
||||
class FeatureSerializer(serializers.ModelSerializer):
|
||||
|
|
@ -40,11 +40,26 @@ class SiteFeatureSerializer(serializers.ModelSerializer):
|
|||
)
|
||||
|
||||
|
||||
class CurrencySerializer(ProjectModelSerializer):
|
||||
"""Currency serializer."""
|
||||
|
||||
name_translated = TranslatedField()
|
||||
|
||||
class Meta:
|
||||
model = models.Currency
|
||||
fields = [
|
||||
'id',
|
||||
'name_translated',
|
||||
'sign'
|
||||
]
|
||||
|
||||
|
||||
class SiteSettingsSerializer(serializers.ModelSerializer):
|
||||
"""Site settings serializer."""
|
||||
|
||||
published_features = SiteFeatureSerializer(source='published_sitefeatures',
|
||||
many=True, allow_null=True)
|
||||
currency = CurrencySerializer()
|
||||
# todo: remove this
|
||||
country_code = serializers.CharField(source='subdomain', read_only=True)
|
||||
|
||||
|
|
@ -63,6 +78,7 @@ class SiteSettingsSerializer(serializers.ModelSerializer):
|
|||
'config',
|
||||
'ad_config',
|
||||
'published_features',
|
||||
'currency'
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -114,17 +130,6 @@ class AwardSerializer(AwardBaseSerializer):
|
|||
fields = AwardBaseSerializer.Meta.fields + ['award_type', ]
|
||||
|
||||
|
||||
class CurrencySerializer(serializers.ModelSerializer):
|
||||
"""Currency serializer"""
|
||||
|
||||
class Meta:
|
||||
model = models.Currency
|
||||
fields = [
|
||||
'id',
|
||||
'name'
|
||||
]
|
||||
|
||||
|
||||
class CarouselListSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for retrieving list of carousel items."""
|
||||
model_name = serializers.CharField()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
from django.contrib import admin
|
||||
from django.conf import settings
|
||||
|
||||
from news import models
|
||||
from .tasks import send_email_with_news
|
||||
|
||||
|
|
@ -12,9 +14,10 @@ class NewsTypeAdmin(admin.ModelAdmin):
|
|||
|
||||
def send_email_action(modeladmin, request, queryset):
|
||||
news_ids = list(queryset.values_list("id", flat=True))
|
||||
|
||||
send_email_with_news.delay(news_ids)
|
||||
|
||||
if settings.USE_CELERY:
|
||||
send_email_with_news.delay(news_ids)
|
||||
else:
|
||||
send_email_with_news(news_ids)
|
||||
|
||||
|
||||
send_email_action.short_description = "Send the selected news by email"
|
||||
|
|
@ -24,3 +27,8 @@ send_email_action.short_description = "Send the selected news by email"
|
|||
class NewsAdmin(admin.ModelAdmin):
|
||||
"""News admin."""
|
||||
actions = [send_email_action]
|
||||
|
||||
|
||||
@admin.register(models.NewsGallery)
|
||||
class NewsGalleryAdmin(admin.ModelAdmin):
|
||||
"""News gallery admin."""
|
||||
|
|
|
|||
26
apps/news/migrations/0015_newsgallery.py
Normal file
26
apps/news/migrations/0015_newsgallery.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Generated by Django 2.2.4 on 2019-09-30 08:57
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('gallery', '0002_auto_20190930_0714'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='NewsGallery',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('image', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='news_gallery', to='gallery.Image', verbose_name='gallery')),
|
||||
('news', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='news_gallery', to='news.News', verbose_name='news')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'news gallery',
|
||||
'verbose_name_plural': 'news galleries',
|
||||
},
|
||||
),
|
||||
]
|
||||
19
apps/news/migrations/0016_news_gallery.py
Normal file
19
apps/news/migrations/0016_news_gallery.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 2.2.4 on 2019-09-30 12:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('gallery', '0002_auto_20190930_0714'),
|
||||
('news', '0015_newsgallery'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='news',
|
||||
name='gallery',
|
||||
field=models.ManyToManyField(through='news.NewsGallery', to='gallery.Image'),
|
||||
),
|
||||
]
|
||||
14
apps/news/migrations/0020_merge_20190930_1251.py
Normal file
14
apps/news/migrations/0020_merge_20190930_1251.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Generated by Django 2.2.4 on 2019-09-30 12:51
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('news', '0019_news_author'),
|
||||
('news', '0016_news_gallery'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
14
apps/news/migrations/0021_merge_20191002_1300.py
Normal file
14
apps/news/migrations/0021_merge_20191002_1300.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-02 13:00
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('news', '0020_remove_news_author'),
|
||||
('news', '0020_merge_20190930_1251'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
14
apps/news/migrations/0022_merge_20191015_0912.py
Normal file
14
apps/news/migrations/0022_merge_20191015_0912.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-15 09:12
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('news', '0021_auto_20191009_1408'),
|
||||
('news', '0021_merge_20191002_1300'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
18
apps/news/migrations/0023_auto_20191023_0903.py
Normal file
18
apps/news/migrations/0023_auto_20191023_0903.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-23 09:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('news', '0022_auto_20191021_1306'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='news',
|
||||
name='slug',
|
||||
field=models.SlugField(max_length=255, unique=True, verbose_name='News slug'),
|
||||
),
|
||||
]
|
||||
14
apps/news/migrations/0023_merge_20191023_1000.py
Normal file
14
apps/news/migrations/0023_merge_20191023_1000.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-23 10:00
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('news', '0021_merge_20191002_1300'),
|
||||
('news', '0022_auto_20191021_1306'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
14
apps/news/migrations/0023_merge_20191025_0423.py
Normal file
14
apps/news/migrations/0023_merge_20191025_0423.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-25 04:23
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('news', '0021_auto_20191021_1120'),
|
||||
('news', '0022_auto_20191021_1306'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
18
apps/news/migrations/0024_newsgallery_is_main.py
Normal file
18
apps/news/migrations/0024_newsgallery_is_main.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-23 10:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('news', '0023_merge_20191023_1000'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='newsgallery',
|
||||
name='is_main',
|
||||
field=models.BooleanField(default=False, verbose_name='Is the main image'),
|
||||
),
|
||||
]
|
||||
15
apps/news/migrations/0025_merge_20191023_1317.py
Normal file
15
apps/news/migrations/0025_merge_20191023_1317.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-23 13:17
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('news', '0024_newsgallery_is_main'),
|
||||
('news', '0023_auto_20191023_0903'),
|
||||
('news', '0022_merge_20191015_0912'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
21
apps/news/migrations/0026_auto_20191024_0913.py
Normal file
21
apps/news/migrations/0026_auto_20191024_0913.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-24 09:13
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('news', '0025_merge_20191023_1317'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='news',
|
||||
name='image_url',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='news',
|
||||
name='preview_image_url',
|
||||
),
|
||||
]
|
||||
17
apps/news/migrations/0027_remove_news_playlist.py
Normal file
17
apps/news/migrations/0027_remove_news_playlist.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-24 09:37
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('news', '0026_auto_20191024_0913'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='news',
|
||||
name='playlist',
|
||||
),
|
||||
]
|
||||
28
apps/news/migrations/0028_auto_20191024_1649.py
Normal file
28
apps/news/migrations/0028_auto_20191024_1649.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-24 16:49
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('news', '0027_remove_news_playlist'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='newsgallery',
|
||||
name='image',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='news_gallery', to='gallery.Image', verbose_name='gallery'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='newsgallery',
|
||||
name='news',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='news_gallery', to='news.News', verbose_name='news'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='newsgallery',
|
||||
unique_together={('news', 'is_main')},
|
||||
),
|
||||
]
|
||||
|
|
@ -141,15 +141,10 @@ class News(BaseAttributes, TranslatedFieldsMixin):
|
|||
verbose_name=_('End'))
|
||||
slug = models.SlugField(unique=True, max_length=255,
|
||||
verbose_name=_('News slug'))
|
||||
playlist = models.IntegerField(_('playlist'))
|
||||
state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES,
|
||||
verbose_name=_('State'))
|
||||
is_highlighted = models.BooleanField(default=False,
|
||||
verbose_name=_('Is highlighted'))
|
||||
image_url = models.URLField(blank=True, null=True, default=None,
|
||||
verbose_name=_('Image URL path'))
|
||||
preview_image_url = models.URLField(blank=True, null=True, default=None,
|
||||
verbose_name=_('Preview image URL path'))
|
||||
template = models.PositiveIntegerField(choices=TEMPLATE_CHOICES, default=NEWSPAPER)
|
||||
address = models.ForeignKey('location.Address', blank=True, null=True,
|
||||
default=None, verbose_name=_('address'),
|
||||
|
|
@ -159,6 +154,7 @@ class News(BaseAttributes, TranslatedFieldsMixin):
|
|||
verbose_name=_('country'))
|
||||
tags = models.ManyToManyField('tag.Tag', related_name='news',
|
||||
verbose_name=_('Tags'))
|
||||
gallery = models.ManyToManyField('gallery.Image', through='news.NewsGallery')
|
||||
ratings = generic.GenericRelation(Rating)
|
||||
|
||||
agenda = models.ForeignKey('news.Agenda', blank=True, null=True,
|
||||
|
|
@ -195,3 +191,48 @@ class News(BaseAttributes, TranslatedFieldsMixin):
|
|||
@property
|
||||
def same_theme(self):
|
||||
return self.__class__.objects.same_theme(self)[:3]
|
||||
|
||||
@property
|
||||
def main_image(self):
|
||||
qs = self.news_gallery.main_image()
|
||||
if qs.exists():
|
||||
return qs.first().image
|
||||
|
||||
@property
|
||||
def image_url(self):
|
||||
return self.main_image.image.url if self.main_image else None
|
||||
|
||||
@property
|
||||
def preview_image_url(self):
|
||||
if self.main_image:
|
||||
return self.main_image.get_image_url(thumbnail_key='news_preview')
|
||||
|
||||
|
||||
class NewsGalleryQuerySet(models.QuerySet):
|
||||
"""QuerySet for model News"""
|
||||
|
||||
def main_image(self):
|
||||
"""Return objects with flag is_main is True"""
|
||||
return self.filter(is_main=True)
|
||||
|
||||
|
||||
class NewsGallery(models.Model):
|
||||
|
||||
news = models.ForeignKey(News, null=True,
|
||||
related_name='news_gallery',
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_('news'))
|
||||
image = models.ForeignKey('gallery.Image', null=True,
|
||||
related_name='news_gallery',
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_('gallery'))
|
||||
is_main = models.BooleanField(default=False,
|
||||
verbose_name=_('Is the main image'))
|
||||
|
||||
objects = NewsGalleryQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
"""NewsGallery meta class."""
|
||||
verbose_name = _('news gallery')
|
||||
verbose_name_plural = _('news galleries')
|
||||
unique_together = ('news', 'is_main')
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
"""News app common serializers."""
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from account.serializers.common import UserBaseSerializer
|
||||
from gallery.models import Image
|
||||
from location import models as location_models
|
||||
from location.serializers import CountrySimpleSerializer, AddressBaseSerializer
|
||||
from news import models
|
||||
|
|
@ -42,6 +45,77 @@ class NewsBannerSerializer(ProjectModelSerializer):
|
|||
)
|
||||
|
||||
|
||||
class CropImageSerializer(serializers.Serializer):
|
||||
"""Serializer for crop images for News object."""
|
||||
|
||||
preview_url = serializers.SerializerMethodField()
|
||||
promo_horizontal_web_url = serializers.SerializerMethodField()
|
||||
promo_horizontal_mobile_url = serializers.SerializerMethodField()
|
||||
tile_horizontal_web_url = serializers.SerializerMethodField()
|
||||
tile_horizontal_mobile_url = serializers.SerializerMethodField()
|
||||
tile_vertical_web_url = serializers.SerializerMethodField()
|
||||
highlight_vertical_web_url = serializers.SerializerMethodField()
|
||||
editor_web_url = serializers.SerializerMethodField()
|
||||
editor_mobile_url = serializers.SerializerMethodField()
|
||||
|
||||
def get_preview_url(self, obj):
|
||||
"""Get crop preview."""
|
||||
return obj.instance.get_image_url('news_preview')
|
||||
|
||||
def get_promo_horizontal_web_url(self, obj):
|
||||
"""Get crop promo_horizontal_web."""
|
||||
return obj.instance.get_image_url('news_promo_horizontal_web')
|
||||
|
||||
def get_promo_horizontal_mobile_url(self, obj):
|
||||
"""Get crop promo_horizontal_mobile."""
|
||||
return obj.instance.get_image_url('news_promo_horizontal_mobile')
|
||||
|
||||
def get_tile_horizontal_web_url(self, obj):
|
||||
"""Get crop tile_horizontal_web."""
|
||||
return obj.instance.get_image_url('news_tile_horizontal_web')
|
||||
|
||||
def get_tile_horizontal_mobile_url(self, obj):
|
||||
"""Get crop tile_horizontal_mobile."""
|
||||
return obj.instance.get_image_url('news_tile_horizontal_mobile')
|
||||
|
||||
def get_tile_vertical_web_url(self, obj):
|
||||
"""Get crop tile_vertical_web."""
|
||||
return obj.instance.get_image_url('news_tile_vertical_web')
|
||||
|
||||
def get_highlight_vertical_web_url(self, obj):
|
||||
"""Get crop highlight_vertical_web."""
|
||||
return obj.instance.get_image_url('news_highlight_vertical_web')
|
||||
|
||||
def get_editor_web_url(self, obj):
|
||||
"""Get crop editor_web."""
|
||||
return obj.instance.get_image_url('news_editor_web')
|
||||
|
||||
def get_editor_mobile_url(self, obj):
|
||||
"""Get crop editor_mobile."""
|
||||
return obj.instance.get_image_url('news_editor_mobile')
|
||||
|
||||
|
||||
class NewsImageSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for returning crop images of news image."""
|
||||
orientation_display = serializers.CharField(source='get_orientation_display',
|
||||
read_only=True)
|
||||
original_url = serializers.URLField(source='image.url')
|
||||
auto_crop_images = CropImageSerializer(source='image', allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Image
|
||||
fields = [
|
||||
'id',
|
||||
'title',
|
||||
'orientation_display',
|
||||
'original_url',
|
||||
'auto_crop_images',
|
||||
]
|
||||
extra_kwargs = {
|
||||
'orientation': {'write_only': True}
|
||||
}
|
||||
|
||||
|
||||
class NewsTypeSerializer(serializers.ModelSerializer):
|
||||
"""News type serializer."""
|
||||
|
||||
|
|
@ -55,11 +129,8 @@ class NewsTypeSerializer(serializers.ModelSerializer):
|
|||
class NewsBaseSerializer(ProjectModelSerializer):
|
||||
"""Base serializer for News model."""
|
||||
|
||||
# read only fields
|
||||
title_translated = TranslatedField(source='title')
|
||||
title_translated = TranslatedField()
|
||||
subtitle_translated = TranslatedField()
|
||||
|
||||
# related fields
|
||||
news_type = NewsTypeSerializer(read_only=True)
|
||||
tags = TagBaseSerializer(read_only=True, many=True)
|
||||
|
||||
|
|
@ -72,14 +143,25 @@ class NewsBaseSerializer(ProjectModelSerializer):
|
|||
'title_translated',
|
||||
'subtitle_translated',
|
||||
'is_highlighted',
|
||||
'image_url',
|
||||
'preview_image_url',
|
||||
'news_type',
|
||||
'tags',
|
||||
'slug',
|
||||
)
|
||||
|
||||
|
||||
class NewsListSerializer(NewsBaseSerializer):
|
||||
"""List serializer for News model."""
|
||||
|
||||
image = NewsImageSerializer(source='main_image', allow_null=True)
|
||||
|
||||
class Meta(NewsBaseSerializer.Meta):
|
||||
"""Meta class."""
|
||||
|
||||
fields = NewsBaseSerializer.Meta.fields + (
|
||||
'image',
|
||||
)
|
||||
|
||||
|
||||
class NewsDetailSerializer(NewsBaseSerializer):
|
||||
"""News detail serializer."""
|
||||
|
||||
|
|
@ -88,6 +170,7 @@ class NewsDetailSerializer(NewsBaseSerializer):
|
|||
author = UserBaseSerializer(source='created_by', read_only=True)
|
||||
state_display = serializers.CharField(source='get_state_display',
|
||||
read_only=True)
|
||||
gallery = NewsImageSerializer(read_only=True, many=True)
|
||||
|
||||
class Meta(NewsBaseSerializer.Meta):
|
||||
"""Meta class."""
|
||||
|
|
@ -96,12 +179,12 @@ class NewsDetailSerializer(NewsBaseSerializer):
|
|||
'description_translated',
|
||||
'start',
|
||||
'end',
|
||||
'playlist',
|
||||
'is_publish',
|
||||
'state',
|
||||
'state_display',
|
||||
'author',
|
||||
'country',
|
||||
'gallery',
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -160,3 +243,48 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
|
|||
'template',
|
||||
'template_display',
|
||||
)
|
||||
|
||||
|
||||
class NewsBackOfficeGallerySerializer(serializers.ModelSerializer):
|
||||
"""Serializer class for model NewsGallery."""
|
||||
|
||||
class Meta:
|
||||
"""Meta class"""
|
||||
|
||||
model = models.NewsGallery
|
||||
fields = [
|
||||
'id',
|
||||
'is_main',
|
||||
]
|
||||
|
||||
def get_request_kwargs(self):
|
||||
"""Get url kwargs from request."""
|
||||
return self.context.get('request').parser_context.get('kwargs')
|
||||
|
||||
def validate(self, attrs):
|
||||
"""Override validate method."""
|
||||
news_pk = self.get_request_kwargs().get('pk')
|
||||
image_id = self.get_request_kwargs().get('image_id')
|
||||
is_main = attrs.get('is_main')
|
||||
|
||||
news_qs = models.News.objects.filter(pk=news_pk)
|
||||
image_qs = Image.objects.filter(id=image_id)
|
||||
|
||||
if not news_qs.exists():
|
||||
raise serializers.ValidationError({'detail': _('News not found')})
|
||||
if not image_qs.exists():
|
||||
raise serializers.ValidationError({'detail': _('Image not found')})
|
||||
|
||||
news = news_qs.first()
|
||||
image = image_qs.first()
|
||||
|
||||
if news.news_gallery.filter(image=image).exists():
|
||||
raise serializers.ValidationError({'detail': _('Image is already added')})
|
||||
|
||||
if is_main and news.news_gallery.main_image().exists():
|
||||
raise serializers.ValidationError({'detail': _('Main image is already added')})
|
||||
|
||||
attrs['news'] = news
|
||||
attrs['image'] = image
|
||||
|
||||
return attrs
|
||||
|
|
|
|||
|
|
@ -18,11 +18,14 @@ class BaseTestCase(APITestCase):
|
|||
self.username = 'sedragurda'
|
||||
self.password = 'sedragurdaredips19'
|
||||
self.email = 'sedragurda@desoz.com'
|
||||
self.user = User.objects.create_user(username=self.username, email=self.email, password=self.password)
|
||||
#get tokkens
|
||||
tokkens = User.create_jwt_tokens(self.user)
|
||||
self.client.cookies = SimpleCookie({'access_token': tokkens.get('access_token'),
|
||||
'refresh_token': tokkens.get('refresh_token')})
|
||||
self.user = User.objects.create_user(
|
||||
username=self.username, email=self.email, password=self.password)
|
||||
|
||||
# get tokens
|
||||
tokens = User.create_jwt_tokens(self.user)
|
||||
self.client.cookies = SimpleCookie(
|
||||
{'access_token': tokens.get('access_token'),
|
||||
'refresh_token': tokens.get('refresh_token')})
|
||||
self.test_news_type = NewsType.objects.create(name="Test news type")
|
||||
|
||||
self.lang = Language.objects.get(
|
||||
|
|
@ -46,21 +49,25 @@ class BaseTestCase(APITestCase):
|
|||
)
|
||||
user_role.save()
|
||||
|
||||
self.test_news = News.objects.create(created_by=self.user, modified_by=self.user,
|
||||
title={"en-GB": "Test news"},
|
||||
news_type=self.test_news_type,
|
||||
description={"en-GB": "Description test news"},
|
||||
playlist=1, start=datetime.now() + timedelta(hours=-2),
|
||||
end=datetime.now() + timedelta(hours=2),
|
||||
state=News.PUBLISHED, slug='test-news-slug',
|
||||
country=self.country_ru)
|
||||
self.test_news = News.objects.create(
|
||||
created_by=self.user, modified_by=self.user,
|
||||
title={"en-GB": "Test news"},
|
||||
news_type=self.test_news_type,
|
||||
description={"en-GB": "Description test news"},
|
||||
start=datetime.now() + timedelta(hours=-2),
|
||||
end=datetime.now() + timedelta(hours=2),
|
||||
state=News.PUBLISHED,
|
||||
slug='test-news-slug',
|
||||
country=self.country_ru,
|
||||
)
|
||||
|
||||
|
||||
class NewsTestCase(BaseTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
def test_web_news(self):
|
||||
response = self.client.get("/api/web/news/")
|
||||
response = self.client.get(reverse('web:news:list'))
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
response = self.client.get(f"/api/web/news/slug/{self.test_news.slug}/")
|
||||
|
|
@ -85,7 +92,6 @@ class NewsTestCase(BaseTestCase):
|
|||
'description': {"en-GB": "Description test news!"},
|
||||
'slug': self.test_news.slug,
|
||||
'start': self.test_news.start,
|
||||
'playlist': self.test_news.playlist,
|
||||
'news_type_id':self.test_news.news_type_id,
|
||||
'country_id': self.country_ru.id
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"""News app urlpatterns for backoffice"""
|
||||
from django.urls import path
|
||||
|
||||
from news import views
|
||||
|
||||
app_name = 'news'
|
||||
|
|
@ -8,4 +9,8 @@ urlpatterns = [
|
|||
path('', views.NewsBackOfficeLCView.as_view(), name='list-create'),
|
||||
path('<int:pk>/', views.NewsBackOfficeRUDView.as_view(),
|
||||
name='retrieve-update-destroy'),
|
||||
path('<int:pk>/gallery/', views.NewsBackOfficeGalleryListView.as_view(),
|
||||
name='gallery-list'),
|
||||
path('<int:pk>/gallery/<int:image_id>/', views.NewsBackOfficeGalleryCreateDestroyView.as_view(),
|
||||
name='gallery-create-destroy'),
|
||||
]
|
||||
|
|
@ -1,6 +1,11 @@
|
|||
"""News app views."""
|
||||
from django.conf import settings
|
||||
from django.db.transaction import on_commit
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import generics, permissions
|
||||
from rest_framework import generics, permissions, status
|
||||
from rest_framework.response import Response
|
||||
|
||||
from gallery.tasks import delete_image
|
||||
from news import filters, models, serializers
|
||||
from rating.tasks import add_rating
|
||||
from utils.permissions import IsCountryAdmin, IsContentPageManager
|
||||
|
|
@ -9,21 +14,40 @@ from utils.permissions import IsCountryAdmin, IsContentPageManager
|
|||
class NewsMixinView:
|
||||
"""News mixin."""
|
||||
|
||||
permission_classes = (permissions.AllowAny, )
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
serializer_class = serializers.NewsBaseSerializer
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
from django.conf import settings
|
||||
"""Override get_queryset method."""
|
||||
qs = models.News.objects.published().with_base_related()\
|
||||
|
||||
qs = models.News.objects.published().with_base_related() \
|
||||
.order_by('-is_highlighted', '-created')
|
||||
if self.request.country_code:
|
||||
qs = qs.by_country_code(self.request.country_code)
|
||||
country_code = self.request.country_code
|
||||
if country_code:
|
||||
|
||||
# temp code
|
||||
# Temporary stub for international news logic
|
||||
# (по договорённости с заказчиком на демонстрации 4 ноября
|
||||
# здесь будет 6 фиксированных новостей)
|
||||
# TODO replace this stub with actual logic
|
||||
if hasattr(settings, 'HARDCODED_INTERNATIONAL_NEWS_IDS'):
|
||||
if country_code and country_code != 'www' and country_code != 'main':
|
||||
qs = qs.by_country_code(country_code)
|
||||
else:
|
||||
qs = models.News.objects.filter(
|
||||
id__in=settings.HARDCODED_INTERNATIONAL_NEWS_IDS)
|
||||
return qs
|
||||
# temp code
|
||||
|
||||
qs = qs.by_country_code(country_code)
|
||||
return qs
|
||||
|
||||
|
||||
class NewsListView(NewsMixinView, generics.ListAPIView):
|
||||
"""News list view."""
|
||||
|
||||
serializer_class = serializers.NewsListSerializer
|
||||
filter_class = filters.NewsListFilterSet
|
||||
|
||||
|
||||
|
|
@ -74,6 +98,64 @@ class NewsBackOfficeLCView(NewsBackOfficeMixinView,
|
|||
return super().get_queryset().with_extended_related()
|
||||
|
||||
|
||||
class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView,
|
||||
generics.CreateAPIView,
|
||||
generics.DestroyAPIView):
|
||||
"""Resource for a create gallery for news for back-office users."""
|
||||
serializer_class = serializers.NewsBackOfficeGallerySerializer
|
||||
|
||||
def get_object(self):
|
||||
"""
|
||||
Returns the object the view is displaying.
|
||||
"""
|
||||
news_qs = self.filter_queryset(self.get_queryset())
|
||||
|
||||
news = get_object_or_404(news_qs, pk=self.kwargs['pk'])
|
||||
gallery = get_object_or_404(news.news_gallery, image_id=self.kwargs['image_id'])
|
||||
|
||||
# May raise a permission denied
|
||||
self.check_object_permissions(self.request, gallery)
|
||||
|
||||
return gallery
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""Override create method"""
|
||||
super().create(request, *args, **kwargs)
|
||||
return Response(status=status.HTTP_201_CREATED)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
"""Override destroy method."""
|
||||
gallery_obj = self.get_object()
|
||||
if settings.USE_CELERY:
|
||||
on_commit(lambda: delete_image.delay(image_id=gallery_obj.image.id,
|
||||
completely=False))
|
||||
else:
|
||||
on_commit(lambda: delete_image(image_id=gallery_obj.image.id,
|
||||
completely=False))
|
||||
# Delete an instances of NewsGallery model
|
||||
gallery_obj.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class NewsBackOfficeGalleryListView(NewsBackOfficeMixinView, generics.ListAPIView):
|
||||
"""Resource for returning gallery for news for back-office users."""
|
||||
serializer_class = serializers.NewsImageSerializer
|
||||
|
||||
def get_object(self):
|
||||
"""Override get_object method."""
|
||||
qs = super(NewsBackOfficeGalleryListView, self).get_queryset()
|
||||
news = get_object_or_404(qs, pk=self.kwargs['pk'])
|
||||
|
||||
# May raise a permission denied
|
||||
self.check_object_permissions(self.request, news)
|
||||
|
||||
return news
|
||||
|
||||
def get_queryset(self):
|
||||
"""Override get_queryset method."""
|
||||
return self.get_object().gallery.all()
|
||||
|
||||
|
||||
class NewsBackOfficeRUDView(NewsBackOfficeMixinView,
|
||||
generics.RetrieveUpdateDestroyAPIView):
|
||||
"""Resource for detailed information about news for back-office users."""
|
||||
|
|
|
|||
7
apps/product/apps.py
Normal file
7
apps/product/apps.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class ProductConfig(AppConfig):
|
||||
name = 'product'
|
||||
verbose_name = _('Product')
|
||||
110
apps/product/models.py
Normal file
110
apps/product/models.py
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
"""Product app models."""
|
||||
from django.db import models
|
||||
from django.contrib.postgres.fields import JSONField
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from utils.models import (BaseAttributes, ProjectBaseMixin,
|
||||
TranslatedFieldsMixin, TJSONField)
|
||||
|
||||
|
||||
class ProductType(TranslatedFieldsMixin, ProjectBaseMixin):
|
||||
"""ProductType model."""
|
||||
|
||||
name = TJSONField(blank=True, null=True, default=None,
|
||||
verbose_name=_('Name'), help_text='{"en-GB":"some text"}')
|
||||
index_name = models.CharField(max_length=50, unique=True, db_index=True,
|
||||
verbose_name=_('Index name'))
|
||||
use_subtypes = models.BooleanField(_('Use subtypes'), default=True)
|
||||
|
||||
class Meta:
|
||||
"""Meta class."""
|
||||
|
||||
verbose_name = _('Product type')
|
||||
verbose_name_plural = _('Product types')
|
||||
|
||||
|
||||
class ProductSubType(TranslatedFieldsMixin, ProjectBaseMixin):
|
||||
"""ProductSubtype model."""
|
||||
|
||||
product_type = models.ForeignKey(ProductType, on_delete=models.CASCADE,
|
||||
related_name='subtypes',
|
||||
verbose_name=_('Product type'))
|
||||
name = TJSONField(blank=True, null=True, default=None,
|
||||
verbose_name=_('Name'), help_text='{"en-GB":"some text"}')
|
||||
index_name = models.CharField(max_length=50, unique=True, db_index=True,
|
||||
verbose_name=_('Index name'))
|
||||
|
||||
class Meta:
|
||||
"""Meta class."""
|
||||
|
||||
verbose_name = _('Product type')
|
||||
verbose_name_plural = _('Product types')
|
||||
|
||||
|
||||
class ProductManager(models.Manager):
|
||||
"""Extended manager for Product model."""
|
||||
|
||||
|
||||
class ProductQuerySet(models.QuerySet):
|
||||
"""Product queryset."""
|
||||
|
||||
def common(self):
|
||||
return self.filter(category=self.model.COMMON)
|
||||
|
||||
def online(self):
|
||||
return self.filter(category=self.model.ONLINE)
|
||||
|
||||
|
||||
class Product(TranslatedFieldsMixin, BaseAttributes):
|
||||
"""Product models."""
|
||||
|
||||
COMMON = 0
|
||||
ONLINE = 1
|
||||
|
||||
CATEGORY_CHOICES = (
|
||||
(COMMON, _('Common')),
|
||||
(ONLINE, _('Online')),
|
||||
)
|
||||
|
||||
category = models.PositiveIntegerField(choices=CATEGORY_CHOICES,
|
||||
default=COMMON)
|
||||
name = TJSONField(_('Name'), null=True, blank=True, default=None,
|
||||
help_text='{"en-GB":"some text"}')
|
||||
description = TJSONField(_('Description'), null=True, blank=True,
|
||||
default=None, help_text='{"en-GB":"some text"}')
|
||||
characteristics = JSONField(_('Characteristics'))
|
||||
country = models.ForeignKey('location.Country', on_delete=models.PROTECT,
|
||||
verbose_name=_('Country'))
|
||||
available = models.BooleanField(_('Available'), default=True)
|
||||
type = models.ForeignKey(ProductType, on_delete=models.PROTECT,
|
||||
related_name='products', verbose_name=_('Type'))
|
||||
subtypes = models.ManyToManyField(ProductSubType, related_name='products',
|
||||
verbose_name=_('Subtypes'))
|
||||
|
||||
objects = ProductManager.from_queryset(ProductQuerySet)()
|
||||
|
||||
class Meta:
|
||||
"""Meta class."""
|
||||
|
||||
verbose_name = _('Product')
|
||||
verbose_name_plural = _('Products')
|
||||
|
||||
|
||||
class OnlineProductManager(ProductManager):
|
||||
"""Extended manger for OnlineProduct model."""
|
||||
|
||||
def get_queryset(self):
|
||||
"""Overrided get_queryset method."""
|
||||
return super().get_queryset().online()
|
||||
|
||||
|
||||
class OnlineProduct(Product):
|
||||
"""Online product."""
|
||||
|
||||
objects = OnlineProductManager.from_queryset(ProductQuerySet)()
|
||||
|
||||
class Meta:
|
||||
"""Meta class."""
|
||||
|
||||
proxy = True
|
||||
verbose_name = _('Online product')
|
||||
verbose_name_plural = _('Online products')
|
||||
0
apps/product/views/mobile.py
Normal file
0
apps/product/views/mobile.py
Normal file
0
apps/product/views/web.py
Normal file
0
apps/product/views/web.py
Normal file
|
|
@ -1,3 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class ProductsConfig(AppConfig):
|
||||
"""Products model."""
|
||||
name = 'products'
|
||||
verbose_name = _('products')
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
# from django.contrib.postgres.fields import JSONField
|
||||
# from django.db import models
|
||||
# from django.utils.translation import gettext_lazy as _
|
||||
#
|
||||
# from utils.models import BaseAttributes
|
||||
#
|
||||
#
|
||||
# class ProductManager(models.Manager):
|
||||
# """Product manager."""
|
||||
#
|
||||
#
|
||||
# class ProductQuerySet(models.QuerySet):
|
||||
# """Product queryset."""
|
||||
#
|
||||
#
|
||||
# class Product(BaseAttributes):
|
||||
# """Product models."""
|
||||
# name = models.CharField(_('name'), max_length=255)
|
||||
# country = models.ForeignKey('location.Country', on_delete=models.CASCADE)
|
||||
# region = models.ForeignKey('location.Region', on_delete=models.CASCADE)
|
||||
# # ASK: What is the "subregion"
|
||||
#
|
||||
# description = JSONField(_('description'))
|
||||
# characteristics = JSONField(_('characteristics'))
|
||||
# metadata_values = JSONField(_('metadata_values'))
|
||||
# # common_relations_id
|
||||
# # product_region_id
|
||||
# code = models.CharField(_('code'), max_length=255)
|
||||
# available = models.BooleanField(_('available'))
|
||||
#
|
||||
# # dealer_type
|
||||
# # target_scope
|
||||
# # target_type
|
||||
# # rank
|
||||
# # excluding_tax_unit_price
|
||||
# # column_21
|
||||
# # currencies_id
|
||||
# # vintage
|
||||
# # producer_price
|
||||
# # producer_description
|
||||
# # annual_produced_quantity
|
||||
# # production_method_description
|
||||
# # unit_name
|
||||
# # unit
|
||||
# # unit_values
|
||||
# # organic_source
|
||||
# # certificates
|
||||
# # establishments_id
|
||||
# # restrictions
|
||||
# #
|
||||
# objects = ProductManager.from_queryset(ProductQuerySet)()
|
||||
#
|
||||
# class Meta:
|
||||
# verbose_name = _('product')
|
||||
# verbose_name_plural = _('products')
|
||||
#
|
||||
#
|
||||
# class ProductType(models.Model):
|
||||
# """ProductType model."""
|
||||
#
|
||||
# class Meta:
|
||||
# verbose_name_plural = _('product types')
|
||||
# verbose_name = _('product type')
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
@ -1 +0,0 @@
|
|||
# Create your views here.
|
||||
|
|
@ -32,6 +32,13 @@ class EstablishmentDocument(Document):
|
|||
}),
|
||||
},
|
||||
multi=True)
|
||||
works_evening = fields.ListField(fields.IntegerField(
|
||||
attr='works_evening'
|
||||
))
|
||||
works_noon = fields.ListField(fields.IntegerField(
|
||||
attr='works_noon'
|
||||
))
|
||||
works_now = fields.BooleanField(attr='works_now')
|
||||
tags = fields.ObjectField(
|
||||
properties={
|
||||
'id': fields.IntegerField(attr='id'),
|
||||
|
|
@ -39,6 +46,14 @@ class EstablishmentDocument(Document):
|
|||
properties=OBJECT_FIELD_PROPERTIES),
|
||||
},
|
||||
multi=True)
|
||||
schedule = fields.ListField(fields.ObjectField(
|
||||
properties={
|
||||
'id': fields.IntegerField(attr='id'),
|
||||
'weekday': fields.IntegerField(attr='weekday'),
|
||||
'weekday_display': fields.KeywordField(attr='get_weekday_display'),
|
||||
'closed_at': fields.KeywordField(attr='closed_at_str'),
|
||||
}
|
||||
))
|
||||
address = fields.ObjectField(
|
||||
properties={
|
||||
'id': fields.IntegerField(),
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ class NewsDocument(Document):
|
|||
country = fields.ObjectField(properties={'id': fields.IntegerField(),
|
||||
'code': fields.KeywordField()})
|
||||
web_url = fields.KeywordField(attr='web_url')
|
||||
image_url = fields.KeywordField(attr='image_url')
|
||||
preview_image_url = fields.KeywordField(attr='preview_image_url')
|
||||
tags = fields.ObjectField(
|
||||
properties={
|
||||
'id': fields.IntegerField(attr='id'),
|
||||
|
|
@ -37,14 +39,11 @@ class NewsDocument(Document):
|
|||
model = models.News
|
||||
fields = (
|
||||
'id',
|
||||
'playlist',
|
||||
'start',
|
||||
'end',
|
||||
'slug',
|
||||
'state',
|
||||
'is_highlighted',
|
||||
'image_url',
|
||||
'preview_image_url',
|
||||
'template',
|
||||
)
|
||||
related_models = [models.NewsType]
|
||||
|
|
|
|||
|
|
@ -30,6 +30,15 @@ class AddressDocumentSerializer(serializers.Serializer):
|
|||
geo_lat = serializers.FloatField(allow_null=True, source='coordinates.lat')
|
||||
|
||||
|
||||
class ScheduleDocumentSerializer(serializers.Serializer):
|
||||
"""Schedule serializer for ES Document"""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
weekday = serializers.IntegerField()
|
||||
weekday_display = serializers.CharField()
|
||||
closed_at = serializers.CharField()
|
||||
|
||||
|
||||
class NewsDocumentSerializer(DocumentSerializer):
|
||||
"""News document serializer."""
|
||||
|
||||
|
|
@ -68,6 +77,7 @@ class EstablishmentDocumentSerializer(DocumentSerializer):
|
|||
|
||||
address = AddressDocumentSerializer()
|
||||
tags = TagsDocumentSerializer(many=True)
|
||||
schedule = ScheduleDocumentSerializer(many=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
"""Meta class."""
|
||||
|
|
@ -84,32 +94,11 @@ class EstablishmentDocumentSerializer(DocumentSerializer):
|
|||
'preview_image',
|
||||
'address',
|
||||
'tags',
|
||||
'schedule',
|
||||
'works_noon',
|
||||
'works_evening',
|
||||
'works_now',
|
||||
# 'collections',
|
||||
# 'establishment_type',
|
||||
# 'establishment_subtypes',
|
||||
)
|
||||
|
||||
|
||||
# def to_representation(self, instance):
|
||||
# ret = super().to_representation(instance)
|
||||
# dict_merge = lambda a, b: a.update(b) or a
|
||||
#
|
||||
# ret['tags'] = map(lambda tag: dict_merge(tag, {'label_translated': get_translated_value(tag.pop('label'))}),
|
||||
# ret['tags'])
|
||||
# ret['establishment_subtypes'] = map(
|
||||
# lambda subtype: dict_merge(subtype, {'name_translated': get_translated_value(subtype.pop('name'))}),
|
||||
# ret['establishment_subtypes'])
|
||||
# if ret.get('establishment_type'):
|
||||
# ret['establishment_type']['name_translated'] = get_translated_value(ret['establishment_type'].pop('name'))
|
||||
# if ret.get('address'):
|
||||
# ret['address']['city']['country']['name_translated'] = get_translated_value(
|
||||
# ret['address']['city']['country'].pop('name'))
|
||||
# location = ret['address'].pop('location')
|
||||
# if location:
|
||||
# ret['address']['geo_lon'] = location['lon']
|
||||
# ret['address']['geo_lat'] = location['lat']
|
||||
#
|
||||
# ret['type'] = ret.pop('establishment_type')
|
||||
# ret['subtypes'] = ret.pop('establishment_subtypes')
|
||||
#
|
||||
# return ret
|
||||
|
|
@ -38,7 +38,7 @@ def update_document(sender, **kwargs):
|
|||
for establishment in establishments:
|
||||
registry.update(establishment)
|
||||
if model_name == 'establishmentsubtype':
|
||||
if instance(instance, establishment_models.EstablishmentSubType):
|
||||
if isinstance(instance, establishment_models.EstablishmentSubType):
|
||||
establishments = Establishment.objects.filter(
|
||||
establishment_subtypes=instance)
|
||||
for establishment in establishments:
|
||||
|
|
|
|||
|
|
@ -111,6 +111,9 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
|
|||
},
|
||||
'tags_category_id': {
|
||||
'field': 'tags.category.id',
|
||||
'lookups': [
|
||||
constants.LOOKUP_QUERY_IN,
|
||||
],
|
||||
},
|
||||
'collection_type': {
|
||||
'field': 'collections.collection_type'
|
||||
|
|
@ -121,6 +124,24 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
|
|||
'establishment_subtypes': {
|
||||
'field': 'establishment_subtypes.id'
|
||||
},
|
||||
'works_noon': {
|
||||
'field': 'works_noon',
|
||||
'lookups': [
|
||||
constants.LOOKUP_QUERY_IN,
|
||||
],
|
||||
},
|
||||
'works_evening': {
|
||||
'field': 'works_evening',
|
||||
'lookups': [
|
||||
constants.LOOKUP_QUERY_IN,
|
||||
],
|
||||
},
|
||||
'works_now': {
|
||||
'field': 'works_now',
|
||||
'lookups': [
|
||||
constants.LOOKUP_FILTER_TERM,
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
geo_spatial_filter_fields = {
|
||||
|
|
|
|||
18
apps/tag/migrations/0005_tagcategory_name_indexing.py
Normal file
18
apps/tag/migrations/0005_tagcategory_name_indexing.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.4 on 2019-10-22 15:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tag', '0004_tag_priority'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='tagcategory',
|
||||
name='index_name',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='indexing name', unique=True),
|
||||
),
|
||||
]
|
||||
|
|
@ -5,6 +5,14 @@ from configuration.models import TranslationSettings
|
|||
from utils.models import TJSONField, TranslatedFieldsMixin
|
||||
|
||||
|
||||
class TagQuerySet(models.QuerySet):
|
||||
def filter_chosen(self):
|
||||
return self.exclude(priority__isnull=True)
|
||||
|
||||
def order_by_priority(self):
|
||||
return self.order_by('priority')
|
||||
|
||||
|
||||
class Tag(TranslatedFieldsMixin, models.Model):
|
||||
"""Tag model."""
|
||||
|
||||
|
|
@ -16,6 +24,8 @@ class Tag(TranslatedFieldsMixin, models.Model):
|
|||
verbose_name=_('Category'))
|
||||
priority = models.IntegerField(unique=True, null=True, default=None)
|
||||
|
||||
objects = TagQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
"""Meta class."""
|
||||
|
||||
|
|
@ -56,7 +66,7 @@ class TagCategoryQuerySet(models.QuerySet):
|
|||
|
||||
def with_tags(self, switcher=True):
|
||||
"""Filter by existing tags."""
|
||||
return self.filter(tags__isnull=not switcher)
|
||||
return self.exclude(tags__isnull=switcher)
|
||||
|
||||
|
||||
class TagCategory(TranslatedFieldsMixin, models.Model):
|
||||
|
|
@ -69,6 +79,8 @@ class TagCategory(TranslatedFieldsMixin, models.Model):
|
|||
on_delete=models.SET_NULL, null=True,
|
||||
default=None)
|
||||
public = models.BooleanField(default=False)
|
||||
index_name = models.CharField(max_length=255, blank=True, null=True,
|
||||
verbose_name=_('indexing name'), unique=True)
|
||||
|
||||
objects = TagCategoryQuerySet.as_manager()
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,8 @@ class TagCategoryBaseSerializer(serializers.ModelSerializer):
|
|||
fields = (
|
||||
'id',
|
||||
'label_translated',
|
||||
'tags'
|
||||
'index_name',
|
||||
'tags',
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
7
apps/tag/urls/mobile.py
Normal file
7
apps/tag/urls/mobile.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
from tag.urls.web import urlpatterns as common_urlpatterns
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
]
|
||||
|
||||
urlpatterns.extend(common_urlpatterns)
|
||||
|
|
@ -7,6 +7,7 @@ app_name = 'tag'
|
|||
|
||||
router = SimpleRouter()
|
||||
router.register(r'categories', views.TagCategoryViewSet)
|
||||
router.register(r'chosen_tags', views.ChosenTagsView, basename='Tag')
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,22 @@
|
|||
"""Tag views."""
|
||||
from rest_framework import viewsets, mixins, status
|
||||
from rest_framework import viewsets, mixins, status, generics
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from tag import filters, models, serializers
|
||||
from rest_framework import permissions
|
||||
|
||||
|
||||
class ChosenTagsView(generics.ListAPIView, viewsets.GenericViewSet):
|
||||
pagination_class = None
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
serializer_class = serializers.TagBaseSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return models.Tag.objects\
|
||||
.filter_chosen() \
|
||||
.order_by_priority()
|
||||
|
||||
|
||||
# User`s views & viewsets
|
||||
class TagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
|
||||
"""ViewSet for TagCategory model."""
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user