Merge branch 'develop' of ssh://gl.id-east.ru:222/gm/gm-backend into develop

This commit is contained in:
Александр Пархомин 2020-02-10 09:59:08 +03:00
commit 893eb6e877
30 changed files with 922 additions and 114 deletions

2
.gitignore vendored
View File

@ -19,9 +19,11 @@ logs/
/datadir/ /datadir/
/_files/ /_files/
/geoip_db/ /geoip_db/
/venv
# dev # dev
./docker-compose.override.yml ./docker-compose.override.yml
docker-compose.override.yml
celerybeat-schedule celerybeat-schedule
local_files local_files
celerybeat.pid celerybeat.pid

View File

@ -0,0 +1,23 @@
# Generated by Django 2.2.7 on 2020-02-07 11:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0098_auto_20200204_1205'),
]
operations = [
migrations.AddField(
model_name='establishment',
name='last_update_by_gm',
field=models.DateTimeField(null=True),
),
migrations.AddField(
model_name='establishment',
name='last_update_by_manager',
field=models.DateTimeField(null=True),
),
]

View File

@ -33,7 +33,7 @@ from utils.models import (
BaseAttributes, FavoritesMixin, FileMixin, GalleryMixin, HasTagsMixin, BaseAttributes, FavoritesMixin, FileMixin, GalleryMixin, HasTagsMixin,
IntermediateGalleryModelMixin, ProjectBaseMixin, TJSONField, TranslatedFieldsMixin, IntermediateGalleryModelMixin, ProjectBaseMixin, TJSONField, TranslatedFieldsMixin,
TypeDefaultImageMixin, URLImageMixin, default_menu_bool_array, PhoneModelMixin, TypeDefaultImageMixin, URLImageMixin, default_menu_bool_array, PhoneModelMixin,
AwardsModelMixin) AwardsModelMixin, CarouselMixin, UpdateByMixin)
# todo: establishment type&subtypes check # todo: establishment type&subtypes check
@ -137,6 +137,14 @@ class EstablishmentQuerySet(models.QuerySet):
"""Return qs with related reviews.""" """Return qs with related reviews."""
return self.prefetch_related('reviews') return self.prefetch_related('reviews')
def with_reviews_sorted(self):
return self.prefetch_related(
Prefetch(
'reviews',
queryset=Review.objects.published().order_by('-published_at'),
)
)
def with_currency_related(self): def with_currency_related(self):
"""Return qs with related """ """Return qs with related """
return self.prefetch_related('currency') return self.prefetch_related('currency')
@ -547,8 +555,15 @@ class EstablishmentQuerySet(models.QuerySet):
return self.prefetch_related('menu_set', 'menu_set__plates', 'back_office_wine') return self.prefetch_related('menu_set', 'menu_set__plates', 'back_office_wine')
class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin, class Establishment(GalleryMixin,
TranslatedFieldsMixin, HasTagsMixin, FavoritesMixin, AwardsModelMixin): ProjectBaseMixin,
URLImageMixin,
TranslatedFieldsMixin,
HasTagsMixin,
FavoritesMixin,
AwardsModelMixin,
CarouselMixin,
UpdateByMixin):
"""Establishment model.""" """Establishment model."""
ABANDONED = 0 ABANDONED = 0
@ -565,12 +580,12 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
(ABANDONED, _('Abandoned')), (ABANDONED, _('Abandoned')),
(CLOSED, _('Closed')), (CLOSED, _('Closed')),
(PUBLISHED, _('Published')), (PUBLISHED, _('Published')),
(UNPICKED, _('Unpicked')), # (UNPICKED, _('Unpicked')),
(WAITING, _('Waiting')), (WAITING, _('Waiting')),
(HIDDEN, _('Hidden')), # (HIDDEN, _('Hidden')),
(DELETED, _('Deleted')), # (DELETED, _('Deleted')),
(OUT_OF_SELECTION, _('Out of selection')), (OUT_OF_SELECTION, _('Out of selection')),
(UNPUBLISHED, _('Unpublished')), # (UNPUBLISHED, _('Unpublished')),
) )
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
@ -717,9 +732,13 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
raise ValidationError('Establishment type of subtype does not match') raise ValidationError('Establishment type of subtype does not match')
self.establishment_subtypes.add(establishment_subtype) self.establishment_subtypes.add(establishment_subtype)
@property
def last_review(self):
return self.reviews.by_status(Review.READY).last()
@property @property
def vintage_year(self): def vintage_year(self):
last_review = self.reviews.by_status(Review.READY).last() last_review = self.last_review
if last_review: if last_review:
return last_review.vintage return last_review.vintage

View File

