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)
class RoleAdmin(admin.ModelAdmin):
list_display = ['role', 'country']
list_display = ['id', 'role', 'country']
raw_id_fields = ['country', ]
@admin.register(models.UserRole)
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)

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)
sites = SiteSettings.objects.filter(old_id=s.site_id)
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():
objects.append(
Role(site=site, role=role_choice)
Role(**data)
)
Role.objects.bulk_create(objects)
@ -81,7 +83,7 @@ class Command(BaseCommand):
def update_site_role(self):
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():
for role in tqdm(roles, desc='Update role country'):
role.country = role.site.country
@ -114,8 +116,7 @@ class Command(BaseCommand):
users = User.objects.filter(old_id=s.account_id)
for user in users:
for role in roles:
user_role = UserRole.objects.get_or_create(user=user,
role=role)
UserRole.objects.get_or_create(user=user, role=role, state=UserRole.VALIDATED)
self.stdout.write(self.style.WARNING(f'Added users roles.'))
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"""
from datetime import datetime
from tabnanny import verbose
from django.conf import settings
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.db import models
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
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):
"""Base Role model."""
STANDARD_USER = 1
@ -46,13 +57,14 @@ class Role(ProjectBaseMixin):
(CONTENT_PAGE_MANAGER, _('Content page manager')),
(ESTABLISHMENT_MANAGER, _('Establishment manager')),
(REVIEWER_MANGER, _('Reviewer manager')),
(RESTAURANT_REVIEWER, 'Restaurant reviewer'),
(SALES_MAN, 'Sales man'),
(WINERY_REVIEWER, 'Winery reviewer'),
(SELLER, 'Seller'),
(LIQUOR_REVIEWER, 'Liquor reviewer'),
(PRODUCT_REVIEWER, 'Product reviewer'),
(RESTAURANT_REVIEWER, _('Restaurant reviewer')),
(SALES_MAN, _('Sales man')),
(WINERY_REVIEWER, _('Winery reviewer')),
(SELLER, _('Seller')),
(LIQUOR_REVIEWER, _('Liquor reviewer')),
(PRODUCT_REVIEWER, _('Product reviewer')),
)
role = models.PositiveIntegerField(verbose_name=_('Role'), choices=ROLE_CHOICES,
null=False, blank=False)
country = models.ForeignKey(Country, verbose_name=_('Country'),
@ -62,6 +74,27 @@ class Role(ProjectBaseMixin):
establishment_subtype = models.ForeignKey(EstablishmentSubType,
verbose_name=_('Establishment subtype'),
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):
@ -238,7 +271,7 @@ class User(AbstractUser):
@property
def reset_password_token(self):
"""Make a token for finish signup."""
return password_token_generator.make_token(self)
return GMTokenGenerator(purpose=GMTokenGenerator.RESET_PASSWORD).make_token(self)
@property
def get_user_uidb64(self):
@ -334,6 +367,22 @@ class User(AbstractUser):
model='product',
).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):
"""UserRole model."""
@ -348,6 +397,7 @@ class UserRole(ProjectBaseMixin):
(CANCELLED, _('cancelled')),
(REJECTED, _('rejected'))
)
user = models.ForeignKey(
'account.User', verbose_name=_('User'), on_delete=models.CASCADE)
role = models.ForeignKey(
@ -358,9 +408,13 @@ class UserRole(ProjectBaseMixin):
state = models.CharField(
_('state'), choices=STATE_CHOICES, max_length=10, default=PENDING)
requester = models.ForeignKey(
'account.User', blank=True, null=True, default=None, related_name='roles_requested',
on_delete=models.SET_NULL)
requester = models.ForeignKey('account.User', on_delete=models.SET_NULL,
blank=True, null=True, default=None,
related_name='roles_requested',
help_text='A user (REQUESTER) who requests a '
'role change for a USER')
objects = UserRoleQueryset.as_manager()
class Meta:
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)
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"""
from rest_framework import serializers
from account import models
from account.models import User
from account.serializers import RoleBaseSerializer, subscriptions_handler
from main.models import SiteSettings
class RoleSerializer(serializers.ModelSerializer):
class Meta:
model = models.Role
fields = [
'role',
'country'
]
class _SiteSettingsSerializer(serializers.ModelSerializer):
class Meta:
model = SiteSettings
@ -26,6 +19,15 @@ class _SiteSettingsSerializer(serializers.ModelSerializer):
class BackUserSerializer(serializers.ModelSerializer):
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:
model = User
@ -45,37 +47,98 @@ class BackUserSerializer(serializers.ModelSerializer):
'unconfirmed_email',
'email_confirmed',
'newsletter',
'roles',
'password',
'city',
'locale',
'last_ip',
'last_country',
'roles',
'subscriptions',
)
extra_kwargs = {
'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):
subscriptions_list = []
if 'subscription_types' in validated_data:
subscriptions_list = validated_data.pop('subscription_types')
user = super().create(validated_data)
user.set_password(validated_data['password'])
user.save()
subscriptions_handler(subscriptions_list, user)
return user
class BackDetailUserSerializer(BackUserSerializer):
class Meta:
model = User
exclude = ('password',)
read_only_fields = ('old_password', 'last_login', 'date_joined')
fields = (
'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):
subscriptions_list = []
if 'subscription_types' in validated_data:
subscriptions_list = validated_data.pop('subscription_types')
user = super().create(validated_data)
user.set_password(validated_data['password'])
user.save()
subscriptions_handler(subscriptions_list, 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 Meta:
@ -85,3 +148,9 @@ class UserRoleSerializer(serializers.ModelSerializer):
'user',
'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"""
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.utils.translation import gettext_lazy as _
from fcm_django.models import FCMDevice
from rest_framework import exceptions
from rest_framework import serializers
from rest_framework import validators as rest_validators
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 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):
"""User serializer."""
# RESPONSE
@ -25,6 +66,15 @@ class UserSerializer(serializers.ModelSerializer):
email = serializers.EmailField(
validators=(rest_validators.UniqueValidator(queryset=models.User.objects.all()),),
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:
model = models.User
@ -38,6 +88,8 @@ class UserSerializer(serializers.ModelSerializer):
'email',
'email_confirmed',
'newsletter',
'roles',
'subscriptions',
]
extra_kwargs = {
'first_name': {'required': False, 'write_only': True, },
@ -49,8 +101,14 @@ class UserSerializer(serializers.ModelSerializer):
}
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)
validated_data['user'] = user
Subscriber.objects.make_subscriber(**validated_data)
subscriptions_handler(subscriptions_list, user)
return user
def validate_email(self, value):
@ -68,6 +126,10 @@ class UserSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
"""Override update method"""
subscriptions_list = []
if 'subscription_types' in validated_data:
subscriptions_list = validated_data.pop('subscription_types')
old_email = instance.email
instance = super().update(instance, validated_data)
if 'email' in validated_data:
@ -80,12 +142,14 @@ class UserSerializer(serializers.ModelSerializer):
tasks.change_email_address.delay(
user_id=instance.id,
country_code=self.context.get('request').country_code,
emails=[validated_data['email'],])
emails=[validated_data['email'], ])
else:
tasks.change_email_address(
user_id=instance.id,
country_code=self.context.get('request').country_code,
emails=[validated_data['email'],])
emails=[validated_data['email'], ])
subscriptions_handler(subscriptions_list, instance)
return instance
@ -212,6 +276,7 @@ class ChangeEmailSerializer(serializers.ModelSerializer):
# Firebase Cloud Messaging serializers
class FCMDeviceSerializer(serializers.ModelSerializer):
"""FCM Device model serializer"""
class Meta:
model = FCMDevice
fields = ('id', 'name', 'registration_id', 'device_id',
@ -231,8 +296,8 @@ class FCMDeviceSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
super(FCMDeviceSerializer, self).__init__(*args, **kwargs)
self.fields['type'].help_text = (
'Should be one of ["%s"]' %
'", "'.join([i for i in self.fields['type'].choices]))
'Should be one of ["%s"]' %
'", "'.join([i for i in self.fields['type'].choices]))
def create(self, validated_data):
user = self.context['request'].user

