Merge branch 'develop' into feature/permission_liquor

This commit is contained in:
Виктор Гладких 2019-12-17 14:24:15 +03:00
commit 458370d4c9
75 changed files with 1720 additions and 305 deletions

View File

@ -0,0 +1,49 @@
from django.core.management.base import BaseCommand
from tqdm import tqdm
from account.models import User, UserRole, Role
from transfer.models import OwnershipAffs
from establishment.models import Establishment
class Command(BaseCommand):
help = """Add ownership to UserRoles."""
def handle(self, *args, **kwarg):
create_user_roles = []
# filter owner records with state not null only
owners = OwnershipAffs.objects.filter(state__isnull=False)
# Get role, may be more then 1
role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER).first()
if not role:
role = Role.objects.create(
role=Role.ESTABLISHMENT_MANAGER
)
for owner in tqdm(owners):
user = User.objects.filter(
old_id=owner.account_id).first()
requester = User.objects.filter(
old_id=owner.requester_id).first()
establishment = Establishment.objects.filter(
old_id=owner.establishment_id).first()
if user and establishment:
user_role = UserRole.objects.filter(
user=user, role=role, establishment=establishment, state=owner.state).first()
if not user_role:
# add to bulk_create
create_user_roles.append(UserRole(
user=user,
role=role,
establishment=establishment,
state=owner.state,
requester=requester
))
UserRole.objects.bulk_create(create_user_roles)
self.stdout.write(
self.style.WARNING(
f'Created roles: {len(create_user_roles)}')
)

View File

@ -0,0 +1,25 @@
# Generated by Django 2.2.7 on 2019-12-11 11:34
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('account', '0025_auto_20191210_0623'),
]
operations = [
migrations.AddField(
model_name='userrole',
name='requester',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='roles_requested', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='userrole',
name='state',
field=models.CharField(choices=[('validated', 'validated'), ('pending', 'pending'), ('cancelled', 'cancelled'), ('rejected', 'rejected')], default='pending', max_length=10, verbose_name='state'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-12-11 14:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('establishment', '0067_auto_20191122_1244'),
('account', '0026_auto_20191211_1134'),
]
operations = [
migrations.AlterUniqueTogether(
name='userrole',
unique_together={('user', 'role', 'establishment', 'state')},
),
]

View File

@ -34,18 +34,18 @@ class Role(ProjectBaseMixin):
REVIEWER_MANGER = 6 REVIEWER_MANGER = 6
RESTAURANT_REVIEWER = 7 RESTAURANT_REVIEWER = 7
SALES_MAN = 8 SALES_MAN = 8
WINERY_REVIEWER = 9 # Establishments subtype "winery" WINERY_REVIEWER = 9 # Establishments subtype "winery"
SELLER = 10 SELLER = 10
LIQUOR_REVIEWER = 11 LIQUOR_REVIEWER = 11
PRODUCT_REVIEWER = 12 PRODUCT_REVIEWER = 12
ROLE_CHOICES = ( ROLE_CHOICES = (
(STANDARD_USER, 'Standard user'), (STANDARD_USER, _('Standard user')),
(COMMENTS_MODERATOR, 'Comments moderator'), (COMMENTS_MODERATOR, _('Comments moderator')),
(COUNTRY_ADMIN, 'Country admin'), (COUNTRY_ADMIN, _('Country admin')),
(CONTENT_PAGE_MANAGER, 'Content page manager'), (CONTENT_PAGE_MANAGER, _('Content page manager')),
(ESTABLISHMENT_MANAGER, 'Establishment manager'), (ESTABLISHMENT_MANAGER, _('Establishment manager')),
(REVIEWER_MANGER, 'Reviewer manager'), (REVIEWER_MANGER, _('Reviewer manager')),
(RESTAURANT_REVIEWER, 'Restaurant reviewer'), (RESTAURANT_REVIEWER, 'Restaurant reviewer'),
(SALES_MAN, 'Sales man'), (SALES_MAN, 'Sales man'),
(WINERY_REVIEWER, 'Winery reviewer'), (WINERY_REVIEWER, 'Winery reviewer'),
@ -97,6 +97,18 @@ class UserQuerySet(models.QuerySet):
return self.filter(oauth2_provider_refreshtoken__token=token, return self.filter(oauth2_provider_refreshtoken__token=token,
oauth2_provider_refreshtoken__expires__gt=timezone.now()) oauth2_provider_refreshtoken__expires__gt=timezone.now())
def by_role(self, role):
"""Filter by role."""
return self.filter(userrole__role=role)
def by_roles(self, roles: list):
"""Filter by roles."""
return self.filter(userrole__role__in=roles)
def establishment_admin(self, establishment):
role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER).first()
return self.by_role(role).filter(userrole__establishment=establishment)
class User(AbstractUser): class User(AbstractUser):
"""Base user model.""" """Base user model."""
@ -120,7 +132,9 @@ class User(AbstractUser):
USERNAME_FIELD = 'username' USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['email'] REQUIRED_FIELDS = ['email']
roles = models.ManyToManyField(Role, verbose_name=_('Roles'), through='UserRole') roles = models.ManyToManyField(
Role, verbose_name=_('Roles'), symmetrical=False,
through_fields=('user', 'role'), through='UserRole')
objects = UserManager.from_queryset(UserQuerySet)() objects = UserManager.from_queryset(UserQuerySet)()
class Meta: class Meta:
@ -305,14 +319,33 @@ class User(AbstractUser):
class UserRole(ProjectBaseMixin): class UserRole(ProjectBaseMixin):
"""UserRole model.""" """UserRole model."""
user = models.ForeignKey('account.User', VALIDATED = 'validated'
verbose_name=_('User'), PENDING = 'pending'
on_delete=models.CASCADE) CANCELLED = 'cancelled'
role = models.ForeignKey(Role, verbose_name=_('Role'), on_delete=models.SET_NULL, null=True) REJECTED = 'rejected'
establishment = models.ForeignKey(Establishment, verbose_name=_('Establishment'),
on_delete=models.SET_NULL, null=True, blank=True) STATE_CHOICES = (
(VALIDATED, _('validated')),
(PENDING, _('pending')),
(CANCELLED, _('cancelled')),
(REJECTED, _('rejected'))
)
user = models.ForeignKey(
'account.User', verbose_name=_('User'), on_delete=models.CASCADE)
role = models.ForeignKey(
Role, verbose_name=_('Role'), on_delete=models.SET_NULL, null=True)
establishment = models.ForeignKey(
Establishment, verbose_name=_('Establishment'),
on_delete=models.SET_NULL, null=True, blank=True)
state = models.CharField(
_('state'), choices=STATE_CHOICES, max_length=10, default=PENDING)
requester = models.ForeignKey(
'account.User', blank=True, null=True, default=None, related_name='roles_requested',
on_delete=models.SET_NULL)
class Meta: class Meta:
unique_together = ['user', 'role'] unique_together = ['user', 'role', 'establishment', 'state']
class OldRole(models.Model): class OldRole(models.Model):

View File

@ -10,4 +10,5 @@ urlpatterns = [
path('user-role/', views.UserRoleLstView.as_view(), name='user-role-list-create'), path('user-role/', views.UserRoleLstView.as_view(), name='user-role-list-create'),
path('user/', views.UserLstView.as_view(), name='user-create-list'), path('user/', views.UserLstView.as_view(), name='user-create-list'),
path('user/<int:id>/', views.UserRUDView.as_view(), name='user-rud'), path('user/<int:id>/', views.UserRUDView.as_view(), name='user-rud'),
path('user/<int:id>/csv', views.get_user_csv, name='user-csv'),
] ]

View File

@ -1,6 +1,9 @@
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics, permissions from rest_framework import generics, permissions
from rest_framework.filters import OrderingFilter from rest_framework.filters import OrderingFilter
import csv
from django.http import HttpResponse, HttpResponseNotFound
from rest_framework.authtoken.models import Token
from account import models from account import models
from account.models import User from account.models import User
@ -46,3 +49,69 @@ class UserRUDView(generics.RetrieveUpdateDestroyAPIView):
serializer_class = serializers.BackDetailUserSerializer serializer_class = serializers.BackDetailUserSerializer
permission_classes = (permissions.IsAdminUser,) permission_classes = (permissions.IsAdminUser,)
lookup_field = 'id' lookup_field = 'id'
def get_user_csv(request, id):
# fields = ["id", "uuid", "nickname", "locale", "country_code", "city", "role", "consent_purpose", "consent_at",
# "last_seen_at", "created_at", "updated_at", "email", "is_admin", "ezuser_id", "ez_user_id",
# "encrypted_password", "reset_password_token", "reset_password_sent_at", "remember_created_at",
# "sign_in_count", "current_sign_in_at", "last_sign_in_at", "current_sign_in_ip", "last_sign_in_ip",
# "confirmation_token", "confirmed_at", "confirmation_sent_at", "unconfirmed_email", "webpush_subscription"]
# uuid == id
#
# Не найдены:
# consent_purpose
# consent_at
# ezuser_id
# ez_user_id
# remember_created_at
# sign_in_count
# current_sign_in_at
# current_sign_in_ip
# last_sign_in_ip
# confirmed_at
# confirmation_sent_at
# webpush_subscription
#
# country_code не получить - клиент не привязан к стране
try:
user = User.objects.get(id=id)
except User.DoesNotExist:
return HttpResponseNotFound("User not found")
try:
roles = " ".join([role for role in user.roles])
except:
roles = ""
token, _ = Token.objects.get_or_create(user=user)
fields = {
"id": user.id,
"uuid": user.id,
"username": getattr(user, "username", ""),
"locale": getattr(user, "locale", ""),
"city": getattr(user, "city", ""),
"role": roles,
"created_at": getattr(user, "date_joined", ""),
"updated_at": user.last_login,
"email": user.email,
"is_admin": user.is_superuser,
"encrypted_password": user.password,
"reset_password_token": token.key,
"reset_password_sent_at": token.created, # TODO: не уверен в назначении поля, лучше проверить
"last_sign_in_at": user.last_login, # Повтор?
"confirmation_token": user.confirm_email_token,
"unconfirmed_email": 1 if user.unconfirmed_email else 0
}
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = f'attachment; filename="{user.email}.csv"'
writer = csv.writer(response)
writer.writerow(fields.keys())
writer.writerow(fields.values())
return response

View File

@ -14,3 +14,8 @@ class PageInline(admin.TabularInline):
class AdvertisementModelAdmin(admin.ModelAdmin): class AdvertisementModelAdmin(admin.ModelAdmin):
"""Admin model for model Advertisement""" """Admin model for model Advertisement"""
inlines = (PageInline, ) inlines = (PageInline, )
list_display = ('id', '__str__', 'block_level',
'start', 'end', 'page_type')
list_filter = ('url', 'block_level', 'start', 'end', 'page_type',
'pages__source')
date_hierarchy = 'created'

View File

@ -71,11 +71,11 @@ class Advertisement(ProjectBaseMixin):
return super().delete(using, keep_parents) return super().delete(using, keep_parents)
@property @property
def mobile_page(self): def mobile_pages(self):
"""Return mobile page""" """Return mobile page"""
return self.pages.by_platform(Page.MOBILE).first() return self.pages.by_platform(Page.MOBILE)
@property @property
def web_page(self): def web_pages(self):
"""Return web page""" """Return web page"""
return self.pages.by_platform(Page.WEB).first() return self.pages.by_platform(Page.WEB)

View File

@ -1,26 +1,14 @@
"""Serializers for back office app advertisements""" """Serializers for back office app advertisements"""
from main.serializers import PageBaseSerializer from advertisement.serializers import AdvertisementBaseSerializer
from main.serializers import PageExtendedSerializer
class AdvertisementPageBaseSerializer(PageBaseSerializer): class AdvertisementDetailSerializer(AdvertisementBaseSerializer):
"""Base serializer for linking page w/ advertisement.""" """Advertisement serializer for back office."""
pages = PageExtendedSerializer(many=True, read_only=True)
class Meta(PageBaseSerializer.Meta): class Meta(AdvertisementBaseSerializer.Meta):
"""Meta class.""" """Meta class."""
fields = AdvertisementBaseSerializer.Meta.fields + [
PageBaseSerializer.Meta.extra_kwargs.update({ 'pages',
'advertisement': {'write_only': True}, ]
'image_url': {'required': True},
'width': {'required': True},
'height': {'required': True},
})
class AdvertisementPageListCreateSerializer(AdvertisementPageBaseSerializer):
"""Serializer for linking page w/ advertisement."""
def create(self, validated_data):
"""Overridden create method."""
validated_data['advertisement'] = self.context.get('view').get_object()
return super().create(validated_data)

View File

@ -2,15 +2,15 @@
from rest_framework import serializers from rest_framework import serializers
from advertisement import models from advertisement import models
from translation.serializers import LanguageSerializer
from main.serializers import SiteShortSerializer, PageBaseSerializer
from translation.models import Language
from main.models import SiteSettings from main.models import SiteSettings
from main.serializers import PageTypeBaseSerializer
from translation.models import Language
class AdvertisementBaseSerializer(serializers.ModelSerializer): class AdvertisementBaseSerializer(serializers.ModelSerializer):
"""Base serializer for model Advertisement.""" """Base serializer for model Advertisement."""
page_type_detail = PageTypeBaseSerializer(read_only=True,
source='page_type')
target_languages = serializers.PrimaryKeyRelatedField( target_languages = serializers.PrimaryKeyRelatedField(
queryset=Language.objects.all(), queryset=Language.objects.all(),
many=True, many=True,
@ -34,16 +34,17 @@ class AdvertisementBaseSerializer(serializers.ModelSerializer):
'target_sites', 'target_sites',
'start', 'start',
'end', 'end',
'page_type',
'page_type_detail',
] ]
extra_kwargs = {
'page_type': {'required': True, 'write_only': True}
}
class AdvertisementPageTypeCommonListSerializer(AdvertisementBaseSerializer): class AdvertisementSerializer(AdvertisementBaseSerializer):
"""Serializer for AdvertisementPageTypeCommonView.""" """Serializer for model Advertisement."""
page = PageBaseSerializer(source='common_page', read_only=True)
class Meta(AdvertisementBaseSerializer.Meta): class Meta(AdvertisementBaseSerializer.Meta):
"""Meta class.""" """Meta class."""
fields = AdvertisementBaseSerializer.Meta.fields + [ fields = AdvertisementBaseSerializer.Meta.fields.copy()
'page', fields.pop(fields.index('page_type_detail'))
]

View File

@ -1,15 +1,15 @@
"""Serializers for mobile app advertisements""" """Serializers for mobile app advertisements"""
from advertisement.serializers import AdvertisementBaseSerializer from advertisement.serializers import AdvertisementSerializer
from main.serializers import PageBaseSerializer from main.serializers import PageBaseSerializer
class AdvertisementPageTypeMobileListSerializer(AdvertisementBaseSerializer): class AdvertisementPageTypeMobileListSerializer(AdvertisementSerializer):
"""Serializer for AdvertisementPageTypeMobileView.""" """Serializer for AdvertisementPageTypeMobileView."""
page = PageBaseSerializer(source='mobile_page', read_only=True) pages = PageBaseSerializer(many=True, source='mobile_pages', read_only=True)
class Meta(AdvertisementBaseSerializer.Meta): class Meta(AdvertisementSerializer.Meta):
"""Meta class.""" """Meta class."""
fields = AdvertisementBaseSerializer.Meta.fields + [ fields = AdvertisementSerializer.Meta.fields + [
'page', 'pages',
] ]

View File

@ -1,15 +1,15 @@
"""Serializers for web app advertisements""" """Serializers for web app advertisements"""
from advertisement.serializers import AdvertisementBaseSerializer from advertisement.serializers import AdvertisementSerializer
from main.serializers import PageBaseSerializer from main.serializers import PageBaseSerializer
class AdvertisementPageTypeWebListSerializer(AdvertisementBaseSerializer): class AdvertisementPageTypeWebListSerializer(AdvertisementSerializer):
"""Serializer for AdvertisementPageTypeWebView.""" """Serializer for AdvertisementPageTypeWebView."""
page = PageBaseSerializer(source='web_page', read_only=True) pages = PageBaseSerializer(many=True, source='web_pages', read_only=True)
class Meta(AdvertisementBaseSerializer.Meta): class Meta(AdvertisementSerializer.Meta):
"""Meta class.""" """Meta class."""
fields = AdvertisementBaseSerializer.Meta.fields + [ fields = AdvertisementSerializer.Meta.fields + [
'page', 'pages',
] ]

View File

@ -9,10 +9,10 @@ app_name = 'advertisements'
urlpatterns = [ urlpatterns = [
path('', views.AdvertisementListCreateView.as_view(), name='list-create'), path('', views.AdvertisementListCreateView.as_view(), name='list-create'),
path('<int:pk>/', views.AdvertisementRUDView.as_view(), name='rud'), path('<int:pk>/', views.AdvertisementRUDView.as_view(), name='rud'),
path('<int:pk>/pages/', views.AdvertisementPageListCreateView.as_view(), path('<int:pk>/pages/', views.AdvertisementPageCreateView.as_view(),
name='page-list-create'), name='ad-page-create'),
path('<int:ad_pk>/pages/<int:page_pk>/', views.AdvertisementPageRUDView.as_view(), path('<int:ad_pk>/pages/<int:page_pk>/', views.AdvertisementPageUDView.as_view(),
name='page-rud') name='ad-page-update-destroy')
] ]
urlpatterns += common_urlpatterns urlpatterns += common_urlpatterns

View File

@ -1,19 +1,19 @@
"""Back office views for app advertisement""" """Back office views for app advertisement"""
from rest_framework import generics from rest_framework import generics, status
from rest_framework.response import Response
from rest_framework import permissions from rest_framework import permissions
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from main.serializers import PageExtendedSerializer
from advertisement.models import Advertisement from advertisement.models import Advertisement
from rest_framework.response import Response
from rest_framework import status
from advertisement.serializers import (AdvertisementBaseSerializer, from advertisement.serializers import (AdvertisementBaseSerializer,
AdvertisementPageBaseSerializer, AdvertisementDetailSerializer)
AdvertisementPageListCreateSerializer)
class AdvertisementBackOfficeViewMixin(generics.GenericAPIView): class AdvertisementBackOfficeViewMixin(generics.GenericAPIView):
"""Base back office advertisement view.""" """Base back office advertisement view."""
pagination_class = None
permission_classes = (permissions.IsAuthenticated, ) permission_classes = (permissions.IsAuthenticated, )
def get_queryset(self): def get_queryset(self):
@ -31,14 +31,14 @@ class AdvertisementRUDView(AdvertisementBackOfficeViewMixin,
generics.RetrieveUpdateDestroyAPIView): generics.RetrieveUpdateDestroyAPIView):
"""Retrieve|Update|Destroy advertisement page view.""" """Retrieve|Update|Destroy advertisement page view."""
serializer_class = AdvertisementBaseSerializer serializer_class = AdvertisementDetailSerializer
class AdvertisementPageListCreateView(AdvertisementBackOfficeViewMixin, class AdvertisementPageCreateView(AdvertisementBackOfficeViewMixin,
generics.ListCreateAPIView): generics.CreateAPIView):
"""Retrieve|Update|Destroy advertisement page view.""" """Create advertisement page view."""
serializer_class = AdvertisementPageListCreateSerializer serializer_class = PageExtendedSerializer
def get_object(self): def get_object(self):
"""Returns the object the view is displaying.""" """Returns the object the view is displaying."""
@ -56,12 +56,19 @@ class AdvertisementPageListCreateView(AdvertisementBackOfficeViewMixin,
"""Overridden get_queryset method.""" """Overridden get_queryset method."""
return self.get_object().pages.all() return self.get_object().pages.all()
def create(self, request, *args, **kwargs):
"""Overridden create method."""
request.data.update({'advertisement': self.get_object().pk})
super().create(request, *args, **kwargs)
return Response(status=status.HTTP_200_OK)
class AdvertisementPageRUDView(AdvertisementBackOfficeViewMixin,
generics.RetrieveUpdateDestroyAPIView):
"""Create|Retrieve|Update|Destroy advertisement page view."""
serializer_class = AdvertisementPageBaseSerializer class AdvertisementPageUDView(AdvertisementBackOfficeViewMixin,
generics.UpdateAPIView,
generics.DestroyAPIView):
"""Update|Destroy advertisement page view."""
serializer_class = PageExtendedSerializer
def get_object(self): def get_object(self):
"""Returns the object the view is displaying.""" """Returns the object the view is displaying."""

View File

@ -3,8 +3,7 @@ from rest_framework import generics
from rest_framework import permissions from rest_framework import permissions
from advertisement.models import Advertisement from advertisement.models import Advertisement
from advertisement.serializers import AdvertisementBaseSerializer, \ from advertisement.serializers import AdvertisementBaseSerializer
AdvertisementPageTypeCommonListSerializer
class AdvertisementBaseView(generics.GenericAPIView): class AdvertisementBaseView(generics.GenericAPIView):
@ -16,8 +15,7 @@ class AdvertisementBaseView(generics.GenericAPIView):
def get_queryset(self): def get_queryset(self):
"""Overridden get queryset method.""" """Overridden get queryset method."""
return Advertisement.objects.with_base_related() \ return Advertisement.objects.with_base_related()
.by_locale(self.request.locale)
class AdvertisementPageTypeListView(AdvertisementBaseView, generics.ListAPIView): class AdvertisementPageTypeListView(AdvertisementBaseView, generics.ListAPIView):

View File

@ -0,0 +1,27 @@
# Generated by Django 2.2.7 on 2019-12-15 21:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('collection', '0023_advertorial'),
]
operations = [
migrations.AddField(
model_name='collection',
name='rank',
field=models.IntegerField(default=None, null=True),
),
migrations.AlterField(
model_name='collection',
name='start',
field=models.DateTimeField(blank=True, default=None, null=True, verbose_name='start'),
),
migrations.RemoveField(
model_name='collection',
name='description',
)
]

View File

@ -0,0 +1,20 @@
# Generated by Django 2.2.7 on 2019-12-16 17:25
from django.db import migrations
import utils.models
class Migration(migrations.Migration):
dependencies = [
('collection', '0024_auto_20191215_2156'),
]
operations = [
migrations.AddField(
model_name='collection',
name='description',
field=utils.models.TJSONField(blank=True, default=None,
help_text='{"en-GB":"some text"}', null=True,
verbose_name='description'),
),
]

View File

@ -6,9 +6,10 @@ from django.core.validators import MaxValueValidator, MinValueValidator
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 utils.models import ProjectBaseMixin, URLImageMixin from utils.models import (
from utils.models import TJSONField ProjectBaseMixin, TJSONField, TranslatedFieldsMixin,
from utils.models import TranslatedFieldsMixin URLImageMixin,
)
from utils.querysets import RelatedObjectsCountMixin from utils.querysets import RelatedObjectsCountMixin
@ -24,7 +25,8 @@ class CollectionNameMixin(models.Model):
class CollectionDateMixin(models.Model): class CollectionDateMixin(models.Model):
"""CollectionDate mixin""" """CollectionDate mixin"""
start = models.DateTimeField(_('start')) start = models.DateTimeField(blank=True, null=True, default=None,
verbose_name=_('start'))
end = models.DateTimeField(blank=True, null=True, default=None, end = models.DateTimeField(blank=True, null=True, default=None,
verbose_name=_('end')) verbose_name=_('end'))
@ -80,6 +82,8 @@ class Collection(ProjectBaseMixin, CollectionDateMixin,
verbose_name=_('Collection slug'), editable=True, null=True) verbose_name=_('Collection slug'), editable=True, null=True)
old_id = models.IntegerField(null=True, blank=True) old_id = models.IntegerField(null=True, blank=True)
rank = models.IntegerField(null=True, default=None)
objects = CollectionQuerySet.as_manager() objects = CollectionQuerySet.as_manager()
class Meta: class Meta:
@ -108,20 +112,32 @@ class Collection(ProjectBaseMixin, CollectionDateMixin,
@property @property
def related_object_names(self) -> list: def related_object_names(self) -> list:
"""Return related object names.""" """Return related object names."""
raw_object_names = [] raw_objects = []
for related_object in [related_object.name for related_object in self._related_objects]: for related_object in [related_object.name for related_object in self._related_objects]:
instances = getattr(self, f'{related_object}') instances = getattr(self, f'{related_object}')
if instances.exists(): if instances.exists():
for instance in instances.all(): for instance in instances.all():
raw_object_names.append(instance.slug if hasattr(instance, 'slug') else None) raw_object = (instance.id, instance.slug) if hasattr(instance, 'slug') else (
instance.id, None
)
raw_objects.append(raw_object)
# parse slugs # parse slugs
object_names = [] related_objects = []
object_names = set()
re_pattern = r'[\w]+' re_pattern = r'[\w]+'
for raw_name in raw_object_names: for object_id, raw_name, in raw_objects:
result = re.findall(re_pattern, raw_name) result = re.findall(re_pattern, raw_name)
if result: object_names.append(' '.join(result).capitalize()) if result:
return set(object_names) name = ' '.join(result).capitalize()
if name not in object_names:
related_objects.append({
'id': object_id,
'name': name
})
object_names.add(name)
return related_objects
class GuideTypeQuerySet(models.QuerySet): class GuideTypeQuerySet(models.QuerySet):

View File

@ -7,7 +7,8 @@ from location.models import Country
from location.serializers import CountrySimpleSerializer from location.serializers import CountrySimpleSerializer
from product.models import Product from product.models import Product
from utils.exceptions import ( from utils.exceptions import (
BindingObjectNotFound, RemovedBindingObjectNotFound, ObjectAlreadyAdded BindingObjectNotFound, ObjectAlreadyAdded,
RemovedBindingObjectNotFound,
) )
@ -33,13 +34,14 @@ class CollectionBackOfficeSerializer(CollectionBaseSerializer):
'on_top', 'on_top',
'country', 'country',
'country_id', 'country_id',
'block_size', # 'block_size',
'description', 'description',
'slug', 'slug',
'start', # 'start',
'end', # 'end',
'count_related_objects', 'count_related_objects',
'related_object_names', 'related_object_names',
'rank',
] ]
@ -68,15 +70,15 @@ class CollectionBindObjectSerializer(serializers.Serializer):
attrs['collection'] = collection attrs['collection'] = collection
if obj_type == self.ESTABLISHMENT: if obj_type == self.ESTABLISHMENT:
establishment = Establishment.objects.filter(pk=obj_id).\ establishment = Establishment.objects.filter(pk=obj_id). \
first() first()
if not establishment: if not establishment:
raise BindingObjectNotFound() raise BindingObjectNotFound()
if request.method == 'POST' and collection.establishments.\ if request.method == 'POST' and collection.establishments. \
filter(pk=establishment.pk).exists(): filter(pk=establishment.pk).exists():
raise ObjectAlreadyAdded() raise ObjectAlreadyAdded()
if request.method == 'DELETE' and not collection.\ if request.method == 'DELETE' and not collection. \
establishments.filter(pk=establishment.pk).\ establishments.filter(pk=establishment.pk). \
exists(): exists():
raise RemovedBindingObjectNotFound() raise RemovedBindingObjectNotFound()
attrs['related_object'] = establishment attrs['related_object'] = establishment
@ -84,10 +86,10 @@ class CollectionBindObjectSerializer(serializers.Serializer):
product = Product.objects.filter(pk=obj_id).first() product = Product.objects.filter(pk=obj_id).first()
if not product: if not product:
raise BindingObjectNotFound() raise BindingObjectNotFound()
if request.method == 'POST' and collection.products.\ if request.method == 'POST' and collection.products. \
filter(pk=product.pk).exists(): filter(pk=product.pk).exists():
raise ObjectAlreadyAdded() raise ObjectAlreadyAdded()
if request.method == 'DELETE' and not collection.products.\ if request.method == 'DELETE' and not collection.products. \
filter(pk=product.pk).exists(): filter(pk=product.pk).exists():
raise RemovedBindingObjectNotFound() raise RemovedBindingObjectNotFound()
attrs['related_object'] = product attrs['related_object'] = product

