Merge branch 'develop' into 'feature/subscriptions'

# Conflicts:
#   apps/account/serializers/common.py
This commit is contained in:
Ruslan Stepanov 2020-01-20 13:24:46 +00:00
commit 27f47cfae6
93 changed files with 1968 additions and 609 deletions

View File

@ -7,12 +7,14 @@ from account import models
@admin.register(models.Role) @admin.register(models.Role)
class RoleAdmin(admin.ModelAdmin): class RoleAdmin(admin.ModelAdmin):
list_display = ['role', 'country'] list_display = ['id', 'role', 'country']
raw_id_fields = ['country', ]
@admin.register(models.UserRole) @admin.register(models.UserRole)
class UserRoleAdmin(admin.ModelAdmin): class UserRoleAdmin(admin.ModelAdmin):
list_display = ['user', 'role', 'establishment'] list_display = ['user', 'role', 'establishment', ]
raw_id_fields = ['user', 'role', 'establishment', 'requester', ]
@admin.register(models.User) @admin.register(models.User)

29
apps/account/filters.py Normal file
View File

@ -0,0 +1,29 @@
"""Account app filters."""
from django.core.validators import EMPTY_VALUES
from django_filters import rest_framework as filters
from account import models
class AccountBackOfficeFilter(filters.FilterSet):
"""Account filter set."""
role = filters.MultipleChoiceFilter(choices=models.Role.ROLE_CHOICES,
method='filter_by_roles')
class Meta:
"""Meta class."""
model = models.User
fields = (
'role',
'email_confirmed',
'is_staff',
'is_active',
'is_superuser',
)
def filter_by_roles(self, queryset, name, value):
if value not in EMPTY_VALUES:
return queryset.by_roles(value)
return queryset

View File

@ -70,10 +70,12 @@ class Command(BaseCommand):
role_choice = getattr(Role, old_role.new_role) role_choice = getattr(Role, old_role.new_role)
sites = SiteSettings.objects.filter(old_id=s.site_id) sites = SiteSettings.objects.filter(old_id=s.site_id)
for site in sites: for site in sites:
role = Role.objects.filter(site=site, role=role_choice) data = {'site': site, 'role': role_choice}
role = Role.objects.filter(**data)
if not role.exists(): if not role.exists():
objects.append( objects.append(
Role(site=site, role=role_choice) Role(**data)
) )
Role.objects.bulk_create(objects) Role.objects.bulk_create(objects)
@ -81,7 +83,7 @@ class Command(BaseCommand):
def update_site_role(self): def update_site_role(self):
roles = Role.objects.filter(country__isnull=True).select_related('site')\ roles = Role.objects.filter(country__isnull=True).select_related('site')\
.filter(site__id__isnull=False).select_for_update() .filter(site__id__isnull=False).select_for_update()
with transaction.atomic(): with transaction.atomic():
for role in tqdm(roles, desc='Update role country'): for role in tqdm(roles, desc='Update role country'):
role.country = role.site.country role.country = role.site.country
@ -114,8 +116,7 @@ class Command(BaseCommand):
users = User.objects.filter(old_id=s.account_id) users = User.objects.filter(old_id=s.account_id)
for user in users: for user in users:
for role in roles: for role in roles:
user_role = UserRole.objects.get_or_create(user=user, UserRole.objects.get_or_create(user=user, role=role, state=UserRole.VALIDATED)
role=role)
self.stdout.write(self.style.WARNING(f'Added users roles.')) self.stdout.write(self.style.WARNING(f'Added users roles.'))
def superuser_role_sql(self): def superuser_role_sql(self):

View File

@ -0,0 +1,26 @@
# Generated by Django 2.2.7 on 2020-01-14 13:11
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('main', '0046_auto_20200114_1218'),
('account', '0031_user_last_country'),
]
operations = [
migrations.AddField(
model_name='role',
name='navigation_bar_permission',
field=models.ForeignKey(blank=True, help_text='navigation bar item permission', null=True, on_delete=django.db.models.deletion.SET_NULL, to='main.NavigationBarPermission', verbose_name='navigation bar permission'),
),
migrations.AlterField(
model_name='userrole',
name='requester',
field=models.ForeignKey(blank=True, default=None, help_text='A user (REQUESTER) who requests a role change for a USER', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='roles_requested', to=settings.AUTH_USER_MODEL),
),
]

View File

@ -1,10 +1,8 @@
"""Account models""" """Account models"""
from datetime import datetime from datetime import datetime
from tabnanny import verbose
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AbstractUser, UserManager as BaseUserManager from django.contrib.auth.models import AbstractUser, UserManager as BaseUserManager
from django.contrib.auth.tokens import default_token_generator as password_token_generator
from django.core.mail import send_mail from django.core.mail import send_mail
from django.db import models from django.db import models
from django.template.loader import render_to_string, get_template from django.template.loader import render_to_string, get_template
@ -24,6 +22,19 @@ from utils.models import ImageMixin, ProjectBaseMixin, PlatformMixin
from utils.tokens import GMRefreshToken from utils.tokens import GMRefreshToken
class RoleQuerySet(models.QuerySet):
def annotate_role_name(self):
return self.annotate(role_name=models.Case(*self.model.role_condition_expressions(),
output_field=models.CharField()))
def annotate_role_counter(self):
return self.annotate(
role_counter=models.Count('userrole',
distinct=True,
filter=models.Q(userrole__state=UserRole.VALIDATED)))
class Role(ProjectBaseMixin): class Role(ProjectBaseMixin):
"""Base Role model.""" """Base Role model."""
STANDARD_USER = 1 STANDARD_USER = 1
@ -46,13 +57,14 @@ class Role(ProjectBaseMixin):
(CONTENT_PAGE_MANAGER, _('Content page manager')), (CONTENT_PAGE_MANAGER, _('Content page manager')),
(ESTABLISHMENT_MANAGER, _('Establishment manager')), (ESTABLISHMENT_MANAGER, _('Establishment manager')),
(REVIEWER_MANGER, _('Reviewer manager')), (REVIEWER_MANGER, _('Reviewer manager')),
(RESTAURANT_REVIEWER, 'Restaurant reviewer'), (RESTAURANT_REVIEWER, _('Restaurant reviewer')),
(SALES_MAN, 'Sales man'), (SALES_MAN, _('Sales man')),
(WINERY_REVIEWER, 'Winery reviewer'), (WINERY_REVIEWER, _('Winery reviewer')),
(SELLER, 'Seller'), (SELLER, _('Seller')),
(LIQUOR_REVIEWER, 'Liquor reviewer'), (LIQUOR_REVIEWER, _('Liquor reviewer')),
(PRODUCT_REVIEWER, 'Product reviewer'), (PRODUCT_REVIEWER, _('Product reviewer')),
) )
role = models.PositiveIntegerField(verbose_name=_('Role'), choices=ROLE_CHOICES, role = models.PositiveIntegerField(verbose_name=_('Role'), choices=ROLE_CHOICES,
null=False, blank=False) null=False, blank=False)
country = models.ForeignKey(Country, verbose_name=_('Country'), country = models.ForeignKey(Country, verbose_name=_('Country'),
@ -62,6 +74,27 @@ class Role(ProjectBaseMixin):
establishment_subtype = models.ForeignKey(EstablishmentSubType, establishment_subtype = models.ForeignKey(EstablishmentSubType,
verbose_name=_('Establishment subtype'), verbose_name=_('Establishment subtype'),
null=True, blank=True, on_delete=models.SET_NULL) null=True, blank=True, on_delete=models.SET_NULL)
navigation_bar_permission = models.ForeignKey('main.NavigationBarPermission',
blank=True, null=True,
on_delete=models.SET_NULL,
help_text='navigation bar item permission',
verbose_name=_('navigation bar permission'))
objects = RoleQuerySet.as_manager()
@classmethod
def role_names(cls):
return [role_name._proxy____args[0]
for role_name in dict(cls.ROLE_CHOICES).values()]
@classmethod
def role_condition_expressions(cls) -> list:
role_choices = {role_id: role_name._proxy____args[0]
for role_id, role_name in dict(cls.ROLE_CHOICES).items()}
whens = [models.When(role=role_id, then=models.Value(role_name))
for role_id, role_name in role_choices.items()]
return whens
class UserManager(BaseUserManager): class UserManager(BaseUserManager):
@ -238,7 +271,7 @@ class User(AbstractUser):
@property @property
def reset_password_token(self): def reset_password_token(self):
"""Make a token for finish signup.""" """Make a token for finish signup."""
return password_token_generator.make_token(self) return GMTokenGenerator(purpose=GMTokenGenerator.RESET_PASSWORD).make_token(self)
@property @property
def get_user_uidb64(self): def get_user_uidb64(self):
@ -334,6 +367,22 @@ class User(AbstractUser):
model='product', model='product',
).values_list('object_id', flat=True) ).values_list('object_id', flat=True)
@property
def subscription_types(self):
result = []
for subscription in self.subscriber.all():
for item in subscription.active_subscriptions:
result.append(item.id)
return set(result)
class UserRoleQueryset(models.QuerySet):
"""QuerySet for model UserRole."""
def country_admin_role(self):
return self.filter(role__role=self.model.role.field.target_field.model.COUNTRY_ADMIN,
state=self.model.VALIDATED)
class UserRole(ProjectBaseMixin): class UserRole(ProjectBaseMixin):
"""UserRole model.""" """UserRole model."""
@ -348,6 +397,7 @@ class UserRole(ProjectBaseMixin):
(CANCELLED, _('cancelled')), (CANCELLED, _('cancelled')),
(REJECTED, _('rejected')) (REJECTED, _('rejected'))
) )
user = models.ForeignKey( user = models.ForeignKey(
'account.User', verbose_name=_('User'), on_delete=models.CASCADE) 'account.User', verbose_name=_('User'), on_delete=models.CASCADE)
role = models.ForeignKey( role = models.ForeignKey(
@ -358,9 +408,13 @@ class UserRole(ProjectBaseMixin):
state = models.CharField( state = models.CharField(
_('state'), choices=STATE_CHOICES, max_length=10, default=PENDING) _('state'), choices=STATE_CHOICES, max_length=10, default=PENDING)
requester = models.ForeignKey( requester = models.ForeignKey('account.User', on_delete=models.SET_NULL,
'account.User', blank=True, null=True, default=None, related_name='roles_requested', blank=True, null=True, default=None,
on_delete=models.SET_NULL) related_name='roles_requested',
help_text='A user (REQUESTER) who requests a '
'role change for a USER')
objects = UserRoleQueryset.as_manager()
class Meta: class Meta:
unique_together = ['user', 'role', 'establishment', 'state'] unique_together = ['user', 'role', 'establishment', 'state']
@ -371,4 +425,4 @@ class OldRole(models.Model):
old_role = models.CharField(verbose_name=_('Old role'), max_length=512, null=True) old_role = models.CharField(verbose_name=_('Old role'), max_length=512, null=True)
class Meta: class Meta:
unique_together = ('new_role', 'old_role') unique_together = ('new_role', 'old_role')

View File

@ -0,0 +1,3 @@
from account.serializers.common import *
from account.serializers.web import *
from account.serializers.back import *

View File

@ -1,19 +1,12 @@
"""Back account serializers""" """Back account serializers"""
from rest_framework import serializers from rest_framework import serializers
from account import models from account import models
from account.models import User from account.models import User
from account.serializers import RoleBaseSerializer, subscriptions_handler
from main.models import SiteSettings from main.models import SiteSettings
class RoleSerializer(serializers.ModelSerializer):
class Meta:
model = models.Role
fields = [
'role',
'country'
]
class _SiteSettingsSerializer(serializers.ModelSerializer): class _SiteSettingsSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = SiteSettings model = SiteSettings
@ -26,6 +19,15 @@ class _SiteSettingsSerializer(serializers.ModelSerializer):
class BackUserSerializer(serializers.ModelSerializer): class BackUserSerializer(serializers.ModelSerializer):
last_country = _SiteSettingsSerializer(read_only=True) last_country = _SiteSettingsSerializer(read_only=True)
roles = RoleBaseSerializer(many=True, read_only=True)
subscriptions = serializers.ListField(
source='subscription_types',
allow_null=True,
allow_empty=True,
child=serializers.IntegerField(min_value=1),
required=False,
help_text='list of subscription_types id',
)
class Meta: class Meta:
model = User model = User
@ -45,37 +47,98 @@ class BackUserSerializer(serializers.ModelSerializer):
'unconfirmed_email', 'unconfirmed_email',
'email_confirmed', 'email_confirmed',
'newsletter', 'newsletter',
'roles',
'password', 'password',
'city', 'city',
'locale', 'locale',
'last_ip', 'last_ip',
'last_country', 'last_country',
'roles',
'subscriptions',
) )
extra_kwargs = { extra_kwargs = {
'password': {'write_only': True}, 'password': {'write_only': True},
} }
read_only_fields = ('old_password', 'last_login', 'date_joined', 'city', 'locale', 'last_ip', 'last_country') read_only_fields = (
'old_password',
'last_login',
'date_joined',
'city',
'locale',
'last_ip',
'last_country',
)
def create(self, validated_data): def create(self, validated_data):
subscriptions_list = []
if 'subscription_types' in validated_data:
subscriptions_list = validated_data.pop('subscription_types')
user = super().create(validated_data) user = super().create(validated_data)
user.set_password(validated_data['password']) user.set_password(validated_data['password'])
user.save() user.save()
subscriptions_handler(subscriptions_list, user)
return user return user
class BackDetailUserSerializer(BackUserSerializer): class BackDetailUserSerializer(BackUserSerializer):
class Meta: class Meta:
model = User model = User
exclude = ('password',) fields = (
read_only_fields = ('old_password', 'last_login', 'date_joined') 'id',
'last_country',
'roles',
'last_login',
'is_superuser',
'first_name',
'last_name',
'is_staff',
'is_active',
'date_joined',
'username',
'image_url',
'cropped_image_url',
'email',
'unconfirmed_email',
'email_confirmed',
'newsletter',
'old_id',
'locale',
'city',
'last_ip',
'groups',
'user_permissions',
'subscriptions',
)
read_only_fields = (
'old_password',
'last_login',
'date_joined',
'last_ip',
'last_country',
)
def create(self, validated_data): def create(self, validated_data):
subscriptions_list = []
if 'subscription_types' in validated_data:
subscriptions_list = validated_data.pop('subscription_types')
user = super().create(validated_data) user = super().create(validated_data)
user.set_password(validated_data['password']) user.set_password(validated_data['password'])
user.save() user.save()
subscriptions_handler(subscriptions_list, user)
return user return user
def update(self, instance, validated_data):
subscriptions_list = []
if 'subscription_types' in validated_data:
subscriptions_list = validated_data.pop('subscription_types')
instance = super().update(instance, validated_data)
subscriptions_handler(subscriptions_list, instance)
return instance
class UserRoleSerializer(serializers.ModelSerializer): class UserRoleSerializer(serializers.ModelSerializer):
class Meta: class Meta:
@ -85,3 +148,9 @@ class UserRoleSerializer(serializers.ModelSerializer):
'user', 'user',
'establishment' 'establishment'
] ]
class RoleTabRetrieveSerializer(serializers.Serializer):
"""Serializer for BackOffice role tab."""
role_name = serializers.CharField()
role_counter = serializers.IntegerField()

View File

