diff --git a/apps/account/filters.py b/apps/account/filters.py index 88c9e9f0..c9f0bb0f 100644 --- a/apps/account/filters.py +++ b/apps/account/filters.py @@ -33,14 +33,8 @@ class AccountBackOfficeFilter(filters.FilterSet): return queryset def search_text(self, queryset, name, value): - queryset = queryset.annotate_vector() if value not in EMPTY_VALUES: - # search by exact value - filtered_qs = queryset.filter(vector=value) - if not filtered_qs.exists(): - # if filtered qs is None find something - filtered_qs = queryset.filter(vector__icontains=value) - return filtered_qs + return queryset.full_text_search(value) return queryset def by_role_country_code(self, queryset, name, value): diff --git a/apps/account/models.py b/apps/account/models.py index acb56089..08944f0c 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -1,6 +1,6 @@ """Account models""" from datetime import datetime - +from django.contrib.postgres.search import TrigramSimilarity from django.conf import settings from django.contrib.auth.models import AbstractUser, UserManager as BaseUserManager from django.core.mail import send_mail @@ -12,6 +12,7 @@ from django.utils.html import mark_safe from django.utils.http import urlsafe_base64_encode from django.utils.translation import ugettext_lazy as _ from rest_framework.authtoken.models import Token +from collections import Counter from authorization.models import Application from establishment.models import Establishment, EstablishmentSubType @@ -20,7 +21,6 @@ from main.models import SiteSettings from utils.models import GMTokenGenerator from utils.models import ImageMixin, ProjectBaseMixin, PlatformMixin from utils.tokens import GMRefreshToken -from django.contrib.postgres.search import SearchVector from phonenumber_field.modelfields import PhoneNumberField @@ -84,26 +84,13 @@ class Role(ProjectBaseMixin): objects = RoleQuerySet.as_manager() - @classmethod - def role_names(cls): - return [role_name for role_name in dict(cls.ROLE_CHOICES).values()] - @classmethod def role_types(cls): roles = [] - for role_id, role in dict(cls.ROLE_CHOICES).items(): - roles.append({'id': role_id, 'name': role}) + for role, display_name in dict(cls.ROLE_CHOICES).items(): + roles.append({'role_name': role, 'role_counter': display_name, 'role': 0}) return roles - @classmethod - def role_condition_expressions(cls) -> list: - role_choices = {role_id: role_name._proxy____args[0] - for role_id, role_name in dict(cls.ROLE_CHOICES).items()} - - whens = [models.When(role=role_id, then=models.Value(role_name)) - for role_id, role_name in role_choices.items()] - return whens - class UserManager(BaseUserManager): """Extended manager for User model.""" @@ -124,6 +111,14 @@ class UserManager(BaseUserManager): class UserQuerySet(models.QuerySet): """Extended queryset for User model.""" + def with_base_related(self): + """Return QuerySet with base related.""" + return self.select_related('last_country', 'last_country__country') + + def with_extend_related(self): + """Return QuerySet with extend related.""" + return self.with_base_related().prefetch_related('roles', 'subscriber') + def active(self, switcher=True): """Filter only active users.""" return self.filter(is_active=switcher) @@ -150,15 +145,56 @@ class UserQuerySet(models.QuerySet): role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER).first() return self.by_role(role).filter(userrole__establishment=establishment) - def annotate_vector(self): - """Full-text search""" - return self.annotate(vector=SearchVector( - 'username', - 'first_name', - 'last_name', - 'email', - 'phone', - )) + def full_text_search(self, search_value: str): + return self.annotate( + username_similarity=models.Case( + models.When( + models.Q(username__isnull=False), + then=TrigramSimilarity('username', search_value.lower()) + ), + default=0, + output_field=models.FloatField() + ), + first_name_similarity=models.Case( + models.When( + models.Q(first_name__isnull=False), + then=TrigramSimilarity('first_name', search_value.lower()) + ), + default=0, + output_field=models.FloatField() + ), + last_name_similarity=models.Case( + models.When( + models.Q(last_name__isnull=False), + then=TrigramSimilarity('last_name', search_value.lower()) + ), + default=0, + output_field=models.FloatField() + ), + email_similarity=models.Case( + models.When( + models.Q(email__isnull=False), + then=TrigramSimilarity('email', search_value.lower()) + ), + default=0, + output_field=models.FloatField() + ), + phone_similarity=models.Case( + models.When( + models.Q(phone__isnull=False), + then=TrigramSimilarity('phone', search_value.lower()) + ), + default=0, + output_field=models.FloatField() + ), + relevance=( + models.F('username_similarity') + + models.F('first_name_similarity') + + models.F('last_name_similarity') + + models.F('email_similarity') + + models.F('phone_similarity') + ), + ).filter(relevance__gte=0.1).order_by('-relevance') def by_role_country_code(self, country_code: str): """Filter by role country code.""" @@ -403,10 +439,47 @@ class User(AbstractUser): class UserRoleQueryset(models.QuerySet): """QuerySet for model UserRole.""" + def _role_counter(self, country_code: str = None) -> dict: + additional_filters = { + 'state': self.model.VALIDATED, + } + + if country_code: + additional_filters.update({'role__site__country__code': country_code}) + + user_roles = ( + self.filter(**additional_filters) + .distinct('user_id', 'role__role') + .values('user_id', 'role__role') + ) + return dict(Counter([i['role__role'] for i in user_roles])) + def country_admin_role(self): return self.filter(role__role=self.model.role.field.target_field.model.COUNTRY_ADMIN, state=self.model.VALIDATED) + def aggregate_role_counter(self, country_code: str = None) -> list: + _role_choices = dict(Role.ROLE_CHOICES) + role_counter = [] + + # fill existed roles + for role, count in self._role_counter(country_code=country_code).items(): + role_counter.append({ + 'role': role, + 'role_name': _role_choices[role], + 'count': count, + }) + + # check by roles + for role, role_name in _role_choices.items(): + if role not in [i['role'] for i in role_counter]: + role_counter.append({ + 'role': role, + 'role_name': _role_choices[role], + 'count': 0, + }) + return role_counter + class UserRole(ProjectBaseMixin): """UserRole model.""" diff --git a/apps/account/serializers/back.py b/apps/account/serializers/back.py index b38b9042..2baaf656 100644 --- a/apps/account/serializers/back.py +++ b/apps/account/serializers/back.py @@ -143,9 +143,3 @@ class UserRoleSerializer(serializers.ModelSerializer): 'user', 'establishment' ] - - -class RoleTabRetrieveSerializer(serializers.Serializer): - """Serializer for BackOffice role tab.""" - role_name = serializers.CharField() - role_counter = serializers.IntegerField() diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index 1673bef9..a537c1bf 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -45,6 +45,7 @@ class RoleBaseSerializer(serializers.ModelSerializer): role_display = serializers.CharField(source='get_role_display', read_only=True) navigation_bar_permission = NavigationBarPermissionBaseSerializer(read_only=True) country_code = serializers.CharField(source='country.code', read_only=True, allow_null=True) + country_name_translated = serializers.CharField(source='country.name_translated', read_only=True, allow_null=True) class Meta: """Meta class.""" @@ -54,6 +55,7 @@ class RoleBaseSerializer(serializers.ModelSerializer): 'role_display', 'navigation_bar_permission', 'country_code', + 'country_name_translated', ] @@ -85,6 +87,7 @@ class UserSerializer(serializers.ModelSerializer): class Meta: model = models.User fields = [ + 'id', 'username', 'first_name', 'last_name', @@ -120,12 +123,6 @@ class UserSerializer(serializers.ModelSerializer): subscriptions_handler(subscriptions_list, user) return user - def validate_email(self, value): - """Validate email value""" - if hasattr(self.instance, 'email') and self.instance.email and value == self.instance.email: - raise serializers.ValidationError(detail='Equal email address.') - return value - def validate_username(self, value): """Custom username validation""" valid = utils_methods.username_validator(username=value) @@ -139,12 +136,13 @@ class UserSerializer(serializers.ModelSerializer): if 'subscription_types' in validated_data: subscriptions_list = validated_data.pop('subscription_types') + new_email = validated_data.get('email') old_email = instance.email instance = super().update(instance, validated_data) - if 'email' in validated_data: + if new_email and new_email != old_email: instance.email_confirmed = False instance.email = old_email - instance.unconfirmed_email = validated_data['email'] + instance.unconfirmed_email = new_email instance.save() # Send verification link on user email for change email address if settings.USE_CELERY: @@ -193,6 +191,7 @@ class UserShortSerializer(UserSerializer): 'id', 'fullname', 'email', + 'username', ] diff --git a/apps/account/urls/back.py b/apps/account/urls/back.py index db7cb772..a24a33e6 100644 --- a/apps/account/urls/back.py +++ b/apps/account/urls/back.py @@ -7,8 +7,7 @@ app_name = 'account' urlpatterns = [ path('role/', views.RoleListView.as_view(), name='role-list-create'), - path('role/types/', views.RoleChoiceListView.as_view(), name='role-type-list'), - path('role/tab/', views.RoleTabRetrieveView.as_view(), name='role-tab'), + path('role/types/', views.RoleTypeRetrieveView.as_view(), name='role-types'), path('user-role/', views.UserRoleListView.as_view(), name='user-role-list-create'), path('user/', views.UserListView.as_view(), name='user-create-list'), path('user//', views.UserRUDView.as_view(), name='user-rud'), diff --git a/apps/account/views/back.py b/apps/account/views/back.py index f339ffb7..0fd9bdde 100644 --- a/apps/account/views/back.py +++ b/apps/account/views/back.py @@ -18,39 +18,18 @@ class RoleListView(generics.ListCreateAPIView): filter_class = filters.RoleListFilter -class RoleChoiceListView(generics.GenericAPIView): - """Return role choices.""" - def get(self, request, *args, **kwargs): - """Implement GET-method""" - return Response(models.Role.role_types(), status=status.HTTP_200_OK) - - -class RoleTabRetrieveView(generics.GenericAPIView): +class RoleTypeRetrieveView(generics.GenericAPIView): permission_classes = [permissions.IsAdminUser] - def get_queryset(self): - """Overridden get_queryset method.""" - additional_filters = {} + def get(self, request, *args, **kwargs): + """Implement GET-method""" + country_code = None if (self.request.user.userrole_set.country_admin_role().exists() and hasattr(self.request, 'country_code')): - additional_filters.update({'country__code': self.request.country_code}) - - return models.Role.objects.filter(**additional_filters)\ - .annotate_role_name()\ - .values('role_name')\ - .annotate_role_counter()\ - .values('role_name', 'role_counter') - - def get(self, request, *args, **kwargs): - """Implement GET-method""" - data = list(self.get_queryset()) - - # todo: Need refactoring. Extend data list with non-existed role. - for role in models.Role.role_names(): - if role not in [role.get('role_name') for role in data]: - data.append({'role_name': role, 'role_counter': 0}) + country_code = self.request.country_code + data = models.UserRole.objects.aggregate_role_counter(country_code) return Response(data, status=status.HTTP_200_OK) @@ -61,7 +40,6 @@ class UserRoleListView(generics.ListCreateAPIView): class UserListView(generics.ListCreateAPIView): """User list create view.""" - queryset = User.objects.prefetch_related('roles', 'subscriber') serializer_class = serializers.BackUserSerializer permission_classes = (permissions.IsAdminUser,) filter_class = filters.AccountBackOfficeFilter @@ -76,6 +54,10 @@ class UserListView(generics.ListCreateAPIView): 'date_joined', ) + def get_queryset(self): + """Overridden get_queryset method.""" + return User.objects.with_extend_related() + class UserRUDView(generics.RetrieveUpdateDestroyAPIView): """User RUD view.""" diff --git a/apps/account/views/common.py b/apps/account/views/common.py index 8b066742..e242c393 100644 --- a/apps/account/views/common.py +++ b/apps/account/views/common.py @@ -17,7 +17,7 @@ from utils.views import JWTGenericViewMixin # User views -class UserRetrieveUpdateView(generics.RetrieveUpdateAPIView): +class UserRetrieveUpdateView(generics.RetrieveUpdateDestroyAPIView): """User update view.""" serializer_class = serializers.UserSerializer queryset = models.User.objects.active() @@ -25,6 +25,10 @@ class UserRetrieveUpdateView(generics.RetrieveUpdateAPIView): def get_object(self): return self.request.user + def delete(self, request, *args, **kwargs): + """Overridden behavior of DELETE method.""" + return Response(status=status.HTTP_204_NO_CONTENT) + class ChangePasswordView(generics.GenericAPIView): """Change password view""" diff --git a/apps/advertisement/models.py b/apps/advertisement/models.py index 5d335176..81b5dbb2 100644 --- a/apps/advertisement/models.py +++ b/apps/advertisement/models.py @@ -45,10 +45,8 @@ class Advertisement(ProjectBaseMixin): url = models.URLField(verbose_name=_('Ad URL')) block_level = models.CharField(verbose_name=_('Block level'), max_length=10, blank=True, null=True) target_languages = models.ManyToManyField(Language) - start = models.DateTimeField(null=True, - verbose_name=_('start')) - end = models.DateTimeField(blank=True, null=True, default=None, - verbose_name=_('end')) + start = models.DateTimeField(null=True, verbose_name=_('start')) + end = models.DateTimeField(blank=True, null=True, default=None, verbose_name=_('end')) sites = models.ManyToManyField('main.SiteSettings', related_name='advertisements', verbose_name=_('site')) diff --git a/apps/advertisement/urls/mobile.py b/apps/advertisement/urls/mobile.py index f61003da..350aecd3 100644 --- a/apps/advertisement/urls/mobile.py +++ b/apps/advertisement/urls/mobile.py @@ -8,6 +8,7 @@ from .common import common_urlpatterns app_name = 'advertisements' urlpatterns = [ + path('', views.AdvertisementPageTypeMobileListView.as_view(), name='list'), path('/', views.AdvertisementPageTypeMobileListView.as_view(), name='list'), ] diff --git a/apps/advertisement/urls/web.py b/apps/advertisement/urls/web.py index 4d17c831..5cc1f2a5 100644 --- a/apps/advertisement/urls/web.py +++ b/apps/advertisement/urls/web.py @@ -7,6 +7,7 @@ from .common import common_urlpatterns app_name = 'advertisements' urlpatterns = [ + path('', views.AdvertisementPageTypeWebListView.as_view(), name='list'), path('/', views.AdvertisementPageTypeWebListView.as_view(), name='list'), ] diff --git a/apps/advertisement/views/common.py b/apps/advertisement/views/common.py index 12702d4b..08f739ef 100644 --- a/apps/advertisement/views/common.py +++ b/apps/advertisement/views/common.py @@ -24,6 +24,10 @@ class AdvertisementPageTypeListView(AdvertisementBaseView, generics.ListAPIView) def get_queryset(self): """Overridden get queryset method.""" product_type = self.kwargs.get('page_type') + + if product_type is None: + product_type = 'mobile' + qs = super(AdvertisementPageTypeListView, self).get_queryset() if product_type: return qs.by_page_type(product_type) \ diff --git a/apps/advertisement/views/mobile.py b/apps/advertisement/views/mobile.py index 7ae2f688..6fdb95bd 100644 --- a/apps/advertisement/views/mobile.py +++ b/apps/advertisement/views/mobile.py @@ -16,4 +16,3 @@ class AdvertisementPageTypeMobileListView(AdvertisementPageTypeListView): qs = super().get_queryset().exclude(frequency_percentage__lte=percentage) qs.update(views_count=F('views_count') + 1) return qs - diff --git a/apps/collection/models.py b/apps/collection/models.py index 0457f38d..91173ed0 100644 --- a/apps/collection/models.py +++ b/apps/collection/models.py @@ -57,6 +57,10 @@ class CollectionQuerySet(RelatedObjectsCountMixin): """Returned only published collection""" return self.filter(is_publish=True) + def with_base_related(self): + """Select relate objects""" + return self.select_related('country') + class Collection(ProjectBaseMixin, CollectionDateMixin, TranslatedFieldsMixin, URLImageMixin): @@ -106,7 +110,7 @@ class Collection(ProjectBaseMixin, CollectionDateMixin, """Return list of related objects.""" related_objects = [] # get related objects - for related_object in self._meta.related_objects: + for related_object in self._meta.related_objects.with_base_related(): related_objects.append(related_object) return related_objects diff --git a/apps/collection/views/back.py b/apps/collection/views/back.py index 481f70da..73fb7b18 100644 --- a/apps/collection/views/back.py +++ b/apps/collection/views/back.py @@ -21,7 +21,7 @@ class CollectionViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): def get_queryset(self): """Overridden method 'get_queryset'.""" - qs = models.Collection.objects.all().order_by('-created') + qs = models.Collection.objects.all().order_by('-created').with_base_related() if self.request.country_code: qs = qs.by_country_code(self.request.country_code) return qs @@ -75,7 +75,7 @@ class CollectionBackOfficeViewSet(mixins.CreateModelMixin, """ViewSet for Collection model for BackOffice users.""" permission_classes = (permissions.IsAuthenticated,) - queryset = models.Collection.objects.all() + queryset = models.Collection.objects.with_base_related() filter_backends = [DjangoFilterBackend, OrderingFilter] serializer_class = serializers.CollectionBackOfficeSerializer bind_object_serializer_class = serializers.CollectionBindObjectSerializer diff --git a/apps/comment/serializers/common.py b/apps/comment/serializers/common.py index 6f0d049d..c928aca4 100644 --- a/apps/comment/serializers/common.py +++ b/apps/comment/serializers/common.py @@ -8,7 +8,7 @@ from establishment.models import EstablishmentType class CommentBaseSerializer(serializers.ModelSerializer): """Comment serializer""" - user_name = serializers.CharField(read_only=True, + nickname = serializers.CharField(read_only=True, source='user.username') is_mine = serializers.BooleanField(read_only=True) profile_pic = serializers.URLField(read_only=True, @@ -32,7 +32,7 @@ class CommentBaseSerializer(serializers.ModelSerializer): 'created', 'text', 'mark', - 'user_name', + 'nickname', 'user_email', 'profile_pic', 'status', diff --git a/apps/establishment/filters.py b/apps/establishment/filters.py index 3a7d3398..b52c909f 100644 --- a/apps/establishment/filters.py +++ b/apps/establishment/filters.py @@ -1,7 +1,8 @@ """Establishment app filters.""" from django.core.validators import EMPTY_VALUES from django.utils.translation import ugettext_lazy as _ -from django_filters import rest_framework as filters +from django_filters import rest_framework as filters, Filter +from django_filters.fields import Lookup from rest_framework.serializers import ValidationError from establishment import models @@ -67,9 +68,9 @@ class EmployeeBackFilter(filters.FilterSet): """Employee filter set.""" search = filters.CharFilter(method='search_by_name_or_last_name') - position_id = filters.NumberFilter(method='search_by_actual_position_id') - public_mark = filters.NumberFilter(method='search_by_public_mark') - toque_number = filters.NumberFilter(method='search_by_toque_number') + position_id = filters.CharFilter(method='search_by_actual_position_id') + public_mark = filters.CharFilter(method='search_by_public_mark') + toque_number = filters.CharFilter(method='search_by_toque_number') username = filters.CharFilter(method='search_by_username_or_name') class Meta: @@ -93,19 +94,22 @@ class EmployeeBackFilter(filters.FilterSet): def search_by_actual_position_id(self, queryset, name, value): """Search by actual position_id.""" if value not in EMPTY_VALUES: - return queryset.search_by_position_id(value) + value_list = [int(val) for val in value.split(',')] + return queryset.search_by_position_id(value_list) return queryset def search_by_public_mark(self, queryset, name, value): """Search by establishment public_mark.""" if value not in EMPTY_VALUES: - return queryset.search_by_public_mark(value) + value_list = [int(val) for val in value.split(',')] + return queryset.search_by_public_mark(value_list) return queryset def search_by_toque_number(self, queryset, name, value): """Search by establishment toque_number.""" if value not in EMPTY_VALUES: - return queryset.search_by_toque_number(value) + value_list = [int(val) for val in value.split(',')] + return queryset.search_by_toque_number(value_list) return queryset def search_by_username_or_name(self, queryset, name, value): @@ -122,3 +126,27 @@ class EmployeeBackSearchFilter(EmployeeBackFilter): raise ValidationError({'detail': _('Type at least 3 characters to search please.')}) return queryset.trigram_search(value) return queryset + + +class MenuDishesBackFilter(filters.FilterSet): + """Menu filter set.""" + + category = filters.CharFilter(method='search_by_category') + is_drinks_included = filters.BooleanFilter(field_name='is_drinks_included') + establishment_id = filters.NumberFilter(field_name='establishment_id') + + class Meta: + """Meta class.""" + + model = models.Menu + fields = ( + 'category', + 'is_drinks_included', + 'establishment_id', + ) + + def search_by_category(self, queryset, name, value): + """Search by category.""" + if value not in EMPTY_VALUES: + return queryset.search_by_category(value) + return queryset diff --git a/apps/establishment/management/commands/add_menus.py b/apps/establishment/management/commands/add_menus.py index 3875b8ca..8692094b 100644 --- a/apps/establishment/management/commands/add_menus.py +++ b/apps/establishment/management/commands/add_menus.py @@ -1,4 +1,5 @@ from django.core.management.base import BaseCommand +from tqdm import tqdm from establishment.models import Establishment, Menu, Plate from transfer.models import Menus @@ -10,14 +11,17 @@ class Command(BaseCommand): def handle(self, *args, **kwargs): count = 0 menus = Menus.objects.filter(name__isnull=False).exclude(name='') - for old_menu in menus: + for old_menu in tqdm(menus, desc='Add formulas menu'): est = Establishment.objects.filter( old_id=old_menu.establishment_id).first() if est: menu, _ = Menu.objects.get_or_create( category={'en-GB': 'formulas'}, - establishment=est + establishment=est, + old_id=old_menu.id, + is_drinks_included=True if old_menu.drinks == 'included' else False, + created=old_menu.created_at, ) plate, created = Plate.objects.get_or_create( name={"en-GB": old_menu.name}, diff --git a/apps/establishment/migrations/0076_auto_20200123_1115.py b/apps/establishment/migrations/0076_auto_20200123_1115.py new file mode 100644 index 00000000..c18a645e --- /dev/null +++ b/apps/establishment/migrations/0076_auto_20200123_1115.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.7 on 2020-01-23 11:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('timetable', '0003_auto_20191003_0943'), + ('establishment', '0075_employee_photo'), + ] + + operations = [ + migrations.AddField( + model_name='menu', + name='is_drinks_included', + field=models.BooleanField(default=False, verbose_name='is drinks included'), + ), + migrations.AddField( + model_name='menu', + name='schedule', + field=models.ManyToManyField(blank=True, related_name='menus', to='timetable.Timetable', verbose_name='Establishment schedule'), + ), + ] diff --git a/apps/establishment/migrations/0077_menuuploads.py b/apps/establishment/migrations/0077_menuuploads.py new file mode 100644 index 00000000..5abdc13b --- /dev/null +++ b/apps/establishment/migrations/0077_menuuploads.py @@ -0,0 +1,34 @@ +# Generated by Django 2.2.7 on 2020-01-23 11:47 + +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('establishment', '0076_auto_20200123_1115'), + ] + + operations = [ + migrations.CreateModel( + name='MenuUploads', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')), + ('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('file', models.FileField(upload_to='', validators=[django.core.validators.FileExtensionValidator(allowed_extensions=('jpg', 'jpeg', 'png', 'doc', 'docx', 'pdf'))], verbose_name='File')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='menuuploads_records_created', to=settings.AUTH_USER_MODEL, verbose_name='created by')), + ('menu', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='uploads', to='establishment.Menu', verbose_name='Menu')), + ('modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='menuuploads_records_modified', to=settings.AUTH_USER_MODEL, verbose_name='modified by')), + ], + options={ + 'verbose_name': 'menu upload', + 'verbose_name_plural': 'menu uploads', + }, + ), + ] diff --git a/apps/establishment/migrations/0078_menu_old_id.py b/apps/establishment/migrations/0078_menu_old_id.py new file mode 100644 index 00000000..9401741e --- /dev/null +++ b/apps/establishment/migrations/0078_menu_old_id.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2020-01-24 05:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0077_menuuploads'), + ] + + operations = [ + migrations.AddField( + model_name='menu', + name='old_id', + field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='old id'), + ), + ] diff --git a/apps/establishment/migrations/0079_auto_20200124_0720.py b/apps/establishment/migrations/0079_auto_20200124_0720.py new file mode 100644 index 00000000..01c4e509 --- /dev/null +++ b/apps/establishment/migrations/0079_auto_20200124_0720.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.7 on 2020-01-24 07:20 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0078_menu_old_id'), + ] + + operations = [ + migrations.AlterModelOptions( + name='menu', + options={'ordering': ('-created',), 'verbose_name': 'menu', 'verbose_name_plural': 'menu'}, + ), + migrations.AlterField( + model_name='plate', + name='menu', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='plates', to='establishment.Menu', verbose_name='menu'), + ), + ] diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 7f20c529..5f575c78 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -6,6 +6,7 @@ from typing import List import elasticsearch_dsl from django.conf import settings +from django.shortcuts import get_object_or_404 from django.contrib.contenttypes import fields as generic from django.contrib.gis.db.models.functions import Distance from django.contrib.gis.geos import Point @@ -14,7 +15,7 @@ from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.search import TrigramDistance, TrigramSimilarity from django.contrib.postgres.indexes import GinIndex from django.core.exceptions import ValidationError -from django.core.validators import MinValueValidator, MaxValueValidator +from django.core.validators import MinValueValidator, MaxValueValidator, FileExtensionValidator from django.db import models from django.db.models import When, Case, F, ExpressionWrapper, Subquery, Q, Prefetch, Sum from django.utils import timezone @@ -141,8 +142,8 @@ class EstablishmentQuerySet(models.QuerySet): def with_extended_related(self): return self.with_extended_address_related().select_related('establishment_type'). \ prefetch_related('establishment_subtypes', 'awards', 'schedule', - 'phones', 'gallery', 'menu_set', 'menu_set__plate_set', - 'menu_set__plate_set__currency', 'currency'). \ + 'phones', 'gallery', 'menu_set', 'menu_set__plates', + 'menu_set__plates__currency', 'currency'). \ prefetch_actual_employees() def with_type_related(self): @@ -689,8 +690,8 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin, plates = self.menu_set.filter( models.Q(category={'en-GB': 'formulas'}) ).aggregate( - max=models.Max('plate__price', output_field=models.FloatField()), - min=models.Min('plate__price', output_field=models.FloatField())) + max=models.Max('plates__price', output_field=models.FloatField()), + min=models.Min('plates__price', output_field=models.FloatField())) return plates @property @@ -700,8 +701,8 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin, models.Q(category={'en-GB': 'main_course'}) | models.Q(category={'en-GB': 'dessert'}) ).aggregate( - max=models.Max('plate__price', output_field=models.FloatField()), - min=models.Min('plate__price', output_field=models.FloatField()), + max=models.Max('plates__price', output_field=models.FloatField()), + min=models.Min('plates__price', output_field=models.FloatField()), ) return plates @@ -764,7 +765,7 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin, """ Return Country id of establishment location """ - return self.address.country_id + return self.address.country_id if hasattr(self.address, 'country_id') else None @property def establishment_id(self): @@ -1020,7 +1021,7 @@ class EmployeeQuerySet(models.QuerySet): ), relevance=(F('search_name_similarity') + F('search_exact_match') + F('search_contains_match') + F('search_last_name_similarity')) - ).filter(relevance__gte=0.3).order_by('-relevance') + ).filter(relevance__gte=0.1).order_by('-relevance') def search_by_name_or_last_name(self, value): """Search by name or last_name.""" @@ -1034,22 +1035,22 @@ class EmployeeQuerySet(models.QuerySet): Q(establishmentemployee__to_date__isnull=True) ) - def search_by_position_id(self, value): + def search_by_position_id(self, value_list): """Search by position_id.""" - return self.filter( - Q(establishmentemployee__position_id=value), + return self.search_by_actual_employee().filter( + Q(establishmentemployee__position_id__in=value_list), ) - def search_by_public_mark(self, value): + def search_by_public_mark(self, value_list): """Search by establishment public_mark.""" - return self.filter( - Q(establishmentemployee__establishment__public_mark=value), + return self.search_by_actual_employee().filter( + Q(establishmentemployee__establishment__public_mark__in=value_list), ) - def search_by_toque_number(self, value): + def search_by_toque_number(self, value_list): """Search by establishment toque_number.""" - return self.filter( - Q(establishmentemployee__establishment__toque_number=value), + return self.search_by_actual_employee().filter( + Q(establishmentemployee__establishment__toque_number__in=value_list), ) def search_by_username_or_name(self, value): @@ -1155,6 +1156,11 @@ class Employee(BaseAttributes): ) return image_property + def remove_award(self, award_id: int): + from main.models import Award + award = get_object_or_404(Award, pk=award_id) + self.awards.remove(award) + class EstablishmentScheduleQuerySet(models.QuerySet): """QuerySet for model EstablishmentSchedule""" @@ -1208,7 +1214,11 @@ class Plate(TranslatedFieldsMixin, models.Model): _('currency code'), max_length=250, blank=True, null=True, default=None) menu = models.ForeignKey( - 'establishment.Menu', verbose_name=_('menu'), on_delete=models.CASCADE) + 'establishment.Menu', + verbose_name=_('menu'), + related_name='plates', + on_delete=models.CASCADE, + ) @property def establishment_id(self): @@ -1219,6 +1229,27 @@ class Plate(TranslatedFieldsMixin, models.Model): verbose_name_plural = _('plates') +class MenuQuerySet(models.QuerySet): + def with_schedule_plates_establishment(self): + return self.select_related( + 'establishment', + ).prefetch_related( + 'schedule', + 'plates', + ) + + def dishes(self): + return self.filter( + Q(category__icontains='starter') | + Q(category__icontains='dessert') | + Q(category__icontains='main_course') + ) + + def search_by_category(self, value): + """Search by category.""" + return self.filter(category__icontains=value) + + class Menu(TranslatedFieldsMixin, BaseAttributes): """Menu model.""" @@ -1230,10 +1261,35 @@ class Menu(TranslatedFieldsMixin, BaseAttributes): establishment = models.ForeignKey( 'establishment.Establishment', verbose_name=_('establishment'), on_delete=models.CASCADE) + is_drinks_included = models.BooleanField(_('is drinks included'), default=False) + schedule = models.ManyToManyField( + to='timetable.Timetable', + blank=True, + verbose_name=_('Establishment schedule'), + related_name='menus', + ) + old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) + + objects = MenuQuerySet.as_manager() class Meta: verbose_name = _('menu') verbose_name_plural = _('menu') + ordering = ('-created',) + + +class MenuUploads(BaseAttributes): + """Menu files""" + + menu = models.ForeignKey(Menu, verbose_name=_('Menu'), on_delete=models.CASCADE, related_name='uploads') + file = models.FileField( + _('File'), + validators=[FileExtensionValidator(allowed_extensions=('jpg', 'jpeg', 'png', 'doc', 'docx', 'pdf')), ], + ) + + class Meta: + verbose_name = _('menu upload') + verbose_name_plural = _('menu uploads') class SocialChoice(models.Model): diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 4729fe06..6eb4b40d 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -14,7 +14,7 @@ from main.models import Currency from main.serializers import AwardSerializer from timetable.serialziers import ScheduleRUDSerializer from utils.decorators import with_base_attributes -from utils.serializers import ImageBaseSerializer, ProjectModelSerializer, TimeZoneChoiceField +from utils.serializers import ImageBaseSerializer, TimeZoneChoiceField, ProjectModelSerializer def phones_handler(phones_list, establishment): diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 066452a7..cd60e9f1 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -24,6 +24,7 @@ from utils.serializers import (ProjectModelSerializer, TranslatedField, logger = logging.getLogger(__name__) + class ContactPhonesSerializer(serializers.ModelSerializer): """Contact phone serializer""" diff --git a/apps/establishment/urls/back.py b/apps/establishment/urls/back.py index 396fa9d7..812ac6ea 100644 --- a/apps/establishment/urls/back.py +++ b/apps/establishment/urls/back.py @@ -30,6 +30,8 @@ urlpatterns = [ name='note-rud'), path('slug//admin/', views.EstablishmentAdminView.as_view(), name='establishment-admin-list'), + path('menus/dishes/', views.MenuDishesListCreateView.as_view(), name='menu-dishes-list'), + path('menus/dishes//', views.MenuDishesRUDView.as_view(), name='menu-dishes-rud'), path('menus/', views.MenuListCreateView.as_view(), name='menu-list'), path('menus//', views.MenuRUDView.as_view(), name='menu-rud'), path('plates/', views.PlateListCreateView.as_view(), name='plates'), @@ -47,6 +49,7 @@ urlpatterns = [ path('employees/', views.EmployeeListCreateView.as_view(), name='employees'), path('employees/search/', views.EmployeesListSearchViews.as_view(), name='employees-search'), path('employees//', views.EmployeeRUDView.as_view(), name='employees-rud'), + path('employees//', views.RemoveAwardView.as_view(), name='employees-award-delete'), path('/employee//position/', views.EstablishmentEmployeeCreateView.as_view(), name='employees-establishment-create'), diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index 4ba795a6..9f6e8b3e 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -242,7 +242,7 @@ class EmployeeListCreateView(generics.ListCreateAPIView): permission_classes = (permissions.AllowAny,) filter_class = filters.EmployeeBackFilter serializer_class = serializers.EmployeeBackSerializers - queryset = models.Employee.objects.all().with_back_office_related() + queryset = models.Employee.objects.all().distinct().with_back_office_related() class EmployeesListSearchViews(generics.ListAPIView): @@ -273,6 +273,21 @@ class EmployeeRUDView(generics.RetrieveUpdateDestroyAPIView): queryset = models.Employee.objects.all().with_back_office_related() +class RemoveAwardView(generics.DestroyAPIView): + lookup_field = 'pk' + serializer_class = serializers.EmployeeBackSerializers + queryset = models.Employee.objects.all().with_back_office_related() + + def get_object(self): + employee = super().get_object() + employee.remove_award(self.kwargs['award_id']) + return employee + + def delete(self, request, *args, **kwargs): + instance = self.get_object() + return Response(status=status.HTTP_204_NO_CONTENT) + + class EstablishmentTypeListCreateView(generics.ListCreateAPIView): """Establishment type list/create view.""" serializer_class = serializers.EstablishmentTypeBaseSerializer @@ -461,3 +476,18 @@ class EstablishmentAdminView(generics.ListAPIView): establishment = get_object_or_404( models.Establishment, slug=self.kwargs['slug']) return User.objects.establishment_admin(establishment).distinct() + + +class MenuDishesListCreateView(generics.ListCreateAPIView): + """Menu (dessert, main_course, starter) list create view.""" + serializer_class = serializers.MenuDishesSerializer + queryset = models.Menu.objects.with_schedule_plates_establishment().dishes().distinct() + permission_classes = [IsWineryReviewer | IsEstablishmentManager] + filter_class = filters.MenuDishesBackFilter + + +class MenuDishesRUDView(generics.RetrieveUpdateDestroyAPIView): + """Menu (dessert, main_course, starter) RUD view.""" + serializer_class = serializers.MenuDishesRUDSerializers + queryset = models.Menu.objects.dishes().distinct() + permission_classes = [IsWineryReviewer | IsEstablishmentManager] diff --git a/apps/gallery/serializers.py b/apps/gallery/serializers.py index 41e75cd7..f7d2d2a3 100644 --- a/apps/gallery/serializers.py +++ b/apps/gallery/serializers.py @@ -59,6 +59,7 @@ class CropImageSerializer(ImageSerializer): MinValueValidator(1), MaxValueValidator(100)]) cropped_image = serializers.DictField(read_only=True, allow_null=True) + certain_aspect = serializers.CharField(allow_blank=True, allow_null=True, required=False) class Meta(ImageSerializer.Meta): """Meta class.""" @@ -71,6 +72,7 @@ class CropImageSerializer(ImageSerializer): 'crop', 'quality', 'cropped_image', + 'certain_aspect', ] def validate(self, attrs): @@ -98,7 +100,8 @@ class CropImageSerializer(ImageSerializer): x1, y1 = int(crop.split(' ')[0][:-2]), int(crop.split(' ')[1][:-2]) x2, y2 = x1 + width, y1 + height crop_params = { - 'geometry': f'{self._image.image.width}x{self._image.image.width}', + 'geometry': f'{round(x2 - x1)}x{round(y2 - y1)}' if 'certain_aspect' not in validated_data else + validated_data['certain_aspect'], 'quality': 100, 'cropbox': f'{x1},{y1},{x2},{y2}' } diff --git a/apps/location/models.py b/apps/location/models.py index 0fa3cdd1..a8f4c43a 100644 --- a/apps/location/models.py +++ b/apps/location/models.py @@ -80,22 +80,19 @@ class RegionQuerySet(models.QuerySet): def without_parent_region(self, switcher: bool = True): """Filter regions by parent region.""" - return self.filter(parent_region__isnull=switcher)\ - .order_by('name') + return self.filter(parent_region__isnull=switcher) def by_region_id(self, region_id): """Filter regions by region id.""" - return self.filter(id=region_id)\ - .order_by('name') + return self.filter(id=region_id) def by_sub_region_id(self, sub_region_id): """Filter sub regions by sub region id.""" - return self.filter(parent_region_id=sub_region_id)\ - .order_by('name') + return self.filter(parent_region_id=sub_region_id) def sub_regions_by_region_id(self, region_id): """Filter regions by sub region id.""" - return self.filter(parent_region_id=region_id).order_by('name') + return self.filter(parent_region_id=region_id) class Region(models.Model): @@ -142,6 +139,9 @@ class CityQuerySet(models.QuerySet): """Return establishments by country code""" return self.filter(country__code=code) + def with_base_related(self): + return self.prefetch_related('country', 'region', 'region__country') + class City(models.Model): """Region model.""" diff --git a/apps/location/serializers/common.py b/apps/location/serializers/common.py index ad60a7a2..1da144f8 100644 --- a/apps/location/serializers/common.py +++ b/apps/location/serializers/common.py @@ -87,7 +87,8 @@ class CityBaseSerializer(serializers.ModelSerializer): image_id = serializers.PrimaryKeyRelatedField( source='image', queryset=gallery_models.Image.objects.all(), - write_only=True + write_only=True, + required=False, ) country = CountrySerializer(read_only=True) diff --git a/apps/location/views/back.py b/apps/location/views/back.py index a0d1bd36..1e1957fe 100644 --- a/apps/location/views/back.py +++ b/apps/location/views/back.py @@ -44,7 +44,7 @@ class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView): def get_queryset(self): """Overridden method 'get_queryset'.""" qs = models.City.objects.all().annotate(locale_name=KeyTextTransform(get_current_locale(), 'name_translated'))\ - .order_by('locale_name') + .order_by('locale_name').with_base_related() if self.request.country_code: qs = qs.by_country_code(self.request.country_code) return qs diff --git a/apps/location/views/common.py b/apps/location/views/common.py index d9d5520b..475288fe 100644 --- a/apps/location/views/common.py +++ b/apps/location/views/common.py @@ -18,13 +18,13 @@ class CountryViewMixin(generics.GenericAPIView): class RegionViewMixin(generics.GenericAPIView): """View Mixin for model Region""" model = models.Region - queryset = models.Region.objects.all() + queryset = models.Region.objects.all().order_by('name', 'code') class CityViewMixin(generics.GenericAPIView): """View Mixin for model City""" model = models.City - queryset = models.City.objects.all() + queryset = models.City.objects.with_base_related() class AddressViewMixin(generics.GenericAPIView): @@ -101,7 +101,7 @@ class CityListView(CityViewMixin, generics.ListAPIView): def get_queryset(self): qs = super().get_queryset() if self.request.country_code: - qs = qs.by_country_code(self.request.country_code) + qs = qs.by_country_code(self.request.country_code).with_base_related() return qs diff --git a/apps/main/models.py b/apps/main/models.py index ea853c04..605cb4e3 100644 --- a/apps/main/models.py +++ b/apps/main/models.py @@ -20,8 +20,10 @@ from review.models import Review from tag.models import Tag from utils.exceptions import UnprocessableEntityError from utils.methods import dictfetchall -from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin, - TranslatedFieldsMixin, PlatformMixin) +from utils.models import ( + ProjectBaseMixin, TJSONField, URLImageMixin, + TranslatedFieldsMixin, PlatformMixin, +) class Currency(TranslatedFieldsMixin, models.Model): @@ -156,10 +158,10 @@ class SiteFeature(ProjectBaseMixin): published = models.BooleanField(default=False, verbose_name=_('Published')) main = models.BooleanField(default=False, help_text='shows on main page', - verbose_name=_('Main'),) + verbose_name=_('Main'), ) backoffice = models.BooleanField(default=False, help_text='shows on backoffice page', - verbose_name=_('backoffice'),) + verbose_name=_('backoffice'), ) nested = models.ManyToManyField('self', blank=True, symmetrical=False) old_id = models.IntegerField(null=True, blank=True) @@ -173,6 +175,12 @@ class SiteFeature(ProjectBaseMixin): unique_together = ('site_settings', 'feature') +class AwardQuerySet(models.QuerySet): + + def with_base_related(self): + return self.prefetch_related('award_type') + + class Award(TranslatedFieldsMixin, URLImageMixin, models.Model): """Award model.""" WAITING = 0 @@ -198,6 +206,8 @@ class Award(TranslatedFieldsMixin, URLImageMixin, models.Model): old_id = models.IntegerField(null=True, blank=True) + objects = AwardQuerySet.as_manager() + def __str__(self): title = 'None' lang = TranslationSettings.get_solo().default_language @@ -458,7 +468,7 @@ class Panel(ProjectBaseMixin): } with connections['default'].cursor() as cursor: count = self._raw_count(raw) - start = page*page_size + start = page * page_size cursor.execute(*self.set_limits(start, page_size)) data["count"] = count data["next"] = self.get_next_page(count, page, page_size) @@ -468,7 +478,7 @@ class Panel(ProjectBaseMixin): return data def get_next_page(self, count, page, page_size): - max_page = count/page_size-1 + max_page = count / page_size - 1 if not 0 <= page <= max_page: raise exceptions.NotFound('Invalid page.') if max_page > page: diff --git a/apps/main/serializers/common.py b/apps/main/serializers/common.py index 6a556ee6..3976c92d 100644 --- a/apps/main/serializers/common.py +++ b/apps/main/serializers/common.py @@ -4,8 +4,9 @@ from rest_framework import serializers from location.serializers import CountrySerializer from main import models +from establishment.models import Employee from tag.serializers import TagBackOfficeSerializer -from utils.serializers import ProjectModelSerializer, TranslatedField, RecursiveFieldSerializer +from utils.serializers import ProjectModelSerializer, RecursiveFieldSerializer, TranslatedField class FeatureSerializer(serializers.ModelSerializer): @@ -192,6 +193,15 @@ class SiteShortSerializer(serializers.ModelSerializer): ] +class AwardTypeBaseSerializer(serializers.ModelSerializer): + class Meta: + model = models.AwardType + fields = ( + 'id', + 'name', + ) + + class AwardBaseSerializer(serializers.ModelSerializer): """Award base serializer.""" @@ -210,6 +220,8 @@ class AwardBaseSerializer(serializers.ModelSerializer): class AwardSerializer(AwardBaseSerializer): """Award serializer.""" + award_type = AwardTypeBaseSerializer(read_only=True) + class Meta: model = models.Award fields = AwardBaseSerializer.Meta.fields + ['award_type', ] @@ -218,6 +230,8 @@ class AwardSerializer(AwardBaseSerializer): class BackAwardSerializer(AwardBaseSerializer): """Award serializer.""" + award_type = AwardTypeBaseSerializer(read_only=True) + class Meta: model = models.Award fields = AwardBaseSerializer.Meta.fields + [ @@ -228,6 +242,31 @@ class BackAwardSerializer(AwardBaseSerializer): ] +class BackAwardEmployeeCreateSerializer(serializers.ModelSerializer): + """Award, The Creator.""" + + award_type = serializers.PrimaryKeyRelatedField(required=True, queryset=models.AwardType.objects.all()) + title = serializers.CharField(write_only=True) + + def get_title(self, obj): + pass + + class Meta: + model = models.Award + fields = ( + 'id', + 'award_type', + 'title', + 'vintage_year', + ) + + def validate(self, attrs): + attrs['object_id'] = self.context.get('request').parser_context.get('kwargs')['employee_id'] + attrs['content_type'] = ContentType.objects.get_for_model(Employee) + attrs['title'] = {self.context.get('request').locale: attrs['title']} + return attrs + + class CarouselListSerializer(serializers.ModelSerializer): """Serializer for retrieving list of carousel items.""" diff --git a/apps/main/urls/back.py b/apps/main/urls/back.py index 3d39f008..8b8f5102 100644 --- a/apps/main/urls/back.py +++ b/apps/main/urls/back.py @@ -8,6 +8,8 @@ app_name = 'main' urlpatterns = [ path('awards/', views.AwardLstView.as_view(), name='awards-list-create'), path('awards//', views.AwardRUDView.as_view(), name='awards-rud'), + path('awards/create-and-bind//', views.AwardCreateAndBind.as_view(), name='award-employee-create'), + path('award-types/', views.AwardTypesListView.as_view(), name='awards-types-list'), path('content_type/', views.ContentTypeView.as_view(), name='content_type-list'), path('sites/', views.SiteListBackOfficeView.as_view(), name='site-list-create'), path('site-settings//', views.SiteSettingsBackOfficeView.as_view(), diff --git a/apps/main/urls/common.py b/apps/main/urls/common.py index 6b8f26ce..9741ca60 100644 --- a/apps/main/urls/common.py +++ b/apps/main/urls/common.py @@ -9,4 +9,10 @@ common_urlpatterns = [ path('awards//', AwardRetrieveView.as_view(), name='awards_retrieve'), path('carousel/', CarouselListView.as_view(), name='carousel-list'), path('determine-location/', DetermineLocation.as_view(), name='determine-location'), + path('content-pages/', ContentPageView.as_view(), name='content-pages-list'), + path('content-pages//', ContentPageIdRetrieveView.as_view(), name='content-pages-retrieve-id'), + path('content-pages/create/', ContentPageAdminView.as_view(), name='content-pages-admin-list'), + path('content-pages/slug//', ContentPageRetrieveView.as_view(), name='content-pages-retrieve-slug'), + path('content-pages/update/slug//', ContentPageRetrieveAdminView.as_view(), + name='content-pages-admin-retrieve') ] diff --git a/apps/main/views/back.py b/apps/main/views/back.py index a8974721..23fdc48a 100644 --- a/apps/main/views/back.py +++ b/apps/main/views/back.py @@ -7,28 +7,54 @@ from rest_framework.response import Response from main import serializers from main.serializers.back import PanelSerializer +from establishment.serializers.back import EmployeeBackSerializers +from establishment.models import Employee from main import tasks from main.filters import AwardFilter -from main.models import Award, Footer, PageType, Panel, SiteFeature, Feature +from main.models import Award, Footer, PageType, Panel, SiteFeature, Feature, AwardType from main.views import SiteSettingsView, SiteListView class AwardLstView(generics.ListCreateAPIView): """Award list create view.""" - queryset = Award.objects.all() + queryset = Award.objects.all().with_base_related() serializer_class = serializers.BackAwardSerializer permission_classes = (permissions.IsAdminUser,) filterset_class = AwardFilter +class AwardCreateAndBind(generics.CreateAPIView): + """Award create and bind to employee by id""" + queryset = Award.objects.all().with_base_related() + serializer_class = serializers.BackAwardEmployeeCreateSerializer + permission_classes = (permissions.IsAdminUser, ) + + def create(self, request, *args, **kwargs): + """!!!Overriden!!!""" + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + response_serializer = EmployeeBackSerializers(Employee.objects.get(pk=kwargs['employee_id'])) + headers = self.get_success_headers(response_serializer.data) + return Response(response_serializer.data, status=status.HTTP_201_CREATED, headers=headers) + + class AwardRUDView(generics.RetrieveUpdateDestroyAPIView): """Award RUD view.""" - queryset = Award.objects.all() + queryset = Award.objects.all().with_base_related() serializer_class = serializers.BackAwardSerializer permission_classes = (permissions.IsAdminUser,) lookup_field = 'id' +class AwardTypesListView(generics.ListAPIView): + """AwardType List view.""" + pagination_class = None + queryset = AwardType.objects.all() + serializer_class = serializers.AwardTypeBaseSerializer + permission_classes = (permissions.AllowAny, ) + + class ContentTypeView(generics.ListAPIView): """ContentType list view""" queryset = ContentType.objects.all() diff --git a/apps/main/views/common.py b/apps/main/views/common.py index c565998d..2670dd50 100644 --- a/apps/main/views/common.py +++ b/apps/main/views/common.py @@ -1,10 +1,14 @@ """Main app views.""" from django.http import Http404 -from django.conf import settings from rest_framework import generics, permissions from rest_framework.response import Response from main import methods, models, serializers +from news.models import News +from news.serializers import NewsDetailSerializer, NewsListSerializer +from news.views import NewsMixinView +from utils.serializers import EmptySerializer + # # class FeatureViewMixin: @@ -42,20 +46,19 @@ from main import methods, models, serializers # class SiteFeaturesRUDView(SiteFeaturesViewMixin, # generics.RetrieveUpdateDestroyAPIView): # """Site features RUD.""" -from utils.serializers import EmptySerializer class AwardView(generics.ListAPIView): """Awards list view.""" serializer_class = serializers.AwardSerializer - queryset = models.Award.objects.all() + queryset = models.Award.objects.all().with_base_related() permission_classes = (permissions.AllowAny,) class AwardRetrieveView(generics.RetrieveAPIView): """Award retrieve view.""" serializer_class = serializers.AwardSerializer - queryset = models.Award.objects.all() + queryset = models.Award.objects.all().with_base_related() permission_classes = (permissions.IsAuthenticatedOrReadOnly,) @@ -95,3 +98,57 @@ class DetermineLocation(generics.GenericAPIView): 'country_code': country_code, }) raise Http404 + + +class ContentPageBaseView(generics.GenericAPIView): + + @property + def static_page_category(self): + return 'static' + + def get_queryset(self): + return News.objects.with_base_related() \ + .order_by('-is_highlighted', '-publication_date', '-publication_time') \ + .filter(news_type__name=self.static_page_category) + + +class ContentPageView(ContentPageBaseView, generics.ListAPIView): + """Method to get content pages""" + + permission_classes = (permissions.AllowAny,) + serializer_class = NewsListSerializer + queryset = News.objects.all() + + +class ContentPageAdminView(ContentPageBaseView, generics.ListCreateAPIView): + """Method to get content pages""" + + permission_classes = (permissions.IsAdminUser,) + serializer_class = NewsListSerializer + queryset = News.objects.all() + + +class ContentPageRetrieveView(ContentPageBaseView, NewsMixinView, generics.RetrieveAPIView): + """Retrieve method to get content pages""" + + lookup_field = None + permission_classes = (permissions.AllowAny,) + serializer_class = NewsDetailSerializer + queryset = News.objects.all() + + +class ContentPageIdRetrieveView(ContentPageBaseView, generics.RetrieveAPIView): + """Retrieve method to get content pages""" + + permission_classes = (permissions.AllowAny,) + serializer_class = NewsDetailSerializer + queryset = News.objects.all() + + +class ContentPageRetrieveAdminView(ContentPageBaseView, NewsMixinView, generics.RetrieveUpdateDestroyAPIView): + """Retrieve method to get content pages""" + + lookup_field = None + permission_classes = (permissions.IsAdminUser,) + serializer_class = NewsDetailSerializer + queryset = News.objects.all() diff --git a/apps/news/filters.py b/apps/news/filters.py index a56463c5..7a2e4382 100644 --- a/apps/news/filters.py +++ b/apps/news/filters.py @@ -24,6 +24,8 @@ class NewsListFilterSet(filters.FilterSet): state = filters.NumberFilter() + state__in = filters.CharFilter(method='by_states_list') + SORT_BY_CREATED_CHOICE = "created" SORT_BY_START_CHOICE = "start" SORT_BY_CHOICES = ( @@ -51,9 +53,13 @@ class NewsListFilterSet(filters.FilterSet): if value not in EMPTY_VALUES: if len(value) < 3: raise ValidationError({'detail': _('Type at least 3 characters to search please.')}) - return queryset.trigram_search(value) + return queryset.es_search(value, relevance_order='ordering' not in self.request.query_params) return queryset + def by_states_list(self, queryset, name, value): + states = value.split('__') + return queryset.filter(state__in=states) + def in_tags(self, queryset, name, value): tags = value.split('__') return queryset.filter(tags__value__in=tags) diff --git a/apps/news/models.py b/apps/news/models.py index 983cc9c2..3a529cc4 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -1,6 +1,7 @@ """News app models.""" import uuid +import elasticsearch_dsl from django.conf import settings from django.contrib.contenttypes import fields as generic from django.contrib.contenttypes.models import ContentType @@ -127,14 +128,21 @@ class NewsQuerySet(TranslationQuerysetMixin): return self.exclude(models.Q(publication_date__isnull=True) | models.Q(publication_time__isnull=True)). \ filter(models.Q(models.Q(end__gte=now) | models.Q(end__isnull=True)), - state__in=self.model.PUBLISHED_STATES, publication_date__lte=date_now, - publication_time__lte=time_now) + state__in=self.model.PUBLISHED_STATES)\ + .annotate(visible_now=Case( + When(publication_date__gt=date_now, then=False), + When(Q(publication_date=date_now) & Q(publication_time__gt=time_now), then=False), + default=True, + output_field=models.BooleanField() + ))\ + .exclude(visible_now=False) # todo: filter by best score # todo: filter by country? def should_read(self, news, user): return self.model.objects.exclude(pk=news.pk).published(). \ annotate_in_favorites(user). \ + filter(country=news.country). \ with_base_related().by_type(news.news_type).distinct().order_by('?') def same_theme(self, news, user): @@ -159,8 +167,41 @@ class NewsQuerySet(TranslationQuerysetMixin): def by_locale(self, locale): return self.filter(title__icontains=locale) + def es_search(self, search_value: str, relevance_order=True): + from search_indexes.documents import NewsDocument + from search_indexes.utils import OBJECT_FIELD_PROPERTIES + search_value = search_value.lower() + search_fields = ('description', 'title', 'subtitle') + field_to_boost = { + 'title': 3.0, + 'subtitle': 2.0, + 'description': 1.0, + } + search_keys = {} + for field in search_fields: + for locale in OBJECT_FIELD_PROPERTIES.keys(): + search_keys.update({f'{field}.{locale}': field_to_boost[field]}) + _query = None + for key, boost in search_keys.items(): + if _query is None: + _query = elasticsearch_dsl.Q('match', **{key: {'query': search_value, 'fuzziness': 'auto:2,5', + 'boost': boost}}) + else: + _query |= elasticsearch_dsl.Q('match', **{key: {'query': search_value, 'fuzziness': 'auto:2,5', + 'boost': boost, + }}) + _query |= elasticsearch_dsl.Q('wildcard', **{key: {'value': f'*{search_value}*', 'boost': boost + 30}}) + search = NewsDocument.search().query('bool', should=_query)[0:10000].execute() + ids = [result.meta.id for result in search] + qs = self.filter(id__in=ids) + if relevance_order: + ids_order = enumerate(ids) + preserved = Case(*[When(pk=pk, then=pos) for pos, pk in ids_order]) + qs = qs.order_by(preserved) + return qs + def trigram_search(self, search_value: str): - """Search with mistakes by name or last name.""" + """Search with mistakes by description or title or subtitle.""" return self.annotate( description_str=Cast('description', models.TextField()), title_str=Cast('title', models.TextField()), diff --git a/apps/news/serializers.py b/apps/news/serializers.py index 564cd2c9..f88c79f0 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -207,7 +207,8 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer): """News back office base serializer.""" is_published = serializers.BooleanField(source='is_publish', read_only=True) descriptions = serializers.ListField(required=False) - agenda = AgendaSerializer() + agenda = AgendaSerializer(required=False, allow_null=True) + state_display = serializers.CharField(source='get_state_display', read_only=True) class Meta(NewsBaseSerializer.Meta): """Meta class.""" @@ -226,7 +227,9 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer): 'created', 'modified', 'descriptions', - 'agenda' + 'agenda', + 'state', + 'state_display', ) extra_kwargs = { 'created': {'read_only': True}, @@ -234,6 +237,8 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer): 'duplication_date': {'read_only': True}, 'locale_to_description_is_active': {'allow_null': False}, 'must_of_the_week': {'read_only': True}, + # 'state': {'read_only': True}, + 'state_display': {'read_only': True}, } def validate(self, attrs): @@ -299,7 +304,7 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer): slugs_set = set(slugs_list) if models.News.objects.filter( slugs__values__contains=list(slugs.values()) - ).exists() or len(slugs_list) != len(slugs_set): + ).exclude(pk=instance.pk).exists() or len(slugs_list) != len(slugs_set): raise serializers.ValidationError({'slugs': _('Slug should be unique')}) agenda_data = validated_data.get('agenda') diff --git a/apps/news/views.py b/apps/news/views.py index 5429b4a7..4f0adc1e 100644 --- a/apps/news/views.py +++ b/apps/news/views.py @@ -1,5 +1,6 @@ """News app views.""" from django.conf import settings +from django.http import Http404 from django.shortcuts import get_object_or_404 from django.utils import translation from rest_framework import generics, permissions, response @@ -40,8 +41,14 @@ class NewsMixinView: return qs def get_object(self): - return self.get_queryset() \ - .filter(slugs__values__contains=[self.kwargs['slug']]).first() + instance = self.get_queryset().filter( + slugs__values__contains=[self.kwargs['slug']] + ).first() + + if instance is None: + raise Http404 + + return instance class NewsListView(NewsMixinView, generics.ListAPIView): diff --git a/apps/notification/migrations/0011_auto_20200124_1351.py b/apps/notification/migrations/0011_auto_20200124_1351.py new file mode 100644 index 00000000..5b86c7ec --- /dev/null +++ b/apps/notification/migrations/0011_auto_20200124_1351.py @@ -0,0 +1,39 @@ +# Generated by Django 2.2.7 on 2020-01-24 13:51 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('notification', '0010_auto_20191231_0135'), + ] + + operations = [ + migrations.AddField( + model_name='subscribe', + name='old_subscriber_id', + field=models.PositiveIntegerField(null=True, verbose_name='Old subscriber id'), + ), + migrations.AddField( + model_name='subscribe', + name='old_subscription_type_id', + field=models.PositiveIntegerField(null=True, verbose_name='Old subscription type id'), + ), + migrations.AlterField( + model_name='subscribe', + name='subscribe_date', + field=models.DateTimeField(blank=True, default=None, null=True, verbose_name='Last subscribe date'), + ), + migrations.AlterField( + model_name='subscribe', + name='subscriber', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='notification.Subscriber'), + ), + migrations.AlterField( + model_name='subscribe', + name='subscription_type', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='notification.SubscriptionType'), + ), + ] diff --git a/apps/notification/migrations/0012_remove_subscribe_subscribe_date.py b/apps/notification/migrations/0012_remove_subscribe_subscribe_date.py new file mode 100644 index 00000000..cc805e4b --- /dev/null +++ b/apps/notification/migrations/0012_remove_subscribe_subscribe_date.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.7 on 2020-01-27 16:37 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('notification', '0011_auto_20200124_1351'), + ] + + operations = [ + migrations.RemoveField( + model_name='subscribe', + name='subscribe_date', + ), + ] diff --git a/apps/notification/models.py b/apps/notification/models.py index e0855c62..a176034e 100644 --- a/apps/notification/models.py +++ b/apps/notification/models.py @@ -2,6 +2,7 @@ from django.conf import settings from django.db import models +from django.db.models import F from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ @@ -10,6 +11,7 @@ from location.models import Country from notification.tasks import send_unsubscribe_email from utils.methods import generate_string_code from utils.models import ProjectBaseMixin, TJSONField, TranslatedFieldsMixin +from notification.tasks import send_unsubscribe_email class SubscriptionType(ProjectBaseMixin, TranslatedFieldsMixin): @@ -133,7 +135,16 @@ class Subscriber(ProjectBaseMixin): def unsubscribe(self, query: dict): """Unsubscribe user.""" - self.subscribe_set.update(unsubscribe_date=now()) + + self.subscribe_set.update( + unsubscribe_date=now(), + old_subscriber_id=F('subscriber_id'), + old_subscription_type_id=F('subscription_type_id') + ) + self.subscribe_set.update( + subscriber_id=None, + subscription_type_id=None + ) if settings.USE_CELERY: send_unsubscribe_email.delay(self.email) @@ -159,6 +170,10 @@ class Subscriber(ProjectBaseMixin): def active_subscriptions(self): return self.subscription_types.exclude(subscriber__subscribe__unsubscribe_date__isnull=False) + @property + def subscription_history(self): + return Subscribe.objects.subscription_history(self.pk) + class SubscribeQuerySet(models.QuerySet): @@ -166,18 +181,26 @@ class SubscribeQuerySet(models.QuerySet): """Fetches active subscriptions.""" return self.exclude(unsubscribe_date__isnull=not switcher) + def subscription_history(self, subscriber_id: int): + return self.filter(old_subscriber_id=subscriber_id) + class Subscribe(ProjectBaseMixin): """Subscribe model.""" - - subscribe_date = models.DateTimeField(_('Last subscribe date'), blank=True, null=True, default=now) unsubscribe_date = models.DateTimeField(_('Last unsubscribe date'), blank=True, null=True, default=None) - subscriber = models.ForeignKey(Subscriber, on_delete=models.CASCADE) - subscription_type = models.ForeignKey(SubscriptionType, on_delete=models.CASCADE) + subscriber = models.ForeignKey(Subscriber, on_delete=models.CASCADE, null=True) + subscription_type = models.ForeignKey(SubscriptionType, on_delete=models.CASCADE, null=True) + + old_subscriber_id = models.PositiveIntegerField(_("Old subscriber id"), null=True) + old_subscription_type_id = models.PositiveIntegerField(_("Old subscription type id"), null=True) objects = SubscribeQuerySet.as_manager() + @property + def subscribe_date(self): + return self.created + class Meta: """Meta class.""" diff --git a/apps/notification/serializers/common.py b/apps/notification/serializers/common.py index 869879cb..282a763c 100644 --- a/apps/notification/serializers/common.py +++ b/apps/notification/serializers/common.py @@ -40,6 +40,7 @@ class CreateAndUpdateSubscribeSerializer(serializers.ModelSerializer): model = models.Subscriber fields = ( + 'id', 'email', 'subscription_types', 'link_to_unsubscribe', @@ -54,7 +55,7 @@ class CreateAndUpdateSubscribeSerializer(serializers.ModelSerializer): user = request.user # validate email - email = attrs.get('send_to') + email = attrs.pop('send_to') if attrs.get('email'): email = attrs.get('email') @@ -95,6 +96,14 @@ class CreateAndUpdateSubscribeSerializer(serializers.ModelSerializer): return subscriber + def update(self, instance, validated_data): + if settings.USE_CELERY: + send_subscribes_update_email.delay(instance.pk) + else: + send_subscribes_update_email(instance.pk) + + return super().update(instance, validated_data) + class UpdateSubscribeSerializer(serializers.ModelSerializer): """Update with code Subscribe serializer.""" @@ -141,14 +150,27 @@ class UpdateSubscribeSerializer(serializers.ModelSerializer): class SubscribeObjectSerializer(serializers.ModelSerializer): - """Subscribe serializer.""" + """Subscription type serializer.""" + + subscription_type = serializers.SerializerMethodField() class Meta: """Meta class.""" - model = models.Subscriber - fields = ('subscriber',) - read_only_fields = ('subscribe_date', 'unsubscribe_date',) + model = models.Subscribe + fields = ( + 'subscribe_date', + 'unsubscribe_date', + 'subscription_type' + ) + extra_kwargs = { + 'subscribe_date': {'read_only': True}, + } + + def get_subscription_type(self, instance): + return SubscriptionTypeSerializer( + models.SubscriptionType.objects.get(pk=instance.old_subscription_type_id) + ).data class SubscribeSerializer(serializers.ModelSerializer): @@ -156,6 +178,7 @@ class SubscribeSerializer(serializers.ModelSerializer): email = serializers.EmailField(required=False, source='send_to') subscription_types = SubscriptionTypeSerializer(source='active_subscriptions', read_only=True, many=True) + history = SubscribeObjectSerializer(source='subscription_history', many=True) class Meta: """Meta class.""" @@ -165,4 +188,16 @@ class SubscribeSerializer(serializers.ModelSerializer): 'email', 'subscription_types', 'link_to_unsubscribe', + 'history', ) + + +class UnsubscribeSerializer(serializers.ModelSerializer): + email = serializers.EmailField(read_only=True, required=False, source='send_to') + subscription_types = SubscriptionTypeSerializer(source='active_subscriptions', read_only=True, many=True) + + class Meta: + """Meta class.""" + + model = models.Subscriber + fields = SubscribeSerializer.Meta.fields diff --git a/apps/notification/tasks.py b/apps/notification/tasks.py index 790c9650..cea15287 100644 --- a/apps/notification/tasks.py +++ b/apps/notification/tasks.py @@ -3,11 +3,11 @@ from datetime import datetime from celery import shared_task from django.conf import settings from django.core.mail import send_mail -from django.utils.translation import gettext_lazy as _ from django.template.loader import get_template, render_to_string from main.models import SiteSettings from notification import models +from django.utils.translation import gettext_lazy as _ @shared_task diff --git a/apps/notification/views/common.py b/apps/notification/views/common.py index a0534d13..39fa55f3 100644 --- a/apps/notification/views/common.py +++ b/apps/notification/views/common.py @@ -1,6 +1,6 @@ """Notification app common views.""" 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 notification import models @@ -15,6 +15,18 @@ class CreateSubscribeView(generics.CreateAPIView): permission_classes = (permissions.AllowAny,) serializer_class = serializers.CreateAndUpdateSubscribeSerializer + def create(self, request, *args, **kwargs): + data = request.data + instance = None + if 'email' in request.data: + # we shouldn't create new subscriber if we have one + instance = models.Subscriber.objects.filter(email=request.data['email']).first() + serializer = self.get_serializer(data=data) if instance is None else self.get_serializer(instance, data=data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + class UpdateSubscribeView(generics.UpdateAPIView): """Subscribe info view.""" @@ -44,20 +56,7 @@ class SubscribeInfoAuthUserView(generics.RetrieveAPIView): lookup_field = None def get_object(self): - user = self.request.user - - subscriber = models.Subscriber.objects.filter(user=user).first() - - if subscriber is None: - subscriber = models.Subscriber.objects.make_subscriber( - email=user.email, user=user, ip_address=get_user_ip(self.request), - country_code=self.request.country_code, locale=self.request.locale - ) - - else: - return get_object_or_404(models.Subscriber, user=user) - - return subscriber + return get_object_or_404(models.Subscriber, user=self.request.user) class UnsubscribeView(generics.UpdateAPIView): @@ -69,7 +68,7 @@ class UnsubscribeView(generics.UpdateAPIView): queryset = models.Subscriber.objects.all() serializer_class = serializers.SubscribeSerializer - def patch(self, request, *args, **kw): + def put(self, request, *args, **kw): obj = self.get_object() obj.unsubscribe(request.query_params) serializer = self.get_serializer(instance=obj) diff --git a/apps/partner/filters.py b/apps/partner/filters.py new file mode 100644 index 00000000..e9b32f5f --- /dev/null +++ b/apps/partner/filters.py @@ -0,0 +1,23 @@ +"""Partner app filters.""" +from django_filters import rest_framework as filters + +from partner.models import Partner + + +class PartnerFilterSet(filters.FilterSet): + """Establishment filter set.""" + + establishment = filters.NumberFilter( + help_text='Allows to get partner list by establishment ID.') + type = filters.ChoiceFilter( + choices=Partner.MODEL_TYPES, + help_text=f'Allows to filter partner list by partner type. ' + f'Enum: {dict(Partner.MODEL_TYPES)}') + + class Meta: + """Meta class.""" + model = Partner + fields = ( + 'establishment', + 'type', + ) diff --git a/apps/partner/views/back.py b/apps/partner/views/back.py index 1033d0ee..7ea440f0 100644 --- a/apps/partner/views/back.py +++ b/apps/partner/views/back.py @@ -1,22 +1,20 @@ -from django_filters.rest_framework import DjangoFilterBackend, filters from rest_framework import generics, permissions +from partner import filters from partner.models import Partner from partner.serializers import back as serializers from utils.permissions import IsEstablishmentManager class PartnerLstView(generics.ListCreateAPIView): - """Partner list create view.""" + """Partner list/create view. + Allows to get partners for current country, or create a new one. + """ queryset = Partner.objects.all() serializer_class = serializers.BackPartnerSerializer pagination_class = None permission_classes = [permissions.IsAdminUser | IsEstablishmentManager] - filter_backends = (DjangoFilterBackend,) - filterset_fields = ( - 'establishment', - 'type', - ) + filter_class = filters.PartnerFilterSet class PartnerRUDView(generics.RetrieveUpdateDestroyAPIView): diff --git a/apps/search_indexes/documents/news.py b/apps/search_indexes/documents/news.py index 6e287b0a..4f7998d3 100644 --- a/apps/search_indexes/documents/news.py +++ b/apps/search_indexes/documents/news.py @@ -7,7 +7,7 @@ from json import dumps NewsIndex = Index(settings.ELASTICSEARCH_INDEX_NAMES.get(__name__, 'news')) -NewsIndex.settings(number_of_shards=1, number_of_replicas=1) +NewsIndex.settings(number_of_shards=10, number_of_replicas=1) @NewsIndex.doc_type @@ -71,5 +71,5 @@ class NewsDocument(Document): The related_models option should be used with caution because it can lead in the index to the updating of a lot of items. """ - if isinstance(related_instance, models.NewsType) and hasattr(related_instance, 'news_set'): - return related_instance.news_set.all() + if isinstance(related_instance, models.NewsType) and hasattr(related_instance, 'news'): + return related_instance.news.all() diff --git a/apps/search_indexes/signals.py b/apps/search_indexes/signals.py index 5d9809b9..0a6fb4f6 100644 --- a/apps/search_indexes/signals.py +++ b/apps/search_indexes/signals.py @@ -1,7 +1,6 @@ """Search indexes app signals.""" from django.db.models.signals import post_save from django.dispatch import receiver -from django_elasticsearch_dsl.registries import registry @receiver(post_save) @@ -31,6 +30,7 @@ def update_document(sender, **kwargs): @receiver(post_save) def update_news(sender, **kwargs): from news.models import News + from search_indexes.tasks import es_update app_label = sender._meta.app_label model_name = sender._meta.model_name instance = kwargs['instance'] @@ -42,13 +42,14 @@ def update_news(sender, **kwargs): filter_name = app_label_model_name_to_filter.get((app_label, model_name)) if filter_name: qs = News.objects.filter(**{filter_name: instance}) - for product in qs: - registry.update(product) + for item in qs: + es_update(item) @receiver(post_save) def update_product(sender, **kwargs): from product.models import Product + from search_indexes.tasks import es_update app_label = sender._meta.app_label model_name = sender._meta.model_name instance = kwargs['instance'] @@ -65,4 +66,4 @@ def update_product(sender, **kwargs): if filter_name: qs = Product.objects.filter(**{filter_name: instance}) for product in qs: - registry.update(product) + es_update(product) diff --git a/apps/search_indexes/utils.py b/apps/search_indexes/utils.py index d4c4f68a..52e0adb1 100644 --- a/apps/search_indexes/utils.py +++ b/apps/search_indexes/utils.py @@ -2,7 +2,7 @@ from django_elasticsearch_dsl import fields from utils.models import get_current_locale, get_default_locale -FACET_MAX_RESPONSE = 9999999 # Unlimited +FACET_MAX_RESPONSE = 9999999 # Unlimited ALL_LOCALES_LIST = [ 'hr-HR', diff --git a/apps/transfer/serializers/product.py b/apps/transfer/serializers/product.py index dddc1bcc..a28217d0 100644 --- a/apps/transfer/serializers/product.py +++ b/apps/transfer/serializers/product.py @@ -12,6 +12,7 @@ from django.conf import settings from functools import reduce from gallery.models import Image from translation.models import SiteInterfaceDictionary +from main.models import SiteSettings class WineColorSerializer(TransferSerializerMixin): @@ -496,12 +497,14 @@ class PlateSerializer(TransferSerializerMixin): id = serializers.IntegerField() name = serializers.CharField() vintage = serializers.CharField() + site_id = serializers.IntegerField() class Meta(ProductSerializer.Meta): fields = ( 'id', 'name', 'vintage', + 'site_id', ) def validate(self, attrs): @@ -513,9 +516,9 @@ class PlateSerializer(TransferSerializerMixin): attrs['vintage'] = self.get_vintage_year(attrs.pop('vintage')) attrs['product_type'] = product_type attrs['state'] = self.Meta.model.PUBLISHED - attrs['subtype'] = self.get_product_sub_type(product_type, - self.PRODUCT_SUB_TYPE_INDEX_NAME) + attrs['subtype'] = self.get_product_sub_type(product_type, self.PRODUCT_SUB_TYPE_INDEX_NAME) attrs['slug'] = self.get_slug(name, old_id) + attrs['site'] = self.get_site(attrs.pop('site_id', None)) return attrs def create(self, validated_data): @@ -532,6 +535,12 @@ class PlateSerializer(TransferSerializerMixin): obj.subtypes.add(*[i for i in subtypes if i]) return obj + def get_site(self, old_id: int): + if old_id: + site_qs = SiteSettings.objects.filter(old_id=old_id) + if site_qs.exists(): + return site_qs.first() + class PlateImageSerializer(TransferSerializerMixin): diff --git a/make_data_migration.sh b/make_data_migration.sh index d5edc793..6de3a713 100755 --- a/make_data_migration.sh +++ b/make_data_migration.sh @@ -37,4 +37,8 @@ ./manage.py add_employee ./manage.py add_position ./manage.py add_empl_position -./manage.py update_employee \ No newline at end of file +./manage.py update_employee + +# меню из Dishes(dessert, main_course, starter) и Menus(formulas) +./manage.py transfer --menu +./manage.py add_menus \ No newline at end of file diff --git a/project/settings/development.py b/project/settings/development.py index 3048541b..d5a31178 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -1,8 +1,6 @@ """Development settings.""" from .base import * from .amazon_s3 import * -import sentry_sdk -from sentry_sdk.integrations.django import DjangoIntegration ALLOWED_HOSTS = ['gm.id-east.ru', '95.213.204.126', '0.0.0.0'] @@ -47,11 +45,6 @@ ELASTICSEARCH_INDEX_NAMES = { # ELASTICSEARCH_DSL_AUTOSYNC = False -sentry_sdk.init( - dsn="https://35d9bb789677410ab84a822831c6314f@sentry.io/1729093", - integrations=[DjangoIntegration()] -) - # DATABASE DATABASES.update({ diff --git a/requirements/base.txt b/requirements/base.txt index 25a0256c..395d7959 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -41,8 +41,6 @@ django-elasticsearch-dsl-drf==0.20.2 elasticsearch==7.1.0 elasticsearch-dsl==7.1.0 -sentry-sdk==0.11.2 - # AMAZON S3 boto3==1.9.238 django-storages==1.7.2