View File

@ -6,9 +6,10 @@ from account.views import back as views
app_name = 'account'
urlpatterns = [
path('role/', views.RoleLstView.as_view(), name='role-list-create'),
path('user-role/', views.UserRoleLstView.as_view(), name='user-role-list-create'),
path('user/', views.UserLstView.as_view(), name='user-create-list'),
path('role/', views.RoleListView.as_view(), name='role-list-create'),
path('role-tab/', views.RoleTabRetrieveView.as_view(), name='role-tab'),
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>/csv/', views.get_user_csv, name='user-csv'),
]

View File

@ -1,45 +1,71 @@
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
import csv
from django.http import HttpResponse, HttpResponseNotFound
from rest_framework.authtoken.models import Token
from account import models
from account import models, filters
from account.models import User
from account.serializers import back as serializers
from account.serializers.common import RoleBaseSerializer
class RoleLstView(generics.ListCreateAPIView):
serializer_class = serializers.RoleSerializer
class RoleListView(generics.ListCreateAPIView):
serializer_class = RoleBaseSerializer
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
queryset = models.UserRole.objects.all()
class UserLstView(generics.ListCreateAPIView):
class UserListView(generics.ListCreateAPIView):
"""User list create view."""
queryset = User.objects.prefetch_related('roles')
serializer_class = serializers.BackUserSerializer
permission_classes = (permissions.IsAdminUser,)
filter_backends = (DjangoFilterBackend, OrderingFilter)
filterset_fields = (
'email_confirmed',
'is_staff',
'is_active',
'is_superuser',
'roles',
)
filter_class = filters.AccountBackOfficeFilter
filter_backends = (OrderingFilter, DjangoFilterBackend)
ordering_fields = (
'email_confirmed',
'is_staff',
'is_active',
'is_superuser',
'roles',
'last_login'
'last_login',
'date_joined',
)

View File

@ -1,6 +1,5 @@
"""Web account views"""
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.utils.encoding import force_text
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.serializers import web as serializers
from utils import exceptions as utils_exceptions
from utils.models import GMTokenGenerator
from utils.views import JWTGenericViewMixin
@ -40,22 +40,23 @@ class PasswordResetConfirmView(JWTGenericViewMixin, generics.GenericAPIView):
queryset = models.User.objects.active()
def get_object(self):
"""Override get_object method"""
"""Overridden get_object method"""
queryset = self.filter_queryset(self.get_queryset())
uidb64 = self.kwargs.get('uidb64')
user_id = force_text(urlsafe_base64_decode(uidb64))
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()
# 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):
"""Implement PATCH method"""

View File

@ -20,6 +20,7 @@ from utils.tokens import GMRefreshToken
# Serializers
class SignupSerializer(serializers.ModelSerializer):
"""Signup serializer serializer mixin"""
class Meta:
model = account_models.User
fields = (
@ -37,11 +38,13 @@ class SignupSerializer(serializers.ModelSerializer):
def validate_username(self, value):
"""Custom username validation"""
valid = utils_methods.username_validator(username=value)
if not valid:
raise utils_exceptions.NotValidUsernameError()
if account_models.User.objects.filter(username__iexact=value).exists():
raise serializers.ValidationError()
if value:
valid = utils_methods.username_validator(username=value)
if not valid:
raise utils_exceptions.NotValidUsernameError()
if account_models.User.objects.filter(username__iexact=value).exists():
raise serializers.ValidationError()
return value
def validate_email(self, value):
@ -63,6 +66,13 @@ class SignupSerializer(serializers.ModelSerializer):
request = self.context.get('request')
"""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(
username=validated_data.get('username'),
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):
"""Return QuerySet with related."""
return self.select_related('guide_type', 'site')
return self.select_related('site', )
def by_country_id(self, country_id):
"""Return QuerySet filtered by country code."""

View File

@ -65,7 +65,8 @@ class CollectionEstablishmentListView(CollectionListView):
# May raise a permission denied
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

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 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):
"""Annotate belonging status"""
return self.annotate(is_mine=models.Case(
@ -27,16 +31,48 @@ class CommentQuerySet(ContentTypeQuerySetMixin):
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):
"""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'))
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'))
old_id = models.IntegerField(null=True, blank=True, default=None)
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,
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)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey('content_type', 'object_id')

View File

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

View File

@ -1,9 +1 @@
"""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
class CommentSerializer(serializers.ModelSerializer):
class CommentBaseSerializer(serializers.ModelSerializer):
"""Comment serializer"""
nickname = serializers.CharField(read_only=True,
source='user.username')
is_mine = serializers.BooleanField(read_only=True)
profile_pic = serializers.URLField(read_only=True,
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:
"""Serializer for model Comment"""
@ -23,19 +26,11 @@ class CommentSerializer(serializers.ModelSerializer):
'text',
'mark',
'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',
'status',
'status_display',
'last_ip',
]
extra_kwargs = {
'status': {'read_only': True},
}

View File

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

View File

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

View File

@ -1,6 +1,8 @@
"""Establishment app filters."""
from django.core.validators import EMPTY_VALUES
from django.utils.translation import ugettext_lazy as _
from django_filters import rest_framework as filters
from rest_framework.serializers import ValidationError
from establishment import models
@ -8,8 +10,8 @@ from establishment import models
class EstablishmentFilter(filters.FilterSet):
"""Establishment filter set."""
tag_id = filters.NumberFilter(field_name='tags__metadata__id',)
award_id = filters.NumberFilter(field_name='awards__id',)
tag_id = filters.NumberFilter(field_name='tags__metadata__id', )
award_id = filters.NumberFilter(field_name='awards__id', )
search = filters.CharFilter(method='search_text')
type = filters.CharFilter(method='by_type')
subtype = filters.CharFilter(method='by_subtype')
@ -65,6 +67,10 @@ class EmployeeBackFilter(filters.FilterSet):
"""Employee filter set."""
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:
"""Meta class."""
@ -72,10 +78,47 @@ class EmployeeBackFilter(filters.FilterSet):
model = models.Employee
fields = (
'search',
'position_id',
'public_mark',
'toque_number',
'username',
)
def search_by_name_or_last_name(self, queryset, name, value):
"""Search by name or last name."""
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

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.measure import Distance as DistanceMeasure
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.validators import MinValueValidator, MaxValueValidator
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.translation import gettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField
@ -58,8 +60,6 @@ class EstablishmentType(TypeDefaultImageMixin, TranslatedFieldsMixin, ProjectBas
blank=True, null=True, default=None,
verbose_name='default image')
chosen_tags = generic.GenericRelation(to='tag.ChosenTag')
class Meta:
"""Meta class."""
@ -122,7 +122,7 @@ class EstablishmentQuerySet(models.QuerySet):
def with_base_related(self):
"""Return qs with related objects."""
return self.select_related('address', 'establishment_type'). \
prefetch_related('tags', 'tags__translation')
prefetch_related('tags', 'tags__translation').with_main_image()
def with_schedule(self):
"""Return qs with related schedule."""
@ -266,16 +266,16 @@ class EstablishmentQuerySet(models.QuerySet):
2 With ordering by distance.
"""
qs = self.similar_base(establishment) \
.filter(**filters)
.filter(**filters)
if establishment.address and establishment.address.coordinates:
return Subquery(
qs.annotate_distance(point=establishment.location)
.order_by('distance')
.distinct()
.values_list('id', flat=True)[:settings.LIMITING_QUERY_OBJECTS]
.order_by('distance')
.distinct()
.values_list('id', flat=True)[:settings.LIMITING_QUERY_OBJECTS]
)
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):
@ -293,12 +293,12 @@ class EstablishmentQuerySet(models.QuerySet):
}
)
return self.filter(id__in=ids_by_subquery.queryset) \
.annotate_intermediate_public_mark() \
.annotate_mark_similarity(mark=restaurant.public_mark) \
.order_by('mark_similarity') \
.distinct('mark_similarity', 'id')
.annotate_intermediate_public_mark() \
.annotate_mark_similarity(mark=restaurant.public_mark) \
.order_by('mark_similarity') \
.distinct('mark_similarity', 'id')
def same_subtype(self, establishment):
def annotate_same_subtype(self, establishment):
"""Annotate flag same subtype."""
return self.annotate(same_subtype=Case(
models.When(
@ -314,7 +314,7 @@ class EstablishmentQuerySet(models.QuerySet):
Return QuerySet with objects that similar to Artisan/Producer(s).
: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 = {
'ordering': [F('same_subtype').desc(), ],
'distinctions': ['same_subtype', ]
@ -325,8 +325,8 @@ class EstablishmentQuerySet(models.QuerySet):
similarity_rules['distinctions'].append('distance')
return base_qs.has_published_reviews() \
.order_by(*similarity_rules['ordering']) \
.distinct(*similarity_rules['distinctions'], 'id')
.order_by(*similarity_rules['ordering']) \
.distinct(*similarity_rules['distinctions'], 'id')
def by_wine_region(self, wine_region):
"""
@ -360,17 +360,19 @@ class EstablishmentQuerySet(models.QuerySet):
similarity_rules['distinctions'].append('distance')
return base_qs.order_by(*similarity_rules['ordering']) \
.distinct(*similarity_rules['distinctions'], 'id')
.distinct(*similarity_rules['distinctions'], 'id')
def similar_distilleries(self, distillery):
"""
Return QuerySet with objects that similar to Distillery.
: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 = {
'ordering': [F('same_subtype').desc(), ],
'distinctions': ['same_subtype', ]
'ordering': [],
'distinctions': []
}
if distillery.address and distillery.address.coordinates:
base_qs = base_qs.annotate_distance(point=distillery.location)
@ -378,27 +380,29 @@ class EstablishmentQuerySet(models.QuerySet):
similarity_rules['distinctions'].append('distance')
return base_qs.published() \
.has_published_reviews() \
.order_by(*similarity_rules['ordering']) \
.distinct(*similarity_rules['distinctions'], 'id')
.has_published_reviews() \
.order_by(*similarity_rules['ordering']) \
.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.
: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 = {
'ordering': [F('same_subtype').desc(), ],
'distinctions': ['same_subtype', ]
'ordering': [],
'distinctions': []
}
if distillery.address and distillery.address.coordinates:
base_qs = base_qs.annotate_distance(point=distillery.location)
if food_producer.address and food_producer.address.coordinates:
base_qs = base_qs.annotate_distance(point=food_producer.location)
similarity_rules['ordering'].append(F('distance').asc())
similarity_rules['distinctions'].append('distance')
return base_qs.order_by(*similarity_rules['ordering']) \
.distinct(*similarity_rules['distinctions'], 'id')
.distinct(*similarity_rules['distinctions'], 'id')
def last_reviewed(self, point: Point):
"""
@ -502,6 +506,13 @@ class EstablishmentQuerySet(models.QuerySet):
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,
TranslatedFieldsMixin, HasTagsMixin, FavoritesMixin):
@ -649,7 +660,6 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
country = self.address.city.country
return country.low_price, country.high_price
def set_establishment_type(self, establishment_type):
self.establishment_type = establishment_type
self.establishment_subtypes.exclude(
@ -769,10 +779,12 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
return self.products.wines()
@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()
if qs.exists():
return qs.first().image
image_model = qs.first()
if image_model is not None:
return image_model.image
@property
def restaurant_category_indexing(self):
@ -786,6 +798,10 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
def artisan_category_indexing(self):
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
def last_comment(self):
if hasattr(self, 'comments_prefetched') and len(self.comments_prefetched):
@ -856,11 +872,6 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
metadata.append(category_tags)
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):
"""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]))
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):
"""Search by name or 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):
e = EstablishmentEmployee.objects.actual().filter(employee=self)
return self.prefetch_related(models.Prefetch('establishmentemployee_set',
queryset=EstablishmentEmployee.objects.actual()
)).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):
"""Employee model."""
@ -1021,6 +1113,11 @@ class Employee(BaseAttributes):
verbose_name=_('Tags'))
# old_id = profile_id
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()
@ -1029,6 +1126,34 @@ class Employee(BaseAttributes):
verbose_name = _('Employee')
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):

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 account.serializers.common import UserShortSerializer
from establishment import models
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 main.models import Currency
from location.models import Address
from main.serializers import AwardSerializer
from utils.decorators import with_base_attributes
from utils.serializers import TimeZoneChoiceField
from gallery.models import Image
from django.utils.translation import gettext_lazy as _
from account.serializers.common import UserShortSerializer
from utils.serializers import TimeZoneChoiceField, ImageBaseSerializer
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):
@ -26,8 +40,6 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria
queryset=models.Address.objects.all(),
write_only=True
)
phones = model_serializers.ContactPhonesSerializer(read_only=True,
many=True, )
emails = model_serializers.ContactEmailsSerializer(read_only=True,
many=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',
queryset=Address.objects.all())
tz = TimeZoneChoiceField()
phones = serializers.ListField(
source='contact_phones',
allow_null=True,
allow_empty=True,
child=serializers.CharField(max_length=128),
required=False,
)
class Meta:
model = models.Establishment
@ -64,6 +83,15 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria
'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):
"""Establishment create serializer"""
@ -73,18 +101,24 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer):
queryset=models.EstablishmentType.objects.all(), write_only=True
)
address = AddressDetailSerializer()
phones = model_serializers.ContactPhonesSerializer(read_only=False,
many=True, )
emails = model_serializers.ContactEmailsSerializer(read_only=False,
many=True, )
socials = model_serializers.SocialNetworkRelatedSerializers(read_only=False,
many=True, )
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:
model = models.Establishment
fields = [
'id',
'slug',
'name',
'website',
'phones',
@ -101,6 +135,15 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer):
'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):
"""SocialChoice serializers."""
@ -166,7 +209,6 @@ class ContactEmailBackSerializers(model_serializers.PlateSerializer):
]
class PositionBackSerializer(serializers.ModelSerializer):
"""Position Back serializer."""
@ -181,6 +223,7 @@ class PositionBackSerializer(serializers.ModelSerializer):
'index_name',
]
# TODO: test decorator
@with_base_attributes
class EmployeeBackSerializers(serializers.ModelSerializer):
@ -189,25 +232,52 @@ class EmployeeBackSerializers(serializers.ModelSerializer):
positions = serializers.SerializerMethodField()
establishment = serializers.SerializerMethodField()
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):
"""Get last list actual public_mark"""
qs = obj.establishmentemployee_set.actual().order_by('-from_date')\
.values('establishment__public_mark').first()
return qs['establishment__public_mark'] if qs else None
if hasattr(obj, 'prefetched_establishment_employee'):
return obj.prefetched_establishment_employee[0].establishment.public_mark if len(
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):
"""Get last list actual positions"""
est_id = obj.establishmentemployee_set.actual().\
order_by('-from_date').first()
if hasattr(obj, 'prefetched_establishment_employee'):
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:
return None
qs = obj.establishmentemployee_set.actual()\
.filter(establishment_id=est_id.establishment_id)\
qs = obj.establishmentemployee_set.actual() \
.filter(establishment_id=est_id.establishment_id) \
.prefetch_related('position').values('position')
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):
"""Get last actual establishment"""
est = obj.establishmentemployee_set.actual().order_by('-from_date')\
.first()
if hasattr(obj, 'prefetched_establishment_employee'):
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:
return None
return {
"id": est.establishment.id,
"slug": est.establishment.slug
"id": est.est_id,
"slug": est.est_slug
}
class Meta:
@ -242,7 +316,9 @@ class EmployeeBackSerializers(serializers.ModelSerializer):
'birth_date',
'email',
'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):
"""Serializer class for model EstablishmentGallery."""
@ -380,6 +503,7 @@ class EstablishmentNoteListCreateSerializer(EstablishmentNoteBaseSerializer):
class EstablishmentAdminListSerializer(UserShortSerializer):
"""Establishment admin serializer."""
class Meta:
model = UserShortSerializer.Meta.model
fields = [

View File

@ -1,4 +1,7 @@
"""Establishment serializers."""
import logging
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from phonenumber_field.phonenumber import to_python as str_to_phonenumber
from rest_framework import serializers
@ -6,8 +9,10 @@ from rest_framework import serializers
from comment import models as comment_models
from comment.serializers import common as comment_serializers
from establishment import models
from location.serializers import AddressBaseSerializer, CitySerializer, AddressDetailSerializer, \
from location.serializers import AddressBaseSerializer, CityBaseSerializer, AddressDetailSerializer, \
CityShortSerializer
from location.serializers import EstablishmentWineRegionBaseSerializer, \
EstablishmentWineOriginBaseSerializer
from main.serializers import AwardSerializer, CurrencySerializer
from review.serializers import ReviewShortSerializer
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 (ProjectModelSerializer, TranslatedField,
FavoritesCreateSerializer)
from location.serializers import EstablishmentWineRegionBaseSerializer, \
EstablishmentWineOriginBaseSerializer
logger = logging.getLogger(__name__)
class ContactPhonesSerializer(serializers.ModelSerializer):
"""Contact phone serializer"""
@ -198,7 +202,7 @@ class EstablishmentEmployeeCreateSerializer(serializers.ModelSerializer):
"""Meta class."""
model = models.EstablishmentEmployee
fields = ('id',)
fields = ('id', 'from_date', 'to_date')
def _validate_entity(self, entity_id_param: str, entity_class):
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):
"""Short serializer for establishment."""
city = CitySerializer(source='address.city', allow_null=True)
city = CityBaseSerializer(source='address.city', allow_null=True)
establishment_type = EstablishmentTypeGeoSerializer()
establishment_subtypes = EstablishmentSubTypeBaseSerializer(many=True)
currency = CurrencySerializer(read_only=True)
@ -253,10 +257,9 @@ class EstablishmentShortSerializer(serializers.ModelSerializer):
class _EstablishmentAddressShortSerializer(serializers.ModelSerializer):
"""Short serializer for establishment."""
city = CitySerializer(source='address.city', allow_null=True)
city = CityBaseSerializer(source='address.city', allow_null=True)
establishment_type = EstablishmentTypeGeoSerializer()
establishment_subtypes = EstablishmentSubTypeBaseSerializer(many=True)
currency = CurrencySerializer(read_only=True)
address = AddressBaseSerializer(read_only=True)
class Meta:
@ -320,15 +323,45 @@ class EstablishmentBaseSerializer(ProjectModelSerializer):
currency = CurrencySerializer()
type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True)
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',
read_only=True, allow_null=True)
preview_image = serializers.URLField(source='preview_image_url',
allow_null=True,
read_only=True)
tz = serializers.CharField(read_only=True, source='timezone_as_str')
new_image = ImageBaseSerializer(source='crop_main_image', allow_null=True, read_only=True)
distillery_types = TagBaseSerializer(read_only=True, many=True, allow_null=True)
new_image = serializers.SerializerMethodField(allow_null=True, read_only=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:
"""Meta class."""
@ -354,7 +387,7 @@ class EstablishmentBaseSerializer(ProjectModelSerializer):
'new_image',
'tz',
'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_cuisine = TagBaseSerializer(read_only=True, many=True, allow_null=True)
artisan_category = TagBaseSerializer(read_only=True, many=True, allow_null=True)
distillery_type = TagBaseSerializer(read_only=True, many=True, allow_null=True)
class Meta(EstablishmentBaseSerializer.Meta):
"""Meta class."""
@ -375,6 +409,7 @@ class EstablishmentListRetrieveSerializer(EstablishmentBaseSerializer):
'restaurant_category',
'restaurant_cuisine',
'artisan_category',
'distillery_type',
]
@ -449,7 +484,7 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer):
class MobileEstablishmentDetailSerializer(EstablishmentDetailSerializer):
"""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):
"""Meta class."""
@ -468,6 +503,7 @@ class EstablishmentSimilarSerializer(EstablishmentBaseSerializer):
artisan_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)
distillery_type = TagBaseSerializer(many=True, allow_null=True, read_only=True)
class Meta(EstablishmentBaseSerializer.Meta):
fields = EstablishmentBaseSerializer.Meta.fields + [
@ -476,16 +512,15 @@ class EstablishmentSimilarSerializer(EstablishmentBaseSerializer):
'artisan_category',
'restaurant_category',
'restaurant_cuisine',
'distillery_type',
]
class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer):
class EstablishmentCommentBaseSerializer(comment_serializers.CommentBaseSerializer):
"""Create comment serializer"""
mark = serializers.IntegerField()
class Meta:
class Meta(comment_serializers.CommentBaseSerializer.Meta):
"""Serializer for model Comment"""
model = comment_models.Comment
fields = [
'id',
'created',
@ -493,8 +528,14 @@ class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer
'mark',
'nickname',
'profile_pic',
'status',
'status_display',
]
class EstablishmentCommentCreateSerializer(EstablishmentCommentBaseSerializer):
"""Extended EstablishmentCommentBaseSerializer."""
def validate(self, attrs):
"""Override validate method"""
# Check establishment object
@ -514,7 +555,7 @@ class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer
return super().create(validated_data)
class EstablishmentCommentRUDSerializer(comment_serializers.CommentSerializer):
class EstablishmentCommentRUDSerializer(comment_serializers.CommentBaseSerializer):
"""Retrieve/Update/Destroy comment serializer."""
class Meta:

View File

@ -1,17 +1,14 @@
"""Establishment app tasks."""
import logging
import requests
from celery import shared_task
from celery.schedules import crontab
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 establishment import models
from location.models import Country
from search_indexes.documents.establishment import EstablishmentDocument
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,
high_price=country.high_price)
# @periodic_task(run_every=crontab(minute=59))
# def rebuild_establishment_indices():
# 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.recalculate_public_mark()
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(),
name='establishment-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('<int:establishment_id>/employee/<int:employee_id>/position/<int:position_id>',
views.EstablishmentEmployeeCreateView.as_view(),

View File

@ -2,7 +2,7 @@
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics, permissions, status
from rest_framework import generics, permissions, status, filters as rest_filters
from account.models import User
from establishment import filters, models, serializers
@ -34,7 +34,10 @@ class EstablishmentListCreateView(EstablishmentMixinViews, generics.ListCreateAP
class EstablishmentRUDView(generics.RetrieveUpdateDestroyAPIView):
lookup_field = 'slug'
queryset = models.Establishment.objects.all()
queryset = models.Establishment.objects.all().prefetch_related(
'establishmentemployee_set',
'establishmentemployee_set__establishment',
)
serializer_class = serializers.EstablishmentRUDSerializer
permission_classes = [IsWineryReviewer | IsCountryAdmin | IsEstablishmentManager]
@ -171,49 +174,61 @@ class EmployeeListCreateView(generics.ListCreateAPIView):
permission_classes = (permissions.AllowAny,)
filter_class = filters.EmployeeBackFilter
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):
"""Establishment emplyoees list view."""
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.EstablishmentEmployeeBackSerializer
serializer_class = serializers.EstEmployeeBackSerializer
pagination_class = None
def get_queryset(self):
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):
"""Employee RUD view."""
serializer_class = serializers.EmployeeBackSerializers
queryset = models.Employee.objects.all()
queryset = models.Employee.objects.all().with_back_office_related()
class EstablishmentTypeListCreateView(generics.ListCreateAPIView):
"""Establishment type list/create view."""
serializer_class = serializers.EstablishmentTypeBaseSerializer
queryset = models.EstablishmentType.objects.all()
queryset = models.EstablishmentType.objects.all().select_related('default_image')
pagination_class = None
class EstablishmentTypeRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Establishment type retrieve/update/destroy view."""
serializer_class = serializers.EstablishmentTypeBaseSerializer
queryset = models.EstablishmentType.objects.all()
queryset = models.EstablishmentType.objects.all().select_related('default_image')
class EstablishmentSubtypeListCreateView(generics.ListCreateAPIView):
"""Establishment subtype list/create view."""
serializer_class = serializers.EstablishmentSubTypeBaseSerializer
queryset = models.EstablishmentSubType.objects.all()
queryset = models.EstablishmentSubType.objects.all().select_related('default_image')
pagination_class = None
class EstablishmentSubtypeRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Establishment subtype retrieve/update/destroy view."""
serializer_class = serializers.EstablishmentSubTypeBaseSerializer
queryset = models.EstablishmentSubType.objects.all()
queryset = models.EstablishmentSubType.objects.all().select_related('default_image')
class EstablishmentGalleryCreateDestroyView(EstablishmentMixinViews,
@ -385,7 +400,7 @@ class EstablishmentPositionListView(generics.ListAPIView):
class EstablishmentAdminView(generics.ListAPIView):
"""Establishment admin list view."""
serializer_class = serializers.EstablishmentAdminListSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly, )
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def get_queryset(self):
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 comment import models as comment_models
from comment.serializers import CommentRUDSerializer
from establishment import filters, models, serializers
from main import methods
from utils.pagination import PortionPagination
@ -38,7 +37,8 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
.with_extended_address_related().with_currency_related() \
.with_certain_tag_category_related('category', 'restaurant_category') \
.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):
@ -67,7 +67,8 @@ class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView
serializer_class = serializers.EstablishmentDetailSerializer
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):
@ -186,18 +187,17 @@ class EstablishmentCommentListView(generics.ListAPIView):
"""View for return list of establishment comments."""
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.EstablishmentCommentCreateSerializer
serializer_class = serializers.EstablishmentCommentBaseSerializer
def get_queryset(self):
"""Override get_queryset method"""
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):
"""View for retrieve/update/destroy establishment comment."""
serializer_class = CommentRUDSerializer
serializer_class = serializers.EstablishmentCommentBaseSerializer
queryset = models.Establishment.objects.all()
def get_object(self):

