Merge branch 'develop' into 'feature/employees-params'

# Conflicts:
#   apps/establishment/serializers/back.py
#   apps/establishment/views/back.py
This commit is contained in:
Ruslan Stepanov 2020-01-27 17:04:15 +00:00
commit 1d534fe1a8
56 changed files with 823 additions and 208 deletions

View File

@ -33,14 +33,8 @@ class AccountBackOfficeFilter(filters.FilterSet):
return queryset return queryset
def search_text(self, queryset, name, value): def search_text(self, queryset, name, value):
queryset = queryset.annotate_vector()
if value not in EMPTY_VALUES: if value not in EMPTY_VALUES:
# search by exact value return queryset.full_text_search(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 return queryset
def by_role_country_code(self, queryset, name, value): def by_role_country_code(self, queryset, name, value):

View File

@ -1,6 +1,6 @@
"""Account models""" """Account models"""
from datetime import datetime from datetime import datetime
from django.contrib.postgres.search import TrigramSimilarity
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AbstractUser, UserManager as BaseUserManager from django.contrib.auth.models import AbstractUser, UserManager as BaseUserManager
from django.core.mail import send_mail 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.http import urlsafe_base64_encode
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from collections import Counter
from authorization.models import Application from authorization.models import Application
from establishment.models import Establishment, EstablishmentSubType from establishment.models import Establishment, EstablishmentSubType
@ -20,7 +21,6 @@ from main.models import SiteSettings
from utils.models import GMTokenGenerator from utils.models import GMTokenGenerator
from utils.models import ImageMixin, ProjectBaseMixin, PlatformMixin from utils.models import ImageMixin, ProjectBaseMixin, PlatformMixin
from utils.tokens import GMRefreshToken from utils.tokens import GMRefreshToken
from django.contrib.postgres.search import SearchVector
from phonenumber_field.modelfields import PhoneNumberField from phonenumber_field.modelfields import PhoneNumberField
@ -84,26 +84,13 @@ class Role(ProjectBaseMixin):
objects = RoleQuerySet.as_manager() objects = RoleQuerySet.as_manager()
@classmethod
def role_names(cls):
return [role_name for role_name in dict(cls.ROLE_CHOICES).values()]
@classmethod @classmethod
def role_types(cls): def role_types(cls):
roles = [] roles = []
for role_id, role in dict(cls.ROLE_CHOICES).items(): for role, display_name in dict(cls.ROLE_CHOICES).items():
roles.append({'id': role_id, 'name': role}) roles.append({'role_name': role, 'role_counter': display_name, 'role': 0})
return roles 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): class UserManager(BaseUserManager):
"""Extended manager for User model.""" """Extended manager for User model."""
@ -124,6 +111,14 @@ class UserManager(BaseUserManager):
class UserQuerySet(models.QuerySet): class UserQuerySet(models.QuerySet):
"""Extended queryset for User model.""" """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): def active(self, switcher=True):
"""Filter only active users.""" """Filter only active users."""
return self.filter(is_active=switcher) return self.filter(is_active=switcher)
@ -150,15 +145,56 @@ class UserQuerySet(models.QuerySet):
role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER).first() role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER).first()
return self.by_role(role).filter(userrole__establishment=establishment) return self.by_role(role).filter(userrole__establishment=establishment)
def annotate_vector(self): def full_text_search(self, search_value: str):
"""Full-text search""" return self.annotate(
return self.annotate(vector=SearchVector( username_similarity=models.Case(
'username', models.When(
'first_name', models.Q(username__isnull=False),
'last_name', then=TrigramSimilarity('username', search_value.lower())
'email', ),
'phone', 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): def by_role_country_code(self, country_code: str):
"""Filter by role country code.""" """Filter by role country code."""
@ -403,10 +439,47 @@ class User(AbstractUser):
class UserRoleQueryset(models.QuerySet): class UserRoleQueryset(models.QuerySet):
"""QuerySet for model UserRole.""" """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): def country_admin_role(self):
return self.filter(role__role=self.model.role.field.target_field.model.COUNTRY_ADMIN, return self.filter(role__role=self.model.role.field.target_field.model.COUNTRY_ADMIN,
state=self.model.VALIDATED) 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): class UserRole(ProjectBaseMixin):
"""UserRole model.""" """UserRole model."""

View File

@ -143,9 +143,3 @@ class UserRoleSerializer(serializers.ModelSerializer):
'user', 'user',
'establishment' 'establishment'
] ]
class RoleTabRetrieveSerializer(serializers.Serializer):
"""Serializer for BackOffice role tab."""
role_name = serializers.CharField()
role_counter = serializers.IntegerField()

View File

@ -45,6 +45,7 @@ class RoleBaseSerializer(serializers.ModelSerializer):
role_display = serializers.CharField(source='get_role_display', read_only=True) role_display = serializers.CharField(source='get_role_display', read_only=True)
navigation_bar_permission = NavigationBarPermissionBaseSerializer(read_only=True) navigation_bar_permission = NavigationBarPermissionBaseSerializer(read_only=True)
country_code = serializers.CharField(source='country.code', read_only=True, allow_null=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: class Meta:
"""Meta class.""" """Meta class."""
@ -54,6 +55,7 @@ class RoleBaseSerializer(serializers.ModelSerializer):
'role_display', 'role_display',
'navigation_bar_permission', 'navigation_bar_permission',
'country_code', 'country_code',
'country_name_translated',
] ]
@ -85,6 +87,7 @@ class UserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.User model = models.User
fields = [ fields = [
'id',
'username', 'username',
'first_name', 'first_name',
'last_name', 'last_name',
@ -120,12 +123,6 @@ class UserSerializer(serializers.ModelSerializer):
subscriptions_handler(subscriptions_list, user) subscriptions_handler(subscriptions_list, user)
return 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): def validate_username(self, value):
"""Custom username validation""" """Custom username validation"""
valid = utils_methods.username_validator(username=value) valid = utils_methods.username_validator(username=value)
@ -139,12 +136,13 @@ class UserSerializer(serializers.ModelSerializer):
if 'subscription_types' in validated_data: if 'subscription_types' in validated_data:
subscriptions_list = validated_data.pop('subscription_types') subscriptions_list = validated_data.pop('subscription_types')
new_email = validated_data.get('email')
old_email = instance.email old_email = instance.email
instance = super().update(instance, validated_data) 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_confirmed = False
instance.email = old_email instance.email = old_email
instance.unconfirmed_email = validated_data['email'] instance.unconfirmed_email = new_email
instance.save() instance.save()
# Send verification link on user email for change email address # Send verification link on user email for change email address
if settings.USE_CELERY: if settings.USE_CELERY:
@ -193,6 +191,7 @@ class UserShortSerializer(UserSerializer):
'id', 'id',
'fullname', 'fullname',
'email', 'email',
'username',
] ]

View File

