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/
/_files/
/geoip_db/
/venv
# dev
./docker-compose.override.yml
docker-compose.override.yml
celerybeat-schedule
local_files
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,
IntermediateGalleryModelMixin, ProjectBaseMixin, TJSONField, TranslatedFieldsMixin,
TypeDefaultImageMixin, URLImageMixin, default_menu_bool_array, PhoneModelMixin,
AwardsModelMixin)
AwardsModelMixin, CarouselMixin, UpdateByMixin)
# todo: establishment type&subtypes check
@ -137,6 +137,14 @@ class EstablishmentQuerySet(models.QuerySet):
"""Return qs with 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):
"""Return qs with related """
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')
class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
TranslatedFieldsMixin, HasTagsMixin, FavoritesMixin, AwardsModelMixin):
class Establishment(GalleryMixin,
ProjectBaseMixin,
URLImageMixin,
TranslatedFieldsMixin,
HasTagsMixin,
FavoritesMixin,
AwardsModelMixin,
CarouselMixin,
UpdateByMixin):
"""Establishment model."""
ABANDONED = 0
@ -565,12 +580,12 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
(ABANDONED, _('Abandoned')),
(CLOSED, _('Closed')),
(PUBLISHED, _('Published')),
(UNPICKED, _('Unpicked')),
# (UNPICKED, _('Unpicked')),
(WAITING, _('Waiting')),
(HIDDEN, _('Hidden')),
(DELETED, _('Deleted')),
# (HIDDEN, _('Hidden')),
# (DELETED, _('Deleted')),
(OUT_OF_SELECTION, _('Out of selection')),
(UNPUBLISHED, _('Unpublished')),
# (UNPUBLISHED, _('Unpublished')),
)
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')
self.establishment_subtypes.add(establishment_subtype)
@property
def last_review(self):
return self.reviews.by_status(Review.READY).last()
@property
def vintage_year(self):
last_review = self.reviews.by_status(Review.READY).last()
last_review = self.last_review
if last_review:
return last_review.vintage

View File

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

View File

@ -60,6 +60,7 @@ urlpatterns = [
path('<int:establishment_id>/employees/', views.EstablishmentEmployeeListView.as_view(),
name='establishment-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/<int:pk>/', views.EmployeeRUDView.as_view(), name='employees-rud'),
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('shop_category', 'artisan_category') \
.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):
@ -209,7 +210,7 @@ class EstablishmentRUDView(EstablishmentMixinViews, generics.RetrieveUpdateDestr
)
def get_queryset(self):
"""Overridden get_queryset method."""
"""An overridden get_queryset method."""
qs = super(EstablishmentRUDView, self).get_queryset()
return qs.prefetch_related(
'establishmentemployee_set',
@ -270,9 +271,36 @@ class EstablishmentScheduleRUDView(EstablishmentMixinViews, generics.RetrieveUpd
class EstablishmentScheduleCreateView(generics.CreateAPIView):
"""
Establishment schedule Create view
Implement creating Establishment shedule.
## Create establishment schedule
### *POST*
#### 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'
serializer_class = ScheduleCreateSerializer
@ -292,8 +320,8 @@ class CardAndWinesListView(generics.RetrieveAPIView):
queryset = models.Establishment.objects.with_base_related()
def get_object(self):
establishment = models.Establishment.objects.prefetch_plates()\
.filter(pk=self.kwargs['establishment_id'])\
establishment = models.Establishment.objects.prefetch_plates() \
.filter(pk=self.kwargs['establishment_id']) \
.first()
if establishment is None:
raise Http404
@ -809,7 +837,7 @@ class EmployeesListSearchViews(generics.ListAPIView):
serializer_class = serializers.EmployeeBackSerializers
queryset = (
models.Employee.objects.with_back_office_related()
.select_related('photo')
.select_related('photo')
)
permission_classes = get_permission_classes(
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):
"""
## Remove award view.
@ -1060,7 +1125,38 @@ class EstablishmentSubtypeRUDView(generics.RetrieveUpdateDestroyAPIView):
class EstablishmentGalleryCreateDestroyView(EstablishmentMixinViews,
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'
serializer_class = serializers.EstablishmentBackOfficeGallerySerializer
permission_classes = get_permission_classes()
@ -1083,7 +1179,28 @@ class EstablishmentGalleryCreateDestroyView(EstablishmentMixinViews,
class EstablishmentGalleryListView(EstablishmentMixinViews,
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'
serializer_class = serializers.ImageBaseSerializer
permission_classes = get_permission_classes()
@ -1093,7 +1210,7 @@ class EstablishmentGalleryListView(EstablishmentMixinViews,
qs = super(EstablishmentGalleryListView, self).get_queryset()
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)
return establishment
@ -1330,10 +1447,10 @@ class EstablishmentGuideCreateDestroyView(generics.GenericAPIView):
lookup_url_kwarg = getattr(self, 'establishment_lookup_url_kwarg', None)
assert lookup_url_kwarg in self.kwargs, (
'Expected view %s to be called with a URL keyword argument '
'named "%s". Fix your URL conf, or set the `.lookup_field` '
'attribute on the view correctly.' %
(self.__class__.__name__, lookup_url_kwarg)
'Expected view %s to be called with a URL keyword argument '
'named "%s". Fix your URL conf, or set the `.lookup_field` '
'attribute on the view correctly.' %
(self.__class__.__name__, 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)
assert lookup_url_kwarg in self.kwargs, (
'Expected view %s to be called with a URL keyword argument '
'named "%s". Fix your URL conf, or set the `.lookup_field` '
'attribute on the view correctly.' %
(self.__class__.__name__, lookup_url_kwarg)
'Expected view %s to be called with a URL keyword argument '
'named "%s". Fix your URL conf, or set the `.lookup_field` '
'attribute on the view correctly.' %
(self.__class__.__name__, lookup_url_kwarg)
)
obj = get_object_or_404(klass=queryset, id=self.kwargs.get(lookup_url_kwarg))

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.utils.translation import gettext_lazy as _
from django.core import validators
from sorl.thumbnail import get_thumbnail
from botocore.exceptions import ClientError
from django.conf import settings
from project.storage_backends import PublicMediaStorage
@ -43,6 +45,8 @@ class Image(BaseAttributes, SORLImageMixin, PlatformMixin):
default=None)
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'))
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()
class Meta:
@ -55,6 +59,16 @@ class Image(BaseAttributes, SORLImageMixin, PlatformMixin):
"""String representation"""
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):
if not settings.AWS_STORAGE_BUCKET_NAME:
"""Backend doesn't use aws s3"""
@ -69,6 +83,12 @@ class Image(BaseAttributes, SORLImageMixin, PlatformMixin):
else:
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
def type(self) -> str:
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.conf import settings
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 . import models
@ -53,6 +53,8 @@ class EstablishmentGallerySerializer(serializers.ModelSerializer):
type = serializers.ChoiceField(read_only=True, choices=models.Image.MEDIA_TYPES)
created_by = UserBaseSerializer(read_only=True, allow_null=True)
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:
model = models.Image
@ -65,11 +67,16 @@ class EstablishmentGallerySerializer(serializers.ModelSerializer):
'preview',
'is_public',
'title',
'is_main',
'created_by',
'created',
'image_size_in_KB',
'cropbox',
'cropped_image',
)
extra_kwargs = {
'created': {'read_only': True},
'created_by': {'read_only': True},
}
def validate(self, attrs):
@ -78,24 +85,43 @@ class EstablishmentGallerySerializer(serializers.ModelSerializer):
if image and image.size >= settings.FILE_UPLOAD_MAX_MEMORY_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
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'])
instance = super().create(validated_data)
instance.created_by = self.context['request'].user
instance.establishment_set.add(establishment)
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
def update(self, instance: models.Image, validated_data):
if instance.is_public != validated_data.get('is_public'):
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)
class CropImageSerializer(ImageSerializer):
"""Serializers for image crops."""