@ -1,18 +1,59 @@
"""Common account serializers""" """Common account serializers"""
from django.conf import settings from django.conf import settings
from django.utils.translation import gettext_lazy as _
from django.contrib.auth import password_validation as password_validators from django.contrib.auth import password_validation as password_validators
from django.utils.translation import gettext_lazy as _
from fcm_django.models import FCMDevice from fcm_django.models import FCMDevice
from rest_framework import exceptions from rest_framework import exceptions
from rest_framework import serializers from rest_framework import serializers
from rest_framework import validators as rest_validators from rest_framework import validators as rest_validators
from account import models, tasks from account import models, tasks
from main.serializers.common import NavigationBarPermissionBaseSerializer
from notification.models import Subscribe, Subscriber
from utils import exceptions as utils_exceptions from utils import exceptions as utils_exceptions
from utils import methods as utils_methods from utils import methods as utils_methods
from utils.methods import generate_string_code
def subscriptions_handler(subscriptions_list, user):
"""
create or update subscriptions for user
"""
Subscribe.objects.filter(subscriber__user=user).delete()
subscriber, _ = Subscriber.objects.get_or_create(
email=user.email,
defaults={
'user': user,
'email': user.email,
'ip_address': user.last_ip,
'country_code': user.last_country.country.code if user.last_country else None,
'locale': user.locale,
'update_code': generate_string_code(),
}
)
for subscription in subscriptions_list:
Subscribe.objects.create(
subscriber=subscriber,
subscription_type_id=subscription,
)
class RoleBaseSerializer(serializers.ModelSerializer):
"""Serializer for model Role."""
role_display = serializers.CharField(source='get_role_display', read_only=True)
navigation_bar_permission = NavigationBarPermissionBaseSerializer(read_only=True)
class Meta:
"""Meta class."""
model = models.Role
fields = [
'id',
'role_display',
'navigation_bar_permission',
]
# User serializers
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
"""User serializer.""" """User serializer."""
# RESPONSE # RESPONSE
@ -25,6 +66,15 @@ class UserSerializer(serializers.ModelSerializer):
email = serializers.EmailField( email = serializers.EmailField(
validators=(rest_validators.UniqueValidator(queryset=models.User.objects.all()),), validators=(rest_validators.UniqueValidator(queryset=models.User.objects.all()),),
required=False) required=False)
roles = RoleBaseSerializer(many=True, read_only=True)
subscriptions = serializers.ListField(
source='subscription_types',
allow_null=True,
allow_empty=True,
child=serializers.IntegerField(min_value=1),
required=False,
help_text='list of subscription_types id',
)
class Meta: class Meta:
model = models.User model = models.User
@ -38,6 +88,8 @@ class UserSerializer(serializers.ModelSerializer):
'email', 'email',
'email_confirmed', 'email_confirmed',
'newsletter', 'newsletter',
'roles',
'subscriptions',
] ]
extra_kwargs = { extra_kwargs = {
'first_name': {'required': False, 'write_only': True, }, 'first_name': {'required': False, 'write_only': True, },
@ -49,8 +101,14 @@ class UserSerializer(serializers.ModelSerializer):
} }
def create(self, validated_data): def create(self, validated_data):
subscriptions_list = []
if 'subscription_types' in validated_data:
subscriptions_list = validated_data.pop('subscription_types')
user = super(UserSerializer, self).create(validated_data) user = super(UserSerializer, self).create(validated_data)
validated_data['user'] = user validated_data['user'] = user
Subscriber.objects.make_subscriber(**validated_data)
subscriptions_handler(subscriptions_list, user)
return user return user
def validate_email(self, value): def validate_email(self, value):
@ -68,6 +126,10 @@ class UserSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data): def update(self, instance, validated_data):
"""Override update method""" """Override update method"""
subscriptions_list = []
if 'subscription_types' in validated_data:
subscriptions_list = validated_data.pop('subscription_types')
old_email = instance.email old_email = instance.email
instance = super().update(instance, validated_data) instance = super().update(instance, validated_data)
if 'email' in validated_data: if 'email' in validated_data:
@ -80,12 +142,14 @@ class UserSerializer(serializers.ModelSerializer):
tasks.change_email_address.delay( tasks.change_email_address.delay(
user_id=instance.id, user_id=instance.id,
country_code=self.context.get('request').country_code, country_code=self.context.get('request').country_code,
emails=[validated_data['email'],]) emails=[validated_data['email'], ])
else: else:
tasks.change_email_address( tasks.change_email_address(
user_id=instance.id, user_id=instance.id,
country_code=self.context.get('request').country_code, country_code=self.context.get('request').country_code,
emails=[validated_data['email'],]) emails=[validated_data['email'], ])
subscriptions_handler(subscriptions_list, instance)
return instance return instance
@ -212,6 +276,7 @@ class ChangeEmailSerializer(serializers.ModelSerializer):
# Firebase Cloud Messaging serializers # Firebase Cloud Messaging serializers
class FCMDeviceSerializer(serializers.ModelSerializer): class FCMDeviceSerializer(serializers.ModelSerializer):
"""FCM Device model serializer""" """FCM Device model serializer"""
class Meta: class Meta:
model = FCMDevice model = FCMDevice
fields = ('id', 'name', 'registration_id', 'device_id', fields = ('id', 'name', 'registration_id', 'device_id',
@ -231,8 +296,8 @@ class FCMDeviceSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(FCMDeviceSerializer, self).__init__(*args, **kwargs) super(FCMDeviceSerializer, self).__init__(*args, **kwargs)
self.fields['type'].help_text = ( self.fields['type'].help_text = (
'Should be one of ["%s"]' % 'Should be one of ["%s"]' %
'", "'.join([i for i in self.fields['type'].choices])) '", "'.join([i for i in self.fields['type'].choices]))
def create(self, validated_data): def create(self, validated_data):
user = self.context['request'].user user = self.context['request'].user

View File

@ -6,9 +6,10 @@ from account.views import back as views
app_name = 'account' app_name = 'account'
urlpatterns = [ urlpatterns = [
path('role/', views.RoleLstView.as_view(), name='role-list-create'), path('role/', views.RoleListView.as_view(), name='role-list-create'),
path('user-role/', views.UserRoleLstView.as_view(), name='user-role-list-create'), path('role-tab/', views.RoleTabRetrieveView.as_view(), name='role-tab'),
path('user/', views.UserLstView.as_view(), name='user-create-list'), path('user-role/', views.UserRoleListView.as_view(), name='user-role-list-create'),
path('user/', views.UserListView.as_view(), name='user-create-list'),
path('user/<int:id>/', views.UserRUDView.as_view(), name='user-rud'), path('user/<int:id>/', views.UserRUDView.as_view(), name='user-rud'),
path('user/<int:id>/csv/', views.get_user_csv, name='user-csv'), path('user/<int:id>/csv/', views.get_user_csv, name='user-csv'),
] ]

View File

@ -1,45 +1,71 @@
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics, permissions from rest_framework import generics, permissions, status
from rest_framework.response import Response
from rest_framework.filters import OrderingFilter from rest_framework.filters import OrderingFilter
import csv import csv
from django.http import HttpResponse, HttpResponseNotFound from django.http import HttpResponse, HttpResponseNotFound
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from account import models from account import models, filters
from account.models import User from account.models import User
from account.serializers import back as serializers from account.serializers import back as serializers
from account.serializers.common import RoleBaseSerializer
class RoleLstView(generics.ListCreateAPIView): class RoleListView(generics.ListCreateAPIView):
serializer_class = serializers.RoleSerializer serializer_class = RoleBaseSerializer
queryset = models.Role.objects.all() queryset = models.Role.objects.all()
class UserRoleLstView(generics.ListCreateAPIView): class RoleTabRetrieveView(generics.GenericAPIView):
permission_classes = [permissions.IsAdminUser]
def get_queryset(self):
"""Overridden get_queryset method."""
additional_filters = {}
if (self.request.user.userrole_set.country_admin_role().exists() and
hasattr(self.request, 'country_code')):
additional_filters.update({'country__code': self.request.country_code})
return models.Role.objects.filter(**additional_filters)\
.annotate_role_name()\
.values('role_name')\
.annotate_role_counter()\
.values('role_name', 'role_counter')
def get(self, request, *args, **kwargs):
"""Implement GET-method"""
data = list(self.get_queryset())
# todo: Need refactoring. Extend data list with non-existed role.
for role in models.Role.role_names():
if role not in [role.get('role_name') for role in data]:
data.append({'role_name': role, 'role_number': 0})
return Response(data, status=status.HTTP_200_OK)
class UserRoleListView(generics.ListCreateAPIView):
serializer_class = serializers.UserRoleSerializer serializer_class = serializers.UserRoleSerializer
queryset = models.UserRole.objects.all() queryset = models.UserRole.objects.all()
class UserLstView(generics.ListCreateAPIView): class UserListView(generics.ListCreateAPIView):
"""User list create view.""" """User list create view."""
queryset = User.objects.prefetch_related('roles') queryset = User.objects.prefetch_related('roles')
serializer_class = serializers.BackUserSerializer serializer_class = serializers.BackUserSerializer
permission_classes = (permissions.IsAdminUser,) permission_classes = (permissions.IsAdminUser,)
filter_backends = (DjangoFilterBackend, OrderingFilter) filter_class = filters.AccountBackOfficeFilter
filterset_fields = ( filter_backends = (OrderingFilter, DjangoFilterBackend)
'email_confirmed',
'is_staff',
'is_active',
'is_superuser',
'roles',
)
ordering_fields = ( ordering_fields = (
'email_confirmed', 'email_confirmed',
'is_staff', 'is_staff',
'is_active', 'is_active',
'is_superuser', 'is_superuser',
'roles', 'last_login',
'last_login' 'date_joined',
) )

View File

@ -1,6 +1,5 @@
"""Web account views""" """Web account views"""
from django.conf import settings from django.conf import settings
from django.contrib.auth.tokens import default_token_generator as password_token_generator
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.http import urlsafe_base64_decode from django.utils.http import urlsafe_base64_decode
@ -10,6 +9,7 @@ from rest_framework.response import Response
from account import tasks, models from account import tasks, models
from account.serializers import web as serializers from account.serializers import web as serializers
from utils import exceptions as utils_exceptions from utils import exceptions as utils_exceptions
from utils.models import GMTokenGenerator
from utils.views import JWTGenericViewMixin from utils.views import JWTGenericViewMixin
@ -40,22 +40,23 @@ class PasswordResetConfirmView(JWTGenericViewMixin, generics.GenericAPIView):
queryset = models.User.objects.active() queryset = models.User.objects.active()
def get_object(self): def get_object(self):
"""Override get_object method""" """Overridden get_object method"""
queryset = self.filter_queryset(self.get_queryset()) queryset = self.filter_queryset(self.get_queryset())
uidb64 = self.kwargs.get('uidb64') uidb64 = self.kwargs.get('uidb64')
user_id = force_text(urlsafe_base64_decode(uidb64)) user_id = force_text(urlsafe_base64_decode(uidb64))
token = self.kwargs.get('token') token = self.kwargs.get('token')
obj = get_object_or_404(queryset, id=user_id) user = get_object_or_404(queryset, id=user_id)
if not password_token_generator.check_token(user=obj, token=token): if not GMTokenGenerator(GMTokenGenerator.RESET_PASSWORD).check_token(
user, token):
raise utils_exceptions.NotValidTokenError() raise utils_exceptions.NotValidTokenError()
# May raise a permission denied # May raise a permission denied
self.check_object_permissions(self.request, obj) self.check_object_permissions(self.request, user)
return obj return user
def patch(self, request, *args, **kwargs): def patch(self, request, *args, **kwargs):
"""Implement PATCH method""" """Implement PATCH method"""

View File

@ -20,6 +20,7 @@ from utils.tokens import GMRefreshToken
# Serializers # Serializers
class SignupSerializer(serializers.ModelSerializer): class SignupSerializer(serializers.ModelSerializer):
"""Signup serializer serializer mixin""" """Signup serializer serializer mixin"""
class Meta: class Meta:
model = account_models.User model = account_models.User
fields = ( fields = (
@ -37,11 +38,13 @@ class SignupSerializer(serializers.ModelSerializer):
def validate_username(self, value): def validate_username(self, value):
"""Custom username validation""" """Custom username validation"""
valid = utils_methods.username_validator(username=value) if value:
if not valid: valid = utils_methods.username_validator(username=value)
raise utils_exceptions.NotValidUsernameError() if not valid:
if account_models.User.objects.filter(username__iexact=value).exists(): raise utils_exceptions.NotValidUsernameError()
raise serializers.ValidationError() if account_models.User.objects.filter(username__iexact=value).exists():
raise serializers.ValidationError()
return value return value
def validate_email(self, value): def validate_email(self, value):
@ -63,6 +66,13 @@ class SignupSerializer(serializers.ModelSerializer):
request = self.context.get('request') request = self.context.get('request')
"""Overridden create method""" """Overridden create method"""
username = validated_data.get('username')
if not username:
username = utils_methods.username_random()
while account_models.User.objects.filter(username__iexact=username).exists():
username = utils_methods.username_random()
obj = account_models.User.objects.make( obj = account_models.User.objects.make(
username=validated_data.get('username'), username=validated_data.get('username'),
password=validated_data.get('password'), password=validated_data.get('password'),

View File

@ -0,0 +1,36 @@
from django.conf import settings
from django.core.management.base import BaseCommand
from django.db import transaction
from sorl.thumbnail import get_thumbnail
from collection.models import Collection
from utils.methods import image_url_valid, get_image_meta_by_url
class Command(BaseCommand):
SORL_THUMBNAIL_ALIAS = 'collection_image'
def handle(self, *args, **options):
max_size = 1048576
with transaction.atomic():
for collection in Collection.objects.all():
if not image_url_valid(collection.image_url):
continue
size, width, height = get_image_meta_by_url(collection.image_url)
if size < max_size:
self.stdout.write(self.style.SUCCESS(f'No need to compress images size is {size / (2 ** 20)}Mb\n'))
continue
percents = round(max_size / (size * 0.01))
width = round(width * percents / 100)
height = round(height * percents / 100)
collection.image_url = get_thumbnail(
file_=collection.image_url,
geometry_string=f'{width}x{height}',
upscale=False
).url
collection.save()

View File

@ -165,7 +165,7 @@ class GuideQuerySet(models.QuerySet):
def with_base_related(self): def with_base_related(self):
"""Return QuerySet with related.""" """Return QuerySet with related."""
return self.select_related('guide_type', 'site') return self.select_related('site', )
def by_country_id(self, country_id): def by_country_id(self, country_id):
"""Return QuerySet filtered by country code.""" """Return QuerySet filtered by country code."""

View File

@ -65,7 +65,8 @@ class CollectionEstablishmentListView(CollectionListView):
# May raise a permission denied # May raise a permission denied
self.check_object_permissions(self.request, collection) self.check_object_permissions(self.request, collection)
return collection.establishments.published().annotate_in_favorites(self.request.user) return collection.establishments.published().annotate_in_favorites(self.request.user) \
.with_base_related().with_extended_related()
# Guide # Guide

View File

@ -0,0 +1,18 @@
from django.core.management.base import BaseCommand
from comment.models import Comment
from tqdm import tqdm
class Command(BaseCommand):
help = 'Add status to comments from is_publish_ flag.'
def handle(self, *args, **kwargs):
to_update = []
for comment in tqdm(Comment.objects.all()):
if hasattr(comment, 'is_publish') and hasattr(comment, 'status'):
comment.status = Comment.PUBLISHED if comment.is_publish else Comment.WAITING
to_update.append(comment)
Comment.objects.bulk_update(to_update, ('status', ))
self.stdout.write(self.style.WARNING(f'Updated {len(to_update)} objects.'))

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2020-01-15 13:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('comment', '0007_comment_site'),
]
operations = [
migrations.AddField(
model_name='comment',
name='status',
field=models.PositiveSmallIntegerField(choices=[(0, 'Waiting'), (1, 'Published'), (2, 'Rejected'), (3, 'Deleted')], default=0, verbose_name='status'),
),
]

View File

@ -16,6 +16,10 @@ class CommentQuerySet(ContentTypeQuerySetMixin):
"""Return comments by author""" """Return comments by author"""
return self.filter(user=user) return self.filter(user=user)
def published(self):
"""Return only published comments."""
return self.filter(status=self.model.PUBLISHED)
def annotate_is_mine_status(self, user): def annotate_is_mine_status(self, user):
"""Annotate belonging status""" """Annotate belonging status"""
return self.annotate(is_mine=models.Case( return self.annotate(is_mine=models.Case(
@ -27,16 +31,48 @@ class CommentQuerySet(ContentTypeQuerySetMixin):
output_field=models.BooleanField() output_field=models.BooleanField()
)) ))
def public(self, user):
"""
Return QuerySet by rules:
1 With status PUBLISHED
2 With status WAITING if comment author is <user>
"""
qs = self.published()
if isinstance(user, User):
waiting_ids = list(
self.filter(status=self.model.WAITING, user=user)
.values_list('id', flat=True))
published_ids = list(qs.values_list('id', flat=True))
waiting_ids.extend(published_ids)
qs = self.filter(id__in=tuple(waiting_ids))
return qs
class Comment(ProjectBaseMixin): class Comment(ProjectBaseMixin):
"""Comment model.""" """Comment model."""
WAITING = 0
PUBLISHED = 1
REJECTED = 2
DELETED = 3
STATUSES = (
(WAITING, _('Waiting')),
(PUBLISHED, _('Published')),
(REJECTED, _('Rejected')),
(DELETED, _('Deleted')),
)
text = models.TextField(verbose_name=_('Comment text')) text = models.TextField(verbose_name=_('Comment text'))
mark = models.PositiveIntegerField(blank=True, null=True, default=None, verbose_name=_('Mark')) mark = models.PositiveIntegerField(blank=True, null=True, default=None, verbose_name=_('Mark'))
user = models.ForeignKey('account.User', related_name='comments', on_delete=models.CASCADE, verbose_name=_('User')) user = models.ForeignKey('account.User', related_name='comments', on_delete=models.CASCADE, verbose_name=_('User'))
old_id = models.IntegerField(null=True, blank=True, default=None) old_id = models.IntegerField(null=True, blank=True, default=None)
is_publish = models.BooleanField(default=False, verbose_name=_('Publish status')) is_publish = models.BooleanField(default=False, verbose_name=_('Publish status'))
status = models.PositiveSmallIntegerField(choices=STATUSES,
default=WAITING,
verbose_name=_('status'))
site = models.ForeignKey('main.SiteSettings', blank=True, null=True, site = models.ForeignKey('main.SiteSettings', blank=True, null=True,
on_delete=models.SET_NULL, verbose_name=_('site')) on_delete=models.SET_NULL, verbose_name=_('site'))
content_type = models.ForeignKey(generic.ContentType, on_delete=models.CASCADE) content_type = models.ForeignKey(generic.ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField() object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey('content_type', 'object_id') content_object = generic.GenericForeignKey('content_type', 'object_id')

View File

@ -1,4 +1,4 @@
from .common import *
from .mobile import * from .mobile import *
from .back import * from .back import *
from .web import * from .web import *
from .common import *

View File

@ -1,9 +1 @@
"""Comment app common serializers.""" """Comment app common serializers."""
from comment import models
from rest_framework import serializers
class CommentBaseSerializer(serializers.ModelSerializer):
class Meta:
model = models.Comment
fields = ('id', 'text', 'mark', 'user', 'object_id', 'content_type')

View File

@ -4,13 +4,16 @@ from rest_framework import serializers
from comment.models import Comment from comment.models import Comment
class CommentSerializer(serializers.ModelSerializer): class CommentBaseSerializer(serializers.ModelSerializer):
"""Comment serializer""" """Comment serializer"""
nickname = serializers.CharField(read_only=True, nickname = serializers.CharField(read_only=True,
source='user.username') source='user.username')
is_mine = serializers.BooleanField(read_only=True) is_mine = serializers.BooleanField(read_only=True)
profile_pic = serializers.URLField(read_only=True, profile_pic = serializers.URLField(read_only=True,
source='user.cropped_image_url') source='user.cropped_image_url')
status_display = serializers.CharField(read_only=True,
source='get_status_display')
last_ip = serializers.IPAddressField(read_only=True, source='user.last_ip')
class Meta: class Meta:
"""Serializer for model Comment""" """Serializer for model Comment"""
@ -23,19 +26,11 @@ class CommentSerializer(serializers.ModelSerializer):
'text', 'text',
'mark', 'mark',
'nickname', 'nickname',
'profile_pic'
]
class CommentRUDSerializer(CommentSerializer):
"""Retrieve/Update/Destroy comment serializer."""
class Meta(CommentSerializer.Meta):
"""Meta class."""
fields = [
'id',
'created',
'text',
'mark',
'nickname',
'profile_pic', 'profile_pic',
'status',
'status_display',
'last_ip',
] ]
extra_kwargs = {
'status': {'read_only': True},
}

View File

@ -20,6 +20,7 @@ def transfer_comments():
'mark', 'mark',
'establishment_id', 'establishment_id',
'account_id', 'account_id',
'state',
) )
serialized_data = CommentSerializer(data=list(queryset.values()), many=True) serialized_data = CommentSerializer(data=list(queryset.values()), many=True)

View File

@ -1,19 +1,19 @@
from rest_framework import generics, permissions from rest_framework import generics, permissions
from comment.serializers import back as serializers from comment.serializers import CommentBaseSerializer
from comment import models from comment import models
from utils.permissions import IsCommentModerator, IsCountryAdmin from utils.permissions import IsCommentModerator, IsCountryAdmin
class CommentLstView(generics.ListCreateAPIView): class CommentLstView(generics.ListCreateAPIView):
"""Comment list create view.""" """Comment list create view."""
serializer_class = serializers.CommentBaseSerializer serializer_class = CommentBaseSerializer
queryset = models.Comment.objects.all() queryset = models.Comment.objects.all()
# permission_classes = [permissions.IsAuthenticatedOrReadOnly| IsCommentModerator|IsCountryAdmin] # permission_classes = [permissions.IsAuthenticatedOrReadOnly| IsCommentModerator|IsCountryAdmin]
class CommentRUDView(generics.RetrieveUpdateDestroyAPIView): class CommentRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Comment RUD view.""" """Comment RUD view."""
serializer_class = serializers.CommentBaseSerializer serializer_class = CommentBaseSerializer
queryset = models.Comment.objects.all() queryset = models.Comment.objects.all()
permission_classes = [IsCommentModerator] permission_classes = [IsCommentModerator]
# permission_classes = [IsCountryAdmin | IsCommentModerator] # permission_classes = [IsCountryAdmin | IsCommentModerator]

View File

@ -1,6 +1,8 @@
"""Establishment app filters.""" """Establishment app filters."""
from django.core.validators import EMPTY_VALUES from django.core.validators import EMPTY_VALUES
from django.utils.translation import ugettext_lazy as _
from django_filters import rest_framework as filters from django_filters import rest_framework as filters
from rest_framework.serializers import ValidationError
from establishment import models from establishment import models
@ -8,8 +10,8 @@ from establishment import models
class EstablishmentFilter(filters.FilterSet): class EstablishmentFilter(filters.FilterSet):
"""Establishment filter set.""" """Establishment filter set."""
tag_id = filters.NumberFilter(field_name='tags__metadata__id',) tag_id = filters.NumberFilter(field_name='tags__metadata__id', )
award_id = filters.NumberFilter(field_name='awards__id',) award_id = filters.NumberFilter(field_name='awards__id', )
search = filters.CharFilter(method='search_text') search = filters.CharFilter(method='search_text')
type = filters.CharFilter(method='by_type') type = filters.CharFilter(method='by_type')
subtype = filters.CharFilter(method='by_subtype') subtype = filters.CharFilter(method='by_subtype')
@ -65,6 +67,10 @@ class EmployeeBackFilter(filters.FilterSet):
"""Employee filter set.""" """Employee filter set."""
search = filters.CharFilter(method='search_by_name_or_last_name') search = filters.CharFilter(method='search_by_name_or_last_name')
position_id = filters.NumberFilter(method='search_by_actual_position_id')
public_mark = filters.NumberFilter(method='search_by_public_mark')
toque_number = filters.NumberFilter(method='search_by_toque_number')
username = filters.CharFilter(method='search_by_username_or_name')
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -72,10 +78,47 @@ class EmployeeBackFilter(filters.FilterSet):
model = models.Employee model = models.Employee
fields = ( fields = (
'search', 'search',
'position_id',
'public_mark',
'toque_number',
'username',
) )
def search_by_name_or_last_name(self, queryset, name, value): def search_by_name_or_last_name(self, queryset, name, value):
"""Search by name or last name.""" """Search by name or last name."""
if value not in EMPTY_VALUES: if value not in EMPTY_VALUES:
return queryset.search_by_name_or_last_name(value) return queryset.trigram_search(value)
return queryset
def search_by_actual_position_id(self, queryset, name, value):
"""Search by actual position_id."""
if value not in EMPTY_VALUES:
return queryset.search_by_position_id(value)
return queryset
def search_by_public_mark(self, queryset, name, value):
"""Search by establishment public_mark."""
if value not in EMPTY_VALUES:
return queryset.search_by_public_mark(value)
return queryset
def search_by_toque_number(self, queryset, name, value):
"""Search by establishment toque_number."""
if value not in EMPTY_VALUES:
return queryset.search_by_toque_number(value)
return queryset
def search_by_username_or_name(self, queryset, name, value):
"""Search by username or name."""
if value not in EMPTY_VALUES:
return queryset.search_by_username_or_name(value)
return queryset
class EmployeeBackSearchFilter(EmployeeBackFilter):
def search_by_name_or_last_name(self, queryset, name, value):
if value not in EMPTY_VALUES:
if len(value) < 3:
raise ValidationError({'detail': _('Type at least 3 characters to search please.')})
return queryset.trigram_search(value)
return queryset return queryset

View File

@ -0,0 +1,35 @@
import os
from django.conf import settings
from django.core.management.base import BaseCommand
from django.db import transaction
from sorl.thumbnail import get_thumbnail
from establishment.models import Establishment
from utils.methods import image_url_valid, get_image_meta_by_url
class Command(BaseCommand):
SORL_THUMBNAIL_ALIAS = 'establishment_collection_image'
def handle(self, *args, **options):
with transaction.atomic():
for establishment in Establishment.objects.all():
if establishment.preview_image_url is None \
or not image_url_valid(establishment.preview_image_url):
continue
_, width, height = get_image_meta_by_url(establishment.preview_image_url)
sorl_settings = settings.SORL_THUMBNAIL_ALIASES[self.SORL_THUMBNAIL_ALIAS]
sorl_width_height = sorl_settings['geometry_string'].split('x')
if int(sorl_width_height[0]) > width or int(sorl_width_height[1]) > height:
_, file_ext = os.path.splitext(establishment.preview_image_url)
self.stdout.write(self.style.SUCCESS(f'Optimize: {establishment.preview_image_url}, extension: {file_ext}'))
establishment.preview_image_url = get_thumbnail(
file_=establishment.preview_image_url,
**sorl_settings,
format='JPEG' if file_ext[1:] == 'jpg' else 'PNG'
).url
establishment.save()

View File

@ -0,0 +1,47 @@
import math
from django.core.management.base import BaseCommand
from celery import group
from establishment.models import Establishment
from establishment.tasks import update_establishment_image_urls
class Command(BaseCommand):
help = """
Updating image links for establishments.
Run command ./manage.py update_establishment_image_urls
"""
def add_arguments(self, parser):
parser.add_argument(
'--bucket_size',
type=int,
default=128,
help='Size of one basket to update'
)
def handle(self, *args, **kwargs):
bucket_size = kwargs.get('bucket_size', 128)
objects = Establishment.objects.all()
objects_size = objects.count()
summary_tasks = math.ceil(objects_size / bucket_size)
tasks = []
for index in range(0, objects_size, bucket_size):
bucket = objects[index: index + bucket_size]
task = update_establishment_image_urls.s(
(index + bucket_size) / bucket_size, summary_tasks,
list(bucket.values_list('id', flat=True))
)
tasks.append(task)
self.stdout.write(self.style.WARNING(f'Created all celery update tasks.\n'))
job = group(*tasks)
job.delay()
self.stdout.write(self.style.SUCCESS(f'Done all celery update tasks.\n'))

View File

@ -0,0 +1,16 @@
# Generated by Django 2.2.7 on 2020-01-15 17:02
from django.db import migrations
from django.contrib.postgres.operations import TrigramExtension, BtreeGinExtension
class Migration(migrations.Migration):
dependencies = [
('establishment', '0071_auto_20200110_1055'),
]
operations = [
TrigramExtension(),
BtreeGinExtension(),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 2.2.7 on 2020-01-15 17:10
import django.contrib.postgres.indexes
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('establishment', '0072_auto_20200115_1702'),
]
operations = [
migrations.AddIndex(
model_name='employee',
index=django.contrib.postgres.indexes.GinIndex(fields=['name'], name='establishme_name_39fda6_gin'),
),
migrations.AddIndex(
model_name='employee',
index=django.contrib.postgres.indexes.GinIndex(fields=['last_name'], name='establishme_last_na_3c53de_gin'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2020-01-17 08:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0073_auto_20200115_1710'),
]
operations = [
migrations.AddField(
model_name='employee',
name='available_for_events',
field=models.BooleanField(default=False, verbose_name='Available for events'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 2.2.7 on 2020-01-17 10:50
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('gallery', '0008_merge_20191212_0752'),
('establishment', '0074_employee_available_for_events'),
]
operations = [
migrations.AddField(
model_name='employee',
name='photo',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='employee_photo', to='gallery.Image', verbose_name='image instance of model Image'),
),
]

View File

@ -11,10 +11,12 @@ from django.contrib.gis.db.models.functions import Distance
from django.contrib.gis.geos import Point from django.contrib.gis.geos import Point
from django.contrib.gis.measure import Distance as DistanceMeasure from django.contrib.gis.measure import Distance as DistanceMeasure
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
from django.contrib.postgres.search import TrigramDistance, TrigramSimilarity
from django.contrib.postgres.indexes import GinIndex
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models from django.db import models
from django.db.models import When, Case, F, ExpressionWrapper, Subquery, Q, Prefetch from django.db.models import When, Case, F, ExpressionWrapper, Subquery, Q, Prefetch, Sum
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField from phonenumber_field.modelfields import PhoneNumberField
@ -58,8 +60,6 @@ class EstablishmentType(TypeDefaultImageMixin, TranslatedFieldsMixin, ProjectBas
blank=True, null=True, default=None, blank=True, null=True, default=None,
verbose_name='default image') verbose_name='default image')
chosen_tags = generic.GenericRelation(to='tag.ChosenTag')
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -122,7 +122,7 @@ class EstablishmentQuerySet(models.QuerySet):
def with_base_related(self): def with_base_related(self):
"""Return qs with related objects.""" """Return qs with related objects."""
return self.select_related('address', 'establishment_type'). \ return self.select_related('address', 'establishment_type'). \
prefetch_related('tags', 'tags__translation') prefetch_related('tags', 'tags__translation').with_main_image()
def with_schedule(self): def with_schedule(self):
"""Return qs with related schedule.""" """Return qs with related schedule."""
@ -266,16 +266,16 @@ class EstablishmentQuerySet(models.QuerySet):
2 With ordering by distance. 2 With ordering by distance.
""" """
qs = self.similar_base(establishment) \ qs = self.similar_base(establishment) \
.filter(**filters) .filter(**filters)
if establishment.address and establishment.address.coordinates: if establishment.address and establishment.address.coordinates:
return Subquery( return Subquery(
qs.annotate_distance(point=establishment.location) qs.annotate_distance(point=establishment.location)
.order_by('distance') .order_by('distance')
.distinct() .distinct()
.values_list('id', flat=True)[:settings.LIMITING_QUERY_OBJECTS] .values_list('id', flat=True)[:settings.LIMITING_QUERY_OBJECTS]
) )
return Subquery( return Subquery(
qs.values_list('id', flat=True)[:settings.LIMITING_QUERY_OBJECTS] qs.values_list('id', flat=True)[:settings.LIMITING_QUERY_OBJECTS]
) )
def similar_restaurants(self, restaurant): def similar_restaurants(self, restaurant):
@ -293,12 +293,12 @@ class EstablishmentQuerySet(models.QuerySet):
} }
) )
return self.filter(id__in=ids_by_subquery.queryset) \ return self.filter(id__in=ids_by_subquery.queryset) \
.annotate_intermediate_public_mark() \ .annotate_intermediate_public_mark() \
.annotate_mark_similarity(mark=restaurant.public_mark) \ .annotate_mark_similarity(mark=restaurant.public_mark) \
.order_by('mark_similarity') \ .order_by('mark_similarity') \
.distinct('mark_similarity', 'id') .distinct('mark_similarity', 'id')
def same_subtype(self, establishment): def annotate_same_subtype(self, establishment):
"""Annotate flag same subtype.""" """Annotate flag same subtype."""
return self.annotate(same_subtype=Case( return self.annotate(same_subtype=Case(
models.When( models.When(
@ -314,7 +314,7 @@ class EstablishmentQuerySet(models.QuerySet):
Return QuerySet with objects that similar to Artisan/Producer(s). Return QuerySet with objects that similar to Artisan/Producer(s).
:param establishment: Establishment instance :param establishment: Establishment instance
""" """
base_qs = self.similar_base(establishment).same_subtype(establishment) base_qs = self.similar_base(establishment).annotate_same_subtype(establishment)
similarity_rules = { similarity_rules = {
'ordering': [F('same_subtype').desc(), ], 'ordering': [F('same_subtype').desc(), ],
'distinctions': ['same_subtype', ] 'distinctions': ['same_subtype', ]
@ -325,8 +325,8 @@ class EstablishmentQuerySet(models.QuerySet):
similarity_rules['distinctions'].append('distance') similarity_rules['distinctions'].append('distance')
return base_qs.has_published_reviews() \ return base_qs.has_published_reviews() \
.order_by(*similarity_rules['ordering']) \ .order_by(*similarity_rules['ordering']) \
.distinct(*similarity_rules['distinctions'], 'id') .distinct(*similarity_rules['distinctions'], 'id')
def by_wine_region(self, wine_region): def by_wine_region(self, wine_region):
""" """
@ -360,17 +360,19 @@ class EstablishmentQuerySet(models.QuerySet):
similarity_rules['distinctions'].append('distance') similarity_rules['distinctions'].append('distance')
return base_qs.order_by(*similarity_rules['ordering']) \ return base_qs.order_by(*similarity_rules['ordering']) \
.distinct(*similarity_rules['distinctions'], 'id') .distinct(*similarity_rules['distinctions'], 'id')
def similar_distilleries(self, distillery): def similar_distilleries(self, distillery):
""" """
Return QuerySet with objects that similar to Distillery. Return QuerySet with objects that similar to Distillery.
:param distillery: Establishment instance :param distillery: Establishment instance
""" """
base_qs = self.similar_base(distillery).same_subtype(distillery) base_qs = self.similar_base(distillery).annotate_same_subtype(distillery).filter(
same_subtype=True
)
similarity_rules = { similarity_rules = {
'ordering': [F('same_subtype').desc(), ], 'ordering': [],
'distinctions': ['same_subtype', ] 'distinctions': []
} }
if distillery.address and distillery.address.coordinates: if distillery.address and distillery.address.coordinates:
base_qs = base_qs.annotate_distance(point=distillery.location) base_qs = base_qs.annotate_distance(point=distillery.location)
@ -378,27 +380,29 @@ class EstablishmentQuerySet(models.QuerySet):
similarity_rules['distinctions'].append('distance') similarity_rules['distinctions'].append('distance')
return base_qs.published() \ return base_qs.published() \
.has_published_reviews() \ .has_published_reviews() \
.order_by(*similarity_rules['ordering']) \ .order_by(*similarity_rules['ordering']) \
.distinct(*similarity_rules['distinctions'], 'id') .distinct(*similarity_rules['distinctions'], 'id')
def similar_food_producers(self, distillery): def similar_food_producers(self, food_producer):
""" """
Return QuerySet with objects that similar to Food Producer. Return QuerySet with objects that similar to Food Producer.
:param distillery: Establishment instance :param food_producer: Establishment instance
""" """
base_qs = self.similar_base(distillery).same_subtype(distillery) base_qs = self.similar_base(food_producer).annotate_same_subtype(food_producer).filter(
same_subtype=True
)
similarity_rules = { similarity_rules = {
'ordering': [F('same_subtype').desc(), ], 'ordering': [],
'distinctions': ['same_subtype', ] 'distinctions': []
} }
if distillery.address and distillery.address.coordinates: if food_producer.address and food_producer.address.coordinates:
base_qs = base_qs.annotate_distance(point=distillery.location) base_qs = base_qs.annotate_distance(point=food_producer.location)
similarity_rules['ordering'].append(F('distance').asc()) similarity_rules['ordering'].append(F('distance').asc())
similarity_rules['distinctions'].append('distance') similarity_rules['distinctions'].append('distance')
return base_qs.order_by(*similarity_rules['ordering']) \ return base_qs.order_by(*similarity_rules['ordering']) \
.distinct(*similarity_rules['distinctions'], 'id') .distinct(*similarity_rules['distinctions'], 'id')
def last_reviewed(self, point: Point): def last_reviewed(self, point: Point):
""" """
@ -502,6 +506,13 @@ class EstablishmentQuerySet(models.QuerySet):
to_attr=attr_name) to_attr=attr_name)
) )
def with_main_image(self):
return self.prefetch_related(
models.Prefetch('establishment_gallery',
queryset=EstablishmentGallery.objects.main_image(),
to_attr='main_image')
)
class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin, class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
TranslatedFieldsMixin, HasTagsMixin, FavoritesMixin): TranslatedFieldsMixin, HasTagsMixin, FavoritesMixin):
@ -649,7 +660,6 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
country = self.address.city.country country = self.address.city.country
return country.low_price, country.high_price return country.low_price, country.high_price
def set_establishment_type(self, establishment_type): def set_establishment_type(self, establishment_type):
self.establishment_type = establishment_type self.establishment_type = establishment_type
self.establishment_subtypes.exclude( self.establishment_subtypes.exclude(
@ -769,10 +779,12 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
return self.products.wines() return self.products.wines()
@property @property
def main_image(self): def _main_image(self):
"""Please consider using prefetched query_set instead due to API performance issues"""
qs = self.establishment_gallery.main_image() qs = self.establishment_gallery.main_image()
if qs.exists(): image_model = qs.first()
return qs.first().image if image_model is not None:
return image_model.image
@property @property
def restaurant_category_indexing(self): def restaurant_category_indexing(self):
@ -786,6 +798,10 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
def artisan_category_indexing(self): def artisan_category_indexing(self):
return self.tags.filter(category__index_name='shop_category') return self.tags.filter(category__index_name='shop_category')
@property
def distillery_type_indexing(self):
return self.tags.filter(category__index_name='distillery_type')
@property @property
def last_comment(self): def last_comment(self):
if hasattr(self, 'comments_prefetched') and len(self.comments_prefetched): if hasattr(self, 'comments_prefetched') and len(self.comments_prefetched):
@ -856,11 +872,6 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
metadata.append(category_tags) metadata.append(category_tags)
return metadata return metadata
@property
def distillery_types(self):
"""Tags from tag category - distillery_type."""
return self.tags.filter(category__index_name='distillery_type')
class EstablishmentNoteQuerySet(models.QuerySet): class EstablishmentNoteQuerySet(models.QuerySet):
"""QuerySet for model EstablishmentNote.""" """QuerySet for model EstablishmentNote."""
@ -976,16 +987,97 @@ class EmployeeQuerySet(models.QuerySet):
] ]
return self.filter(reduce(lambda x, y: x | y, [models.Q(**i) for i in filters])) return self.filter(reduce(lambda x, y: x | y, [models.Q(**i) for i in filters]))
def trigram_search(self, search_value: str):
"""Search with mistakes by name or last name."""
return self.annotate(
search_exact_match=models.Case(
models.When(Q(name__iexact=search_value) | Q(last_name__iexact=search_value),
then=100),
default=0,
output_field=models.FloatField()
),
search_contains_match=models.Case(
models.When(Q(name__icontains=search_value) | Q(last_name__icontains=search_value),
then=50),
default=0,
output_field=models.FloatField()
),
search_name_similarity=models.Case(
models.When(
Q(name__isnull=False),
then=TrigramSimilarity('name', search_value.lower())
),
default=0,
output_field=models.FloatField()
),
search_last_name_similarity=models.Case(
models.When(
Q(last_name__isnull=False),
then=TrigramSimilarity('last_name', search_value.lower())
),
default=0,
output_field=models.FloatField()
),
relevance=(F('search_name_similarity') + F('search_exact_match')
+ F('search_contains_match') + F('search_last_name_similarity'))
).filter(relevance__gte=0.3).order_by('-relevance')
def search_by_name_or_last_name(self, value): def search_by_name_or_last_name(self, value):
"""Search by name or last_name.""" """Search by name or last_name."""
return self._generic_search(value, ['name', 'last_name']) return self._generic_search(value, ['name', 'last_name'])
def search_by_actual_employee(self):
"""Search by actual employee."""
return self.filter(
Q(establishmentemployee__from_date__lte=datetime.now()),
Q(establishmentemployee__to_date__gte=datetime.now()) |
Q(establishmentemployee__to_date__isnull=True)
)
def search_by_position_id(self, value):
"""Search by position_id."""
return self.filter(
Q(establishmentemployee__position_id=value),
)
def search_by_public_mark(self, value):
"""Search by establishment public_mark."""
return self.filter(
Q(establishmentemployee__establishment__public_mark=value),
)
def search_by_toque_number(self, value):
"""Search by establishment toque_number."""
return self.filter(
Q(establishmentemployee__establishment__toque_number=value),
)
def search_by_username_or_name(self, value):
"""Search by username or name."""
return self.search_by_actual_employee().filter(
Q(user__username__icontains=value) |
Q(user__first_name__icontains=value) |
Q(user__last_name__icontains=value)
)
def actual_establishment(self): def actual_establishment(self):
e = EstablishmentEmployee.objects.actual().filter(employee=self) e = EstablishmentEmployee.objects.actual().filter(employee=self)
return self.prefetch_related(models.Prefetch('establishmentemployee_set', return self.prefetch_related(models.Prefetch('establishmentemployee_set',
queryset=EstablishmentEmployee.objects.actual() queryset=EstablishmentEmployee.objects.actual()
)).all().distinct() )).all().distinct()
def with_extended_related(self):
return self.prefetch_related('establishments')
def with_back_office_related(self):
return self.prefetch_related(
Prefetch('establishmentemployee_set',
queryset=EstablishmentEmployee.objects.actual()
.prefetch_related('establishment', 'position').order_by('-from_date'),
to_attr='prefetched_establishment_employee'),
'awards'
)
class Employee(BaseAttributes): class Employee(BaseAttributes):
"""Employee model.""" """Employee model."""
@ -1021,6 +1113,11 @@ class Employee(BaseAttributes):
verbose_name=_('Tags')) verbose_name=_('Tags'))
# old_id = profile_id # old_id = profile_id
old_id = models.IntegerField(verbose_name=_('Old id'), null=True, blank=True) old_id = models.IntegerField(verbose_name=_('Old id'), null=True, blank=True)
available_for_events = models.BooleanField(_('Available for events'), default=False)
photo = models.ForeignKey('gallery.Image', on_delete=models.SET_NULL,
blank=True, null=True, default=None,
related_name='employee_photo',
verbose_name=_('image instance of model Image'))
objects = EmployeeQuerySet.as_manager() objects = EmployeeQuerySet.as_manager()
@ -1029,6 +1126,34 @@ class Employee(BaseAttributes):
verbose_name = _('Employee') verbose_name = _('Employee')
verbose_name_plural = _('Employees') verbose_name_plural = _('Employees')
indexes = [
GinIndex(fields=('name',)),
GinIndex(fields=('last_name',))
]
@property
def image_object(self):
"""Return image object."""
return self.photo.image if self.photo else None
@property
def crop_image(self):
if hasattr(self, 'photo') and hasattr(self, '_meta'):
if self.photo:
image_property = {
'id': self.photo.id,
'title': self.photo.title,
'original_url': self.photo.image.url,
'orientation_display': self.photo.get_orientation_display(),
'auto_crop_images': {},
}
crop_parameters = [p for p in settings.SORL_THUMBNAIL_ALIASES
if p.startswith(self._meta.model_name.lower())]
for crop in crop_parameters:
image_property['auto_crop_images'].update(
{crop: self.photo.get_image_url(crop)}
)
return image_property
class EstablishmentScheduleQuerySet(models.QuerySet): class EstablishmentScheduleQuerySet(models.QuerySet):

View File

@ -1,16 +1,30 @@
from functools import lru_cache
from django.db.models import F
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from account.serializers.common import UserShortSerializer
from establishment import models from establishment import models
from establishment import serializers as model_serializers from establishment import serializers as model_serializers
from establishment.models import ContactPhone, EstablishmentEmployee
from gallery.models import Image
from location.models import Address
from location.serializers import AddressDetailSerializer, TranslatedField from location.serializers import AddressDetailSerializer, TranslatedField
from main.models import Currency from main.models import Currency
from location.models import Address
from main.serializers import AwardSerializer from main.serializers import AwardSerializer
from utils.decorators import with_base_attributes from utils.decorators import with_base_attributes
from utils.serializers import TimeZoneChoiceField from utils.serializers import TimeZoneChoiceField, ImageBaseSerializer
from gallery.models import Image
from django.utils.translation import gettext_lazy as _
from account.serializers.common import UserShortSerializer def phones_handler(phones_list, establishment):
"""
create or update phones for establishment
"""
ContactPhone.objects.filter(establishment=establishment).delete()
for new_phone in phones_list:
ContactPhone.objects.create(establishment=establishment, phone=new_phone)
class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSerializer): class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSerializer):
@ -26,8 +40,6 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria
queryset=models.Address.objects.all(), queryset=models.Address.objects.all(),
write_only=True write_only=True
) )
phones = model_serializers.ContactPhonesSerializer(read_only=True,
many=True, )
emails = model_serializers.ContactEmailsSerializer(read_only=True, emails = model_serializers.ContactEmailsSerializer(read_only=True,
many=True, ) many=True, )
socials = model_serializers.SocialNetworkRelatedSerializers(read_only=True, socials = model_serializers.SocialNetworkRelatedSerializers(read_only=True,
@ -37,6 +49,13 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria
address_id = serializers.PrimaryKeyRelatedField(write_only=True, source='address', address_id = serializers.PrimaryKeyRelatedField(write_only=True, source='address',
queryset=Address.objects.all()) queryset=Address.objects.all())
tz = TimeZoneChoiceField() tz = TimeZoneChoiceField()
phones = serializers.ListField(
source='contact_phones',
allow_null=True,
allow_empty=True,
child=serializers.CharField(max_length=128),
required=False,
)
class Meta: class Meta:
model = models.Establishment model = models.Establishment
@ -64,6 +83,15 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria
'address_id', 'address_id',
] ]
def create(self, validated_data):
phones_list = []
if 'contact_phones' in validated_data:
phones_list = validated_data.pop('contact_phones')
instance = super().create(validated_data)
phones_handler(phones_list, instance)
return instance
class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer): class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer):
"""Establishment create serializer""" """Establishment create serializer"""
@ -73,18 +101,24 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer):
queryset=models.EstablishmentType.objects.all(), write_only=True queryset=models.EstablishmentType.objects.all(), write_only=True
) )
address = AddressDetailSerializer() address = AddressDetailSerializer()
phones = model_serializers.ContactPhonesSerializer(read_only=False,
many=True, )
emails = model_serializers.ContactEmailsSerializer(read_only=False, emails = model_serializers.ContactEmailsSerializer(read_only=False,
many=True, ) many=True, )
socials = model_serializers.SocialNetworkRelatedSerializers(read_only=False, socials = model_serializers.SocialNetworkRelatedSerializers(read_only=False,
many=True, ) many=True, )
type = model_serializers.EstablishmentTypeBaseSerializer(source='establishment_type') type = model_serializers.EstablishmentTypeBaseSerializer(source='establishment_type')
phones = serializers.ListField(
source='contact_phones',
allow_null=True,
allow_empty=True,
child=serializers.CharField(max_length=128),
required=False,
)
class Meta: class Meta:
model = models.Establishment model = models.Establishment
fields = [ fields = [
'id', 'id',
'slug',
'name', 'name',
'website', 'website',
'phones', 'phones',
@ -101,6 +135,15 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer):
'tags', 'tags',
] ]
def update(self, instance, validated_data):
phones_list = []
if 'contact_phones' in validated_data:
phones_list = validated_data.pop('contact_phones')
instance = super().update(instance, validated_data)
phones_handler(phones_list, instance)
return instance
class SocialChoiceSerializers(serializers.ModelSerializer): class SocialChoiceSerializers(serializers.ModelSerializer):
"""SocialChoice serializers.""" """SocialChoice serializers."""
@ -166,7 +209,6 @@ class ContactEmailBackSerializers(model_serializers.PlateSerializer):
] ]
class PositionBackSerializer(serializers.ModelSerializer): class PositionBackSerializer(serializers.ModelSerializer):
"""Position Back serializer.""" """Position Back serializer."""
@ -181,6 +223,7 @@ class PositionBackSerializer(serializers.ModelSerializer):
'index_name', 'index_name',
] ]
# TODO: test decorator # TODO: test decorator
@with_base_attributes @with_base_attributes
class EmployeeBackSerializers(serializers.ModelSerializer): class EmployeeBackSerializers(serializers.ModelSerializer):
@ -189,25 +232,52 @@ class EmployeeBackSerializers(serializers.ModelSerializer):
positions = serializers.SerializerMethodField() positions = serializers.SerializerMethodField()
establishment = serializers.SerializerMethodField() establishment = serializers.SerializerMethodField()
awards = AwardSerializer(many=True, read_only=True) awards = AwardSerializer(many=True, read_only=True)
toque_number = serializers.SerializerMethodField()
photo = ImageBaseSerializer(source='crop_image', read_only=True)
@staticmethod
@lru_cache(maxsize=32)
def get_qs(obj):
return obj.establishmentemployee_set.actual().annotate(
public_mark=F('establishment__public_mark'),
est_id=F('establishment__id'),
est_slug=F('establishment__slug'),
toque_number=F('establishment__toque_number'),
).order_by('-from_date').first()
def get_public_mark(self, obj): def get_public_mark(self, obj):
"""Get last list actual public_mark""" """Get last list actual public_mark"""
qs = obj.establishmentemployee_set.actual().order_by('-from_date')\ if hasattr(obj, 'prefetched_establishment_employee'):
.values('establishment__public_mark').first() return obj.prefetched_establishment_employee[0].establishment.public_mark if len(
return qs['establishment__public_mark'] if qs else None obj.prefetched_establishment_employee) else None
qs = self.get_qs(obj)
if qs:
return qs.public_mark
return None
def get_toque_number(self, obj):
if hasattr(obj, 'prefetched_establishment_employee'):
return obj.prefetched_establishment_employee[0].establishment.toque_number if len(
obj.prefetched_establishment_employee) else None
qs = self.get_qs(obj)
if qs:
return qs.toque_number
return None
def get_positions(self, obj): def get_positions(self, obj):
"""Get last list actual positions""" """Get last list actual positions"""
est_id = obj.establishmentemployee_set.actual().\ if hasattr(obj, 'prefetched_establishment_employee'):
order_by('-from_date').first() if not len(obj.prefetched_establishment_employee):
return []
return [PositionBackSerializer(ee.position).data for ee in obj.prefetched_establishment_employee]
est_id = self.get_qs(obj)
if not est_id: if not est_id:
return None return None
qs = obj.establishmentemployee_set.actual()\ qs = obj.establishmentemployee_set.actual() \
.filter(establishment_id=est_id.establishment_id)\ .filter(establishment_id=est_id.establishment_id) \
.prefetch_related('position').values('position') .prefetch_related('position').values('position')
positions = models.Position.objects.filter(id__in=[q['position'] for q in qs]) positions = models.Position.objects.filter(id__in=[q['position'] for q in qs])
@ -216,15 +286,19 @@ class EmployeeBackSerializers(serializers.ModelSerializer):
def get_establishment(self, obj): def get_establishment(self, obj):
"""Get last actual establishment""" """Get last actual establishment"""
est = obj.establishmentemployee_set.actual().order_by('-from_date')\ if hasattr(obj, 'prefetched_establishment_employee'):
.first() return {
'id': obj.prefetched_establishment_employee[0].establishment.pk,
'slug': obj.prefetched_establishment_employee[0].establishment.slug,
} if len(obj.prefetched_establishment_employee) else None
est = self.get_qs(obj)
if not est: if not est:
return None return None
return { return {
"id": est.establishment.id, "id": est.est_id,
"slug": est.establishment.slug "slug": est.est_slug
} }
class Meta: class Meta:
@ -242,7 +316,9 @@ class EmployeeBackSerializers(serializers.ModelSerializer):
'birth_date', 'birth_date',
'email', 'email',
'phone', 'phone',
'toque_number' 'toque_number',
'available_for_events',
'photo',
] ]
@ -264,6 +340,53 @@ class EstablishmentEmployeeBackSerializer(serializers.ModelSerializer):
] ]
class EstEmployeeBackSerializer(EmployeeBackSerializers):
@property
def request_kwargs(self):
"""Get url kwargs from request."""
return self.context.get('request').parser_context.get('kwargs')
def get_positions(self, obj):
establishment_id = self.request_kwargs.get('establishment_id')
es_emp = EstablishmentEmployee.objects.filter(
employee=obj,
establishment_id=establishment_id,
).distinct().order_by('position_id')
result = []
for item in es_emp:
result.append({
'id': item.id,
'from_date': item.from_date,
'to_date': item.to_date,
'status': item.status,
'position_id': item.position_id,
'position_priority': item.position.priority,
'position_index_name': item.position.index_name,
'position_name_translated': item.position.name_translated,
})
return result
class Meta:
model = models.Employee
fields = [
'id',
'name',
'last_name',
'user',
'public_mark',
'positions',
'awards',
'sex',
'birth_date',
'email',
'phone',
'toque_number',
'available_for_events',
'photo',
]
class EstablishmentBackOfficeGallerySerializer(serializers.ModelSerializer): class EstablishmentBackOfficeGallerySerializer(serializers.ModelSerializer):
"""Serializer class for model EstablishmentGallery.""" """Serializer class for model EstablishmentGallery."""
@ -380,6 +503,7 @@ class EstablishmentNoteListCreateSerializer(EstablishmentNoteBaseSerializer):
class EstablishmentAdminListSerializer(UserShortSerializer): class EstablishmentAdminListSerializer(UserShortSerializer):
"""Establishment admin serializer.""" """Establishment admin serializer."""
class Meta: class Meta:
model = UserShortSerializer.Meta.model model = UserShortSerializer.Meta.model
fields = [ fields = [

View File

@ -1,4 +1,7 @@
"""Establishment serializers.""" """Establishment serializers."""
import logging
from django.conf import settings
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from phonenumber_field.phonenumber import to_python as str_to_phonenumber from phonenumber_field.phonenumber import to_python as str_to_phonenumber
from rest_framework import serializers from rest_framework import serializers
@ -6,8 +9,10 @@ from rest_framework import serializers
from comment import models as comment_models from comment import models as comment_models
from comment.serializers import common as comment_serializers from comment.serializers import common as comment_serializers
from establishment import models from establishment import models
from location.serializers import AddressBaseSerializer, CitySerializer, AddressDetailSerializer, \ from location.serializers import AddressBaseSerializer, CityBaseSerializer, AddressDetailSerializer, \
CityShortSerializer CityShortSerializer
from location.serializers import EstablishmentWineRegionBaseSerializer, \
EstablishmentWineOriginBaseSerializer
from main.serializers import AwardSerializer, CurrencySerializer from main.serializers import AwardSerializer, CurrencySerializer
from review.serializers import ReviewShortSerializer from review.serializers import ReviewShortSerializer
from tag.serializers import TagBaseSerializer from tag.serializers import TagBaseSerializer
@ -16,9 +21,8 @@ from utils import exceptions as utils_exceptions
from utils.serializers import ImageBaseSerializer, CarouselCreateSerializer from utils.serializers import ImageBaseSerializer, CarouselCreateSerializer
from utils.serializers import (ProjectModelSerializer, TranslatedField, from utils.serializers import (ProjectModelSerializer, TranslatedField,
FavoritesCreateSerializer) FavoritesCreateSerializer)
from location.serializers import EstablishmentWineRegionBaseSerializer, \
EstablishmentWineOriginBaseSerializer
logger = logging.getLogger(__name__)
class ContactPhonesSerializer(serializers.ModelSerializer): class ContactPhonesSerializer(serializers.ModelSerializer):
"""Contact phone serializer""" """Contact phone serializer"""
@ -198,7 +202,7 @@ class EstablishmentEmployeeCreateSerializer(serializers.ModelSerializer):
"""Meta class.""" """Meta class."""
model = models.EstablishmentEmployee model = models.EstablishmentEmployee
fields = ('id',) fields = ('id', 'from_date', 'to_date')
def _validate_entity(self, entity_id_param: str, entity_class): def _validate_entity(self, entity_id_param: str, entity_class):
entity_id = self.context.get('request').parser_context.get('kwargs').get(entity_id_param) entity_id = self.context.get('request').parser_context.get('kwargs').get(entity_id_param)
@ -231,7 +235,7 @@ class EstablishmentEmployeeCreateSerializer(serializers.ModelSerializer):
class EstablishmentShortSerializer(serializers.ModelSerializer): class EstablishmentShortSerializer(serializers.ModelSerializer):
"""Short serializer for establishment.""" """Short serializer for establishment."""
city = CitySerializer(source='address.city', allow_null=True) city = CityBaseSerializer(source='address.city', allow_null=True)
establishment_type = EstablishmentTypeGeoSerializer() establishment_type = EstablishmentTypeGeoSerializer()
establishment_subtypes = EstablishmentSubTypeBaseSerializer(many=True) establishment_subtypes = EstablishmentSubTypeBaseSerializer(many=True)
currency = CurrencySerializer(read_only=True) currency = CurrencySerializer(read_only=True)
@ -253,10 +257,9 @@ class EstablishmentShortSerializer(serializers.ModelSerializer):
class _EstablishmentAddressShortSerializer(serializers.ModelSerializer): class _EstablishmentAddressShortSerializer(serializers.ModelSerializer):
"""Short serializer for establishment.""" """Short serializer for establishment."""
city = CitySerializer(source='address.city', allow_null=True) city = CityBaseSerializer(source='address.city', allow_null=True)
establishment_type = EstablishmentTypeGeoSerializer() establishment_type = EstablishmentTypeGeoSerializer()
establishment_subtypes = EstablishmentSubTypeBaseSerializer(many=True) establishment_subtypes = EstablishmentSubTypeBaseSerializer(many=True)
currency = CurrencySerializer(read_only=True)
address = AddressBaseSerializer(read_only=True) address = AddressBaseSerializer(read_only=True)
class Meta: class Meta:
@ -320,15 +323,45 @@ class EstablishmentBaseSerializer(ProjectModelSerializer):
currency = CurrencySerializer() currency = CurrencySerializer()
type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True) type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True)
subtypes = EstablishmentSubTypeBaseSerializer(many=True, source='establishment_subtypes') subtypes = EstablishmentSubTypeBaseSerializer(many=True, source='establishment_subtypes')
image = serializers.URLField(source='image_url', read_only=True) image = serializers.SerializerMethodField(read_only=True)
wine_regions = EstablishmentWineRegionBaseSerializer(many=True, source='wine_origins_unique', wine_regions = EstablishmentWineRegionBaseSerializer(many=True, source='wine_origins_unique',
read_only=True, allow_null=True) read_only=True, allow_null=True)
preview_image = serializers.URLField(source='preview_image_url', preview_image = serializers.URLField(source='preview_image_url',
allow_null=True, allow_null=True,
read_only=True) read_only=True)
tz = serializers.CharField(read_only=True, source='timezone_as_str') tz = serializers.CharField(read_only=True, source='timezone_as_str')
new_image = ImageBaseSerializer(source='crop_main_image', allow_null=True, read_only=True) new_image = serializers.SerializerMethodField(allow_null=True, read_only=True)
distillery_types = TagBaseSerializer(read_only=True, many=True, allow_null=True) distillery_type = TagBaseSerializer(read_only=True, many=True, allow_null=True)
def get_image(self, obj):
if obj.main_image:
return obj.main_image[0].image.image.url if len(obj.main_image) else None
logging.info('Possibly not optimal image reading')
return obj._main_image # backwards compatibility
def get_new_image(self, obj):
if hasattr(self, 'main_image') and hasattr(self, '_meta'):
if obj.main_image and len(obj.main_image):
main_image = obj.main_image[0].image
else:
logging.info('Possibly not optimal image reading')
main_image = obj._main_image
if main_image:
image = main_image
image_property = {
'id': image.id,
'title': image.title,
'original_url': image.image.url,
'orientation_display': image.get_orientation_display(),
'auto_crop_images': {},
}
crop_parameters = [p for p in settings.SORL_THUMBNAIL_ALIASES
if p.startswith(self._meta.model_name.lower())]
for crop in crop_parameters:
image_property['auto_crop_images'].update(
{crop: image.get_image_url(crop)}
)
return image_property
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -354,7 +387,7 @@ class EstablishmentBaseSerializer(ProjectModelSerializer):
'new_image', 'new_image',
'tz', 'tz',
'wine_regions', 'wine_regions',
'distillery_types', 'distillery_type',
] ]
@ -366,6 +399,7 @@ class EstablishmentListRetrieveSerializer(EstablishmentBaseSerializer):
restaurant_category = TagBaseSerializer(read_only=True, many=True, allow_null=True) restaurant_category = TagBaseSerializer(read_only=True, many=True, allow_null=True)
restaurant_cuisine = TagBaseSerializer(read_only=True, many=True, allow_null=True) restaurant_cuisine = TagBaseSerializer(read_only=True, many=True, allow_null=True)
artisan_category = TagBaseSerializer(read_only=True, many=True, allow_null=True) artisan_category = TagBaseSerializer(read_only=True, many=True, allow_null=True)
distillery_type = TagBaseSerializer(read_only=True, many=True, allow_null=True)
class Meta(EstablishmentBaseSerializer.Meta): class Meta(EstablishmentBaseSerializer.Meta):
"""Meta class.""" """Meta class."""
@ -375,6 +409,7 @@ class EstablishmentListRetrieveSerializer(EstablishmentBaseSerializer):
'restaurant_category', 'restaurant_category',
'restaurant_cuisine', 'restaurant_cuisine',
'artisan_category', 'artisan_category',
'distillery_type',
] ]
@ -449,7 +484,7 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer):
class MobileEstablishmentDetailSerializer(EstablishmentDetailSerializer): class MobileEstablishmentDetailSerializer(EstablishmentDetailSerializer):
"""Serializer for Establishment model for mobiles.""" """Serializer for Establishment model for mobiles."""
last_comment = comment_serializers.CommentRUDSerializer(allow_null=True) last_comment = comment_serializers.CommentBaseSerializer(allow_null=True)
class Meta(EstablishmentDetailSerializer.Meta): class Meta(EstablishmentDetailSerializer.Meta):
"""Meta class.""" """Meta class."""
@ -468,6 +503,7 @@ class EstablishmentSimilarSerializer(EstablishmentBaseSerializer):
artisan_category = TagBaseSerializer(many=True, allow_null=True, read_only=True) artisan_category = TagBaseSerializer(many=True, allow_null=True, read_only=True)
restaurant_category = TagBaseSerializer(many=True, allow_null=True, read_only=True) restaurant_category = TagBaseSerializer(many=True, allow_null=True, read_only=True)
restaurant_cuisine = TagBaseSerializer(many=True, allow_null=True, read_only=True) restaurant_cuisine = TagBaseSerializer(many=True, allow_null=True, read_only=True)
distillery_type = TagBaseSerializer(many=True, allow_null=True, read_only=True)
class Meta(EstablishmentBaseSerializer.Meta): class Meta(EstablishmentBaseSerializer.Meta):
fields = EstablishmentBaseSerializer.Meta.fields + [ fields = EstablishmentBaseSerializer.Meta.fields + [
@ -476,16 +512,15 @@ class EstablishmentSimilarSerializer(EstablishmentBaseSerializer):
'artisan_category', 'artisan_category',
'restaurant_category', 'restaurant_category',
'restaurant_cuisine', 'restaurant_cuisine',
'distillery_type',
] ]
class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer): class EstablishmentCommentBaseSerializer(comment_serializers.CommentBaseSerializer):
"""Create comment serializer""" """Create comment serializer"""
mark = serializers.IntegerField()
class Meta: class Meta(comment_serializers.CommentBaseSerializer.Meta):
"""Serializer for model Comment""" """Serializer for model Comment"""
model = comment_models.Comment
fields = [ fields = [
'id', 'id',
'created', 'created',
@ -493,8 +528,14 @@ class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer
'mark', 'mark',
'nickname', 'nickname',
'profile_pic', 'profile_pic',
'status',
'status_display',
] ]
class EstablishmentCommentCreateSerializer(EstablishmentCommentBaseSerializer):
"""Extended EstablishmentCommentBaseSerializer."""
def validate(self, attrs): def validate(self, attrs):
"""Override validate method""" """Override validate method"""
# Check establishment object # Check establishment object
@ -514,7 +555,7 @@ class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer
return super().create(validated_data) return super().create(validated_data)
class EstablishmentCommentRUDSerializer(comment_serializers.CommentSerializer): class EstablishmentCommentRUDSerializer(comment_serializers.CommentBaseSerializer):
"""Retrieve/Update/Destroy comment serializer.""" """Retrieve/Update/Destroy comment serializer."""
class Meta: class Meta:

View File

@ -1,17 +1,14 @@
"""Establishment app tasks.""" """Establishment app tasks."""
import logging import logging
import requests
from celery import shared_task from celery import shared_task
from celery.schedules import crontab from celery.schedules import crontab
from celery.task import periodic_task from celery.task import periodic_task
from django.core import management
from django_elasticsearch_dsl.management.commands import search_index
from django_elasticsearch_dsl.registries import registry from django_elasticsearch_dsl.registries import registry
from establishment import models from establishment import models
from location.models import Country from location.models import Country
from search_indexes.documents.establishment import EstablishmentDocument
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -28,6 +25,7 @@ def recalculate_price_levels_by_country(country_id):
establishment.recalculate_price_level(low_price=country.low_price, establishment.recalculate_price_level(low_price=country.low_price,
high_price=country.high_price) high_price=country.high_price)
# @periodic_task(run_every=crontab(minute=59)) # @periodic_task(run_every=crontab(minute=59))
# def rebuild_establishment_indices(): # def rebuild_establishment_indices():
# management.call_command(search_index.Command(), action='populate', models=[models.Establishment.__name__], # management.call_command(search_index.Command(), action='populate', models=[models.Establishment.__name__],
@ -50,3 +48,47 @@ def recalculation_public_mark(establishment_id):
establishment = models.Establishment.objects.get(id=establishment_id) establishment = models.Establishment.objects.get(id=establishment_id)
establishment.recalculate_public_mark() establishment.recalculate_public_mark()
establishment.recalculate_toque_number() establishment.recalculate_toque_number()
@shared_task
def update_establishment_image_urls(part_number: int, summary_tasks: int, bucket_ids: list):
queryset = models.Establishment.objects.filter(id__in=bucket_ids)
for establishment in queryset:
live_link = None
image_urls = [
('image_url', establishment.image_url),
('preview_image_url', establishment.preview_image_url)
]
for data in image_urls:
attr, url = data
if establishment.image_url is not None:
try:
response = requests.get(url, allow_redirects=True)
if response.status_code != 200:
setattr(establishment, attr, None)
else:
live_link = url
except (
requests.exceptions.ConnectionError,
requests.exceptions.ConnectTimeout
):
setattr(establishment, attr, None)
if live_link is not None:
if establishment.image_url is None:
establishment.image_url = live_link
elif establishment.preview_image_url is None:
establishment.preview_image_url = live_link
establishment.save()
logger.info(f'The {part_number}th part of the image update '
f'from {summary_tasks} parts was completed')

View File

@ -45,6 +45,7 @@ urlpatterns = [
path('<int:establishment_id>/employees/', views.EstablishmentEmployeeListView.as_view(), path('<int:establishment_id>/employees/', views.EstablishmentEmployeeListView.as_view(),
name='establishment-employees'), name='establishment-employees'),
path('employees/', views.EmployeeListCreateView.as_view(), name='employees'), path('employees/', views.EmployeeListCreateView.as_view(), name='employees'),
path('employees/search/', views.EmployeesListSearchViews.as_view(), name='employees-search'),
path('employees/<int:pk>/', views.EmployeeRUDView.as_view(), name='employees-rud'), path('employees/<int:pk>/', views.EmployeeRUDView.as_view(), name='employees-rud'),
path('<int:establishment_id>/employee/<int:employee_id>/position/<int:position_id>', path('<int:establishment_id>/employee/<int:employee_id>/position/<int:position_id>',
views.EstablishmentEmployeeCreateView.as_view(), views.EstablishmentEmployeeCreateView.as_view(),

View File

@ -2,7 +2,7 @@
from django.http import Http404, HttpResponse from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics, permissions, status from rest_framework import generics, permissions, status, filters as rest_filters
from account.models import User from account.models import User
from establishment import filters, models, serializers from establishment import filters, models, serializers
@ -34,7 +34,10 @@ class EstablishmentListCreateView(EstablishmentMixinViews, generics.ListCreateAP
class EstablishmentRUDView(generics.RetrieveUpdateDestroyAPIView): class EstablishmentRUDView(generics.RetrieveUpdateDestroyAPIView):
lookup_field = 'slug' lookup_field = 'slug'
queryset = models.Establishment.objects.all() queryset = models.Establishment.objects.all().prefetch_related(
'establishmentemployee_set',
'establishmentemployee_set__establishment',
)
serializer_class = serializers.EstablishmentRUDSerializer serializer_class = serializers.EstablishmentRUDSerializer
permission_classes = [IsWineryReviewer | IsCountryAdmin | IsEstablishmentManager] permission_classes = [IsWineryReviewer | IsCountryAdmin | IsEstablishmentManager]
@ -171,49 +174,61 @@ class EmployeeListCreateView(generics.ListCreateAPIView):
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
filter_class = filters.EmployeeBackFilter filter_class = filters.EmployeeBackFilter
serializer_class = serializers.EmployeeBackSerializers serializer_class = serializers.EmployeeBackSerializers
queryset = models.Employee.objects.all() queryset = models.Employee.objects.all().with_back_office_related()
class EmployeesListSearchViews(generics.ListAPIView):
"""Employee search view"""
pagination_class = None
permission_classes = (permissions.AllowAny,)
queryset = models.Employee.objects.all().with_back_office_related().select_related('photo')
filter_class = filters.EmployeeBackSearchFilter
serializer_class = serializers.EmployeeBackSerializers
class EstablishmentEmployeeListView(generics.ListCreateAPIView): class EstablishmentEmployeeListView(generics.ListCreateAPIView):
"""Establishment emplyoees list view.""" """Establishment emplyoees list view."""
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
serializer_class = serializers.EstablishmentEmployeeBackSerializer serializer_class = serializers.EstEmployeeBackSerializer
pagination_class = None
def get_queryset(self): def get_queryset(self):
establishment_id = self.kwargs['establishment_id'] establishment_id = self.kwargs['establishment_id']
return models.EstablishmentEmployee.objects.filter(establishment__id=establishment_id) return models.Employee.objects.filter(
establishmentemployee__establishment_id=establishment_id,
).distinct()
class EmployeeRUDView(generics.RetrieveUpdateDestroyAPIView): class EmployeeRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Employee RUD view.""" """Employee RUD view."""
serializer_class = serializers.EmployeeBackSerializers serializer_class = serializers.EmployeeBackSerializers
queryset = models.Employee.objects.all() queryset = models.Employee.objects.all().with_back_office_related()
class EstablishmentTypeListCreateView(generics.ListCreateAPIView): class EstablishmentTypeListCreateView(generics.ListCreateAPIView):
"""Establishment type list/create view.""" """Establishment type list/create view."""
serializer_class = serializers.EstablishmentTypeBaseSerializer serializer_class = serializers.EstablishmentTypeBaseSerializer
queryset = models.EstablishmentType.objects.all() queryset = models.EstablishmentType.objects.all().select_related('default_image')
pagination_class = None pagination_class = None
class EstablishmentTypeRUDView(generics.RetrieveUpdateDestroyAPIView): class EstablishmentTypeRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Establishment type retrieve/update/destroy view.""" """Establishment type retrieve/update/destroy view."""
serializer_class = serializers.EstablishmentTypeBaseSerializer serializer_class = serializers.EstablishmentTypeBaseSerializer
queryset = models.EstablishmentType.objects.all() queryset = models.EstablishmentType.objects.all().select_related('default_image')
class EstablishmentSubtypeListCreateView(generics.ListCreateAPIView): class EstablishmentSubtypeListCreateView(generics.ListCreateAPIView):
"""Establishment subtype list/create view.""" """Establishment subtype list/create view."""
serializer_class = serializers.EstablishmentSubTypeBaseSerializer serializer_class = serializers.EstablishmentSubTypeBaseSerializer
queryset = models.EstablishmentSubType.objects.all() queryset = models.EstablishmentSubType.objects.all().select_related('default_image')
pagination_class = None pagination_class = None
class EstablishmentSubtypeRUDView(generics.RetrieveUpdateDestroyAPIView): class EstablishmentSubtypeRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Establishment subtype retrieve/update/destroy view.""" """Establishment subtype retrieve/update/destroy view."""
serializer_class = serializers.EstablishmentSubTypeBaseSerializer serializer_class = serializers.EstablishmentSubTypeBaseSerializer
queryset = models.EstablishmentSubType.objects.all() queryset = models.EstablishmentSubType.objects.all().select_related('default_image')
class EstablishmentGalleryCreateDestroyView(EstablishmentMixinViews, class EstablishmentGalleryCreateDestroyView(EstablishmentMixinViews,
@ -385,7 +400,7 @@ class EstablishmentPositionListView(generics.ListAPIView):
class EstablishmentAdminView(generics.ListAPIView): class EstablishmentAdminView(generics.ListAPIView):
"""Establishment admin list view.""" """Establishment admin list view."""
serializer_class = serializers.EstablishmentAdminListSerializer serializer_class = serializers.EstablishmentAdminListSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly, ) permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def get_queryset(self): def get_queryset(self):
establishment = get_object_or_404( establishment = get_object_or_404(

View File

@ -5,7 +5,6 @@ from django.shortcuts import get_object_or_404
from rest_framework import generics, permissions from rest_framework import generics, permissions
from comment import models as comment_models from comment import models as comment_models
from comment.serializers import CommentRUDSerializer
from establishment import filters, models, serializers from establishment import filters, models, serializers
from main import methods from main import methods
from utils.pagination import PortionPagination from utils.pagination import PortionPagination
@ -38,7 +37,8 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
.with_extended_address_related().with_currency_related() \ .with_extended_address_related().with_currency_related() \
.with_certain_tag_category_related('category', 'restaurant_category') \ .with_certain_tag_category_related('category', 'restaurant_category') \
.with_certain_tag_category_related('cuisine', 'restaurant_cuisine') \ .with_certain_tag_category_related('cuisine', 'restaurant_cuisine') \
.with_certain_tag_category_related('shop_category', 'artisan_category') .with_certain_tag_category_related('shop_category', 'artisan_category') \
.with_certain_tag_category_related('distillery_type', 'distillery_type')
class EstablishmentSimilarView(EstablishmentListView): class EstablishmentSimilarView(EstablishmentListView):
@ -67,7 +67,8 @@ class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView
serializer_class = serializers.EstablishmentDetailSerializer serializer_class = serializers.EstablishmentDetailSerializer
def get_queryset(self): def get_queryset(self):
return super().get_queryset().with_extended_related() return super().get_queryset().with_extended_related() \
.with_certain_tag_category_related('distillery_type', 'distillery_type')
class EstablishmentMobileRetrieveView(EstablishmentRetrieveView): class EstablishmentMobileRetrieveView(EstablishmentRetrieveView):
@ -186,18 +187,17 @@ class EstablishmentCommentListView(generics.ListAPIView):
"""View for return list of establishment comments.""" """View for return list of establishment comments."""
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
serializer_class = serializers.EstablishmentCommentCreateSerializer serializer_class = serializers.EstablishmentCommentBaseSerializer
def get_queryset(self): def get_queryset(self):
"""Override get_queryset method""" """Override get_queryset method"""
establishment = get_object_or_404(models.Establishment, slug=self.kwargs['slug']) establishment = get_object_or_404(models.Establishment, slug=self.kwargs['slug'])
return establishment.comments.order_by('-created') return establishment.comments.public(self.request.user).order_by('-created')
class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView): class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView):
"""View for retrieve/update/destroy establishment comment.""" """View for retrieve/update/destroy establishment comment."""
serializer_class = CommentRUDSerializer serializer_class = serializers.EstablishmentCommentBaseSerializer
queryset = models.Establishment.objects.all() queryset = models.Establishment.objects.all()
def get_object(self): def get_object(self):

View File

@ -32,7 +32,8 @@ class FavoritesEstablishmentListView(generics.ListAPIView):
.order_by('-favorites').with_base_related() \ .order_by('-favorites').with_base_related() \
.with_certain_tag_category_related('category', 'restaurant_category') \ .with_certain_tag_category_related('category', 'restaurant_category') \
.with_certain_tag_category_related('cuisine', 'restaurant_cuisine') \ .with_certain_tag_category_related('cuisine', 'restaurant_cuisine') \
.with_certain_tag_category_related('shop_category', 'artisan_category') .with_certain_tag_category_related('shop_category', 'artisan_category') \
.with_certain_tag_category_related('distillery_type', 'distillery_type')
class FavoritesProductListView(generics.ListAPIView): class FavoritesProductListView(generics.ListAPIView):

View File

@ -0,0 +1,20 @@
# Generated by Django 2.2.7 on 2020-01-15 09:45
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('gallery', '0008_merge_20191212_0752'),
('location', '0033_merge_20191224_0920'),
]
operations = [
migrations.AddField(
model_name='city',
name='image',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='city_image', to='gallery.Image', verbose_name='image instance of model Image'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 2.2.7 on 2020-01-15 11:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('location', '0034_city_image'),
]
operations = [
migrations.RemoveField(
model_name='city',
name='gallery',
),
migrations.DeleteModel(
name='CityGallery',
),
]

View File

@ -75,7 +75,6 @@ class Country(TranslatedFieldsMixin,
return self.id return self.id
class RegionQuerySet(models.QuerySet): class RegionQuerySet(models.QuerySet):
"""QuerySet for model Region.""" """QuerySet for model Region."""
@ -144,7 +143,7 @@ class CityQuerySet(models.QuerySet):
return self.filter(country__code=code) return self.filter(country__code=code)
class City(GalleryMixin, models.Model): class City(models.Model):
"""Region model.""" """Region model."""
name = models.CharField(_('name'), max_length=250) name = models.CharField(_('name'), max_length=250)
name_translated = TJSONField(blank=True, null=True, default=None, name_translated = TJSONField(blank=True, null=True, default=None,
@ -167,8 +166,10 @@ class City(GalleryMixin, models.Model):
map2 = models.CharField(max_length=255, blank=True, null=True) map2 = models.CharField(max_length=255, blank=True, null=True)
map_ref = models.CharField(max_length=255, blank=True, null=True) map_ref = models.CharField(max_length=255, blank=True, null=True)
situation = models.CharField(max_length=255, blank=True, null=True) situation = models.CharField(max_length=255, blank=True, null=True)
image = models.ForeignKey('gallery.Image', on_delete=models.SET_NULL,
gallery = models.ManyToManyField('gallery.Image', through='location.CityGallery', blank=True) blank=True, null=True, default=None,
related_name='city_image',
verbose_name=_('image instance of model Image'))
mysql_id = models.IntegerField(blank=True, null=True, default=None) mysql_id = models.IntegerField(blank=True, null=True, default=None)
@ -181,23 +182,29 @@ class City(GalleryMixin, models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
@property
def image_object(self):
"""Return image object."""
return self.image.image if self.image else None
class CityGallery(IntermediateGalleryModelMixin): @property
"""Gallery for model City.""" def crop_image(self):
city = models.ForeignKey(City, null=True, if hasattr(self, 'image') and hasattr(self, '_meta'):
related_name='city_gallery', if self.image:
on_delete=models.CASCADE, image_property = {
verbose_name=_('city')) 'id': self.image.id,
image = models.ForeignKey('gallery.Image', null=True, 'title': self.image.title,
related_name='city_gallery', 'original_url': self.image.image.url,
on_delete=models.CASCADE, 'orientation_display': self.image.get_orientation_display(),
verbose_name=_('image')) 'auto_crop_images': {},
}
class Meta: crop_parameters = [p for p in settings.SORL_THUMBNAIL_ALIASES
"""CityGallery meta class.""" if p.startswith(self._meta.model_name.lower())]
verbose_name = _('city gallery') for crop in crop_parameters:
verbose_name_plural = _('city galleries') image_property['auto_crop_images'].update(
unique_together = (('city', 'is_main'), ('city', 'image')) {crop: self.image.get_image_url(crop)}
)
return image_property
class Address(models.Model): class Address(models.Model):

View File

@ -1,8 +1,5 @@
from location import models from location import models
from location.serializers import common from location.serializers import common
from rest_framework import serializers
from gallery.models import Image
from django.utils.translation import gettext_lazy as _
class AddressCreateSerializer(common.AddressDetailSerializer): class AddressCreateSerializer(common.AddressDetailSerializer):
@ -21,46 +18,3 @@ class CountryBackSerializer(common.CountrySerializer):
'name', 'name',
'country_id' 'country_id'
] ]
class CityGallerySerializer(serializers.ModelSerializer):
"""Serializer class for model CityGallery."""
class Meta:
"""Meta class"""
model = models.CityGallery
fields = [
'id',
'is_main',
]
@property
def request_kwargs(self):
"""Get url kwargs from request."""
return self.context.get('request').parser_context.get('kwargs')
def validate(self, attrs):
"""Override validate method."""
city_pk = self.request_kwargs.get('pk')
image_id = self.request_kwargs.get('image_id')
city_qs = models.City.objects.filter(pk=city_pk)
image_qs = Image.objects.filter(id=image_id)
if not city_qs.exists():
raise serializers.ValidationError({'detail': _('City not found')})
if not image_qs.exists():
raise serializers.ValidationError({'detail': _('Image not found')})
city = city_qs.first()
image = image_qs.first()
if image in city.gallery.all():
raise serializers.ValidationError({'detail': _('Image is already added.')})
attrs['city'] = city
attrs['image'] = image
return attrs

View File

@ -3,7 +3,8 @@ from django.contrib.gis.geos import Point
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from location import models from location import models
from utils.serializers import TranslatedField from gallery import models as gallery_models
from utils.serializers import TranslatedField, ImageBaseSerializer
class CountrySerializer(serializers.ModelSerializer): class CountrySerializer(serializers.ModelSerializer):
@ -70,7 +71,7 @@ class CityShortSerializer(serializers.ModelSerializer):
) )
class CitySerializer(serializers.ModelSerializer): class CityBaseSerializer(serializers.ModelSerializer):
"""City serializer.""" """City serializer."""
region = RegionSerializer(read_only=True) region = RegionSerializer(read_only=True)
region_id = serializers.PrimaryKeyRelatedField( region_id = serializers.PrimaryKeyRelatedField(
@ -83,6 +84,11 @@ class CitySerializer(serializers.ModelSerializer):
queryset=models.Country.objects.all(), queryset=models.Country.objects.all(),
write_only=True write_only=True
) )
image_id = serializers.PrimaryKeyRelatedField(
source='image',
queryset=gallery_models.Image.objects.all(),
write_only=True
)
country = CountrySerializer(read_only=True) country = CountrySerializer(read_only=True)
class Meta: class Meta:
@ -96,6 +102,22 @@ class CitySerializer(serializers.ModelSerializer):
'country', 'country',
'postal_code', 'postal_code',
'is_island', 'is_island',
'image',
'image_id',
]
extra_fields = {
'image': {'read_only': True}
}
class CityDetailSerializer(CityBaseSerializer):
"""Serializer for detail view."""
image = ImageBaseSerializer(source='crop_image', read_only=True)
class Meta(CityBaseSerializer.Meta):
"""Meta class."""
fields = CityBaseSerializer.Meta.fields + [
'image',
] ]
@ -154,7 +176,7 @@ class AddressDetailSerializer(AddressBaseSerializer):
city_id = serializers.PrimaryKeyRelatedField( city_id = serializers.PrimaryKeyRelatedField(
source='city', write_only=True, source='city', write_only=True,
queryset=models.City.objects.all()) queryset=models.City.objects.all())
city = CitySerializer(read_only=True) city = CityBaseSerializer(read_only=True)
class Meta(AddressBaseSerializer.Meta): class Meta(AddressBaseSerializer.Meta):
"""Meta class.""" """Meta class."""

View File

@ -10,7 +10,7 @@ from tqdm import tqdm
from account.models import Role from account.models import Role
from collection.models import Collection from collection.models import Collection
from gallery.models import Image from gallery.models import Image
from location.models import Country, Region, City, Address, CityGallery from location.models import Country, Region, City, Address
from main.models import AwardType from main.models import AwardType
from news.models import News from news.models import News
from review.models import Review from review.models import Review
@ -218,29 +218,6 @@ def migrate_city_map_situation(get_exists_cities=False):
pprint(f"City info serializer errors: {serialized_data.errors}") pprint(f"City info serializer errors: {serialized_data.errors}")
def migrate_city_photos():
queryset = transfer_models.CityPhotos.objects.raw("""SELECT city_photos.id, city_photos.city_id, city_photos.attachment_file_name
FROM city_photos WHERE
city_photos.attachment_file_name IS NOT NULL AND
city_id IN(
SELECT cities.id
FROM cities WHERE
region_code IS NOT NULL AND
region_code != "" AND
country_code_2 IS NOT NULL AND
country_code_2 != ""
)
""")
queryset = [vars(query) for query in queryset]
serialized_data = location_serializers.CityGallerySerializer(data=queryset, many=True)
if serialized_data.is_valid():
serialized_data.save()
else:
pprint(f"Address serializer errors: {serialized_data.errors}")
# Update location models with ruby library # Update location models with ruby library
# Utils functions defined before transfer functions # Utils functions defined before transfer functions
def get_ruby_socket(params): def get_ruby_socket(params):
@ -554,10 +531,10 @@ def remove_old_records():
clean_old_region_records(Region, {"mysql_ids__isnull": True}) clean_old_region_records(Region, {"mysql_ids__isnull": True})
def transfer_city_gallery(): def transfer_city_photos():
created_counter = 0 created_counter = 0
cities_not_exists = {} cities_not_exists = {}
gallery_obj_exists_counter = 0 cities_has_same_image = 0
city_gallery = transfer_models.CityPhotos.objects.exclude(city__isnull=True) \ city_gallery = transfer_models.CityPhotos.objects.exclude(city__isnull=True) \
.exclude(city__country_code_2__isnull=True) \ .exclude(city__country_code_2__isnull=True) \
@ -565,7 +542,7 @@ def transfer_city_gallery():
.exclude(city__region_code__isnull=True) \ .exclude(city__region_code__isnull=True) \
.exclude(city__region_code__iexact='') \ .exclude(city__region_code__iexact='') \
.values_list('city_id', 'attachment_suffix_url') .values_list('city_id', 'attachment_suffix_url')
for old_city_id, image_suffix_url in city_gallery: for old_city_id, image_suffix_url in tqdm(city_gallery):
city = City.objects.filter(old_id=old_city_id) city = City.objects.filter(old_id=old_city_id)
if city.exists(): if city.exists():
city = city.first() city = city.first()
@ -575,19 +552,18 @@ def transfer_city_gallery():
'orientation': Image.HORIZONTAL, 'orientation': Image.HORIZONTAL,
'title': f'{city.name} - {image_suffix_url}', 'title': f'{city.name} - {image_suffix_url}',
}) })
city_gallery, created = CityGallery.objects.get_or_create(image=image, if city.image != image:
city=city, city.image = image
is_main=True) city.save()
if created:
created_counter += 1 created_counter += 1
else: else:
gallery_obj_exists_counter += 1 cities_has_same_image += 1
else: else:
cities_not_exists.update({'city_old_id': old_city_id}) cities_not_exists.update({'city_old_id': old_city_id})
print(f'Created: {created_counter}\n' print(f'Created: {created_counter}\n'
f'City not exists: {cities_not_exists}\n' f'City not exists: {cities_not_exists}\n'
f'Already added: {gallery_obj_exists_counter}') f'City has same image: {cities_has_same_image}')
@atomic @atomic
@ -758,8 +734,8 @@ def setup_clean_db():
print('update_flags') print('update_flags')
update_flags() update_flags()
print('transfer_city_gallery') print('transfer_city_photos')
transfer_city_gallery() transfer_city_photos()
def set_unused_regions(): def set_unused_regions():
@ -796,7 +772,6 @@ def set_unused_regions():
) )
data_types = { data_types = {
"dictionaries": [ "dictionaries": [
# transfer_countries, # transfer_countries,
@ -813,9 +788,6 @@ data_types = {
"update_city_info": [ "update_city_info": [
migrate_city_map_situation migrate_city_map_situation
], ],
"migrate_city_gallery": [
migrate_city_photos
],
"fix_location": [ "fix_location": [
add_fake_country, add_fake_country,
fix_location_models, fix_location_models,
@ -823,13 +795,12 @@ data_types = {
"remove_old_locations": [ "remove_old_locations": [
remove_old_records remove_old_records
], ],
"fill_city_gallery": [ "migrate_city_photos": [
transfer_city_gallery transfer_city_photos,
], ],
"add_fake_country": [ "add_fake_country": [
add_fake_country, add_fake_country,
], ],
"setup_clean_db": [setup_clean_db], "setup_clean_db": [setup_clean_db],
"set_unused_regions": [set_unused_regions], "set_unused_regions": [set_unused_regions],
"update_fake_country_flag": [update_fake_country_flag] "update_fake_country_flag": [update_fake_country_flag]

View File

@ -12,11 +12,6 @@ urlpatterns = [
path('cities/', views.CityListCreateView.as_view(), name='city-list-create'), path('cities/', views.CityListCreateView.as_view(), name='city-list-create'),
path('cities/all/', views.CityListSearchView.as_view(), name='city-list-create'), path('cities/all/', views.CityListSearchView.as_view(), name='city-list-create'),
path('cities/<int:pk>/', views.CityRUDView.as_view(), name='city-retrieve'), path('cities/<int:pk>/', views.CityRUDView.as_view(), name='city-retrieve'),
path('cities/<int:pk>/gallery/', views.CityGalleryListView.as_view(),
name='gallery-list'),
path('cities/<int:pk>/gallery/<int:image_id>/',
views.CityGalleryCreateDestroyView.as_view(),
name='gallery-create-destroy'),
path('countries/', views.CountryListCreateView.as_view(), name='country-list-create'), path('countries/', views.CountryListCreateView.as_view(), name='country-list-create'),
path('countries/<int:pk>/', views.CountryRUDView.as_view(), name='country-retrieve'), path('countries/<int:pk>/', views.CountryRUDView.as_view(), name='country-retrieve'),

View File

@ -36,7 +36,7 @@ class AddressRUDView(common.AddressViewMixin, generics.RetrieveUpdateDestroyAPIV
# City # City
class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView): class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView):
"""Create view for model City.""" """Create view for model City."""
serializer_class = serializers.CitySerializer serializer_class = serializers.CityBaseSerializer
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin] permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
queryset = models.City.objects.all() queryset = models.City.objects.all()
filter_class = filters.CityBackFilter filter_class = filters.CityBackFilter
@ -52,7 +52,7 @@ class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView):
class CityListSearchView(common.CityViewMixin, generics.ListCreateAPIView): class CityListSearchView(common.CityViewMixin, generics.ListCreateAPIView):
"""Create view for model City.""" """Create view for model City."""
serializer_class = serializers.CitySerializer serializer_class = serializers.CityBaseSerializer
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin] permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
queryset = models.City.objects.all()\ queryset = models.City.objects.all()\
.annotate(locale_name=KeyTextTransform(get_current_locale(), 'name_translated'))\ .annotate(locale_name=KeyTextTransform(get_current_locale(), 'name_translated'))\
@ -63,61 +63,10 @@ class CityListSearchView(common.CityViewMixin, generics.ListCreateAPIView):
class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView): class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView):
"""RUD view for model City.""" """RUD view for model City."""
serializer_class = serializers.CitySerializer serializer_class = serializers.CityDetailSerializer
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin] permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
class CityGalleryCreateDestroyView(common.CityViewMixin,
CreateDestroyGalleryViewMixin):
"""Resource for a create gallery for product for back-office users."""
serializer_class = serializers.CityGallerySerializer
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
def get_object(self):
"""
Returns the object the view is displaying.
"""
city_qs = self.filter_queryset(self.get_queryset())
city = get_object_or_404(city_qs, pk=self.kwargs.get('pk'))
gallery = get_object_or_404(city.city_gallery, image_id=self.kwargs.get('image_id'))
# May raise a permission denied
self.check_object_permissions(self.request, gallery)
return gallery
def create(self, request, *args, **kwargs):
try:
return super(CityGalleryCreateDestroyView, self).create(request, *args, **kwargs)
except IntegrityError as e:
if not 'unique constraint' in e.args[0]:
raise e
models.CityGallery.objects.filter(city=kwargs['pk'], is_main=request.data['is_main']).delete()
return super(CityGalleryCreateDestroyView, self).create(request, *args, **kwargs)
class CityGalleryListView(common.CityViewMixin,
generics.ListAPIView):
"""Resource for returning gallery for product for back-office users."""
serializer_class = ImageBaseSerializer
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
def get_object(self):
"""Override get_object method."""
qs = super(CityGalleryListView, self).get_queryset()
city = get_object_or_404(qs, pk=self.kwargs['pk'])
# May raise a permission denied
self.check_object_permissions(self.request, city)
return city
def get_queryset(self):
"""Override get_queryset method."""
return self.get_object().crop_gallery
# Region # Region
class RegionListCreateView(common.RegionViewMixin, generics.ListCreateAPIView): class RegionListCreateView(common.RegionViewMixin, generics.ListCreateAPIView):
"""Create view for model Region""" """Create view for model Region"""

View File

@ -85,18 +85,18 @@ class RegionUpdateView(RegionViewMixin, generics.UpdateAPIView):
# City # City
class CityCreateView(CityViewMixin, generics.CreateAPIView): class CityCreateView(CityViewMixin, generics.CreateAPIView):
"""Create view for model City""" """Create view for model City"""
serializer_class = serializers.CitySerializer serializer_class = serializers.CityBaseSerializer
class CityRetrieveView(CityViewMixin, generics.RetrieveAPIView): class CityRetrieveView(CityViewMixin, generics.RetrieveAPIView):
"""Retrieve view for model City""" """Retrieve view for model City"""
serializer_class = serializers.CitySerializer serializer_class = serializers.CityDetailSerializer
class CityListView(CityViewMixin, generics.ListAPIView): class CityListView(CityViewMixin, generics.ListAPIView):
"""List view for model City""" """List view for model City"""
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
serializer_class = serializers.CitySerializer serializer_class = serializers.CityBaseSerializer
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() qs = super().get_queryset()
@ -107,12 +107,12 @@ class CityListView(CityViewMixin, generics.ListAPIView):
class CityDestroyView(CityViewMixin, generics.DestroyAPIView): class CityDestroyView(CityViewMixin, generics.DestroyAPIView):
"""Destroy view for model City""" """Destroy view for model City"""
serializer_class = serializers.CitySerializer serializer_class = serializers.CityBaseSerializer
class CityUpdateView(CityViewMixin, generics.UpdateAPIView): class CityUpdateView(CityViewMixin, generics.UpdateAPIView):
"""Update view for model City""" """Update view for model City"""
serializer_class = serializers.CitySerializer serializer_class = serializers.CityBaseSerializer
# Address # Address

View File

@ -14,9 +14,18 @@ class SiteSettingsAdmin(admin.ModelAdmin):
inlines = [SiteSettingsInline, ] inlines = [SiteSettingsInline, ]
@admin.register(models.SiteFeature)
class SiteFeatureAdmin(admin.ModelAdmin):
"""Site feature admin conf."""
list_display = ['id', 'site_settings', 'feature',
'published', 'main', 'backoffice', ]
raw_id_fields = ['site_settings', 'feature', ]
@admin.register(models.Feature) @admin.register(models.Feature)
class FeatureAdmin(admin.ModelAdmin): class FeatureAdmin(admin.ModelAdmin):
"""Feature admin conf.""" """Feature admin conf."""
list_display = ['id', '__str__', 'priority', 'route', ]
@admin.register(models.AwardType) @admin.register(models.AwardType)
@ -46,6 +55,7 @@ class CarouselAdmin(admin.ModelAdmin):
@admin.register(models.PageType) @admin.register(models.PageType)
class PageTypeAdmin(admin.ModelAdmin): class PageTypeAdmin(admin.ModelAdmin):
"""PageType admin.""" """PageType admin."""
list_display = ['id', '__str__', ]
@admin.register(models.Page) @admin.register(models.Page)
@ -80,3 +90,13 @@ class PanelAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'user', 'created',) list_display = ('id', 'name', 'user', 'created',)
raw_id_fields = ('user',) raw_id_fields = ('user',)
list_display_links = ('id', 'name',) list_display_links = ('id', 'name',)
@admin.register(models.NavigationBarPermission)
class NavigationBarPermissionAdmin(admin.ModelAdmin):
"""NavigationBarPermission admin."""
list_display = ('id', 'permission_mode_display', )
def permission_mode_display(self, obj):
"""Get permission mode display."""
return obj.get_permission_mode_display()

View File

@ -0,0 +1,44 @@
# Generated by Django 2.2.7 on 2020-01-14 12:18
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('main', '0045_carousel_is_international'),
]
operations = [
migrations.AddField(
model_name='sitefeature',
name='backoffice',
field=models.BooleanField(default=False, help_text='shows on backoffice page', verbose_name='backoffice'),
),
migrations.AlterField(
model_name='sitefeature',
name='main',
field=models.BooleanField(default=False, help_text='shows on main page', verbose_name='Main'),
),
migrations.AlterField(
model_name='sitefeature',
name='nested',
field=models.ManyToManyField(to='main.SiteFeature', blank=True),
),
migrations.CreateModel(
name='NavigationBarPermission',
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')),
('permission_mode', models.PositiveSmallIntegerField(choices=[(0, 'read'), (1, 'write')], default=0, help_text='READ - allows only retrieve data,WRITE - allows to perform any operations over the object', verbose_name='permission mode')),
('section', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.SiteFeature', verbose_name='section')),
],
options={
'verbose_name': 'Navigation bar item permission',
'verbose_name_plural': 'Navigation bar item permissions',
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2020-01-15 10:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0046_auto_20200114_1218'),
]
operations = [
migrations.AlterField(
model_name='feature',
name='priority',
field=models.IntegerField(blank=True, default=None, null=True),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 2.2.7 on 2020-01-15 19:44
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('main', '0047_auto_20200115_1013'),
]
operations = [
migrations.AddField(
model_name='navigationbarpermission',
name='sections',
field=models.ManyToManyField(to='main.SiteFeature', verbose_name='sections'),
),
migrations.AlterField(
model_name='navigationbarpermission',
name='section',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='old_sections', to='main.SiteFeature', verbose_name='section'),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 2.2.7 on 2020-01-15 20:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0048_auto_20200115_1944'),
]
operations = [
migrations.RemoveField(
model_name='navigationbarpermission',
name='section',
),
]

View File

@ -6,16 +6,18 @@ from django.contrib.contenttypes import fields as generic
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import JSONField from django.contrib.postgres.fields import JSONField
from django.core.validators import EMPTY_VALUES from django.core.validators import EMPTY_VALUES
from django.db import connections, connection from django.db import connections
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from mptt.models import MPTTModel, TreeForeignKey
from rest_framework import exceptions from rest_framework import exceptions
from configuration.models import TranslationSettings from configuration.models import TranslationSettings
from location.models import Country from location.models import Country
from main import methods from main import methods
from review.models import Review from review.models import Review
from tag.models import Tag
from utils.exceptions import UnprocessableEntityError from utils.exceptions import UnprocessableEntityError
from utils.methods import dictfetchall from utils.methods import dictfetchall
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin, from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
@ -112,11 +114,13 @@ class Feature(ProjectBaseMixin, PlatformMixin):
"""Feature model.""" """Feature model."""
slug = models.SlugField(max_length=255, unique=True) slug = models.SlugField(max_length=255, unique=True)
priority = models.IntegerField(unique=True, null=True, default=None) priority = models.IntegerField(blank=True, null=True, default=None)
route = models.ForeignKey('PageType', on_delete=models.PROTECT, null=True, default=None) route = models.ForeignKey('PageType', on_delete=models.PROTECT, null=True, default=None)
site_settings = models.ManyToManyField(SiteSettings, through='SiteFeature') site_settings = models.ManyToManyField(SiteSettings, through='SiteFeature')
old_id = models.IntegerField(null=True, blank=True) old_id = models.IntegerField(null=True, blank=True)
chosen_tags = generic.GenericRelation(to='tag.ChosenTag')
class Meta: class Meta:
"""Meta class.""" """Meta class."""
verbose_name = _('Feature') verbose_name = _('Feature')
@ -125,6 +129,10 @@ class Feature(ProjectBaseMixin, PlatformMixin):
def __str__(self): def __str__(self):
return f'{self.slug}' return f'{self.slug}'
@property
def get_chosen_tags(self):
return Tag.objects.filter(chosen_tags__in=self.chosen_tags.all()).distinct()
class SiteFeatureQuerySet(models.QuerySet): class SiteFeatureQuerySet(models.QuerySet):
"""Extended queryset for SiteFeature model.""" """Extended queryset for SiteFeature model."""
@ -144,9 +152,15 @@ class SiteFeature(ProjectBaseMixin):
site_settings = models.ForeignKey(SiteSettings, on_delete=models.CASCADE) site_settings = models.ForeignKey(SiteSettings, on_delete=models.CASCADE)
feature = models.ForeignKey(Feature, on_delete=models.PROTECT) feature = models.ForeignKey(Feature, on_delete=models.PROTECT)
published = models.BooleanField(default=False, verbose_name=_('Published')) published = models.BooleanField(default=False, verbose_name=_('Published'))
main = models.BooleanField(default=False, verbose_name=_('Main')) main = models.BooleanField(default=False,
nested = models.ManyToManyField('self', symmetrical=False) help_text='shows on main page',
verbose_name=_('Main'),)
backoffice = models.BooleanField(default=False,
help_text='shows on backoffice page',
verbose_name=_('backoffice'),)
nested = models.ManyToManyField('self', blank=True, symmetrical=False)
old_id = models.IntegerField(null=True, blank=True) old_id = models.IntegerField(null=True, blank=True)
objects = SiteFeatureQuerySet.as_manager() objects = SiteFeatureQuerySet.as_manager()
@ -216,6 +230,8 @@ class CarouselQuerySet(models.QuerySet):
def by_country_code(self, code): def by_country_code(self, code):
"""Filter collection by country code.""" """Filter collection by country code."""
if code in settings.INTERNATIONAL_COUNTRY_CODES:
return self.filter(is_international=True)
return self.filter(country__code=code) return self.filter(country__code=code)
def get_international(self): def get_international(self):
@ -520,3 +536,28 @@ class Panel(ProjectBaseMixin):
params = params + new_params params = params + new_params
query = self.query + limit_offset query = self.query + limit_offset
return query, params return query, params
class NavigationBarPermission(ProjectBaseMixin):
"""Model for navigation bar item permissions."""
READ = 0
WRITE = 1
PERMISSION_MODES = (
(READ, _('read')),
(WRITE, _('write')),
)
sections = models.ManyToManyField('main.SiteFeature',
verbose_name=_('sections'))
permission_mode = models.PositiveSmallIntegerField(choices=PERMISSION_MODES,
default=READ,
help_text='READ - allows only retrieve data,'
'WRITE - allows to perform any '
'operations over the object',
verbose_name=_('permission mode'))
class Meta:
"""Meta class."""
verbose_name = _('Navigation bar item permission')
verbose_name_plural = _('Navigation bar item permissions')

View File

@ -0,0 +1 @@
from main.serializers.common import *

View File

@ -0,0 +1,29 @@
from rest_framework import serializers
from account.models import User
from account.serializers import BackUserSerializer
from main import models
class PanelSerializer(serializers.ModelSerializer):
"""Serializer for Custom panel."""
user_id = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(),
source='user',
write_only=True
)
user = BackUserSerializer(read_only=True)
class Meta:
model = models.Panel
fields = [
'id',
'name',
'display',
'description',
'query',
'created',
'modified',
'user',
'user_id'
]

View File

@ -4,9 +4,8 @@ from rest_framework import serializers
from location.serializers import CountrySerializer from location.serializers import CountrySerializer
from main import models from main import models
from tag.serializers import TagBackOfficeSerializer
from utils.serializers import ProjectModelSerializer, TranslatedField, RecursiveFieldSerializer from utils.serializers import ProjectModelSerializer, TranslatedField, RecursiveFieldSerializer
from account.serializers.back import BackUserSerializer
from account.models import User
class FeatureSerializer(serializers.ModelSerializer): class FeatureSerializer(serializers.ModelSerializer):
@ -84,24 +83,46 @@ class FooterBackSerializer(FooterSerializer):
class SiteFeatureSerializer(serializers.ModelSerializer): class SiteFeatureSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(source='feature.id') id = serializers.IntegerField(source='feature.id', allow_null=True)
slug = serializers.CharField(source='feature.slug') slug = serializers.CharField(source='feature.slug', allow_null=True)
priority = serializers.IntegerField(source='feature.priority') priority = serializers.IntegerField(source='feature.priority', allow_null=True)
route = serializers.CharField(source='feature.route.name') route = serializers.CharField(source='feature.route.name', allow_null=True)
source = serializers.IntegerField(source='feature.source') source = serializers.IntegerField(source='feature.source', allow_null=True)
nested = RecursiveFieldSerializer(many=True, allow_null=True) nested = RecursiveFieldSerializer(many=True, read_only=True, allow_null=True)
chosen_tags = TagBackOfficeSerializer(
source='feature.get_chosen_tags', many=True, read_only=True)
class Meta: class Meta:
"""Meta class.""" """Meta class."""
model = models.SiteFeature model = models.SiteFeature
fields = ('main', fields = (
'id', 'id',
'slug', 'main',
'priority', 'slug',
'route', 'priority',
'source', 'route',
'nested', 'source',
) 'nested',
'chosen_tags',
)
class NavigationBarSectionBaseSerializer(SiteFeatureSerializer):
"""Serializer for navigation bar."""
source_display = serializers.CharField(source='feature.get_source_display',
read_only=True)
class Meta(SiteFeatureSerializer.Meta):
model = models.SiteFeature
fields = [
'id',
'slug',
'route',
'source',
'source_display',
'priority',
'nested',
]
class SiteSettingsSerializer(serializers.ModelSerializer): class SiteSettingsSerializer(serializers.ModelSerializer):
@ -291,30 +312,6 @@ class ContentTypeBackSerializer(serializers.ModelSerializer):
fields = '__all__' fields = '__all__'
class PanelSerializer(serializers.ModelSerializer):
"""Serializer for Custom panel."""
user_id = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(),
source='user',
write_only=True
)
user = BackUserSerializer(read_only=True)
class Meta:
model = models.Panel
fields = [
'id',
'name',
'display',
'description',
'query',
'created',
'modified',
'user',
'user_id'
]
class PanelExecuteSerializer(serializers.ModelSerializer): class PanelExecuteSerializer(serializers.ModelSerializer):
"""Panel execute serializer.""" """Panel execute serializer."""
@ -331,3 +328,20 @@ class PanelExecuteSerializer(serializers.ModelSerializer):
'user', 'user',
'user_id' 'user_id'
] ]
class NavigationBarPermissionBaseSerializer(serializers.ModelSerializer):
"""Navigation bar permission serializer."""
sections = NavigationBarSectionBaseSerializer(many=True, read_only=True)
permission_mode_display = serializers.CharField(source='get_permission_mode_display',
read_only=True)
class Meta:
"""Meta class."""
model = models.NavigationBarPermission
fields = [
'id',
'sections',
'permission_mode_display',
]