@ -7,8 +7,7 @@ app_name = 'account'
urlpatterns = [ urlpatterns = [
path('role/', views.RoleListView.as_view(), name='role-list-create'), path('role/', views.RoleListView.as_view(), name='role-list-create'),
path('role/types/', views.RoleChoiceListView.as_view(), name='role-type-list'), path('role/types/', views.RoleTypeRetrieveView.as_view(), name='role-types'),
path('role/tab/', views.RoleTabRetrieveView.as_view(), name='role-tab'),
path('user-role/', views.UserRoleListView.as_view(), name='user-role-list-create'), 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.UserListView.as_view(), name='user-create-list'),
path('user/<int:id>/', views.UserRUDView.as_view(), name='user-rud'), path('user/<int:id>/', views.UserRUDView.as_view(), name='user-rud'),

View File

@ -18,39 +18,18 @@ class RoleListView(generics.ListCreateAPIView):
filter_class = filters.RoleListFilter filter_class = filters.RoleListFilter
class RoleChoiceListView(generics.GenericAPIView): class RoleTypeRetrieveView(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):
permission_classes = [permissions.IsAdminUser] permission_classes = [permissions.IsAdminUser]
def get_queryset(self): def get(self, request, *args, **kwargs):
"""Overridden get_queryset method.""" """Implement GET-method"""
additional_filters = {} country_code = None
if (self.request.user.userrole_set.country_admin_role().exists() and if (self.request.user.userrole_set.country_admin_role().exists() and
hasattr(self.request, 'country_code')): hasattr(self.request, 'country_code')):
additional_filters.update({'country__code': self.request.country_code}) 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})
data = models.UserRole.objects.aggregate_role_counter(country_code)
return Response(data, status=status.HTTP_200_OK) return Response(data, status=status.HTTP_200_OK)
@ -61,7 +40,6 @@ class UserRoleListView(generics.ListCreateAPIView):
class UserListView(generics.ListCreateAPIView): class UserListView(generics.ListCreateAPIView):
"""User list create view.""" """User list create view."""
queryset = User.objects.prefetch_related('roles', 'subscriber')
serializer_class = serializers.BackUserSerializer serializer_class = serializers.BackUserSerializer
permission_classes = (permissions.IsAdminUser,) permission_classes = (permissions.IsAdminUser,)
filter_class = filters.AccountBackOfficeFilter filter_class = filters.AccountBackOfficeFilter
@ -76,6 +54,10 @@ class UserListView(generics.ListCreateAPIView):
'date_joined', 'date_joined',
) )
def get_queryset(self):
"""Overridden get_queryset method."""
return User.objects.with_extend_related()
class UserRUDView(generics.RetrieveUpdateDestroyAPIView): class UserRUDView(generics.RetrieveUpdateDestroyAPIView):
"""User RUD view.""" """User RUD view."""

View File

@ -17,7 +17,7 @@ from utils.views import JWTGenericViewMixin
# User views # User views
class UserRetrieveUpdateView(generics.RetrieveUpdateAPIView): class UserRetrieveUpdateView(generics.RetrieveUpdateDestroyAPIView):
"""User update view.""" """User update view."""
serializer_class = serializers.UserSerializer serializer_class = serializers.UserSerializer
queryset = models.User.objects.active() queryset = models.User.objects.active()
@ -25,6 +25,10 @@ class UserRetrieveUpdateView(generics.RetrieveUpdateAPIView):
def get_object(self): def get_object(self):
return self.request.user 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): class ChangePasswordView(generics.GenericAPIView):
"""Change password view""" """Change password view"""

View File

@ -45,10 +45,8 @@ class Advertisement(ProjectBaseMixin):
url = models.URLField(verbose_name=_('Ad URL')) url = models.URLField(verbose_name=_('Ad URL'))
block_level = models.CharField(verbose_name=_('Block level'), max_length=10, blank=True, null=True) block_level = models.CharField(verbose_name=_('Block level'), max_length=10, blank=True, null=True)
target_languages = models.ManyToManyField(Language) target_languages = models.ManyToManyField(Language)
start = models.DateTimeField(null=True, start = models.DateTimeField(null=True, verbose_name=_('start'))
verbose_name=_('start')) end = models.DateTimeField(blank=True, null=True, default=None, verbose_name=_('end'))
end = models.DateTimeField(blank=True, null=True, default=None,
verbose_name=_('end'))
sites = models.ManyToManyField('main.SiteSettings', sites = models.ManyToManyField('main.SiteSettings',
related_name='advertisements', related_name='advertisements',
verbose_name=_('site')) verbose_name=_('site'))

View File

@ -8,6 +8,7 @@ from .common import common_urlpatterns
app_name = 'advertisements' app_name = 'advertisements'
urlpatterns = [ urlpatterns = [
path('', views.AdvertisementPageTypeMobileListView.as_view(), name='list'),
path('<page_type>/', views.AdvertisementPageTypeMobileListView.as_view(), name='list'), path('<page_type>/', views.AdvertisementPageTypeMobileListView.as_view(), name='list'),
] ]

View File

@ -7,6 +7,7 @@ from .common import common_urlpatterns
app_name = 'advertisements' app_name = 'advertisements'
urlpatterns = [ urlpatterns = [
path('', views.AdvertisementPageTypeWebListView.as_view(), name='list'),
path('<page_type>/', views.AdvertisementPageTypeWebListView.as_view(), name='list'), path('<page_type>/', views.AdvertisementPageTypeWebListView.as_view(), name='list'),
] ]

View File

@ -24,6 +24,10 @@ class AdvertisementPageTypeListView(AdvertisementBaseView, generics.ListAPIView)
def get_queryset(self): def get_queryset(self):
"""Overridden get queryset method.""" """Overridden get queryset method."""
product_type = self.kwargs.get('page_type') product_type = self.kwargs.get('page_type')
if product_type is None:
product_type = 'mobile'
qs = super(AdvertisementPageTypeListView, self).get_queryset() qs = super(AdvertisementPageTypeListView, self).get_queryset()
if product_type: if product_type:
return qs.by_page_type(product_type) \ return qs.by_page_type(product_type) \

View File

@ -16,4 +16,3 @@ class AdvertisementPageTypeMobileListView(AdvertisementPageTypeListView):
qs = super().get_queryset().exclude(frequency_percentage__lte=percentage) qs = super().get_queryset().exclude(frequency_percentage__lte=percentage)
qs.update(views_count=F('views_count') + 1) qs.update(views_count=F('views_count') + 1)
return qs return qs

View File