View File

@ -1,6 +1,7 @@
from django.conf import settings
from django.db.transaction import on_commit
from rest_framework import generics, status
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from utils.methods import get_permission_classes
@ -8,6 +9,7 @@ from utils.permissions import IsContentPageManager, IsCountryAdmin, IsEstablishm
IsProducerFoodInspector, IsEstablishmentAdministrator
from . import tasks, models, serializers
class ImageBaseView(generics.GenericAPIView):
"""Base Image view."""
model = models.Image
@ -19,24 +21,83 @@ class ImageBaseView(generics.GenericAPIView):
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):
"""View for creating and retrieving certain establishment media."""
pagination_class = None
permission_classes = (IsCountryAdmin, IsEstablishmentAdministrator, IsEstablishmentManager, IsProducerFoodInspector)
# permission_classes = (IsCountryAdmin, IsEstablishmentAdministrator, IsEstablishmentManager, IsProducerFoodInspector)
permission_classes = (AllowAny, )
serializer_class = serializers.EstablishmentGallerySerializer
def get_queryset(self):
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"""
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):

View File

@ -223,11 +223,18 @@ class AddressBaseSerializer(serializers.ModelSerializer):
class AddressEstablishmentSerializer(AddressBaseSerializer):
"""Address serializer."""
id = serializers.IntegerField(required=True)
street_name_1 = serializers.CharField(required=False, default='')
street_name_2 = serializers.CharField(required=False, default='')
id = serializers.IntegerField(required=False)
street_name_1 = serializers.CharField(required=False, allow_blank=True, default='')
street_name_2 = serializers.CharField(required=False, allow_blank=True, default='')
number = serializers.IntegerField(required=False, default=0)
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):
"""Meta class."""
@ -238,6 +245,8 @@ class AddressEstablishmentSerializer(AddressBaseSerializer):
'street_name_2',
'number',
'postal_code',
'city_id',
'city',
)

View File

@ -38,3 +38,23 @@ class AwardFilter(filters.FilterSet):
if value not in EMPTY_VALUES:
return queryset.by_employee_id(value, content_type='establishmentemployee')
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."""
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):
"""AwardType model."""