View File

@ -1,5 +1,6 @@
from rest_framework import permissions from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets, mixins from rest_framework import mixins, permissions, viewsets
from rest_framework.filters import OrderingFilter
from collection import models from collection import models
from collection.serializers import back as serializers from collection.serializers import back as serializers
@ -9,11 +10,17 @@ from utils.views import BindObjectMixin
class CollectionViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): class CollectionViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
"""ViewSet for Collection model.""" """ViewSet for Collection model."""
pagination_class = None # pagination_class = None
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
queryset = models.Collection.objects.all()
serializer_class = serializers.CollectionBackOfficeSerializer serializer_class = serializers.CollectionBackOfficeSerializer
def get_queryset(self):
"""Overridden method 'get_queryset'."""
qs = models.Collection.objects.all().order_by('-created')
if self.request.country_code:
qs = qs.by_country_code(self.request.country_code)
return qs
class CollectionBackOfficeViewSet(mixins.CreateModelMixin, class CollectionBackOfficeViewSet(mixins.CreateModelMixin,
mixins.UpdateModelMixin, mixins.UpdateModelMixin,
@ -25,9 +32,13 @@ class CollectionBackOfficeViewSet(mixins.CreateModelMixin,
permission_classes = (permissions.IsAuthenticated,) permission_classes = (permissions.IsAuthenticated,)
queryset = models.Collection.objects.all() queryset = models.Collection.objects.all()
filter_backends = [DjangoFilterBackend, OrderingFilter]
serializer_class = serializers.CollectionBackOfficeSerializer serializer_class = serializers.CollectionBackOfficeSerializer
bind_object_serializer_class = serializers.CollectionBindObjectSerializer bind_object_serializer_class = serializers.CollectionBindObjectSerializer
ordering_fields = ('rank', 'start')
ordering = ('-start', )
def perform_binding(self, serializer): def perform_binding(self, serializer):
data = serializer.validated_data data = serializer.validated_data
collection = data.pop('collection') collection = data.pop('collection')

View File

@ -212,8 +212,18 @@ class EstablishmentQuerySet(models.QuerySet):
output_field=models.FloatField(default=0) output_field=models.FloatField(default=0)
)) ))
def similar_base(self, establishment): def has_location(self):
"""Return objects with geo location."""
return self.filter(address__coordinates__isnull=False)
def similar_base(self, establishment):
"""
Return filtered QuerySet by base filters.
Filters including:
1 Filter by type (and subtype) establishment.
2 Filter by published Review.
3 With annotated distance.
"""
filters = { filters = {
'reviews__status': Review.READY, 'reviews__status': Review.READY,
'establishment_type': establishment.establishment_type, 'establishment_type': establishment.establishment_type,
@ -224,27 +234,69 @@ class EstablishmentQuerySet(models.QuerySet):
.filter(**filters) \ .filter(**filters) \
.annotate_distance(point=establishment.location) .annotate_distance(point=establishment.location)
def similar_base_subquery(self, establishment, filters: dict) -> Subquery:
"""
Return filtered Subquery object by filters.
Filters including:
1 Filter by transmitted filters.
2 With ordering by distance.
"""
return Subquery(
self.similar_base(establishment)
.filter(**filters)
.order_by('distance')[:settings.LIMITING_QUERY_OBJECTS]
.values('id')
)
def similar_restaurants(self, slug): def similar_restaurants(self, slug):
""" """
Return QuerySet with objects that similar to Restaurant. Return QuerySet with objects that similar to Restaurant.
:param restaurant_slug: str Establishment slug :param slug: str restaurant slug
""" """
restaurant_qs = self.filter(slug=slug, restaurant_qs = self.filter(slug=slug)
public_mark__isnull=False)
if restaurant_qs.exists(): if restaurant_qs.exists():
establishment = restaurant_qs.first() restaurant = restaurant_qs.first()
subquery_filter_by_distance = Subquery( ids_by_subquery = self.similar_base_subquery(
self.similar_base(establishment) establishment=restaurant,
.filter(public_mark__gte=10, filters={
establishment_gallery__is_main=True) 'public_mark__gte': 10,
.order_by('distance')[:settings.LIMITING_QUERY_OBJECTS] 'establishment_gallery__is_main': True,
.values('id') }
) )
return self.filter(id__in=subquery_filter_by_distance) \ return self.filter(id__in=ids_by_subquery) \
.annotate_intermediate_public_mark() \ .annotate_intermediate_public_mark() \
.annotate_mark_similarity(mark=establishment.public_mark) \ .annotate_mark_similarity(mark=restaurant.public_mark) \
.order_by('mark_similarity') \ .order_by('mark_similarity') \
.distinct('mark_similarity', 'id') .distinct('mark_similarity', 'id')
else:
return self.none()
def same_subtype(self, establishment):
"""Annotate flag same subtype."""
return self.annotate(same_subtype=Case(
models.When(
establishment_subtypes__in=establishment.establishment_subtypes.all(),
then=True
),
default=False,
output_field=models.BooleanField(default=False)
))
def similar_artisans_producers(self, slug):
"""
Return QuerySet with objects that similar to Artisan/Producer(s).
:param slug: str artisan/producer slug
"""
establishment_qs = self.filter(slug=slug)
if establishment_qs.exists():
establishment = establishment_qs.first()
return self.similar_base(establishment) \
.same_subtype(establishment) \
.order_by(F('same_subtype').desc(),
F('distance').asc()) \
.distinct('same_subtype', 'distance', 'id')
else:
return self.none()
def by_wine_region(self, wine_region): def by_wine_region(self, wine_region):
""" """
@ -498,9 +550,15 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin,
def visible_tags(self): def visible_tags(self):
return super().visible_tags \ return super().visible_tags \
.exclude(category__index_name__in=['guide', 'collection', 'purchased_item', .exclude(category__index_name__in=['guide', 'collection', 'purchased_item',
'business_tag', 'business_tags_de', 'tag']) 'business_tag', 'business_tags_de']) \
.exclude(value__in=['rss', 'rss_selection'])
# todo: recalculate toque_number # todo: recalculate toque_number
@property
def visible_tags_detail(self):
"""Removes some tags from detail Establishment representation"""
return self.visible_tags.exclude(category__index_name__in=['tag'])
def recalculate_toque_number(self): def recalculate_toque_number(self):
toque_number = 0 toque_number = 0
if self.address and self.public_mark: if self.address and self.public_mark:

View File

@ -324,3 +324,14 @@ class EstablishmentNoteListCreateSerializer(EstablishmentNoteBaseSerializer):
"""Return establishment instance from view.""" """Return establishment instance from view."""
if self.serializer_view: if self.serializer_view:
return self.serializer_view.get_object() return self.serializer_view.get_object()
class EstablishmentAdminListSerializer(UserShortSerializer):
"""Establishment admin serializer."""
class Meta:
model = UserShortSerializer.Meta.model
fields = [
'id',
'username',
'email'
]

View File

@ -28,6 +28,8 @@ urlpatterns = [
name='note-list-create'), name='note-list-create'),
path('slug/<slug:slug>/notes/<int:note_pk>/', views.EstablishmentNoteRUDView.as_view(), path('slug/<slug:slug>/notes/<int:note_pk>/', views.EstablishmentNoteRUDView.as_view(),
name='note-rud'), name='note-rud'),
path('slug/<slug:slug>/admin/', views.EstablishmentAdminView.as_view(),
name='establishment-admin-list'),
path('menus/', views.MenuListCreateView.as_view(), name='menu-list'), path('menus/', views.MenuListCreateView.as_view(), name='menu-list'),
path('menus/<int:pk>/', views.MenuRUDView.as_view(), name='menu-rud'), path('menus/<int:pk>/', views.MenuRUDView.as_view(), name='menu-rud'),
path('plates/', views.PlateListCreateView.as_view(), name='plates'), path('plates/', views.PlateListCreateView.as_view(), name='plates'),

View File

@ -17,10 +17,14 @@ urlpatterns = [
path('slug/<slug:slug>/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(), path('slug/<slug:slug>/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(),
name='create-destroy-favorites'), name='create-destroy-favorites'),
# similar establishments # similar establishments by type/subtype
path('slug/<slug:slug>/similar/', views.RestaurantSimilarListView.as_view(), path('slug/<slug:slug>/similar/', views.RestaurantSimilarListView.as_view(),
name='similar-restaurants'), name='similar-restaurants'),
path('slug/<slug:slug>/similar/wineries/', views.WinerySimilarListView.as_view(), path('slug/<slug:slug>/similar/wineries/', views.WinerySimilarListView.as_view(),
name='similar-restaurants'), name='similar-wineries'),
# temporary uses single mechanism, bec. description in process
path('slug/<slug:slug>/similar/artisans/', views.ArtisanProducerSimilarListView.as_view(),
name='similar-artisans'),
path('slug/<slug:slug>/similar/producers/', views.ArtisanProducerSimilarListView.as_view(),
name='similar-producers'),
] ]