@ -57,6 +57,10 @@ class CollectionQuerySet(RelatedObjectsCountMixin):
"""Returned only published collection""" """Returned only published collection"""
return self.filter(is_publish=True) return self.filter(is_publish=True)
def with_base_related(self):
"""Select relate objects"""
return self.select_related('country')
class Collection(ProjectBaseMixin, CollectionDateMixin, class Collection(ProjectBaseMixin, CollectionDateMixin,
TranslatedFieldsMixin, URLImageMixin): TranslatedFieldsMixin, URLImageMixin):
@ -106,7 +110,7 @@ class Collection(ProjectBaseMixin, CollectionDateMixin,
"""Return list of related objects.""" """Return list of related objects."""
related_objects = [] related_objects = []
# get 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) related_objects.append(related_object)
return related_objects return related_objects

View File

@ -21,7 +21,7 @@ class CollectionViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
def get_queryset(self): def get_queryset(self):
"""Overridden method 'get_queryset'.""" """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: if self.request.country_code:
qs = qs.by_country_code(self.request.country_code) qs = qs.by_country_code(self.request.country_code)
return qs return qs
@ -75,7 +75,7 @@ class CollectionBackOfficeViewSet(mixins.CreateModelMixin,
"""ViewSet for Collection model for BackOffice users.""" """ViewSet for Collection model for BackOffice users."""
permission_classes = (permissions.IsAuthenticated,) permission_classes = (permissions.IsAuthenticated,)
queryset = models.Collection.objects.all() queryset = models.Collection.objects.with_base_related()
filter_backends = [DjangoFilterBackend, OrderingFilter] filter_backends = [DjangoFilterBackend, OrderingFilter]
serializer_class = serializers.CollectionBackOfficeSerializer serializer_class = serializers.CollectionBackOfficeSerializer
bind_object_serializer_class = serializers.CollectionBindObjectSerializer bind_object_serializer_class = serializers.CollectionBindObjectSerializer

View File

@ -8,7 +8,7 @@ from establishment.models import EstablishmentType
class CommentBaseSerializer(serializers.ModelSerializer): class CommentBaseSerializer(serializers.ModelSerializer):
"""Comment serializer""" """Comment serializer"""
user_name = serializers.CharField(read_only=True, nickname = serializers.CharField(read_only=True,
source='user.username') source='user.username')
is_mine = serializers.BooleanField(read_only=True) is_mine = serializers.BooleanField(read_only=True)
profile_pic = serializers.URLField(read_only=True, profile_pic = serializers.URLField(read_only=True,
@ -32,7 +32,7 @@ class CommentBaseSerializer(serializers.ModelSerializer):
'created', 'created',
'text', 'text',
'mark', 'mark',
'user_name', 'nickname',
'user_email', 'user_email',
'profile_pic', 'profile_pic',
'status', 'status',

View File

@ -1,7 +1,8 @@
"""Establishment app filters.""" """Establishment app filters."""
from django.core.validators import EMPTY_VALUES from django.core.validators import EMPTY_VALUES
from django.utils.translation import ugettext_lazy as _ 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 rest_framework.serializers import ValidationError
from establishment import models from establishment import models
@ -67,9 +68,9 @@ class EmployeeBackFilter(filters.FilterSet):
"""Employee filter set.""" """Employee filter set."""
search = filters.CharFilter(method='search_by_name_or_last_name') search = filters.CharFilter(method='search_by_name_or_last_name')
position_id = filters.NumberFilter(method='search_by_actual_position_id') position_id = filters.CharFilter(method='search_by_actual_position_id')
public_mark = filters.NumberFilter(method='search_by_public_mark') public_mark = filters.CharFilter(method='search_by_public_mark')
toque_number = filters.NumberFilter(method='search_by_toque_number') toque_number = filters.CharFilter(method='search_by_toque_number')
username = filters.CharFilter(method='search_by_username_or_name') username = filters.CharFilter(method='search_by_username_or_name')
class Meta: class Meta:
@ -93,19 +94,22 @@ class EmployeeBackFilter(filters.FilterSet):
def search_by_actual_position_id(self, queryset, name, value): def search_by_actual_position_id(self, queryset, name, value):
"""Search by actual position_id.""" """Search by actual position_id."""
if value not in EMPTY_VALUES: 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 return queryset
def search_by_public_mark(self, queryset, name, value): def search_by_public_mark(self, queryset, name, value):
"""Search by establishment public_mark.""" """Search by establishment public_mark."""
if value not in EMPTY_VALUES: 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 return queryset
def search_by_toque_number(self, queryset, name, value): def search_by_toque_number(self, queryset, name, value):
"""Search by establishment toque_number.""" """Search by establishment toque_number."""
if value not in EMPTY_VALUES: 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 return queryset
def search_by_username_or_name(self, queryset, name, value): 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.')}) raise ValidationError({'detail': _('Type at least 3 characters to search please.')})
return queryset.trigram_search(value) return queryset.trigram_search(value)
return queryset 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

View File

@ -1,4 +1,5 @@
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from tqdm import tqdm
from establishment.models import Establishment, Menu, Plate from establishment.models import Establishment, Menu, Plate
from transfer.models import Menus from transfer.models import Menus
@ -10,14 +11,17 @@ class Command(BaseCommand):
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
count = 0 count = 0
menus = Menus.objects.filter(name__isnull=False).exclude(name='') 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( est = Establishment.objects.filter(
old_id=old_menu.establishment_id).first() old_id=old_menu.establishment_id).first()
if est: if est:
menu, _ = Menu.objects.get_or_create( menu, _ = Menu.objects.get_or_create(
category={'en-GB': 'formulas'}, 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( plate, created = Plate.objects.get_or_create(
name={"en-GB": old_menu.name}, name={"en-GB": old_menu.name},

View 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'),
),
]

View 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',
},
),
]

View 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'),
),
]

View 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'),
),
]

View File

@ -6,6 +6,7 @@ from typing import List
import elasticsearch_dsl import elasticsearch_dsl
from django.conf import settings from django.conf import settings
from django.shortcuts import get_object_or_404
from django.contrib.contenttypes import fields as generic from django.contrib.contenttypes import fields as generic
from django.contrib.gis.db.models.functions import Distance from django.contrib.gis.db.models.functions import Distance
from django.contrib.gis.geos import Point 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.search import TrigramDistance, TrigramSimilarity
from django.contrib.postgres.indexes import GinIndex from django.contrib.postgres.indexes import GinIndex
from django.core.exceptions import ValidationError 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 import models
from django.db.models import When, Case, F, ExpressionWrapper, Subquery, Q, Prefetch, Sum from django.db.models import When, Case, F, ExpressionWrapper, Subquery, Q, Prefetch, Sum
from django.utils import timezone from django.utils import timezone
@ -141,8 +142,8 @@ class EstablishmentQuerySet(models.QuerySet):
def with_extended_related(self): def with_extended_related(self):
return self.with_extended_address_related().select_related('establishment_type'). \ return self.with_extended_address_related().select_related('establishment_type'). \
prefetch_related('establishment_subtypes', 'awards', 'schedule', prefetch_related('establishment_subtypes', 'awards', 'schedule',
'phones', 'gallery', 'menu_set', 'menu_set__plate_set', 'phones', 'gallery', 'menu_set', 'menu_set__plates',
'menu_set__plate_set__currency', 'currency'). \ 'menu_set__plates__currency', 'currency'). \
prefetch_actual_employees() prefetch_actual_employees()
def with_type_related(self): def with_type_related(self):
@ -689,8 +690,8 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
plates = self.menu_set.filter( plates = self.menu_set.filter(
models.Q(category={'en-GB': 'formulas'}) models.Q(category={'en-GB': 'formulas'})
).aggregate( ).aggregate(
max=models.Max('plate__price', output_field=models.FloatField()), max=models.Max('plates__price', output_field=models.FloatField()),
min=models.Min('plate__price', output_field=models.FloatField())) min=models.Min('plates__price', output_field=models.FloatField()))
return plates return plates
@property @property
@ -700,8 +701,8 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
models.Q(category={'en-GB': 'main_course'}) | models.Q(category={'en-GB': 'main_course'}) |
models.Q(category={'en-GB': 'dessert'}) models.Q(category={'en-GB': 'dessert'})
).aggregate( ).aggregate(
max=models.Max('plate__price', output_field=models.FloatField()), max=models.Max('plates__price', output_field=models.FloatField()),
min=models.Min('plate__price', output_field=models.FloatField()), min=models.Min('plates__price', output_field=models.FloatField()),
) )
return plates return plates
@ -764,7 +765,7 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
""" """
Return Country id of establishment location 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 @property
def establishment_id(self): def establishment_id(self):
@ -1020,7 +1021,7 @@ class EmployeeQuerySet(models.QuerySet):
), ),
relevance=(F('search_name_similarity') + F('search_exact_match') relevance=(F('search_name_similarity') + F('search_exact_match')
+ F('search_contains_match') + F('search_last_name_similarity')) + 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): def search_by_name_or_last_name(self, value):
"""Search by name or last_name.""" """Search by name or last_name."""
@ -1034,22 +1035,22 @@ class EmployeeQuerySet(models.QuerySet):
Q(establishmentemployee__to_date__isnull=True) 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.""" """Search by position_id."""
return self.filter( return self.search_by_actual_employee().filter(
Q(establishmentemployee__position_id=value), 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.""" """Search by establishment public_mark."""
return self.filter( return self.search_by_actual_employee().filter(
Q(establishmentemployee__establishment__public_mark=value), 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.""" """Search by establishment toque_number."""
return self.filter( return self.search_by_actual_employee().filter(
Q(establishmentemployee__establishment__toque_number=value), Q(establishmentemployee__establishment__toque_number__in=value_list),
) )
def search_by_username_or_name(self, value): def search_by_username_or_name(self, value):
@ -1155,6 +1156,11 @@ class Employee(BaseAttributes):
) )
return image_property 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): class EstablishmentScheduleQuerySet(models.QuerySet):
"""QuerySet for model EstablishmentSchedule""" """QuerySet for model EstablishmentSchedule"""
@ -1208,7 +1214,11 @@ class Plate(TranslatedFieldsMixin, models.Model):
_('currency code'), max_length=250, blank=True, null=True, default=None) _('currency code'), max_length=250, blank=True, null=True, default=None)
menu = models.ForeignKey( 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 @property
def establishment_id(self): def establishment_id(self):
@ -1219,6 +1229,27 @@ class Plate(TranslatedFieldsMixin, models.Model):
verbose_name_plural = _('plates') 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): class Menu(TranslatedFieldsMixin, BaseAttributes):
"""Menu model.""" """Menu model."""
@ -1230,10 +1261,35 @@ class Menu(TranslatedFieldsMixin, BaseAttributes):
establishment = models.ForeignKey( establishment = models.ForeignKey(
'establishment.Establishment', verbose_name=_('establishment'), 'establishment.Establishment', verbose_name=_('establishment'),
on_delete=models.CASCADE) 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: class Meta:
verbose_name = _('menu') verbose_name = _('menu')
verbose_name_plural = _('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): class SocialChoice(models.Model):

View File

@ -14,7 +14,7 @@ from main.models import Currency
from main.serializers import AwardSerializer from main.serializers import AwardSerializer
from timetable.serialziers import ScheduleRUDSerializer from timetable.serialziers import ScheduleRUDSerializer
from utils.decorators import with_base_attributes 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): def phones_handler(phones_list, establishment):

View File

@ -24,6 +24,7 @@ from utils.serializers import (ProjectModelSerializer, TranslatedField,
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class ContactPhonesSerializer(serializers.ModelSerializer): class ContactPhonesSerializer(serializers.ModelSerializer):
"""Contact phone serializer""" """Contact phone serializer"""

View File

@ -30,6 +30,8 @@ urlpatterns = [
name='note-rud'), name='note-rud'),
path('slug/<slug:slug>/admin/', views.EstablishmentAdminView.as_view(), path('slug/<slug:slug>/admin/', views.EstablishmentAdminView.as_view(),
name='establishment-admin-list'), 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/', views.MenuListCreateView.as_view(), name='menu-list'),
path('menus/<int:pk>/', views.MenuRUDView.as_view(), name='menu-rud'), path('menus/<int:pk>/', views.MenuRUDView.as_view(), name='menu-rud'),
path('plates/', views.PlateListCreateView.as_view(), name='plates'), path('plates/', views.PlateListCreateView.as_view(), name='plates'),
@ -47,6 +49,7 @@ urlpatterns = [
path('employees/', views.EmployeeListCreateView.as_view(), name='employees'), path('employees/', views.EmployeeListCreateView.as_view(), name='employees'),
path('employees/search/', views.EmployeesListSearchViews.as_view(), name='employees-search'), 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>/', 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>', path('<int:establishment_id>/employee/<int:employee_id>/position/<int:position_id>',
views.EstablishmentEmployeeCreateView.as_view(), views.EstablishmentEmployeeCreateView.as_view(),
name='employees-establishment-create'), name='employees-establishment-create'),

View File

@ -242,7 +242,7 @@ class EmployeeListCreateView(generics.ListCreateAPIView):
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
filter_class = filters.EmployeeBackFilter filter_class = filters.EmployeeBackFilter
serializer_class = serializers.EmployeeBackSerializers 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): class EmployeesListSearchViews(generics.ListAPIView):
@ -273,6 +273,21 @@ class EmployeeRUDView(generics.RetrieveUpdateDestroyAPIView):
queryset = models.Employee.objects.all().with_back_office_related() 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): class EstablishmentTypeListCreateView(generics.ListCreateAPIView):
"""Establishment type list/create view.""" """Establishment type list/create view."""
serializer_class = serializers.EstablishmentTypeBaseSerializer serializer_class = serializers.EstablishmentTypeBaseSerializer
@ -461,3 +476,18 @@ class EstablishmentAdminView(generics.ListAPIView):
establishment = get_object_or_404( establishment = get_object_or_404(
models.Establishment, slug=self.kwargs['slug']) models.Establishment, slug=self.kwargs['slug'])
return User.objects.establishment_admin(establishment).distinct() 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]

View File

@ -59,6 +59,7 @@ class CropImageSerializer(ImageSerializer):
MinValueValidator(1), MinValueValidator(1),
MaxValueValidator(100)]) MaxValueValidator(100)])
cropped_image = serializers.DictField(read_only=True, allow_null=True) 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): class Meta(ImageSerializer.Meta):
"""Meta class.""" """Meta class."""
@ -71,6 +72,7 @@ class CropImageSerializer(ImageSerializer):
'crop', 'crop',
'quality', 'quality',
'cropped_image', 'cropped_image',
'certain_aspect',
] ]
def validate(self, attrs): def validate(self, attrs):
@ -98,7 +100,8 @@ class CropImageSerializer(ImageSerializer):
x1, y1 = int(crop.split(' ')[0][:-2]), int(crop.split(' ')[1][:-2]) x1, y1 = int(crop.split(' ')[0][:-2]), int(crop.split(' ')[1][:-2])
x2, y2 = x1 + width, y1 + height x2, y2 = x1 + width, y1 + height
crop_params = { 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, 'quality': 100,
'cropbox': f'{x1},{y1},{x2},{y2}' 'cropbox': f'{x1},{y1},{x2},{y2}'
} }

View File

@ -80,22 +80,19 @@ class RegionQuerySet(models.QuerySet):
def without_parent_region(self, switcher: bool = True): def without_parent_region(self, switcher: bool = True):
"""Filter regions by parent region.""" """Filter regions by parent region."""
return self.filter(parent_region__isnull=switcher)\ return self.filter(parent_region__isnull=switcher)
.order_by('name')
def by_region_id(self, region_id): def by_region_id(self, region_id):
"""Filter regions by region id.""" """Filter regions by region id."""
return self.filter(id=region_id)\ return self.filter(id=region_id)
.order_by('name')
def by_sub_region_id(self, sub_region_id): def by_sub_region_id(self, sub_region_id):
"""Filter sub regions by sub region id.""" """Filter sub regions by sub region id."""
return self.filter(parent_region_id=sub_region_id)\ return self.filter(parent_region_id=sub_region_id)
.order_by('name')
def sub_regions_by_region_id(self, region_id): def sub_regions_by_region_id(self, region_id):
"""Filter regions by sub 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): class Region(models.Model):
@ -142,6 +139,9 @@ class CityQuerySet(models.QuerySet):
"""Return establishments by country code""" """Return establishments by country code"""
return self.filter(country__code=code) return self.filter(country__code=code)
def with_base_related(self):
return self.prefetch_related('country', 'region', 'region__country')
class City(models.Model): class City(models.Model):
"""Region model.""" """Region model."""

View File

@ -87,7 +87,8 @@ class CityBaseSerializer(serializers.ModelSerializer):
image_id = serializers.PrimaryKeyRelatedField( image_id = serializers.PrimaryKeyRelatedField(
source='image', source='image',
queryset=gallery_models.Image.objects.all(), queryset=gallery_models.Image.objects.all(),
write_only=True write_only=True,
required=False,
) )
country = CountrySerializer(read_only=True) country = CountrySerializer(read_only=True)

View File

@ -44,7 +44,7 @@ class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView):
def get_queryset(self): def get_queryset(self):
"""Overridden method 'get_queryset'.""" """Overridden method 'get_queryset'."""
qs = models.City.objects.all().annotate(locale_name=KeyTextTransform(get_current_locale(), 'name_translated'))\ 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: if self.request.country_code:
qs = qs.by_country_code(self.request.country_code) qs = qs.by_country_code(self.request.country_code)
return qs return qs

View File

@ -18,13 +18,13 @@ class CountryViewMixin(generics.GenericAPIView):
class RegionViewMixin(generics.GenericAPIView): class RegionViewMixin(generics.GenericAPIView):
"""View Mixin for model Region""" """View Mixin for model Region"""
model = models.Region model = models.Region
queryset = models.Region.objects.all() queryset = models.Region.objects.all().order_by('name', 'code')
class CityViewMixin(generics.GenericAPIView): class CityViewMixin(generics.GenericAPIView):
"""View Mixin for model City""" """View Mixin for model City"""
model = models.City model = models.City
queryset = models.City.objects.all() queryset = models.City.objects.with_base_related()
class AddressViewMixin(generics.GenericAPIView): class AddressViewMixin(generics.GenericAPIView):
@ -101,7 +101,7 @@ class CityListView(CityViewMixin, generics.ListAPIView):
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() qs = super().get_queryset()
if self.request.country_code: 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 return qs

View File

@ -20,8 +20,10 @@ from review.models import Review
from tag.models import Tag from tag.models import Tag
from utils.exceptions import UnprocessableEntityError from utils.exceptions import UnprocessableEntityError
from utils.methods import dictfetchall from utils.methods import dictfetchall
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin, from utils.models import (
TranslatedFieldsMixin, PlatformMixin) ProjectBaseMixin, TJSONField, URLImageMixin,
TranslatedFieldsMixin, PlatformMixin,
)
class Currency(TranslatedFieldsMixin, models.Model): class Currency(TranslatedFieldsMixin, models.Model):
@ -156,10 +158,10 @@ class SiteFeature(ProjectBaseMixin):
published = models.BooleanField(default=False, verbose_name=_('Published')) published = models.BooleanField(default=False, verbose_name=_('Published'))
main = models.BooleanField(default=False, main = models.BooleanField(default=False,
help_text='shows on main page', help_text='shows on main page',
verbose_name=_('Main'),) verbose_name=_('Main'), )
backoffice = models.BooleanField(default=False, backoffice = models.BooleanField(default=False,
help_text='shows on backoffice page', help_text='shows on backoffice page',
verbose_name=_('backoffice'),) verbose_name=_('backoffice'), )
nested = models.ManyToManyField('self', blank=True, symmetrical=False) nested = models.ManyToManyField('self', blank=True, symmetrical=False)
old_id = models.IntegerField(null=True, blank=True) old_id = models.IntegerField(null=True, blank=True)
@ -173,6 +175,12 @@ class SiteFeature(ProjectBaseMixin):
unique_together = ('site_settings', 'feature') 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): class Award(TranslatedFieldsMixin, URLImageMixin, models.Model):
"""Award model.""" """Award model."""
WAITING = 0 WAITING = 0
@ -198,6 +206,8 @@ class Award(TranslatedFieldsMixin, URLImageMixin, models.Model):
old_id = models.IntegerField(null=True, blank=True) old_id = models.IntegerField(null=True, blank=True)
objects = AwardQuerySet.as_manager()
def __str__(self): def __str__(self):
title = 'None' title = 'None'
lang = TranslationSettings.get_solo().default_language lang = TranslationSettings.get_solo().default_language
@ -458,7 +468,7 @@ class Panel(ProjectBaseMixin):
} }
with connections['default'].cursor() as cursor: with connections['default'].cursor() as cursor:
count = self._raw_count(raw) count = self._raw_count(raw)
start = page*page_size start = page * page_size
cursor.execute(*self.set_limits(start, page_size)) cursor.execute(*self.set_limits(start, page_size))
data["count"] = count data["count"] = count
data["next"] = self.get_next_page(count, page, page_size) data["next"] = self.get_next_page(count, page, page_size)
@ -468,7 +478,7 @@ class Panel(ProjectBaseMixin):
return data return data
def get_next_page(self, count, page, page_size): 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: if not 0 <= page <= max_page:
raise exceptions.NotFound('Invalid page.') raise exceptions.NotFound('Invalid page.')
if max_page > page: if max_page > page:

View File

@ -4,8 +4,9 @@ from rest_framework import serializers
from location.serializers import CountrySerializer from location.serializers import CountrySerializer
from main import models from main import models
from establishment.models import Employee
from tag.serializers import TagBackOfficeSerializer from tag.serializers import TagBackOfficeSerializer
from utils.serializers import ProjectModelSerializer, TranslatedField, RecursiveFieldSerializer from utils.serializers import ProjectModelSerializer, RecursiveFieldSerializer, TranslatedField
class FeatureSerializer(serializers.ModelSerializer): 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): class AwardBaseSerializer(serializers.ModelSerializer):
"""Award base serializer.""" """Award base serializer."""
@ -210,6 +220,8 @@ class AwardBaseSerializer(serializers.ModelSerializer):
class AwardSerializer(AwardBaseSerializer): class AwardSerializer(AwardBaseSerializer):
"""Award serializer.""" """Award serializer."""
award_type = AwardTypeBaseSerializer(read_only=True)
class Meta: class Meta:
model = models.Award model = models.Award
fields = AwardBaseSerializer.Meta.fields + ['award_type', ] fields = AwardBaseSerializer.Meta.fields + ['award_type', ]
@ -218,6 +230,8 @@ class AwardSerializer(AwardBaseSerializer):
class BackAwardSerializer(AwardBaseSerializer): class BackAwardSerializer(AwardBaseSerializer):
"""Award serializer.""" """Award serializer."""
award_type = AwardTypeBaseSerializer(read_only=True)
class Meta: class Meta:
model = models.Award model = models.Award
fields = AwardBaseSerializer.Meta.fields + [ 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): class CarouselListSerializer(serializers.ModelSerializer):
"""Serializer for retrieving list of carousel items.""" """Serializer for retrieving list of carousel items."""

View File

@ -8,6 +8,8 @@ app_name = 'main'
urlpatterns = [ urlpatterns = [
path('awards/', views.AwardLstView.as_view(), name='awards-list-create'), path('awards/', views.AwardLstView.as_view(), name='awards-list-create'),
path('awards/<int:id>/', views.AwardRUDView.as_view(), name='awards-rud'), 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('content_type/', views.ContentTypeView.as_view(), name='content_type-list'),
path('sites/', views.SiteListBackOfficeView.as_view(), name='site-list-create'), path('sites/', views.SiteListBackOfficeView.as_view(), name='site-list-create'),
path('site-settings/<subdomain>/', views.SiteSettingsBackOfficeView.as_view(), path('site-settings/<subdomain>/', views.SiteSettingsBackOfficeView.as_view(),

View File

@ -9,4 +9,10 @@ common_urlpatterns = [
path('awards/<int:pk>/', AwardRetrieveView.as_view(), name='awards_retrieve'), path('awards/<int:pk>/', AwardRetrieveView.as_view(), name='awards_retrieve'),
path('carousel/', CarouselListView.as_view(), name='carousel-list'), path('carousel/', CarouselListView.as_view(), name='carousel-list'),
path('determine-location/', DetermineLocation.as_view(), name='determine-location'), 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')
] ]

View File

@ -7,28 +7,54 @@ from rest_framework.response import Response
from main import serializers from main import serializers
from main.serializers.back import PanelSerializer from main.serializers.back import PanelSerializer
from establishment.serializers.back import EmployeeBackSerializers
from establishment.models import Employee
from main import tasks from main import tasks
from main.filters import AwardFilter 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 from main.views import SiteSettingsView, SiteListView
class AwardLstView(generics.ListCreateAPIView): class AwardLstView(generics.ListCreateAPIView):
"""Award list create view.""" """Award list create view."""
queryset = Award.objects.all() queryset = Award.objects.all().with_base_related()
serializer_class = serializers.BackAwardSerializer serializer_class = serializers.BackAwardSerializer
permission_classes = (permissions.IsAdminUser,) permission_classes = (permissions.IsAdminUser,)
filterset_class = AwardFilter 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): class AwardRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Award RUD view.""" """Award RUD view."""
queryset = Award.objects.all() queryset = Award.objects.all().with_base_related()
serializer_class = serializers.BackAwardSerializer serializer_class = serializers.BackAwardSerializer
permission_classes = (permissions.IsAdminUser,) permission_classes = (permissions.IsAdminUser,)
lookup_field = 'id' 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): class ContentTypeView(generics.ListAPIView):
"""ContentType list view""" """ContentType list view"""
queryset = ContentType.objects.all() queryset = ContentType.objects.all()

