Merge branch 'develop' into 'feature/employees-params'
# Conflicts: # apps/establishment/serializers/back.py # apps/establishment/views/back.py
This commit is contained in:
commit
1d534fe1a8
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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/<int:id>/', views.UserRUDView.as_view(), name='user-rud'),
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from .common import common_urlpatterns
|
|||
app_name = 'advertisements'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.AdvertisementPageTypeMobileListView.as_view(), name='list'),
|
||||
path('<page_type>/', views.AdvertisementPageTypeMobileListView.as_view(), name='list'),
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from .common import common_urlpatterns
|
|||
app_name = 'advertisements'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.AdvertisementPageTypeWebListView.as_view(), name='list'),
|
||||
path('<page_type>/', views.AdvertisementPageTypeWebListView.as_view(), name='list'),
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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) \
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
24
apps/establishment/migrations/0076_auto_20200123_1115.py
Normal file
24
apps/establishment/migrations/0076_auto_20200123_1115.py
Normal file
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
34
apps/establishment/migrations/0077_menuuploads.py
Normal file
34
apps/establishment/migrations/0077_menuuploads.py
Normal file
|
|
@ -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',
|
||||
},
|
||||
),
|
||||
]
|
||||
18
apps/establishment/migrations/0078_menu_old_id.py
Normal file
18
apps/establishment/migrations/0078_menu_old_id.py
Normal file
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
23
apps/establishment/migrations/0079_auto_20200124_0720.py
Normal file
23
apps/establishment/migrations/0079_auto_20200124_0720.py
Normal file
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ from utils.serializers import (ProjectModelSerializer, TranslatedField,
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ContactPhonesSerializer(serializers.ModelSerializer):
|
||||
"""Contact phone serializer"""
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ urlpatterns = [
|
|||
name='note-rud'),
|
||||
path('slug/<slug:slug>/admin/', views.EstablishmentAdminView.as_view(),
|
||||
name='establishment-admin-list'),
|
||||
path('menus/dishes/', views.MenuDishesListCreateView.as_view(), name='menu-dishes-list'),
|
||||
path('menus/dishes/<int:pk>/', views.MenuDishesRUDView.as_view(), name='menu-dishes-rud'),
|
||||
path('menus/', views.MenuListCreateView.as_view(), name='menu-list'),
|
||||
path('menus/<int:pk>/', 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/<int:pk>/', views.EmployeeRUDView.as_view(), name='employees-rud'),
|
||||
path('employees/<int:pk>/<int:award_id>', views.RemoveAwardView.as_view(), name='employees-award-delete'),
|
||||
path('<int:establishment_id>/employee/<int:employee_id>/position/<int:position_id>',
|
||||
views.EstablishmentEmployeeCreateView.as_view(),
|
||||
name='employees-establishment-create'),
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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}'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ app_name = 'main'
|
|||
urlpatterns = [
|
||||
path('awards/', views.AwardLstView.as_view(), name='awards-list-create'),
|
||||
path('awards/<int:id>/', views.AwardRUDView.as_view(), name='awards-rud'),
|
||||
path('awards/create-and-bind/<int:employee_id>/', 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/<subdomain>/', views.SiteSettingsBackOfficeView.as_view(),
|
||||
|
|
|
|||
|
|
@ -9,4 +9,10 @@ common_urlpatterns = [
|
|||
path('awards/<int:pk>/', 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/<int:pk>/', ContentPageIdRetrieveView.as_view(), name='content-pages-retrieve-id'),
|
||||
path('content-pages/create/', ContentPageAdminView.as_view(), name='content-pages-admin-list'),
|
||||
path('content-pages/slug/<slug:slug>/', ContentPageRetrieveView.as_view(), name='content-pages-retrieve-slug'),
|
||||
path('content-pages/update/slug/<slug:slug>/', ContentPageRetrieveAdminView.as_view(),
|
||||
name='content-pages-admin-retrieve')
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
39
apps/notification/migrations/0011_auto_20200124_1351.py
Normal file
39
apps/notification/migrations/0011_auto_20200124_1351.py
Normal file
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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',
|
||||
),
|
||||
]
|
||||
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
23
apps/partner/filters.py
Normal file
23
apps/partner/filters.py
Normal file
|
|
@ -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',
|
||||
)
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
|
|
@ -38,3 +38,7 @@
|
|||
./manage.py add_position
|
||||
./manage.py add_empl_position
|
||||
./manage.py update_employee
|
||||
|
||||
# меню из Dishes(dessert, main_course, starter) и Menus(formulas)
|
||||
./manage.py transfer --menu
|
||||
./manage.py add_menus
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user