Merge branch 'develop' into feature/permission_liquor
This commit is contained in:
commit
458370d4c9
49
apps/account/management/commands/add_ownership.py
Normal file
49
apps/account/management/commands/add_ownership.py
Normal 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)}')
|
||||
)
|
||||
25
apps/account/migrations/0026_auto_20191211_1134.py
Normal file
25
apps/account/migrations/0026_auto_20191211_1134.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
18
apps/account/migrations/0027_auto_20191211_1444.py
Normal file
18
apps/account/migrations/0027_auto_20191211_1444.py
Normal 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')},
|
||||
),
|
||||
]
|
||||
|
|
@ -34,18 +34,18 @@ class Role(ProjectBaseMixin):
|
|||
REVIEWER_MANGER = 6
|
||||
RESTAURANT_REVIEWER = 7
|
||||
SALES_MAN = 8
|
||||
WINERY_REVIEWER = 9 # Establishments subtype "winery"
|
||||
WINERY_REVIEWER = 9 # Establishments subtype "winery"
|
||||
SELLER = 10
|
||||
LIQUOR_REVIEWER = 11
|
||||
PRODUCT_REVIEWER = 12
|
||||
|
||||
ROLE_CHOICES = (
|
||||
(STANDARD_USER, 'Standard user'),
|
||||
(COMMENTS_MODERATOR, 'Comments moderator'),
|
||||
(COUNTRY_ADMIN, 'Country admin'),
|
||||
(CONTENT_PAGE_MANAGER, 'Content page manager'),
|
||||
(ESTABLISHMENT_MANAGER, 'Establishment manager'),
|
||||
(REVIEWER_MANGER, 'Reviewer manager'),
|
||||
(STANDARD_USER, _('Standard user')),
|
||||
(COMMENTS_MODERATOR, _('Comments moderator')),
|
||||
(COUNTRY_ADMIN, _('Country admin')),
|
||||
(CONTENT_PAGE_MANAGER, _('Content page manager')),
|
||||
(ESTABLISHMENT_MANAGER, _('Establishment manager')),
|
||||
(REVIEWER_MANGER, _('Reviewer manager')),
|
||||
(RESTAURANT_REVIEWER, 'Restaurant reviewer'),
|
||||
(SALES_MAN, 'Sales man'),
|
||||
(WINERY_REVIEWER, 'Winery reviewer'),
|
||||
|
|
@ -97,6 +97,18 @@ class UserQuerySet(models.QuerySet):
|
|||
return self.filter(oauth2_provider_refreshtoken__token=token,
|
||||
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):
|
||||
"""Base user model."""
|
||||
|
|
@ -120,7 +132,9 @@ class User(AbstractUser):
|
|||
USERNAME_FIELD = 'username'
|
||||
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)()
|
||||
|
||||
class Meta:
|
||||
|
|
@ -305,14 +319,33 @@ class User(AbstractUser):
|
|||
|
||||
class UserRole(ProjectBaseMixin):
|
||||
"""UserRole model."""
|
||||
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)
|
||||
VALIDATED = 'validated'
|
||||
PENDING = 'pending'
|
||||
CANCELLED = 'cancelled'
|
||||
REJECTED = 'rejected'
|
||||
|
||||
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:
|
||||
unique_together = ['user', 'role']
|
||||
unique_together = ['user', 'role', 'establishment', 'state']
|
||||
|
||||
|
||||
class OldRole(models.Model):
|
||||
|
|
|
|||
|
|
@ -10,4 +10,5 @@ urlpatterns = [
|
|||
path('user-role/', views.UserRoleLstView.as_view(), name='user-role-list-create'),
|
||||
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>/csv', views.get_user_csv, name='user-csv'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import generics, permissions
|
||||
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.models import User
|
||||
|
|
@ -46,3 +49,69 @@ class UserRUDView(generics.RetrieveUpdateDestroyAPIView):
|
|||
serializer_class = serializers.BackDetailUserSerializer
|
||||
permission_classes = (permissions.IsAdminUser,)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -14,3 +14,8 @@ class PageInline(admin.TabularInline):
|
|||
class AdvertisementModelAdmin(admin.ModelAdmin):
|
||||
"""Admin model for model Advertisement"""
|
||||
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'
|
||||
|
|
|
|||
|
|
@ -71,11 +71,11 @@ class Advertisement(ProjectBaseMixin):
|
|||
return super().delete(using, keep_parents)
|
||||
|
||||
@property
|
||||
def mobile_page(self):
|
||||
def mobile_pages(self):
|
||||
"""Return mobile page"""
|
||||
return self.pages.by_platform(Page.MOBILE).first()
|
||||
return self.pages.by_platform(Page.MOBILE)
|
||||
|
||||
@property
|
||||
def web_page(self):
|
||||
def web_pages(self):
|
||||
"""Return web page"""
|
||||
return self.pages.by_platform(Page.WEB).first()
|
||||
return self.pages.by_platform(Page.WEB)
|
||||
|
|
|
|||
|
|
@ -1,26 +1,14 @@
|
|||
"""Serializers for back office app advertisements"""
|
||||
from main.serializers import PageBaseSerializer
|
||||
from advertisement.serializers import AdvertisementBaseSerializer
|
||||
from main.serializers import PageExtendedSerializer
|
||||
|
||||
|
||||
class AdvertisementPageBaseSerializer(PageBaseSerializer):
|
||||
"""Base serializer for linking page w/ advertisement."""
|
||||
class AdvertisementDetailSerializer(AdvertisementBaseSerializer):
|
||||
"""Advertisement serializer for back office."""
|
||||
pages = PageExtendedSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta(PageBaseSerializer.Meta):
|
||||
class Meta(AdvertisementBaseSerializer.Meta):
|
||||
"""Meta class."""
|
||||
|
||||
PageBaseSerializer.Meta.extra_kwargs.update({
|
||||
'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)
|
||||
fields = AdvertisementBaseSerializer.Meta.fields + [
|
||||
'pages',
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
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.serializers import PageTypeBaseSerializer
|
||||
from translation.models import Language
|
||||
|
||||
|
||||
class AdvertisementBaseSerializer(serializers.ModelSerializer):
|
||||
"""Base serializer for model Advertisement."""
|
||||
|
||||
page_type_detail = PageTypeBaseSerializer(read_only=True,
|
||||
source='page_type')
|
||||
target_languages = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Language.objects.all(),
|
||||
many=True,
|
||||
|
|
@ -34,16 +34,17 @@ class AdvertisementBaseSerializer(serializers.ModelSerializer):
|
|||
'target_sites',
|
||||
'start',
|
||||
'end',
|
||||
'page_type',
|
||||
'page_type_detail',
|
||||
]
|
||||
extra_kwargs = {
|
||||
'page_type': {'required': True, 'write_only': True}
|
||||
}
|
||||
|
||||
|
||||
class AdvertisementPageTypeCommonListSerializer(AdvertisementBaseSerializer):
|
||||
"""Serializer for AdvertisementPageTypeCommonView."""
|
||||
|
||||
page = PageBaseSerializer(source='common_page', read_only=True)
|
||||
|
||||
class AdvertisementSerializer(AdvertisementBaseSerializer):
|
||||
"""Serializer for model Advertisement."""
|
||||
class Meta(AdvertisementBaseSerializer.Meta):
|
||||
"""Meta class."""
|
||||
fields = AdvertisementBaseSerializer.Meta.fields + [
|
||||
'page',
|
||||
]
|
||||
fields = AdvertisementBaseSerializer.Meta.fields.copy()
|
||||
fields.pop(fields.index('page_type_detail'))
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
"""Serializers for mobile app advertisements"""
|
||||
from advertisement.serializers import AdvertisementBaseSerializer
|
||||
from advertisement.serializers import AdvertisementSerializer
|
||||
from main.serializers import PageBaseSerializer
|
||||
|
||||
|
||||
class AdvertisementPageTypeMobileListSerializer(AdvertisementBaseSerializer):
|
||||
class AdvertisementPageTypeMobileListSerializer(AdvertisementSerializer):
|
||||
"""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."""
|
||||
fields = AdvertisementBaseSerializer.Meta.fields + [
|
||||
'page',
|
||||
fields = AdvertisementSerializer.Meta.fields + [
|
||||
'pages',
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
"""Serializers for web app advertisements"""
|
||||
from advertisement.serializers import AdvertisementBaseSerializer
|
||||
from advertisement.serializers import AdvertisementSerializer
|
||||
from main.serializers import PageBaseSerializer
|
||||
|
||||
|
||||
class AdvertisementPageTypeWebListSerializer(AdvertisementBaseSerializer):
|
||||
class AdvertisementPageTypeWebListSerializer(AdvertisementSerializer):
|
||||
"""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."""
|
||||
fields = AdvertisementBaseSerializer.Meta.fields + [
|
||||
'page',
|
||||
fields = AdvertisementSerializer.Meta.fields + [
|
||||
'pages',
|
||||
]
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ app_name = 'advertisements'
|
|||
urlpatterns = [
|
||||
path('', views.AdvertisementListCreateView.as_view(), name='list-create'),
|
||||
path('<int:pk>/', views.AdvertisementRUDView.as_view(), name='rud'),
|
||||
path('<int:pk>/pages/', views.AdvertisementPageListCreateView.as_view(),
|
||||
name='page-list-create'),
|
||||
path('<int:ad_pk>/pages/<int:page_pk>/', views.AdvertisementPageRUDView.as_view(),
|
||||
name='page-rud')
|
||||
path('<int:pk>/pages/', views.AdvertisementPageCreateView.as_view(),
|
||||
name='ad-page-create'),
|
||||
path('<int:ad_pk>/pages/<int:page_pk>/', views.AdvertisementPageUDView.as_view(),
|
||||
name='ad-page-update-destroy')
|
||||
]
|
||||
|
||||
urlpatterns += common_urlpatterns
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
"""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 django.shortcuts import get_object_or_404
|
||||
|
||||
from main.serializers import PageExtendedSerializer
|
||||
from advertisement.models import Advertisement
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from advertisement.serializers import (AdvertisementBaseSerializer,
|
||||
AdvertisementPageBaseSerializer,
|
||||
AdvertisementPageListCreateSerializer)
|
||||
AdvertisementDetailSerializer)
|
||||
|
||||
|
||||
class AdvertisementBackOfficeViewMixin(generics.GenericAPIView):
|
||||
"""Base back office advertisement view."""
|
||||
|
||||
pagination_class = None
|
||||
permission_classes = (permissions.IsAuthenticated, )
|
||||
|
||||
def get_queryset(self):
|
||||
|
|
@ -31,14 +31,14 @@ class AdvertisementRUDView(AdvertisementBackOfficeViewMixin,
|
|||
generics.RetrieveUpdateDestroyAPIView):
|
||||
"""Retrieve|Update|Destroy advertisement page view."""
|
||||
|
||||
serializer_class = AdvertisementBaseSerializer
|
||||
serializer_class = AdvertisementDetailSerializer
|
||||
|
||||
|
||||
class AdvertisementPageListCreateView(AdvertisementBackOfficeViewMixin,
|
||||
generics.ListCreateAPIView):
|
||||
"""Retrieve|Update|Destroy advertisement page view."""
|
||||
class AdvertisementPageCreateView(AdvertisementBackOfficeViewMixin,
|
||||
generics.CreateAPIView):
|
||||
"""Create advertisement page view."""
|
||||
|
||||
serializer_class = AdvertisementPageListCreateSerializer
|
||||
serializer_class = PageExtendedSerializer
|
||||
|
||||
def get_object(self):
|
||||
"""Returns the object the view is displaying."""
|
||||
|
|
@ -56,12 +56,19 @@ class AdvertisementPageListCreateView(AdvertisementBackOfficeViewMixin,
|
|||
"""Overridden get_queryset method."""
|
||||
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):
|
||||
"""Returns the object the view is displaying."""
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ from rest_framework import generics
|
|||
from rest_framework import permissions
|
||||
|
||||
from advertisement.models import Advertisement
|
||||
from advertisement.serializers import AdvertisementBaseSerializer, \
|
||||
AdvertisementPageTypeCommonListSerializer
|
||||
from advertisement.serializers import AdvertisementBaseSerializer
|
||||
|
||||
|
||||
class AdvertisementBaseView(generics.GenericAPIView):
|
||||
|
|
@ -16,8 +15,7 @@ class AdvertisementBaseView(generics.GenericAPIView):
|
|||
|
||||
def get_queryset(self):
|
||||
"""Overridden get queryset method."""
|
||||
return Advertisement.objects.with_base_related() \
|
||||
.by_locale(self.request.locale)
|
||||
return Advertisement.objects.with_base_related()
|
||||
|
||||
|
||||
class AdvertisementPageTypeListView(AdvertisementBaseView, generics.ListAPIView):
|
||||
|
|
|
|||
27
apps/collection/migrations/0024_auto_20191215_2156.py
Normal file
27
apps/collection/migrations/0024_auto_20191215_2156.py
Normal 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',
|
||||
)
|
||||
]
|
||||
20
apps/collection/migrations/0025_collection_description.py
Normal file
20
apps/collection/migrations/0025_collection_description.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -6,9 +6,10 @@ from django.core.validators import MaxValueValidator, MinValueValidator
|
|||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from utils.models import ProjectBaseMixin, URLImageMixin
|
||||
from utils.models import TJSONField
|
||||
from utils.models import TranslatedFieldsMixin
|
||||
from utils.models import (
|
||||
ProjectBaseMixin, TJSONField, TranslatedFieldsMixin,
|
||||
URLImageMixin,
|
||||
)
|
||||
from utils.querysets import RelatedObjectsCountMixin
|
||||
|
||||
|
||||
|
|
@ -24,7 +25,8 @@ class CollectionNameMixin(models.Model):
|
|||
|
||||
class CollectionDateMixin(models.Model):
|
||||
"""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,
|
||||
verbose_name=_('end'))
|
||||
|
||||
|
|
@ -80,6 +82,8 @@ class Collection(ProjectBaseMixin, CollectionDateMixin,
|
|||
verbose_name=_('Collection slug'), editable=True, null=True)
|
||||
old_id = models.IntegerField(null=True, blank=True)
|
||||
|
||||
rank = models.IntegerField(null=True, default=None)
|
||||
|
||||
objects = CollectionQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
|
|
@ -108,20 +112,32 @@ class Collection(ProjectBaseMixin, CollectionDateMixin,
|
|||
@property
|
||||
def related_object_names(self) -> list:
|
||||
"""Return related object names."""
|
||||
raw_object_names = []
|
||||
raw_objects = []
|
||||
for related_object in [related_object.name for related_object in self._related_objects]:
|
||||
instances = getattr(self, f'{related_object}')
|
||||
if instances.exists():
|
||||
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
|
||||
object_names = []
|
||||
related_objects = []
|
||||
object_names = set()
|
||||
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)
|
||||
if result: object_names.append(' '.join(result).capitalize())
|
||||
return set(object_names)
|
||||
if result:
|
||||
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):
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ from location.models import Country
|
|||
from location.serializers import CountrySimpleSerializer
|
||||
from product.models import Product
|
||||
from utils.exceptions import (
|
||||
BindingObjectNotFound, RemovedBindingObjectNotFound, ObjectAlreadyAdded
|
||||
BindingObjectNotFound, ObjectAlreadyAdded,
|
||||
RemovedBindingObjectNotFound,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -33,13 +34,14 @@ class CollectionBackOfficeSerializer(CollectionBaseSerializer):
|
|||
'on_top',
|
||||
'country',
|
||||
'country_id',
|
||||
'block_size',
|
||||
# 'block_size',
|
||||
'description',
|
||||
'slug',
|
||||
'start',
|
||||
'end',
|
||||
# 'start',
|
||||
# 'end',
|
||||
'count_related_objects',
|
||||
'related_object_names',
|
||||
'rank',
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -68,15 +70,15 @@ class CollectionBindObjectSerializer(serializers.Serializer):
|
|||
attrs['collection'] = collection
|
||||
|
||||
if obj_type == self.ESTABLISHMENT:
|
||||
establishment = Establishment.objects.filter(pk=obj_id).\
|
||||
establishment = Establishment.objects.filter(pk=obj_id). \
|
||||
first()
|
||||
if not establishment:
|
||||
raise BindingObjectNotFound()
|
||||
if request.method == 'POST' and collection.establishments.\
|
||||
if request.method == 'POST' and collection.establishments. \
|
||||
filter(pk=establishment.pk).exists():
|
||||
raise ObjectAlreadyAdded()
|
||||
if request.method == 'DELETE' and not collection.\
|
||||
establishments.filter(pk=establishment.pk).\
|
||||
if request.method == 'DELETE' and not collection. \
|
||||
establishments.filter(pk=establishment.pk). \
|
||||
exists():
|
||||
raise RemovedBindingObjectNotFound()
|
||||
attrs['related_object'] = establishment
|
||||
|
|
@ -84,10 +86,10 @@ class CollectionBindObjectSerializer(serializers.Serializer):
|
|||
product = Product.objects.filter(pk=obj_id).first()
|
||||
if not product:
|
||||
raise BindingObjectNotFound()
|
||||
if request.method == 'POST' and collection.products.\
|
||||
if request.method == 'POST' and collection.products. \
|
||||
filter(pk=product.pk).exists():
|
||||
raise ObjectAlreadyAdded()
|
||||
if request.method == 'DELETE' and not collection.products.\
|
||||
if request.method == 'DELETE' and not collection.products. \
|
||||
filter(pk=product.pk).exists():
|
||||
raise RemovedBindingObjectNotFound()
|
||||
attrs['related_object'] = product
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from rest_framework import permissions
|
||||
from rest_framework import viewsets, mixins
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import mixins, permissions, viewsets
|
||||
from rest_framework.filters import OrderingFilter
|
||||
|
||||
from collection import models
|
||||
from collection.serializers import back as serializers
|
||||
|
|
@ -9,11 +10,17 @@ from utils.views import BindObjectMixin
|
|||
class CollectionViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
|
||||
"""ViewSet for Collection model."""
|
||||
|
||||
pagination_class = None
|
||||
# pagination_class = None
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
queryset = models.Collection.objects.all()
|
||||
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,
|
||||
mixins.UpdateModelMixin,
|
||||
|
|
@ -25,9 +32,13 @@ class CollectionBackOfficeViewSet(mixins.CreateModelMixin,
|
|||
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
queryset = models.Collection.objects.all()
|
||||
filter_backends = [DjangoFilterBackend, OrderingFilter]
|
||||
serializer_class = serializers.CollectionBackOfficeSerializer
|
||||
bind_object_serializer_class = serializers.CollectionBindObjectSerializer
|
||||
|
||||
ordering_fields = ('rank', 'start')
|
||||
ordering = ('-start', )
|
||||
|
||||
def perform_binding(self, serializer):
|
||||
data = serializer.validated_data
|
||||
collection = data.pop('collection')
|
||||
|
|
|
|||
|
|
@ -212,8 +212,18 @@ class EstablishmentQuerySet(models.QuerySet):
|
|||
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 = {
|
||||
'reviews__status': Review.READY,
|
||||
'establishment_type': establishment.establishment_type,
|
||||
|
|
@ -224,27 +234,69 @@ class EstablishmentQuerySet(models.QuerySet):
|
|||
.filter(**filters) \
|
||||
.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):
|
||||
"""
|
||||
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,
|
||||
public_mark__isnull=False)
|
||||
restaurant_qs = self.filter(slug=slug)
|
||||
if restaurant_qs.exists():
|
||||
establishment = restaurant_qs.first()
|
||||
subquery_filter_by_distance = Subquery(
|
||||
self.similar_base(establishment)
|
||||
.filter(public_mark__gte=10,
|
||||
establishment_gallery__is_main=True)
|
||||
.order_by('distance')[:settings.LIMITING_QUERY_OBJECTS]
|
||||
.values('id')
|
||||
restaurant = restaurant_qs.first()
|
||||
ids_by_subquery = self.similar_base_subquery(
|
||||
establishment=restaurant,
|
||||
filters={
|
||||
'public_mark__gte': 10,
|
||||
'establishment_gallery__is_main': True,
|
||||
}
|
||||
)
|
||||
return self.filter(id__in=subquery_filter_by_distance) \
|
||||
return self.filter(id__in=ids_by_subquery) \
|
||||
.annotate_intermediate_public_mark() \
|
||||
.annotate_mark_similarity(mark=establishment.public_mark) \
|
||||
.annotate_mark_similarity(mark=restaurant.public_mark) \
|
||||
.order_by('mark_similarity') \
|
||||
.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):
|
||||
"""
|
||||
|
|
@ -498,9 +550,15 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin,
|
|||
def visible_tags(self):
|
||||
return super().visible_tags \
|
||||
.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
|
||||
|
||||
@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):
|
||||
toque_number = 0
|
||||
if self.address and self.public_mark:
|
||||
|
|
|
|||
|
|
@ -324,3 +324,14 @@ class EstablishmentNoteListCreateSerializer(EstablishmentNoteBaseSerializer):
|
|||
"""Return establishment instance from view."""
|
||||
if self.serializer_view:
|
||||
return self.serializer_view.get_object()
|
||||
|
||||
|
||||
class EstablishmentAdminListSerializer(UserShortSerializer):
|
||||
"""Establishment admin serializer."""
|
||||
class Meta:
|
||||
model = UserShortSerializer.Meta.model
|
||||
fields = [
|
||||
'id',
|
||||
'username',
|
||||
'email'
|
||||
]
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ urlpatterns = [
|
|||
name='note-list-create'),
|
||||
path('slug/<slug:slug>/notes/<int:note_pk>/', views.EstablishmentNoteRUDView.as_view(),
|
||||
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/<int:pk>/', views.MenuRUDView.as_view(), name='menu-rud'),
|
||||
path('plates/', views.PlateListCreateView.as_view(), name='plates'),
|
||||
|
|
|
|||
|
|
@ -17,10 +17,14 @@ urlpatterns = [
|
|||
path('slug/<slug:slug>/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(),
|
||||
name='create-destroy-favorites'),
|
||||
|
||||
# similar establishments
|
||||
# similar establishments by type/subtype
|
||||
path('slug/<slug:slug>/similar/', views.RestaurantSimilarListView.as_view(),
|
||||
name='similar-restaurants'),
|
||||
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'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
"""Establishment app views."""
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import generics, permissions, status
|
||||
|
||||
from account.models import User
|
||||
from establishment import filters, models, serializers
|
||||
from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer
|
||||
from utils.permissions import IsCountryAdmin, IsEstablishmentManager, IsWineryReviewer
|
||||
|
|
@ -41,7 +43,7 @@ class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView):
|
|||
"""Establishment schedule RUD view"""
|
||||
lookup_field = 'slug'
|
||||
serializer_class = ScheduleRUDSerializer
|
||||
permission_classes = [IsWineryReviewer |IsEstablishmentManager]
|
||||
permission_classes = [IsWineryReviewer | IsEstablishmentManager]
|
||||
|
||||
def get_object(self):
|
||||
"""
|
||||
|
|
@ -75,6 +77,11 @@ class MenuListCreateView(generics.ListCreateAPIView):
|
|||
serializer_class = serializers.MenuSerializers
|
||||
queryset = models.Menu.objects.all()
|
||||
permission_classes = [IsWineryReviewer | IsEstablishmentManager]
|
||||
filter_backends = (DjangoFilterBackend,)
|
||||
filterset_fields = (
|
||||
'establishment',
|
||||
'establishment__slug',
|
||||
)
|
||||
|
||||
|
||||
class MenuRUDView(generics.RetrieveUpdateDestroyAPIView):
|
||||
|
|
@ -161,7 +168,7 @@ class EmailRUDView(generics.RetrieveUpdateDestroyAPIView):
|
|||
|
||||
class EmployeeListCreateView(generics.ListCreateAPIView):
|
||||
"""Emplyoee list create view."""
|
||||
permission_classes = (permissions.AllowAny, )
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
filter_class = filters.EmployeeBackFilter
|
||||
serializer_class = serializers.EmployeeBackSerializers
|
||||
queryset = models.Employee.objects.all()
|
||||
|
|
@ -170,7 +177,7 @@ class EmployeeListCreateView(generics.ListCreateAPIView):
|
|||
|
||||
class EstablishmentEmployeeListView(generics.ListCreateAPIView):
|
||||
"""Establishment emplyoees list view."""
|
||||
permission_classes = (permissions.AllowAny, )
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
serializer_class = serializers.EstablishmentEmployeeBackSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
|
@ -352,8 +359,8 @@ class EstablishmentEmployeeCreateView(generics.CreateAPIView):
|
|||
class EstablishmentEmployeeDeleteView(generics.DestroyAPIView):
|
||||
|
||||
def _get_object_to_delete(self, establishment_id, employee_id):
|
||||
result_qs = models.EstablishmentEmployee\
|
||||
.objects\
|
||||
result_qs = models.EstablishmentEmployee \
|
||||
.objects \
|
||||
.filter(establishment_id=establishment_id, employee_id=employee_id)
|
||||
if not result_qs.exists():
|
||||
raise Http404
|
||||
|
|
@ -371,6 +378,17 @@ class EstablishmentPositionListView(generics.ListAPIView):
|
|||
"""Establishment positions list view."""
|
||||
|
||||
pagination_class = None
|
||||
permission_classes = (permissions.AllowAny, )
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
queryset = models.Position.objects.all()
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from comment import models as comment_models
|
|||
from comment.serializers import CommentRUDSerializer
|
||||
from establishment import filters, models, serializers
|
||||
from main import methods
|
||||
from utils.pagination import EstablishmentPortionPagination
|
||||
from utils.pagination import PortionPagination
|
||||
from utils.views import FavoritesCreateDestroyMixinView, CarouselCreateDestroyMixinView
|
||||
|
||||
|
||||
|
|
@ -41,6 +41,12 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
|
|||
.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):
|
||||
"""Resource for getting a establishment."""
|
||||
|
||||
|
|
@ -61,7 +67,7 @@ class EstablishmentMobileRetrieveView(EstablishmentRetrieveView):
|
|||
class EstablishmentRecentReviewListView(EstablishmentListView):
|
||||
"""List view for last reviewed establishments."""
|
||||
|
||||
pagination_class = EstablishmentPortionPagination
|
||||
pagination_class = PortionPagination
|
||||
|
||||
def get_queryset(self):
|
||||
"""Overridden method 'get_queryset'."""
|
||||
|
|
@ -77,30 +83,36 @@ class EstablishmentRecentReviewListView(EstablishmentListView):
|
|||
return qs.last_reviewed(point=point)
|
||||
|
||||
|
||||
class EstablishmentSimilarList(EstablishmentListView):
|
||||
"""Resource for getting a list of similar establishments."""
|
||||
serializer_class = serializers.EstablishmentSimilarSerializer
|
||||
pagination_class = EstablishmentPortionPagination
|
||||
|
||||
|
||||
class RestaurantSimilarListView(EstablishmentSimilarList):
|
||||
class RestaurantSimilarListView(EstablishmentSimilarView):
|
||||
"""Resource for getting a list of similar restaurants."""
|
||||
|
||||
def get_queryset(self):
|
||||
"""Override get_queryset method"""
|
||||
"""Overridden get_queryset method"""
|
||||
return EstablishmentMixinView.get_queryset(self) \
|
||||
.has_location() \
|
||||
.similar_restaurants(slug=self.kwargs.get('slug'))
|
||||
|
||||
|
||||
class WinerySimilarListView(EstablishmentSimilarList):
|
||||
class WinerySimilarListView(EstablishmentSimilarView):
|
||||
"""Resource for getting a list of similar wineries."""
|
||||
|
||||
def get_queryset(self):
|
||||
"""Override get_queryset method"""
|
||||
"""Overridden get_queryset method"""
|
||||
return EstablishmentMixinView.get_queryset(self) \
|
||||
.has_location() \
|
||||
.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):
|
||||
"""Resource for getting a list of establishment types."""
|
||||
|
||||
|
|
|
|||
17
apps/gallery/migrations/0007_auto_20191211_1528.py
Normal file
17
apps/gallery/migrations/0007_auto_20191211_1528.py
Normal 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'},
|
||||
),
|
||||
]
|
||||
|
|
@ -34,6 +34,7 @@ class Image(ProjectBaseMixin, SORLImageMixin, PlatformMixin):
|
|||
"""Meta class."""
|
||||
verbose_name = _('Image')
|
||||
verbose_name_plural = _('Images')
|
||||
ordering = ['-modified']
|
||||
|
||||
def __str__(self):
|
||||
"""String representation"""
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
from django.conf import settings
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.core.files.base import ContentFile
|
||||
from rest_framework import serializers
|
||||
from sorl.thumbnail.parsers import parse_crop
|
||||
from sorl.thumbnail.parsers import ThumbnailParseError
|
||||
from sorl.thumbnail import get_thumbnail
|
||||
from sorl.thumbnail.parsers import parse_crop, ThumbnailParseError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from . import models
|
||||
|
|
@ -88,15 +89,23 @@ class CropImageSerializer(ImageSerializer):
|
|||
quality = validated_data.pop('quality')
|
||||
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.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:
|
||||
setattr(image,
|
||||
'cropped_image',
|
||||
image.get_cropped_image(
|
||||
geometry=f'{width}x{height}',
|
||||
quality=quality,
|
||||
crop=crop))
|
||||
cropped_image)
|
||||
return image
|
||||
|
||||
@property
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ urlpatterns = [
|
|||
path('addresses/<int:pk>/', views.AddressRUDView.as_view(), name='address-RUD'),
|
||||
|
||||
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>/gallery/', views.CityGalleryListView.as_view(),
|
||||
name='gallery-list'),
|
||||
|
|
|
|||
|
|
@ -37,6 +37,15 @@ class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView):
|
|||
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):
|
||||
"""RUD view for model City."""
|
||||
serializer_class = serializers.CitySerializer
|
||||
|
|
|
|||
|
|
@ -51,3 +51,6 @@ class PageTypeAdmin(admin.ModelAdmin):
|
|||
@admin.register(models.Page)
|
||||
class PageAdmin(admin.ModelAdmin):
|
||||
"""Page admin."""
|
||||
list_display = ('id', '__str__', 'advertisement')
|
||||
list_filter = ('advertisement__url', 'source')
|
||||
date_hierarchy = 'created'
|
||||
|
|
|
|||
38
apps/main/management/commands/add_panels.py
Normal file
38
apps/main/management/commands/add_panels.py
Normal 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.'))
|
||||
18
apps/main/migrations/0041_auto_20191211_0631.py
Normal file
18
apps/main/migrations/0041_auto_20191211_0631.py
Normal 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')},
|
||||
),
|
||||
]
|
||||
36
apps/main/migrations/0042_panel.py
Normal file
36
apps/main/migrations/0042_panel.py
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -305,7 +305,7 @@ class PageQuerySet(models.QuerySet):
|
|||
|
||||
def by_platform(self, platform: int):
|
||||
"""Filter by platform."""
|
||||
return self.filter(source=platform)
|
||||
return self.filter(source__in=[Page.ALL, platform])
|
||||
|
||||
|
||||
class Page(URLImageMixin, PlatformMixin, ProjectBaseMixin):
|
||||
|
|
@ -325,6 +325,7 @@ class Page(URLImageMixin, PlatformMixin, ProjectBaseMixin):
|
|||
"""Meta class."""
|
||||
verbose_name = _('page')
|
||||
verbose_name_plural = _('pages')
|
||||
unique_together = ('advertisement', 'source')
|
||||
|
||||
def __str__(self):
|
||||
"""Overridden dunder method."""
|
||||
|
|
@ -360,3 +361,46 @@ class Footer(ProjectBaseMixin):
|
|||
)
|
||||
about_us = models.TextField(_('about_us'))
|
||||
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
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ from rest_framework import serializers
|
|||
from location.serializers import CountrySerializer
|
||||
from main import models
|
||||
from utils.serializers import ProjectModelSerializer, TranslatedField, RecursiveFieldSerializer
|
||||
from account.serializers.back import BackUserSerializer
|
||||
from account.models import User
|
||||
|
||||
|
||||
class FeatureSerializer(serializers.ModelSerializer):
|
||||
|
|
@ -152,8 +154,6 @@ class SiteShortSerializer(serializers.ModelSerializer):
|
|||
]
|
||||
|
||||
|
||||
|
||||
|
||||
class AwardBaseSerializer(serializers.ModelSerializer):
|
||||
"""Award base serializer."""
|
||||
|
||||
|
|
@ -234,10 +234,26 @@ class PageBaseSerializer(serializers.ModelSerializer):
|
|||
'advertisement',
|
||||
]
|
||||
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):
|
||||
"""Serializer fro model PageType."""
|
||||
|
||||
|
|
@ -251,8 +267,32 @@ class PageTypeBaseSerializer(serializers.ModelSerializer):
|
|||
|
||||
|
||||
class ContentTypeBackSerializer(serializers.ModelSerializer):
|
||||
"""Serializer fro model ContentType."""
|
||||
"""Serializer for model ContentType."""
|
||||
|
||||
class Meta:
|
||||
model = ContentType
|
||||
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'
|
||||
]
|
||||
|
|
|
|||
|
|
@ -20,6 +20,12 @@ urlpatterns = [
|
|||
name='site-feature-rud'),
|
||||
path('footer/', views.FooterBackView.as_view(), name='footer-list-create'),
|
||||
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')
|
||||
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from rest_framework import generics, permissions
|
|||
|
||||
from main import serializers
|
||||
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
|
||||
|
||||
|
||||
|
|
@ -81,3 +81,29 @@ class FooterRUDBackView(generics.RetrieveUpdateDestroyAPIView):
|
|||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
serializer_class = serializers.FooterBackSerializer
|
||||
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()
|
||||
|
|
@ -70,9 +70,6 @@ class CarouselListView(generics.ListAPIView):
|
|||
|
||||
def get_queryset(self):
|
||||
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()
|
||||
if country_code:
|
||||
qs = qs.by_country_code(country_code)
|
||||
|
|
|
|||
18
apps/news/migrations/0041_auto_20191211_1528.py
Normal file
18
apps/news/migrations/0041_auto_20191211_1528.py
Normal 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')},
|
||||
),
|
||||
]
|
||||
18
apps/news/migrations/0042_news_duplication_date.py
Normal file
18
apps/news/migrations/0042_news_duplication_date.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
37
apps/news/migrations/0043_auto_20191216_1920.py
Normal file
37
apps/news/migrations/0043_auto_20191216_1920.py
Normal 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),
|
||||
]
|
||||
18
apps/news/migrations/0044_auto_20191216_2044.py
Normal file
18
apps/news/migrations/0044_auto_20191216_2044.py
Normal 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',
|
||||
),
|
||||
]
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
"""News app models."""
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes import fields as generic
|
||||
from django.contrib.postgres.fields import HStoreField
|
||||
from django.db import models
|
||||
from django.db.models import Case, When
|
||||
from django.utils import timezone
|
||||
|
|
@ -11,8 +15,6 @@ from utils.models import (BaseAttributes, TJSONField, TranslatedFieldsMixin, Has
|
|||
ProjectBaseMixin, GalleryModelMixin, IntermediateGalleryModelMixin,
|
||||
FavoritesMixin)
|
||||
from utils.querysets import TranslationQuerysetMixin
|
||||
from django.conf import settings
|
||||
from django.contrib.postgres.fields import HStoreField
|
||||
|
||||
|
||||
class Agenda(ProjectBaseMixin, TranslatedFieldsMixin):
|
||||
|
|
@ -177,11 +179,14 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
|
|||
description = TJSONField(blank=True, null=True, default=None,
|
||||
verbose_name=_('description'),
|
||||
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,
|
||||
verbose_name=_('Start'))
|
||||
end = models.DateTimeField(blank=True, null=True, default=None,
|
||||
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'),
|
||||
help_text='{"en-GB":"some slug"}')
|
||||
state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES,
|
||||
|
|
@ -211,6 +216,10 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
|
|||
verbose_name=_('banner'))
|
||||
site = models.ForeignKey('main.SiteSettings', blank=True, null=True,
|
||||
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()
|
||||
|
||||
class Meta:
|
||||
|
|
@ -220,7 +229,22 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
|
|||
verbose_name_plural = _('news')
|
||||
|
||||
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
|
||||
def is_publish(self):
|
||||
|
|
@ -320,4 +344,4 @@ class NewsGallery(IntermediateGalleryModelMixin):
|
|||
"""NewsGallery meta class."""
|
||||
verbose_name = _('news gallery')
|
||||
verbose_name_plural = _('news galleries')
|
||||
unique_together = (('news', 'is_main'), ('news', 'image'))
|
||||
unique_together = [['news', 'image'],]
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ from tag.serializers import TagBaseSerializer
|
|||
from utils import exceptions as utils_exceptions
|
||||
from utils.serializers import (TranslatedField, ProjectModelSerializer,
|
||||
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):
|
||||
|
|
@ -68,6 +71,13 @@ class NewsBaseSerializer(ProjectModelSerializer):
|
|||
tags = TagBaseSerializer(read_only=True, many=True, source='visible_tags')
|
||||
in_favorites = serializers.BooleanField(allow_null=True, 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:
|
||||
"""Meta class."""
|
||||
|
|
@ -75,12 +85,12 @@ class NewsBaseSerializer(ProjectModelSerializer):
|
|||
model = models.News
|
||||
fields = (
|
||||
'id',
|
||||
'slug',
|
||||
'title_translated',
|
||||
'subtitle_translated',
|
||||
'is_highlighted',
|
||||
'news_type',
|
||||
'tags',
|
||||
'slugs',
|
||||
'view_counter',
|
||||
)
|
||||
|
||||
|
|
@ -171,19 +181,48 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
|
|||
'title',
|
||||
'backoffice_title',
|
||||
'subtitle',
|
||||
'slugs',
|
||||
'locale_to_description_is_active',
|
||||
'is_published',
|
||||
'duplication_date',
|
||||
)
|
||||
extra_kwargs = {
|
||||
'backoffice_title': {'allow_null': False},
|
||||
'duplication_date': {'read_only': True},
|
||||
'locale_to_description_is_active': {'allow_null': False}
|
||||
}
|
||||
|
||||
def validate(self, attrs):
|
||||
slugs = attrs.get('slugs', {})
|
||||
if models.News.objects.filter(
|
||||
slugs__values__contains=list(slugs.values())
|
||||
).exclude(id=attrs.get('id', 0)).exists():
|
||||
raise serializers.ValidationError({'slugs': _('News with this slug already exists.')})
|
||||
return attrs
|
||||
def create(self, validated_data):
|
||||
slugs = validated_data.get('slugs')
|
||||
if slugs:
|
||||
if models.News.objects.filter(
|
||||
slugs__values__contains=list(slugs.values())
|
||||
).exists():
|
||||
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,
|
||||
|
|
@ -201,6 +240,7 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
|
|||
queryset=SiteSettings.objects.all())
|
||||
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):
|
||||
"""Meta class."""
|
||||
|
|
@ -214,6 +254,7 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
|
|||
'template',
|
||||
'template_display',
|
||||
'is_international',
|
||||
'duplicates',
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -233,6 +274,16 @@ class NewsBackOfficeGallerySerializer(serializers.ModelSerializer):
|
|||
"""Get url kwargs from request."""
|
||||
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):
|
||||
"""Override validate method."""
|
||||
news_pk = self.get_request_kwargs().get('pk')
|
||||
|
|
@ -249,8 +300,8 @@ class NewsBackOfficeGallerySerializer(serializers.ModelSerializer):
|
|||
news = news_qs.first()
|
||||
image = image_qs.first()
|
||||
|
||||
if image in news.gallery.all():
|
||||
raise serializers.ValidationError({'detail': _('Image is already added.')})
|
||||
# if image in news.gallery.all():
|
||||
# raise serializers.ValidationError({'detail': _('Image is already added.')})
|
||||
|
||||
attrs['news'] = news
|
||||
attrs['image'] = image
|
||||
|
|
@ -307,3 +358,26 @@ class NewsCarouselCreateSerializer(CarouselCreateSerializer):
|
|||
'content_object': validated_data.pop('news')
|
||||
})
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -14,4 +14,5 @@ urlpatterns = [
|
|||
path('<int:pk>/gallery/<int:image_id>/', views.NewsBackOfficeGalleryCreateDestroyView.as_view(),
|
||||
name='gallery-create-destroy'),
|
||||
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'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
"""News app views."""
|
||||
from django.conf import settings
|
||||
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 rating.tasks import add_rating
|
||||
from utils.permissions import IsCountryAdmin, IsContentPageManager
|
||||
from utils.views import CreateDestroyGalleryViewMixin, FavoritesCreateDestroyMixinView, CarouselCreateDestroyMixinView
|
||||
from utils.serializers import ImageBaseSerializer
|
||||
from utils.serializers import ImageBaseSerializer, EmptySerializer
|
||||
|
||||
|
||||
class NewsMixinView:
|
||||
|
|
@ -99,7 +99,10 @@ class NewsBackOfficeLCView(NewsBackOfficeMixinView,
|
|||
|
||||
def get_queryset(self):
|
||||
"""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,
|
||||
|
|
@ -107,6 +110,13 @@ class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView,
|
|||
"""Resource for a create gallery for news for back-office users."""
|
||||
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):
|
||||
"""
|
||||
Returns the object the view is displaying.
|
||||
|
|
@ -167,3 +177,10 @@ class NewsCarouselCreateDestroyView(CarouselCreateDestroyMixinView):
|
|||
|
||||
_model = models.News
|
||||
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()
|
||||
|
|
|
|||
19
apps/product/migrations/0021_auto_20191212_0926.py
Normal file
19
apps/product/migrations/0021_auto_20191212_0926.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,13 +1,17 @@
|
|||
"""Product app models."""
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes import fields as generic
|
||||
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.validators import MaxValueValidator, MinValueValidator
|
||||
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 location.models import WineOriginAddressMixin
|
||||
from review.models import Review
|
||||
from utils.models import (BaseAttributes, ProjectBaseMixin, HasTagsMixin,
|
||||
TranslatedFieldsMixin, TJSONField, FavoritesMixin,
|
||||
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,
|
||||
HasTagsMixin, FavoritesMixin):
|
||||
|
|
@ -178,7 +236,7 @@ class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes,
|
|||
default=None, help_text='{"en-GB":"some text"}')
|
||||
available = models.BooleanField(_('available'), default=True)
|
||||
product_type = models.ForeignKey(ProductType, on_delete=models.PROTECT,
|
||||
null=True,
|
||||
null=True, blank=True, default=None,
|
||||
related_name='products', verbose_name=_('Type'))
|
||||
subtypes = models.ManyToManyField(ProductSubType, blank=True,
|
||||
related_name='products',
|
||||
|
|
@ -226,8 +284,8 @@ class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes,
|
|||
awards = generic.GenericRelation(to='main.Award', related_query_name='product')
|
||||
|
||||
serial_number = models.CharField(max_length=255,
|
||||
default=None, null=True,
|
||||
verbose_name=_('Serial number'))
|
||||
default=None, null=True,
|
||||
verbose_name=_('Serial number'))
|
||||
|
||||
site = models.ForeignKey(to='main.SiteSettings', null=True, blank=True, on_delete=models.CASCADE)
|
||||
|
||||
|
|
|
|||
|
|
@ -218,4 +218,4 @@ class ProductCommentCreateSerializer(CommentSerializer):
|
|||
'user': self.context.get('request').user,
|
||||
'content_object': validated_data.pop('product')
|
||||
})
|
||||
return super().create(validated_data)
|
||||
return super().create(validated_data)
|
||||
|
|
|
|||
|
|
@ -16,4 +16,13 @@ urlpatterns = [
|
|||
name='create-comment'),
|
||||
path('slug/<slug:slug>/comments/<int:comment_id>/', views.ProductCommentRUDView.as_view(),
|
||||
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'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from comment.models import Comment
|
|||
from product import filters, serializers
|
||||
from comment.serializers import CommentRUDSerializer
|
||||
from utils.views import FavoritesCreateDestroyMixinView
|
||||
from utils.pagination import PortionPagination
|
||||
|
||||
|
||||
class ProductBaseView(generics.GenericAPIView):
|
||||
|
|
@ -31,6 +32,12 @@ class ProductListView(ProductBaseView, generics.ListAPIView):
|
|||
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):
|
||||
"""Detail view fro model Product."""
|
||||
lookup_field = 'slug'
|
||||
|
|
@ -81,3 +88,14 @@ class ProductCommentRUDView(generics.RetrieveUpdateDestroyAPIView):
|
|||
self.check_object_permissions(self.request, 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'))
|
||||
|
||||
|
|
|
|||
18
apps/recipe/migrations/0002_recipe_old_id.py
Normal file
18
apps/recipe/migrations/0002_recipe_old_id.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
18
apps/recipe/migrations/0003_recipe_slug.py
Normal file
18
apps/recipe/migrations/0003_recipe_slug.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -25,6 +25,9 @@ class RecipeQuerySet(models.QuerySet):
|
|||
default=False,
|
||||
output_field=models.BooleanField(default=False)))
|
||||
|
||||
def by_locale(self, locale):
|
||||
return self.filter(title__icontains=locale)
|
||||
|
||||
|
||||
class Recipe(TranslatedFieldsMixin, ImageMixin, BaseAttributes):
|
||||
"""Recipe model."""
|
||||
|
|
@ -43,22 +46,19 @@ class Recipe(TranslatedFieldsMixin, ImageMixin, BaseAttributes):
|
|||
|
||||
STR_FIELD_NAME = 'title'
|
||||
|
||||
title = TJSONField(blank=True, null=True, default=None, verbose_name=_('Title'),
|
||||
help_text='{"en-GB": "some text"}')
|
||||
title = TJSONField(blank=True, null=True, default=None, verbose_name=_('Title'), help_text='{"en-GB": "some text"}')
|
||||
subtitle = TJSONField(blank=True, null=True, default=None, verbose_name=_('Subtitle'),
|
||||
help_text='{"en-GB": "some text"}')
|
||||
description = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'),
|
||||
help_text='{"en-GB": "some text"}')
|
||||
state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES,
|
||||
verbose_name=_('State'))
|
||||
author = models.CharField(max_length=255, blank=True, null=True, default=None,
|
||||
verbose_name=_('Author'))
|
||||
published_at = models.DateTimeField(verbose_name=_('Published at'),
|
||||
blank=True, default=None, null=True,
|
||||
state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES, verbose_name=_('State'))
|
||||
author = models.CharField(max_length=255, blank=True, null=True, default=None, verbose_name=_('Author'))
|
||||
published_at = models.DateTimeField(verbose_name=_('Published at'), blank=True, default=None, null=True,
|
||||
help_text=_('Published at'))
|
||||
published_scheduled_at = models.DateTimeField(verbose_name=_('Published scheduled at'),
|
||||
blank=True, default=None, null=True,
|
||||
help_text=_('Published scheduled at'))
|
||||
published_scheduled_at = models.DateTimeField(verbose_name=_('Published scheduled at'), blank=True, default=None,
|
||||
null=True, 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()
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,15 @@ class RecipeListSerializer(serializers.ModelSerializer):
|
|||
"""Meta class."""
|
||||
|
||||
model = models.Recipe
|
||||
fields = ('id', 'title_translated', 'subtitle_translated', 'author',
|
||||
'published_at', 'in_favorites')
|
||||
fields = (
|
||||
'id',
|
||||
'title_translated',
|
||||
'subtitle_translated',
|
||||
'author',
|
||||
'created_by',
|
||||
'published_at',
|
||||
'in_favorites',
|
||||
)
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,49 @@
|
|||
from django.db.models import Value, IntegerField, F
|
||||
from pprint import pprint
|
||||
|
||||
from django.db.models import Count
|
||||
|
||||
from recipe.models import Recipe
|
||||
from transfer.models import PageTexts
|
||||
from transfer.serializers.recipe import RecipeSerializer
|
||||
|
||||
|
||||
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():
|
||||
serialized_data.save()
|
||||
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 = {
|
||||
"recipe": [transfer_recipe]
|
||||
'recipe': [transfer_recipe]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
"""Recipe app common views."""
|
||||
from django.utils import translation
|
||||
from rest_framework import generics, permissions
|
||||
|
||||
from recipe import models
|
||||
from recipe.serializers import common as serializers
|
||||
|
||||
|
|
@ -10,9 +12,14 @@ class RecipeViewMixin(generics.GenericAPIView):
|
|||
pagination_class = None
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
|
||||
def get_queryset(self):
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
user = self.request.user
|
||||
qs = models.Recipe.objects.published().annotate_in_favorites(user)
|
||||
|
||||
locale = kwargs.get('locale')
|
||||
if locale:
|
||||
qs = qs.by_locale(locale)
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
|
|
@ -21,6 +28,11 @@ class RecipeListView(RecipeViewMixin, generics.ListAPIView):
|
|||
|
||||
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):
|
||||
"""Resource for detailed recipe information."""
|
||||
|
|
|
|||
|
|
@ -127,8 +127,10 @@ def transfer_product_reviews():
|
|||
|
||||
|
||||
data_types = {
|
||||
"languages": [
|
||||
transfer_languages,
|
||||
],
|
||||
"overlook": [
|
||||
# transfer_languages,
|
||||
transfer_reviews,
|
||||
transfer_text_review,
|
||||
make_en_text_review,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from search_indexes.utils import OBJECT_FIELD_PROPERTIES
|
|||
from product import models
|
||||
|
||||
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
|
||||
|
|
@ -14,12 +14,13 @@ class ProductDocument(Document):
|
|||
|
||||
description = fields.ObjectField(attr='description_indexing',
|
||||
properties=OBJECT_FIELD_PROPERTIES)
|
||||
product_type = fields.ObjectField(properties={
|
||||
'id': fields.IntegerField(),
|
||||
'name': fields.ObjectField(attr='name_indexing', properties=OBJECT_FIELD_PROPERTIES),
|
||||
'index_name': fields.KeywordField(),
|
||||
'use_subtypes': fields.BooleanField(),
|
||||
})
|
||||
product_type = fields.ObjectField(
|
||||
properties={
|
||||
'id': fields.IntegerField(),
|
||||
'name': fields.ObjectField(attr='name_indexing', properties=OBJECT_FIELD_PROPERTIES),
|
||||
'index_name': fields.KeywordField(),
|
||||
},
|
||||
)
|
||||
subtypes = fields.ObjectField(
|
||||
properties={
|
||||
'id': fields.IntegerField(),
|
||||
|
|
@ -54,15 +55,12 @@ class ProductDocument(Document):
|
|||
),
|
||||
'address': fields.ObjectField(
|
||||
properties={
|
||||
'city': fields.ObjectField(
|
||||
properties={
|
||||
'country': fields.ObjectField(
|
||||
properties={
|
||||
'code': fields.KeywordField()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
'id': fields.IntegerField(),
|
||||
'street_name_1': fields.TextField(
|
||||
fields={'raw': 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_fr = fields.TextField(attr='display_name', analyzer='french')
|
||||
favorites_for_users = fields.ListField(field=fields.IntegerField())
|
||||
created = fields.DateField(attr='created') # publishing date (?)
|
||||
created = fields.DateField(attr='created') # publishing date (?)
|
||||
|
||||
class Django:
|
||||
model = models.Product
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ class ProductSubtypeDocumentSerializer(serializers.Serializer):
|
|||
name_translated = serializers.SerializerMethodField()
|
||||
|
||||
def get_name_translated(self, obj):
|
||||
if isinstance(obj, dict):
|
||||
return get_translated_value(obj.get('name'))
|
||||
return get_translated_value(obj.name)
|
||||
|
||||
|
||||
|
|
@ -93,7 +95,7 @@ class TagDocumentSerializer(serializers.Serializer):
|
|||
return get_translated_value(obj.label)
|
||||
|
||||
|
||||
class ProductTypeDocumentSerializer(serializers.Serializer):
|
||||
class ProductTypeSerializer(serializers.Serializer):
|
||||
"""Product type ES document serializer."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
|
|
@ -102,8 +104,13 @@ class ProductTypeDocumentSerializer(serializers.Serializer):
|
|||
|
||||
@staticmethod
|
||||
def get_name_translated(obj):
|
||||
if isinstance(obj, dict):
|
||||
return get_translated_value(obj.get('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):
|
||||
"""City serializer for ES Document,"""
|
||||
|
|
@ -114,7 +121,6 @@ class CityDocumentShortSerializer(serializers.Serializer):
|
|||
|
||||
|
||||
class CountryDocumentSerializer(serializers.Serializer):
|
||||
|
||||
id = serializers.IntegerField()
|
||||
code = serializers.CharField(allow_null=True)
|
||||
svg_image = serializers.CharField()
|
||||
|
|
@ -122,11 +128,12 @@ class CountryDocumentSerializer(serializers.Serializer):
|
|||
|
||||
@staticmethod
|
||||
def get_name_translated(obj):
|
||||
if isinstance(obj, dict):
|
||||
return get_translated_value(obj.get('name'))
|
||||
return get_translated_value(obj.name)
|
||||
|
||||
|
||||
class AnotherCityDocumentShortSerializer(CityDocumentShortSerializer):
|
||||
|
||||
country = CountryDocumentSerializer()
|
||||
|
||||
def to_representation(self, instance):
|
||||
|
|
@ -136,19 +143,6 @@ class AnotherCityDocumentShortSerializer(CityDocumentShortSerializer):
|
|||
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):
|
||||
"""Address serializer for ES Document."""
|
||||
|
||||
|
|
@ -171,6 +165,28 @@ class AddressDocumentSerializer(serializers.Serializer):
|
|||
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):
|
||||
"""Schedule serializer for ES Document"""
|
||||
|
||||
|
|
@ -206,6 +222,7 @@ class NewsDocumentSerializer(InFavoritesMixin, DocumentSerializer):
|
|||
subtitle_translated = serializers.SerializerMethodField(allow_null=True)
|
||||
news_type = NewsTypeSerializer()
|
||||
tags = TagsDocumentSerializer(many=True, source='visible_tags')
|
||||
slug = serializers.SerializerMethodField(allow_null=True)
|
||||
|
||||
class Meta:
|
||||
"""Meta class."""
|
||||
|
|
@ -221,9 +238,13 @@ class NewsDocumentSerializer(InFavoritesMixin, DocumentSerializer):
|
|||
'news_type',
|
||||
'tags',
|
||||
'start',
|
||||
'slugs',
|
||||
'slug',
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_slug(obj):
|
||||
return get_translated_value(obj.slugs)
|
||||
|
||||
@staticmethod
|
||||
def get_title_translated(obj):
|
||||
return get_translated_value(obj.title)
|
||||
|
|
@ -285,15 +306,15 @@ class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer):
|
|||
)
|
||||
|
||||
|
||||
class ProductDocumentSerializer(InFavoritesMixin, DocumentSerializer):
|
||||
class ProductDocumentSerializer(InFavoritesMixin):
|
||||
"""Product document serializer"""
|
||||
|
||||
tags = TagsDocumentSerializer(many=True, source='related_tags')
|
||||
subtypes = ProductSubtypeDocumentSerializer(many=True, allow_null=True)
|
||||
wine_colors = TagDocumentSerializer(many=True)
|
||||
grape_variety = TagDocumentSerializer(many=True)
|
||||
product_type = ProductTypeDocumentSerializer(allow_null=True)
|
||||
establishment_detail = ProductEstablishmentDocumentSerializer(source='establishment', allow_null=True)
|
||||
product_type = ProductTypeSerializer(allow_null=True)
|
||||
establishment_detail = ProductEstablishmentSerializer(source='establishment', allow_null=True)
|
||||
wine_origins = WineOriginSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
|
|
@ -317,7 +338,6 @@ class ProductDocumentSerializer(InFavoritesMixin, DocumentSerializer):
|
|||
'subtypes',
|
||||
'wine_colors',
|
||||
'grape_variety',
|
||||
'establishment_detail',
|
||||
'average_price',
|
||||
'created',
|
||||
'wine_origins',
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ def update_document(sender, **kwargs):
|
|||
instance = kwargs['instance']
|
||||
|
||||
app_label_model_name_to_filter = {
|
||||
('location','country'): 'address__city__country',
|
||||
('location','city'): 'address__city',
|
||||
('location', 'country'): 'address__city__country',
|
||||
('location', 'city'): 'address__city',
|
||||
('location', 'address'): 'address',
|
||||
# todo: remove after migration
|
||||
('establishment', 'establishmenttype'): 'establishment_type',
|
||||
|
|
@ -34,8 +34,8 @@ def update_news(sender, **kwargs):
|
|||
model_name = sender._meta.model_name
|
||||
instance = kwargs['instance']
|
||||
app_label_model_name_to_filter = {
|
||||
('location','country'): 'country',
|
||||
('news','newstype'): 'news_type',
|
||||
('location', 'country'): 'country',
|
||||
('news', 'newstype'): 'news_type',
|
||||
('tag', 'tag'): 'tags',
|
||||
}
|
||||
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
|
||||
instance = kwargs['instance']
|
||||
app_label_model_name_to_filter = {
|
||||
('product','productstandard'): 'standards',
|
||||
('product', 'productstandard'): 'standards',
|
||||
('product', 'producttype'): 'product_type',
|
||||
('tag','tag'): 'tags',
|
||||
('tag', 'tag'): 'tags',
|
||||
('location', 'wineregion'): 'wine_region',
|
||||
('location', 'winesubregion'): 'wine_sub_region',
|
||||
('location', 'winevillage'): 'wine_village',
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ from rest_framework import permissions
|
|||
from django_elasticsearch_dsl_drf import constants
|
||||
from django_elasticsearch_dsl_drf.filter_backends import (
|
||||
FilteringFilterBackend,
|
||||
GeoSpatialFilteringFilterBackend,
|
||||
GeoSpatialOrderingFilterBackend,
|
||||
OrderingFilterBackend,
|
||||
)
|
||||
|
|
@ -62,11 +61,18 @@ class NewsDocumentViewSet(BaseDocumentViewSet):
|
|||
)
|
||||
|
||||
filter_fields = {
|
||||
'tags_id': {
|
||||
'field': 'tags.id',
|
||||
'lookups': [
|
||||
constants.LOOKUP_QUERY_IN,
|
||||
constants.LOOKUP_QUERY_EXCLUDE,
|
||||
],
|
||||
},
|
||||
'tag': {
|
||||
'field': 'tags.id',
|
||||
'lookups': [
|
||||
constants.LOOKUP_QUERY_IN,
|
||||
constants.LOOKUP_QUERY_EXCLUDE
|
||||
constants.LOOKUP_QUERY_EXCLUDE,
|
||||
]
|
||||
},
|
||||
'tag_value': {
|
||||
|
|
@ -391,7 +397,7 @@ class ProductDocumentViewSet(BaseDocumentViewSet):
|
|||
'lookups': [constants.LOOKUP_QUERY_IN],
|
||||
},
|
||||
'country': {
|
||||
'field': 'establishment.address.city.country.code',
|
||||
'field': 'establishment.city.country.code',
|
||||
},
|
||||
'wine_colors_id': {
|
||||
'field': 'wine_colors.id',
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@
|
|||
from rest_framework import serializers
|
||||
from rest_framework.fields import SerializerMethodField
|
||||
|
||||
from establishment.models import (Establishment, EstablishmentType)
|
||||
from news.models import News, NewsType
|
||||
from establishment.models import Establishment
|
||||
from establishment.models import EstablishmentType
|
||||
from news.models import News
|
||||
from news.models import NewsType
|
||||
from tag import models
|
||||
from utils.exceptions import (ObjectAlreadyAdded, BindingObjectNotFound,
|
||||
RemovedBindingObjectNotFound)
|
||||
from utils.exceptions import BindingObjectNotFound
|
||||
from utils.exceptions import ObjectAlreadyAdded
|
||||
from utils.exceptions import RemovedBindingObjectNotFound
|
||||
from utils.serializers import TranslatedField
|
||||
|
||||
|
||||
|
|
@ -95,6 +98,62 @@ class TagCategoryBaseSerializer(serializers.ModelSerializer):
|
|||
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):
|
||||
"""Serializer for model TagCategory."""
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ app_name = 'tag'
|
|||
|
||||
router = SimpleRouter()
|
||||
router.register(r'categories', views.TagCategoryViewSet)
|
||||
router.register(r'filters', views.FiltersTagCategoryViewSet)
|
||||
router.register(r'chosen_tags', views.ChosenTagsView)
|
||||
|
||||
urlpatterns = [
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
"""Tag views."""
|
||||
from django.conf import settings
|
||||
from rest_framework import permissions
|
||||
from rest_framework import viewsets, mixins, status, generics
|
||||
from rest_framework import generics, mixins, permissions, status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
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
|
||||
|
||||
|
||||
|
|
@ -36,7 +40,8 @@ class ChosenTagsView(generics.ListAPIView, viewsets.GenericViewSet):
|
|||
serializer = self.get_serializer(queryset, many=True)
|
||||
result_list = serializer.data
|
||||
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']))
|
||||
return Response(result_list)
|
||||
|
||||
|
|
@ -53,6 +58,233 @@ class TagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
|
|||
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
|
||||
class BindObjectMixin:
|
||||
"""Bind object mixin."""
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class Command(BaseCommand):
|
|||
'news', # перенос новостей (после №2)
|
||||
'account', # №1 - перенос пользователей
|
||||
'subscriber',
|
||||
'recipe',
|
||||
'recipe', # №2 - рецепты
|
||||
'partner',
|
||||
'establishment', # №3 - перенос заведений
|
||||
'gallery',
|
||||
|
|
@ -49,6 +49,7 @@ class Command(BaseCommand):
|
|||
'guide_elements_bulk',
|
||||
'guide_element_advertorials',
|
||||
'guide_complete',
|
||||
'languages', # №4 - перенос языков
|
||||
]
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
|
|
|||
|
|
@ -1222,3 +1222,35 @@ class Footers(MigrateMixin):
|
|||
class Meta:
|
||||
managed = False
|
||||
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'
|
||||
|
|
|
|||
|
|
@ -1,55 +1,87 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from account.models import User
|
||||
from recipe.models import Recipe
|
||||
from utils.legacy_parser import parse_legacy_news_content
|
||||
|
||||
|
||||
class RecipeSerializer(serializers.ModelSerializer):
|
||||
locale = serializers.CharField()
|
||||
class RecipeSerializer(serializers.Serializer):
|
||||
id = serializers.IntegerField()
|
||||
title = serializers.CharField(allow_null=True)
|
||||
summary = serializers.CharField(allow_null=True, allow_blank=True)
|
||||
body = serializers.CharField(allow_null=True)
|
||||
title = serializers.CharField()
|
||||
state = serializers.CharField()
|
||||
created_at = serializers.DateTimeField(source="published_at", format='%m-%d-%Y %H:%M:%S')
|
||||
|
||||
class Meta:
|
||||
model = Recipe
|
||||
fields = (
|
||||
"body",
|
||||
"title",
|
||||
"state",
|
||||
"created_at",
|
||||
'locale',
|
||||
)
|
||||
locale = serializers.CharField(allow_null=True)
|
||||
state = serializers.CharField(allow_null=True)
|
||||
slug = serializers.CharField(allow_null=True)
|
||||
created_at = serializers.DateTimeField(format='%m-%d-%Y %H:%M:%S')
|
||||
page__attachment_suffix_url = serializers.CharField(allow_null=True)
|
||||
page__account_id = serializers.IntegerField(allow_null=True)
|
||||
|
||||
def validate(self, data):
|
||||
data["state"] = self.get_state(data)
|
||||
data["title"] = self.get_title(data)
|
||||
data["description"] = self.get_description(data)
|
||||
data.pop("body")
|
||||
data.pop("locale")
|
||||
data.update({
|
||||
'old_id': data.pop('id'),
|
||||
'title': self.get_title(data),
|
||||
'subtitle': self.get_subtitle(data),
|
||||
'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
|
||||
|
||||
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):
|
||||
if obj["state"] == "published":
|
||||
return Recipe.PUBLISHED
|
||||
elif obj["state"] == "hidden":
|
||||
return Recipe.HIDDEN
|
||||
elif obj["state"] == "published_exclusive":
|
||||
return Recipe.PUBLISHED_EXCLUSIVE
|
||||
else:
|
||||
return Recipe.WAITING
|
||||
@staticmethod
|
||||
def get_title(data):
|
||||
if data.get('title') and data.get('locale'):
|
||||
return {data['locale']: data['title']}
|
||||
return None
|
||||
|
||||
def get_title(self, obj):
|
||||
# tit = obj.get("title")
|
||||
# return {"en-GB": tit}
|
||||
return {obj['locale']: obj['title']}
|
||||
@staticmethod
|
||||
def get_subtitle(data):
|
||||
if data.get('summary') and data.get('locale'):
|
||||
return {data['locale']: data['summary']}
|
||||
return None
|
||||
|
||||
def get_description(self, obj):
|
||||
# desc = obj.get("body")
|
||||
# return {"en-GB": desc}
|
||||
content = None
|
||||
if obj['body']:
|
||||
content = parse_legacy_news_content(obj['body'])
|
||||
return {obj['locale']: content}
|
||||
@staticmethod
|
||||
def get_description(data):
|
||||
if data.get('body') and data.get('locale'):
|
||||
content = parse_legacy_news_content(data['body'])
|
||||
return {data['locale']: content}
|
||||
return None
|
||||
|
||||
@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
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import logging
|
|||
import random
|
||||
import re
|
||||
import string
|
||||
from collections import namedtuple
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
|
|
@ -124,3 +125,10 @@ def absolute_url_decorator(func):
|
|||
def get_point_from_coordinates(latitude: str, longitude: str):
|
||||
if latitude and longitude:
|
||||
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()]
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from django.conf import settings
|
|||
from rest_framework.pagination import CursorPagination, PageNumberPagination
|
||||
from django_elasticsearch_dsl_drf.pagination import PageNumberPagination as ESPagination
|
||||
|
||||
|
||||
class ProjectPageNumberPagination(PageNumberPagination):
|
||||
"""Customized pagination class."""
|
||||
|
||||
|
|
@ -82,7 +83,7 @@ class ESDocumentPagination(ESPagination):
|
|||
return page.facets._d_
|
||||
|
||||
|
||||
class EstablishmentPortionPagination(ProjectMobilePagination):
|
||||
class PortionPagination(ProjectMobilePagination):
|
||||
"""
|
||||
Pagination for app establishments with limit page size equal to 12
|
||||
"""
|
||||
|
|
|
|||
12
db_migration_resolve.txt
Normal file
12
db_migration_resolve.txt
Normal 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, в исходное состояние
|
||||
|
|
@ -16,8 +16,6 @@ services:
|
|||
- .:/code
|
||||
|
||||
|
||||
|
||||
|
||||
# PostgreSQL database
|
||||
db:
|
||||
build:
|
||||
|
|
|
|||
|
|
@ -516,9 +516,6 @@ PHONENUMBER_DEFAULT_REGION = "FR"
|
|||
|
||||
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']
|
||||
NEWS_CHOSEN_TAGS = ['eat', 'drink', 'cook', 'style', 'international', 'event', 'partnership']
|
||||
INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next']
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user