View File

@ -6,6 +6,7 @@ from rest_framework.generics import get_object_or_404
from rest_framework.response import Response from rest_framework.response import Response
from main import serializers from main import serializers
from main.serializers.back import PanelSerializer
from main import tasks from main import tasks
from main.filters import AwardFilter from main.filters import AwardFilter
from main.models import Award, Footer, PageType, Panel, SiteFeature, Feature from main.models import Award, Footer, PageType, Panel, SiteFeature, Feature
@ -108,7 +109,7 @@ class PanelsListCreateView(generics.ListCreateAPIView):
permission_classes = ( permission_classes = (
permissions.IsAdminUser, permissions.IsAdminUser,
) )
serializer_class = serializers.PanelSerializer serializer_class = PanelSerializer
queryset = Panel.objects.all() queryset = Panel.objects.all()
@ -117,7 +118,7 @@ class PanelsRUDView(generics.RetrieveUpdateDestroyAPIView):
permission_classes = ( permission_classes = (
permissions.IsAdminUser, permissions.IsAdminUser,
) )
serializer_class = serializers.PanelSerializer serializer_class = PanelSerializer
queryset = Panel.objects.all() queryset = Panel.objects.all()

View File

@ -0,0 +1,69 @@
# coding=utf-8
from django.core.management.base import BaseCommand
from utils.methods import get_url_images_in_text, get_image_meta_by_url
from news.models import News
from sorl.thumbnail import get_thumbnail
class Command(BaseCommand):
IMAGE_MAX_SIZE_IN_BYTES = 1048576 # ~ 1mb
IMAGE_QUALITY_PERCENTS = 50
def add_arguments(self, parser):
parser.add_argument(
'-s',
'--size',
default=self.IMAGE_MAX_SIZE_IN_BYTES,
help='Максимальный размер файла в байтах',
type=int
)
parser.add_argument(
'-q',
'--quality',
default=self.IMAGE_QUALITY_PERCENTS,
help='Качество изображения',
type=int
)
def optimize(self, text, max_size, max_quality):
"""optimize news images"""
if isinstance(text, str):
for image in get_url_images_in_text(text):
try:
size, width, height = get_image_meta_by_url(image)
except IOError as ie:
self.stdout.write(self.style.NOTICE(f'{ie}\n'))
continue
if size < max_size:
self.stdout.write(self.style.SUCCESS(f'No need to compress images size is {size / (2**20)}Mb\n'))
continue
percents = round(max_size / (size * 0.01))
width = round(width * percents / 100)
height = round(height * percents / 100)
optimized_image = get_thumbnail(
file_=image,
geometry_string=f'{width}x{height}',
upscale=False,
quality=max_quality
).url
text = text.replace(image, optimized_image)
self.stdout.write(self.style.SUCCESS(f'Optimized {image} -> {optimized_image}\n'
f'Quality [{percents}%]\n'))
return text
def handle(self, *args, **options):
size = options['size']
quality = options['quality']
for news in News.objects.all():
if not isinstance(news.description, dict):
continue
news.description = {
locale: self.optimize(text, size, quality)
for locale, text in news.description.items()
}
news.save()

