Merge branch 'develop' into 'feature/subscriptions'
# Conflicts: # apps/account/serializers/common.py
This commit is contained in:
commit
27f47cfae6
|
|
@ -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
29
apps/account/filters.py
Normal 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
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
26
apps/account/migrations/0032_auto_20200114_1311.py
Normal file
26
apps/account/migrations/0032_auto_20200114_1311.py
Normal 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),
|
||||
),
|
||||
]
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
from account.serializers.common import *
|
||||
from account.serializers.web import *
|
||||
from account.serializers.back import *
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
18
apps/comment/management/commands/add_status_to_comments.py
Normal file
18
apps/comment/management/commands/add_status_to_comments.py
Normal 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.'))
|
||||
18
apps/comment/migrations/0008_comment_status.py
Normal file
18
apps/comment/migrations/0008_comment_status.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from .common import *
|
||||
from .mobile import *
|
||||
from .back import *
|
||||
from .web import *
|
||||
from .common import *
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
@ -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},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ def transfer_comments():
|
|||
'mark',
|
||||
'establishment_id',
|
||||
'account_id',
|
||||
'state',
|
||||
)
|
||||
|
||||
serialized_data = CommentSerializer(data=list(queryset.values()), many=True)
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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'))
|
||||
16
apps/establishment/migrations/0072_auto_20200115_1702.py
Normal file
16
apps/establishment/migrations/0072_auto_20200115_1702.py
Normal 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(),
|
||||
]
|
||||
22
apps/establishment/migrations/0073_auto_20200115_1710.py
Normal file
22
apps/establishment/migrations/0073_auto_20200115_1710.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
20
apps/establishment/migrations/0075_employee_photo.py
Normal file
20
apps/establishment/migrations/0075_employee_photo.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
20
apps/location/migrations/0034_city_image.py
Normal file
20
apps/location/migrations/0034_city_image.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
20
apps/location/migrations/0035_auto_20200115_1117.py
Normal file
20
apps/location/migrations/0035_auto_20200115_1117.py
Normal 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',
|
||||
),
|
||||
]
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
44
apps/main/migrations/0046_auto_20200114_1218.py
Normal file
44
apps/main/migrations/0046_auto_20200114_1218.py
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
||||
18
apps/main/migrations/0047_auto_20200115_1013.py
Normal file
18
apps/main/migrations/0047_auto_20200115_1013.py
Normal 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),
|
||||
),
|
||||
]
|
||||
24
apps/main/migrations/0048_auto_20200115_1944.py
Normal file
24
apps/main/migrations/0048_auto_20200115_1944.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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',
|
||||
),
|
||||
]
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
1
apps/main/serializers/__init__.py
Normal file
1
apps/main/serializers/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from main.serializers.common import *
|
||||
29
apps/main/serializers/back.py
Normal file
29
apps/main/serializers/back.py
Normal 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'
|
||||
]
|
||||
|
|
@ -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',
|
||||
]
|
||||
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
|||
69
apps/news/management/commands/news_optimize_images.py
Normal file
69
apps/news/management/commands/news_optimize_images.py
Normal 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()
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
|
|
@ -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',)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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__()
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
18
apps/translation/migrations/0008_index_siteifdict_text.py
Normal file
18
apps/translation/migrations/0008_index_siteifdict_text.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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}'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
# },
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ django-storages==1.7.2
|
|||
|
||||
sorl-thumbnail==12.5.0
|
||||
|
||||
|
||||
PyYAML==5.1.2
|
||||
|
||||
# temp solution
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
-r base.txt
|
||||
ipdb
|
||||
ipython
|
||||
mysqlclient==1.4.4
|
||||
mysqlclient==1.4.4
|
||||
|
||||
pyparsing
|
||||
graphviz
|
||||
pydot
|
||||
Loading…
Reference in New Issue
Block a user