View File

@ -32,7 +32,8 @@ class FavoritesEstablishmentListView(generics.ListAPIView):
.order_by('-favorites').with_base_related() \
.with_certain_tag_category_related('category', 'restaurant_category') \
.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):

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
class RegionQuerySet(models.QuerySet):
"""QuerySet for model Region."""
@ -144,7 +143,7 @@ class CityQuerySet(models.QuerySet):
return self.filter(country__code=code)
class City(GalleryMixin, models.Model):
class City(models.Model):
"""Region model."""
name = models.CharField(_('name'), max_length=250)
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)
map_ref = models.CharField(max_length=255, blank=True, null=True)
situation = models.CharField(max_length=255, blank=True, null=True)
gallery = models.ManyToManyField('gallery.Image', through='location.CityGallery', blank=True)
image = models.ForeignKey('gallery.Image', on_delete=models.SET_NULL,
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)
@ -181,23 +182,29 @@ class City(GalleryMixin, models.Model):
def __str__(self):
return self.name
@property
def image_object(self):
"""Return image object."""
return self.image.image if self.image else None
class CityGallery(IntermediateGalleryModelMixin):
"""Gallery for model City."""
city = models.ForeignKey(City, null=True,
related_name='city_gallery',
on_delete=models.CASCADE,
verbose_name=_('city'))
image = models.ForeignKey('gallery.Image', null=True,
related_name='city_gallery',
on_delete=models.CASCADE,
verbose_name=_('image'))
class Meta:
"""CityGallery meta class."""
verbose_name = _('city gallery')
verbose_name_plural = _('city galleries')
unique_together = (('city', 'is_main'), ('city', 'image'))
@property
def crop_image(self):
if hasattr(self, 'image') and hasattr(self, '_meta'):
if self.image:
image_property = {
'id': self.image.id,
'title': self.image.title,
'original_url': self.image.image.url,
'orientation_display': self.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: self.image.get_image_url(crop)}
)
return image_property
class Address(models.Model):

View File

@ -1,8 +1,5 @@
from location import models
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):
@ -21,46 +18,3 @@ class CountryBackSerializer(common.CountrySerializer):
'name',
'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 rest_framework import serializers
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):
@ -70,7 +71,7 @@ class CityShortSerializer(serializers.ModelSerializer):
)
class CitySerializer(serializers.ModelSerializer):
class CityBaseSerializer(serializers.ModelSerializer):
"""City serializer."""
region = RegionSerializer(read_only=True)
region_id = serializers.PrimaryKeyRelatedField(
@ -83,6 +84,11 @@ class CitySerializer(serializers.ModelSerializer):
queryset=models.Country.objects.all(),
write_only=True
)
image_id = serializers.PrimaryKeyRelatedField(
source='image',
queryset=gallery_models.Image.objects.all(),
write_only=True
)
country = CountrySerializer(read_only=True)
class Meta:
@ -96,6 +102,22 @@ class CitySerializer(serializers.ModelSerializer):
'country',
'postal_code',
'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(
source='city', write_only=True,
queryset=models.City.objects.all())
city = CitySerializer(read_only=True)
city = CityBaseSerializer(read_only=True)
class Meta(AddressBaseSerializer.Meta):
"""Meta class."""

View File

@ -10,7 +10,7 @@ from tqdm import tqdm
from account.models import Role
from collection.models import Collection
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 news.models import News
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}")
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
# Utils functions defined before transfer functions
def get_ruby_socket(params):
@ -554,10 +531,10 @@ def remove_old_records():
clean_old_region_records(Region, {"mysql_ids__isnull": True})
def transfer_city_gallery():
def transfer_city_photos():
created_counter = 0
cities_not_exists = {}
gallery_obj_exists_counter = 0
cities_has_same_image = 0
city_gallery = transfer_models.CityPhotos.objects.exclude(city__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__iexact='') \
.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)
if city.exists():
city = city.first()
@ -575,19 +552,18 @@ def transfer_city_gallery():
'orientation': Image.HORIZONTAL,
'title': f'{city.name} - {image_suffix_url}',
})
city_gallery, created = CityGallery.objects.get_or_create(image=image,
city=city,
is_main=True)
if created:
if city.image != image:
city.image = image
city.save()
created_counter += 1
else:
gallery_obj_exists_counter += 1
cities_has_same_image += 1
else:
cities_not_exists.update({'city_old_id': old_city_id})
print(f'Created: {created_counter}\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
@ -758,8 +734,8 @@ def setup_clean_db():
print('update_flags')
update_flags()
print('transfer_city_gallery')
transfer_city_gallery()
print('transfer_city_photos')
transfer_city_photos()
def set_unused_regions():
@ -796,7 +772,6 @@ def set_unused_regions():
)
data_types = {
"dictionaries": [
# transfer_countries,
@ -813,9 +788,6 @@ data_types = {
"update_city_info": [
migrate_city_map_situation
],
"migrate_city_gallery": [
migrate_city_photos
],
"fix_location": [
add_fake_country,
fix_location_models,
@ -823,13 +795,12 @@ data_types = {
"remove_old_locations": [
remove_old_records
],
"fill_city_gallery": [
transfer_city_gallery
"migrate_city_photos": [
transfer_city_photos,
],
"add_fake_country": [
add_fake_country,
],
"setup_clean_db": [setup_clean_db],
"set_unused_regions": [set_unused_regions],
"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/all/', views.CityListSearchView.as_view(), name='city-list-create'),
path('cities/<int:pk>/', views.CityRUDView.as_view(), name='city-retrieve'),
path('cities/<int:pk>/gallery/', views.CityGalleryListView.as_view(),
name='gallery-list'),
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/<int:pk>/', views.CountryRUDView.as_view(), name='country-retrieve'),

View File

@ -36,7 +36,7 @@ class AddressRUDView(common.AddressViewMixin, generics.RetrieveUpdateDestroyAPIV
# City
class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView):
"""Create view for model City."""
serializer_class = serializers.CitySerializer
serializer_class = serializers.CityBaseSerializer
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
queryset = models.City.objects.all()
filter_class = filters.CityBackFilter
@ -52,7 +52,7 @@ class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView):
class CityListSearchView(common.CityViewMixin, generics.ListCreateAPIView):
"""Create view for model City."""
serializer_class = serializers.CitySerializer
serializer_class = serializers.CityBaseSerializer
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
queryset = models.City.objects.all()\
.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):
"""RUD view for model City."""
serializer_class = serializers.CitySerializer
serializer_class = serializers.CityDetailSerializer
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
class RegionListCreateView(common.RegionViewMixin, generics.ListCreateAPIView):
"""Create view for model Region"""

View File

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

View File

@ -14,9 +14,18 @@ class SiteSettingsAdmin(admin.ModelAdmin):
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)
class FeatureAdmin(admin.ModelAdmin):
"""Feature admin conf."""
list_display = ['id', '__str__', 'priority', 'route', ]
@admin.register(models.AwardType)
@ -46,6 +55,7 @@ class CarouselAdmin(admin.ModelAdmin):
@admin.register(models.PageType)
class PageTypeAdmin(admin.ModelAdmin):
"""PageType admin."""
list_display = ['id', '__str__', ]
@admin.register(models.Page)
@ -80,3 +90,13 @@ class PanelAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'user', 'created',)
raw_id_fields = ('user',)
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.postgres.fields import JSONField
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.models import Q
from django.utils.translation import gettext_lazy as _
from mptt.models import MPTTModel, TreeForeignKey
from rest_framework import exceptions
from configuration.models import TranslationSettings
from location.models import Country
from main import methods
from review.models import Review
from tag.models import Tag
from utils.exceptions import UnprocessableEntityError
from utils.methods import dictfetchall
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
@ -112,11 +114,13 @@ class Feature(ProjectBaseMixin, PlatformMixin):
"""Feature model."""
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)
site_settings = models.ManyToManyField(SiteSettings, through='SiteFeature')
old_id = models.IntegerField(null=True, blank=True)
chosen_tags = generic.GenericRelation(to='tag.ChosenTag')
class Meta:
"""Meta class."""
verbose_name = _('Feature')
@ -125,6 +129,10 @@ class Feature(ProjectBaseMixin, PlatformMixin):
def __str__(self):
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):
"""Extended queryset for SiteFeature model."""
@ -144,9 +152,15 @@ class SiteFeature(ProjectBaseMixin):
site_settings = models.ForeignKey(SiteSettings, on_delete=models.CASCADE)
feature = models.ForeignKey(Feature, on_delete=models.PROTECT)
published = models.BooleanField(default=False, verbose_name=_('Published'))
main = models.BooleanField(default=False, verbose_name=_('Main'))
nested = models.ManyToManyField('self', symmetrical=False)
main = models.BooleanField(default=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)
objects = SiteFeatureQuerySet.as_manager()
@ -216,6 +230,8 @@ class CarouselQuerySet(models.QuerySet):
def by_country_code(self, 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)
def get_international(self):
@ -520,3 +536,28 @@ class Panel(ProjectBaseMixin):
params = params + new_params
query = self.query + limit_offset
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 main import models
from tag.serializers import TagBackOfficeSerializer
from utils.serializers import ProjectModelSerializer, TranslatedField, RecursiveFieldSerializer
from account.serializers.back import BackUserSerializer
from account.models import User
class FeatureSerializer(serializers.ModelSerializer):
@ -84,24 +83,46 @@ class FooterBackSerializer(FooterSerializer):
class SiteFeatureSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(source='feature.id')
slug = serializers.CharField(source='feature.slug')
priority = serializers.IntegerField(source='feature.priority')
route = serializers.CharField(source='feature.route.name')
source = serializers.IntegerField(source='feature.source')
nested = RecursiveFieldSerializer(many=True, allow_null=True)
id = serializers.IntegerField(source='feature.id', allow_null=True)
slug = serializers.CharField(source='feature.slug', allow_null=True)
priority = serializers.IntegerField(source='feature.priority', allow_null=True)
route = serializers.CharField(source='feature.route.name', allow_null=True)
source = serializers.IntegerField(source='feature.source', 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:
"""Meta class."""
model = models.SiteFeature
fields = ('main',
'id',
'slug',
'priority',
'route',
'source',
'nested',
)
fields = (
'id',
'main',
'slug',
'priority',
'route',
'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):
@ -291,30 +312,6 @@ class ContentTypeBackSerializer(serializers.ModelSerializer):
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):
"""Panel execute serializer."""
@ -331,3 +328,20 @@ class PanelExecuteSerializer(serializers.ModelSerializer):
'user',
'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 main import serializers
from main.serializers.back import PanelSerializer
from main import tasks
from main.filters import AwardFilter
from main.models import Award, Footer, PageType, Panel, SiteFeature, Feature
@ -108,7 +109,7 @@ class PanelsListCreateView(generics.ListCreateAPIView):
permission_classes = (
permissions.IsAdminUser,
)
serializer_class = serializers.PanelSerializer
serializer_class = PanelSerializer
queryset = Panel.objects.all()
@ -117,7 +118,7 @@ class PanelsRUDView(generics.RetrieveUpdateDestroyAPIView):
permission_classes = (
permissions.IsAdminUser,
)
serializer_class = serializers.PanelSerializer
serializer_class = PanelSerializer
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.db import models
from django.db.models import Case, When
from django.urls.exceptions import NoReverseMatch
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from rest_framework.reverse import reverse
@ -54,7 +55,6 @@ class NewsType(models.Model):
name = models.CharField(_('name'), max_length=250)
tag_categories = models.ManyToManyField('tag.TagCategory',
related_name='news_types')
chosen_tags = generic.GenericRelation(to='tag.ChosenTag')
class Meta:
"""Meta class."""
@ -79,7 +79,7 @@ class NewsQuerySet(TranslationQuerysetMixin):
def with_base_related(self):
"""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):
"""Return qs with related objects."""
@ -296,7 +296,10 @@ class News(GalleryMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin,
@property
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):
return self.__class__.objects.should_read(self, user)[:3]
@ -307,8 +310,9 @@ class News(GalleryMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin,
@property
def main_image(self):
qs = self.news_gallery.main_image()
if qs.exists():
return qs.order_by('-id').first().image
image_model = qs.order_by('-id').first()
if image_model is not None:
return image_model.image
@property
def image_url(self):

View File

@ -22,7 +22,9 @@ from utils.serializers import (
class AgendaSerializer(ProjectModelSerializer):
start_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()
content_translated = TranslatedField()
@ -36,7 +38,8 @@ class AgendaSerializer(ProjectModelSerializer):
'end_datetime',
'address',
'content_translated',
'event_name_translated'
'event_name_translated',
'address_id',
)
@ -158,6 +161,7 @@ class NewsDetailWebSerializer(NewsDetailSerializer):
should_read = SerializerMethodField()
agenda = AgendaSerializer()
banner = NewsBannerSerializer()
in_favorites = serializers.BooleanField(read_only=True)
class Meta(NewsDetailSerializer.Meta):
"""Meta class."""
@ -167,6 +171,7 @@ class NewsDetailWebSerializer(NewsDetailSerializer):
'should_read',
'agenda',
'banner',
'in_favorites',
)
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
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):
"""News back office base serializer."""
is_published = serializers.BooleanField(source='is_publish', read_only=True)
descriptions = serializers.ListField(required=False)
agenda = AgendaSerializer()
class Meta(NewsBaseSerializer.Meta):
"""Meta class."""
@ -198,6 +224,7 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
'created',
'modified',
'descriptions',
'agenda'
)
extra_kwargs = {
'created': {'read_only': True},
@ -228,6 +255,7 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
for locale in locales:
if not attrs[key].get(locale):
attrs[key][locale] = getattr(instance, key).get(locale)
return attrs
def create(self, validated_data):
@ -245,10 +273,25 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
user = request.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):
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:
slugs_list = list(map(lambda x: x.lower(), slugs.values()))
slugs_set = set(slugs_list)
@ -256,6 +299,29 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
slugs__values__contains=list(slugs.values())
).exists() or len(slugs_list) != len(slugs_set):
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)

View File

@ -11,6 +11,7 @@ from tag.models import TagCategory, Tag
from translation.models import SiteInterfaceDictionary
from transfer.models import PageTexts, PageCounters, PageMetadata
from transfer.serializers.news import NewsSerializer
from utils.methods import transform_camelcase_to_underscore
def add_locale(locale, data):
@ -36,35 +37,38 @@ def clear_old_news():
images.delete()
news.delete()
# NewsType.objects.all().delete()
print(f'Deleted {img_num} images')
print(f'Deleted {news_num} news')
def transfer_news():
news_type, _ = NewsType.objects.get_or_create(name='news')
migrated_news_types = ('News', 'StaticPage', )
queryset = PageTexts.objects.filter(
page__type='News',
).annotate(
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'),
)
for news_type in migrated_news_types:
news_type_obj, _ = NewsType.objects.get_or_create(
name=transform_camelcase_to_underscore(news_type))
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}')
queryset = PageTexts.objects.filter(
page__type=news_type,
).annotate(
page__id=F('page__id'),
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():
@ -166,5 +170,5 @@ data_types = {
update_en_gb_locales,
add_views_count,
add_tags,
]
],
}

View File

@ -7,4 +7,5 @@ common_urlpatterns = [
path('slug/<slug:slug>/', views.NewsDetailView.as_view(), name='rud'),
path('slug/<slug:slug>/favorites/', views.NewsFavoritesCreateDestroyView.as_view(),
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
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):
"""NewsType list view."""

View File

@ -1,3 +1,20 @@
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'),
(BOOK, 'book')
)
INDEX_PLURAL_ONE = {
'food': 'food',
'wines': 'wine',
'liquors': 'liquor',
}
name = TJSONField(blank=True, null=True, default=None,
verbose_name=_('Name'), help_text='{"en-GB":"some text"}')
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 self.filter(establishment__address__coordinates__isnull=False)
def same_subtype(self, product):
def annotate_same_subtype(self, product):
"""Annotate flag same subtype."""
return self.annotate(same_subtype=Case(
models.When(
@ -215,7 +222,7 @@ class ProductQuerySet(models.QuerySet):
similarity_rules['ordering'].append(F('distance').asc())
similarity_rules['distinction'].append('distance')
return self.similar_base(product) \
.same_subtype(product) \
.annotate_same_subtype(product) \
.order_by(*similarity_rules['ordering']) \
.distinct(*similarity_rules['distinction'],
'id')

View File

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

View File

@ -19,10 +19,13 @@ urlpatterns = [
# similar products by type/subtype
# temporary uses single mechanism, bec. description in process
path('slug/<slug:slug>/similar/wines/', views.SimilarListView.as_view(),
name='similar-wine'),
path('slug/<slug:slug>/similar/liquors/', views.SimilarListView.as_view(),
name='similar-liquor'),
path('slug/<slug:slug>/similar/food/', views.SimilarListView.as_view(),
name='similar-food'),
# path('slug/<slug:slug>/similar/wines/', views.SimilarListView.as_view(),
# name='similar-wine'),
# path('slug/<slug:slug>/similar/liquors/', views.SimilarListView.as_view(),
# name='similar-liquor'),
# path('slug/<slug:slug>/similar/food/', views.SimilarListView.as_view(),
# name='similar-food'),
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 comment.models import Comment
from comment.serializers import CommentRUDSerializer
from comment.serializers import CommentBaseSerializer
from product import filters, serializers
from product.models import Product
from product.models import Product, ProductType
from utils.views import FavoritesCreateDestroyMixinView
from utils.pagination import PortionPagination
from django.conf import settings
class ProductBaseView(generics.GenericAPIView):
@ -44,8 +42,16 @@ class ProductSimilarView(ProductListView):
"""
Return base product instance for a getting list of similar products.
"""
product = get_object_or_404(Product.objects.all(),
slug=self.kwargs.get('slug'))
find_by = {
'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
@ -73,17 +79,17 @@ class ProductCommentListView(generics.ListAPIView):
"""View for return list of product comments."""
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.ProductCommentCreateSerializer
serializer_class = serializers.ProductCommentBaseSerializer
def get_queryset(self):
"""Override get_queryset method"""
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):
"""View for retrieve/update/destroy product comment."""
serializer_class = CommentRUDSerializer
serializer_class = serializers.ProductCommentBaseSerializer
queryset = Product.objects.all()
def get_object(self):

View File

@ -77,15 +77,15 @@ class EstablishmentDocument(Document):
'value': fields.KeywordField(),
},
multi=True, attr='artisan_category_indexing')
visible_tags = fields.ObjectField(
distillery_type = fields.ObjectField(
properties={
'id': fields.IntegerField(attr='id'),
'label': fields.ObjectField(attr='label_indexing',
properties=OBJECT_FIELD_PROPERTIES),
'value': fields.KeywordField(),
},
multi=True)
distillery_types = fields.ObjectField(
multi=True, attr='distillery_type_indexing')
visible_tags = fields.ObjectField(
properties={
'id': fields.IntegerField(attr='id'),
'label': fields.ObjectField(attr='label_indexing',
@ -153,6 +153,7 @@ class EstablishmentDocument(Document):
'id': fields.IntegerField(attr='id'),
'weekday': fields.IntegerField(attr='weekday'),
'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'),
'opening_at': fields.KeywordField(attr='opening_at_str'),
'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
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()

View File

@ -108,9 +108,9 @@ class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend):
tag_facets = []
preserve_ids = []
facet_name = '_filter_' + __field
all_tag_categories = TagCategoryDocument.search() \
all_tag_categories = list(TagCategoryDocument.search() \
.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:
tags_to_remove = list(map(lambda t: str(t.id), category.tags))
qs = queryset.__copy__()

View File

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

View File

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

View File

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

View File

@ -90,6 +90,7 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet):
params_type = query_params.get('product_type')
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')
filter_flags = {flag_name: False for flag_name in flags}
additional_flags = []
@ -155,7 +156,7 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet):
"filters": [{
"id": weekday,
"index_name": week_days[weekday].lower(),
"label_translated": week_days[weekday]
"label_translated": short_week_days[weekday],
} for weekday in range(7)]
}
result_list.append(works_noon)
@ -170,7 +171,7 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet):
"filters": [{
"id": weekday,
"index_name": week_days[weekday].lower(),
"label_translated": week_days[weekday]
"label_translated": short_week_days[weekday],
} for weekday in range(7)]
}
result_list.append(works_evening)
@ -193,7 +194,7 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet):
"filters": [{
"id": weekday,
"index_name": week_days[weekday].lower(),
"label_translated": week_days[weekday]
"label_translated": short_week_days[weekday],
} for weekday in range(7)]
}
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.utils.translation import gettext_lazy as _
from datetime import time, datetime
from utils.models import ProjectBaseMixin
@ -24,7 +25,8 @@ class Timetable(ProjectBaseMixin):
(THURSDAY, _('Thursday')),
(FRIDAY, _('Friday')),
(SATURDAY, _('Saturday')),
(SUNDAY, _('Sunday')))
(SUNDAY, _('Sunday'))
)
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_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
def closed_at_str(self):
return str(self.closed_at) if self.closed_at else None
@ -61,11 +70,13 @@ class Timetable(ProjectBaseMixin):
@property
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
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
def opening_time(self):

View File

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

View File

@ -40,7 +40,6 @@ class Command(BaseCommand):
'product_review',
'newsletter_subscriber', # подписчики на рассылку - переносить после переноса пользователей №1
'purchased_plaques', # №6 - перенос купленных тарелок
'fill_city_gallery', # №3 - перенос галереи городов
'guides',
'guide_filters',
'guide_element_sections',
@ -51,7 +50,6 @@ class Command(BaseCommand):
'guide_element_label_photo',
'guide_complete',
'update_city_info',
'migrate_city_gallery',
'fix_location',
'remove_old_locations',
'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)
account_id = serializers.IntegerField()
establishment_id = serializers.CharField()
state = serializers.CharField()
def validate(self, data):
data.update({
@ -18,14 +19,18 @@ class CommentSerializer(serializers.Serializer):
'mark': self.get_mark(data),
'content_object': self.get_content_object(data),
'user': self.get_account(data),
'status': self.get_status(data),
})
data.pop('establishment_id')
data.pop('account_id')
data.pop('state')
return data
def create(self, validated_data):
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:
raise ValueError(f"Error creating comment with {validated_data}: {e}")
@ -48,3 +53,12 @@ class CommentSerializer(serializers.Serializer):
if not data['mark']:
return None
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()
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):
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."""
from django.contrib.postgres.fields import JSONField
from django.contrib.postgres.indexes import GinIndex
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.apps import apps
@ -105,6 +106,9 @@ class SiteInterfaceDictionary(ProjectBaseMixin):
verbose_name = _('Site interface dictionary')
verbose_name_plural = _('Site interface dictionary')
indexes = [
GinIndex(fields=['text'])
]
def __str__(self):
return f'{self.page}: {self.keywords}'

View File

@ -4,6 +4,8 @@ import random
import re
import string
from collections import namedtuple
from functools import reduce
from io import BytesIO
import requests
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.http.request import HttpRequest
from django.utils.timezone import datetime
from PIL import Image
from rest_framework import status
from rest_framework.request import Request
@ -53,6 +56,19 @@ def username_validator(username: str) -> bool:
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):
"""Determine avatar path method."""
filename = '%s.jpeg' % generate_code()
@ -119,6 +135,7 @@ def absolute_url_decorator(func):
return f'{settings.MEDIA_URL}{url_path}/'
else:
return url_path
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])}"
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):
"""
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)
if result:
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."""
import logging
from django.utils import translation, timezone
from django.db import connection
from account.models import User
from configuration.models import TranslationSettings
@ -7,6 +10,8 @@ from main.methods import determine_user_city
from main.models import SiteSettings
from translation.models import Language
logger = logging.getLogger(__name__)
def get_locale(cookie_dict):
return cookie_dict.get('locale')
@ -92,3 +97,26 @@ def user_last_ip(get_response):
return response
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."""
crop_image = self.get_image(thumbnail_key)
if hasattr(crop_image, 'url'):
return self.get_image(thumbnail_key).url
return crop_image.url
def image_tag(self):
"""Admin preview tag."""

View File

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

View File

@ -27,4 +27,14 @@
./manage.py transfer --overlook
./manage.py transfer --inquiries
./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
SORL_THUMBNAIL_ALIASES = {
'news_preview': {'geometry_string': '300x260', 'crop': 'center'},
'news_description': {'geometry_string': '100x100'},
'news_promo_horizontal_web': {'geometry_string': '1900x600', 'crop': 'center'},
'news_promo_horizontal_mobile': {'geometry_string': '375x260', '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_original': {'geometry_string': '2048x1536', '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_INDEX_NAMES = {}
THUMBNAIL_FORCE_OVERWRITE = True

View File

@ -80,4 +80,7 @@ EMAIL_USE_TLS = True
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_HOST_USER = 'anatolyfeteleu@gmail.com'
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'
},
},
'legacy': {
'ENGINE': 'django.db.backends.mysql',
# 'HOST': '172.22.0.1',
'HOST': 'mysql_db',
'PORT': 3306,
'NAME': 'dev',
'USER': 'dev',
'PASSWORD': 'octosecret123'
},
'legacy': {
'ENGINE': 'django.db.backends.mysql',
# 'HOST': '172.22.0.1',
'HOST': 'mysql_db',
'PORT': 3306,
'NAME': 'dev',
'USER': 'dev',
'PASSWORD': 'octosecret123'
},
}
@ -84,11 +84,11 @@ LOGGING = {
'py.warnings': {
'handlers': ['console'],
},
'django.db.backends': {
'handlers': ['console', ],
'level': 'DEBUG',
'propagate': False,
},
# 'django.db.backends': {
# 'handlers': ['console', ],
# 'level': 'DEBUG',
# 'propagate': False,
# },
}
}

View File

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

View File

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

View File

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