View File

@ -1,8 +1,10 @@
"""Establishment app views.""" """Establishment app views."""
from django.http import Http404, HttpResponse from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics, permissions, status from rest_framework import generics, permissions, status
from account.models import User
from establishment import filters, models, serializers from establishment import filters, models, serializers
from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer
from utils.permissions import IsCountryAdmin, IsEstablishmentManager, IsWineryReviewer from utils.permissions import IsCountryAdmin, IsEstablishmentManager, IsWineryReviewer
@ -41,7 +43,7 @@ class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Establishment schedule RUD view""" """Establishment schedule RUD view"""
lookup_field = 'slug' lookup_field = 'slug'
serializer_class = ScheduleRUDSerializer serializer_class = ScheduleRUDSerializer
permission_classes = [IsWineryReviewer |IsEstablishmentManager] permission_classes = [IsWineryReviewer | IsEstablishmentManager]
def get_object(self): def get_object(self):
""" """
@ -75,6 +77,11 @@ class MenuListCreateView(generics.ListCreateAPIView):
serializer_class = serializers.MenuSerializers serializer_class = serializers.MenuSerializers
queryset = models.Menu.objects.all() queryset = models.Menu.objects.all()
permission_classes = [IsWineryReviewer | IsEstablishmentManager] permission_classes = [IsWineryReviewer | IsEstablishmentManager]
filter_backends = (DjangoFilterBackend,)
filterset_fields = (
'establishment',
'establishment__slug',
)
class MenuRUDView(generics.RetrieveUpdateDestroyAPIView): class MenuRUDView(generics.RetrieveUpdateDestroyAPIView):
@ -161,7 +168,7 @@ class EmailRUDView(generics.RetrieveUpdateDestroyAPIView):
class EmployeeListCreateView(generics.ListCreateAPIView): class EmployeeListCreateView(generics.ListCreateAPIView):
"""Emplyoee list create view.""" """Emplyoee list create view."""
permission_classes = (permissions.AllowAny, ) permission_classes = (permissions.AllowAny,)
filter_class = filters.EmployeeBackFilter filter_class = filters.EmployeeBackFilter
serializer_class = serializers.EmployeeBackSerializers serializer_class = serializers.EmployeeBackSerializers
queryset = models.Employee.objects.all() queryset = models.Employee.objects.all()
@ -170,7 +177,7 @@ class EmployeeListCreateView(generics.ListCreateAPIView):
class EstablishmentEmployeeListView(generics.ListCreateAPIView): class EstablishmentEmployeeListView(generics.ListCreateAPIView):
"""Establishment emplyoees list view.""" """Establishment emplyoees list view."""
permission_classes = (permissions.AllowAny, ) permission_classes = (permissions.AllowAny,)
serializer_class = serializers.EstablishmentEmployeeBackSerializer serializer_class = serializers.EstablishmentEmployeeBackSerializer
def get_queryset(self): def get_queryset(self):
@ -352,8 +359,8 @@ class EstablishmentEmployeeCreateView(generics.CreateAPIView):
class EstablishmentEmployeeDeleteView(generics.DestroyAPIView): class EstablishmentEmployeeDeleteView(generics.DestroyAPIView):
def _get_object_to_delete(self, establishment_id, employee_id): def _get_object_to_delete(self, establishment_id, employee_id):
result_qs = models.EstablishmentEmployee\ result_qs = models.EstablishmentEmployee \
.objects\ .objects \
.filter(establishment_id=establishment_id, employee_id=employee_id) .filter(establishment_id=establishment_id, employee_id=employee_id)
if not result_qs.exists(): if not result_qs.exists():
raise Http404 raise Http404
@ -371,6 +378,17 @@ class EstablishmentPositionListView(generics.ListAPIView):
"""Establishment positions list view.""" """Establishment positions list view."""
pagination_class = None pagination_class = None
permission_classes = (permissions.AllowAny, ) permission_classes = (permissions.AllowAny,)
queryset = models.Position.objects.all() queryset = models.Position.objects.all()
serializer_class = serializers.PositionBackSerializer serializer_class = serializers.PositionBackSerializer
class EstablishmentAdminView(generics.ListAPIView):
"""Establishment admin list view."""
serializer_class = serializers.EstablishmentAdminListSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly, )
def get_queryset(self):
establishment = get_object_or_404(
models.Establishment, slug=self.kwargs['slug'])
return User.objects.establishment_admin(establishment).distinct()

View File

@ -8,7 +8,7 @@ from comment import models as comment_models
from comment.serializers import CommentRUDSerializer from comment.serializers import CommentRUDSerializer
from establishment import filters, models, serializers from establishment import filters, models, serializers
from main import methods from main import methods
from utils.pagination import EstablishmentPortionPagination from utils.pagination import PortionPagination
from utils.views import FavoritesCreateDestroyMixinView, CarouselCreateDestroyMixinView from utils.views import FavoritesCreateDestroyMixinView, CarouselCreateDestroyMixinView
@ -41,6 +41,12 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
.with_certain_tag_category_related('shop_category', 'artisan_category') .with_certain_tag_category_related('shop_category', 'artisan_category')
class EstablishmentSimilarView(EstablishmentListView):
"""Resource for getting a list of similar establishments."""
serializer_class = serializers.EstablishmentSimilarSerializer
pagination_class = PortionPagination
class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView): class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView):
"""Resource for getting a establishment.""" """Resource for getting a establishment."""
@ -61,7 +67,7 @@ class EstablishmentMobileRetrieveView(EstablishmentRetrieveView):
class EstablishmentRecentReviewListView(EstablishmentListView): class EstablishmentRecentReviewListView(EstablishmentListView):
"""List view for last reviewed establishments.""" """List view for last reviewed establishments."""
pagination_class = EstablishmentPortionPagination pagination_class = PortionPagination
def get_queryset(self): def get_queryset(self):
"""Overridden method 'get_queryset'.""" """Overridden method 'get_queryset'."""
@ -77,30 +83,36 @@ class EstablishmentRecentReviewListView(EstablishmentListView):
return qs.last_reviewed(point=point) return qs.last_reviewed(point=point)
class EstablishmentSimilarList(EstablishmentListView): class RestaurantSimilarListView(EstablishmentSimilarView):
"""Resource for getting a list of similar establishments."""
serializer_class = serializers.EstablishmentSimilarSerializer
pagination_class = EstablishmentPortionPagination
class RestaurantSimilarListView(EstablishmentSimilarList):
"""Resource for getting a list of similar restaurants.""" """Resource for getting a list of similar restaurants."""
def get_queryset(self): def get_queryset(self):
"""Override get_queryset method""" """Overridden get_queryset method"""
return EstablishmentMixinView.get_queryset(self) \ return EstablishmentMixinView.get_queryset(self) \
.has_location() \
.similar_restaurants(slug=self.kwargs.get('slug')) .similar_restaurants(slug=self.kwargs.get('slug'))
class WinerySimilarListView(EstablishmentSimilarList): class WinerySimilarListView(EstablishmentSimilarView):
"""Resource for getting a list of similar wineries.""" """Resource for getting a list of similar wineries."""
def get_queryset(self): def get_queryset(self):
"""Override get_queryset method""" """Overridden get_queryset method"""
return EstablishmentMixinView.get_queryset(self) \ return EstablishmentMixinView.get_queryset(self) \
.has_location() \
.similar_wineries(slug=self.kwargs.get('slug')) .similar_wineries(slug=self.kwargs.get('slug'))
class ArtisanProducerSimilarListView(EstablishmentSimilarView):
"""Resource for getting a list of similar artisan/producer(s)."""
def get_queryset(self):
"""Overridden get_queryset method"""
return EstablishmentMixinView.get_queryset(self) \
.has_location() \
.similar_artisans_producers(slug=self.kwargs.get('slug'))
class EstablishmentTypeListView(generics.ListAPIView): class EstablishmentTypeListView(generics.ListAPIView):
"""Resource for getting a list of establishment types.""" """Resource for getting a list of establishment types."""

View File

@ -0,0 +1,17 @@
# Generated by Django 2.2.7 on 2019-12-11 15:28
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('gallery', '0006_merge_20191027_1758'),
]
operations = [
migrations.AlterModelOptions(
name='image',
options={'ordering': ['-modified'], 'verbose_name': 'Image', 'verbose_name_plural': 'Images'},
),
]

View File

@ -34,6 +34,7 @@ class Image(ProjectBaseMixin, SORLImageMixin, PlatformMixin):
"""Meta class.""" """Meta class."""
verbose_name = _('Image') verbose_name = _('Image')
verbose_name_plural = _('Images') verbose_name_plural = _('Images')
ordering = ['-modified']
def __str__(self): def __str__(self):
"""String representation""" """String representation"""

View File

@ -1,8 +1,9 @@
from django.conf import settings from django.conf import settings
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.files.base import ContentFile
from rest_framework import serializers from rest_framework import serializers
from sorl.thumbnail.parsers import parse_crop from sorl.thumbnail import get_thumbnail
from sorl.thumbnail.parsers import ThumbnailParseError 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 . import models from . import models
@ -88,15 +89,23 @@ class CropImageSerializer(ImageSerializer):
quality = validated_data.pop('quality') quality = validated_data.pop('quality')
crop = validated_data.pop('crop') crop = validated_data.pop('crop')
crop_params = {
'geometry': f'{width}x{height}',
'quality': quality,
'crop': crop,
}
cropped_image = self._image.get_cropped_image(**crop_params)
image = self._image image = self._image
image.pk = None
crop_params['geometry_string'] = crop_params.pop('geometry')
resized = get_thumbnail(self._image.image, **crop_params)
image.image.save(resized.name, ContentFile(resized.read()), True)
image.save()
if image and width and height: if image and width and height:
setattr(image, setattr(image,
'cropped_image', 'cropped_image',
image.get_cropped_image( cropped_image)
geometry=f'{width}x{height}',
quality=quality,
crop=crop))
return image return image
@property @property

View File

@ -10,6 +10,7 @@ urlpatterns = [
path('addresses/<int:pk>/', views.AddressRUDView.as_view(), name='address-RUD'), path('addresses/<int:pk>/', views.AddressRUDView.as_view(), name='address-RUD'),
path('cities/', views.CityListCreateView.as_view(), name='city-list-create'), path('cities/', views.CityListCreateView.as_view(), name='city-list-create'),
path('cities/all/', views.CityListSearchView.as_view(), name='city-list-create'),
path('cities/<int:pk>/', views.CityRUDView.as_view(), name='city-retrieve'), path('cities/<int:pk>/', views.CityRUDView.as_view(), name='city-retrieve'),
path('cities/<int:pk>/gallery/', views.CityGalleryListView.as_view(), path('cities/<int:pk>/gallery/', views.CityGalleryListView.as_view(),
name='gallery-list'), name='gallery-list'),

View File

@ -37,6 +37,15 @@ class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView):
filter_class = filters.CityBackFilter filter_class = filters.CityBackFilter
class CityListSearchView(common.CityViewMixin, generics.ListCreateAPIView):
"""Create view for model City."""
serializer_class = serializers.CitySerializer
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
queryset = models.City.objects.all()
filter_class = filters.CityBackFilter
pagination_class = None
class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView): class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView):
"""RUD view for model City.""" """RUD view for model City."""
serializer_class = serializers.CitySerializer serializer_class = serializers.CitySerializer

View File

@ -51,3 +51,6 @@ class PageTypeAdmin(admin.ModelAdmin):
@admin.register(models.Page) @admin.register(models.Page)
class PageAdmin(admin.ModelAdmin): class PageAdmin(admin.ModelAdmin):
"""Page admin.""" """Page admin."""
list_display = ('id', '__str__', 'advertisement')
list_filter = ('advertisement__url', 'source')
date_hierarchy = 'created'

View File

@ -0,0 +1,38 @@
from django.core.management.base import BaseCommand
from tqdm import tqdm
from account.models import User
from main.models import Panel, SiteSettings
from transfer.models import Panels
class Command(BaseCommand):
help = '''Add panels from legacy DB.'''
def handle(self, *args, **kwargs):
objects = []
deleted = 0
panels_list = Panels.objects.filter(name__isnull=False)
# remove existing panel
exist_panel = Panel.objects.filter(old_id__isnull=False)
if exist_panel.exists():
deleted = exist_panel.count()
exist_panel.delete()
for old_panel in tqdm(panels_list, desc='Add panels'):
site = SiteSettings.objects.filter(old_id=old_panel.site_id).first()
user = User.objects.filter(old_id=old_panel.site_id).first()
if site:
new_panel = Panel(
old_id=old_panel.id,
user=user,
site=site,
name=old_panel.name,
display=old_panel.display,
description=old_panel.description,
query=old_panel.query,
)
objects.append(new_panel)
Panel.objects.bulk_create(objects)
self.stdout.write(
self.style.WARNING(f'Created {len(objects)}/Deleted {deleted} footer objects.'))

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-12-11 06:31
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('advertisement', '0008_auto_20191116_1135'),
('main', '0040_footer'),
]
operations = [
migrations.AlterUniqueTogether(
name='page',
unique_together={('advertisement', 'source')},
),
]

View File

@ -0,0 +1,36 @@
# Generated by Django 2.2.7 on 2019-12-12 12:00
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('main', '0041_auto_20191211_0631'),
]
operations = [
migrations.CreateModel(
name='Panel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')),
('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('name', models.CharField(max_length=255, verbose_name='name')),
('display', models.CharField(blank=True, choices=[('table', 'table'), ('table', 'mailing')], default=None, max_length=255, null=True, verbose_name='display')),
('description', models.CharField(blank=True, default=None, max_length=255, null=True, verbose_name='description')),
('query', models.TextField(blank=True, default=None, null=True, verbose_name='query')),
('old_id', models.IntegerField(blank=True, default=None, null=True, verbose_name='old id')),
('site', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='main.SiteSettings', verbose_name='site')),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
'verbose_name': 'panel',
'verbose_name_plural': 'panels',
},
),
]

View File

@ -305,7 +305,7 @@ class PageQuerySet(models.QuerySet):
def by_platform(self, platform: int): def by_platform(self, platform: int):
"""Filter by platform.""" """Filter by platform."""
return self.filter(source=platform) return self.filter(source__in=[Page.ALL, platform])
class Page(URLImageMixin, PlatformMixin, ProjectBaseMixin): class Page(URLImageMixin, PlatformMixin, ProjectBaseMixin):
@ -325,6 +325,7 @@ class Page(URLImageMixin, PlatformMixin, ProjectBaseMixin):
"""Meta class.""" """Meta class."""
verbose_name = _('page') verbose_name = _('page')
verbose_name_plural = _('pages') verbose_name_plural = _('pages')
unique_together = ('advertisement', 'source')
def __str__(self): def __str__(self):
"""Overridden dunder method.""" """Overridden dunder method."""
@ -360,3 +361,46 @@ class Footer(ProjectBaseMixin):
) )
about_us = models.TextField(_('about_us')) about_us = models.TextField(_('about_us'))
copyright = models.TextField(_('copyright')) copyright = models.TextField(_('copyright'))
class PanelQuerySet(models.QuerySet):
"""Panels QuerySet."""
class Panel(ProjectBaseMixin):
"""Custom panel model with stored SQL query."""
TABLE = 'table'
MAILING = 'table'
DISPLAY_CHOICES = (
(TABLE, _('table')),
(MAILING, _('mailing'))
)
name = models.CharField(_('name'), max_length=255)
display = models.CharField(
_('display'), max_length=255, choices=DISPLAY_CHOICES,
blank=True, null=True, default=None
)
description = models.CharField(
_('description'), max_length=255, blank=True, null=True, default=None)
query = models.TextField(_('query'), blank=True, null=True, default=None)
user = models.ForeignKey(
'account.User', verbose_name=_('user'), null=True,
on_delete=models.SET_NULL)
site = models.ForeignKey(
'main.SiteSettings', verbose_name=_('site'), null=True,
on_delete=models.SET_NULL)
old_id = models.IntegerField(
_('old id'), null=True, blank=True, default=None)
objects = PanelQuerySet.as_manager()
class Meta:
verbose_name = _('panel')
verbose_name_plural = _('panels')
def __str__(self):
return self.name
def execute_query(self):
pass

View File

@ -5,6 +5,8 @@ from rest_framework import serializers
from location.serializers import CountrySerializer from location.serializers import CountrySerializer
from main import models from main import models
from utils.serializers import ProjectModelSerializer, TranslatedField, RecursiveFieldSerializer from utils.serializers import ProjectModelSerializer, TranslatedField, RecursiveFieldSerializer
from account.serializers.back import BackUserSerializer
from account.models import User
class FeatureSerializer(serializers.ModelSerializer): class FeatureSerializer(serializers.ModelSerializer):
@ -152,8 +154,6 @@ class SiteShortSerializer(serializers.ModelSerializer):
] ]
class AwardBaseSerializer(serializers.ModelSerializer): class AwardBaseSerializer(serializers.ModelSerializer):
"""Award base serializer.""" """Award base serializer."""
@ -234,10 +234,26 @@ class PageBaseSerializer(serializers.ModelSerializer):
'advertisement', 'advertisement',
] ]
extra_kwargs = { extra_kwargs = {
'establishment': {'write_only': True} 'advertisement': {'write_only': True},
'image_url': {'required': True},
'width': {'required': True},
'height': {'required': True},
} }
class PageExtendedSerializer(PageBaseSerializer):
"""Extended serializer for model Page."""
source_display = serializers.CharField(read_only=True,
source='get_source_display')
class Meta(PageBaseSerializer.Meta):
"""Meta class."""
fields = PageBaseSerializer.Meta.fields + [
'source',
'source_display',
]
class PageTypeBaseSerializer(serializers.ModelSerializer): class PageTypeBaseSerializer(serializers.ModelSerializer):
"""Serializer fro model PageType.""" """Serializer fro model PageType."""
@ -251,8 +267,32 @@ class PageTypeBaseSerializer(serializers.ModelSerializer):
class ContentTypeBackSerializer(serializers.ModelSerializer): class ContentTypeBackSerializer(serializers.ModelSerializer):
"""Serializer fro model ContentType.""" """Serializer for model ContentType."""
class Meta: class Meta:
model = ContentType model = ContentType
fields = '__all__' fields = '__all__'
class PanelSerializer(serializers.ModelSerializer):
"""Serializer for Custom panel."""
user_id = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(),
source='user',
write_only=True
)
user = BackUserSerializer(read_only=True)
class Meta:
model = models.Panel
fields = [
'id',
'name',
'display',
'description',
'query',
'created',
'modified',
'user',
'user_id'
]

View File

@ -20,6 +20,12 @@ urlpatterns = [
name='site-feature-rud'), name='site-feature-rud'),
path('footer/', views.FooterBackView.as_view(), name='footer-list-create'), path('footer/', views.FooterBackView.as_view(), name='footer-list-create'),
path('footer/<int:pk>/', views.FooterRUDBackView.as_view(), name='footer-rud'), path('footer/<int:pk>/', views.FooterRUDBackView.as_view(), name='footer-rud'),
path('page-types/', views.PageTypeListCreateView.as_view(),
name='page-types-list-create'),
path('panels/', views.PanelsListCreateView.as_view(), name='panels'),
path('panels/<int:pk>/', views.PanelsListCreateView.as_view(), name='panels-rud'),
# path('panels/<int:pk>/execute/', views.PanelsView.as_view(), name='panels-execute')
] ]

View File

@ -4,7 +4,7 @@ from rest_framework import generics, permissions
from main import serializers from main import serializers
from main.filters import AwardFilter from main.filters import AwardFilter
from main.models import Award, Footer from main.models import Award, Footer, PageType, Panel
from main.views import SiteSettingsView, SiteListView from main.views import SiteSettingsView, SiteListView
@ -81,3 +81,29 @@ class FooterRUDBackView(generics.RetrieveUpdateDestroyAPIView):
permission_classes = (permissions.IsAuthenticatedOrReadOnly,) permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
serializer_class = serializers.FooterBackSerializer serializer_class = serializers.FooterBackSerializer
queryset = Footer.objects.all() queryset = Footer.objects.all()
class PageTypeListCreateView(generics.ListCreateAPIView):
"""PageType back office view."""
permission_classes = (permissions.IsAuthenticatedOrReadOnly, )
pagination_class = None
serializer_class = serializers.PageTypeBaseSerializer
queryset = PageType.objects.all()
class PanelsListCreateView(generics.ListCreateAPIView):
"""Custom panels view."""
permission_classes = (
permissions.IsAdminUser,
)
serializer_class = serializers.PanelSerializer
queryset = Panel.objects.all()
class PanelsRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Custom panels view."""
permission_classes = (
permissions.IsAdminUser,
)
serializer_class = serializers.PanelSerializer
queryset = Panel.objects.all()

View File

@ -70,9 +70,6 @@ class CarouselListView(generics.ListAPIView):
def get_queryset(self): def get_queryset(self):
country_code = self.request.country_code country_code = self.request.country_code
if hasattr(settings, 'CAROUSEL_ITEMS') and country_code in settings.INTERNATIONAL_COUNTRY_CODES:
qs = models.Carousel.objects.filter(id__in=settings.CAROUSEL_ITEMS)
return qs
qs = models.Carousel.objects.is_parsed().active() qs = models.Carousel.objects.is_parsed().active()
if country_code: if country_code:
qs = qs.by_country_code(country_code) qs = qs.by_country_code(country_code)

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-12-11 15:28
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('gallery', '0007_auto_20191211_1528'),
('news', '0040_remove_news_slug'),
]
operations = [
migrations.AlterUniqueTogether(
name='newsgallery',
unique_together={('news', 'image')},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-12-12 13:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('news', '0041_auto_20191211_1528'),
]
operations = [
migrations.AddField(
model_name='news',
name='duplication_date',
field=models.DateTimeField(blank=True, default=None, null=True, verbose_name='Duplication datetime'),
),
]

View File

@ -0,0 +1,37 @@
# Generated by Django 2.2.7 on 2019-12-16 19:20
import django.contrib.postgres.fields.hstore
from django.db import migrations, models
import uuid
def fill_uuid(apps, schemaeditor):
News = apps.get_model('news', 'News')
for news in News.objects.all():
news.duplication_uuid = uuid.uuid4()
news.save()
class Migration(migrations.Migration):
dependencies = [
('news', '0042_news_duplication_date'),
]
operations = [
migrations.AddField(
model_name='news',
name='description_to_locale_is_active',
field=django.contrib.postgres.fields.hstore.HStoreField(blank=True, default=dict, help_text='{"en-GB": true, "fr-FR": false}', null=True, verbose_name='Is description for certain locale active'),
),
migrations.AddField(
model_name='news',
name='duplication_uuid',
field=models.UUIDField(default=uuid.uuid4, verbose_name='Field to detect doubles'),
),
migrations.AlterField(
model_name='news',
name='slugs',
field=django.contrib.postgres.fields.hstore.HStoreField(blank=True, default=dict, help_text='{"en-GB":"some slug"}', null=True, verbose_name='Slugs for current news obj'),
),
migrations.RunPython(fill_uuid, migrations.RunPython.noop),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-12-16 20:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('news', '0043_auto_20191216_1920'),
]
operations = [
migrations.RenameField(
model_name='news',
old_name='description_to_locale_is_active',
new_name='locale_to_description_is_active',
),
]

View File

@ -1,5 +1,9 @@
"""News app models.""" """News app models."""
import uuid
from django.conf import settings
from django.contrib.contenttypes import fields as generic from django.contrib.contenttypes import fields as generic
from django.contrib.postgres.fields import HStoreField
from django.db import models from django.db import models
from django.db.models import Case, When from django.db.models import Case, When
from django.utils import timezone from django.utils import timezone
@ -11,8 +15,6 @@ from utils.models import (BaseAttributes, TJSONField, TranslatedFieldsMixin, Has
ProjectBaseMixin, GalleryModelMixin, IntermediateGalleryModelMixin, ProjectBaseMixin, GalleryModelMixin, IntermediateGalleryModelMixin,
FavoritesMixin) FavoritesMixin)
from utils.querysets import TranslationQuerysetMixin from utils.querysets import TranslationQuerysetMixin
from django.conf import settings
from django.contrib.postgres.fields import HStoreField
class Agenda(ProjectBaseMixin, TranslatedFieldsMixin): class Agenda(ProjectBaseMixin, TranslatedFieldsMixin):
@ -177,11 +179,14 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
description = TJSONField(blank=True, null=True, default=None, description = TJSONField(blank=True, null=True, default=None,
verbose_name=_('description'), verbose_name=_('description'),
help_text='{"en-GB":"some text"}') help_text='{"en-GB":"some text"}')
locale_to_description_is_active = HStoreField(null=True, default=dict, blank=True,
verbose_name=_('Is description for certain locale active'),
help_text='{"en-GB": true, "fr-FR": false}')
start = models.DateTimeField(blank=True, null=True, default=None, start = models.DateTimeField(blank=True, null=True, default=None,
verbose_name=_('Start')) verbose_name=_('Start'))
end = models.DateTimeField(blank=True, null=True, default=None, end = models.DateTimeField(blank=True, null=True, default=None,
verbose_name=_('End')) verbose_name=_('End'))
slugs = HStoreField(null=True, blank=True, default=None, slugs = HStoreField(null=True, blank=True, default=dict,
verbose_name=_('Slugs for current news obj'), verbose_name=_('Slugs for current news obj'),
help_text='{"en-GB":"some slug"}') help_text='{"en-GB":"some slug"}')
state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES, state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES,
@ -211,6 +216,10 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
verbose_name=_('banner')) verbose_name=_('banner'))
site = models.ForeignKey('main.SiteSettings', blank=True, null=True, site = models.ForeignKey('main.SiteSettings', blank=True, null=True,
on_delete=models.SET_NULL, verbose_name=_('site settings')) on_delete=models.SET_NULL, verbose_name=_('site settings'))
duplication_date = models.DateTimeField(blank=True, null=True, default=None,
verbose_name=_('Duplication datetime'))
duplication_uuid = models.UUIDField(default=uuid.uuid4, editable=True, unique=False,
verbose_name=_('Field to detect doubles'))
objects = NewsQuerySet.as_manager() objects = NewsQuerySet.as_manager()
class Meta: class Meta:
@ -220,7 +229,22 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
verbose_name_plural = _('news') verbose_name_plural = _('news')
def __str__(self): def __str__(self):
return f'news: {self.slug}' return f'news: {next(iter(self.slugs.values()))}'
def create_duplicate(self, new_country, view_count_model):
self.pk = None
self.state = self.WAITING
self.slugs = {locale: f'{slug}-{new_country.code}' for locale, slug in self.slugs.items()}
self.country = new_country
self.views_count = view_count_model
self.duplication_date = timezone.now()
self.save()
@property
def duplicates(self):
"""Duplicates for this news item excluding same country code labeled"""
return News.objects.filter(duplication_uuid=self.duplication_uuid).exclude(country=self.country)
@property @property
def is_publish(self): def is_publish(self):
@ -320,4 +344,4 @@ class NewsGallery(IntermediateGalleryModelMixin):
"""NewsGallery meta class.""" """NewsGallery meta class."""
verbose_name = _('news gallery') verbose_name = _('news gallery')
verbose_name_plural = _('news galleries') verbose_name_plural = _('news galleries')
unique_together = (('news', 'is_main'), ('news', 'image')) unique_together = [['news', 'image'],]