View File

@ -1,10 +1,14 @@
"""Main app views.""" """Main app views."""
from django.http import Http404 from django.http import Http404
from django.conf import settings
from rest_framework import generics, permissions from rest_framework import generics, permissions
from rest_framework.response import Response from rest_framework.response import Response
from main import methods, models, serializers 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: # class FeatureViewMixin:
@ -42,20 +46,19 @@ from main import methods, models, serializers
# class SiteFeaturesRUDView(SiteFeaturesViewMixin, # class SiteFeaturesRUDView(SiteFeaturesViewMixin,
# generics.RetrieveUpdateDestroyAPIView): # generics.RetrieveUpdateDestroyAPIView):
# """Site features RUD.""" # """Site features RUD."""
from utils.serializers import EmptySerializer
class AwardView(generics.ListAPIView): class AwardView(generics.ListAPIView):
"""Awards list view.""" """Awards list view."""
serializer_class = serializers.AwardSerializer serializer_class = serializers.AwardSerializer
queryset = models.Award.objects.all() queryset = models.Award.objects.all().with_base_related()
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
class AwardRetrieveView(generics.RetrieveAPIView): class AwardRetrieveView(generics.RetrieveAPIView):
"""Award retrieve view.""" """Award retrieve view."""
serializer_class = serializers.AwardSerializer serializer_class = serializers.AwardSerializer
queryset = models.Award.objects.all() queryset = models.Award.objects.all().with_base_related()
permission_classes = (permissions.IsAuthenticatedOrReadOnly,) permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
@ -95,3 +98,57 @@ class DetermineLocation(generics.GenericAPIView):
'country_code': country_code, 'country_code': country_code,
}) })
raise Http404 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()

View File

@ -24,6 +24,8 @@ class NewsListFilterSet(filters.FilterSet):
state = filters.NumberFilter() state = filters.NumberFilter()
state__in = filters.CharFilter(method='by_states_list')
SORT_BY_CREATED_CHOICE = "created" SORT_BY_CREATED_CHOICE = "created"
SORT_BY_START_CHOICE = "start" SORT_BY_START_CHOICE = "start"
SORT_BY_CHOICES = ( SORT_BY_CHOICES = (
@ -51,9 +53,13 @@ class NewsListFilterSet(filters.FilterSet):
if value not in EMPTY_VALUES: if value not in EMPTY_VALUES:
if len(value) < 3: if len(value) < 3:
raise ValidationError({'detail': _('Type at least 3 characters to search please.')}) 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 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): def in_tags(self, queryset, name, value):
tags = value.split('__') tags = value.split('__')
return queryset.filter(tags__value__in=tags) return queryset.filter(tags__value__in=tags)

View File

@ -1,6 +1,7 @@
"""News app models.""" """News app models."""
import uuid import uuid
import elasticsearch_dsl
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes import fields as generic from django.contrib.contenttypes import fields as generic
from django.contrib.contenttypes.models import ContentType 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)). \ return self.exclude(models.Q(publication_date__isnull=True) | models.Q(publication_time__isnull=True)). \
filter(models.Q(models.Q(end__gte=now) | filter(models.Q(models.Q(end__gte=now) |
models.Q(end__isnull=True)), models.Q(end__isnull=True)),
state__in=self.model.PUBLISHED_STATES, publication_date__lte=date_now, state__in=self.model.PUBLISHED_STATES)\
publication_time__lte=time_now) .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 best score
# todo: filter by country? # todo: filter by country?
def should_read(self, news, user): def should_read(self, news, user):
return self.model.objects.exclude(pk=news.pk).published(). \ return self.model.objects.exclude(pk=news.pk).published(). \
annotate_in_favorites(user). \ annotate_in_favorites(user). \
filter(country=news.country). \
with_base_related().by_type(news.news_type).distinct().order_by('?') with_base_related().by_type(news.news_type).distinct().order_by('?')
def same_theme(self, news, user): def same_theme(self, news, user):
@ -159,8 +167,41 @@ class NewsQuerySet(TranslationQuerysetMixin):
def by_locale(self, locale): def by_locale(self, locale):
return self.filter(title__icontains=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): 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( return self.annotate(
description_str=Cast('description', models.TextField()), description_str=Cast('description', models.TextField()),
title_str=Cast('title', models.TextField()), title_str=Cast('title', models.TextField()),

View File

@ -207,7 +207,8 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
"""News back office base serializer.""" """News back office base serializer."""
is_published = serializers.BooleanField(source='is_publish', read_only=True) is_published = serializers.BooleanField(source='is_publish', read_only=True)
descriptions = serializers.ListField(required=False) 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): class Meta(NewsBaseSerializer.Meta):
"""Meta class.""" """Meta class."""
@ -226,7 +227,9 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
'created', 'created',
'modified', 'modified',
'descriptions', 'descriptions',
'agenda' 'agenda',
'state',
'state_display',
) )
extra_kwargs = { extra_kwargs = {
'created': {'read_only': True}, 'created': {'read_only': True},
@ -234,6 +237,8 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
'duplication_date': {'read_only': True}, 'duplication_date': {'read_only': True},
'locale_to_description_is_active': {'allow_null': False}, 'locale_to_description_is_active': {'allow_null': False},
'must_of_the_week': {'read_only': True}, 'must_of_the_week': {'read_only': True},
# 'state': {'read_only': True},
'state_display': {'read_only': True},
} }
def validate(self, attrs): def validate(self, attrs):
@ -299,7 +304,7 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
slugs_set = set(slugs_list) slugs_set = set(slugs_list)
if models.News.objects.filter( if models.News.objects.filter(
slugs__values__contains=list(slugs.values()) 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')}) raise serializers.ValidationError({'slugs': _('Slug should be unique')})
agenda_data = validated_data.get('agenda') agenda_data = validated_data.get('agenda')

View File

@ -1,5 +1,6 @@
"""News app views.""" """News app views."""
from django.conf import settings from django.conf import settings
from django.http import Http404
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils import translation from django.utils import translation
from rest_framework import generics, permissions, response from rest_framework import generics, permissions, response
@ -40,8 +41,14 @@ class NewsMixinView:
return qs return qs
def get_object(self): def get_object(self):
return self.get_queryset() \ instance = self.get_queryset().filter(
.filter(slugs__values__contains=[self.kwargs['slug']]).first() slugs__values__contains=[self.kwargs['slug']]
).first()
if instance is None:
raise Http404
return instance
class NewsListView(NewsMixinView, generics.ListAPIView): class NewsListView(NewsMixinView, generics.ListAPIView):

View 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'),
),
]