View File

@ -3,6 +3,7 @@ from rest_framework import serializers
from account.models import User
from account.serializers import BackUserSerializer
from main import models
from main.serializers import CarouselListSerializer
class PanelSerializer(serializers.ModelSerializer):
@ -27,3 +28,15 @@ class PanelSerializer(serializers.ModelSerializer):
'user',
'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."""
from typing import Union
from django.contrib.contenttypes.models import ContentType
from rest_framework import serializers
from establishment.models import Employee
from location.serializers import CountrySerializer
from main import models
from establishment.models import Employee
from tag.serializers import TagBackOfficeSerializer
from utils.exceptions import EmployeeNotFoundError
from utils.serializers import ProjectModelSerializer, RecursiveFieldSerializer, TranslatedField
@ -207,6 +210,7 @@ class AwardBaseSerializer(serializers.ModelSerializer):
"""Award base serializer."""
title_translated = serializers.CharField(read_only=True, allow_null=True)
title = serializers.CharField(write_only=True, help_text='Title text')
class Meta:
model = models.Award
@ -215,45 +219,63 @@ class AwardBaseSerializer(serializers.ModelSerializer):
'title_translated',
'vintage_year',
'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):
"""Award serializer."""
award_type = AwardTypeBaseSerializer(read_only=True)
class Meta:
model = models.Award
class Meta(AwardBaseSerializer.Meta):
fields = AwardBaseSerializer.Meta.fields + ['award_type', ]
class BackAwardSerializer(AwardBaseSerializer):
"""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:
model = models.Award
class Meta(AwardBaseSerializer.Meta):
fields = AwardBaseSerializer.Meta.fields + [
'award_type',
'award_type_display',
'state',
'content_type',
'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_type = serializers.PrimaryKeyRelatedField(required=True, queryset=models.AwardType.objects.all())
title = serializers.CharField(write_only=True)
def get_title(self, obj):
pass
class Meta:
model = models.Award
class Meta(AwardBaseSerializer.Meta):
fields = (
'id',
'award_type',
@ -262,9 +284,15 @@ class BackAwardEmployeeCreateSerializer(serializers.ModelSerializer):
)
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['title'] = {self.context.get('request').locale: attrs['title']}
return attrs

View File

@ -8,7 +8,8 @@ app_name = 'main'
urlpatterns = [
path('awards/', views.AwardLstView.as_view(), name='awards-list-create'),
path('awards/<int:id>/', views.AwardRUDView.as_view(), name='awards-rud'),
path('awards/create-and-bind/<int:employee_id>/', views.AwardCreateAndBind.as_view(), name='award-employee-create'),
path('awards/create-and-bind/<int:employee_id>/', views.AwardCreateAndBind.as_view(),
name='award-employee-create'),
path('award-types/', views.AwardTypesListView.as_view(), name='awards-types-list'),
path('content_type/', views.ContentTypeView.as_view(), name='content_type-list'),
path('sites/', views.SiteListBackOfficeView.as_view(), name='site-list-create'),
@ -28,7 +29,6 @@ urlpatterns = [
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>/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 main import serializers
from main import tasks
from main.filters import AwardFilter
from main.models import Award, Footer, PageType, Panel, SiteFeature, Feature, AwardType
from main.serializers.back import PanelSerializer
from main.filters import AwardFilter, AwardTypeFilterSet
from main.models import Award, Footer, PageType, Panel, SiteFeature, Feature, AwardType, Carousel
from main.serializers.back import PanelSerializer, BackCarouselListSerializer
from main.views import SiteSettingsView, SiteListView
from utils.methods import get_permission_classes
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()
serializer_class = serializers.BackAwardSerializer
permission_classes = get_permission_classes()
@ -25,7 +71,35 @@ class AwardLstView(generics.ListCreateAPIView):
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()
serializer_class = serializers.BackAwardEmployeeCreateSerializer
permission_classes = get_permission_classes()
@ -41,7 +115,52 @@ class AwardCreateAndBind(generics.CreateAPIView):
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()
serializer_class = serializers.BackAwardSerializer
permission_classes = get_permission_classes()
@ -49,20 +168,32 @@ class AwardRUDView(generics.RetrieveUpdateDestroyAPIView):
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
serializer_class = serializers.AwardTypeBaseSerializer
permission_classes = get_permission_classes()
filter_backends = (DjangoFilterBackend,)
ordering_fields = '__all__'
lookup_field = 'id'
filterset_fields = (
'id',
'name',
)
filter_class = AwardTypeFilterSet
def get_queryset(self):
"""Overridden get_queryset method."""
"""An overridden get_queryset method."""
if hasattr(self, 'request') and hasattr(self.request, 'country_code'):
return AwardType.objects.by_country_code(self.request.country_code)
return AwardType.objects.none()
@ -200,3 +331,40 @@ class PanelsExecuteXLSView(PanelsExecuteView):
{"success": _('The file will be sent to your email.')},
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,
ProjectBaseMixin,
TJSONField, TranslatedFieldsMixin, TypeDefaultImageMixin,
)
CarouselMixin)
from utils.querysets import TranslationQuerysetMixin
from location.models import Country
from utils.parsers import NewsSlug
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
class News(GalleryMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin,
FavoritesMixin):
class News(GalleryMixin,
BaseAttributes,
TranslatedFieldsMixin,
HasTagsMixin,
FavoritesMixin,
CarouselMixin):
"""News model."""
STR_FIELD_NAME = 'title'
@ -358,24 +364,41 @@ class News(GalleryMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin,
return f'news: {next(iter(self.slugs.values()))}'
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.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.views_count = view_count_model
self.duplication_date = timezone.now()
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
def publication_datetime(self):
"""Represents datetime object combined from `publication_date` & `publication_time` fields"""

View File

@ -176,7 +176,38 @@ class NewsBackOfficeLCView(NewsBackOfficeMixinView,
class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView,
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
def create(self, request, *args, **kwargs):
@ -203,7 +234,28 @@ class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView,
class NewsBackOfficeGalleryListView(NewsBackOfficeMixinView,
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
def get_object(self):

View File

@ -46,7 +46,38 @@ class ProductSubTypeBackOfficeMixinView:
class ProductBackOfficeGalleryCreateDestroyView(ProductBackOfficeMixinView,
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
def get_object(self):
@ -66,7 +97,28 @@ class ProductBackOfficeGalleryCreateDestroyView(ProductBackOfficeMixinView,
class ProductBackOfficeGalleryListView(ProductBackOfficeMixinView,
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
def get_object(self):

View File

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

View File

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

View File

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

View File

@ -1,7 +1,5 @@
"""Serializer for app timetable"""
import datetime
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
@ -23,8 +21,6 @@ class ScheduleRUDSerializer(serializers.ModelSerializer):
dinner_end = serializers.TimeField(required=False)
opening_at = serializers.TimeField(required=False)
closed_at = serializers.TimeField(required=False)
# For permission!!
establishment_id = serializers.ReadOnlyField(source='establishment.id')
class Meta:
"""Meta class."""
@ -40,7 +36,6 @@ class ScheduleRUDSerializer(serializers.ModelSerializer):
'dinner_end',
'opening_at',
'closed_at',
'establishment_id'
]
def validate(self, attrs):

View File

@ -32,6 +32,11 @@ class UserNotFoundError(AuthErrorMixin, ProjectBaseException):
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):
"""The exception should be thrown when unable to send an email"""
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.contrib.auth.tokens import PasswordResetTokenGenerator
from django.contrib.contenttypes.models import ContentType
from django.contrib.gis.db import models
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import JSONField
@ -517,6 +518,7 @@ def default_menu_bool_array():
class PhoneModelMixin:
"""Mixin for PhoneNumberField."""
@cached_property
def country_calling_code(self):
"""Return phone code from PhonеNumberField."""
@ -537,3 +539,32 @@ class AwardsModelMixin:
if hasattr(self, 'awards'):
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
fields = [
'id',
'active',
]
@property

View File

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