View File

@ -13,6 +13,9 @@ from tag.serializers import TagBaseSerializer
from utils import exceptions as utils_exceptions from utils import exceptions as utils_exceptions
from utils.serializers import (TranslatedField, ProjectModelSerializer, from utils.serializers import (TranslatedField, ProjectModelSerializer,
FavoritesCreateSerializer, ImageBaseSerializer, CarouselCreateSerializer) FavoritesCreateSerializer, ImageBaseSerializer, CarouselCreateSerializer)
from rating import models as rating_models
from django.shortcuts import get_object_or_404
from utils.models import get_current_locale, get_default_locale
class AgendaSerializer(ProjectModelSerializer): class AgendaSerializer(ProjectModelSerializer):
@ -68,6 +71,13 @@ class NewsBaseSerializer(ProjectModelSerializer):
tags = TagBaseSerializer(read_only=True, many=True, source='visible_tags') tags = TagBaseSerializer(read_only=True, many=True, source='visible_tags')
in_favorites = serializers.BooleanField(allow_null=True, read_only=True) in_favorites = serializers.BooleanField(allow_null=True, read_only=True)
view_counter = serializers.IntegerField(read_only=True) view_counter = serializers.IntegerField(read_only=True)
slug = serializers.SerializerMethodField(read_only=True, allow_null=True)
def get_slug(self, obj):
if obj.slugs:
return obj.slugs.get(get_current_locale()) \
or obj.slugs.get(get_default_locale()) \
or next(iter(obj.slugs.values()))
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -75,12 +85,12 @@ class NewsBaseSerializer(ProjectModelSerializer):
model = models.News model = models.News
fields = ( fields = (
'id', 'id',
'slug',
'title_translated', 'title_translated',
'subtitle_translated', 'subtitle_translated',
'is_highlighted', 'is_highlighted',
'news_type', 'news_type',
'tags', 'tags',
'slugs',
'view_counter', 'view_counter',
) )
@ -171,19 +181,48 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
'title', 'title',
'backoffice_title', 'backoffice_title',
'subtitle', 'subtitle',
'slugs',
'locale_to_description_is_active',
'is_published', 'is_published',
'duplication_date',
) )
extra_kwargs = { extra_kwargs = {
'backoffice_title': {'allow_null': False}, 'backoffice_title': {'allow_null': False},
'duplication_date': {'read_only': True},
'locale_to_description_is_active': {'allow_null': False}
} }
def validate(self, attrs): def create(self, validated_data):
slugs = attrs.get('slugs', {}) slugs = validated_data.get('slugs')
if models.News.objects.filter( if slugs:
slugs__values__contains=list(slugs.values()) if models.News.objects.filter(
).exclude(id=attrs.get('id', 0)).exists(): slugs__values__contains=list(slugs.values())
raise serializers.ValidationError({'slugs': _('News with this slug already exists.')}) ).exists():
return attrs raise serializers.ValidationError({'slugs': _('News with this slug already exists.')})
return super().create(validated_data)
def update(self, instance, validated_data):
slugs = validated_data.get('slugs')
if slugs:
if models.News.objects.filter(
slugs__values__contains=list(slugs.values())
).exclude(pk=instance.pk).exists():
raise serializers.ValidationError({'slugs': _('News with this slug already exists.')})
return super().update(instance, validated_data)
class NewsBackOfficeDuplicationInfoSerializer(serializers.ModelSerializer):
"""Duplication info for news detail."""
country = CountrySimpleSerializer(read_only=True)
class Meta:
model = models.News
fields = (
'id',
'duplication_date',
'country',
)
class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer, class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
@ -201,6 +240,7 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
queryset=SiteSettings.objects.all()) queryset=SiteSettings.objects.all())
template_display = serializers.CharField(source='get_template_display', template_display = serializers.CharField(source='get_template_display',
read_only=True) read_only=True)
duplicates = NewsBackOfficeDuplicationInfoSerializer(many=True, allow_null=True, read_only=True)
class Meta(NewsBackOfficeBaseSerializer.Meta, NewsDetailSerializer.Meta): class Meta(NewsBackOfficeBaseSerializer.Meta, NewsDetailSerializer.Meta):
"""Meta class.""" """Meta class."""
@ -214,6 +254,7 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
'template', 'template',
'template_display', 'template_display',
'is_international', 'is_international',
'duplicates',
) )
@ -233,6 +274,16 @@ class NewsBackOfficeGallerySerializer(serializers.ModelSerializer):
"""Get url kwargs from request.""" """Get url kwargs from request."""
return self.context.get('request').parser_context.get('kwargs') return self.context.get('request').parser_context.get('kwargs')
def create(self, validated_data):
news_pk = self.get_request_kwargs().get('pk')
image_id = self.get_request_kwargs().get('image_id')
qs = models.NewsGallery.objects.filter(image_id=image_id, news_id=news_pk)
instance = qs.first()
if instance:
qs.update(**validated_data)
return instance
return super().create(validated_data)
def validate(self, attrs): def validate(self, attrs):
"""Override validate method.""" """Override validate method."""
news_pk = self.get_request_kwargs().get('pk') news_pk = self.get_request_kwargs().get('pk')
@ -249,8 +300,8 @@ class NewsBackOfficeGallerySerializer(serializers.ModelSerializer):
news = news_qs.first() news = news_qs.first()
image = image_qs.first() image = image_qs.first()
if image in news.gallery.all(): # if image in news.gallery.all():
raise serializers.ValidationError({'detail': _('Image is already added.')}) # raise serializers.ValidationError({'detail': _('Image is already added.')})
attrs['news'] = news attrs['news'] = news
attrs['image'] = image attrs['image'] = image
@ -307,3 +358,26 @@ class NewsCarouselCreateSerializer(CarouselCreateSerializer):
'content_object': validated_data.pop('news') 'content_object': validated_data.pop('news')
}) })
return super().create(validated_data) return super().create(validated_data)
class NewsCloneCreateSerializer(NewsBackOfficeBaseSerializer,
NewsDetailSerializer):
"""Serializer for creating news clone."""
template_display = serializers.CharField(source='get_template_display',
read_only=True)
duplicates = NewsBackOfficeDuplicationInfoSerializer(many=True, allow_null=True, read_only=True)
class Meta(NewsBackOfficeBaseSerializer.Meta, NewsDetailSerializer.Meta):
fields = NewsBackOfficeBaseSerializer.Meta.fields + NewsDetailSerializer.Meta.fields + (
'template_display',
'duplicates',
)
read_only_fields = fields
def create(self, validated_data):
kwargs = self.context.get('request').parser_context.get('kwargs')
instance = get_object_or_404(models.News, pk=kwargs['pk'])
new_country = get_object_or_404(location_models.Country, code=kwargs['country_code'])
view_count_model = rating_models.ViewCount.objects.create(count=0)
instance.create_duplicate(new_country, view_count_model)
return instance

View File

@ -14,4 +14,5 @@ urlpatterns = [
path('<int:pk>/gallery/<int:image_id>/', views.NewsBackOfficeGalleryCreateDestroyView.as_view(), path('<int:pk>/gallery/<int:image_id>/', views.NewsBackOfficeGalleryCreateDestroyView.as_view(),
name='gallery-create-destroy'), name='gallery-create-destroy'),
path('<int:pk>/carousels/', views.NewsCarouselCreateDestroyView.as_view(), name='create-destroy-carousels'), path('<int:pk>/carousels/', views.NewsCarouselCreateDestroyView.as_view(), name='create-destroy-carousels'),
path('<int:pk>/clone/<str:country_code>', views.NewsCloneView.as_view(), name='create-destroy-carousels'),
] ]

View File

@ -1,13 +1,13 @@
"""News app views.""" """News app views."""
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 rest_framework import generics, permissions from rest_framework import generics, permissions, response
from news import filters, models, serializers from news import filters, models, serializers
from rating.tasks import add_rating from rating.tasks import add_rating
from utils.permissions import IsCountryAdmin, IsContentPageManager from utils.permissions import IsCountryAdmin, IsContentPageManager
from utils.views import CreateDestroyGalleryViewMixin, FavoritesCreateDestroyMixinView, CarouselCreateDestroyMixinView from utils.views import CreateDestroyGalleryViewMixin, FavoritesCreateDestroyMixinView, CarouselCreateDestroyMixinView
from utils.serializers import ImageBaseSerializer from utils.serializers import ImageBaseSerializer, EmptySerializer
class NewsMixinView: class NewsMixinView:
@ -99,7 +99,10 @@ class NewsBackOfficeLCView(NewsBackOfficeMixinView,
def get_queryset(self): def get_queryset(self):
"""Override get_queryset method.""" """Override get_queryset method."""
return super().get_queryset().with_extended_related() qs = super().get_queryset().with_extended_related()
if self.request.country_code:
qs = qs.by_country_code(self.request.country_code)
return qs
class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView, class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView,
@ -107,6 +110,13 @@ class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView,
"""Resource for a create gallery for news for back-office users.""" """Resource for a create gallery for news for back-office users."""
serializer_class = serializers.NewsBackOfficeGallerySerializer serializer_class = serializers.NewsBackOfficeGallerySerializer
def create(self, request, *args, **kwargs):
_ = super().create(request, *args, **kwargs)
news_qs = self.filter_queryset(self.get_queryset())
return response.Response(
data=serializers.NewsDetailSerializer(get_object_or_404(news_qs, pk=kwargs.get('pk'))).data
)
def get_object(self): def get_object(self):
""" """
Returns the object the view is displaying. Returns the object the view is displaying.
@ -167,3 +177,10 @@ class NewsCarouselCreateDestroyView(CarouselCreateDestroyMixinView):
_model = models.News _model = models.News
serializer_class = serializers.NewsCarouselCreateSerializer serializer_class = serializers.NewsCarouselCreateSerializer
class NewsCloneView(generics.CreateAPIView):
"""View for creating clone News"""
permission_classes = (permissions.AllowAny, )
serializer_class = serializers.NewsCloneCreateSerializer
queryset = models.News.objects.all()

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.7 on 2019-12-12 09:26
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('product', '0020_merge_20191209_0911'),
]
operations = [
migrations.AlterField(
model_name='product',
name='product_type',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='products', to='product.ProductType', verbose_name='Type'),
),
]

View File