View File

@ -7,6 +7,7 @@ from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import HStoreField from django.contrib.postgres.fields import HStoreField
from django.db import models from django.db import models
from django.db.models import Case, When from django.db.models import Case, When
from django.urls.exceptions import NoReverseMatch
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
@ -54,7 +55,6 @@ class NewsType(models.Model):
name = models.CharField(_('name'), max_length=250) name = models.CharField(_('name'), max_length=250)
tag_categories = models.ManyToManyField('tag.TagCategory', tag_categories = models.ManyToManyField('tag.TagCategory',
related_name='news_types') related_name='news_types')
chosen_tags = generic.GenericRelation(to='tag.ChosenTag')
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -79,7 +79,7 @@ class NewsQuerySet(TranslationQuerysetMixin):
def with_base_related(self): def with_base_related(self):
"""Return qs with related objects.""" """Return qs with related objects."""
return self.select_related('news_type', 'country').prefetch_related('tags', 'tags__translation') return self.select_related('news_type', 'country').prefetch_related('tags', 'tags__translation', 'gallery')
def with_extended_related(self): def with_extended_related(self):
"""Return qs with related objects.""" """Return qs with related objects."""
@ -296,7 +296,10 @@ class News(GalleryMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin,
@property @property
def web_url(self): def web_url(self):
return reverse('web:news:rud', kwargs={'slug': next(iter(self.slugs.values()))}) try:
return reverse('web:news:rud', kwargs={'slug': next(iter(self.slugs.values()))})
except NoReverseMatch as e:
return None # no active links
def should_read(self, user): def should_read(self, user):
return self.__class__.objects.should_read(self, user)[:3] return self.__class__.objects.should_read(self, user)[:3]
@ -307,8 +310,9 @@ class News(GalleryMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin,
@property @property
def main_image(self): def main_image(self):
qs = self.news_gallery.main_image() qs = self.news_gallery.main_image()
if qs.exists(): image_model = qs.order_by('-id').first()
return qs.order_by('-id').first().image if image_model is not None:
return image_model.image
@property @property
def image_url(self): def image_url(self):

View File

@ -22,7 +22,9 @@ from utils.serializers import (
class AgendaSerializer(ProjectModelSerializer): class AgendaSerializer(ProjectModelSerializer):
start_datetime = serializers.DateTimeField() start_datetime = serializers.DateTimeField()
end_datetime = serializers.DateTimeField() end_datetime = serializers.DateTimeField()
address = AddressBaseSerializer() address = AddressBaseSerializer(read_only=True)
address_id = serializers.PrimaryKeyRelatedField(write_only=True, queryset=location_models.Address.objects.all(),
source='address')
event_name_translated = TranslatedField() event_name_translated = TranslatedField()
content_translated = TranslatedField() content_translated = TranslatedField()
@ -36,7 +38,8 @@ class AgendaSerializer(ProjectModelSerializer):
'end_datetime', 'end_datetime',
'address', 'address',
'content_translated', 'content_translated',
'event_name_translated' 'event_name_translated',
'address_id',
) )
@ -158,6 +161,7 @@ class NewsDetailWebSerializer(NewsDetailSerializer):
should_read = SerializerMethodField() should_read = SerializerMethodField()
agenda = AgendaSerializer() agenda = AgendaSerializer()
banner = NewsBannerSerializer() banner = NewsBannerSerializer()
in_favorites = serializers.BooleanField(read_only=True)
class Meta(NewsDetailSerializer.Meta): class Meta(NewsDetailSerializer.Meta):
"""Meta class.""" """Meta class."""
@ -167,6 +171,7 @@ class NewsDetailWebSerializer(NewsDetailSerializer):
'should_read', 'should_read',
'agenda', 'agenda',
'banner', 'banner',
'in_favorites',
) )
def get_same_theme(self, obj): def get_same_theme(self, obj):
@ -176,10 +181,31 @@ class NewsDetailWebSerializer(NewsDetailSerializer):
return NewsSimilarListSerializer(obj.should_read(self.context['request'].user), many=True, read_only=True).data return NewsSimilarListSerializer(obj.should_read(self.context['request'].user), many=True, read_only=True).data
class NewsPreviewWebSerializer(NewsDetailSerializer):
"""News preview serializer for web users.."""
same_theme = SerializerMethodField()
agenda = AgendaSerializer()
banner = NewsBannerSerializer()
class Meta(NewsDetailSerializer.Meta):
"""Meta class."""
fields = NewsDetailSerializer.Meta.fields + (
'same_theme',
'agenda',
'banner',
)
def get_same_theme(self, obj):
return NewsSimilarListSerializer(obj.same_theme(self.context['request'].user), many=True, read_only=True).data
class NewsBackOfficeBaseSerializer(NewsBaseSerializer): class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
"""News back office base serializer.""" """News back office base serializer."""
is_published = serializers.BooleanField(source='is_publish', read_only=True) is_published = serializers.BooleanField(source='is_publish', read_only=True)
descriptions = serializers.ListField(required=False) descriptions = serializers.ListField(required=False)
agenda = AgendaSerializer()
class Meta(NewsBaseSerializer.Meta): class Meta(NewsBaseSerializer.Meta):
"""Meta class.""" """Meta class."""
@ -198,6 +224,7 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
'created', 'created',
'modified', 'modified',
'descriptions', 'descriptions',
'agenda'
) )
extra_kwargs = { extra_kwargs = {
'created': {'read_only': True}, 'created': {'read_only': True},
@ -228,6 +255,7 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
for locale in locales: for locale in locales:
if not attrs[key].get(locale): if not attrs[key].get(locale):
attrs[key][locale] = getattr(instance, key).get(locale) attrs[key][locale] = getattr(instance, key).get(locale)
return attrs return attrs
def create(self, validated_data): def create(self, validated_data):
@ -245,10 +273,25 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
user = request.user user = request.user
validated_data['created_by'] = user validated_data['created_by'] = user
return super().create(validated_data) agenda_data = validated_data.get('agenda')
agenda = None
if agenda_data is not None:
agenda_data['address_id'] = agenda_data.pop('address').pk
agenda_serializer = AgendaSerializer(data=agenda_data)
agenda_serializer.is_valid(raise_exception=True)
agenda = agenda_serializer.save()
instance = super().create(validated_data)
instance.agenda = agenda
instance.save()
return instance
def update(self, instance, validated_data): def update(self, instance, validated_data):
slugs = validated_data.get('slugs') slugs = validated_data.get('slugs')
slugs_list = list(map(lambda x: x.lower(), slugs.values() if slugs else ()))
slugs_set = set(slugs_list)
if slugs: if slugs:
slugs_list = list(map(lambda x: x.lower(), slugs.values())) slugs_list = list(map(lambda x: x.lower(), slugs.values()))
slugs_set = set(slugs_list) slugs_set = set(slugs_list)
@ -256,6 +299,29 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
slugs__values__contains=list(slugs.values()) slugs__values__contains=list(slugs.values())
).exists() or len(slugs_list) != len(slugs_set): ).exists() or len(slugs_list) != len(slugs_set):
raise serializers.ValidationError({'slugs': _('Slug should be unique')}) raise serializers.ValidationError({'slugs': _('Slug should be unique')})
agenda_data = validated_data.get('agenda')
agenda = instance.agenda
if agenda is None and agenda_data is not None:
agenda_data['address_id'] = agenda_data.pop('address').pk
agenda_serializer = AgendaSerializer(data=agenda_data)
agenda_serializer.is_valid(raise_exception=True)
agenda_serializer.save()
elif agenda_data is not None:
agenda.start_datetime = agenda_data.pop(
'start_datetime') if 'start_datetime' in agenda_data else agenda.start_datetime
agenda.end_datetime = agenda_data.pop(
'end_datetime') if 'end_datetime' in agenda_data else agenda.end_datetime
agenda.address = agenda_data.pop(
'address') if 'address' in agenda_data else agenda.address
agenda.event_name = agenda_data.pop(
'event_name') if 'event_time' in agenda_data else agenda.event_name
agenda.content = agenda_data.pop(
'content') if 'content' in agenda_data else agenda.content
agenda.save()
return super().update(instance, validated_data) return super().update(instance, validated_data)

View File

@ -11,6 +11,7 @@ from tag.models import TagCategory, Tag
from translation.models import SiteInterfaceDictionary from translation.models import SiteInterfaceDictionary
from transfer.models import PageTexts, PageCounters, PageMetadata from transfer.models import PageTexts, PageCounters, PageMetadata
from transfer.serializers.news import NewsSerializer from transfer.serializers.news import NewsSerializer
from utils.methods import transform_camelcase_to_underscore
def add_locale(locale, data): def add_locale(locale, data):
@ -36,35 +37,38 @@ def clear_old_news():
images.delete() images.delete()
news.delete() news.delete()
# NewsType.objects.all().delete()
print(f'Deleted {img_num} images') print(f'Deleted {img_num} images')
print(f'Deleted {news_num} news') print(f'Deleted {news_num} news')
def transfer_news(): def transfer_news():
news_type, _ = NewsType.objects.get_or_create(name='news') migrated_news_types = ('News', 'StaticPage', )
queryset = PageTexts.objects.filter( for news_type in migrated_news_types:
page__type='News', news_type_obj, _ = NewsType.objects.get_or_create(
).annotate( name=transform_camelcase_to_underscore(news_type))
page__id=F('page__id'),
news_type_id=Value(news_type.id, output_field=IntegerField()),
page__created_at=F('page__created_at'),
page__account_id=F('page__account_id'),
page__state=F('page__state'),
page__template=F('page__template'),
page__site__country_code_2=F('page__site__country_code_2'),
page__root_title=F('page__root_title'),
page__attachment_suffix_url=F('page__attachment_suffix_url'),
page__published_at=F('page__published_at'),
)
serialized_data = NewsSerializer(data=list(queryset.values()), many=True) queryset = PageTexts.objects.filter(
if serialized_data.is_valid(): page__type=news_type,
serialized_data.save() ).annotate(
else: page__id=F('page__id'),
pprint(f'News serializer errors: {serialized_data.errors}') news_type_id=Value(news_type_obj.id, output_field=IntegerField()),
page__created_at=F('page__created_at'),
page__account_id=F('page__account_id'),
page__state=F('page__state'),
page__template=F('page__template'),
page__site__country_code_2=F('page__site__country_code_2'),
page__root_title=F('page__root_title'),
page__attachment_suffix_url=F('page__attachment_suffix_url'),
page__published_at=F('page__published_at'),
)
serialized_data = NewsSerializer(data=list(queryset.values()), many=True)
if serialized_data.is_valid():
serialized_data.save()
else:
pprint(f'News serializer errors: {serialized_data.errors}')
def update_en_gb_locales(): def update_en_gb_locales():
@ -166,5 +170,5 @@ data_types = {
update_en_gb_locales, update_en_gb_locales,
add_views_count, add_views_count,
add_tags, add_tags,
] ],
} }

View File

@ -7,4 +7,5 @@ common_urlpatterns = [
path('slug/<slug:slug>/', views.NewsDetailView.as_view(), name='rud'), path('slug/<slug:slug>/', views.NewsDetailView.as_view(), name='rud'),
path('slug/<slug:slug>/favorites/', views.NewsFavoritesCreateDestroyView.as_view(), path('slug/<slug:slug>/favorites/', views.NewsFavoritesCreateDestroyView.as_view(),
name='create-destroy-favorites'), name='create-destroy-favorites'),
path('preview/slug/<slug:slug>/', views.NewsPreviewView.as_view(), name='preview'),
] ]

View File

@ -70,6 +70,18 @@ class NewsDetailView(NewsMixinView, generics.RetrieveAPIView):
return qs return qs
class NewsPreviewView(NewsMixinView, generics.RetrieveAPIView):
"""News preview view."""
lookup_field = None
serializer_class = serializers.NewsPreviewWebSerializer
def get_queryset(self):
"""Override get_queryset method."""
qs = models.News.objects.all().annotate_in_favorites(self.request.user)
return qs
class NewsTypeListView(generics.ListAPIView): class NewsTypeListView(generics.ListAPIView):
"""NewsType list view.""" """NewsType list view."""

View File

@ -1,3 +1,20 @@
from django.contrib import admin from django.contrib import admin
from notification import models
# Register your models here.
@admin.register(models.SubscriptionType)
class SubscriptionTypeAdmin(admin.ModelAdmin):
"""SubscriptionType admin."""
list_display = ['index_name', 'country']
@admin.register(models.Subscriber)
class SubscriberAdmin(admin.ModelAdmin):
"""Subscriber admin."""
raw_id_fields = ('user',)
@admin.register(models.Subscribe)
class SubscribeAdmin(admin.ModelAdmin):
"""Subscribe admin."""
raw_id_fields = ('subscriber',)

View File

@ -38,6 +38,13 @@ class ProductType(TypeDefaultImageMixin, TranslatedFieldsMixin, ProjectBaseMixin
(SOUVENIR, 'souvenir'), (SOUVENIR, 'souvenir'),
(BOOK, 'book') (BOOK, 'book')
) )
INDEX_PLURAL_ONE = {
'food': 'food',
'wines': 'wine',
'liquors': 'liquor',
}
name = TJSONField(blank=True, null=True, default=None, name = TJSONField(blank=True, null=True, default=None,
verbose_name=_('Name'), help_text='{"en-GB":"some text"}') verbose_name=_('Name'), help_text='{"en-GB":"some text"}')
index_name = models.CharField(max_length=50, unique=True, db_index=True, index_name = models.CharField(max_length=50, unique=True, db_index=True,
@ -176,7 +183,7 @@ class ProductQuerySet(models.QuerySet):
"""Return objects with geo location.""" """Return objects with geo location."""
return self.filter(establishment__address__coordinates__isnull=False) return self.filter(establishment__address__coordinates__isnull=False)
def same_subtype(self, product): def annotate_same_subtype(self, product):
"""Annotate flag same subtype.""" """Annotate flag same subtype."""
return self.annotate(same_subtype=Case( return self.annotate(same_subtype=Case(
models.When( models.When(
@ -215,7 +222,7 @@ class ProductQuerySet(models.QuerySet):
similarity_rules['ordering'].append(F('distance').asc()) similarity_rules['ordering'].append(F('distance').asc())
similarity_rules['distinction'].append('distance') similarity_rules['distinction'].append('distance')
return self.similar_base(product) \ return self.similar_base(product) \
.same_subtype(product) \ .annotate_same_subtype(product) \
.order_by(*similarity_rules['ordering']) \ .order_by(*similarity_rules['ordering']) \
.distinct(*similarity_rules['distinction'], .distinct(*similarity_rules['distinction'],
'id') 'id')

View File

@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from comment.models import Comment from comment.models import Comment
from comment.serializers import CommentSerializer from comment.serializers import CommentBaseSerializer
from establishment.serializers import EstablishmentProductShortSerializer from establishment.serializers import EstablishmentProductShortSerializer
from establishment.serializers.common import _EstablishmentAddressShortSerializer from establishment.serializers.common import _EstablishmentAddressShortSerializer
from location.serializers import WineOriginRegionBaseSerializer,\ from location.serializers import WineOriginRegionBaseSerializer,\
@ -200,13 +200,11 @@ class ProductFavoritesCreateSerializer(FavoritesCreateSerializer):
return super().create(validated_data) return super().create(validated_data)
class ProductCommentCreateSerializer(CommentSerializer): class ProductCommentBaseSerializer(CommentBaseSerializer):
"""Create comment serializer""" """Create comment serializer."""
mark = serializers.IntegerField()
class Meta: class Meta(CommentBaseSerializer.Meta):
"""Serializer for model Comment""" """Serializer for model Comment"""
model = Comment
fields = [ fields = [
'id', 'id',
'created', 'created',
@ -214,8 +212,14 @@ class ProductCommentCreateSerializer(CommentSerializer):
'mark', 'mark',
'nickname', 'nickname',
'profile_pic', 'profile_pic',
'status',
'status_display',
] ]
class ProductCommentCreateSerializer(ProductCommentBaseSerializer):
"""Serializer for creating comments for product."""
def validate(self, attrs): def validate(self, attrs):
"""Override validate method""" """Override validate method"""
# Check product object # Check product object

View File

@ -19,10 +19,13 @@ urlpatterns = [
# similar products by type/subtype # similar products by type/subtype
# temporary uses single mechanism, bec. description in process # temporary uses single mechanism, bec. description in process
path('slug/<slug:slug>/similar/wines/', views.SimilarListView.as_view(), # path('slug/<slug:slug>/similar/wines/', views.SimilarListView.as_view(),
name='similar-wine'), # name='similar-wine'),
path('slug/<slug:slug>/similar/liquors/', views.SimilarListView.as_view(), # path('slug/<slug:slug>/similar/liquors/', views.SimilarListView.as_view(),
name='similar-liquor'), # name='similar-liquor'),
path('slug/<slug:slug>/similar/food/', views.SimilarListView.as_view(), # path('slug/<slug:slug>/similar/food/', views.SimilarListView.as_view(),
name='similar-food'), # name='similar-food'),
path('slug/<slug:slug>/similar/<str:type>/', views.SimilarListView.as_view(),
name='similar-products')
] ]

View File

@ -4,12 +4,10 @@ from django.shortcuts import get_object_or_404
from rest_framework import generics, permissions from rest_framework import generics, permissions
from comment.models import Comment from comment.models import Comment
from comment.serializers import CommentRUDSerializer from comment.serializers import CommentBaseSerializer
from product import filters, serializers from product import filters, serializers
from product.models import Product from product.models import Product, ProductType
from utils.views import FavoritesCreateDestroyMixinView from utils.views import FavoritesCreateDestroyMixinView
from utils.pagination import PortionPagination
from django.conf import settings
class ProductBaseView(generics.GenericAPIView): class ProductBaseView(generics.GenericAPIView):
@ -44,8 +42,16 @@ class ProductSimilarView(ProductListView):
""" """
Return base product instance for a getting list of similar products. Return base product instance for a getting list of similar products.
""" """
product = get_object_or_404(Product.objects.all(), find_by = {
slug=self.kwargs.get('slug')) 'slug': self.kwargs.get('slug'),
}
if isinstance(self.kwargs.get('type'), str):
if not self.kwargs.get('type') in ProductType.INDEX_PLURAL_ONE:
return None
find_by['product_type'] = get_object_or_404(ProductType.objects.all(), index_name=ProductType.INDEX_PLURAL_ONE[self.kwargs.get('type')])
product = get_object_or_404(Product.objects.all(), **find_by)
return product return product
@ -73,17 +79,17 @@ class ProductCommentListView(generics.ListAPIView):
"""View for return list of product comments.""" """View for return list of product comments."""
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
serializer_class = serializers.ProductCommentCreateSerializer serializer_class = serializers.ProductCommentBaseSerializer
def get_queryset(self): def get_queryset(self):
"""Override get_queryset method""" """Override get_queryset method"""
product = get_object_or_404(Product, slug=self.kwargs['slug']) product = get_object_or_404(Product, slug=self.kwargs['slug'])
return product.comments.order_by('-created') return product.comments.public(self.request.user).order_by('-created')
class ProductCommentRUDView(generics.RetrieveUpdateDestroyAPIView): class ProductCommentRUDView(generics.RetrieveUpdateDestroyAPIView):
"""View for retrieve/update/destroy product comment.""" """View for retrieve/update/destroy product comment."""
serializer_class = CommentRUDSerializer serializer_class = serializers.ProductCommentBaseSerializer
queryset = Product.objects.all() queryset = Product.objects.all()
def get_object(self): def get_object(self):

View File

@ -77,15 +77,15 @@ class EstablishmentDocument(Document):
'value': fields.KeywordField(), 'value': fields.KeywordField(),
}, },
multi=True, attr='artisan_category_indexing') multi=True, attr='artisan_category_indexing')
visible_tags = fields.ObjectField( distillery_type = fields.ObjectField(
properties={ properties={
'id': fields.IntegerField(attr='id'), 'id': fields.IntegerField(attr='id'),
'label': fields.ObjectField(attr='label_indexing', 'label': fields.ObjectField(attr='label_indexing',
properties=OBJECT_FIELD_PROPERTIES), properties=OBJECT_FIELD_PROPERTIES),
'value': fields.KeywordField(), 'value': fields.KeywordField(),
}, },
multi=True) multi=True, attr='distillery_type_indexing')
distillery_types = fields.ObjectField( visible_tags = fields.ObjectField(
properties={ properties={
'id': fields.IntegerField(attr='id'), 'id': fields.IntegerField(attr='id'),
'label': fields.ObjectField(attr='label_indexing', 'label': fields.ObjectField(attr='label_indexing',
@ -153,6 +153,7 @@ class EstablishmentDocument(Document):
'id': fields.IntegerField(attr='id'), 'id': fields.IntegerField(attr='id'),
'weekday': fields.IntegerField(attr='weekday'), 'weekday': fields.IntegerField(attr='weekday'),
'weekday_display': fields.KeywordField(attr='get_weekday_display'), 'weekday_display': fields.KeywordField(attr='get_weekday_display'),
'weekday_display_short': fields.KeywordField(attr='weekday_display_short'),
'closed_at': fields.KeywordField(attr='closed_at_str'), 'closed_at': fields.KeywordField(attr='closed_at_str'),
'opening_at': fields.KeywordField(attr='opening_at_str'), 'opening_at': fields.KeywordField(attr='opening_at_str'),
'closed_at_indexing': fields.DateField(), 'closed_at_indexing': fields.DateField(),

View File

@ -71,5 +71,5 @@ class NewsDocument(Document):
The related_models option should be used with caution because it can lead in the index The related_models option should be used with caution because it can lead in the index
to the updating of a lot of items. to the updating of a lot of items.
""" """
if isinstance(related_instance, models.NewsType): if isinstance(related_instance, models.NewsType) and hasattr(related_instance, 'news_set'):
return related_instance.news_set.all() return related_instance.news_set.all()

View File

@ -108,9 +108,9 @@ class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend):
tag_facets = [] tag_facets = []
preserve_ids = [] preserve_ids = []
facet_name = '_filter_' + __field facet_name = '_filter_' + __field
all_tag_categories = TagCategoryDocument.search() \ all_tag_categories = list(TagCategoryDocument.search() \
.filter('term', public=True) \ .filter('term', public=True) \
.filter(Q('term', value_type=TagCategory.LIST) | Q('match', index_name='wine-color')) .filter(Q('term', value_type=TagCategory.LIST) | Q('match', index_name='wine-color'))[0:1000])
for category in all_tag_categories: for category in all_tag_categories:
tags_to_remove = list(map(lambda t: str(t.id), category.tags)) tags_to_remove = list(map(lambda t: str(t.id), category.tags))
qs = queryset.__copy__() qs = queryset.__copy__()

View File

@ -277,7 +277,7 @@ class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer):
tags = TagsDocumentSerializer(many=True, source='visible_tags') tags = TagsDocumentSerializer(many=True, source='visible_tags')
restaurant_category = TagsDocumentSerializer(many=True, allow_null=True) restaurant_category = TagsDocumentSerializer(many=True, allow_null=True)
restaurant_cuisine = TagsDocumentSerializer(many=True, allow_null=True) restaurant_cuisine = TagsDocumentSerializer(many=True, allow_null=True)
distillery_types = TagsDocumentSerializer(many=True, allow_null=True) distillery_type = TagsDocumentSerializer(many=True, allow_null=True)
artisan_category = TagsDocumentSerializer(many=True, allow_null=True) artisan_category = TagsDocumentSerializer(many=True, allow_null=True)
schedule = ScheduleDocumentSerializer(many=True, allow_null=True) schedule = ScheduleDocumentSerializer(many=True, allow_null=True)
wine_origins = WineOriginSerializer(many=True) wine_origins = WineOriginSerializer(many=True)
@ -311,7 +311,7 @@ class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer):
# 'collections', # 'collections',
'type', 'type',
'subtypes', 'subtypes',
'distillery_types', 'distillery_type',
) )

View File

@ -5,6 +5,7 @@ from django.conf import settings
from tag import models from tag import models
from product import models as product_models from product import models as product_models
class TagsBaseFilterSet(filters.FilterSet): class TagsBaseFilterSet(filters.FilterSet):
# Object type choices # Object type choices

View File

@ -9,6 +9,7 @@ from tag import models
from utils.exceptions import BindingObjectNotFound, ObjectAlreadyAdded, RemovedBindingObjectNotFound from utils.exceptions import BindingObjectNotFound, ObjectAlreadyAdded, RemovedBindingObjectNotFound
from utils.serializers import TranslatedField from utils.serializers import TranslatedField
from utils.models import get_default_locale, get_language, to_locale from utils.models import get_default_locale, get_language, to_locale
from main.models import Feature
def translate_obj(obj): def translate_obj(obj):
@ -309,48 +310,25 @@ class ChosenTagSerializer(serializers.ModelSerializer):
class ChosenTagBindObjectSerializer(serializers.Serializer): class ChosenTagBindObjectSerializer(serializers.Serializer):
"""Serializer for binding chosen tag and objects""" """Serializer for binding chosen tag and objects"""
ESTABLISHMENT_TYPE = 'establishment_type' feature_id = serializers.IntegerField()
NEWS_TYPE = 'news_type'
TYPE_CHOICES = (
(ESTABLISHMENT_TYPE, 'Establishment type'),
(NEWS_TYPE, 'News type'),
)
type = serializers.ChoiceField(TYPE_CHOICES)
object_id = serializers.IntegerField()
def validate(self, attrs): def validate(self, attrs):
view = self.context.get('view') view = self.context.get('view')
request = self.context.get('request') request = self.context.get('request')
obj_type = attrs.get('type') obj_id = attrs.get('feature_id')
obj_id = attrs.get('object_id')
tag = view.get_object() tag = view.get_object()
attrs['tag'] = tag attrs['tag'] = tag
if obj_type == self.ESTABLISHMENT_TYPE: feature = Feature.objects.filter(pk=obj_id). \
establishment_type = EstablishmentType.objects.filter(pk=obj_id). \ first()
first() if not feature:
if not establishment_type: raise BindingObjectNotFound()
raise BindingObjectNotFound() if request.method == 'DELETE' and not feature. \
if request.method == 'DELETE' and not establishment_type. \ chosen_tags.filter(tag=tag). \
chosen_tags.filter(tag=tag). \ exists():
exists(): raise RemovedBindingObjectNotFound()
raise RemovedBindingObjectNotFound() attrs['related_object'] = feature
attrs['related_object'] = establishment_type
elif obj_type == self.NEWS_TYPE:
news_type = NewsType.objects.filter(pk=obj_id).first()
if not news_type:
raise BindingObjectNotFound()
if request.method == 'POST' and news_type.chosen_tags. \
filter(tag=tag).exists():
raise ObjectAlreadyAdded()
if request.method == 'DELETE' and not news_type.chosen_tags. \
filter(tag=tag).exists():
raise RemovedBindingObjectNotFound()
attrs['related_object'] = news_type
return attrs return attrs

View File

@ -90,6 +90,7 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet):
params_type = query_params.get('product_type') params_type = query_params.get('product_type')
week_days = tuple(map(_, ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"))) week_days = tuple(map(_, ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")))
short_week_days = tuple(map(_, ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")))
flags = ('toque_number', 'wine_region', 'works_noon', 'works_evening', 'works_now', 'works_at_weekday') flags = ('toque_number', 'wine_region', 'works_noon', 'works_evening', 'works_now', 'works_at_weekday')
filter_flags = {flag_name: False for flag_name in flags} filter_flags = {flag_name: False for flag_name in flags}
additional_flags = [] additional_flags = []
@ -155,7 +156,7 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet):
"filters": [{ "filters": [{
"id": weekday, "id": weekday,
"index_name": week_days[weekday].lower(), "index_name": week_days[weekday].lower(),
"label_translated": week_days[weekday] "label_translated": short_week_days[weekday],
} for weekday in range(7)] } for weekday in range(7)]
} }
result_list.append(works_noon) result_list.append(works_noon)
@ -170,7 +171,7 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet):
"filters": [{ "filters": [{
"id": weekday, "id": weekday,
"index_name": week_days[weekday].lower(), "index_name": week_days[weekday].lower(),
"label_translated": week_days[weekday] "label_translated": short_week_days[weekday],
} for weekday in range(7)] } for weekday in range(7)]
} }
result_list.append(works_evening) result_list.append(works_evening)
@ -193,7 +194,7 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet):
"filters": [{ "filters": [{
"id": weekday, "id": weekday,
"index_name": week_days[weekday].lower(), "index_name": week_days[weekday].lower(),
"label_translated": week_days[weekday] "label_translated": short_week_days[weekday],
} for weekday in range(7)] } for weekday in range(7)]
} }
result_list.append(works_at_weekday) result_list.append(works_at_weekday)