View File

@ -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',
),
]

View File

@ -2,6 +2,7 @@
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models
from django.db.models import F
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _ 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 notification.tasks import send_unsubscribe_email
from utils.methods import generate_string_code from utils.methods import generate_string_code
from utils.models import ProjectBaseMixin, TJSONField, TranslatedFieldsMixin from utils.models import ProjectBaseMixin, TJSONField, TranslatedFieldsMixin
from notification.tasks import send_unsubscribe_email
class SubscriptionType(ProjectBaseMixin, TranslatedFieldsMixin): class SubscriptionType(ProjectBaseMixin, TranslatedFieldsMixin):
@ -133,7 +135,16 @@ class Subscriber(ProjectBaseMixin):
def unsubscribe(self, query: dict): def unsubscribe(self, query: dict):
"""Unsubscribe user.""" """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: if settings.USE_CELERY:
send_unsubscribe_email.delay(self.email) send_unsubscribe_email.delay(self.email)
@ -159,6 +170,10 @@ class Subscriber(ProjectBaseMixin):
def active_subscriptions(self): def active_subscriptions(self):
return self.subscription_types.exclude(subscriber__subscribe__unsubscribe_date__isnull=False) 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): class SubscribeQuerySet(models.QuerySet):
@ -166,18 +181,26 @@ class SubscribeQuerySet(models.QuerySet):
"""Fetches active subscriptions.""" """Fetches active subscriptions."""
return self.exclude(unsubscribe_date__isnull=not switcher) 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): class Subscribe(ProjectBaseMixin):
"""Subscribe model.""" """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) unsubscribe_date = models.DateTimeField(_('Last unsubscribe date'), blank=True, null=True, default=None)
subscriber = models.ForeignKey(Subscriber, on_delete=models.CASCADE) subscriber = models.ForeignKey(Subscriber, on_delete=models.CASCADE, null=True)
subscription_type = models.ForeignKey(SubscriptionType, on_delete=models.CASCADE) 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() objects = SubscribeQuerySet.as_manager()
@property
def subscribe_date(self):
return self.created
class Meta: class Meta:
"""Meta class.""" """Meta class."""

View File

@ -40,6 +40,7 @@ class CreateAndUpdateSubscribeSerializer(serializers.ModelSerializer):
model = models.Subscriber model = models.Subscriber
fields = ( fields = (
'id',
'email', 'email',
'subscription_types', 'subscription_types',
'link_to_unsubscribe', 'link_to_unsubscribe',
@ -54,7 +55,7 @@ class CreateAndUpdateSubscribeSerializer(serializers.ModelSerializer):
user = request.user user = request.user
# validate email # validate email
email = attrs.get('send_to') email = attrs.pop('send_to')
if attrs.get('email'): if attrs.get('email'):
email = attrs.get('email') email = attrs.get('email')
@ -95,6 +96,14 @@ class CreateAndUpdateSubscribeSerializer(serializers.ModelSerializer):
return subscriber 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): class UpdateSubscribeSerializer(serializers.ModelSerializer):
"""Update with code Subscribe serializer.""" """Update with code Subscribe serializer."""
@ -141,14 +150,27 @@ class UpdateSubscribeSerializer(serializers.ModelSerializer):
class SubscribeObjectSerializer(serializers.ModelSerializer): class SubscribeObjectSerializer(serializers.ModelSerializer):
"""Subscribe serializer.""" """Subscription type serializer."""
subscription_type = serializers.SerializerMethodField()
class Meta: class Meta:
"""Meta class.""" """Meta class."""
model = models.Subscriber model = models.Subscribe
fields = ('subscriber',) fields = (
read_only_fields = ('subscribe_date', 'unsubscribe_date',) '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): class SubscribeSerializer(serializers.ModelSerializer):
@ -156,6 +178,7 @@ class SubscribeSerializer(serializers.ModelSerializer):
email = serializers.EmailField(required=False, source='send_to') email = serializers.EmailField(required=False, source='send_to')
subscription_types = SubscriptionTypeSerializer(source='active_subscriptions', read_only=True, many=True) subscription_types = SubscriptionTypeSerializer(source='active_subscriptions', read_only=True, many=True)
history = SubscribeObjectSerializer(source='subscription_history', many=True)
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -165,4 +188,16 @@ class SubscribeSerializer(serializers.ModelSerializer):
'email', 'email',
'subscription_types', 'subscription_types',
'link_to_unsubscribe', '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

View File

@ -3,11 +3,11 @@ from datetime import datetime
from celery import shared_task from celery import shared_task
from django.conf import settings from django.conf import settings
from django.core.mail import send_mail 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 django.template.loader import get_template, render_to_string
from main.models import SiteSettings from main.models import SiteSettings
from notification import models from notification import models
from django.utils.translation import gettext_lazy as _
@shared_task @shared_task

View File

@ -1,6 +1,6 @@
"""Notification app common views.""" """Notification app common views."""
from django.shortcuts import get_object_or_404 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 rest_framework.response import Response
from notification import models from notification import models
@ -15,6 +15,18 @@ class CreateSubscribeView(generics.CreateAPIView):
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
serializer_class = serializers.CreateAndUpdateSubscribeSerializer 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): class UpdateSubscribeView(generics.UpdateAPIView):
"""Subscribe info view.""" """Subscribe info view."""
@ -44,20 +56,7 @@ class SubscribeInfoAuthUserView(generics.RetrieveAPIView):
lookup_field = None lookup_field = None
def get_object(self): def get_object(self):
user = self.request.user return get_object_or_404(models.Subscriber, 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
class UnsubscribeView(generics.UpdateAPIView): class UnsubscribeView(generics.UpdateAPIView):
@ -69,7 +68,7 @@ class UnsubscribeView(generics.UpdateAPIView):
queryset = models.Subscriber.objects.all() queryset = models.Subscriber.objects.all()
serializer_class = serializers.SubscribeSerializer serializer_class = serializers.SubscribeSerializer
def patch(self, request, *args, **kw): def put(self, request, *args, **kw):
obj = self.get_object() obj = self.get_object()
obj.unsubscribe(request.query_params) obj.unsubscribe(request.query_params)
serializer = self.get_serializer(instance=obj) serializer = self.get_serializer(instance=obj)

23
apps/partner/filters.py Normal file
View 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',
)

View File

@ -1,22 +1,20 @@
from django_filters.rest_framework import DjangoFilterBackend, filters
from rest_framework import generics, permissions from rest_framework import generics, permissions
from partner import filters
from partner.models import Partner from partner.models import Partner
from partner.serializers import back as serializers from partner.serializers import back as serializers
from utils.permissions import IsEstablishmentManager from utils.permissions import IsEstablishmentManager
class PartnerLstView(generics.ListCreateAPIView): 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() queryset = Partner.objects.all()
serializer_class = serializers.BackPartnerSerializer serializer_class = serializers.BackPartnerSerializer
pagination_class = None pagination_class = None
permission_classes = [permissions.IsAdminUser | IsEstablishmentManager] permission_classes = [permissions.IsAdminUser | IsEstablishmentManager]
filter_backends = (DjangoFilterBackend,) filter_class = filters.PartnerFilterSet
filterset_fields = (
'establishment',
'type',
)
class PartnerRUDView(generics.RetrieveUpdateDestroyAPIView): class PartnerRUDView(generics.RetrieveUpdateDestroyAPIView):

View File

@ -7,7 +7,7 @@ from json import dumps
NewsIndex = Index(settings.ELASTICSEARCH_INDEX_NAMES.get(__name__, 'news')) 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 @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 The related_models option should be used with caution because it can lead in the index
to the updating of a lot of items. to the updating of a lot of items.
""" """
if isinstance(related_instance, models.NewsType) and hasattr(related_instance, 'news_set'): if isinstance(related_instance, models.NewsType) and hasattr(related_instance, 'news'):
return related_instance.news_set.all() return related_instance.news.all()

View File

@ -1,7 +1,6 @@
"""Search indexes app signals.""" """Search indexes app signals."""
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django_elasticsearch_dsl.registries import registry
@receiver(post_save) @receiver(post_save)
@ -31,6 +30,7 @@ def update_document(sender, **kwargs):
@receiver(post_save) @receiver(post_save)
def update_news(sender, **kwargs): def update_news(sender, **kwargs):
from news.models import News from news.models import News
from search_indexes.tasks import es_update
app_label = sender._meta.app_label app_label = sender._meta.app_label
model_name = sender._meta.model_name model_name = sender._meta.model_name
instance = kwargs['instance'] 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)) filter_name = app_label_model_name_to_filter.get((app_label, model_name))
if filter_name: if filter_name:
qs = News.objects.filter(**{filter_name: instance}) qs = News.objects.filter(**{filter_name: instance})
for product in qs: for item in qs:
registry.update(product) es_update(item)
@receiver(post_save) @receiver(post_save)
def update_product(sender, **kwargs): def update_product(sender, **kwargs):
from product.models import Product from product.models import Product
from search_indexes.tasks import es_update
app_label = sender._meta.app_label app_label = sender._meta.app_label
model_name = sender._meta.model_name model_name = sender._meta.model_name
instance = kwargs['instance'] instance = kwargs['instance']
@ -65,4 +66,4 @@ def update_product(sender, **kwargs):
if filter_name: if filter_name:
qs = Product.objects.filter(**{filter_name: instance}) qs = Product.objects.filter(**{filter_name: instance})
for product in qs: for product in qs:
registry.update(product) es_update(product)

View File

@ -2,7 +2,7 @@
from django_elasticsearch_dsl import fields from django_elasticsearch_dsl import fields
from utils.models import get_current_locale, get_default_locale from utils.models import get_current_locale, get_default_locale
FACET_MAX_RESPONSE = 9999999 # Unlimited FACET_MAX_RESPONSE = 9999999 # Unlimited
ALL_LOCALES_LIST = [ ALL_LOCALES_LIST = [
'hr-HR', 'hr-HR',

View File

@ -12,6 +12,7 @@ from django.conf import settings
from functools import reduce from functools import reduce
from gallery.models import Image from gallery.models import Image
from translation.models import SiteInterfaceDictionary from translation.models import SiteInterfaceDictionary
from main.models import SiteSettings
class WineColorSerializer(TransferSerializerMixin): class WineColorSerializer(TransferSerializerMixin):
@ -496,12 +497,14 @@ class PlateSerializer(TransferSerializerMixin):
id = serializers.IntegerField() id = serializers.IntegerField()
name = serializers.CharField() name = serializers.CharField()
vintage = serializers.CharField() vintage = serializers.CharField()
site_id = serializers.IntegerField()
class Meta(ProductSerializer.Meta): class Meta(ProductSerializer.Meta):
fields = ( fields = (
'id', 'id',
'name', 'name',
'vintage', 'vintage',
'site_id',
) )
def validate(self, attrs): def validate(self, attrs):
@ -513,9 +516,9 @@ class PlateSerializer(TransferSerializerMixin):
attrs['vintage'] = self.get_vintage_year(attrs.pop('vintage')) attrs['vintage'] = self.get_vintage_year(attrs.pop('vintage'))
attrs['product_type'] = product_type attrs['product_type'] = product_type
attrs['state'] = self.Meta.model.PUBLISHED attrs['state'] = self.Meta.model.PUBLISHED
attrs['subtype'] = self.get_product_sub_type(product_type, attrs['subtype'] = self.get_product_sub_type(product_type, self.PRODUCT_SUB_TYPE_INDEX_NAME)
self.PRODUCT_SUB_TYPE_INDEX_NAME)
attrs['slug'] = self.get_slug(name, old_id) attrs['slug'] = self.get_slug(name, old_id)
attrs['site'] = self.get_site(attrs.pop('site_id', None))
return attrs return attrs
def create(self, validated_data): def create(self, validated_data):
@ -532,6 +535,12 @@ class PlateSerializer(TransferSerializerMixin):
obj.subtypes.add(*[i for i in subtypes if i]) obj.subtypes.add(*[i for i in subtypes if i])
return obj 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): class PlateImageSerializer(TransferSerializerMixin):

View File

@ -37,4 +37,8 @@
./manage.py add_employee ./manage.py add_employee
./manage.py add_position ./manage.py add_position
./manage.py add_empl_position ./manage.py add_empl_position
./manage.py update_employee ./manage.py update_employee
# меню из Dishes(dessert, main_course, starter) и Menus(formulas)
./manage.py transfer --menu
./manage.py add_menus

View File

@ -1,8 +1,6 @@
"""Development settings.""" """Development settings."""
from .base import * from .base import *
from .amazon_s3 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'] 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 # ELASTICSEARCH_DSL_AUTOSYNC = False
sentry_sdk.init(
dsn="https://35d9bb789677410ab84a822831c6314f@sentry.io/1729093",
integrations=[DjangoIntegration()]
)
# DATABASE # DATABASE
DATABASES.update({ DATABASES.update({

View File

@ -41,8 +41,6 @@ django-elasticsearch-dsl-drf==0.20.2
elasticsearch==7.1.0 elasticsearch==7.1.0
elasticsearch-dsl==7.1.0 elasticsearch-dsl==7.1.0
sentry-sdk==0.11.2
# AMAZON S3 # AMAZON S3
boto3==1.9.238 boto3==1.9.238
django-storages==1.7.2 django-storages==1.7.2