Merge branch 'develop' into feature/roles

# Conflicts:
#	apps/account/models.py
#	apps/collection/views/back.py
#	apps/comment/views/back.py
#	apps/establishment/models.py
#	apps/establishment/views/back.py
#	apps/news/views.py
#	apps/partner/views/back.py
This commit is contained in:
Anatoly 2020-01-30 10:56:46 +03:00
commit 25c63b8097
66 changed files with 5754 additions and 2602 deletions

View File

@ -15,6 +15,8 @@ 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 phonenumber_field.modelfields import PhoneNumberField from phonenumber_field.modelfields import PhoneNumberField
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from collections import Counter
from typing import List
from authorization.models import Application from authorization.models import Application
from establishment.models import Establishment, EstablishmentSubType from establishment.models import Establishment, EstablishmentSubType
@ -475,6 +477,15 @@ class User(AbstractUser):
.distinct() .distinct()
) )
def set_roles(self, ids: List[int]):
"""
Set user roles
:param ids: list of role ids
:return: bool
"""
self.roles.set(Role.objects.filter(id__in=ids))
return self
class UserRoleQueryset(models.QuerySet): class UserRoleQueryset(models.QuerySet):
"""QuerySet for model UserRole.""" """QuerySet for model UserRole."""

View File

@ -19,7 +19,7 @@ class _SiteSettingsSerializer(serializers.ModelSerializer):
class BackUserSerializer(UserSerializer): class BackUserSerializer(UserSerializer):
last_country = _SiteSettingsSerializer(read_only=True) last_country = _SiteSettingsSerializer(read_only=True)
roles = RoleBaseSerializer(many=True, read_only=True) roles = RoleBaseSerializer(many=True)
class Meta(UserSerializer.Meta): class Meta(UserSerializer.Meta):
fields = ( fields = (
@ -115,6 +115,7 @@ class BackDetailUserSerializer(BackUserSerializer):
def create(self, validated_data): def create(self, validated_data):
subscriptions_list = [] subscriptions_list = []
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')
@ -127,11 +128,17 @@ class BackDetailUserSerializer(BackUserSerializer):
def update(self, instance, validated_data): def update(self, instance, validated_data):
subscriptions_list = [] subscriptions_list = []
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')
if 'roles' in validated_data:
roles_ids = [role['id'] for role in validated_data.pop('roles') if 'id' in role]
instance.set_roles(roles_ids)
instance = super().update(instance, validated_data) instance = super().update(instance, validated_data)
subscriptions_handler(subscriptions_list, instance) subscriptions_handler(subscriptions_list, instance)
return instance return instance

View File

@ -8,6 +8,7 @@ from rest_framework import serializers
from rest_framework import validators as rest_validators from rest_framework import validators as rest_validators
from account import models, tasks from account import models, tasks
from account.models import User, Role
from main.serializers.common import NavigationBarPermissionBaseSerializer from main.serializers.common import NavigationBarPermissionBaseSerializer
from notification.models import Subscribe, Subscriber from notification.models import Subscribe, Subscriber
from utils import exceptions as utils_exceptions from utils import exceptions as utils_exceptions
@ -27,7 +28,7 @@ def subscriptions_handler(subscriptions_list, user):
'user': user, 'user': user,
'email': user.email, 'email': user.email,
'ip_address': user.last_ip, 'ip_address': user.last_ip,
'country_code': user.last_country.country.code if user.last_country else None, 'country_code': user.last_country.country.code if user.last_country and user.last_country.country else None,
'locale': user.locale, 'locale': user.locale,
'update_code': generate_string_code(), 'update_code': generate_string_code(),
} }
@ -42,6 +43,7 @@ def subscriptions_handler(subscriptions_list, user):
class RoleBaseSerializer(serializers.ModelSerializer): class RoleBaseSerializer(serializers.ModelSerializer):
"""Serializer for model Role.""" """Serializer for model Role."""
id = serializers.IntegerField()
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)
@ -87,6 +89,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',
@ -122,12 +125,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)
@ -138,29 +135,39 @@ class UserSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data): def update(self, instance, validated_data):
"""Override update method""" """Override update method"""
subscriptions_list = [] subscriptions_list = []
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: request = self.context['request']
instance.email_confirmed = False user = request.user
instance.email = old_email if not user.is_superuser and not user.is_staff and \
instance.unconfirmed_email = validated_data['email'] not user.roles.filter(country__code=request.country_code, role=models.Role.COUNTRY_ADMIN).exists():
instance.save() """
# Send verification link on user email for change email address superuser or country admin changes email immediately!
if settings.USE_CELERY: """
tasks.change_email_address.delay( if new_email and new_email != old_email:
user_id=instance.id, instance.email_confirmed = False
country_code=self.context.get('request').country_code, instance.email = old_email
emails=[validated_data['email'], ]) instance.unconfirmed_email = new_email
else: instance.save()
tasks.change_email_address( # Send verification link on user email for change email address
user_id=instance.id, if settings.USE_CELERY:
country_code=self.context.get('request').country_code, tasks.change_email_address.delay(
emails=[validated_data['email'], ]) user_id=instance.id,
country_code=self.context.get('request').country_code,
emails=[validated_data['email'], ])
else:
tasks.change_email_address(
user_id=instance.id,
country_code=self.context.get('request').country_code,
emails=[validated_data['email'], ])
subscriptions_handler(subscriptions_list, instance) subscriptions_handler(subscriptions_list, instance)
return instance return instance
@ -195,6 +202,7 @@ class UserShortSerializer(UserSerializer):
'id', 'id',
'fullname', 'fullname',
'email', 'email',
'username',
] ]

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