@ -1,13 +1,17 @@
"""Product app models.""" """Product app models."""
from django.conf import settings
from django.contrib.contenttypes import fields as generic from django.contrib.contenttypes import fields as generic
from django.contrib.gis.db import models as gis_models from django.contrib.gis.db import models as gis_models
from django.contrib.gis.db.models.functions import Distance
from django.contrib.gis.geos import Point
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models import Case, When from django.db.models import Case, When, F
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from location.models import WineOriginAddressMixin from location.models import WineOriginAddressMixin
from review.models import Review
from utils.models import (BaseAttributes, ProjectBaseMixin, HasTagsMixin, from utils.models import (BaseAttributes, ProjectBaseMixin, HasTagsMixin,
TranslatedFieldsMixin, TJSONField, FavoritesMixin, TranslatedFieldsMixin, TJSONField, FavoritesMixin,
GalleryModelMixin, IntermediateGalleryModelMixin) GalleryModelMixin, IntermediateGalleryModelMixin)
@ -143,6 +147,60 @@ class ProductQuerySet(models.QuerySet):
) )
) )
def annotate_distance(self, point: Point = None):
"""
Return QuerySet with annotated field - distance
Description:
"""
return self.annotate(distance=Distance('establishment__address__coordinates',
point,
srid=settings.GEO_DEFAULT_SRID))
def has_location(self):
"""Return objects with geo location."""
return self.filter(establishment__address__coordinates__isnull=False)
def same_subtype(self, product):
"""Annotate flag same subtype."""
return self.annotate(same_subtype=Case(
models.When(
subtypes__in=product.subtypes.all(),
then=True
),
default=False,
output_field=models.BooleanField(default=False)
))
def similar_base(self, product):
"""Return QuerySet filtered by base filters for Product model."""
filters = {
'reviews__status': Review.READY,
'product_type': product.product_type,
}
if product.subtypes.exists():
filters.update(
{'subtypes__in': product.subtypes.all()})
return self.exclude(id=product.id) \
.filter(**filters) \
.annotate_distance(point=product.establishment.location)
def similar(self, slug):
"""
Return QuerySet with objects that similar to Product.
:param slug: str product slug
"""
product_qs = self.filter(slug=slug)
if product_qs.exists():
product = product_qs.first()
return self.similar_base(product) \
.same_subtype(product) \
.order_by(F('same_subtype').desc(),
F('distance').asc()) \
.distinct('same_subtype', 'distance', 'id')
else:
return self.none()
class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes, class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes,
HasTagsMixin, FavoritesMixin): HasTagsMixin, FavoritesMixin):
@ -178,7 +236,7 @@ class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes,
default=None, help_text='{"en-GB":"some text"}') default=None, help_text='{"en-GB":"some text"}')
available = models.BooleanField(_('available'), default=True) available = models.BooleanField(_('available'), default=True)
product_type = models.ForeignKey(ProductType, on_delete=models.PROTECT, product_type = models.ForeignKey(ProductType, on_delete=models.PROTECT,
null=True, null=True, blank=True, default=None,
related_name='products', verbose_name=_('Type')) related_name='products', verbose_name=_('Type'))
subtypes = models.ManyToManyField(ProductSubType, blank=True, subtypes = models.ManyToManyField(ProductSubType, blank=True,
related_name='products', related_name='products',
@ -226,8 +284,8 @@ class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes,
awards = generic.GenericRelation(to='main.Award', related_query_name='product') awards = generic.GenericRelation(to='main.Award', related_query_name='product')
serial_number = models.CharField(max_length=255, serial_number = models.CharField(max_length=255,
default=None, null=True, default=None, null=True,
verbose_name=_('Serial number')) verbose_name=_('Serial number'))
site = models.ForeignKey(to='main.SiteSettings', null=True, blank=True, on_delete=models.CASCADE) site = models.ForeignKey(to='main.SiteSettings', null=True, blank=True, on_delete=models.CASCADE)

View File

@ -16,4 +16,13 @@ urlpatterns = [
name='create-comment'), name='create-comment'),
path('slug/<slug:slug>/comments/<int:comment_id>/', views.ProductCommentRUDView.as_view(), path('slug/<slug:slug>/comments/<int:comment_id>/', views.ProductCommentRUDView.as_view(),
name='rud-comment'), name='rud-comment'),
# similar products by type/subtype
# temporary uses single mechanism, bec. description in process
path('slug/<slug:slug>/similar/wines/', views.SimilarListView.as_view(),
name='similar-wine'),
path('slug/<slug:slug>/similar/liquors/', views.SimilarListView.as_view(),
name='similar-liquor'),
path('slug/<slug:slug>/similar/food/', views.SimilarListView.as_view(),
name='similar-food'),
] ]

View File

@ -6,6 +6,7 @@ from comment.models import Comment
from product import filters, serializers from product import filters, serializers
from comment.serializers import CommentRUDSerializer from comment.serializers import CommentRUDSerializer
from utils.views import FavoritesCreateDestroyMixinView from utils.views import FavoritesCreateDestroyMixinView
from utils.pagination import PortionPagination
class ProductBaseView(generics.GenericAPIView): class ProductBaseView(generics.GenericAPIView):
@ -31,6 +32,12 @@ class ProductListView(ProductBaseView, generics.ListAPIView):
return qs return qs
class ProductSimilarView(ProductListView):
"""Resource for getting a list of similar product."""
serializer_class = serializers.ProductBaseSerializer
pagination_class = PortionPagination
class ProductDetailView(ProductBaseView, generics.RetrieveAPIView): class ProductDetailView(ProductBaseView, generics.RetrieveAPIView):
"""Detail view fro model Product.""" """Detail view fro model Product."""
lookup_field = 'slug' lookup_field = 'slug'
@ -81,3 +88,14 @@ class ProductCommentRUDView(generics.RetrieveUpdateDestroyAPIView):
self.check_object_permissions(self.request, comment_obj) self.check_object_permissions(self.request, comment_obj)
return comment_obj return comment_obj
class SimilarListView(ProductSimilarView):
"""Return similar products."""
def get_queryset(self):
"""Overridden get_queryset method."""
return super().get_queryset() \
.has_location() \
.similar(slug=self.kwargs.get('slug'))

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-12-16 06:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recipe', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='recipe',
name='old_id',
field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='old id'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-12-16 13:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recipe', '0002_recipe_old_id'),
]
operations = [
migrations.AddField(
model_name='recipe',
name='slug',
field=models.SlugField(max_length=255, null=True, unique=True, verbose_name='Slug'),
),
]

View File

@ -25,6 +25,9 @@ class RecipeQuerySet(models.QuerySet):
default=False, default=False,
output_field=models.BooleanField(default=False))) output_field=models.BooleanField(default=False)))
def by_locale(self, locale):
return self.filter(title__icontains=locale)
class Recipe(TranslatedFieldsMixin, ImageMixin, BaseAttributes): class Recipe(TranslatedFieldsMixin, ImageMixin, BaseAttributes):
"""Recipe model.""" """Recipe model."""
@ -43,22 +46,19 @@ class Recipe(TranslatedFieldsMixin, ImageMixin, BaseAttributes):
STR_FIELD_NAME = 'title' STR_FIELD_NAME = 'title'
title = TJSONField(blank=True, null=True, default=None, verbose_name=_('Title'), title = TJSONField(blank=True, null=True, default=None, verbose_name=_('Title'), help_text='{"en-GB": "some text"}')
help_text='{"en-GB": "some text"}')
subtitle = TJSONField(blank=True, null=True, default=None, verbose_name=_('Subtitle'), subtitle = TJSONField(blank=True, null=True, default=None, verbose_name=_('Subtitle'),
help_text='{"en-GB": "some text"}') help_text='{"en-GB": "some text"}')
description = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'), description = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'),
help_text='{"en-GB": "some text"}') help_text='{"en-GB": "some text"}')
state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES, state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES, verbose_name=_('State'))
verbose_name=_('State')) author = models.CharField(max_length=255, blank=True, null=True, default=None, verbose_name=_('Author'))
author = models.CharField(max_length=255, blank=True, null=True, default=None, published_at = models.DateTimeField(verbose_name=_('Published at'), blank=True, default=None, null=True,
verbose_name=_('Author'))
published_at = models.DateTimeField(verbose_name=_('Published at'),
blank=True, default=None, null=True,
help_text=_('Published at')) help_text=_('Published at'))
published_scheduled_at = models.DateTimeField(verbose_name=_('Published scheduled at'), published_scheduled_at = models.DateTimeField(verbose_name=_('Published scheduled at'), blank=True, default=None,
blank=True, default=None, null=True, null=True, help_text=_('Published scheduled at'))
help_text=_('Published scheduled at')) old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
slug = models.SlugField(unique=True, max_length=255, null=True, verbose_name=_('Slug'))
objects = RecipeQuerySet.as_manager() objects = RecipeQuerySet.as_manager()

View File

@ -14,8 +14,15 @@ class RecipeListSerializer(serializers.ModelSerializer):
"""Meta class.""" """Meta class."""
model = models.Recipe model = models.Recipe
fields = ('id', 'title_translated', 'subtitle_translated', 'author', fields = (
'published_at', 'in_favorites') 'id',
'title_translated',
'subtitle_translated',
'author',
'created_by',
'published_at',
'in_favorites',
)
read_only_fields = fields read_only_fields = fields

View File

@ -1,19 +1,49 @@
from django.db.models import Value, IntegerField, F
from pprint import pprint from pprint import pprint
from django.db.models import Count
from recipe.models import Recipe
from transfer.models import PageTexts from transfer.models import PageTexts
from transfer.serializers.recipe import RecipeSerializer from transfer.serializers.recipe import RecipeSerializer
def transfer_recipe(): def transfer_recipe():
queryset = PageTexts.objects.filter(page__type="Recipe") queryset = PageTexts.objects.filter(
page__type='Recipe',
).values(
'id',
'title',
'summary',
'body',
'locale',
'state',
'slug',
'created_at',
'page__attachment_suffix_url',
'page__account_id',
)
serialized_data = RecipeSerializer(data=list(queryset.values()), many=True) serialized_data = RecipeSerializer(data=list(queryset), many=True)
if serialized_data.is_valid(): if serialized_data.is_valid():
serialized_data.save() serialized_data.save()
else: else:
pprint(f"News serializer errors: {serialized_data.errors}") pprint(f'Recipe serializer errors: {serialized_data.errors}')
return
# Удаление дубликатов рецептов по одинаковым description
duplicate_descriptions = Recipe.objects.values(
'description'
).annotate(
description_count=Count('description')
).filter(
description_count__gt=1
)
for data in duplicate_descriptions:
description = data['description']
_list = list(Recipe.objects.filter(description=description).values_list('pk', flat=True)[1:])
Recipe.objects.filter(id__in=_list).delete()
data_types = { data_types = {
"recipe": [transfer_recipe] 'recipe': [transfer_recipe]
} }

View File

@ -1,5 +1,7 @@
"""Recipe app common views.""" """Recipe app common views."""
from django.utils import translation
from rest_framework import generics, permissions from rest_framework import generics, permissions
from recipe import models from recipe import models
from recipe.serializers import common as serializers from recipe.serializers import common as serializers
@ -10,9 +12,14 @@ class RecipeViewMixin(generics.GenericAPIView):
pagination_class = None pagination_class = None
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
def get_queryset(self): def get_queryset(self, *args, **kwargs):
user = self.request.user user = self.request.user
qs = models.Recipe.objects.published().annotate_in_favorites(user) qs = models.Recipe.objects.published().annotate_in_favorites(user)
locale = kwargs.get('locale')
if locale:
qs = qs.by_locale(locale)
return qs return qs
@ -21,6 +28,11 @@ class RecipeListView(RecipeViewMixin, generics.ListAPIView):
serializer_class = serializers.RecipeListSerializer serializer_class = serializers.RecipeListSerializer
def get_queryset(self, *args, **kwargs):
locale = translation.get_language()
kwargs.update({'locale': locale})
return super().get_queryset(*args, **kwargs)
class RecipeDetailView(RecipeViewMixin, generics.RetrieveAPIView): class RecipeDetailView(RecipeViewMixin, generics.RetrieveAPIView):
"""Resource for detailed recipe information.""" """Resource for detailed recipe information."""

View File

@ -127,8 +127,10 @@ def transfer_product_reviews():
data_types = { data_types = {
"languages": [
transfer_languages,
],
"overlook": [ "overlook": [
# transfer_languages,
transfer_reviews, transfer_reviews,
transfer_text_review, transfer_text_review,
make_en_text_review, make_en_text_review,

View File

@ -5,7 +5,7 @@ from search_indexes.utils import OBJECT_FIELD_PROPERTIES
from product import models from product import models
ProductIndex = Index(settings.ELASTICSEARCH_INDEX_NAMES.get(__name__, 'product')) ProductIndex = Index(settings.ELASTICSEARCH_INDEX_NAMES.get(__name__, 'product'))
ProductIndex.settings(number_of_shards=5, number_of_replicas=2, mapping={'total_fields':{'limit': 3000}}) ProductIndex.settings(number_of_shards=5, number_of_replicas=2, mapping={'total_fields': {'limit': 3000}})
@ProductIndex.doc_type @ProductIndex.doc_type
@ -14,12 +14,13 @@ class ProductDocument(Document):
description = fields.ObjectField(attr='description_indexing', description = fields.ObjectField(attr='description_indexing',
properties=OBJECT_FIELD_PROPERTIES) properties=OBJECT_FIELD_PROPERTIES)
product_type = fields.ObjectField(properties={ product_type = fields.ObjectField(
'id': fields.IntegerField(), properties={
'name': fields.ObjectField(attr='name_indexing', properties=OBJECT_FIELD_PROPERTIES), 'id': fields.IntegerField(),
'index_name': fields.KeywordField(), 'name': fields.ObjectField(attr='name_indexing', properties=OBJECT_FIELD_PROPERTIES),
'use_subtypes': fields.BooleanField(), 'index_name': fields.KeywordField(),
}) },
)
subtypes = fields.ObjectField( subtypes = fields.ObjectField(
properties={ properties={
'id': fields.IntegerField(), 'id': fields.IntegerField(),
@ -54,15 +55,12 @@ class ProductDocument(Document):
), ),
'address': fields.ObjectField( 'address': fields.ObjectField(
properties={ properties={
'city': fields.ObjectField( 'id': fields.IntegerField(),
properties={ 'street_name_1': fields.TextField(
'country': fields.ObjectField( fields={'raw': fields.KeywordField()}
properties={ ),
'code': fields.KeywordField() 'postal_code': fields.KeywordField(),
} 'coordinates': fields.GeoPointField(attr='location_field_indexing'),
)
}
)
} }
) )
} }
@ -155,7 +153,7 @@ class ProductDocument(Document):
name_ru = fields.TextField(attr='display_name', analyzer='russian') name_ru = fields.TextField(attr='display_name', analyzer='russian')
name_fr = fields.TextField(attr='display_name', analyzer='french') name_fr = fields.TextField(attr='display_name', analyzer='french')
favorites_for_users = fields.ListField(field=fields.IntegerField()) favorites_for_users = fields.ListField(field=fields.IntegerField())
created = fields.DateField(attr='created') # publishing date (?) created = fields.DateField(attr='created') # publishing date (?)
class Django: class Django:
model = models.Product model = models.Product

View File

@ -40,6 +40,8 @@ class ProductSubtypeDocumentSerializer(serializers.Serializer):
name_translated = serializers.SerializerMethodField() name_translated = serializers.SerializerMethodField()
def get_name_translated(self, obj): def get_name_translated(self, obj):
if isinstance(obj, dict):
return get_translated_value(obj.get('name'))
return get_translated_value(obj.name) return get_translated_value(obj.name)
@ -93,7 +95,7 @@ class TagDocumentSerializer(serializers.Serializer):
return get_translated_value(obj.label) return get_translated_value(obj.label)
class ProductTypeDocumentSerializer(serializers.Serializer): class ProductTypeSerializer(serializers.Serializer):
"""Product type ES document serializer.""" """Product type ES document serializer."""
id = serializers.IntegerField() id = serializers.IntegerField()
@ -102,8 +104,13 @@ class ProductTypeDocumentSerializer(serializers.Serializer):
@staticmethod @staticmethod
def get_name_translated(obj): def get_name_translated(obj):
if isinstance(obj, dict):
return get_translated_value(obj.get('name'))
return get_translated_value(obj.name) return get_translated_value(obj.name)
def get_attribute(self, instance):
return instance.product_type if instance and instance.product_type else None
class CityDocumentShortSerializer(serializers.Serializer): class CityDocumentShortSerializer(serializers.Serializer):
"""City serializer for ES Document,""" """City serializer for ES Document,"""
@ -114,7 +121,6 @@ class CityDocumentShortSerializer(serializers.Serializer):
class CountryDocumentSerializer(serializers.Serializer): class CountryDocumentSerializer(serializers.Serializer):
id = serializers.IntegerField() id = serializers.IntegerField()
code = serializers.CharField(allow_null=True) code = serializers.CharField(allow_null=True)
svg_image = serializers.CharField() svg_image = serializers.CharField()
@ -122,11 +128,12 @@ class CountryDocumentSerializer(serializers.Serializer):
@staticmethod @staticmethod
def get_name_translated(obj): def get_name_translated(obj):
if isinstance(obj, dict):
return get_translated_value(obj.get('name'))
return get_translated_value(obj.name) return get_translated_value(obj.name)
class AnotherCityDocumentShortSerializer(CityDocumentShortSerializer): class AnotherCityDocumentShortSerializer(CityDocumentShortSerializer):
country = CountryDocumentSerializer() country = CountryDocumentSerializer()
def to_representation(self, instance): def to_representation(self, instance):
@ -136,19 +143,6 @@ class AnotherCityDocumentShortSerializer(CityDocumentShortSerializer):
return None return None
class ProductEstablishmentDocumentSerializer(serializers.Serializer):
"""Related to Product Establishment ES document serializer."""
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
index_name = serializers.CharField()
city = AnotherCityDocumentShortSerializer()
def get_attribute(self, instance):
return instance.establishment if instance and instance.establishment else None
class AddressDocumentSerializer(serializers.Serializer): class AddressDocumentSerializer(serializers.Serializer):
"""Address serializer for ES Document.""" """Address serializer for ES Document."""
@ -171,6 +165,28 @@ class AddressDocumentSerializer(serializers.Serializer):
return None return None
class PSAddressDocumentSerializer(serializers.Serializer):
"""Address serializer for ES Document."""
id = serializers.IntegerField()
street_name_1 = serializers.CharField()
postal_code = serializers.CharField()
class ProductEstablishmentSerializer(serializers.Serializer):
"""Related to Product Establishment ES document serializer."""
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
index_name = serializers.CharField()
city = AnotherCityDocumentShortSerializer()
address = PSAddressDocumentSerializer(allow_null=True)
def get_attribute(self, instance):
return instance.establishment if instance and instance.establishment else None
class ScheduleDocumentSerializer(serializers.Serializer): class ScheduleDocumentSerializer(serializers.Serializer):
"""Schedule serializer for ES Document""" """Schedule serializer for ES Document"""
@ -206,6 +222,7 @@ class NewsDocumentSerializer(InFavoritesMixin, DocumentSerializer):
subtitle_translated = serializers.SerializerMethodField(allow_null=True) subtitle_translated = serializers.SerializerMethodField(allow_null=True)
news_type = NewsTypeSerializer() news_type = NewsTypeSerializer()
tags = TagsDocumentSerializer(many=True, source='visible_tags') tags = TagsDocumentSerializer(many=True, source='visible_tags')
slug = serializers.SerializerMethodField(allow_null=True)
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -221,9 +238,13 @@ class NewsDocumentSerializer(InFavoritesMixin, DocumentSerializer):
'news_type', 'news_type',
'tags', 'tags',
'start', 'start',
'slugs', 'slug',
) )
@staticmethod
def get_slug(obj):
return get_translated_value(obj.slugs)
@staticmethod @staticmethod
def get_title_translated(obj): def get_title_translated(obj):
return get_translated_value(obj.title) return get_translated_value(obj.title)
@ -285,15 +306,15 @@ class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer):
) )
class ProductDocumentSerializer(InFavoritesMixin, DocumentSerializer): class ProductDocumentSerializer(InFavoritesMixin):
"""Product document serializer""" """Product document serializer"""
tags = TagsDocumentSerializer(many=True, source='related_tags') tags = TagsDocumentSerializer(many=True, source='related_tags')
subtypes = ProductSubtypeDocumentSerializer(many=True, allow_null=True) subtypes = ProductSubtypeDocumentSerializer(many=True, allow_null=True)
wine_colors = TagDocumentSerializer(many=True) wine_colors = TagDocumentSerializer(many=True)
grape_variety = TagDocumentSerializer(many=True) grape_variety = TagDocumentSerializer(many=True)
product_type = ProductTypeDocumentSerializer(allow_null=True) product_type = ProductTypeSerializer(allow_null=True)
establishment_detail = ProductEstablishmentDocumentSerializer(source='establishment', allow_null=True) establishment_detail = ProductEstablishmentSerializer(source='establishment', allow_null=True)
wine_origins = WineOriginSerializer(many=True) wine_origins = WineOriginSerializer(many=True)
class Meta: class Meta:
@ -317,7 +338,6 @@ class ProductDocumentSerializer(InFavoritesMixin, DocumentSerializer):
'subtypes', 'subtypes',
'wine_colors', 'wine_colors',
'grape_variety', 'grape_variety',
'establishment_detail',
'average_price', 'average_price',
'created', 'created',
'wine_origins', 'wine_origins',

View File

@ -12,8 +12,8 @@ def update_document(sender, **kwargs):
instance = kwargs['instance'] instance = kwargs['instance']
app_label_model_name_to_filter = { app_label_model_name_to_filter = {
('location','country'): 'address__city__country', ('location', 'country'): 'address__city__country',
('location','city'): 'address__city', ('location', 'city'): 'address__city',
('location', 'address'): 'address', ('location', 'address'): 'address',
# todo: remove after migration # todo: remove after migration
('establishment', 'establishmenttype'): 'establishment_type', ('establishment', 'establishmenttype'): 'establishment_type',
@ -34,8 +34,8 @@ def update_news(sender, **kwargs):
model_name = sender._meta.model_name model_name = sender._meta.model_name
instance = kwargs['instance'] instance = kwargs['instance']
app_label_model_name_to_filter = { app_label_model_name_to_filter = {
('location','country'): 'country', ('location', 'country'): 'country',
('news','newstype'): 'news_type', ('news', 'newstype'): 'news_type',
('tag', 'tag'): 'tags', ('tag', 'tag'): 'tags',
} }
filter_name = app_label_model_name_to_filter.get((app_label, model_name)) filter_name = app_label_model_name_to_filter.get((app_label, model_name))
@ -52,9 +52,9 @@ def update_product(sender, **kwargs):
model_name = sender._meta.model_name model_name = sender._meta.model_name
instance = kwargs['instance'] instance = kwargs['instance']
app_label_model_name_to_filter = { app_label_model_name_to_filter = {
('product','productstandard'): 'standards', ('product', 'productstandard'): 'standards',
('product', 'producttype'): 'product_type', ('product', 'producttype'): 'product_type',
('tag','tag'): 'tags', ('tag', 'tag'): 'tags',
('location', 'wineregion'): 'wine_region', ('location', 'wineregion'): 'wine_region',
('location', 'winesubregion'): 'wine_sub_region', ('location', 'winesubregion'): 'wine_sub_region',
('location', 'winevillage'): 'wine_village', ('location', 'winevillage'): 'wine_village',

View File

@ -3,7 +3,6 @@ from rest_framework import permissions
from django_elasticsearch_dsl_drf import constants from django_elasticsearch_dsl_drf import constants
from django_elasticsearch_dsl_drf.filter_backends import ( from django_elasticsearch_dsl_drf.filter_backends import (
FilteringFilterBackend, FilteringFilterBackend,
GeoSpatialFilteringFilterBackend,
GeoSpatialOrderingFilterBackend, GeoSpatialOrderingFilterBackend,
OrderingFilterBackend, OrderingFilterBackend,
) )
@ -62,11 +61,18 @@ class NewsDocumentViewSet(BaseDocumentViewSet):
) )
filter_fields = { filter_fields = {
'tags_id': {
'field': 'tags.id',
'lookups': [
constants.LOOKUP_QUERY_IN,
constants.LOOKUP_QUERY_EXCLUDE,
],
},
'tag': { 'tag': {
'field': 'tags.id', 'field': 'tags.id',
'lookups': [ 'lookups': [
constants.LOOKUP_QUERY_IN, constants.LOOKUP_QUERY_IN,
constants.LOOKUP_QUERY_EXCLUDE constants.LOOKUP_QUERY_EXCLUDE,
] ]
}, },
'tag_value': { 'tag_value': {
@ -391,7 +397,7 @@ class ProductDocumentViewSet(BaseDocumentViewSet):
'lookups': [constants.LOOKUP_QUERY_IN], 'lookups': [constants.LOOKUP_QUERY_IN],
}, },
'country': { 'country': {
'field': 'establishment.address.city.country.code', 'field': 'establishment.city.country.code',
}, },
'wine_colors_id': { 'wine_colors_id': {
'field': 'wine_colors.id', 'field': 'wine_colors.id',

View File

@ -2,11 +2,14 @@
from rest_framework import serializers from rest_framework import serializers
from rest_framework.fields import SerializerMethodField from rest_framework.fields import SerializerMethodField
from establishment.models import (Establishment, EstablishmentType) from establishment.models import Establishment
from news.models import News, NewsType from establishment.models import EstablishmentType
from news.models import News
from news.models import NewsType
from tag import models from tag import models
from utils.exceptions import (ObjectAlreadyAdded, BindingObjectNotFound, from utils.exceptions import BindingObjectNotFound
RemovedBindingObjectNotFound) from utils.exceptions import ObjectAlreadyAdded
from utils.exceptions import RemovedBindingObjectNotFound
from utils.serializers import TranslatedField from utils.serializers import TranslatedField
@ -95,6 +98,62 @@ class TagCategoryBaseSerializer(serializers.ModelSerializer):
return TagBaseSerializer(instance=tags, many=True, read_only=True).data return TagBaseSerializer(instance=tags, many=True, read_only=True).data
class FiltersTagCategoryBaseSerializer(serializers.ModelSerializer):
"""Serializer for model TagCategory."""
label_translated = TranslatedField()
filters = SerializerMethodField()
param_name = SerializerMethodField()
type = SerializerMethodField()
class Meta:
"""Meta class."""
model = models.TagCategory
fields = (
'id',
'label_translated',
'index_name',
'param_name',
'type',
'filters',
)
def get_type(self, obj):
return obj in ['open_now', ]
def get_param_name(self, obj):
if obj.index_name == 'wine-color':
return 'wine_colors_id__in'
return 'tags_id__in'
def get_fields(self, *args, **kwargs):
fields = super(FiltersTagCategoryBaseSerializer, self).get_fields()
if self.get_type(self):
fields.pop('filters', None)
else:
fields.pop('type', None)
return fields
def get_filters(self, obj):
query_params = dict(self.context['request'].query_params)
params = {}
if 'establishment_type' in query_params:
params = {
'establishments__isnull': False,
}
elif 'product_type' in query_params:
params = {
'products__isnull': False,
}
tags = obj.tags.filter(**params).distinct()
return TagBaseSerializer(instance=tags, many=True, read_only=True).data
class TagCategoryShortSerializer(serializers.ModelSerializer): class TagCategoryShortSerializer(serializers.ModelSerializer):
"""Serializer for model TagCategory.""" """Serializer for model TagCategory."""

View File

@ -7,6 +7,7 @@ app_name = 'tag'
router = SimpleRouter() router = SimpleRouter()
router.register(r'categories', views.TagCategoryViewSet) router.register(r'categories', views.TagCategoryViewSet)
router.register(r'filters', views.FiltersTagCategoryViewSet)
router.register(r'chosen_tags', views.ChosenTagsView) router.register(r'chosen_tags', views.ChosenTagsView)
urlpatterns = [ urlpatterns = [

View File

@ -1,10 +1,14 @@
"""Tag views.""" """Tag views."""
from django.conf import settings from django.conf import settings
from rest_framework import permissions from rest_framework import generics, mixins, permissions, status, viewsets
from rest_framework import viewsets, mixins, status, generics
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ValidationError
from django.utils.translation import gettext_lazy as _
from search_indexes import views as search_views
from location.models import WineRegion
from product.models import ProductType
from tag import filters, models, serializers from tag import filters, models, serializers
@ -36,7 +40,8 @@ class ChosenTagsView(generics.ListAPIView, viewsets.GenericViewSet):
serializer = self.get_serializer(queryset, many=True) serializer = self.get_serializer(queryset, many=True)
result_list = serializer.data result_list = serializer.data
if request.query_params.get('type') and (settings.ESTABLISHMENT_CHOSEN_TAGS or settings.NEWS_CHOSEN_TAGS): if request.query_params.get('type') and (settings.ESTABLISHMENT_CHOSEN_TAGS or settings.NEWS_CHOSEN_TAGS):
ordered_list = settings.ESTABLISHMENT_CHOSEN_TAGS if request.query_params.get('type') == 'establishment' else settings.NEWS_CHOSEN_TAGS ordered_list = settings.ESTABLISHMENT_CHOSEN_TAGS if request.query_params.get(
'type') == 'establishment' else settings.NEWS_CHOSEN_TAGS
result_list = sorted(result_list, key=lambda x: ordered_list.index(x['index_name'])) result_list = sorted(result_list, key=lambda x: ordered_list.index(x['index_name']))
return Response(result_list) return Response(result_list)
@ -53,6 +58,233 @@ class TagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
serializer_class = serializers.TagCategoryBaseSerializer serializer_class = serializers.TagCategoryBaseSerializer
# User`s views & viewsets
class FiltersTagCategoryViewSet(TagCategoryViewSet):
"""ViewSet for TagCategory model."""
serializer_class = serializers.FiltersTagCategoryBaseSerializer
index_name_to_order = {
'open_now': 9,
'works_noon': 8,
'works_evening': 7,
'pop': 6,
'category': 5,
'toque_number': 4,
'cuisine': 3,
'moment': 2,
'service': 1,
}
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset().exclude(public=False))
serializer = self.get_serializer(queryset, many=True)
result_list = serializer.data
query_params = request.query_params
params_type = query_params.get('type')
if query_params.get('establishment_type'):
params_type = query_params.get('establishment_type')
elif query_params.get('product_type'):
params_type = query_params.get('product_type')
week_days = tuple(map(_, ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")))
flags = ('toque_number', 'wine_region', 'works_noon', 'works_evening', 'works_now', 'works_at_weekday')
filter_flags = {flag_name: False for flag_name in flags}
additional_flags = []
if params_type == 'restaurant':
additional_flags += ['toque_number', 'works_noon', 'works_evening', 'works_now']
elif params_type in ['winery', 'wine']:
additional_flags += ['wine_region']
elif params_type == 'artisan':
additional_flags += ['works_now', 'works_at_weekday']
for flag_name in additional_flags:
filter_flags[flag_name] = True
if request.query_params.get('product_type') == ProductType.WINE:
wine_region_id = query_params.get('wine_region_id__in')
if str(wine_region_id).isdigit():
queryset = WineRegion.objects.filter(id=int(wine_region_id))
else:
queryset = WineRegion.objects.all()
wine_regions = {
"index_name": "wine_region",
"label_translated": "Wine region",
"param_name": "wine_region_id__in",
"filters": [{
"id": obj.id,
"index_name": obj.name.lower().replace(' ', '_'),
"label_translated": obj.name
} for obj in queryset]
}
result_list.append(wine_regions)
for item in result_list:
if 'filters' in item:
item['filters'].sort(key=lambda x: x.get('label_translated'))
if filter_flags['toque_number']:
toques = {
"index_name": "toque_number",
"label_translated": "Toques",
"param_name": "toque_number__in",
'type': 'toque',
"filters": [{
"id": toque_id,
"index_name": "toque_%d" % toque_id,
"label_translated": "Toque %d" % toque_id
} for toque_id in range(6)]
}
result_list.append(toques)
if filter_flags['works_noon']:
works_noon = {
"index_name": "works_noon",
"label_translated": "Open noon",
"param_name": "works_noon__in",
'type': 'weekday',
"filters": [{
"id": weekday,
"index_name": week_days[weekday].lower(),
"label_translated": week_days[weekday]
} for weekday in range(7)]
}
result_list.append(works_noon)
if filter_flags['works_evening']:
works_evening = {
"index_name": "works_evening",
"label_translated": "Open evening",
"param_name": "works_evening__in",
'type': 'weekday',
"filters": [{
"id": weekday,
"index_name": week_days[weekday].lower(),
"label_translated": week_days[weekday]
} for weekday in range(7)]
}
result_list.append(works_evening)
if filter_flags['works_now']:
works_now = {
"index_name": "open_now",
"label_translated": "Open now",
"param_name": "open_now",
"type": 'bool',
}
result_list.append(works_now)
if filter_flags['works_at_weekday']:
works_at_weekday = {
"index_name": "works_at_weekday",
"label_translated": "Works at weekday",
"param_name": "works_at_weekday__in",
'type': 'weekday',
"filters": [{
"id": weekday,
"index_name": week_days[weekday].lower(),
"label_translated": week_days[weekday]
} for weekday in range(7)]
}
result_list.append(works_at_weekday)
search_view_class = self.define_search_view_by_request(request)
facets = search_view_class.as_view({'get': 'list'})(self.mutate_request(self.request)).data['facets']
result_list = self.remove_empty_filters(result_list, facets)
tag_category = list(filter(lambda x: x.get('index_name') == 'tag', result_list))
result_list = [category for category in result_list if category.get('index_name') != 'tag']
if len(tag_category):
tag_category = list(filter(lambda x: x.get('index_name') == 'pop', tag_category[0]['filters']))
if len(tag_category): # we have Pop tag in our results
tag_category = tag_category[0]
tag_category['param_name'] = 'tags_id__in'
result_list.append(tag_category)
result_list.sort(key=lambda x: self.index_name_to_order.get(x.get('index_name'), 0), reverse=True)
return Response(result_list)
@staticmethod
def mutate_request(request):
"""Remove all filtering get params and remove s_ from the rest of them"""
request.GET._mutable = True
for name in request.query_params.copy().keys():
value = request.query_params.pop(name)
if name.startswith('s_'):
request.query_params[name[2:]] = value[0]
request.GET._mutable = False
return request._request
@staticmethod
def define_search_view_by_request(request):
request.GET._mutable = True
if request.query_params.get('items'):
items = request.query_params.pop('items')[0]
else:
raise ValidationError({'detail': _('Missing required "items" parameter')})
item_to_class = {
'news': search_views.NewsDocumentViewSet,
'establishments': search_views.EstablishmentDocumentViewSet,
'products': search_views.ProductDocumentViewSet,
}
klass = item_to_class.get(items)
if klass is None:
raise ValidationError({'detail': _('news/establishments/products')})
request.GET._mutable = False
return klass
@staticmethod
def remove_empty_filters(filters, facets):
# parse facets
if facets.get('_filter_tag'):
tags_to_preserve = list(map(lambda el: el['key'], facets['_filter_tag']['tag']['buckets']))
if facets.get('_filter_wine_colors'):
wine_colors_to_preserve = list(
map(lambda el: el['key'], facets['_filter_wine_colors']['wine_colors']['buckets']))
if facets.get('_filter_wine_region_id'):
wine_regions_to_preserve = list(
map(lambda el: el['key'], facets['_filter_wine_region_id']['wine_region_id']['buckets']))
if facets.get('_filter_toque_number'):
toque_numbers = list(map(lambda el: el['key'], facets['_filter_toque_number']['toque_number']['buckets']))
if facets.get('_filter_works_noon'):
works_noon = list(map(lambda el: el['key'], facets['_filter_works_noon']['works_noon']['buckets']))
if facets.get('_filter_works_evening'):
works_evening = list(map(lambda el: el['key'], facets['_filter_works_evening']['works_evening']['buckets']))
if facets.get('_filter_works_at_weekday'):
works_at_weekday = list(
map(lambda el: el['key'], facets['_filter_works_at_weekday']['works_at_weekday']['buckets']))
if facets.get('_filter_works_now'):
works_now = list(map(lambda el: el['key'], facets['_filter_works_now']['works_now']['buckets']))
# remove empty filters
for category in filters:
param_name = category.get('param_name')
if param_name == 'tags_id__in':
category['filters'] = list(filter(lambda tag: tag['id'] in tags_to_preserve, category['filters']))
elif param_name == 'wine_colors_id__in':
category['filters'] = list(
filter(lambda tag: tag['id'] in wine_colors_to_preserve, category['filters']))
elif param_name == 'wine_region_id__in':
category['filters'] = list(
filter(lambda tag: tag['id'] in wine_regions_to_preserve, category['filters']))
elif param_name == 'toque_number__in':
category['filters'] = list(filter(lambda tag: tag['id'] in toque_numbers, category['filters']))
elif param_name == 'works_noon__in':
category['filters'] = list(filter(lambda tag: tag['id'] in works_noon, category['filters']))
elif param_name == 'works_evening__in':
category['filters'] = list(filter(lambda tag: tag['id'] in works_evening, category['filters']))
elif param_name == 'works_at_weekday__in':
category['filters'] = list(filter(lambda tag: tag['id'] in works_at_weekday, category['filters']))
return filters
# BackOffice user`s views & viewsets # BackOffice user`s views & viewsets
class BindObjectMixin: class BindObjectMixin:
"""Bind object mixin.""" """Bind object mixin."""

View File

@ -13,7 +13,7 @@ class Command(BaseCommand):
'news', # перенос новостей (после №2) 'news', # перенос новостей (после №2)
'account', # №1 - перенос пользователей 'account', # №1 - перенос пользователей
'subscriber', 'subscriber',
'recipe', 'recipe', # №2 - рецепты
'partner', 'partner',
'establishment', # №3 - перенос заведений 'establishment', # №3 - перенос заведений
'gallery', 'gallery',
@ -49,6 +49,7 @@ class Command(BaseCommand):
'guide_elements_bulk', 'guide_elements_bulk',
'guide_element_advertorials', 'guide_element_advertorials',
'guide_complete', 'guide_complete',
'languages', # №4 - перенос языков
] ]
def handle(self, *args, **options): def handle(self, *args, **options):

View File

@ -1222,3 +1222,35 @@ class Footers(MigrateMixin):
class Meta: class Meta:
managed = False managed = False
db_table = 'footers' db_table = 'footers'
class OwnershipAffs(MigrateMixin):
using = 'legacy'
role = models.CharField(max_length=255, blank=True, null=True)
state = models.CharField(max_length=255, blank=True, null=True)
account_id = models.IntegerField(blank=True, null=True)
establishment_id = models.IntegerField(blank=True, null=True)
created_at = models.DateTimeField()
updated_at = models.DateTimeField()
requester_id = models.IntegerField(blank=True, null=True)
class Meta:
managed = False
db_table = 'ownership_affs'
class Panels(MigrateMixin):
using = 'legacy'
name = models.CharField(max_length=255, blank=True, null=True)
display = models.CharField(max_length=255, blank=True, null=True)
description = models.CharField(max_length=255, blank=True, null=True)
query = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(blank=True, null=True)
updated_at = models.DateTimeField(blank=True, null=True)
account_id = models.IntegerField(blank=True, null=True)
site_id = models.IntegerField(blank=True, null=True)
class Meta:
managed = False
db_table = 'panels'

View File

@ -1,55 +1,87 @@
from rest_framework import serializers from rest_framework import serializers
from account.models import User
from recipe.models import Recipe from recipe.models import Recipe
from utils.legacy_parser import parse_legacy_news_content from utils.legacy_parser import parse_legacy_news_content
class RecipeSerializer(serializers.ModelSerializer): class RecipeSerializer(serializers.Serializer):
locale = serializers.CharField() id = serializers.IntegerField()
title = serializers.CharField(allow_null=True)
summary = serializers.CharField(allow_null=True, allow_blank=True)
body = serializers.CharField(allow_null=True) body = serializers.CharField(allow_null=True)
title = serializers.CharField() locale = serializers.CharField(allow_null=True)
state = serializers.CharField() state = serializers.CharField(allow_null=True)
created_at = serializers.DateTimeField(source="published_at", format='%m-%d-%Y %H:%M:%S') slug = serializers.CharField(allow_null=True)
created_at = serializers.DateTimeField(format='%m-%d-%Y %H:%M:%S')
class Meta: page__attachment_suffix_url = serializers.CharField(allow_null=True)
model = Recipe page__account_id = serializers.IntegerField(allow_null=True)
fields = (
"body",
"title",
"state",
"created_at",
'locale',
)
def validate(self, data): def validate(self, data):
data["state"] = self.get_state(data) data.update({
data["title"] = self.get_title(data) 'old_id': data.pop('id'),
data["description"] = self.get_description(data) 'title': self.get_title(data),
data.pop("body") 'subtitle': self.get_subtitle(data),
data.pop("locale") 'description': self.get_description(data),
'state': self.get_state(data),
'created': data.pop('created_at'),
'image': self.get_image(data),
'created_by': self.get_account(data),
'modified_by': self.get_account(data),
})
data.pop('page__account_id')
data.pop('page__attachment_suffix_url')
data.pop('summary')
data.pop('body')
data.pop('locale')
return data return data
def create(self, validated_data): def create(self, validated_data):
return Recipe.objects.create(**validated_data) obj, _ = Recipe.objects.update_or_create(
old_id=validated_data['old_id'],
defaults=validated_data,
)
return obj
def get_state(self, obj): @staticmethod
if obj["state"] == "published": def get_title(data):
return Recipe.PUBLISHED if data.get('title') and data.get('locale'):
elif obj["state"] == "hidden": return {data['locale']: data['title']}
return Recipe.HIDDEN return None
elif obj["state"] == "published_exclusive":
return Recipe.PUBLISHED_EXCLUSIVE
else:
return Recipe.WAITING
def get_title(self, obj): @staticmethod
# tit = obj.get("title") def get_subtitle(data):
# return {"en-GB": tit} if data.get('summary') and data.get('locale'):
return {obj['locale']: obj['title']} return {data['locale']: data['summary']}
return None
def get_description(self, obj): @staticmethod
# desc = obj.get("body") def get_description(data):
# return {"en-GB": desc} if data.get('body') and data.get('locale'):
content = None content = parse_legacy_news_content(data['body'])
if obj['body']: return {data['locale']: content}
content = parse_legacy_news_content(obj['body']) return None
return {obj['locale']: content}
@staticmethod
def get_state(data):
value = data.get('state')
states = {
'published': Recipe.PUBLISHED,
'hidden': Recipe.HIDDEN,
'published_exclusive': Recipe.PUBLISHED_EXCLUSIVE
}
return states.get(value, Recipe.WAITING)
@staticmethod
def get_image(data):
values = (None, 'default/missing.png')
if data.get('page__attachment_suffix_url') not in values:
return data['page__attachment_suffix_url']
return None
@staticmethod
def get_account(data):
if data.get('page__account_id'):
return User.objects.filter(old_id=data['page__account_id']).first()
return None

View File

@ -3,6 +3,7 @@ import logging
import random import random
import re import re
import string import string
from collections import namedtuple
import requests import requests
from django.conf import settings from django.conf import settings
@ -124,3 +125,10 @@ def absolute_url_decorator(func):
def get_point_from_coordinates(latitude: str, longitude: str): def get_point_from_coordinates(latitude: str, longitude: str):
if latitude and longitude: if latitude and longitude:
return Point(x=longitude, y=latitude, srid=4326) return Point(x=longitude, y=latitude, srid=4326)
def namedtuplefetchall(cursor):
"""Return all rows from a cursor as a namedtuple."""
desc = cursor.description
nt_result = namedtuple('Result', [col[0] for col in desc])
return [nt_result(*row) for row in cursor.fetchall()]

View File

@ -6,6 +6,7 @@ from django.conf import settings
from rest_framework.pagination import CursorPagination, PageNumberPagination from rest_framework.pagination import CursorPagination, PageNumberPagination
from django_elasticsearch_dsl_drf.pagination import PageNumberPagination as ESPagination from django_elasticsearch_dsl_drf.pagination import PageNumberPagination as ESPagination
class ProjectPageNumberPagination(PageNumberPagination): class ProjectPageNumberPagination(PageNumberPagination):
"""Customized pagination class.""" """Customized pagination class."""
@ -82,7 +83,7 @@ class ESDocumentPagination(ESPagination):
return page.facets._d_ return page.facets._d_
class EstablishmentPortionPagination(ProjectMobilePagination): class PortionPagination(ProjectMobilePagination):
""" """
Pagination for app establishments with limit page size equal to 12 Pagination for app establishments with limit page size equal to 12
""" """

12
db_migration_resolve.txt Normal file
View File

@ -0,0 +1,12 @@
В случае возникновения проблемы с применением миграции account 0027:
1 Удаляем unique together constrain для app - account
ALTER TABLE account_userrole
DROP CONSTRAINT account_userrole_user_id_role_id_26fa14c4_uniq;
2 Правим миграцию 0023
migrations.AlterUniqueTogether(
name='userrole',
unique_together=set(),
),
3 Применяем account 0027
4 Возвращаем миграцию account 0023, в исходное состояние

View File

@ -16,8 +16,6 @@ services:
- .:/code - .:/code
# PostgreSQL database # PostgreSQL database
db: db:
build: build:

View File

@ -516,9 +516,6 @@ PHONENUMBER_DEFAULT_REGION = "FR"
FALLBACK_LOCALE = 'en-GB' FALLBACK_LOCALE = 'en-GB'
# TMP TODO remove it later
# Временный хардкод для демонстрации > 15 ноября, потом удалить!
CAROUSEL_ITEMS = [465]
ESTABLISHMENT_CHOSEN_TAGS = ['gastronomic', 'en_vogue', 'terrace', 'streetfood', 'business', 'bar_cocktail', 'brunch', 'pop'] ESTABLISHMENT_CHOSEN_TAGS = ['gastronomic', 'en_vogue', 'terrace', 'streetfood', 'business', 'bar_cocktail', 'brunch', 'pop']
NEWS_CHOSEN_TAGS = ['eat', 'drink', 'cook', 'style', 'international', 'event', 'partnership'] NEWS_CHOSEN_TAGS = ['eat', 'drink', 'cook', 'style', 'international', 'event', 'partnership']
INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next'] INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next']