@ -1,3 +1,4 @@
from datetime import datetime
from functools import lru_cache from functools import lru_cache
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
@ -9,17 +10,20 @@ from rest_framework import serializers
from slugify import slugify from slugify import slugify
from account import models as account_models from account import models as account_models
from account.models import Role
from account.serializers.common import UserShortSerializer from account.serializers.common import UserShortSerializer
from collection.models import Guide from collection.models import Guide
from establishment import models, serializers as model_serializers from establishment import models, serializers as model_serializers
from establishment.models import ContactEmail, ContactPhone, EstablishmentEmployee from establishment.models import ContactEmail, ContactPhone, EstablishmentEmployee
from establishment.serializers.common import ContactPhonesSerializer from establishment.serializers.common import ContactPhonesSerializer
from review.serializers.common import ReviewBaseSerializer
from gallery.models import Image from gallery.models import Image
from location.serializers import AddressDetailSerializer, TranslatedField, AddressBaseSerializer, \ from location.serializers import AddressDetailSerializer, TranslatedField, AddressBaseSerializer, \
AddressEstablishmentSerializer AddressEstablishmentSerializer
from main import models as main_models from main import models as main_models
from main.models import Currency from main.models import Currency
from main.serializers import AwardSerializer from main.serializers import AwardSerializer
from review.serializers import ReviewBaseSerializer, User
from tag.serializers import TagBaseSerializer from tag.serializers import TagBaseSerializer
from utils.decorators import with_base_attributes from utils.decorators import with_base_attributes
from utils.methods import string_random from utils.methods import string_random
@ -92,12 +96,13 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria
) )
subtypes = model_serializers.EstablishmentSubTypeBaseSerializer(source='establishment_subtypes', subtypes = model_serializers.EstablishmentSubTypeBaseSerializer(source='establishment_subtypes',
read_only=True, many=True) read_only=True, many=True)
reviews = ReviewBaseSerializer(allow_null=True, read_only=True, many=True)
restaurant_category = TagBaseSerializer(read_only=True, many=True, allow_null=True) restaurant_category = TagBaseSerializer(read_only=True, many=True, allow_null=True)
restaurant_cuisine = TagBaseSerializer(read_only=True, many=True, allow_null=True) restaurant_cuisine = TagBaseSerializer(read_only=True, many=True, allow_null=True)
artisan_category = TagBaseSerializer(read_only=True, many=True, allow_null=True) artisan_category = TagBaseSerializer(read_only=True, many=True, allow_null=True)
distillery_type = TagBaseSerializer(read_only=True, many=True, allow_null=True) distillery_type = TagBaseSerializer(read_only=True, many=True, allow_null=True)
food_producer = TagBaseSerializer(read_only=True, many=True, allow_null=True) food_producer = TagBaseSerializer(read_only=True, many=True, allow_null=True)
vintage_year = serializers.IntegerField(read_only=True, allow_null=True)
class Meta(model_serializers.EstablishmentBaseSerializer.Meta): class Meta(model_serializers.EstablishmentBaseSerializer.Meta):
fields = [ fields = [
@ -137,6 +142,8 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria
'artisan_category', 'artisan_category',
'distillery_type', 'distillery_type',
'food_producer', 'food_producer',
'reviews',
'vintage_year',
] ]
def to_representation(self, instance): def to_representation(self, instance):
@ -167,13 +174,9 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria
validated_data['slug'] = slug validated_data['slug'] = slug
if 'address' in validated_data: if 'address' in validated_data:
address_fields = validated_data.pop('address') address = models.Address(**validated_data.pop('address'))
address_instance = get_object_or_404(models.Address, id=address_fields['id'] or None) address.save()
address_id = getattr(address_instance, 'id') validated_data['address_id'] = address.id
models.Address.objects.filter(id=address_id).update(**address_fields)
validated_data['address_id'] = address_id
instance = super().create(validated_data) instance = super().create(validated_data)
@ -215,6 +218,7 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer):
subtypes = model_serializers.EstablishmentSubTypeBaseSerializer(source='establishment_subtypes', subtypes = model_serializers.EstablishmentSubTypeBaseSerializer(source='establishment_subtypes',
read_only=True, many=True) read_only=True, many=True)
type = model_serializers.EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True) type = model_serializers.EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True)
phones = serializers.ListField( phones = serializers.ListField(
source='contact_phones', source='contact_phones',
allow_null=True, allow_null=True,
@ -223,8 +227,11 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer):
required=False, required=False,
write_only=True, write_only=True,
) )
contact_phones = ContactPhonesSerializer(source='phones', read_only=True, many=True) contact_phones = ContactPhonesSerializer(source='phones', read_only=True, many=True)
last_review = ReviewBaseSerializer(read_only=True)
class Meta(model_serializers.EstablishmentBaseSerializer.Meta): class Meta(model_serializers.EstablishmentBaseSerializer.Meta):
fields = [ fields = [
'id', 'id',
@ -253,6 +260,10 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer):
'tags', 'tags',
'status', 'status',
'status_display', 'status_display',
'last_review',
'must_of_the_week',
'last_update_by_gm',
'last_update_by_manager',
] ]
def to_representation(self, instance): def to_representation(self, instance):
@ -260,7 +271,7 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer):
data['phones'] = data.pop('contact_phones', None) data['phones'] = data.pop('contact_phones', None)
return data return data
def update(self, instance, validated_data): def update(self, instance: models.Establishment, validated_data):
phones_list = [] phones_list = []
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')
@ -269,9 +280,30 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer):
if 'contact_emails' in validated_data: if 'contact_emails' in validated_data:
emails_list = validated_data.pop('contact_emails') emails_list = validated_data.pop('contact_emails')
request = self.context.get('request')
if request and hasattr(request, 'user'):
user = request.user
if isinstance(user, User):
is_by_manager = user.userrole_set.filter(
pk=user.pk,
role__in=(
Role.ESTABLISHMENT_MANAGER,
Role.ESTABLISHMENT_ADMINISTRATOR,
Role.COUNTRY_ADMIN
)
).exists()
if is_by_manager:
instance.last_update_by_manager = datetime.now()
else:
''' by gm. '''
instance.last_update_by_gm = datetime.now()
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) emails_handler(emails_list, instance)
return instance return instance
@ -359,6 +391,17 @@ class PositionBackSerializer(serializers.ModelSerializer):
] ]
class AdminEmployeeBackSerializers(serializers.ModelSerializer):
class Meta:
model = models.Employee
fields = [
'id',
'name',
'last_name',
]
# TODO: test decorator # TODO: test decorator
@with_base_attributes @with_base_attributes
class EmployeeBackSerializers(PhoneMixinSerializer, serializers.ModelSerializer): class EmployeeBackSerializers(PhoneMixinSerializer, serializers.ModelSerializer):
@ -972,6 +1015,7 @@ class CardAndWinesSerializer(serializers.ModelSerializer):
class TeamMemberSerializer(serializers.ModelSerializer): class TeamMemberSerializer(serializers.ModelSerializer):
"""Serializer for team establishment BO section""" """Serializer for team establishment BO section"""
class Meta: class Meta:
model = account_models.User model = account_models.User
fields = ( fields = (

View File

@ -60,6 +60,7 @@ urlpatterns = [
path('<int:establishment_id>/employees/', views.EstablishmentEmployeeListView.as_view(), path('<int:establishment_id>/employees/', views.EstablishmentEmployeeListView.as_view(),
name='establishment-employees'), name='establishment-employees'),
path('employees/', views.EmployeeListCreateView.as_view(), name='employees'), path('employees/', views.EmployeeListCreateView.as_view(), name='employees'),
path('employees/for_admin/', views.AdminEmployeeListView.as_view(), name='employees-list-for-admin'),
path('employees/search/', views.EmployeesListSearchViews.as_view(), name='employees-search'), path('employees/search/', views.EmployeesListSearchViews.as_view(), name='employees-search'),
path('employees/<int:pk>/', views.EmployeeRUDView.as_view(), name='employees-rud'), path('employees/<int:pk>/', views.EmployeeRUDView.as_view(), name='employees-rud'),
path('employees/<int:pk>/<int:award_id>', views.RemoveAwardView.as_view(), name='employees-award-delete'), path('employees/<int:pk>/<int:award_id>', views.RemoveAwardView.as_view(), name='employees-award-delete'),

View File

@ -65,7 +65,8 @@ class EstablishmentListCreateView(EstablishmentMixinViews, generics.ListCreateAP
.with_certain_tag_category_related('cuisine', 'restaurant_cuisine') \ .with_certain_tag_category_related('cuisine', 'restaurant_cuisine') \
.with_certain_tag_category_related('shop_category', 'artisan_category') \ .with_certain_tag_category_related('shop_category', 'artisan_category') \
.with_certain_tag_category_related('distillery_type', 'distillery_type') \ .with_certain_tag_category_related('distillery_type', 'distillery_type') \
.with_certain_tag_category_related('producer_type', 'food_producer') .with_certain_tag_category_related('producer_type', 'food_producer') \
.with_reviews_sorted()
class EmployeeEstablishmentPositionsView(generics.ListAPIView): class EmployeeEstablishmentPositionsView(generics.ListAPIView):
@ -209,7 +210,7 @@ class EstablishmentRUDView(EstablishmentMixinViews, generics.RetrieveUpdateDestr
) )
def get_queryset(self): def get_queryset(self):
"""Overridden get_queryset method.""" """An overridden get_queryset method."""
qs = super(EstablishmentRUDView, self).get_queryset() qs = super(EstablishmentRUDView, self).get_queryset()
return qs.prefetch_related( return qs.prefetch_related(
'establishmentemployee_set', 'establishmentemployee_set',
@ -270,9 +271,36 @@ class EstablishmentScheduleRUDView(EstablishmentMixinViews, generics.RetrieveUpd
class EstablishmentScheduleCreateView(generics.CreateAPIView): class EstablishmentScheduleCreateView(generics.CreateAPIView):
""" """
Establishment schedule Create view ## Create establishment schedule
### *POST*
Implement creating Establishment shedule. #### Description
Create schedule for establishment by establishment `slug`.
##### Request
Required:
* weekday (`enum`)
```
0 (Monday),
1 (Tuesday),
2 (Wednesday),
3 (Thursday),
4 (Friday),
5 (Saturday),
6 (Sunday)
```
Non-required:
* lunch_start (str) - time in a format (`ISO-8601`, e.g. - `hh:mm:ss`)
* lunch_end (str) - time in a format (`ISO-8601`, e.g. - `hh:mm:ss`)
* dinner_start (str) - time in a format (`ISO-8601`, e.g. - `hh:mm:ss`)
* dinner_end (str) - time in a format (`ISO-8601`, e.g. - `hh:mm:ss`)
* opening_at (str) - time in a format (`ISO-8601`, e.g. - `hh:mm:ss`)
* closed_at (str) - time in a format (`ISO-8601`, e.g. - `hh:mm:ss`)
##### Response
```
{
"id": 1,
...
}
```
""" """
lookup_field = 'slug' lookup_field = 'slug'
serializer_class = ScheduleCreateSerializer serializer_class = ScheduleCreateSerializer
@ -292,8 +320,8 @@ class CardAndWinesListView(generics.RetrieveAPIView):
queryset = models.Establishment.objects.with_base_related() queryset = models.Establishment.objects.with_base_related()
def get_object(self): def get_object(self):
establishment = models.Establishment.objects.prefetch_plates()\ establishment = models.Establishment.objects.prefetch_plates() \
.filter(pk=self.kwargs['establishment_id'])\ .filter(pk=self.kwargs['establishment_id']) \
.first() .first()
if establishment is None: if establishment is None:
raise Http404 raise Http404
@ -809,7 +837,7 @@ class EmployeesListSearchViews(generics.ListAPIView):
serializer_class = serializers.EmployeeBackSerializers serializer_class = serializers.EmployeeBackSerializers
queryset = ( queryset = (
models.Employee.objects.with_back_office_related() models.Employee.objects.with_back_office_related()
.select_related('photo') .select_related('photo')
) )
permission_classes = get_permission_classes( permission_classes = get_permission_classes(
IsEstablishmentManager, IsEstablishmentManager,
@ -931,6 +959,43 @@ class EmployeeRUDView(generics.RetrieveUpdateDestroyAPIView):
) )
class AdminEmployeeListView(generics.ListAPIView):
"""
## Employee list view, where request user is ESTABLISHMENT_ADMINISTRATOR.
### *GET*
#### Description
Return paginated list of employees.
#### Response
```
{
"id": 1324,
"name": "Alex",
"last_name": "Wolf",
{
```
"""
serializer_class = serializers.AdminEmployeeBackSerializers
permission_classes = get_permission_classes(IsEstablishmentAdministrator, )
pagination_class = None
def get_queryset(self):
user = self.request.user
if user.is_anonymous:
return None
est_ids = models.Establishment.objects.filter(
userrole__user=user,
userrole__role__role=Role.ESTABLISHMENT_ADMINISTRATOR,
).values_list('id', flat=True)
qs = models.Employee.objects.filter(establishments__in=est_ids).distinct().with_back_office_related()
if self.request.country_code:
qs = qs.filter(establishments__address__city__country__code=self.request.country_code)
return qs
class RemoveAwardView(generics.DestroyAPIView): class RemoveAwardView(generics.DestroyAPIView):
""" """
## Remove award view. ## Remove award view.
@ -1060,7 +1125,38 @@ class EstablishmentSubtypeRUDView(generics.RetrieveUpdateDestroyAPIView):
class EstablishmentGalleryCreateDestroyView(EstablishmentMixinViews, class EstablishmentGalleryCreateDestroyView(EstablishmentMixinViews,
CreateDestroyGalleryViewMixin): CreateDestroyGalleryViewMixin):
"""Resource for a create|destroy gallery for establishment for back-office users.""" """
## Establishment gallery image Create/Destroy view
### *POST*
#### Description
Attaching existing **image** by `image identifier` to **establishment** by `establishment slug`
in request kwargs.
##### Request
```
No body
```
##### Response
E.g.:
```
No content
```
### *DELETE*
#### Description
Delete existing **gallery image** from **establishment** gallery, by `image identifier`
and `establishment slug` in request kwargs.
**Note**:
> Image wouldn't be deleted after all.
##### Request
```
No body
```
##### Response
```
No content
```
"""
lookup_field = 'slug' lookup_field = 'slug'
serializer_class = serializers.EstablishmentBackOfficeGallerySerializer serializer_class = serializers.EstablishmentBackOfficeGallerySerializer
permission_classes = get_permission_classes() permission_classes = get_permission_classes()
@ -1083,7 +1179,28 @@ class EstablishmentGalleryCreateDestroyView(EstablishmentMixinViews,
class EstablishmentGalleryListView(EstablishmentMixinViews, class EstablishmentGalleryListView(EstablishmentMixinViews,
generics.ListAPIView): generics.ListAPIView):
"""Resource for returning gallery for establishment for back-office users.""" """
## Establishment gallery image list view
### *GET*
#### Description
Returning paginated list of establishment images by `establishment slug`,
with cropped images.
##### Response
E.g.:
```
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 11,
...
}
]
}
```
"""
lookup_field = 'slug' lookup_field = 'slug'
serializer_class = serializers.ImageBaseSerializer serializer_class = serializers.ImageBaseSerializer
permission_classes = get_permission_classes() permission_classes = get_permission_classes()
@ -1093,7 +1210,7 @@ class EstablishmentGalleryListView(EstablishmentMixinViews,
qs = super(EstablishmentGalleryListView, self).get_queryset() qs = super(EstablishmentGalleryListView, self).get_queryset()
establishment = get_object_or_404(qs, slug=self.kwargs.get('slug')) establishment = get_object_or_404(qs, slug=self.kwargs.get('slug'))
# May raise a permission denied # May raises a permission denied
self.check_object_permissions(self.request, establishment) self.check_object_permissions(self.request, establishment)
return establishment return establishment
@ -1330,10 +1447,10 @@ class EstablishmentGuideCreateDestroyView(generics.GenericAPIView):
lookup_url_kwarg = getattr(self, 'establishment_lookup_url_kwarg', None) lookup_url_kwarg = getattr(self, 'establishment_lookup_url_kwarg', None)
assert lookup_url_kwarg in self.kwargs, ( assert lookup_url_kwarg in self.kwargs, (
'Expected view %s to be called with a URL keyword argument ' 'Expected view %s to be called with a URL keyword argument '
'named "%s". Fix your URL conf, or set the `.lookup_field` ' 'named "%s". Fix your URL conf, or set the `.lookup_field` '
'attribute on the view correctly.' % 'attribute on the view correctly.' %
(self.__class__.__name__, lookup_url_kwarg) (self.__class__.__name__, lookup_url_kwarg)
) )
filters = {'klass': queryset, lookup_url_kwarg: self.kwargs.get(lookup_url_kwarg)} filters = {'klass': queryset, lookup_url_kwarg: self.kwargs.get(lookup_url_kwarg)}
@ -1351,10 +1468,10 @@ class EstablishmentGuideCreateDestroyView(generics.GenericAPIView):
lookup_url_kwarg = getattr(self, 'guide_lookup_url_kwarg', None) lookup_url_kwarg = getattr(self, 'guide_lookup_url_kwarg', None)
assert lookup_url_kwarg in self.kwargs, ( assert lookup_url_kwarg in self.kwargs, (
'Expected view %s to be called with a URL keyword argument ' 'Expected view %s to be called with a URL keyword argument '
'named "%s". Fix your URL conf, or set the `.lookup_field` ' 'named "%s". Fix your URL conf, or set the `.lookup_field` '
'attribute on the view correctly.' % 'attribute on the view correctly.' %
(self.__class__.__name__, lookup_url_kwarg) (self.__class__.__name__, lookup_url_kwarg)
) )
obj = get_object_or_404(klass=queryset, id=self.kwargs.get(lookup_url_kwarg)) obj = get_object_or_404(klass=queryset, id=self.kwargs.get(lookup_url_kwarg))
@ -1535,4 +1652,4 @@ class EstablishmentAwardCreateAndBind(generics.CreateAPIView, generics.DestroyAP
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
establishment = get_object_or_404(models.Establishment, id=kwargs['establishment_id']) establishment = get_object_or_404(models.Establishment, id=kwargs['establishment_id'])
establishment.remove_award(kwargs['award_id']) establishment.remove_award(kwargs['award_id'])
return self._award_list_for_establishment(kwargs['establishment_id'], status.HTTP_200_OK) return self._award_list_for_establishment(kwargs['establishment_id'], status.HTTP_200_OK)

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2020-02-06 19:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gallery', '0009_auto_20200206_1749'),
]
operations = [
migrations.AlterField(
model_name='image',
name='is_public',
field=models.BooleanField(default=True, verbose_name='Is media source public'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 2.2.7 on 2020-02-08 19:00
import django.core.validators
from django.db import migrations, models
import re
class Migration(migrations.Migration):
dependencies = [
('gallery', '0010_auto_20200206_1944'),
]
operations = [
migrations.AddField(
model_name='image',
name='cropbox',
field=models.CharField(default=None, max_length=500, null=True, validators=[django.core.validators.RegexValidator(re.compile('^\\d+(?:,\\d+)*\\Z'), code='invalid', message='Enter only digits separated by commas.')], verbose_name='x1,y1,x2,y2 crop settings'),
),
]

View File

@ -1,5 +1,7 @@
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.core import validators
from sorl.thumbnail import get_thumbnail
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
from django.conf import settings from django.conf import settings
from project.storage_backends import PublicMediaStorage from project.storage_backends import PublicMediaStorage
@ -43,6 +45,8 @@ class Image(BaseAttributes, SORLImageMixin, PlatformMixin):
default=None) default=None)
link = models.URLField(blank=True, null=True, default=None, verbose_name=_('mp4 or youtube video link')) link = models.URLField(blank=True, null=True, default=None, verbose_name=_('mp4 or youtube video link'))
order = models.PositiveIntegerField(default=0, verbose_name=_('Sorting order')) order = models.PositiveIntegerField(default=0, verbose_name=_('Sorting order'))
cropbox = models.CharField(max_length=500, validators=[validators.validate_comma_separated_integer_list], null=True,
default=None, verbose_name=_('x1,y1,x2,y2 crop settings'))
objects = ImageQuerySet.as_manager() objects = ImageQuerySet.as_manager()
class Meta: class Meta:
@ -55,6 +59,16 @@ class Image(BaseAttributes, SORLImageMixin, PlatformMixin):
"""String representation""" """String representation"""
return f'{self.id}' return f'{self.id}'
@property
def image_by_cropbox(self):
"""Returns cropped image if cropbox is set"""
if self.cropbox and self.image:
x1, y1, x2, y2 = map(int, self.cropbox.split(','))
return get_thumbnail(self.image,
geometry_string=f'{round(x2 - x1)}x{round(y2 - y1)}',
cropbox=self.cropbox,
quality=100)
def set_pubic(self, is_public=True): def set_pubic(self, is_public=True):
if not settings.AWS_STORAGE_BUCKET_NAME: if not settings.AWS_STORAGE_BUCKET_NAME:
"""Backend doesn't use aws s3""" """Backend doesn't use aws s3"""
@ -69,6 +83,12 @@ class Image(BaseAttributes, SORLImageMixin, PlatformMixin):
else: else:
file_object.Acl().put(ACL='authenticated-read') file_object.Acl().put(ACL='authenticated-read')
@property
def is_main(self) -> bool:
establishment_gallery_list = list(self.establishment_gallery.all())
if establishment_gallery_list and len(establishment_gallery_list):
return establishment_gallery_list[0].is_main
@property @property
def type(self) -> str: def type(self) -> str:
if self.image: if self.image:

View File

@ -6,7 +6,7 @@ from sorl.thumbnail.parsers import parse_crop, ThumbnailParseError
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.conf import settings from django.conf import settings
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from establishment.models import Establishment from establishment.models import Establishment, EstablishmentGallery
from account.serializers.common import UserBaseSerializer from account.serializers.common import UserBaseSerializer
from . import models from . import models
@ -53,6 +53,8 @@ class EstablishmentGallerySerializer(serializers.ModelSerializer):
type = serializers.ChoiceField(read_only=True, choices=models.Image.MEDIA_TYPES) type = serializers.ChoiceField(read_only=True, choices=models.Image.MEDIA_TYPES)
created_by = UserBaseSerializer(read_only=True, allow_null=True) created_by = UserBaseSerializer(read_only=True, allow_null=True)
image_size_in_KB = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=20) image_size_in_KB = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=20)
is_main = serializers.BooleanField()
cropped_image = serializers.ImageField(source='image_by_cropbox', allow_null=True, read_only=True)
class Meta: class Meta:
model = models.Image model = models.Image
@ -65,11 +67,16 @@ class EstablishmentGallerySerializer(serializers.ModelSerializer):
'preview', 'preview',
'is_public', 'is_public',
'title', 'title',
'is_main',
'created_by', 'created_by',
'created',
'image_size_in_KB', 'image_size_in_KB',
'cropbox',
'cropped_image',
) )
extra_kwargs = { extra_kwargs = {
'created': {'read_only': True}, 'created': {'read_only': True},
'created_by': {'read_only': True},
} }
def validate(self, attrs): def validate(self, attrs):
@ -78,24 +85,43 @@ class EstablishmentGallerySerializer(serializers.ModelSerializer):
if image and image.size >= settings.FILE_UPLOAD_MAX_MEMORY_SIZE: if image and image.size >= settings.FILE_UPLOAD_MAX_MEMORY_SIZE:
raise serializers.ValidationError({'detail': _('File size too large: %s bytes') % image.size}) raise serializers.ValidationError({'detail': _('File size too large: %s bytes') % image.size})
if attrs.get('cropbox'):
if len(attrs['cropbox'].split(',')) != 4:
raise serializers.ValidationError({'detail': _('Cropbox contains 4 integer values separated by comma.')})
return attrs return attrs
def create(self, validated_data): def create(self, validated_data):
is_main = validated_data.pop('is_main')
establishment = get_object_or_404(klass=Establishment, pk=self.context['view'].kwargs['establishment_id']) establishment = get_object_or_404(klass=Establishment, pk=self.context['view'].kwargs['establishment_id'])
instance = super().create(validated_data) instance = super().create(validated_data)
instance.created_by = self.context['request'].user instance.created_by = self.context['request'].user
instance.establishment_set.add(establishment) instance.establishment_set.add(establishment)
instance.save() instance.save()
if is_main:
EstablishmentGallery.objects.filter(
establishment=establishment
).update(is_main=False) # reset all before setting True on some instance
EstablishmentGallery.objects.filter(
image=instance
).update(is_main=is_main)
return instance return instance
def update(self, instance: models.Image, validated_data): def update(self, instance: models.Image, validated_data):
if instance.is_public != validated_data.get('is_public'): if instance.is_public != validated_data.get('is_public'):
instance.set_pubic(validated_data.get('is_public', True)) instance.set_pubic(validated_data.get('is_public', True))
if 'is_main' in validated_data:
is_main = validated_data.pop('is_main')
if is_main:
establishment = instance.establishment_gallery.all()[0].establishment
EstablishmentGallery.objects.filter(
establishment=establishment
).update(is_main=False) # reset all before setting True on some instance
EstablishmentGallery.objects.filter(
image=instance
).update(is_main=is_main)
return super().update(instance, validated_data) return super().update(instance, validated_data)
class CropImageSerializer(ImageSerializer): class CropImageSerializer(ImageSerializer):
"""Serializers for image crops.""" """Serializers for image crops."""

View File

@ -1,6 +1,7 @@
from django.conf import settings from django.conf import settings
from django.db.transaction import on_commit from django.db.transaction import on_commit
from rest_framework import generics, status from rest_framework import generics, status
from rest_framework.permissions import AllowAny
from rest_framework.response import Response from rest_framework.response import Response
from utils.methods import get_permission_classes from utils.methods import get_permission_classes
@ -8,6 +9,7 @@ from utils.permissions import IsContentPageManager, IsCountryAdmin, IsEstablishm
IsProducerFoodInspector, IsEstablishmentAdministrator IsProducerFoodInspector, IsEstablishmentAdministrator
from . import tasks, models, serializers from . import tasks, models, serializers
class ImageBaseView(generics.GenericAPIView): class ImageBaseView(generics.GenericAPIView):
"""Base Image view.""" """Base Image view."""
model = models.Image model = models.Image
@ -19,24 +21,83 @@ class ImageBaseView(generics.GenericAPIView):
class ImageListCreateView(ImageBaseView, generics.ListCreateAPIView): class ImageListCreateView(ImageBaseView, generics.ListCreateAPIView):
"""List/Create Image view.""" """
## List/Create view
### *GET*
#### Description
Get paginated list of images, with ordering by field `modified` (descending)
#### Response
E.g.:
```
{
"count": 40595,
"next": 2,
"previous": null,
"results": [
{
"id": 47336,
...
}
]
}
```
### *POST*
#### Description
Upload an image on a server.
##### Request
Required:
* file (`file`) - download file
Available:
* orientation (`enum`) - default: `null`
```
0 (Horizontal)
1 (Vertical)
```
* title (`str`) - title of image file (default - `''`)
* is_public (`bool`) - flag that responds for availability
for displaying (default - `True`)
* preview (`file`) - download preview file (default - `null`)
* link (`str`) - mp4 or youtube video link (default - `null`)
* order (`int`) - order number (default - `0`)
##### Response
E.g.:
```
{
"id": 47336,
...
}
```
"""
class MediaForEstablishmentView(ImageBaseView, generics.ListCreateAPIView): class MediaForEstablishmentView(ImageBaseView, generics.ListCreateAPIView):
"""View for creating and retrieving certain establishment media.""" """View for creating and retrieving certain establishment media."""
pagination_class = None pagination_class = None
permission_classes = (IsCountryAdmin, IsEstablishmentAdministrator, IsEstablishmentManager, IsProducerFoodInspector) # permission_classes = (IsCountryAdmin, IsEstablishmentAdministrator, IsEstablishmentManager, IsProducerFoodInspector)
permission_classes = (AllowAny, )
serializer_class = serializers.EstablishmentGallerySerializer serializer_class = serializers.EstablishmentGallerySerializer
def get_queryset(self): def get_queryset(self):
return super().get_queryset().filter(establishment__pk=self.kwargs['establishment_id'])\ return super().get_queryset().filter(establishment__pk=self.kwargs['establishment_id'])\
.order_by('-order').prefetch_related('created_by') .order_by('-establishment_gallery__is_main', '-order').prefetch_related('created_by',
'establishment_gallery')
class MediaUpdateView(ImageBaseView, generics.UpdateAPIView): class MediaUpdateView(ImageBaseView, generics.UpdateAPIView, generics.DestroyAPIView):
"""View for updating media data""" """View for updating media data"""
serializer_class = serializers.EstablishmentGallerySerializer serializer_class = serializers.EstablishmentGallerySerializer
permission_classes = () # permission_classes = (IsCountryAdmin, IsEstablishmentAdministrator, IsEstablishmentManager, IsProducerFoodInspector)
permission_classes = (AllowAny, )
def delete(self, request, *args, **kwargs):
"""Override destroy view"""
instance = self.get_object()
if settings.USE_CELERY:
on_commit(lambda: tasks.delete_image.delay(image_id=instance.id))
else:
on_commit(lambda: tasks.delete_image(image_id=instance.id))
return Response(status=status.HTTP_204_NO_CONTENT)
class ImageRetrieveDestroyView(ImageBaseView, generics.RetrieveDestroyAPIView): class ImageRetrieveDestroyView(ImageBaseView, generics.RetrieveDestroyAPIView):

View File

@ -223,11 +223,18 @@ class AddressBaseSerializer(serializers.ModelSerializer):
class AddressEstablishmentSerializer(AddressBaseSerializer): class AddressEstablishmentSerializer(AddressBaseSerializer):
"""Address serializer.""" """Address serializer."""
id = serializers.IntegerField(required=True) id = serializers.IntegerField(required=False)
street_name_1 = serializers.CharField(required=False, default='') street_name_1 = serializers.CharField(required=False, allow_blank=True, default='')
street_name_2 = serializers.CharField(required=False, default='') street_name_2 = serializers.CharField(required=False, allow_blank=True, default='')
number = serializers.IntegerField(required=False, default=0) number = serializers.IntegerField(required=False, default=0)
postal_code = serializers.CharField(required=False, default='') postal_code = serializers.CharField(required=False, default='')
city_id = serializers.PrimaryKeyRelatedField(
source='city',
queryset=models.City.objects.all(),
write_only=True,
required=True,
)
city = CityBaseSerializer(read_only=True)
class Meta(AddressBaseSerializer.Meta): class Meta(AddressBaseSerializer.Meta):
"""Meta class.""" """Meta class."""
@ -238,6 +245,8 @@ class AddressEstablishmentSerializer(AddressBaseSerializer):
'street_name_2', 'street_name_2',
'number', 'number',
'postal_code', 'postal_code',
'city_id',
'city',
) )

View File

@ -38,3 +38,23 @@ class AwardFilter(filters.FilterSet):
if value not in EMPTY_VALUES: if value not in EMPTY_VALUES:
return queryset.by_employee_id(value, content_type='establishmentemployee') return queryset.by_employee_id(value, content_type='establishmentemployee')
return queryset return queryset
class AwardTypeFilterSet(filters.FilterSet):
"""Award type FilterSet."""
id = filters.NumberFilter(help_text='Filter by AwardType identifier.')
name = filters.CharFilter(method='by_name', help_text='Filter by AwardType name.')
class Meta:
"""Meta class."""
model = models.AwardType
fields = [
'id',
'name',
]
def by_name(self, queryset, name, value):
if value not in EMPTY_VALUES:
return queryset.by_name(value)
return queryset

View File

@ -223,6 +223,10 @@ class AwardTypeQuerySet(models.QuerySet):
"""Filter QuerySet by country code.""" """Filter QuerySet by country code."""
return self.filter(country__code=country_code) return self.filter(country__code=country_code)
def by_name(self, name: str):
"""Filter by name field."""
return self.filter(name__icontains=name)
class AwardType(models.Model): class AwardType(models.Model):
"""AwardType model.""" """AwardType model."""

View File

@ -3,6 +3,7 @@ from rest_framework import serializers
from account.models import User from account.models import User
from account.serializers import BackUserSerializer from account.serializers import BackUserSerializer
from main import models from main import models
from main.serializers import CarouselListSerializer
class PanelSerializer(serializers.ModelSerializer): class PanelSerializer(serializers.ModelSerializer):
@ -27,3 +28,15 @@ class PanelSerializer(serializers.ModelSerializer):
'user', 'user',
'user_id' 'user_id'
] ]
class BackCarouselListSerializer(CarouselListSerializer):
"""Serializer for retrieving list of carousel items."""
class Meta:
"""Meta class."""
model = models.Carousel
fields = CarouselListSerializer.Meta.fields + [
'active',
]

View File

@ -1,11 +1,14 @@
"""Main app serializers.""" """Main app serializers."""
from typing import Union
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from rest_framework import serializers from rest_framework import serializers
from establishment.models import Employee
from location.serializers import CountrySerializer from location.serializers import CountrySerializer
from main import models from main import models
from establishment.models import Employee
from tag.serializers import TagBackOfficeSerializer from tag.serializers import TagBackOfficeSerializer
from utils.exceptions import EmployeeNotFoundError
from utils.serializers import ProjectModelSerializer, RecursiveFieldSerializer, TranslatedField from utils.serializers import ProjectModelSerializer, RecursiveFieldSerializer, TranslatedField
@ -207,6 +210,7 @@ class AwardBaseSerializer(serializers.ModelSerializer):
"""Award base serializer.""" """Award base serializer."""
title_translated = serializers.CharField(read_only=True, allow_null=True) title_translated = serializers.CharField(read_only=True, allow_null=True)
title = serializers.CharField(write_only=True, help_text='Title text')
class Meta: class Meta:
model = models.Award model = models.Award
@ -215,45 +219,63 @@ class AwardBaseSerializer(serializers.ModelSerializer):
'title_translated', 'title_translated',
'vintage_year', 'vintage_year',
'image_url', 'image_url',
'title',
] ]
@property
def request(self):
"""Return a request object"""
return self.context.get('request')
@property
def context_kwargs(self) -> Union[dict, None]:
"""Return a request kwargs."""
if hasattr(self.request, 'parser_context'):
return self.request.parser_context.get('kwargs')
def validate_title(self, value) -> dict:
"""Construct title str to JSON that contains locale from request."""
return {self.request.locale: value}
class AwardSerializer(AwardBaseSerializer): class AwardSerializer(AwardBaseSerializer):
"""Award serializer.""" """Award serializer."""
award_type = AwardTypeBaseSerializer(read_only=True) award_type = AwardTypeBaseSerializer(read_only=True)
class Meta: class Meta(AwardBaseSerializer.Meta):
model = models.Award
fields = AwardBaseSerializer.Meta.fields + ['award_type', ] fields = AwardBaseSerializer.Meta.fields + ['award_type', ]
class BackAwardSerializer(AwardBaseSerializer): class BackAwardSerializer(AwardBaseSerializer):
"""Award serializer.""" """Award serializer."""
award_type_display = AwardTypeBaseSerializer(read_only=True,
source='award_type')
award_type = serializers.PrimaryKeyRelatedField(
queryset=models.AwardType.objects.all(),
write_only=True,
required=True,
)
award_type = AwardTypeBaseSerializer(read_only=True) class Meta(AwardBaseSerializer.Meta):
class Meta:
model = models.Award
fields = AwardBaseSerializer.Meta.fields + [ fields = AwardBaseSerializer.Meta.fields + [
'award_type', 'award_type',
'award_type_display',
'state', 'state',
'content_type', 'content_type',
'object_id', 'object_id',
] ]
def to_representation(self, instance):
data = super(BackAwardSerializer, self).to_representation(instance)
data['award_type'] = data.pop('award_type_display', None)
return data
class BackAwardEmployeeCreateSerializer(serializers.ModelSerializer):
class BackAwardEmployeeCreateSerializer(AwardBaseSerializer):
"""Award, The Creator.""" """Award, The Creator."""
award_type = serializers.PrimaryKeyRelatedField(required=True, queryset=models.AwardType.objects.all()) award_type = serializers.PrimaryKeyRelatedField(required=True, queryset=models.AwardType.objects.all())
title = serializers.CharField(write_only=True)
def get_title(self, obj): class Meta(AwardBaseSerializer.Meta):
pass
class Meta:
model = models.Award
fields = ( fields = (
'id', 'id',
'award_type', 'award_type',
@ -262,9 +284,15 @@ class BackAwardEmployeeCreateSerializer(serializers.ModelSerializer):
) )
def validate(self, attrs): def validate(self, attrs):
attrs['object_id'] = self.context.get('request').parser_context.get('kwargs')['employee_id'] """An overridden validate method."""
employee_id = self.context_kwargs.get('employee_id')
employee_qs = Employee.objects.filter(id=employee_id)
if not employee_qs.exists():
raise EmployeeNotFoundError()
attrs['object_id'] = employee_id
attrs['content_type'] = ContentType.objects.get_for_model(Employee) attrs['content_type'] = ContentType.objects.get_for_model(Employee)
attrs['title'] = {self.context.get('request').locale: attrs['title']}
return attrs return attrs