@ -0,0 +1,56 @@
"""Collection app filters."""
from django_filters import rest_framework as filters
from django.core.validators import EMPTY_VALUES
from collection import models
class CollectionFilterSet(filters.FilterSet):
"""Collection filter set."""
establishment_id = filters.NumberFilter(
field_name='establishments__id',
help_text='Establishment id. Allows to filter list of collections by choosen estblishment. '
'Use for Establishment detail\'s sheet to content display within '
'"Collections & Guides" tab.'
)
# "ordering" instead of "o" is for backward compatibility
ordering = filters.OrderingFilter(
# tuple-mapping retains order
fields=(
('rank', 'rank'),
('start', 'start'),
),
help_text='Ordering by fields - rank, start',
)
class Meta:
"""Meta class."""
model = models.Collection
fields = (
'ordering',
'establishment_id',
)
class GuideFilterSet(filters.FilterSet):
"""Guide filter set."""
establishment_id = filters.NumberFilter(
method='by_establishment_id',
help_text='Establishment id. Allows to filter list of guides by choosen establishment. '
'Use for Establishment detail\'s sheet to content display within '
'"Collections & Guides" tab.'
)
class Meta:
"""Meta class."""
model = models.Guide
fields = (
'establishment_id',
)
def by_establishment_id(self, queryset, name, value):
"""Filter by establishment id."""
if value not in EMPTY_VALUES:
return queryset.by_establishment_id(value)
return queryset

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
@ -167,17 +171,26 @@ class GuideQuerySet(models.QuerySet):
"""Return QuerySet with related.""" """Return QuerySet with related."""
return self.select_related('site', ) return self.select_related('site', )
def with_extended_related(self):
"""Return QuerySet with extended related."""
return self.with_base_related().prefetch_related('guideelement_set')
def by_country_id(self, country_id): def by_country_id(self, country_id):
"""Return QuerySet filtered by country code.""" """Return QuerySet filtered by country code."""
return self.filter(country_json__id__contains=country_id) return self.filter(country_json__id__contains=country_id)
def annotate_in_restaurant_section(self): def annotate_in_restaurant_section(self):
"""Annotate flag if GuideElement in RestaurantSectionNode.""" """Annotate flag if GuideElement in RestaurantSectionNode."""
restaurant_guides = models.Subquery(
self.filter(
guideelement__guide_element_type__name='EstablishmentNode',
guideelement__parent__guide_element_type__name='RestaurantSectionNode',
).values_list('id', flat=True).distinct()
)
return self.annotate( return self.annotate(
in_restaurant_section=models.Case( in_restaurant_section=models.Case(
models.When( models.When(
guideelement__guide_element_type__name='EstablishmentNode', id__in=restaurant_guides,
guideelement__parent__guide_element_type__name='RestaurantSectionNode',
then=True), then=True),
default=False, default=False,
output_field=models.BooleanField(default=False) output_field=models.BooleanField(default=False)
@ -186,11 +199,16 @@ class GuideQuerySet(models.QuerySet):
def annotate_in_shop_section(self): def annotate_in_shop_section(self):
"""Annotate flag if GuideElement in ShopSectionNode.""" """Annotate flag if GuideElement in ShopSectionNode."""
shop_guides = models.Subquery(
self.filter(
guideelement__guide_element_type__name='EstablishmentNode',
guideelement__parent__guide_element_type__name='ShopSectionNode',
).values_list('guideelement__id', flat=True).distinct()
)
return self.annotate( return self.annotate(
in_shop_section=models.Case( in_shop_section=models.Case(
models.When( models.When(
guideelement__guide_element_type__name='EstablishmentNode', id__in=shop_guides,
guideelement__parent__guide_element_type__name='ShopSectionNode',
then=True), then=True),
default=False, default=False,
output_field=models.BooleanField(default=False) output_field=models.BooleanField(default=False)
@ -201,37 +219,60 @@ class GuideQuerySet(models.QuerySet):
"""Return QuerySet with annotated field - restaurant_counter.""" """Return QuerySet with annotated field - restaurant_counter."""
return self.annotate_in_restaurant_section().annotate( return self.annotate_in_restaurant_section().annotate(
restaurant_counter=models.Count( restaurant_counter=models.Count(
'guideelement', 'guideelement__establishment',
filter=models.Q(in_restaurant_section=True) & filter=models.Q(in_restaurant_section=True) &
models.Q(guideelement__parent_id__isnull=False), models.Q(guideelement__parent_id__isnull=True),
distinct=True)) distinct=True
)
)
def annotate_shop_counter(self): def annotate_shop_counter(self):
"""Return QuerySet with annotated field - shop_counter.""" """Return QuerySet with annotated field - shop_counter."""
return self.annotate_in_shop_section().annotate( return self.annotate_in_shop_section().annotate(
shop_counter=models.Count( shop_counter=models.Count(
'guideelement', 'guideelement__establishment',
filter=models.Q(in_shop_section=True) & filter=models.Q(in_shop_section=True) &
models.Q(guideelement__parent_id__isnull=False), models.Q(guideelement__parent_id__isnull=True),
distinct=True)) distinct=True
)
)
def annotate_wine_counter(self): def annotate_wine_counter(self):
"""Return QuerySet with annotated field - shop_counter.""" """Return QuerySet with annotated field - shop_counter."""
return self.annotate_in_restaurant_section().annotate( return self.annotate_in_restaurant_section().annotate(
wine_counter=models.Count( wine_counter=models.Count(
'guideelement', 'guideelement__product',
filter=models.Q(guideelement__guide_element_type__name='WineNode') & filter=models.Q(guideelement__guide_element_type__name='WineNode') &
models.Q(guideelement__parent_id__isnull=False), models.Q(guideelement__parent_id__isnull=False),
distinct=True)) distinct=True
)
)
def annotate_present_objects_counter(self): def annotate_present_objects_counter(self):
"""Return QuerySet with annotated field - present_objects_counter.""" """Return QuerySet with annotated field - present_objects_counter."""
return self.annotate_in_restaurant_section().annotate( return (
present_objects_counter=models.Count( self.annotate_restaurant_counter()
'guideelement', .annotate_shop_counter()
filter=models.Q(guideelement__guide_element_type__name__in=['EstablishmentNode', 'WineNode']) & .annotate_wine_counter()
models.Q(guideelement__parent_id__isnull=False), .annotate(
distinct=True)) present_objects_counter=(
models.F('restaurant_counter') +
models.F('shop_counter') +
models.F('wine_counter')
)
)
)
def annotate_counters(self):
return (
self.annotate_restaurant_counter()
.annotate_shop_counter()
.annotate_wine_counter()
.annotate_present_objects_counter()
)
def by_establishment_id(self, establishment_id: int):
return self.filter(guideelement__establishment=establishment_id).distinct()
class Guide(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin): class Guide(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin):

View File

@ -109,6 +109,7 @@ class GuideBaseSerializer(serializers.ModelSerializer):
restaurant_counter = serializers.IntegerField(read_only=True) restaurant_counter = serializers.IntegerField(read_only=True)
shop_counter = serializers.IntegerField(read_only=True) shop_counter = serializers.IntegerField(read_only=True)
wine_counter = serializers.IntegerField(read_only=True) wine_counter = serializers.IntegerField(read_only=True)
present_objects_counter = serializers.IntegerField(read_only=True)
count_objects_during_init = serializers.IntegerField(read_only=True, count_objects_during_init = serializers.IntegerField(read_only=True,
source='count_related_objects') source='count_related_objects')
@ -131,6 +132,7 @@ class GuideBaseSerializer(serializers.ModelSerializer):
'restaurant_counter', 'restaurant_counter',
'shop_counter', 'shop_counter',
'wine_counter', 'wine_counter',
'present_objects_counter',
'count_objects_during_init', 'count_objects_during_init',
] ]
extra_kwargs = { extra_kwargs = {

View File

@ -1,14 +1,11 @@
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics from rest_framework import generics
from rest_framework import mixins, permissions, viewsets from rest_framework import mixins, permissions, viewsets
from rest_framework import status from rest_framework import status
from rest_framework.filters import OrderingFilter
from rest_framework.response import Response from rest_framework.response import Response
from collection import models, serializers from collection import models, serializers, filters, tasks
from collection import tasks
from utils.methods import get_permission_classes from utils.methods import get_permission_classes
from utils.views import BindObjectMixin from utils.views import BindObjectMixin
@ -22,7 +19,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
@ -35,12 +32,7 @@ class GuideBaseView(generics.GenericAPIView):
def get_queryset(self): def get_queryset(self):
"""Overridden get_queryset method.""" """Overridden get_queryset method."""
return models.Guide.objects.with_base_related() \ return models.Guide.objects.with_extended_related().annotate_counters()
.annotate_restaurant_counter() \
.annotate_shop_counter() \
.annotate_wine_counter() \
.annotate_present_objects_counter() \
.distinct()
class GuideFilterBaseView(generics.GenericAPIView): class GuideFilterBaseView(generics.GenericAPIView):
@ -73,17 +65,14 @@ class CollectionBackOfficeViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
BindObjectMixin, BindObjectMixin,
CollectionViewSet): CollectionViewSet):
"""ViewSet for Collection model for BackOffice users.""" """ViewSet for Collections list for BackOffice users and Collection create."""
queryset = models.Collection.objects.all() queryset = models.Collection.objects.with_base_related().order_by('-start')
filter_backends = [DjangoFilterBackend, OrderingFilter] filter_class = filters.CollectionFilterSet
serializer_class = serializers.CollectionBackOfficeSerializer serializer_class = serializers.CollectionBackOfficeSerializer
bind_object_serializer_class = serializers.CollectionBindObjectSerializer bind_object_serializer_class = serializers.CollectionBindObjectSerializer
permission_classes = get_permission_classes() permission_classes = get_permission_classes()
ordering_fields = ('rank', 'start')
ordering = ('-start', )
def perform_binding(self, serializer): def perform_binding(self, serializer):
data = serializer.validated_data data = serializer.validated_data
collection = data.pop('collection') collection = data.pop('collection')
@ -107,7 +96,8 @@ class CollectionBackOfficeViewSet(mixins.CreateModelMixin,
class GuideListCreateView(GuideBaseView, class GuideListCreateView(GuideBaseView,
generics.ListCreateAPIView): generics.ListCreateAPIView):
"""View for Guide model for BackOffice users.""" """View for Guides list for BackOffice users and Guide create."""
filter_class = filters.GuideFilterSet
class GuideFilterCreateView(GuideFilterBaseView, class GuideFilterCreateView(GuideFilterBaseView,

View File

@ -48,6 +48,9 @@ class CommentQuerySet(ContentTypeQuerySetMixin):
qs = self.filter(id__in=tuple(waiting_ids)) qs = self.filter(id__in=tuple(waiting_ids))
return qs return qs
def with_base_related(self):
return self.prefetch_related("content_object").select_related("user", "content_type")
class Comment(ProjectBaseMixin): class Comment(ProjectBaseMixin):
"""Comment model.""" """Comment model."""

View File

@ -1,9 +1,7 @@
"""Common serializers for app comment.""" """Common serializers for app comment."""
from rest_framework import serializers from rest_framework import serializers
import establishment.serializers.common as establishment_serializers
from comment.models import Comment from comment.models import Comment
from establishment.models import EstablishmentType
class CommentBaseSerializer(serializers.ModelSerializer): class CommentBaseSerializer(serializers.ModelSerializer):
@ -22,6 +20,8 @@ class CommentBaseSerializer(serializers.ModelSerializer):
user_email = serializers.CharField(read_only=True, source='user.email') user_email = serializers.CharField(read_only=True, source='user.email')
slug = serializers.CharField(read_only=True, source='content_object.slug')
class Meta: class Meta:
"""Serializer for model Comment""" """Serializer for model Comment"""
model = Comment model = Comment
@ -39,18 +39,29 @@ class CommentBaseSerializer(serializers.ModelSerializer):
'status_display', 'status_display',
'last_ip', 'last_ip',
'content_type', 'content_type',
'content_name' 'content_name',
'slug',
] ]
extra_kwargs = { extra_kwargs = {
# 'status': {'read_only': True}, # 'status': {'read_only': True},
} }
def get_content_type(self, instance: Comment): def get_content_type(self, instance: Comment):
if instance.content_object.establishment_type == EstablishmentType.PRODUCER: import establishment.serializers.common as establishment_serializers
return establishment_serializers.EstablishmentSubTypeBaseSerializer( from establishment.models import EstablishmentType, Establishment
instance.content_object.establishment_subtypes, many=True from product.models import Product
).data from product.serializers import ProductTypeBaseSerializer
return establishment_serializers.EstablishmentTypeBaseSerializer( if isinstance(instance.content_object, Establishment):
instance.content_object.establishment_type if instance.content_object.establishment_type == EstablishmentType.PRODUCER:
).data return establishment_serializers.EstablishmentSubTypeBaseSerializer(
instance.content_object.establishment_subtypes, many=True
).data
return establishment_serializers.EstablishmentTypeBaseSerializer(
instance.content_object.establishment_type
).data
if isinstance(instance.content_object, Product):
return ProductTypeBaseSerializer(
instance.content_object.product_type
).data

View File

@ -8,4 +8,6 @@ app_name = 'comment'
urlpatterns = [ urlpatterns = [
path('', views.CommentLstView.as_view(), name='comment-list-create'), path('', views.CommentLstView.as_view(), name='comment-list-create'),
path('<int:id>/', views.CommentRUDView.as_view(), name='comment-crud'), path('<int:id>/', views.CommentRUDView.as_view(), name='comment-crud'),
path('<str:type>/', views.CommentLstView.as_view(), name='comment-list-by-type-create'),
path('<str:type>/<int:object>', views.CommentLstView.as_view(), name='comment-list-by-type-object-create'),
] ]

View File

@ -8,6 +8,26 @@ from utils.permissions import IsModerator
class CommentLstView(generics.ListCreateAPIView): class CommentLstView(generics.ListCreateAPIView):
"""Comment list create view.""" """Comment list create view."""
def get_queryset(self):
from product.models import Product
from establishment.models import Establishment
allowed = {
"product": Product.__name__.lower(),
"establishment": Establishment.__name__.lower()
}
qs = models.Comment.objects.with_base_related()
if "object" in self.kwargs:
qs = qs.by_object_id(self.kwargs["object"])
if "type" in self.kwargs and self.kwargs["type"] in allowed:
model = allowed[self.kwargs["type"]]
qs = qs.by_content_type(self.kwargs["type"], model)
return qs.order_by('-created')
serializer_class = CommentBaseSerializer serializer_class = CommentBaseSerializer
queryset = models.Comment.objects.all() queryset = models.Comment.objects.all()
permission_classes = get_permission_classes(IsModerator) permission_classes = get_permission_classes(IsModerator)

View File

@ -1,8 +1,7 @@
"""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, Filter from django_filters import rest_framework as filters
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

View File

@ -0,0 +1,40 @@
# Generated by Django 2.2.7 on 2020-01-28 09:04
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('gallery', '0008_merge_20191212_0752'),
('establishment', '0079_auto_20200124_0720'),
]
operations = [
migrations.AlterField(
model_name='menuuploads',
name='file',
field=models.FileField(upload_to='', validators=[django.core.validators.FileExtensionValidator(allowed_extensions=('doc', 'docx', 'pdf'))], verbose_name='File'),
),
migrations.AlterField(
model_name='menuuploads',
name='menu',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='menu_uploads', to='establishment.Menu', verbose_name='menu'),
),
migrations.CreateModel(
name='MenuGallery',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_main', models.BooleanField(default=False, verbose_name='Is the main image')),
('image', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='menu_gallery', to='gallery.Image', verbose_name='image')),
('menu', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='menu_gallery', to='establishment.Menu', verbose_name='menu')),
],
options={
'verbose_name': 'menu gallery',
'verbose_name_plural': 'menu galleries',
'unique_together': {('menu', 'image'), ('menu', 'is_main')},
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2020-01-28 09:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0080_auto_20200128_0904'),
]
operations = [
migrations.AddField(
model_name='menuuploads',
name='title',
field=models.CharField(default='', max_length=255, verbose_name='title'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2020-01-28 12:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0081_menuuploads_title'),
]
operations = [
migrations.AddField(
model_name='establishment',
name='western_name',
field=models.CharField(default='', max_length=255, verbose_name='Western name'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2020-01-28 12:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0082_establishment_western_name'),
]
operations = [
migrations.AddField(
model_name='establishment',
name='instagram',
field=models.URLField(blank=True, default=None, max_length=255, null=True, verbose_name='Instagram URL'),
),
]

View File

@ -0,0 +1,69 @@
# Generated by Django 2.2.7 on 2020-01-29 11:13
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import utils.methods
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('establishment', '0083_establishment_instagram'),
]
operations = [
migrations.CreateModel(
name='MenuFiles',
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(blank=True, default=None, null=True, upload_to=utils.methods.file_path, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=('jpg', 'jpeg', 'png', 'doc', 'docx', 'pdf'))], verbose_name='File')),
('name', models.CharField(default='', max_length=255, verbose_name='name')),
('type', models.CharField(default='', max_length=65, verbose_name='type')),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='menufiles_records_created', to=settings.AUTH_USER_MODEL, verbose_name='created by')),
('modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='menufiles_records_modified', to=settings.AUTH_USER_MODEL, verbose_name='modified by')),
],
options={
'verbose_name': 'menu upload',
'verbose_name_plural': 'menu uploads',
},
),
migrations.RemoveField(
model_name='menuuploads',
name='created_by',
),
migrations.RemoveField(
model_name='menuuploads',
name='menu',
),
migrations.RemoveField(
model_name='menuuploads',
name='modified_by',
),
migrations.AddField(
model_name='menu',
name='name',
field=models.CharField(default='', max_length=255, verbose_name='name'),
),
migrations.AlterField(
model_name='menu',
name='schedule',
field=models.ManyToManyField(blank=True, to='timetable.Timetable', verbose_name='Menu schedule'),
),
migrations.DeleteModel(
name='MenuGallery',
),
migrations.DeleteModel(
name='MenuUploads',
),
migrations.AddField(
model_name='menu',
name='uploads',
field=models.ManyToManyField(blank=True, to='establishment.MenuFiles', verbose_name='Menu files'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2020-01-29 11:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0084_auto_20200129_1113'),
]
operations = [
migrations.AddField(
model_name='menu',
name='price',
field=models.IntegerField(blank=True, default=None, null=True, verbose_name='price'),
),
]

View File

@ -0,0 +1,47 @@
# Generated by Django 2.2.7 on 2020-01-29 13:01
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import utils.models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('establishment', '0085_menu_price'),
]
operations = [
migrations.AlterField(
model_name='menufiles',
name='type',
field=models.CharField(choices=[('image', 'Image'), ('file', 'File')], default='', max_length=65, verbose_name='type'),
),
migrations.CreateModel(
name='MenuDish',
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')),
('name', models.CharField(default='', max_length=255, verbose_name='name')),
('category', utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='category')),
('price', models.IntegerField(blank=True, default=None, null=True, verbose_name='price')),
('signature', models.BooleanField(default=False, verbose_name='signature')),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='menudish_records_created', to=settings.AUTH_USER_MODEL, verbose_name='created by')),
('modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='menudish_records_modified', to=settings.AUTH_USER_MODEL, verbose_name='modified by')),
],
options={
'verbose_name': 'dish',
'verbose_name_plural': 'dishes',
'ordering': ('-created',),
},
),
migrations.AddField(
model_name='menu',
name='dishes',
field=models.ManyToManyField(blank=True, to='establishment.MenuDish', verbose_name='Menu dishes'),
),
]

View File

@ -33,7 +33,7 @@ from utils.methods import transform_into_readable_str
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin, from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
TranslatedFieldsMixin, BaseAttributes, GalleryMixin, TranslatedFieldsMixin, BaseAttributes, GalleryMixin,
IntermediateGalleryModelMixin, HasTagsMixin, IntermediateGalleryModelMixin, HasTagsMixin,
FavoritesMixin, TypeDefaultImageMixin) FavoritesMixin, TypeDefaultImageMixin, FileMixin)
# todo: establishment type&subtypes check # todo: establishment type&subtypes check
@ -541,6 +541,9 @@ class EstablishmentQuerySet(models.QuerySet):
self.filter(id__in=administrator_establishment_ids) self.filter(id__in=administrator_establishment_ids)
) )
def with_contacts(self):
return self.prefetch_related('emails', 'phones')
class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin, class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
TranslatedFieldsMixin, HasTagsMixin, FavoritesMixin): TranslatedFieldsMixin, HasTagsMixin, FavoritesMixin):
@ -552,6 +555,8 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
name = models.CharField(_('name'), max_length=255, default='') name = models.CharField(_('name'), max_length=255, default='')
transliterated_name = models.CharField(default='', max_length=255, transliterated_name = models.CharField(default='', max_length=255,
verbose_name=_('Transliterated name')) verbose_name=_('Transliterated name'))
western_name = models.CharField(default='', max_length=255,
verbose_name=_('Western name'))
index_name = models.CharField(_('Index name'), max_length=255, default='') index_name = models.CharField(_('Index name'), max_length=255, default='')
description = TJSONField(blank=True, null=True, default=None, description = TJSONField(blank=True, null=True, default=None,
verbose_name=_('description'), verbose_name=_('description'),
@ -583,6 +588,8 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
verbose_name=_('Facebook URL')) verbose_name=_('Facebook URL'))
twitter = models.URLField(blank=True, null=True, default=None, max_length=255, twitter = models.URLField(blank=True, null=True, default=None, max_length=255,
verbose_name=_('Twitter URL')) verbose_name=_('Twitter URL'))
instagram = models.URLField(blank=True, null=True, default=None, max_length=255,
verbose_name=_('Instagram URL'))
lafourchette = models.URLField(blank=True, null=True, default=None, max_length=255, lafourchette = models.URLField(blank=True, null=True, default=None, max_length=255,
verbose_name=_('Lafourchette URL')) verbose_name=_('Lafourchette URL'))
guestonline_id = models.PositiveIntegerField(blank=True, verbose_name=_('guestonline id'), guestonline_id = models.PositiveIntegerField(blank=True, verbose_name=_('guestonline id'),
@ -754,7 +761,8 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
now_at_est_tz = datetime.now(tz=self.tz) now_at_est_tz = datetime.now(tz=self.tz)
current_week = now_at_est_tz.weekday() current_week = now_at_est_tz.weekday()
schedule_for_today = self.schedule.filter(weekday=current_week).first() schedule_for_today = self.schedule.filter(weekday=current_week).first()
if schedule_for_today is None or schedule_for_today.opening_time is None or schedule_for_today.ending_time is None: if schedule_for_today is None or schedule_for_today.opening_time is None or schedule_for_today.ending_time is \
None:
return False return False
time_at_est_tz = now_at_est_tz.time() time_at_est_tz = now_at_est_tz.time()
return schedule_for_today.ending_time > time_at_est_tz > schedule_for_today.opening_time return schedule_for_today.ending_time > time_at_est_tz > schedule_for_today.opening_time
@ -766,8 +774,10 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
@property @property
def tags_indexing(self): def tags_indexing(self):
return [{'id': tag.metadata.id, return [{
'label': tag.metadata.label} for tag in self.tags.all()] 'id': tag.metadata.id,
'label': tag.metadata.label
} for tag in self.tags.all()]
@property @property
def last_published_review(self): def last_published_review(self):
@ -837,6 +847,11 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
if self.phones: if self.phones:
return [phone.phone.as_e164 for phone in self.phones.all()] return [phone.phone.as_e164 for phone in self.phones.all()]
@property
def contact_emails(self):
if self.phones:
return [email.email for email in self.emails.all()]
@property @property
def establishment_subtype_labels(self): def establishment_subtype_labels(self):
if self.establishment_subtypes: if self.establishment_subtypes:
@ -1249,6 +1264,24 @@ class Plate(TranslatedFieldsMixin, models.Model):
verbose_name_plural = _('plates') verbose_name_plural = _('plates')
class MenuDish(BaseAttributes):
"""Dish model."""
STR_FIELD_NAME = 'category'
name = models.CharField(_('name'), max_length=255, default='')
category = TJSONField(
blank=True, null=True, default=None, verbose_name=_('category'),
help_text='{"en-GB":"some text"}')
price = models.IntegerField(blank=True, null=True, default=None, verbose_name=_('price'))
signature = models.BooleanField(_('signature'), default=False)
class Meta:
verbose_name = _('dish')
verbose_name_plural = _('dishes')
ordering = ('-created',)
class MenuQuerySet(models.QuerySet): class MenuQuerySet(models.QuerySet):
def with_schedule_plates_establishment(self): def with_schedule_plates_establishment(self):
return self.select_related( return self.select_related(
@ -1258,6 +1291,11 @@ class MenuQuerySet(models.QuerySet):
'plates', 'plates',
) )
def with_gallery(self):
return self.prefetch_related(
'menu_gallery'
)
def dishes(self): def dishes(self):
return self.filter( return self.filter(
Q(category__icontains='starter') | Q(category__icontains='starter') |
@ -1269,25 +1307,38 @@ class MenuQuerySet(models.QuerySet):
"""Search by category.""" """Search by category."""
return self.filter(category__icontains=value) return self.filter(category__icontains=value)
def with_dishes(self):
return self.filter(~Q(dishes=None))
class Menu(TranslatedFieldsMixin, BaseAttributes): class Menu(TranslatedFieldsMixin, BaseAttributes):
"""Menu model.""" """Menu model."""
STR_FIELD_NAME = 'category' name = models.CharField(_('name'), max_length=255, default='')
category = TJSONField(
blank=True, null=True, default=None, verbose_name=_('category'),
help_text='{"en-GB":"some text"}')
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) is_drinks_included = models.BooleanField(_('is drinks included'), default=False)
price = models.IntegerField(blank=True, null=True, default=None, verbose_name=_('price'))
schedule = models.ManyToManyField( schedule = models.ManyToManyField(
to='timetable.Timetable', to='timetable.Timetable',
blank=True, blank=True,
verbose_name=_('Establishment schedule'), verbose_name=_('Menu schedule'),
related_name='menus',
) )
uploads = models.ManyToManyField(
to='MenuFiles',
blank=True,
verbose_name=_('Menu files'),
)
dishes = models.ManyToManyField(
to='MenuDish',
blank=True,
verbose_name=_('Menu dishes')
)
category = TJSONField(
blank=True, null=True, default=None, verbose_name=_('category'),
help_text='{"en-GB":"some text"}')
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
objects = MenuQuerySet.as_manager() objects = MenuQuerySet.as_manager()
@ -1298,15 +1349,16 @@ class Menu(TranslatedFieldsMixin, BaseAttributes):
ordering = ('-created',) ordering = ('-created',)
class MenuUploads(BaseAttributes): class MenuFiles(FileMixin, BaseAttributes):
"""Menu files""" """Menu files"""
TYPES = (
menu = models.ForeignKey(Menu, verbose_name=_('Menu'), on_delete=models.CASCADE, related_name='uploads') ('image', 'Image'),
file = models.FileField( ('file', 'File')
_('File'),
validators=[FileExtensionValidator(allowed_extensions=('jpg', 'jpeg', 'png', 'doc', 'docx', 'pdf')), ],
) )
name = models.CharField(_('name'), max_length=255, default='')
type = models.CharField(_('type'), choices=TYPES, max_length=65, default='')
class Meta: class Meta:
verbose_name = _('menu upload') verbose_name = _('menu upload')
verbose_name_plural = _('menu uploads') verbose_name_plural = _('menu uploads')

View File

@ -1,20 +1,22 @@
from functools import lru_cache from functools import lru_cache
from pprint import pprint
from django.db.models import F from django.db.models import F
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from account.serializers.common import UserShortSerializer from account.serializers.common import UserShortSerializer
from establishment import models, serializers as model_serializers from establishment import models, serializers as model_serializers
from establishment.models import ContactPhone, EstablishmentEmployee from establishment.models import ContactEmail, ContactPhone, EstablishmentEmployee
from establishment.serializers import MenuDishSerializer
from gallery.models import Image from gallery.models import Image
from location.models import Address from location.models import Address
from location.serializers import AddressDetailSerializer, TranslatedField from location.serializers import AddressDetailSerializer, TranslatedField
from main.models import Currency from main.models import Currency
from main.serializers import AwardSerializer from main.serializers import AwardSerializer
from timetable.serialziers import ScheduleRUDSerializer
from utils.decorators import with_base_attributes from utils.decorators import with_base_attributes
from utils.serializers import ImageBaseSerializer, TimeZoneChoiceField, ProjectModelSerializer from utils.serializers import ImageBaseSerializer, ProjectModelSerializer, TimeZoneChoiceField
def phones_handler(phones_list, establishment): def phones_handler(phones_list, establishment):
@ -27,6 +29,16 @@ def phones_handler(phones_list, establishment):
ContactPhone.objects.create(establishment=establishment, phone=new_phone) ContactPhone.objects.create(establishment=establishment, phone=new_phone)
def emails_handler(emails_list, establishment):
"""
create or update emails for establishment
"""
ContactEmail.objects.filter(establishment=establishment).delete()
for new_email in emails_list:
ContactEmail.objects.create(establishment=establishment, email=new_email)
class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSerializer): class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSerializer):
"""Establishment create serializer""" """Establishment create serializer"""
@ -40,14 +52,14 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria
queryset=models.Address.objects.all(), queryset=models.Address.objects.all(),
write_only=True write_only=True
) )
emails = model_serializers.ContactEmailsSerializer(read_only=True, socials = model_serializers.SocialNetworkRelatedSerializers(
many=True, ) read_only=True,
socials = model_serializers.SocialNetworkRelatedSerializers(read_only=True, many=True,
many=True, ) )
type = model_serializers.EstablishmentTypeBaseSerializer(source='establishment_type', type = model_serializers.EstablishmentTypeBaseSerializer(
read_only=True) source='establishment_type',
address_id = serializers.PrimaryKeyRelatedField(write_only=True, source='address', read_only=True,
queryset=Address.objects.all()) )
tz = TimeZoneChoiceField() tz = TimeZoneChoiceField()
phones = serializers.ListField( phones = serializers.ListField(
source='contact_phones', source='contact_phones',
@ -56,6 +68,13 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria
child=serializers.CharField(max_length=128), child=serializers.CharField(max_length=128),
required=False, required=False,
) )
emails = serializers.ListField(
source='contact_emails',
allow_null=True,
allow_empty=True,
child=serializers.CharField(max_length=128),
required=False,
)
class Meta: class Meta:
model = models.Establishment model = models.Establishment
@ -81,6 +100,7 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria
'tags', 'tags',
'tz', 'tz',
'address_id', 'address_id',
'western_name',
] ]
def create(self, validated_data): def create(self, validated_data):
@ -88,11 +108,29 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria
if 'contact_phones' in validated_data: if 'contact_phones' in validated_data:
phones_list = validated_data.pop('contact_phones') phones_list = validated_data.pop('contact_phones')
emails_list = []
if 'contact_emails' in validated_data:
emails_list = validated_data.pop('contact_emails')
instance = super().create(validated_data) instance = super().create(validated_data)
phones_handler(phones_list, instance) phones_handler(phones_list, instance)
emails_handler(emails_list, instance)
return instance return instance
class EstablishmentPositionListSerializer(model_serializers.EstablishmentBaseSerializer):
"""Establishment position serializer"""
class Meta:
model = models.Establishment
fields = [
'id',
'name',
'transliterated_name',
'index_name',
]
class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer): class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer):
"""Establishment create serializer""" """Establishment create serializer"""
@ -100,9 +138,14 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer):
source='establishment_type', source='establishment_type',
queryset=models.EstablishmentType.objects.all(), write_only=True queryset=models.EstablishmentType.objects.all(), write_only=True
) )
address = AddressDetailSerializer() address = AddressDetailSerializer(read_only=True)
emails = model_serializers.ContactEmailsSerializer(read_only=False, emails = serializers.ListField(
many=True, ) source='contact_emails',
allow_null=True,
allow_empty=True,
child=serializers.CharField(max_length=128),
required=False,
)
socials = model_serializers.SocialNetworkRelatedSerializers(read_only=False, socials = model_serializers.SocialNetworkRelatedSerializers(read_only=False,
many=True, ) many=True, )
type = model_serializers.EstablishmentTypeBaseSerializer(source='establishment_type') type = model_serializers.EstablishmentTypeBaseSerializer(source='establishment_type')
@ -119,6 +162,7 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer):
fields = [ fields = [
'id', 'id',
'slug', 'slug',
'western_name',
'name', 'name',
'website', 'website',
'phones', 'phones',
@ -132,6 +176,7 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer):
# TODO: check in admin filters # TODO: check in admin filters
'is_publish', 'is_publish',
'address', 'address',
'transportation',
'tags', 'tags',
] ]
@ -140,8 +185,13 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer):
if 'contact_phones' in validated_data: if 'contact_phones' in validated_data:
phones_list = validated_data.pop('contact_phones') phones_list = validated_data.pop('contact_phones')
emails_list = []
if 'contact_emails' in validated_data:
emails_list = validated_data.pop('contact_emails')
instance = super().update(instance, validated_data) instance = super().update(instance, validated_data)
phones_handler(phones_list, instance) phones_handler(phones_list, instance)
emails_handler(emails_list, instance)
return instance return instance
@ -516,52 +566,79 @@ class EstablishmentAdminListSerializer(UserShortSerializer):
] ]
class _PlateSerializer(ProjectModelSerializer): class EstablishmentEmployeePositionsSerializer(serializers.ModelSerializer):
name_translated = TranslatedField() """Establishments from employee serializer"""
restaurant_name = serializers.CharField(read_only=True, source='establishment.name')
position = PositionBackSerializer(read_only=True)
state = serializers.CharField(read_only=True, source='status')
start = serializers.DateTimeField(read_only=True, source='from_date')
end = serializers.DateTimeField(read_only=True, source='to_date')
class Meta: class Meta:
model = models.Plate model = models.EstablishmentEmployee
fields = [ fields = [
'name_translated', 'restaurant_name',
'price', 'position',
'state',
'start',
'end',
] ]
class MenuDishesSerializer(ProjectModelSerializer): class MenuDishesSerializer(ProjectModelSerializer):
"""for dessert, main_course and starter category""" """for dessert, main_course and starter category"""
schedule = ScheduleRUDSerializer(many=True, allow_null=True) establishment_id = serializers.PrimaryKeyRelatedField(read_only=True)
plates = _PlateSerializer(read_only=True, many=True, source='plate_set', allow_null=True) establishment_slug = serializers.CharField(read_only=True, source='establishment.slug')
category_translated = serializers.CharField(read_only=True) dishes = MenuDishSerializer(many=True, read_only=True)
last_update = serializers.DateTimeField(source='created')
class Meta: class Meta:
model = models.Menu model = models.Menu
fields = [ fields = [
'id', 'id',
'category', 'establishment_id',
'category_translated', 'establishment_slug',
'establishment', 'dishes',
'is_drinks_included',
'schedule',
'plates',
'last_update',
] ]
class MenuDishesCreateSerializer(ProjectModelSerializer):
"""Menu dishes create serializer"""
menu_id = serializers.IntegerField(write_only=True)
class Meta:
model = models.MenuDish
fields = [
'id',
'name',
'category',
'price',
'signature',
'menu_id'
]
def create(self, validated_data):
menu_id = validated_data.pop('menu_id')
menu = get_object_or_404(models.Menu, pk=menu_id)
instance = models.MenuDish.objects.create(**validated_data)
menu.dishes.add(instance)
return instance
class MenuDishesRUDSerializers(ProjectModelSerializer): class MenuDishesRUDSerializers(ProjectModelSerializer):
"""for dessert, main_course and starter category""" """for dessert, main_course and starter category"""
plates = _PlateSerializer(read_only=True, many=True, source='plate_set', allow_null=True) establishment_id = serializers.PrimaryKeyRelatedField(queryset=models.Establishment.objects.all())
schedule = ScheduleRUDSerializer(read_only=True, many=True, allow_null=True) establishment_slug = serializers.CharField(read_only=True, source='establishment.slug')
dishes = MenuDishSerializer(many=True, read_only=True)
class Meta: class Meta:
model = models.Menu model = models.Menu
fields = [ fields = [
'id', 'id',
'category', 'establishment_id',
'plates', 'establishment_slug',
'establishment', 'dishes',
'is_drinks_included',
'schedule',
] ]

View File

@ -2,6 +2,7 @@
import logging import logging
from django.conf import settings from django.conf import settings
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from phonenumber_field.phonenumber import to_python as str_to_phonenumber from phonenumber_field.phonenumber import to_python as str_to_phonenumber
from rest_framework import serializers from rest_framework import serializers
@ -9,18 +10,19 @@ from rest_framework import serializers
from comment import models as comment_models from comment import models as comment_models
from comment.serializers import common as comment_serializers from comment.serializers import common as comment_serializers
from establishment import models from establishment import models
from location.serializers import AddressBaseSerializer, CityBaseSerializer, AddressDetailSerializer, \ from location.serializers import (
CityShortSerializer AddressBaseSerializer, AddressDetailSerializer, CityBaseSerializer,
from location.serializers import EstablishmentWineRegionBaseSerializer, \ CityShortSerializer, EstablishmentWineOriginBaseSerializer, EstablishmentWineRegionBaseSerializer,
EstablishmentWineOriginBaseSerializer )
from main.serializers import AwardSerializer, CurrencySerializer from main.serializers import AwardSerializer, CurrencySerializer
from review.serializers import ReviewShortSerializer from review.serializers import ReviewShortSerializer
from tag.serializers import TagBaseSerializer from tag.serializers import TagBaseSerializer
from timetable.serialziers import ScheduleRUDSerializer from timetable.serialziers import ScheduleRUDSerializer
from utils import exceptions as utils_exceptions from utils import exceptions as utils_exceptions
from utils.serializers import ImageBaseSerializer, CarouselCreateSerializer from utils.serializers import (
from utils.serializers import (ProjectModelSerializer, TranslatedField, CarouselCreateSerializer, FavoritesCreateSerializer, ImageBaseSerializer,
FavoritesCreateSerializer) ProjectModelSerializer, TranslatedField,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -70,31 +72,87 @@ class PlateSerializer(ProjectModelSerializer):
] ]
class MenuSerializers(ProjectModelSerializer): class MenuDishSerializer(ProjectModelSerializer):
plates = PlateSerializer(read_only=True, many=True, source='plate_set')
category_translated = serializers.CharField(read_only=True)
class Meta: class Meta:
model = models.Menu model = models.MenuDish
fields = [ fields = [
'id', 'id',
'name',
'category', 'category',
'category_translated', 'price',
'plates', 'signature'
'establishment'
] ]
class MenuRUDSerializers(ProjectModelSerializer): class MenuFilesSerializers(ProjectModelSerializer):
plates = PlateSerializer(read_only=True, many=True, source='plate_set') menu_id = serializers.IntegerField(write_only=True)
class Meta:
model = models.MenuFiles
fields = [
'id',
'name',
'type',
'file',
'menu_id'
]
def create(self, validated_data):
menu_id = validated_data.pop('menu_id')
menu = get_object_or_404(models.Menu, pk=menu_id)
instance = models.MenuFiles.objects.create(**validated_data)
menu.uploads.add(instance)
return instance
class MenuSerializers(ProjectModelSerializer):
name = serializers.CharField()
establishment_id = serializers.PrimaryKeyRelatedField(queryset=models.Establishment.objects.all())
establishment_slug = serializers.CharField(read_only=True, source='establishment.slug')
price = serializers.IntegerField(required=False)
drinks_included = serializers.BooleanField(source='is_drinks_included', required=False)
schedules = ScheduleRUDSerializer(many=True, allow_null=True, required=False)
uploads = MenuFilesSerializers(many=True, read_only=True)
class Meta: class Meta:
model = models.Menu model = models.Menu
fields = [ fields = [
'id', 'id',
'category', 'name',
'plates', 'establishment_id',
'establishment' 'establishment_slug',
'price',
'drinks_included',
'schedules',
'uploads',
]
def create(self, validated_data):
validated_data['establishment'] = validated_data.pop('establishment_id')
instance = models.Menu.objects.create(**validated_data)
return instance
class MenuRUDSerializers(ProjectModelSerializer):
name = serializers.CharField()
establishment_id = serializers.PrimaryKeyRelatedField(read_only=True)
establishment_slug = serializers.CharField(read_only=True, source='establishment.slug')
price = serializers.IntegerField(required=False)
drinks_included = serializers.BooleanField(source='is_drinks_included', required=False)
schedules = ScheduleRUDSerializer(many=True, allow_null=True, required=False)
uploads = MenuFilesSerializers(many=True)
class Meta:
model = models.Menu
fields = [
'id',
'name',
'establishment_id',
'establishment_slug',
'price',
'drinks_included',
'schedules',
'uploads',
] ]
@ -464,6 +522,7 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer):
'website', 'website',
'facebook', 'facebook',
'twitter', 'twitter',
'instagram',
'lafourchette', 'lafourchette',
'booking', 'booking',
'phones', 'phones',
@ -527,7 +586,7 @@ class EstablishmentCommentBaseSerializer(comment_serializers.CommentBaseSerializ
'created', 'created',
'text', 'text',
'mark', 'mark',
'nickname', # 'nickname',
'profile_pic', 'profile_pic',
'status', 'status',
'status_display', 'status_display',
@ -567,7 +626,7 @@ class EstablishmentCommentRUDSerializer(comment_serializers.CommentBaseSerialize
'created', 'created',
'text', 'text',
'mark', 'mark',
'nickname', # 'nickname',
'profile_pic', 'profile_pic',
] ]

View File

@ -30,10 +30,15 @@ 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/', views.MenuDishesListView.as_view(), name='menu-dishes-list'),
path('menus/dishes/create/', views.MenuDishesCreateView.as_view(), name='menu-dishes-create'),
path('menus/dishes/<int:pk>/', views.MenuDishesRUDView.as_view(), name='menu-dishes-rud'), path('menus/dishes/<int:pk>/', views.MenuDishesRUDView.as_view(), name='menu-dishes-rud'),
path('menus/dishes/slug/<slug:slug>/', 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('menus/slug/<slug:slug>/', views.MenuRUDView.as_view(), name='menu-rud'),
path('menus/uploads/', views.MenuFilesListCreateView.as_view(), name='menu-files-list'),
path('menus/uploads/<int:pk>/', views.MenuFilesRUDView.as_view(), name='menu-files-rud'),
path('plates/', views.PlateListCreateView.as_view(), name='plates'), path('plates/', views.PlateListCreateView.as_view(), name='plates'),
path('plates/<int:pk>/', views.PlateRUDView.as_view(), name='plate-rud'), path('plates/<int:pk>/', views.PlateRUDView.as_view(), name='plate-rud'),
path('social_choice/', views.SocialChoiceListCreateView.as_view(), name='socials_choice'), path('social_choice/', views.SocialChoiceListCreateView.as_view(), name='socials_choice'),
@ -60,6 +65,10 @@ urlpatterns = [
path('subtypes/', views.EstablishmentSubtypeListCreateView.as_view(), name='subtype-list'), path('subtypes/', views.EstablishmentSubtypeListCreateView.as_view(), name='subtype-list'),
path('subtypes/<int:pk>/', views.EstablishmentSubtypeRUDView.as_view(), name='subtype-rud'), path('subtypes/<int:pk>/', views.EstablishmentSubtypeRUDView.as_view(), name='subtype-rud'),
path('positions/', views.EstablishmentPositionListView.as_view(), name='position-list'), path('positions/', views.EstablishmentPositionListView.as_view(), name='position-list'),
path('employee_positions/<int:pk>/', views.EmployeePositionsListView.as_view(),
name='employee-positions-list'),
path('employee_establishments/<int:pk>/', views.EmployeeEstablishmentsListView.as_view(), path('employee_establishments/<int:pk>/', views.EmployeeEstablishmentsListView.as_view(),
name='employee-establishments-list') name='employee-establishments-list'),
path('employee_establishment_positions/<int:pk>/', views.EmployeeEstablishmentPositionsView.as_view(),
name='employee-establishment-positions')
] ]

View File

@ -1,7 +1,9 @@
"""Establishment app views.""" """Establishment app views."""
from django.db.models.query_utils import Q
from django.http import Http404
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics, status from rest_framework import generics, status, permissions
from rest_framework.response import Response from rest_framework.response import Response
from account.models import User from account.models import User
@ -16,6 +18,20 @@ from utils.permissions import (
from utils.views import CreateDestroyGalleryViewMixin from utils.views import CreateDestroyGalleryViewMixin
class MenuRUDMixinViews:
"""Menu mixin"""
def get_object(self):
instance = self.get_queryset().filter(
Q(establishment__slug=self.kwargs.get('slug')) | Q(establishment__id=self.kwargs.get('pk'))
).first()
if instance is None:
raise Http404
return instance
class EstablishmentMixinViews: class EstablishmentMixinViews:
"""Establishment mixin.""" """Establishment mixin."""
@ -34,12 +50,27 @@ class EstablishmentListCreateView(EstablishmentMixinViews, generics.ListCreateAP
filter_class = filters.EstablishmentFilter filter_class = filters.EstablishmentFilter
serializer_class = serializers.EstablishmentListCreateSerializer serializer_class = serializers.EstablishmentListCreateSerializer
permission_classes = get_permission_classes( permission_classes = get_permission_classes(
IsReviewManager,
IsEstablishmentManager, IsEstablishmentManager,
IsEstablishmentAdministrator, IsEstablishmentAdministrator,
) )
class EmployeeEstablishmentPositionsView(generics.ListAPIView):
"""Establishment by employee view."""
queryset = models.EstablishmentEmployee.objects.all()
serializer_class = serializers.EstablishmentEmployeePositionsSerializer
permission_classes = get_permission_classes(
IsEstablishmentManager,
IsEstablishmentAdministrator
)
def get_queryset(self):
employee_pk = self.kwargs.get('pk')
return super().get_queryset().filter(employee__id=employee_pk).all().prefetch_related(
'establishment').select_related('position')
class EmployeeEstablishmentsListView(generics.ListAPIView): class EmployeeEstablishmentsListView(generics.ListAPIView):
"""Establishment by employee list view.""" """Establishment by employee list view."""
serializer_class = serializers.EstablishmentListCreateSerializer serializer_class = serializers.EstablishmentListCreateSerializer
@ -54,6 +85,22 @@ class EmployeeEstablishmentsListView(generics.ListAPIView):
return employee.establishments.with_extended_related() return employee.establishments.with_extended_related()
class EmployeePositionsListView(generics.ListAPIView):
"""Establishment position by employee list view."""
queryset = models.Establishment.objects.all()
serializer_class = serializers.EstablishmentPositionListSerializer
permission_classes = get_permission_classes(
IsEstablishmentManager,
IsEstablishmentAdministrator
)
def get_queryset(self):
pk = self.kwargs.get('pk')
employee = get_object_or_404(models.Employee, pk=pk)
return employee.establishments.with_extended_related()
class EstablishmentRUDView(EstablishmentMixinViews, generics.RetrieveUpdateDestroyAPIView): class EstablishmentRUDView(EstablishmentMixinViews, generics.RetrieveUpdateDestroyAPIView):
lookup_field = 'slug' lookup_field = 'slug'
serializer_class = serializers.EstablishmentRUDSerializer serializer_class = serializers.EstablishmentRUDSerializer
@ -120,13 +167,17 @@ class MenuListCreateView(generics.ListCreateAPIView):
IsEstablishmentAdministrator, IsEstablishmentAdministrator,
) )
filterset_fields = ( filterset_fields = (
'establishment', 'establishment__id',
'establishment__slug', 'establishment__slug',
) )
def get_queryset(self):
return super().get_queryset().prefetch_related('establishment')
class MenuRUDView(generics.RetrieveUpdateDestroyAPIView):
class MenuRUDView(MenuRUDMixinViews, generics.RetrieveUpdateDestroyAPIView):
"""Menu RUD view.""" """Menu RUD view."""
lookup_field = None
serializer_class = serializers.MenuRUDSerializers serializer_class = serializers.MenuRUDSerializers
queryset = models.Menu.objects.all() queryset = models.Menu.objects.all()
permission_classes = get_permission_classes( permission_classes = get_permission_classes(
@ -135,6 +186,26 @@ class MenuRUDView(generics.RetrieveUpdateDestroyAPIView):
) )
class MenuFilesListCreateView(generics.ListCreateAPIView):
"""Menu files list create view."""
serializer_class = serializers.MenuFilesSerializers
queryset = models.MenuFiles.objects.all()
permission_classes = get_permission_classes(
IsEstablishmentManager,
IsEstablishmentAdministrator
)
class MenuFilesRUDView(generics.RetrieveDestroyAPIView):
"""Menu files RUD view."""
serializer_class = serializers.MenuFilesSerializers
queryset = models.MenuFiles.objects.all()
permission_classes = get_permission_classes(
IsEstablishmentManager,
IsEstablishmentAdministrator
)
class SocialChoiceListCreateView(generics.ListCreateAPIView): class SocialChoiceListCreateView(generics.ListCreateAPIView):
"""SocialChoice list create view.""" """SocialChoice list create view."""
serializer_class = serializers.SocialChoiceSerializers serializer_class = serializers.SocialChoiceSerializers
@ -523,10 +594,10 @@ class EstablishmentAdminView(generics.ListAPIView):
return User.objects.establishment_admin(establishment).distinct() return User.objects.establishment_admin(establishment).distinct()
class MenuDishesListCreateView(generics.ListCreateAPIView): class MenuDishesListView(generics.ListAPIView):
"""Menu (dessert, main_course, starter) list create view.""" """Menu (dessert, main_course, starter) list create view."""
serializer_class = serializers.MenuDishesSerializer serializer_class = serializers.MenuDishesSerializer
queryset = models.Menu.objects.with_schedule_plates_establishment().dishes().distinct() queryset = models.Menu.objects.with_dishes()
filter_class = filters.MenuDishesBackFilter filter_class = filters.MenuDishesBackFilter
permission_classes = get_permission_classes( permission_classes = get_permission_classes(
IsEstablishmentManager, IsEstablishmentManager,
@ -534,10 +605,21 @@ class MenuDishesListCreateView(generics.ListCreateAPIView):
) )
class MenuDishesRUDView(generics.RetrieveUpdateDestroyAPIView): class MenuDishesRUDView(MenuRUDMixinViews, generics.RetrieveUpdateDestroyAPIView):
"""Menu (dessert, main_course, starter) RUD view.""" """Menu (dessert, main_course, starter) RUD view."""
lookup_field = None
serializer_class = serializers.MenuDishesRUDSerializers serializer_class = serializers.MenuDishesRUDSerializers
queryset = models.Menu.objects.dishes().distinct() queryset = models.Menu.objects.with_dishes()
permission_classes = get_permission_classes(
IsEstablishmentManager,
IsEstablishmentAdministrator
)
class MenuDishesCreateView(generics.CreateAPIView):
"""Menu (dessert, main_course, starter) list create view."""
serializer_class = serializers.MenuDishesCreateSerializer
queryset = models.MenuDish.objects.all()
filter_class = filters.MenuDishesBackFilter
permission_classes = get_permission_classes( permission_classes = get_permission_classes(
IsEstablishmentManager, IsEstablishmentManager,
IsEstablishmentAdministrator IsEstablishmentAdministrator

View File

@ -0,0 +1,45 @@
# Generated by Django 2.2.7 on 2020-01-27 20:04
from django.db import migrations, models
import utils.models
def clear_data(apps, schema_editor):
City = apps.get_model('location', 'City')
for city in City.objects.all():
city.name = None
city.save()
def preserve_field_data(apps, schema_editor):
City = apps.get_model('location', 'City')
for city in City.objects.all():
city.name = city.name_translated
city.save()
class Migration(migrations.Migration):
dependencies = [
('location', '0035_auto_20200115_1117'),
]
operations = [
migrations.AlterField(
model_name='city',
name='name',
field=models.CharField(max_length=250, null=True, verbose_name='name'),
),
migrations.RunPython(clear_data, migrations.RunPython.noop),
migrations.AlterField(
model_name='city',
name='name',
field=utils.models.TJSONField(default=None, help_text='{"en-GB":"some city name"}', null=True, verbose_name='City name json'),
),
migrations.RunPython(preserve_field_data, migrations.RunPython.noop),
migrations.RemoveField(
model_name='city',
name='name_translated',
)
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2020-01-28 12:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('location', '0036_auto_20200127_2004'),
]
operations = [
migrations.AddField(
model_name='address',
name='district_name',
field=models.CharField(blank=True, default='', max_length=500, verbose_name='District name'),
),
]

View File

@ -1,20 +1,20 @@
"""Location app models.""" """Location app models."""
from functools import reduce
from json import dumps
from typing import List
from django.conf import settings from django.conf import settings
from django.contrib.gis.db import models from django.contrib.gis.db import models
from django.contrib.postgres.fields import ArrayField
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.db.transaction import on_commit from django.db.transaction import on_commit
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from functools import reduce
from typing import List
from django.contrib.postgres.fields.jsonb import KeyTextTransform
from django.contrib.postgres.fields import ArrayField
from translation.models import Language from translation.models import Language
from utils.models import (ProjectBaseMixin, SVGImageMixin, TJSONField, from utils.models import (ProjectBaseMixin, SVGImageMixin, TJSONField,
TranslatedFieldsMixin, get_current_locale, TranslatedFieldsMixin, get_current_locale,
IntermediateGalleryModelMixin, GalleryMixin) IntermediateGalleryModelMixin)
class CountryQuerySet(models.QuerySet): class CountryQuerySet(models.QuerySet):
@ -139,13 +139,14 @@ 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, TranslatedFieldsMixin):
"""Region model.""" """Region model."""
name = models.CharField(_('name'), max_length=250) name = TJSONField(default=None, null=True, help_text='{"en-GB":"some city name"}',
name_translated = TJSONField(blank=True, null=True, default=None, verbose_name=_('City name json'))
verbose_name=_('Translated name'),
help_text='{"en-GB":"some text"}')
code = models.CharField(_('code'), max_length=250) code = models.CharField(_('code'), max_length=250)
region = models.ForeignKey(Region, on_delete=models.CASCADE, region = models.ForeignKey(Region, on_delete=models.CASCADE,
blank=True, null=True, blank=True, null=True,
@ -177,7 +178,12 @@ class City(models.Model):
verbose_name = _('city') verbose_name = _('city')
def __str__(self): def __str__(self):
return self.name return self.name_dumped
@property
def name_dumped(self):
"""Used for indexing as string"""
return f'{self.id}: {dumps(self.name)}'
@property @property
def image_object(self): def image_object(self):
@ -217,6 +223,8 @@ class Address(models.Model):
default='', help_text=_('Ex.: 350018')) default='', help_text=_('Ex.: 350018'))
coordinates = models.PointField( coordinates = models.PointField(
_('Coordinates'), blank=True, null=True, default=None) _('Coordinates'), blank=True, null=True, default=None)
district_name = models.CharField(
_('District name'), max_length=500, blank=True, default='')
old_id = models.IntegerField(null=True, blank=True, default=None) old_id = models.IntegerField(null=True, blank=True, default=None)
class Meta: class Meta:

View File

@ -1,6 +1,8 @@
from location import models from location import models
from location.serializers import common from location.serializers import common
from utils.serializers import TranslatedField
class AddressCreateSerializer(common.AddressDetailSerializer): class AddressCreateSerializer(common.AddressDetailSerializer):
"""Address create serializer.""" """Address create serializer."""
@ -9,6 +11,8 @@ class AddressCreateSerializer(common.AddressDetailSerializer):
class CountryBackSerializer(common.CountrySerializer): class CountryBackSerializer(common.CountrySerializer):
"""Country back-office serializer.""" """Country back-office serializer."""
name_translated = TranslatedField()
class Meta: class Meta:
model = models.Country model = models.Country
fields = [ fields = [
@ -16,5 +20,6 @@ class CountryBackSerializer(common.CountrySerializer):
'code', 'code',
'svg_image', 'svg_image',
'name', 'name',
'name_translated',
'country_id' 'country_id'
] ]

View File

@ -34,6 +34,27 @@ class CountrySimpleSerializer(serializers.ModelSerializer):
fields = ('id', 'code', 'name_translated') fields = ('id', 'code', 'name_translated')
class ParentRegionSerializer(serializers.ModelSerializer):
"""Region serializer"""
country = CountrySerializer(read_only=True)
country_id = serializers.PrimaryKeyRelatedField(
source='country',
queryset=models.Country.objects.all(),
write_only=True
)
class Meta:
model = models.Region
fields = [
'id',
'name',
'code',
'country',
'country_id'
]
class RegionSerializer(serializers.ModelSerializer): class RegionSerializer(serializers.ModelSerializer):
"""Region serializer""" """Region serializer"""
@ -43,6 +64,7 @@ class RegionSerializer(serializers.ModelSerializer):
queryset=models.Country.objects.all(), queryset=models.Country.objects.all(),
write_only=True write_only=True
) )
parent_region = ParentRegionSerializer(read_only=True)
class Meta: class Meta:
model = models.Region model = models.Region
@ -59,13 +81,14 @@ class RegionSerializer(serializers.ModelSerializer):
class CityShortSerializer(serializers.ModelSerializer): class CityShortSerializer(serializers.ModelSerializer):
"""Short city serializer""" """Short city serializer"""
country = CountrySerializer(read_only=True) country = CountrySerializer(read_only=True)
name_translated = TranslatedField()
class Meta: class Meta:
"""Meta class""" """Meta class"""
model = models.City model = models.City
fields = ( fields = (
'id', 'id',
'name', 'name_translated',
'code', 'code',
'country', 'country',
) )
@ -91,12 +114,14 @@ class CityBaseSerializer(serializers.ModelSerializer):
required=False, required=False,
) )
country = CountrySerializer(read_only=True) country = CountrySerializer(read_only=True)
name_translated = TranslatedField()
class Meta: class Meta:
model = models.City model = models.City
fields = [ fields = [
'id', 'id',
'name', 'name',
'name_translated',
'region', 'region',
'region_id', 'region_id',
'country_id', 'country_id',
@ -146,6 +171,7 @@ class AddressBaseSerializer(serializers.ModelSerializer):
'postal_code', 'postal_code',
'latitude', 'latitude',
'longitude', 'longitude',
'district_name',
# todo: remove this fields (backward compatibility) # todo: remove this fields (backward compatibility)
'geo_lon', 'geo_lon',

View File

@ -50,8 +50,8 @@ 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'))\
.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
@ -61,7 +61,7 @@ class CityListSearchView(common.CityViewMixin, generics.ListCreateAPIView):
"""Create view for model City.""" """Create view for model City."""
serializer_class = serializers.CityBaseSerializer serializer_class = serializers.CityBaseSerializer
queryset = models.City.objects.all()\ queryset = models.City.objects.all()\
.annotate(locale_name=KeyTextTransform(get_current_locale(), 'name_translated'))\ .annotate(locale_name=KeyTextTransform(get_current_locale(), 'name'))\
.order_by('locale_name') .order_by('locale_name')
filter_class = filters.CityBackFilter filter_class = filters.CityBackFilter
pagination_class = None pagination_class = None

View File

@ -24,7 +24,7 @@ class RegionViewMixin(generics.GenericAPIView):
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

@ -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 = (
@ -54,6 +56,10 @@ class NewsListFilterSet(filters.FilterSet):
return queryset.es_search(value, relevance_order='ordering' not in self.request.query_params) 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

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2020-01-28 14:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('news', '0052_auto_20200121_0940'),
]
operations = [
migrations.AlterField(
model_name='news',
name='state',
field=models.PositiveSmallIntegerField(choices=[(0, 'remove'), (1, 'hidden'), (2, 'published'), (3, 'not published')], default=3, verbose_name='State'),
),
]

View File

@ -101,6 +101,10 @@ class NewsQuerySet(TranslationQuerysetMixin):
"""Return qs with related objects.""" """Return qs with related objects."""
return self.select_related('created_by', 'agenda', 'banner') return self.select_related('created_by', 'agenda', 'banner')
def visible(self):
"""Narrows qs by excluding invisible for API (at all) news"""
return self.exclude(state=self.model.REMOVE)
def by_type(self, news_type): def by_type(self, news_type):
"""Filter News by type""" """Filter News by type"""
return self.filter(news_type__name=news_type) return self.filter(news_type__name=news_type)
@ -128,14 +132,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):
@ -252,18 +263,18 @@ class News(GalleryMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin,
) )
# STATE CHOICES # STATE CHOICES
WAITING = 0 REMOVE = 0
HIDDEN = 1 HIDDEN = 1
PUBLISHED = 2 PUBLISHED = 2
PUBLISHED_EXCLUSIVE = 3 UNPUBLISHED = 3
PUBLISHED_STATES = [PUBLISHED, PUBLISHED_EXCLUSIVE] PUBLISHED_STATES = [PUBLISHED]
STATE_CHOICES = ( STATE_CHOICES = (
(WAITING, _('Waiting')), (REMOVE, _('remove')), # simply stored in DB news. not shown anywhere
(HIDDEN, _('Hidden')), (HIDDEN, _('hidden')), # not shown in api/web or api/mobile
(PUBLISHED, _('Published')), (PUBLISHED, _('published')), # shown everywhere
(PUBLISHED_EXCLUSIVE, _('Published exclusive')), (UNPUBLISHED, _('not published')), # newly created news
) )
INTERNATIONAL_TAG_VALUE = 'international' INTERNATIONAL_TAG_VALUE = 'international'
@ -295,7 +306,7 @@ class News(GalleryMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin,
slugs = HStoreField(null=True, blank=True, default=dict, slugs = HStoreField(null=True, blank=True, default=dict,
verbose_name=_('Slugs for current news obj'), verbose_name=_('Slugs for current news obj'),
help_text='{"en-GB":"some slug"}') help_text='{"en-GB":"some slug"}')
state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES, state = models.PositiveSmallIntegerField(default=UNPUBLISHED, choices=STATE_CHOICES,
verbose_name=_('State')) verbose_name=_('State'))
is_highlighted = models.BooleanField(default=False, is_highlighted = models.BooleanField(default=False,
verbose_name=_('Is highlighted')) verbose_name=_('Is highlighted'))
@ -340,7 +351,7 @@ class News(GalleryMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin,
def create_duplicate(self, new_country, view_count_model): def create_duplicate(self, new_country, view_count_model):
self.pk = None self.pk = None
self.state = self.WAITING self.state = self.UNPUBLISHED
self.slugs = {locale: f'{slug}-{new_country.code}' for locale, slug in self.slugs.items()} self.slugs = {locale: f'{slug}-{new_country.code}' for locale, slug in self.slugs.items()}
self.country = new_country self.country = new_country
self.views_count = view_count_model self.views_count = view_count_model

View File

@ -155,6 +155,9 @@ class NewsDetailSerializer(NewsBaseSerializer):
'gallery', 'gallery',
) )
def update(self, instance, validated_data):
return super().update(instance, validated_data)
class NewsDetailWebSerializer(NewsDetailSerializer): class NewsDetailWebSerializer(NewsDetailSerializer):
"""News detail serializer for web users..""" """News detail serializer for web users.."""
@ -207,7 +210,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 +230,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 +240,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):
@ -255,7 +263,7 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
instance = models.News.objects.get(pk=self.context['request'].data['id']) instance = models.News.objects.get(pk=self.context['request'].data['id'])
for key in ['slugs', 'title', 'locale_to_description_is_active', 'description']: for key in ['slugs', 'title', 'locale_to_description_is_active', 'description']:
for locale in locales: for locale in locales:
if not attrs[key].get(locale): if locale not in attrs[key]:
attrs[key][locale] = getattr(instance, key).get(locale) attrs[key][locale] = getattr(instance, key).get(locale)
return attrs return attrs
@ -299,7 +307,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')
@ -357,8 +365,9 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
template_display = serializers.CharField(source='get_template_display', template_display = serializers.CharField(source='get_template_display',
read_only=True) read_only=True)
duplicates = NewsBackOfficeDuplicationInfoSerializer(many=True, allow_null=True, read_only=True) duplicates = NewsBackOfficeDuplicationInfoSerializer(many=True, allow_null=True, read_only=True)
agenda = AgendaSerializer(required=False, allow_null=True, read_only=True)
class Meta(NewsBackOfficeBaseSerializer.Meta, NewsDetailSerializer.Meta): class Meta(NewsDetailSerializer.Meta, NewsBackOfficeBaseSerializer.Meta):
"""Meta class.""" """Meta class."""
fields = NewsBackOfficeBaseSerializer.Meta.fields + \ fields = NewsBackOfficeBaseSerializer.Meta.fields + \
@ -373,6 +382,13 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
'duplicates', 'duplicates',
) )
def validate(self, attrs):
"""Overridden validate method."""
return super().validate(attrs)
def update(self, instance, validated_data):
return super().update(instance, validated_data)
class NewsBackOfficeGallerySerializer(serializers.ModelSerializer): class NewsBackOfficeGallerySerializer(serializers.ModelSerializer):
"""Serializer class for model NewsGallery.""" """Serializer class for model NewsGallery."""
@ -503,3 +519,8 @@ class NewsCloneCreateSerializer(NewsBackOfficeBaseSerializer,
view_count_model = rating_models.ViewCount.objects.create(count=0) view_count_model = rating_models.ViewCount.objects.create(count=0)
instance.create_duplicate(new_country, view_count_model) instance.create_duplicate(new_country, view_count_model)
return get_object_or_404(models.News, pk=kwargs['pk']) return get_object_or_404(models.News, pk=kwargs['pk'])
class NewsStatesSerializer(serializers.Serializer):
value = serializers.IntegerField()
state_translated = serializers.CharField()

View File

@ -8,6 +8,7 @@ app_name = 'news'
urlpatterns = [ urlpatterns = [
path('', views.NewsBackOfficeLCView.as_view(), name='list-create'), path('', views.NewsBackOfficeLCView.as_view(), name='list-create'),
path('states/', views.NewsStatesView.as_view(), name='possible-news-states-list'),
path('<int:pk>/', views.NewsBackOfficeRUDView.as_view(), name='retrieve-update-destroy'), path('<int:pk>/', views.NewsBackOfficeRUDView.as_view(), name='retrieve-update-destroy'),
path('<int:pk>/gallery/', views.NewsBackOfficeGalleryListView.as_view(), name='gallery-list'), path('<int:pk>/gallery/', views.NewsBackOfficeGalleryListView.as_view(), name='gallery-list'),
path('<int:pk>/gallery/<int:image_id>/', views.NewsBackOfficeGalleryCreateDestroyView.as_view(), path('<int:pk>/gallery/<int:image_id>/', views.NewsBackOfficeGalleryCreateDestroyView.as_view(),

View File

@ -26,6 +26,7 @@ class NewsMixinView:
"""Override get_queryset method.""" """Override get_queryset method."""
qs = models.News.objects.published() \ qs = models.News.objects.published() \
.with_base_related() \ .with_base_related() \
.visible() \
.annotate_in_favorites(self.request.user) \ .annotate_in_favorites(self.request.user) \
.order_by('-is_highlighted', '-publication_date', '-publication_time') .order_by('-is_highlighted', '-publication_date', '-publication_time')
@ -43,7 +44,7 @@ class NewsMixinView:
return qs return qs
def get_object(self): def get_object(self):
instance = self.get_queryset().filter( instance = self.get_queryset().visible().with_base_related().filter(
slugs__values__contains=[self.kwargs['slug']] slugs__values__contains=[self.kwargs['slug']]
).first() ).first()
@ -53,6 +54,23 @@ class NewsMixinView:
return instance return instance
class NewsStatesView(generics.ListAPIView):
"""Possible project news states"""
pagination_class = None
serializer_class = serializers.NewsStatesSerializer
def get_queryset(self):
return None
def list(self, request, *args, **kwargs):
mutated_for_serializer = [{
'value': state[0],
'state_translated': state[1],
} for state in models.News.STATE_CHOICES]
serializer = self.get_serializer(mutated_for_serializer, many=True)
return response.Response(serializer.data)
class NewsListView(NewsMixinView, generics.ListAPIView): class NewsListView(NewsMixinView, generics.ListAPIView):
"""News list view.""" """News list view."""
@ -137,7 +155,7 @@ class NewsBackOfficeLCView(NewsBackOfficeMixinView,
def get_queryset(self): def get_queryset(self):
"""Override get_queryset method.""" """Override get_queryset method."""
qs = super().get_queryset().with_extended_related() qs = super().get_queryset().with_extended_related().visible()
if 'ordering' in self.request.query_params: if 'ordering' in self.request.query_params:
self.request.GET._mutable = True self.request.GET._mutable = True
if '-publication_datetime' in self.request.query_params['ordering']: if '-publication_datetime' in self.request.query_params['ordering']:

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):
@ -131,14 +133,23 @@ class Subscriber(ProjectBaseMixin):
self.update_code = generate_string_code() self.update_code = generate_string_code()
return super(Subscriber, self).save(*args, **kwargs) return super(Subscriber, self).save(*args, **kwargs)
def unsubscribe(self, query: dict): def unsubscribe(self):
"""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.pk)
else: else:
send_unsubscribe_email(self.email) send_unsubscribe_email(self.pk)
@property @property
def send_to(self): def send_to(self):
@ -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')
@ -89,12 +90,20 @@ class CreateAndUpdateSubscribeSerializer(serializers.ModelSerializer):
subscriber = models.Subscriber.objects.make_subscriber(**validated_data) subscriber = models.Subscriber.objects.make_subscriber(**validated_data)
if settings.USE_CELERY: if settings.USE_CELERY:
send_subscribes_update_email.delay(subscriber.email) send_subscribes_update_email.delay(subscriber.pk)
else: else:
send_subscribes_update_email(subscriber.email) send_subscribes_update_email(subscriber.pk)
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,16 +3,16 @@ 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
def send_subscribes_update_email(email): def send_subscribes_update_email(subscriber_id):
subscriber = models.Subscriber.objects.filter(email=email).first() subscriber = models.Subscriber.objects.get(pk=subscriber_id)
if subscriber is None: if subscriber is None:
return return
@ -53,8 +53,8 @@ def send_subscribes_update_email(email):
@shared_task @shared_task
def send_unsubscribe_email(email): def send_unsubscribe_email(subscriber_id):
subscriber = models.Subscriber.objects.filter(email=email).first() subscriber = models.Subscriber.objects.get(pk=subscriber_id)
if subscriber is None: if subscriber is None:
return return

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."""
@ -41,20 +53,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):
@ -65,9 +64,9 @@ 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()
serializer = self.get_serializer(instance=obj) serializer = self.get_serializer(instance=obj)
return Response(data=serializer.data) return Response(data=serializer.data)
@ -81,7 +80,7 @@ class UnsubscribeAuthUserView(generics.GenericAPIView):
def patch(self, request, *args, **kw): def patch(self, request, *args, **kw):
user = request.user user = request.user
obj = models.Subscriber.objects.filter(user=user).first() obj = models.Subscriber.objects.filter(user=user).first()
obj.unsubscribe(request.query_params) obj.unsubscribe()
serializer = self.get_serializer(instance=obj) serializer = self.get_serializer(instance=obj)
return Response(data=serializer.data) return Response(data=serializer.data)

34
apps/partner/filters.py Normal file
View File

@ -0,0 +1,34 @@
"""Partner app filters."""
from django_filters import rest_framework as filters
from partner.models import Partner
from django.core.validators import EMPTY_VALUES
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)}')
ordering = filters.CharFilter(method='sort_partner')
class Meta:
"""Meta class."""
model = Partner
fields = (
'establishment',
'type',
'ordering',
)
@staticmethod
def sort_partner(queryset, name, value):
if value not in EMPTY_VALUES:
if 'date_bind' in value:
value = value.replace('date_bind', 'partnertoestablishment__partner_bind_date')
queryset = queryset.order_by(value)
return queryset

View File

@ -0,0 +1,44 @@
# Generated by Django 2.2.7 on 2020-01-28 17:46
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
def make_relations(apps, schemaeditor):
PartnerToEstablishment = apps.get_model('partner', 'PartnerToEstablishment')
Partner = apps.get_model('partner', 'Partner')
Establishment = apps.get_model('establishment', 'Establishment')
touched_partners_ids = []
for establishment in Establishment.objects.filter(partners__isnull=False):
for related_partner in establishment.partners.all():
real_partner = Partner.objects.filter(name=related_partner.name, type=related_partner.type,
url=related_partner.url).first()
touched_partners_ids.append(related_partner.pk)
PartnerToEstablishment(establishment=establishment, partner=real_partner, partner_bind_date=real_partner.created).save()
class Migration(migrations.Migration):
dependencies = [
('establishment', '0083_establishment_instagram'),
('partner', '0003_auto_20191121_1059'),
]
operations = [
migrations.CreateModel(
name='PartnerToEstablishment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('partner_bind_date', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date partner binded')),
('establishment', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='establishment.Establishment')),
('partner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='partner.Partner')),
],
),
migrations.AddField(
model_name='partner',
name='establishments',
field=models.ManyToManyField(null=True, related_name='new_partners', through='partner.PartnerToEstablishment', to='establishment.Establishment', verbose_name='Establishments'),
),
migrations.RunPython(make_relations, migrations.RunPython.noop),
]

View File

@ -0,0 +1,45 @@
# Generated by Django 2.2.7 on 2020-01-28 17:54
from django.db import migrations, models
import django.db.models.deletion
def delete_unused_partners(apps, schema_editor):
PartnerToEstablishment = apps.get_model('partner', 'PartnerToEstablishment')
Partner = apps.get_model('partner', 'Partner')
ids_to_preserve = []
for p_t_e in PartnerToEstablishment.objects.all():
ids_to_preserve.append(p_t_e.partner.pk)
Partner.objects.exclude(id__in=ids_to_preserve).delete()
class Migration(migrations.Migration):
dependencies = [
('location', '0037_address_district_name'),
('establishment', '0083_establishment_instagram'),
('partner', '0004_auto_20200128_1746'),
]
operations = [
migrations.RemoveField(
model_name='partner',
name='establishments',
),
migrations.AddField(
model_name='partner',
name='country',
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL,
to='location.Country'),
),
migrations.RemoveField(
model_name='partner',
name='establishment',
),
migrations.AddField(
model_name='partner',
name='establishment',
field=models.ManyToManyField(related_name='partners', through='partner.PartnerToEstablishment',
to='establishment.Establishment', verbose_name='Establishments'),
),
migrations.RunPython(delete_unused_partners, migrations.RunPython.noop, atomic=True)
]

View File

@ -0,0 +1,37 @@
# Generated by Django 2.2.7 on 2020-01-29 12:01
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('partner', '0005_auto_20200128_1754'),
]
operations = [
migrations.RemoveField(
model_name='partner',
name='image',
),
migrations.RemoveField(
model_name='partner',
name='url',
),
migrations.AddField(
model_name='partner',
name='images',
field=django.contrib.postgres.fields.ArrayField(base_field=models.URLField(verbose_name='Partner image URL'), blank=True, default=None, null=True, size=None),
),
migrations.AddField(
model_name='partnertoestablishment',
name='image',
field=models.URLField(null=True, verbose_name='Partner image URL'),
),
migrations.AddField(
model_name='partnertoestablishment',
name='url',
field=models.URLField(blank=True, default=None, null=True, verbose_name='Establishment to Partner URL'),
),
]

View File

@ -1,10 +1,25 @@
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from django.contrib.postgres.fields import ArrayField
from establishment.models import Establishment from establishment.models import Establishment
from utils.models import ImageMixin, ProjectBaseMixin from utils.models import ImageMixin, ProjectBaseMixin
class PartnerQueryset(models.QuerySet):
def with_base_related(self):
return self.prefetch_related('establishment__establishment_type', 'establishment__establishment_subtypes',
'establishment__awards', 'establishment__schedule', 'establishment__phones',
'establishment__gallery', 'establishment__menu_set',
'establishment__menu_set__plates', 'establishment__menu_set__plates__currency',
'establishment__currency', 'establishment__address__city',
'establishment__address__city__region',
'establishment__address__city__region__country',
'establishment__address__city__country', 'country')
class Partner(ProjectBaseMixin): class Partner(ProjectBaseMixin):
"""Partner model.""" """Partner model."""
@ -17,24 +32,36 @@ class Partner(ProjectBaseMixin):
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
name = models.CharField(_('name'), max_length=255, blank=True, null=True) name = models.CharField(_('name'), max_length=255, blank=True, null=True)
url = models.URLField(verbose_name=_('Partner URL')) images = ArrayField(
image = models.URLField(verbose_name=_('Partner image URL'), null=True) models.URLField(verbose_name=_('Partner image URL')), blank=True, null=True, default=None,
establishment = models.ForeignKey(
Establishment,
verbose_name=_('Establishment'),
related_name='partners',
on_delete=models.CASCADE,
blank=True,
null=True,
) )
establishment = models.ManyToManyField('establishment.Establishment', related_name='partners',
through='PartnerToEstablishment',
verbose_name=_('Establishments'))
type = models.PositiveSmallIntegerField(choices=MODEL_TYPES, default=PARTNER) type = models.PositiveSmallIntegerField(choices=MODEL_TYPES, default=PARTNER)
starting_date = models.DateField(_('starting date'), blank=True, null=True) starting_date = models.DateField(_('starting date'), blank=True, null=True)
expiry_date = models.DateField(_('expiry date'), blank=True, null=True) expiry_date = models.DateField(_('expiry date'), blank=True, null=True)
price_per_month = models.DecimalField(_('price per month'), max_digits=10, decimal_places=2, blank=True, null=True) price_per_month = models.DecimalField(_('price per month'), max_digits=10, decimal_places=2, blank=True, null=True)
country = models.ForeignKey('location.Country', null=True, default=None, on_delete=models.SET_NULL)
objects = PartnerQueryset.as_manager()
class Meta: class Meta:
verbose_name = _('partner') verbose_name = _('partner')
verbose_name_plural = _('partners') verbose_name_plural = _('partners')
def __str__(self): def __str__(self):
return f'{self.url}' return f'{self.name}'
@property
def type_display(self):
return self.MODEL_TYPES[self.type][1]
class PartnerToEstablishment(models.Model):
partner_bind_date = models.DateTimeField(default=timezone.now, editable=False,
verbose_name=_('Date partner binded'))
url = models.URLField(verbose_name=_('Establishment to Partner URL'), null=True, blank=True, default=None)
image = models.URLField(verbose_name=_('Partner image URL'), null=True)
partner = models.ForeignKey(Partner, on_delete=models.CASCADE, null=True)
establishment = models.ForeignKey('establishment.Establishment', on_delete=models.CASCADE, null=True)

View File

@ -1,21 +1,85 @@
"""Back account serializers""" """Back account serializers"""
from rest_framework import serializers from rest_framework import serializers
from partner.models import Partner from partner.models import Partner, PartnerToEstablishment
from establishment.serializers import EstablishmentShortSerializer
from location.serializers import CountrySimpleSerializer
from location.models import Country
from django.shortcuts import get_object_or_404
from establishment.models import Establishment
class BackPartnerSerializer(serializers.ModelSerializer): class BackPartnerSerializer(serializers.ModelSerializer):
# establishments = EstablishmentShortSerializer(many=True, read_only=True, source='establishment')
country = CountrySimpleSerializer(read_only=True)
type_display = serializers.CharField(read_only=True)
country_id = serializers.PrimaryKeyRelatedField(
queryset=Country.objects.all(),
required=False,
write_only=True,
source='country'
)
class Meta: class Meta:
model = Partner model = Partner
fields = ( fields = (
'id', 'id',
'old_id',
'name', 'name',
'url', 'images',
'image',
'establishment',
'establishment_id',
'type', 'type',
'type_display',
'starting_date', 'starting_date',
'expiry_date', 'expiry_date',
'price_per_month', 'price_per_month',
'country',
'country_id',
# 'establishments',
)
extra_kwargs = {
'type': {'write_only': True},
}
class PartnersForEstablishmentSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(source='partner.pk', read_only=True)
name = serializers.CharField(source='partner.name', read_only=True)
type = serializers.IntegerField(source='partner.type', read_only=True)
type_display = serializers.CharField(source='partner.type_display', read_only=True)
starting_date = serializers.DateField(source='partner.starting_date', read_only=True)
expiry_date = serializers.DateField(source='partner.expiry_date', read_only=True)
price_per_month = serializers.DecimalField(source='partner.price_per_month', max_digits=10, decimal_places=2,
read_only=True)
country = CountrySimpleSerializer(read_only=True, source='partner.country')
class Meta:
model = PartnerToEstablishment
fields = (
'id',
'name',
'type',
'type_display',
'starting_date',
'expiry_date',
'price_per_month',
'country',
'url', # own field
'image', # own field
)
def create(self, validated_data):
establishment_id = self.context['view'].kwargs['establishment_id']
partner_id = self.context['view'].kwargs['partner_id']
establishment = get_object_or_404(Establishment, pk=establishment_id)
partner = get_object_or_404(Partner, pk=partner_id)
instance = PartnerToEstablishment.objects.create(**validated_data,
establishment=establishment,
partner=partner)
return instance
class PartnerPicturesSerializer(serializers.ModelSerializer):
class Meta:
model = Partner
fields = (
'images',
) )

View File

@ -11,6 +11,6 @@ class PartnerSerializer(serializers.ModelSerializer):
model = models.Partner model = models.Partner
fields = ( fields = (
'id', 'id',
'image', # 'image',
'url' 'url'
) )

View File

@ -1,7 +1,7 @@
from pprint import pprint from pprint import pprint
from establishment.models import Establishment from establishment.models import Establishment
from partner.models import Partner from partner.models import Partner, PartnerToEstablishment
from transfer.models import EstablishmentBacklinks from transfer.models import EstablishmentBacklinks
from transfer.serializers.partner import PartnerSerializer from transfer.serializers.partner import PartnerSerializer

View File

@ -8,4 +8,11 @@ app_name = 'partner'
urlpatterns = [ urlpatterns = [
path('', views.PartnerLstView.as_view(), name='partner-list-create'), path('', views.PartnerLstView.as_view(), name='partner-list-create'),
path('<int:id>/', views.PartnerRUDView.as_view(), name='partner-rud'), path('<int:id>/', views.PartnerRUDView.as_view(), name='partner-rud'),
path('for_establishment/<int:establishment_id>/', views.EstablishmentPartners.as_view(),
name='partners-for-establishment'),
path('pictures/<int:id>/', views.PartnerPicturesListView.as_view(), name='partner-pictures-get'),
path('bind/<int:partner_id>/<int:establishment_id>/', views.BindPartnerToEstablishmentView.as_view(),
name='bind-partner-to-establishment'),
path('unbind/<int:partner_id>/<int:establishment_id>/', views.UnbindPartnerFromEstablishmentView.as_view(),
name='unbind-partner-from-establishment'),
] ]

View File

@ -1,34 +1,81 @@
from django.shortcuts import get_object_or_404
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics from rest_framework import generics
from rest_framework.filters import OrderingFilter
from partner.models import Partner from partner import filters
from partner.models import Partner, PartnerToEstablishment
from partner.serializers import back as serializers from partner.serializers import back as serializers
from utils.methods import get_permission_classes from utils.methods import get_permission_classes
from utils.permissions import IsEstablishmentManager, IsEstablishmentAdministrator from utils.permissions import IsEstablishmentManager, IsEstablishmentAdministrator
class PartnerLstView(generics.ListCreateAPIView): class PartnerLstView(generics.ListCreateAPIView):
"""Partner list create view.""" """Partner list/create view.
queryset = Partner.objects.all() Allows to get partners for current country, or create a new one.
"""
queryset = Partner.objects.with_base_related()
serializer_class = serializers.BackPartnerSerializer serializer_class = serializers.BackPartnerSerializer
pagination_class = None pagination_class = None
filter_backends = (DjangoFilterBackend,)
permission_classes = get_permission_classes( permission_classes = get_permission_classes(
IsEstablishmentManager, IsEstablishmentManager,
IsEstablishmentAdministrator IsEstablishmentAdministrator
) )
filterset_fields = ( filter_class = filters.PartnerFilterSet
'establishment',
'type',
class EstablishmentPartners(generics.ListAPIView):
queryset = PartnerToEstablishment.objects.prefetch_related('partner', 'partner__country')
serializer_class = serializers.PartnersForEstablishmentSerializer
pagination_class = None
permission_classes = get_permission_classes(
IsEstablishmentManager,
IsEstablishmentAdministrator
) )
filter_backends = (OrderingFilter, DjangoFilterBackend)
ordering_fields = '__all__'
ordering = '-partner_bind_date'
def get_queryset(self):
return super().get_queryset().filter(establishment=self.kwargs['establishment_id'])
class PartnerRUDView(generics.RetrieveUpdateDestroyAPIView): class PartnerRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Partner RUD view.""" """Partner RUD view."""
queryset = Partner.objects.all() queryset = Partner.objects.with_base_related()
serializer_class = serializers.BackPartnerSerializer serializer_class = serializers.BackPartnerSerializer
lookup_field = 'id' lookup_field = 'id'
permission_classes = get_permission_classes( permission_classes = get_permission_classes(
IsEstablishmentManager, IsEstablishmentManager,
IsEstablishmentAdministrator IsEstablishmentAdministrator
) )
class PartnerPicturesListView(generics.RetrieveAPIView):
lookup_field = 'id'
serializer_class = serializers.PartnerPicturesSerializer
queryset = Partner.objects.with_base_related()
permission_classes = get_permission_classes(
IsEstablishmentManager,
IsEstablishmentAdministrator
)
class BindPartnerToEstablishmentView(generics.CreateAPIView):
serializer_class = serializers.PartnersForEstablishmentSerializer
permission_classes = get_permission_classes(
IsEstablishmentManager,
IsEstablishmentAdministrator
)
class UnbindPartnerFromEstablishmentView(generics.DestroyAPIView):
serializer_class = serializers.PartnersForEstablishmentSerializer
permission_classes = get_permission_classes(
IsEstablishmentManager,
IsEstablishmentAdministrator
)
def get_object(self):
return get_object_or_404(PartnerToEstablishment, establishment_id=self.kwargs['establishment_id'],
partner_id=self.kwargs['partner_id'])

View File

@ -175,7 +175,8 @@ class EstablishmentDocument(Document):
'city': fields.ObjectField( 'city': fields.ObjectField(
properties={ properties={
'id': fields.IntegerField(), 'id': fields.IntegerField(),
'name': fields.KeywordField(), 'name': fields.KeywordField(attr='name_dumped'),
'name_translated': fields.KeywordField(),
'is_island': fields.BooleanField(), 'is_island': fields.BooleanField(),
'country': fields.ObjectField( 'country': fields.ObjectField(
properties={ properties={

View File

@ -44,7 +44,8 @@ class ProductDocument(Document):
attr='address.city', attr='address.city',
properties={ properties={
'id': fields.IntegerField(), 'id': fields.IntegerField(),
'name': fields.KeywordField(), 'name': fields.KeywordField(attr='name_dumped'),
'name_translated': fields.KeywordField(),
'code': fields.KeywordField(), 'code': fields.KeywordField(),
'country': fields.ObjectField( 'country': fields.ObjectField(
properties={ properties={

View File

@ -124,7 +124,13 @@ class CityDocumentShortSerializer(serializers.Serializer):
id = serializers.IntegerField() id = serializers.IntegerField()
code = serializers.CharField(allow_null=True) code = serializers.CharField(allow_null=True)
name = serializers.CharField() # todo: index and use name dict field
name_translated = serializers.SerializerMethodField()
@staticmethod
def get_name_translated(obj):
return get_translated_value(loads(obj.name))
class CountryDocumentSerializer(serializers.Serializer): class CountryDocumentSerializer(serializers.Serializer):

View File

@ -13,7 +13,7 @@ def update_document(sender, **kwargs):
app_label_model_name_to_filter = { app_label_model_name_to_filter = {
('location', 'country'): 'address__city__country', ('location', 'country'): 'address__city__country',
('location', 'city'): 'address__city', # ('location', 'city'): 'address__city',
('location', 'address'): 'address', ('location', 'address'): 'address',
# todo: remove after migration # todo: remove after migration
('establishment', 'establishmenttype'): 'establishment_type', ('establishment', 'establishmenttype'): 'establishment_type',

View File

@ -1,7 +1,7 @@
from rest_framework import serializers from rest_framework import serializers
from establishment.models import Establishment from establishment.models import Establishment
from partner.models import Partner from partner.models import Partner, PartnerToEstablishment
class PartnerSerializer(serializers.Serializer): class PartnerSerializer(serializers.Serializer):
@ -43,10 +43,32 @@ class PartnerSerializer(serializers.Serializer):
return establishment return establishment
def create(self, validated_data): def create(self, validated_data):
obj, _ = Partner.objects.update_or_create( establishment = validated_data.pop('establishment')
old_id=validated_data['old_id'], url = validated_data.pop('url')
defaults=validated_data, image = validated_data.pop('image')
old_id = validated_data.pop('old_id')
created = validated_data.pop('created')
obj, is_created = Partner.objects.update_or_create(
# old_id=validated_data['old_id'],
**validated_data
) )
obj.old_id = old_id
obj.created = created
obj.establishment.add(establishment)
if is_created:
obj.images = [image]
elif image not in obj.images:
obj.images.append(image)
obj.save()
p_t_e = PartnerToEstablishment.objects.filter(establishment=establishment, partner=obj).first()
p_t_e.url = url
p_t_e.image = image
p_t_e.save()
return obj return obj

View File

@ -6,7 +6,7 @@ import string
from collections import namedtuple from collections import namedtuple
from functools import reduce from functools import reduce
from io import BytesIO from io import BytesIO
import pathlib
import requests import requests
from PIL import Image from PIL import Image
from django.conf import settings from django.conf import settings
@ -69,6 +69,15 @@ def username_random():
) )
def file_path(instance, filename):
"""Determine file path method."""
filename = '%s.%s' % (generate_code(), pathlib.Path(filename).suffix.lstrip('.'))
return 'files/%s/%s/%s' % (
instance._meta.model_name,
datetime.now().strftime(settings.REST_DATE_FORMAT),
filename)
def image_path(instance, filename): def image_path(instance, filename):
"""Determine avatar path method.""" """Determine avatar path method."""
filename = '%s.jpeg' % generate_code() filename = '%s.jpeg' % generate_code()

View File

@ -8,6 +8,7 @@ from django.contrib.gis.db import models
from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import JSONField from django.contrib.postgres.fields import JSONField
from django.contrib.postgres.fields.jsonb import KeyTextTransform from django.contrib.postgres.fields.jsonb import KeyTextTransform
from django.core.validators import FileExtensionValidator
from django.utils import timezone from django.utils import timezone
from django.utils.html import mark_safe from django.utils.html import mark_safe
from django.utils.translation import ugettext_lazy as _, get_language from django.utils.translation import ugettext_lazy as _, get_language
@ -16,7 +17,7 @@ from sorl.thumbnail import get_thumbnail
from sorl.thumbnail.fields import ImageField as SORLImageField from sorl.thumbnail.fields import ImageField as SORLImageField
from configuration.models import TranslationSettings from configuration.models import TranslationSettings
from utils.methods import image_path, svg_image_path from utils.methods import image_path, svg_image_path, file_path
from utils.validators import svg_image_validator from utils.validators import svg_image_validator
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -86,6 +87,7 @@ def translate_field(self, field_name, toggle_field_name=None):
return None return None
return value return value
return None return None
return translate return translate
@ -159,6 +161,33 @@ class BaseAttributes(ProjectBaseMixin):
abstract = True abstract = True
class FileMixin(models.Model):
"""File model."""
file = models.FileField(upload_to=file_path,
blank=True, null=True, default=None,
verbose_name=_('File'),
validators=[FileExtensionValidator(
allowed_extensions=('jpg', 'jpeg', 'png', 'doc', 'docx', 'pdf')
)])
class Meta:
"""Meta class."""
abstract = True
def get_file_url(self):
"""Get file url."""
return self.file.url if self.file else None
def get_full_file_url(self, request):
"""Get full file url"""
if self.file and exists(self.file.path):
return request.build_absolute_uri(self.file.url)
else:
return None
class ImageMixin(models.Model): class ImageMixin(models.Model):
"""Avatar model.""" """Avatar model."""
@ -230,7 +259,7 @@ class SORLImageMixin(models.Model):
else: else:
return None return None
def get_cropped_image(self, geometry: str, quality: int, cropbox:str) -> dict: def get_cropped_image(self, geometry: str, quality: int, cropbox: str) -> dict:
cropped_image = get_thumbnail(self.image, cropped_image = get_thumbnail(self.image,
geometry_string=geometry, geometry_string=geometry,
# crop=crop, # crop=crop,

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff