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
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):

View File

@ -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."""

View File

@ -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()

View File

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

View File

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

View File

@ -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."""

View File

@ -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"""

View File

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

View File

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

View File

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

View File

@ -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) \

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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},

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
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):

View File

@ -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):

View File

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

View File

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

View File

@ -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]

View File

@ -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}'
}

View File

@ -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."""

View File

@ -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)

View File

@ -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

View File

@ -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

View File

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

View File

@ -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."""

View File

@ -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(),

View File

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

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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()),

View File

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

View File

@ -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):

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.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."""

View File

@ -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

View File

@ -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

View File

@ -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
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 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):

View File

@ -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()

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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({

View File

@ -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