View File

@ -1,6 +1,7 @@
from datetime import datetime, time, date, timedelta
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from datetime import time, datetime
from utils.models import ProjectBaseMixin from utils.models import ProjectBaseMixin
@ -24,7 +25,8 @@ class Timetable(ProjectBaseMixin):
(THURSDAY, _('Thursday')), (THURSDAY, _('Thursday')),
(FRIDAY, _('Friday')), (FRIDAY, _('Friday')),
(SATURDAY, _('Saturday')), (SATURDAY, _('Saturday')),
(SUNDAY, _('Sunday'))) (SUNDAY, _('Sunday'))
)
weekday = models.PositiveSmallIntegerField(choices=WEEKDAYS_CHOICES, verbose_name=_('Week day')) weekday = models.PositiveSmallIntegerField(choices=WEEKDAYS_CHOICES, verbose_name=_('Week day'))
@ -51,6 +53,13 @@ class Timetable(ProjectBaseMixin):
f'works_at_noon - {self.works_at_noon}, ' \ f'works_at_noon - {self.works_at_noon}, ' \
f'works_at_afternoon: {self.works_at_afternoon})' f'works_at_afternoon: {self.works_at_afternoon})'
@property
def weekday_display_short(self):
"""Translated short day of the week"""
monday = date(2020, 1, 6)
with_weekday = monday + timedelta(days=self.weekday)
return _(with_weekday.strftime("%a"))
@property @property
def closed_at_str(self): def closed_at_str(self):
return str(self.closed_at) if self.closed_at else None return str(self.closed_at) if self.closed_at else None
@ -61,11 +70,13 @@ class Timetable(ProjectBaseMixin):
@property @property
def closed_at_indexing(self): def closed_at_indexing(self):
return datetime.combine(time=self.closed_at, date=datetime(1970, 1, 1 + self.weekday).date()) if self.closed_at else None return datetime.combine(time=self.closed_at,
date=datetime(1970, 1, 1 + self.weekday).date()) if self.closed_at else None
@property @property
def opening_at_indexing(self): def opening_at_indexing(self):
return datetime.combine(time=self.opening_at, date=datetime(1970, 1, 1 + self.weekday).date()) if self.opening_at else None return datetime.combine(time=self.opening_at,
date=datetime(1970, 1, 1 + self.weekday).date()) if self.opening_at else None
@property @property
def opening_time(self): def opening_time(self):

View File

@ -1,4 +1,7 @@
"""Serializer for app timetable""" """Serializer for app timetable"""
import datetime
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
@ -11,8 +14,8 @@ class ScheduleRUDSerializer(serializers.ModelSerializer):
NULLABLE_FIELDS = ['lunch_start', 'lunch_end', 'dinner_start', NULLABLE_FIELDS = ['lunch_start', 'lunch_end', 'dinner_start',
'dinner_end', 'opening_at', 'closed_at'] 'dinner_end', 'opening_at', 'closed_at']
weekday_display = serializers.CharField(source='get_weekday_display', weekday_display = serializers.CharField(source='get_weekday_display', read_only=True)
read_only=True) weekday_display_short = serializers.CharField(read_only=True)
lunch_start = serializers.TimeField(required=False) lunch_start = serializers.TimeField(required=False)
lunch_end = serializers.TimeField(required=False) lunch_end = serializers.TimeField(required=False)
@ -29,6 +32,7 @@ class ScheduleRUDSerializer(serializers.ModelSerializer):
fields = [ fields = [
'id', 'id',
'weekday_display', 'weekday_display',
'weekday_display_short',
'weekday', 'weekday',
'lunch_start', 'lunch_start',
'lunch_end', 'lunch_end',
@ -41,13 +45,13 @@ class ScheduleRUDSerializer(serializers.ModelSerializer):
def validate(self, attrs): def validate(self, attrs):
"""Override validate method""" """Override validate method"""
establishment_pk = self.context.get('request')\ establishment_pk = self.context.get('request') \
.parser_context.get('view')\ .parser_context.get('view') \
.kwargs.get('pk') .kwargs.get('pk')
establishment_slug = self.context.get('request')\ establishment_slug = self.context.get('request') \
.parser_context.get('view')\ .parser_context.get('view') \
.kwargs.get('slug') .kwargs.get('slug')
search_kwargs = {'pk': establishment_pk} if establishment_pk else {'slug': establishment_slug} search_kwargs = {'pk': establishment_pk} if establishment_pk else {'slug': establishment_slug}
@ -91,13 +95,14 @@ class ScheduleCreateSerializer(ScheduleRUDSerializer):
class TimetableSerializer(serializers.ModelSerializer): class TimetableSerializer(serializers.ModelSerializer):
"""Serailzier for Timetable model.""" """Serailzier for Timetable model."""
weekday_display = serializers.CharField(source='get_weekday_display', weekday_display = serializers.CharField(source='get_weekday_display', read_only=True)
read_only=True) weekday_display_short = serializers.CharField(read_only=True)
class Meta: class Meta:
model = Timetable model = Timetable
fields = ( fields = (
'id', 'id',
'weekday_display', 'weekday_display',
'weekday_display_short',
'works_at_noon', 'works_at_noon',
) )

View File

@ -40,7 +40,6 @@ class Command(BaseCommand):
'product_review', 'product_review',
'newsletter_subscriber', # подписчики на рассылку - переносить после переноса пользователей №1 'newsletter_subscriber', # подписчики на рассылку - переносить после переноса пользователей №1
'purchased_plaques', # №6 - перенос купленных тарелок 'purchased_plaques', # №6 - перенос купленных тарелок
'fill_city_gallery', # №3 - перенос галереи городов
'guides', 'guides',
'guide_filters', 'guide_filters',
'guide_element_sections', 'guide_element_sections',
@ -51,7 +50,6 @@ class Command(BaseCommand):
'guide_element_label_photo', 'guide_element_label_photo',
'guide_complete', 'guide_complete',
'update_city_info', 'update_city_info',
'migrate_city_gallery',
'fix_location', 'fix_location',
'remove_old_locations', 'remove_old_locations',
'add_fake_country', 'add_fake_country',

View File

@ -10,6 +10,7 @@ class CommentSerializer(serializers.Serializer):
mark = serializers.DecimalField(max_digits=4, decimal_places=2, allow_null=True) mark = serializers.DecimalField(max_digits=4, decimal_places=2, allow_null=True)
account_id = serializers.IntegerField() account_id = serializers.IntegerField()
establishment_id = serializers.CharField() establishment_id = serializers.CharField()
state = serializers.CharField()
def validate(self, data): def validate(self, data):
data.update({ data.update({
@ -18,14 +19,18 @@ class CommentSerializer(serializers.Serializer):
'mark': self.get_mark(data), 'mark': self.get_mark(data),
'content_object': self.get_content_object(data), 'content_object': self.get_content_object(data),
'user': self.get_account(data), 'user': self.get_account(data),
'status': self.get_status(data),
}) })
data.pop('establishment_id') data.pop('establishment_id')
data.pop('account_id') data.pop('account_id')
data.pop('state')
return data return data
def create(self, validated_data): def create(self, validated_data):
try: try:
return Comment.objects.create(**validated_data) comment, _ = Comment.objects.get_or_create(old_id=validated_data.get('old_id'),
defaults=validated_data)
return comment
except Exception as e: except Exception as e:
raise ValueError(f"Error creating comment with {validated_data}: {e}") raise ValueError(f"Error creating comment with {validated_data}: {e}")
@ -48,3 +53,12 @@ class CommentSerializer(serializers.Serializer):
if not data['mark']: if not data['mark']:
return None return None
return data['mark'] * -1 if data['mark'] < 0 else data['mark'] return data['mark'] * -1 if data['mark'] < 0 else data['mark']
@staticmethod
def get_status(data):
if data.get('state'):
state = data.get('state')
if state == 'published':
return Comment.PUBLISHED
elif state == 'deleted':
return Comment.DELETED

View File

@ -431,50 +431,6 @@ class CityMapCorrectSerializer(CityMapSerializer):
city.save() city.save()
class CityGallerySerializer(serializers.ModelSerializer):
id = serializers.IntegerField()
city_id = serializers.IntegerField()
attachment_file_name = serializers.CharField()
class Meta:
model = models.CityGallery
fields = ("id", "city_id", "attachment_file_name")
def validate(self, data):
data = self.set_old_id(data)
data = self.set_gallery(data)
data = self.set_city(data)
return data
def create(self, validated_data):
return models.CityGallery.objects.create(**validated_data)
def set_old_id(self, data):
data['old_id'] = data.pop('id')
return data
def set_gallery(self, data):
link_prefix = "city_photos/00baf486523f62cdf131fa1b19c5df2bf21fc9f8/"
try:
data['image'] = Image.objects.create(
image=f"{link_prefix}{data['attachment_file_name']}"
)
except Exception as e:
raise ValueError(f"Cannot create image with {data}: {e}")
del(data['attachment_file_name'])
return data
def set_city(self, data):
try:
data['city'] = models.City.objects.get(old_id=data.pop('city_id'))
except models.City.DoesNotExist as e:
raise ValueError(f"Cannot get city with {data}: {e}")
except MultipleObjectsReturned as e:
raise ValueError(f"Multiple cities find with {data}: {e}")
return data
class CepageWineRegionSerializer(TransferSerializerMixin): class CepageWineRegionSerializer(TransferSerializerMixin):
CATEGORY_LABEL = 'Cepage' CATEGORY_LABEL = 'Cepage'

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2020-01-17 22:01
import django.contrib.postgres.indexes
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('translation', '0007_language_is_active'),
]
operations = [
migrations.AddIndex(
model_name='siteinterfacedictionary',
index=django.contrib.postgres.indexes.GinIndex(fields=['text'], name='translation_text_0b2bfa_gin'),
),
]

View File

@ -1,5 +1,6 @@
"""Translation app models.""" """Translation app models."""
from django.contrib.postgres.fields import JSONField from django.contrib.postgres.fields import JSONField
from django.contrib.postgres.indexes import GinIndex
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.apps import apps from django.apps import apps
@ -105,6 +106,9 @@ class SiteInterfaceDictionary(ProjectBaseMixin):
verbose_name = _('Site interface dictionary') verbose_name = _('Site interface dictionary')
verbose_name_plural = _('Site interface dictionary') verbose_name_plural = _('Site interface dictionary')
indexes = [
GinIndex(fields=['text'])
]
def __str__(self): def __str__(self):
return f'{self.page}: {self.keywords}' return f'{self.page}: {self.keywords}'

View File

@ -4,6 +4,8 @@ import random
import re import re
import string import string
from collections import namedtuple from collections import namedtuple
from functools import reduce
from io import BytesIO
import requests import requests
from django.conf import settings from django.conf import settings
@ -11,6 +13,7 @@ from django.contrib.contenttypes.models import ContentType
from django.contrib.gis.geos import Point from django.contrib.gis.geos import Point
from django.http.request import HttpRequest from django.http.request import HttpRequest
from django.utils.timezone import datetime from django.utils.timezone import datetime
from PIL import Image
from rest_framework import status from rest_framework import status
from rest_framework.request import Request from rest_framework.request import Request
@ -53,6 +56,19 @@ def username_validator(username: str) -> bool:
return True return True
def username_random():
"""Generate random username"""
username = list('{letters}{digits}'.format(
letters=''.join([random.choice(string.ascii_lowercase) for _ in range(4)]),
digits=random.randrange(100, 1000)
))
random.shuffle(username)
return '{first}{username}'.format(
first=random.choice(string.ascii_lowercase),
username=''.join(username)
)
def image_path(instance, filename): def image_path(instance, filename):
"""Determine avatar path method.""" """Determine avatar path method."""
filename = '%s.jpeg' % generate_code() filename = '%s.jpeg' % generate_code()
@ -119,6 +135,7 @@ def absolute_url_decorator(func):
return f'{settings.MEDIA_URL}{url_path}/' return f'{settings.MEDIA_URL}{url_path}/'
else: else:
return url_path return url_path
return get_absolute_image_url return get_absolute_image_url
@ -157,6 +174,23 @@ def transform_into_readable_str(raw_string: str, postfix: str = 'SectionNode'):
return f"{''.join([i.capitalize() for i in result])}" return f"{''.join([i.capitalize() for i in result])}"
def transform_camelcase_to_underscore(raw_string: str):
"""
Transform str, i.e:
from
"ContentPage"
to
"content_page"
"""
re_exp = r'[A-Z][^A-Z]*'
result = [i.lower() for i in re.findall(re_exp, raw_string) if i]
if result:
return reduce(lambda x, y: f'{x}_{y}', result)
else:
return raw_string
def section_name_into_index_name(section_name: str): def section_name_into_index_name(section_name: str):
""" """
Transform slug into section name, i.e: Transform slug into section name, i.e:
@ -169,3 +203,16 @@ def section_name_into_index_name(section_name: str):
result = re.findall(re_exp, section_name) result = re.findall(re_exp, section_name)
if result: if result:
return f"{' '.join([word.capitalize() if i == 0 else word for i, word in enumerate(result[:-2])])}" return f"{' '.join([word.capitalize() if i == 0 else word for i, word in enumerate(result[:-2])])}"
def get_url_images_in_text(text):
"""Find images urls in text"""
return re.findall(r'\<img.+src="([^"]+)".+>', text)
def get_image_meta_by_url(url) -> (int, int, int):
"""Returns image size (bytes, width, height)"""
image_raw = requests.get(url)
image = Image.open(BytesIO(image_raw.content))
width, height = image.size
return int(image_raw.headers.get('content-length')), width, height

View File

@ -1,5 +1,8 @@
"""Custom middlewares.""" """Custom middlewares."""
import logging
from django.utils import translation, timezone from django.utils import translation, timezone
from django.db import connection
from account.models import User from account.models import User
from configuration.models import TranslationSettings from configuration.models import TranslationSettings
@ -7,6 +10,8 @@ from main.methods import determine_user_city
from main.models import SiteSettings from main.models import SiteSettings
from translation.models import Language from translation.models import Language
logger = logging.getLogger(__name__)
def get_locale(cookie_dict): def get_locale(cookie_dict):
return cookie_dict.get('locale') return cookie_dict.get('locale')
@ -92,3 +97,26 @@ def user_last_ip(get_response):
return response return response
return middleware return middleware
def log_db_queries_per_API_request(get_response):
"""Middleware-helper to optimize requests performance"""
def middleware(request):
total_time = 0
response = get_response(request)
for query in connection.queries:
query_time = query.get('time')
if query_time is None:
query_time = query.get('duration', 0) / 1000
total_time += float(query_time)
total_queries = len(connection.queries)
if total_queries > 10:
logger.error(
f'\t{len(connection.queries)} queries run, total {total_time} seconds \t'
f'URL: "{request.method} {request.get_full_path_info()}"'
)
return response
return middleware

View File

@ -221,7 +221,7 @@ class SORLImageMixin(models.Model):
"""Get image thumbnail url.""" """Get image thumbnail url."""
crop_image = self.get_image(thumbnail_key) crop_image = self.get_image(thumbnail_key)
if hasattr(crop_image, 'url'): if hasattr(crop_image, 'url'):
return self.get_image(thumbnail_key).url return crop_image.url
def image_tag(self): def image_tag(self):
"""Admin preview tag.""" """Admin preview tag."""

View File

@ -5,14 +5,13 @@ from sorl.thumbnail.engines.pil_engine import Engine as PILEngine
class GMEngine(PILEngine): class GMEngine(PILEngine):
def create(self, image, geometry, options): def create(self, image, geometry, options):
"""
Processing conductor, returns the thumbnail as an image engine instance
"""
image = self.cropbox(image, geometry, options) image = self.cropbox(image, geometry, options)
image = self.orientation(image, geometry, options) image = self.orientation(image, geometry, options)
image = self.colorspace(image, geometry, options) image = self.colorspace(image, geometry, options)
image = self.remove_border(image, options) image = self.remove_border(image, options)
image = self.scale(image, geometry, options)
image = self.crop(image, geometry, options) image = self.crop(image, geometry, options)
image = self.scale(image, geometry, options)
image = self.rounded(image, geometry, options) image = self.rounded(image, geometry, options)
image = self.blur(image, geometry, options) image = self.blur(image, geometry, options)
image = self.padding(image, geometry, options) image = self.padding(image, geometry, options)

View File

@ -27,4 +27,14 @@
./manage.py transfer --overlook ./manage.py transfer --overlook
./manage.py transfer --inquiries ./manage.py transfer --inquiries
./manage.py transfer --product_review ./manage.py transfer --product_review
./manage.py transfer --transfer_text_review ./manage.py transfer --transfer_text_review
# оптимизация изображений
/manage.py news_optimize_images # сжимает картинки в описаниях новостей
/manage.py update_establishment_image_urls # удаляет неотображаемые картинки из модели заведения
# сотрудники с позициями для заведений
./manage.py add_employee
./manage.py add_position
./manage.py add_empl_position
./manage.py update_employee

View File

@ -385,6 +385,7 @@ THUMBNAIL_QUALITY = 85
THUMBNAIL_DEBUG = False THUMBNAIL_DEBUG = False
SORL_THUMBNAIL_ALIASES = { SORL_THUMBNAIL_ALIASES = {
'news_preview': {'geometry_string': '300x260', 'crop': 'center'}, 'news_preview': {'geometry_string': '300x260', 'crop': 'center'},
'news_description': {'geometry_string': '100x100'},
'news_promo_horizontal_web': {'geometry_string': '1900x600', 'crop': 'center'}, 'news_promo_horizontal_web': {'geometry_string': '1900x600', 'crop': 'center'},
'news_promo_horizontal_mobile': {'geometry_string': '375x260', 'crop': 'center'}, 'news_promo_horizontal_mobile': {'geometry_string': '375x260', 'crop': 'center'},
'news_tile_horizontal_web': {'geometry_string': '300x275', 'crop': 'center'}, 'news_tile_horizontal_web': {'geometry_string': '300x275', 'crop': 'center'},
@ -411,6 +412,8 @@ SORL_THUMBNAIL_ALIASES = {
'city_detail': {'geometry_string': '1120x1120', 'crop': 'center'}, 'city_detail': {'geometry_string': '1120x1120', 'crop': 'center'},
'city_original': {'geometry_string': '2048x1536', 'crop': 'center'}, 'city_original': {'geometry_string': '2048x1536', 'crop': 'center'},
'type_preview': {'geometry_string': '300x260', 'crop': 'center'}, 'type_preview': {'geometry_string': '300x260', 'crop': 'center'},
'collection_image': {'geometry_string': '940x620', 'upscale': False, 'quality': 100},
'establishment_collection_image': {'geometry_string': '940x620', 'upscale': False, 'quality': 100}
} }
@ -533,3 +536,5 @@ COOKIE_DOMAIN = None
ELASTICSEARCH_DSL = {} ELASTICSEARCH_DSL = {}
ELASTICSEARCH_INDEX_NAMES = {} ELASTICSEARCH_INDEX_NAMES = {}
THUMBNAIL_FORCE_OVERWRITE = True

View File

@ -80,4 +80,7 @@ EMAIL_USE_TLS = True
EMAIL_HOST = 'smtp.gmail.com' EMAIL_HOST = 'smtp.gmail.com'
EMAIL_HOST_USER = 'anatolyfeteleu@gmail.com' EMAIL_HOST_USER = 'anatolyfeteleu@gmail.com'
EMAIL_HOST_PASSWORD = 'nggrlnbehzksgmbt' EMAIL_HOST_PASSWORD = 'nggrlnbehzksgmbt'
EMAIL_PORT = 587 EMAIL_PORT = 587
MIDDLEWARE.append('utils.middleware.log_db_queries_per_API_request')

View File

@ -43,15 +43,15 @@ DATABASES = {
'options': '-c search_path=gm,public' 'options': '-c search_path=gm,public'
}, },
}, },
'legacy': { 'legacy': {
'ENGINE': 'django.db.backends.mysql', 'ENGINE': 'django.db.backends.mysql',
# 'HOST': '172.22.0.1', # 'HOST': '172.22.0.1',
'HOST': 'mysql_db', 'HOST': 'mysql_db',
'PORT': 3306, 'PORT': 3306,
'NAME': 'dev', 'NAME': 'dev',
'USER': 'dev', 'USER': 'dev',
'PASSWORD': 'octosecret123' 'PASSWORD': 'octosecret123'
}, },
} }
@ -84,11 +84,11 @@ LOGGING = {
'py.warnings': { 'py.warnings': {
'handlers': ['console'], 'handlers': ['console'],
}, },
'django.db.backends': { # 'django.db.backends': {
'handlers': ['console', ], # 'handlers': ['console', ],
'level': 'DEBUG', # 'level': 'DEBUG',
'propagate': False, # 'propagate': False,
}, # },
} }
} }

View File

@ -36,6 +36,9 @@
{% trans "Please confirm your email address to complete the registration:" %} {% trans "Please confirm your email address to complete the registration:" %}
https://{{ country_code }}.{{ domain_uri }}/registered/{{ uidb64 }}/{{ token }}/ https://{{ country_code }}.{{ domain_uri }}/registered/{{ uidb64 }}/{{ token }}/
<br> <br>
{% trans "If you use the mobile app, enter the following code in the form:" %}
{{ token }}
<br>
{% trans "Thanks for using our site!" %} {% trans "Thanks for using our site!" %}
<br><br> <br><br>
{% blocktrans %}The {{ site_name }} team{% endblocktrans %} {% blocktrans %}The {{ site_name }} team{% endblocktrans %}

View File

@ -49,7 +49,6 @@ django-storages==1.7.2
sorl-thumbnail==12.5.0 sorl-thumbnail==12.5.0
PyYAML==5.1.2 PyYAML==5.1.2
# temp solution # temp solution

View File

@ -1,4 +1,8 @@
-r base.txt -r base.txt
ipdb ipdb
ipython ipython
mysqlclient==1.4.4 mysqlclient==1.4.4
pyparsing
graphviz
pydot