View File

@ -8,7 +8,8 @@ app_name = 'main'
urlpatterns = [ urlpatterns = [
path('awards/', views.AwardLstView.as_view(), name='awards-list-create'), path('awards/', views.AwardLstView.as_view(), name='awards-list-create'),
path('awards/<int:id>/', views.AwardRUDView.as_view(), name='awards-rud'), path('awards/<int:id>/', views.AwardRUDView.as_view(), name='awards-rud'),
path('awards/create-and-bind/<int:employee_id>/', views.AwardCreateAndBind.as_view(), name='award-employee-create'), path('awards/create-and-bind/<int:employee_id>/', views.AwardCreateAndBind.as_view(),
name='award-employee-create'),
path('award-types/', views.AwardTypesListView.as_view(), name='awards-types-list'), path('award-types/', views.AwardTypesListView.as_view(), name='awards-types-list'),
path('content_type/', views.ContentTypeView.as_view(), name='content_type-list'), path('content_type/', views.ContentTypeView.as_view(), name='content_type-list'),
path('sites/', views.SiteListBackOfficeView.as_view(), name='site-list-create'), path('sites/', views.SiteListBackOfficeView.as_view(), name='site-list-create'),
@ -28,7 +29,6 @@ urlpatterns = [
path('panels/<int:pk>/', views.PanelsRUDView.as_view(), name='panels-rud'), path('panels/<int:pk>/', views.PanelsRUDView.as_view(), name='panels-rud'),
path('panels/<int:pk>/execute/', views.PanelsExecuteView.as_view(), name='panels-execute'), path('panels/<int:pk>/execute/', views.PanelsExecuteView.as_view(), name='panels-execute'),
path('panels/<int:pk>/csv/', views.PanelsExportCSVView.as_view(), name='panels-csv'), path('panels/<int:pk>/csv/', views.PanelsExportCSVView.as_view(), name='panels-csv'),
path('panels/<int:pk>/xls/', views.PanelsExecuteXLSView.as_view(), name='panels-xls') path('panels/<int:pk>/xls/', views.PanelsExecuteXLSView.as_view(), name='panels-xls'),
path('carousel/', views.BackCarouselListView.as_view(), name='carousel-list'),
] ]

View File

@ -9,15 +9,61 @@ from establishment.models import Employee
from establishment.serializers.back import EmployeeBackSerializers from establishment.serializers.back import EmployeeBackSerializers
from main import serializers from main import serializers
from main import tasks from main import tasks
from main.filters import AwardFilter from main.filters import AwardFilter, AwardTypeFilterSet
from main.models import Award, Footer, PageType, Panel, SiteFeature, Feature, AwardType from main.models import Award, Footer, PageType, Panel, SiteFeature, Feature, AwardType, Carousel
from main.serializers.back import PanelSerializer from main.serializers.back import PanelSerializer, BackCarouselListSerializer
from main.views import SiteSettingsView, SiteListView from main.views import SiteSettingsView, SiteListView
from utils.methods import get_permission_classes from utils.methods import get_permission_classes
class AwardLstView(generics.ListCreateAPIView): class AwardLstView(generics.ListCreateAPIView):
"""Award list create view.""" """
## List of awards
### *GET*
#### Description
Return paginated list of awards.
Available filters:
* establishment_id (`int`) - Filter by establishment identifier
* product_id (`int`) - Filter by product identifier
* employee_id (`int`) - Filter by employee identifier
* state (`enum`) - `0 (Waiting)`, `1 (Published)`
* award_type (`str`) - Filter by award type identifier
* vintage_year (`str`) - Filter by a vintage year
##### Response
E.g.:
```
{
"count": 58,
"next": 2,
"previous": null,
"results": [
{
"id": 1,
...
}
]
}
```
### *POST*
#### Description
Create a record in Award table.
##### Request
Required:
* content_type (`int`) - identifier of content type entity
* object_id (`int`) - identifier of content object
* award_type (`int`) - identifier of award type
* title (`str`) - title of an award
Non required:
* vintage_year (str) - vintage year in a format - `yyyy`
##### Response
```
{
"id": 1,
...
}
```
"""
queryset = Award.objects.all().with_base_related() queryset = Award.objects.all().with_base_related()
serializer_class = serializers.BackAwardSerializer serializer_class = serializers.BackAwardSerializer
permission_classes = get_permission_classes() permission_classes = get_permission_classes()
@ -25,7 +71,35 @@ class AwardLstView(generics.ListCreateAPIView):
class AwardCreateAndBind(generics.CreateAPIView): class AwardCreateAndBind(generics.CreateAPIView):
"""Award create and bind to employee by id""" """
## Creating an Award for an Employee.
### *POST*
#### Description
Creating an Award for an Employee and return in response
serialized Employee object.
##### Response
E.g.
```
{
"id": 1,
...
}
```
##### Request
Required:
* award_type (`int`) - identifier of award type
* title (`str`) - title of an award
Non required:
* vintage_year (str) - vintage year in a format - `yyyy`
##### Response
E.g.
```
{
"id": 1,
...
}
```
"""
queryset = Award.objects.all().with_base_related() queryset = Award.objects.all().with_base_related()
serializer_class = serializers.BackAwardEmployeeCreateSerializer serializer_class = serializers.BackAwardEmployeeCreateSerializer
permission_classes = get_permission_classes() permission_classes = get_permission_classes()
@ -41,7 +115,52 @@ class AwardCreateAndBind(generics.CreateAPIView):
class AwardRUDView(generics.RetrieveUpdateDestroyAPIView): class AwardRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Award RUD view.""" """
## Retrieve/Update/Destroy Award view
### *GET*
#### Description
Retrieving serialized object of an Award by an identifier
#### Response
E.g.
```
{
"id": 1,
...
}
```
### *PATCH*
#### Description
Partially update Award object by identifier
##### Request
Available:
* content_type (`int`) - identifier of content type entity
* object_id (`int`) - identifier of content object
* award_type (`int`) - identifier of award type
* title (`str`) - title of an award
* vintage_year (str) - vintage year in a format - `yyyy`
##### Response
E.g.
```
{
"id": 1,
...
}
```
### *DELETE*
#### Description
Delete an Award instance by award identifier
##### Request
```
No request data
```
##### Response
E.g.
```
No content
```
"""
queryset = Award.objects.all().with_base_related() queryset = Award.objects.all().with_base_related()
serializer_class = serializers.BackAwardSerializer serializer_class = serializers.BackAwardSerializer
permission_classes = get_permission_classes() permission_classes = get_permission_classes()
@ -49,20 +168,32 @@ class AwardRUDView(generics.RetrieveUpdateDestroyAPIView):
class AwardTypesListView(generics.ListAPIView): class AwardTypesListView(generics.ListAPIView):
"""AwardType List view.""" """
## List of Award types view.
### *GET*
#### Description
Return non paginated list of Award types filtered by request country code.
Available filters:
* id (`int`) - award type identifier
* name (`str`) - award type name
##### Response
```
[
{
"id": 1,
...
}
]
```
"""
pagination_class = None pagination_class = None
serializer_class = serializers.AwardTypeBaseSerializer serializer_class = serializers.AwardTypeBaseSerializer
permission_classes = get_permission_classes() permission_classes = get_permission_classes()
filter_backends = (DjangoFilterBackend,)
ordering_fields = '__all__'
lookup_field = 'id' lookup_field = 'id'
filterset_fields = ( filter_class = AwardTypeFilterSet
'id',
'name',
)
def get_queryset(self): def get_queryset(self):
"""Overridden get_queryset method.""" """An overridden get_queryset method."""
if hasattr(self, 'request') and hasattr(self.request, 'country_code'): if hasattr(self, 'request') and hasattr(self.request, 'country_code'):
return AwardType.objects.by_country_code(self.request.country_code) return AwardType.objects.by_country_code(self.request.country_code)
return AwardType.objects.none() return AwardType.objects.none()
@ -200,3 +331,40 @@ class PanelsExecuteXLSView(PanelsExecuteView):
{"success": _('The file will be sent to your email.')}, {"success": _('The file will be sent to your email.')},
status=status.HTTP_200_OK status=status.HTTP_200_OK
) )
class BackCarouselListView(generics.ListAPIView):
"""
## List of carousel.
### *GET*
#### Description
Return list of carousel items.
##### Response
E.g.:
```
{
"id": 1,
"model_name": "model_name",
"name": "name",
...
"awards": [
{
"id": 1,
...
}
]
}
```
"""
queryset = Carousel.objects.all()
serializer_class = BackCarouselListSerializer
permission_classes = get_permission_classes()
pagination_class = None
def get_queryset(self):
country_code = self.request.country_code
qs = Carousel.objects.all()
if country_code:
qs = qs.by_country_code(country_code)
return qs

View File

@ -22,8 +22,10 @@ from utils.models import (
BaseAttributes, FavoritesMixin, GalleryMixin, HasTagsMixin, IntermediateGalleryModelMixin, BaseAttributes, FavoritesMixin, GalleryMixin, HasTagsMixin, IntermediateGalleryModelMixin,
ProjectBaseMixin, ProjectBaseMixin,
TJSONField, TranslatedFieldsMixin, TypeDefaultImageMixin, TJSONField, TranslatedFieldsMixin, TypeDefaultImageMixin,
) CarouselMixin)
from utils.querysets import TranslationQuerysetMixin from utils.querysets import TranslationQuerysetMixin
from location.models import Country
from utils.parsers import NewsSlug
class Agenda(ProjectBaseMixin, TranslatedFieldsMixin): class Agenda(ProjectBaseMixin, TranslatedFieldsMixin):
@ -257,8 +259,12 @@ class NewsQuerySet(TranslationQuerysetMixin):
return self.filter(site__country__code=country_code) if not user.is_superuser else self return self.filter(site__country__code=country_code) if not user.is_superuser else self
class News(GalleryMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin, class News(GalleryMixin,
FavoritesMixin): BaseAttributes,
TranslatedFieldsMixin,
HasTagsMixin,
FavoritesMixin,
CarouselMixin):
"""News model.""" """News model."""
STR_FIELD_NAME = 'title' STR_FIELD_NAME = 'title'
@ -358,24 +364,41 @@ class News(GalleryMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin,
return f'news: {next(iter(self.slugs.values()))}' return f'news: {next(iter(self.slugs.values()))}'
def create_duplicate(self, new_country, view_count_model): def create_duplicate(self, new_country, view_count_model):
country_codes = list(Country.objects.all().values_list('code', flat=True))
# Get all existed slugs
all_slugs = {slug_value
for slug_dict in News.objects.all().values_list('slugs', flat=True)
for slug_value in slug_dict.values()}
new_slugs = {}
for locale, raw_slug in self.slugs.items():
slug = NewsSlug.parse(raw_slug, country_codes)
# all slugs LIKE% slug
similar_slugs = sorted(x for x in all_slugs if NewsSlug.parse(x, country_codes).value == slug.value)
if len(similar_slugs) == 0:
# It is impossible because at least current instance has slug
raise ValueError('Duplicating unsaved object')
else:
# The last slug in similar_slugs is slug with largest count
last_slug = NewsSlug.parse(similar_slugs[-1], country_codes)
new_slug = NewsSlug(slug.value, new_country.code, last_slug.count)
if last_slug.country_code is not None:
new_slug.count += 1
new_slugs[locale] = str(new_slug)
self.pk = None self.pk = None
self.state = self.UNPUBLISHED self.state = self.UNPUBLISHED
self.slugs = {locale: f'{slug}-{new_country.code}' for locale, slug in self.slugs.items()} self.slugs = new_slugs
self.country = new_country self.country = new_country
self.views_count = view_count_model self.views_count = view_count_model
self.duplication_date = timezone.now() self.duplication_date = timezone.now()
self.save() self.save()
@property
def must_of_the_week(self) -> bool:
"""Detects whether current item in carousel"""
kwargs = {
'content_type': ContentType.objects.get_for_model(self),
'object_id': self.pk,
'country': self.country,
}
return Carousel.objects.filter(**kwargs).exists()
@property @property
def publication_datetime(self): def publication_datetime(self):
"""Represents datetime object combined from `publication_date` & `publication_time` fields""" """Represents datetime object combined from `publication_date` & `publication_time` fields"""

View File

@ -176,7 +176,38 @@ class NewsBackOfficeLCView(NewsBackOfficeMixinView,
class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView, class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView,
CreateDestroyGalleryViewMixin): CreateDestroyGalleryViewMixin):
"""Resource for a create gallery for news for back-office users.""" """
## News gallery image Create/Destroy view
### *POST*
#### Description
Attaching existing **image** by `image identifier` to **news** by `news identifier`
in request kwargs.
##### Request
```
No body
```
##### Response
E.g.:
```
No content
```
### *DELETE*
#### Description
Delete existing **gallery image** from **news** gallery, by `image identifier`
and `news identifier` in request kwargs.
**Note**:
> Image wouldn't be deleted after all.
##### Request
```
No body
```
##### Response
```
No content
```
"""
serializer_class = serializers.NewsBackOfficeGallerySerializer serializer_class = serializers.NewsBackOfficeGallerySerializer
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
@ -203,7 +234,28 @@ class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView,
class NewsBackOfficeGalleryListView(NewsBackOfficeMixinView, class NewsBackOfficeGalleryListView(NewsBackOfficeMixinView,
generics.ListAPIView): generics.ListAPIView):
"""Resource for returning gallery for news for back-office users.""" """
## News gallery image list view
### *GET*
#### Description
Returning paginated list of news images by `news identifier`,
with cropped images.
##### Response
E.g.:
```
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 11,
...
}
]
}
```
"""
serializer_class = ImageBaseSerializer serializer_class = ImageBaseSerializer
def get_object(self): def get_object(self):

View File

@ -46,7 +46,38 @@ class ProductSubTypeBackOfficeMixinView:
class ProductBackOfficeGalleryCreateDestroyView(ProductBackOfficeMixinView, class ProductBackOfficeGalleryCreateDestroyView(ProductBackOfficeMixinView,
CreateDestroyGalleryViewMixin): CreateDestroyGalleryViewMixin):
"""Resource for a create gallery for product for back-office users.""" """
## Product gallery image Create/Destroy view
### *POST*
#### Description
Attaching existing **image** by `image identifier` to **product** by `product identifier`
in request kwargs.
##### Request
```
No body
```
##### Response
E.g.:
```
No content
```
### *DELETE*
#### Description
Delete existing **gallery image** from **product** gallery, by `image identifier`
and `product identifier` in request kwargs.
**Note**:
> Image wouldn't be deleted after all.
##### Request
```
No body
```
##### Response
```
No content
```
"""
serializer_class = serializers.ProductBackOfficeGallerySerializer serializer_class = serializers.ProductBackOfficeGallerySerializer
def get_object(self): def get_object(self):
@ -66,7 +97,28 @@ class ProductBackOfficeGalleryCreateDestroyView(ProductBackOfficeMixinView,
class ProductBackOfficeGalleryListView(ProductBackOfficeMixinView, class ProductBackOfficeGalleryListView(ProductBackOfficeMixinView,
generics.ListAPIView): generics.ListAPIView):
"""Resource for returning gallery for product for back-office users.""" """
## Product gallery image list view
### *GET*
#### Description
Returning paginated list of product images by `product identifier`,
with cropped images.
##### Response
E.g.:
```
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 11,
...
}
]
}
```
"""
serializer_class = ImageBaseSerializer serializer_class = ImageBaseSerializer
def get_object(self): def get_object(self):

View File

@ -97,6 +97,10 @@ class Review(BaseAttributes, TranslatedFieldsMixin):
objects = ReviewQuerySet.as_manager() objects = ReviewQuerySet.as_manager()
@property
def status_display(self):
return self.REVIEW_STATUSES[self.status][1]
class Meta: class Meta:
"""Meta class.""" """Meta class."""
verbose_name = _('Review') verbose_name = _('Review')

View File

@ -4,14 +4,19 @@ from review.models import Review, Inquiries, GridItems
class ReviewBaseSerializer(serializers.ModelSerializer): class ReviewBaseSerializer(serializers.ModelSerializer):
text_translated = serializers.CharField(read_only=True)
status_display = serializers.CharField(read_only=True)
class Meta: class Meta:
model = Review model = Review
fields = ( fields = (
'id', 'id',
'reviewer', 'reviewer',
'text', 'text',
'text_translated',
'priority', 'priority',
'status', 'status',
'status_display',
'child', 'child',
'published_at', 'published_at',
'vintage', 'vintage',

View File

@ -55,7 +55,7 @@ class TagCategoryFilterSet(TagsBaseFilterSet):
if value == EstablishmentType.ARTISAN: if value == EstablishmentType.ARTISAN:
qs = models.TagCategory.objects.with_base_related().filter(index_name='shop_category') qs = models.TagCategory.objects.with_base_related().filter(index_name='shop_category')
else: else:
qs = queryset.by_establishment_type(value) qs = queryset.by_establishment_type(value).exclude(index_name__in=['guide', 'collection'])
return qs return qs

View File

@ -1,7 +1,5 @@
"""Serializer for app timetable""" """Serializer for app timetable"""
import datetime
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
@ -23,8 +21,6 @@ class ScheduleRUDSerializer(serializers.ModelSerializer):
dinner_end = serializers.TimeField(required=False) dinner_end = serializers.TimeField(required=False)
opening_at = serializers.TimeField(required=False) opening_at = serializers.TimeField(required=False)
closed_at = serializers.TimeField(required=False) closed_at = serializers.TimeField(required=False)
# For permission!!
establishment_id = serializers.ReadOnlyField(source='establishment.id')
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -40,7 +36,6 @@ class ScheduleRUDSerializer(serializers.ModelSerializer):
'dinner_end', 'dinner_end',
'opening_at', 'opening_at',
'closed_at', 'closed_at',
'establishment_id'
] ]
def validate(self, attrs): def validate(self, attrs):

View File

@ -32,6 +32,11 @@ class UserNotFoundError(AuthErrorMixin, ProjectBaseException):
default_detail = _('User not found') default_detail = _('User not found')
class EmployeeNotFoundError(ProjectBaseException):
"""The exception should be thrown when the employee cannot get"""
default_detail = _('Employee not found')
class EmailSendingError(exceptions.APIException): class EmailSendingError(exceptions.APIException):
"""The exception should be thrown when unable to send an email""" """The exception should be thrown when unable to send an email"""
status_code = status.HTTP_400_BAD_REQUEST status_code = status.HTTP_400_BAD_REQUEST

View File

@ -4,6 +4,7 @@ from os.path import exists
from django.conf import settings from django.conf import settings
from django.contrib.auth.tokens import PasswordResetTokenGenerator from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.contrib.contenttypes.models import ContentType
from django.contrib.gis.db import models 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
@ -517,6 +518,7 @@ def default_menu_bool_array():
class PhoneModelMixin: class PhoneModelMixin:
"""Mixin for PhoneNumberField.""" """Mixin for PhoneNumberField."""
@cached_property @cached_property
def country_calling_code(self): def country_calling_code(self):
"""Return phone code from PhonеNumberField.""" """Return phone code from PhonеNumberField."""
@ -537,3 +539,32 @@ class AwardsModelMixin:
if hasattr(self, 'awards'): if hasattr(self, 'awards'):
self.awards.remove(award) self.awards.remove(award)
class CarouselMixin:
@property
def must_of_the_week(self) -> bool:
"""Detects whether current item in carousel"""
from main.models import Carousel
if hasattr(self, 'pk') and (hasattr(self, 'country') or hasattr(self, 'country_id')):
kwargs = {
'content_type': ContentType.objects.get_for_model(self),
'object_id': self.pk,
'country': getattr(self, 'country', getattr(self, 'country_id', None)),
}
return Carousel.objects.filter(**kwargs).exists()
return False
class UpdateByMixin(models.Model):
"""Modify by mixin"""
last_update_by_manager = models.DateTimeField(null=True)
last_update_by_gm = models.DateTimeField(null=True)
class Meta:
"""Meta class."""
abstract = True

49
apps/utils/parsers.py Normal file
View File

@ -0,0 +1,49 @@
class NewsSlug:
def __init__(self, value=None, country_code=None, count=0):
self.value = value
self.country_code = country_code
self.count = count
@classmethod
def parse(cls, raw_slug, country_codes):
slug, *rest = raw_slug.split('-')
instance = NewsSlug()
if len(rest) >= 1 and rest[-1] in country_codes:
# It is like 'slug-en'
instance.value = '-'.join([slug, *rest[:-1]])
instance.country_code = rest[-1]
elif len(rest) >= 2 and rest[-1].isdigit() and rest[-2] in country_codes:
# It is like 'slug-en-1'
instance.value = '-'.join([slug, *rest[:-2]])
instance.country_code = rest[-2]
instance.count = int(rest[-1])
else:
# It is like 'slug'
instance.value = '-'.join([slug, *rest])
return instance
def __lt__(self, other):
return self.value < other.value
def __str__(self):
if self.value is None:
raise ValueError('No value for slug')
slug_parts = [self.value]
if self.country_code is not None:
slug_parts.append(self.country_code)
if self.count != 0:
slug_parts.append(str(self.count))
return '-'.join(slug_parts)
def __repr__(self):
return f'<{self.__class__.__name__} {self.value}, {self.country_code}, {self.count}>'

View File

@ -108,6 +108,7 @@ class CarouselCreateSerializer(serializers.ModelSerializer):
model = Carousel model = Carousel
fields = [ fields = [
'id', 'id',
'active',
] ]
@property @property

View File

@ -1,5 +1,4 @@
"""Development settings.""" """Development settings."""
from .amazon_s3 import *
from .base import * from .base import *
ALLOWED_HOSTS = ['gm.id-east.ru', '95.213.204.126', '0.0.0.0'] ALLOWED_HOSTS = ['gm.id-east.ru', '95.213.204.126', '0.0.0.0']
@ -15,7 +14,6 @@ DEFAULT_SUBDOMAIN = 'www'
SITE_DOMAIN_URI = 'id-east.ru' SITE_DOMAIN_URI = 'id-east.ru'
DOMAIN_URI = 'gm.id-east.ru' DOMAIN_URI = 'gm.id-east.ru'
CACHES = { CACHES = {
'default': { 'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
@ -26,7 +24,6 @@ CACHES = {
} }
} }
# ELASTICSEARCH SETTINGS # ELASTICSEARCH SETTINGS
ELASTICSEARCH_DSL = { ELASTICSEARCH_DSL = {
'default': { 'default': {
@ -35,7 +32,6 @@ ELASTICSEARCH_DSL = {
} }
} }
ELASTICSEARCH_INDEX_NAMES = { ELASTICSEARCH_INDEX_NAMES = {
'search_indexes.documents.news': 'development_news', 'search_indexes.documents.news': 'development_news',
'search_indexes.documents.establishment': 'development_establishment', 'search_indexes.documents.establishment': 'development_establishment',
@ -45,7 +41,6 @@ ELASTICSEARCH_INDEX_NAMES = {
# ELASTICSEARCH_DSL_AUTOSYNC = False # ELASTICSEARCH_DSL_AUTOSYNC = False
# DATABASE # DATABASE
DATABASES.update({ DATABASES.update({
'legacy': { 'legacy': {
@ -75,7 +70,6 @@ EMAIL_HOST_USER = 'anatolyfeteleu@gmail.com'
EMAIL_HOST_PASSWORD = 'nggrlnbehzksgmbt' EMAIL_HOST_PASSWORD = 'nggrlnbehzksgmbt'
EMAIL_PORT = 587 EMAIL_PORT = 587
MIDDLEWARE.append('utils.middleware.log_db_queries_per_API_request') MIDDLEWARE.append('utils.middleware.log_db_queries_per_API_request')
LOGGING = { LOGGING = {
@ -107,3 +101,7 @@ LOGGING = {
}, },
} }
} }
EMAIL_TECHNICAL_SUPPORT = 'n.malinova@octopod.ru'
from .amazon_s3 import *