Merge branch 'develop' into 'feature/agenda-back-office'
# Conflicts: # apps/news/serializers.py
This commit is contained in:
commit
903bdbb39d
|
|
@ -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)
|
||||
|
|
|
|||
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
|
||||
|
|
@ -53,6 +51,7 @@ class Role(ProjectBaseMixin):
|
|||
(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 +61,11 @@ 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'))
|
||||
|
||||
|
||||
class UserManager(BaseUserManager):
|
||||
|
|
@ -147,8 +151,8 @@ class User(AbstractUser):
|
|||
)
|
||||
|
||||
EMAIL_FIELD = 'email'
|
||||
USERNAME_FIELD = 'username'
|
||||
REQUIRED_FIELDS = ['email']
|
||||
USERNAME_FIELD = 'email'
|
||||
REQUIRED_FIELDS = ['username']
|
||||
|
||||
roles = models.ManyToManyField(
|
||||
Role, verbose_name=_('Roles'), symmetrical=False,
|
||||
|
|
@ -238,7 +242,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 +338,14 @@ 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 UserRole(ProjectBaseMixin):
|
||||
"""UserRole model."""
|
||||
|
|
@ -348,6 +360,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 +371,11 @@ 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')
|
||||
|
||||
class Meta:
|
||||
unique_together = ['user', 'role', 'establishment', 'state']
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -1,19 +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 notification.models import Subscriber
|
||||
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
|
||||
|
|
@ -26,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
|
||||
|
|
@ -39,6 +88,8 @@ class UserSerializer(serializers.ModelSerializer):
|
|||
'email',
|
||||
'email_confirmed',
|
||||
'newsletter',
|
||||
'roles',
|
||||
'subscriptions',
|
||||
]
|
||||
extra_kwargs = {
|
||||
'first_name': {'required': False, 'write_only': True, },
|
||||
|
|
@ -50,9 +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):
|
||||
|
|
@ -70,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:
|
||||
|
|
@ -88,6 +148,8 @@ class UserSerializer(serializers.ModelSerializer):
|
|||
user_id=instance.id,
|
||||
country_code=self.context.get('request').country_code,
|
||||
emails=[validated_data['email'], ])
|
||||
|
||||
subscriptions_handler(subscriptions_list, instance)
|
||||
return instance
|
||||
|
||||
|
||||
|
|
@ -214,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',
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ 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('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'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -8,24 +8,26 @@ from rest_framework.authtoken.models import Token
|
|||
from account import models
|
||||
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 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',
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ from utils.tokens import GMRefreshToken
|
|||
# Serializers
|
||||
class SignupSerializer(serializers.ModelSerializer):
|
||||
"""Signup serializer serializer mixin"""
|
||||
|
||||
class Meta:
|
||||
model = account_models.User
|
||||
fields = (
|
||||
|
|
@ -35,11 +36,13 @@ class SignupSerializer(serializers.ModelSerializer):
|
|||
|
||||
def validate_username(self, value):
|
||||
"""Custom username validation"""
|
||||
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):
|
||||
|
|
@ -59,6 +62,13 @@ class SignupSerializer(serializers.ModelSerializer):
|
|||
|
||||
def create(self, validated_data):
|
||||
"""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,29 @@
|
|||
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):
|
||||
with transaction.atomic():
|
||||
for collection in Collection.objects.all():
|
||||
if not image_url_valid(collection.image_url):
|
||||
continue
|
||||
|
||||
_, width, height = get_image_meta_by_url(collection.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:
|
||||
collection.image_url = get_thumbnail(
|
||||
file_=collection.image_url,
|
||||
**settings.SORL_THUMBNAIL_ALIASES[self.SORL_THUMBNAIL_ALIAS]
|
||||
).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,14 +31,46 @@ 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'))
|
||||
content_type = models.ForeignKey(generic.ContentType, on_delete=models.CASCADE)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -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(field_name='toque_number')
|
||||
username = filters.CharFilter(method='search_by_username_or_name')
|
||||
|
||||
class Meta:
|
||||
"""Meta class."""
|
||||
|
|
@ -72,6 +78,10 @@ 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):
|
||||
|
|
@ -79,3 +89,30 @@ class EmployeeBackFilter(filters.FilterSet):
|
|||
if value not in EMPTY_VALUES:
|
||||
return queryset.search_by_name_or_last_name(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_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()
|
||||
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,6 +11,8 @@ 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
|
||||
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
|
||||
|
|
@ -120,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."""
|
||||
|
|
@ -296,7 +298,7 @@ class EstablishmentQuerySet(models.QuerySet):
|
|||
.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(
|
||||
|
|
@ -312,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', ]
|
||||
|
|
@ -365,10 +367,12 @@ class EstablishmentQuerySet(models.QuerySet):
|
|||
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)
|
||||
|
|
@ -380,18 +384,20 @@ class EstablishmentQuerySet(models.QuerySet):
|
|||
.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')
|
||||
|
||||
|
|
@ -500,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):
|
||||
|
|
@ -647,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(
|
||||
|
|
@ -767,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):
|
||||
|
|
@ -784,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):
|
||||
|
|
@ -969,16 +987,54 @@ 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(
|
||||
name_distance=TrigramDistance('name', search_value.lower()),
|
||||
last_name_distance=TrigramDistance('last_name', search_value.lower()),
|
||||
).filter(Q(name_distance__lte=0.7) | Q(last_name_distance__lte=0.7)).order_by('name_distance')
|
||||
|
||||
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.search_by_actual_employee().filter(
|
||||
Q(establishmentemployee__position_id=value),
|
||||
)
|
||||
|
||||
def search_by_public_mark(self, value):
|
||||
"""Search by establishment public_mark."""
|
||||
return self.search_by_actual_employee().filter(
|
||||
Q(establishmentemployee__establishment__public_mark=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')
|
||||
|
||||
|
||||
class Employee(BaseAttributes):
|
||||
"""Employee model."""
|
||||
|
|
@ -1014,6 +1070,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()
|
||||
|
||||
|
|
@ -1022,6 +1083,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,27 @@
|
|||
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
|
||||
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 +37,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 +46,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 +80,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 +98,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 +132,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 +206,6 @@ class ContactEmailBackSerializers(model_serializers.PlateSerializer):
|
|||
]
|
||||
|
||||
|
||||
|
||||
class PositionBackSerializer(serializers.ModelSerializer):
|
||||
"""Position Back serializer."""
|
||||
|
||||
|
|
@ -181,6 +220,7 @@ class PositionBackSerializer(serializers.ModelSerializer):
|
|||
'index_name',
|
||||
]
|
||||
|
||||
|
||||
# TODO: test decorator
|
||||
@with_base_attributes
|
||||
class EmployeeBackSerializers(serializers.ModelSerializer):
|
||||
|
|
@ -189,7 +229,8 @@ 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)
|
||||
|
||||
def get_public_mark(self, obj):
|
||||
"""Get last list actual public_mark"""
|
||||
|
|
@ -197,6 +238,10 @@ class EmployeeBackSerializers(serializers.ModelSerializer):
|
|||
.values('establishment__public_mark').first()
|
||||
return qs['establishment__public_mark'] if qs else None
|
||||
|
||||
def get_toque_number(self, obj):
|
||||
qs = obj.establishmentemployee_set.actual().order_by('-from_date') \
|
||||
.values('establishment__toque_number').first()
|
||||
return qs['establishment__toque_number'] if qs else None
|
||||
|
||||
def get_positions(self, obj):
|
||||
"""Get last list actual positions"""
|
||||
|
|
@ -242,7 +287,9 @@ class EmployeeBackSerializers(serializers.ModelSerializer):
|
|||
'birth_date',
|
||||
'email',
|
||||
'phone',
|
||||
'toque_number'
|
||||
'toque_number',
|
||||
'available_for_events',
|
||||
'photo',
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -380,6 +427,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"""
|
||||
|
|
@ -54,7 +58,6 @@ class SocialNetworkRelatedSerializers(serializers.ModelSerializer):
|
|||
|
||||
class PlateSerializer(ProjectModelSerializer):
|
||||
name_translated = TranslatedField()
|
||||
currency = CurrencySerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Plate
|
||||
|
|
@ -232,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)
|
||||
|
|
@ -254,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:
|
||||
|
|
@ -321,14 +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)
|
||||
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,6 +387,7 @@ class EstablishmentBaseSerializer(ProjectModelSerializer):
|
|||
'new_image',
|
||||
'tz',
|
||||
'wine_regions',
|
||||
'distillery_type',
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -365,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."""
|
||||
|
|
@ -374,6 +409,7 @@ class EstablishmentListRetrieveSerializer(EstablishmentBaseSerializer):
|
|||
'restaurant_category',
|
||||
'restaurant_cuisine',
|
||||
'artisan_category',
|
||||
'distillery_type',
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -448,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."""
|
||||
|
|
@ -467,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 + [
|
||||
|
|
@ -475,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',
|
||||
|
|
@ -492,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
|
||||
|
|
@ -513,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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -19,7 +19,7 @@ class EstablishmentMixinViews:
|
|||
|
||||
def get_queryset(self):
|
||||
"""Overrided method 'get_queryset'."""
|
||||
return models.Establishment.objects.published().with_base_related()
|
||||
return models.Establishment.objects.with_base_related()
|
||||
|
||||
|
||||
class EstablishmentListCreateView(EstablishmentMixinViews, generics.ListCreateAPIView):
|
||||
|
|
@ -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,17 +174,33 @@ 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().prefetch_related(
|
||||
'establishmentemployee_set',
|
||||
'establishmentemployee_set__establishment',
|
||||
)
|
||||
|
||||
|
||||
class EmployeesListSearchViews(generics.ListAPIView):
|
||||
"""Employee search view"""
|
||||
pagination_class = None
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
queryset = models.Employee.objects.all().prefetch_related(
|
||||
'establishmentemployee_set',
|
||||
'establishmentemployee_set__establishment',
|
||||
).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
|
||||
pagination_class = None
|
||||
|
||||
def get_queryset(self):
|
||||
establishment_id = self.kwargs['establishment_id']
|
||||
return models.EstablishmentEmployee.objects.filter(establishment__id=establishment_id)
|
||||
return models.EstablishmentEmployee.objects.filter(establishment__id=establishment_id).order_by('position')
|
||||
|
||||
|
||||
class EmployeeRUDView(generics.RetrieveUpdateDestroyAPIView):
|
||||
|
|
@ -193,27 +212,27 @@ class EmployeeRUDView(generics.RetrieveUpdateDestroyAPIView):
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -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,7 @@ 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 utils.serializers import TranslatedField, ImageBaseSerializer
|
||||
|
||||
|
||||
class CountrySerializer(serializers.ModelSerializer):
|
||||
|
|
@ -70,7 +70,7 @@ class CityShortSerializer(serializers.ModelSerializer):
|
|||
)
|
||||
|
||||
|
||||
class CitySerializer(serializers.ModelSerializer):
|
||||
class CityBaseSerializer(serializers.ModelSerializer):
|
||||
"""City serializer."""
|
||||
region = RegionSerializer(read_only=True)
|
||||
region_id = serializers.PrimaryKeyRelatedField(
|
||||
|
|
@ -96,6 +96,21 @@ class CitySerializer(serializers.ModelSerializer):
|
|||
'country',
|
||||
'postal_code',
|
||||
'is_island',
|
||||
'image',
|
||||
]
|
||||
extra_fields = {
|
||||
'image': {'write_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 +169,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,26 +83,48 @@ 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',
|
||||
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):
|
||||
"""Site settings serializer."""
|
||||
|
||||
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
|||
68
apps/news/management/commands/news_optimize_images.py
Normal file
68
apps/news/management/commands/news_optimize_images.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# 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"""
|
||||
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()
|
||||
|
|
@ -78,7 +78,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."""
|
||||
|
|
@ -306,8 +306,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):
|
||||
|
|
|
|||
|
|
@ -238,9 +238,9 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
|
|||
|
||||
def create(self, validated_data):
|
||||
slugs = validated_data.get('slugs')
|
||||
if slugs:
|
||||
slugs_list = list(map(lambda x: x.lower(), slugs.values()))
|
||||
slugs_set = set(slugs_list)
|
||||
if slugs:
|
||||
if models.News.objects.filter(
|
||||
slugs__values__contains=list(slugs.values())
|
||||
).exists() or len(slugs_list) != len(slugs_set):
|
||||
|
|
@ -271,6 +271,8 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
|
|||
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)
|
||||
if models.News.objects.filter(
|
||||
slugs__values__contains=list(slugs.values())
|
||||
).exists() or len(slugs_list) != len(slugs_set):
|
||||
|
|
|
|||
|
|
@ -15,27 +15,35 @@ from notification.models import Subscribe
|
|||
@shared_task
|
||||
def send_email_with_news(news_ids):
|
||||
subscribes = Subscribe.objects.all() \
|
||||
.prefetch_related('subscriber') \
|
||||
.prefetch_related('subscription_type')
|
||||
.prefetch_related('subscriber', 'subscription_type') \
|
||||
.active()
|
||||
|
||||
sent_news = models.News.objects.filter(id__in=news_ids)
|
||||
|
||||
htmly = get_template(settings.NEWS_EMAIL_TEMPLATE)
|
||||
html_template = get_template(settings.NEWS_EMAIL_TEMPLATE)
|
||||
year = datetime.now().year
|
||||
|
||||
socials = list(SiteSettings.objects.with_country().select_related('country'))
|
||||
socials = dict(zip(map(lambda social: social.country.code, socials), socials))
|
||||
|
||||
for subscribe in subscribes.filter(unsubscribe_date=None):
|
||||
for subscribe in subscribes:
|
||||
subscriber = subscribe.subscriber
|
||||
|
||||
country = subscribe.subscription_type.country
|
||||
|
||||
if country is None:
|
||||
continue
|
||||
continue # subscription_type has no country
|
||||
|
||||
socials_for_subscriber = socials.get(country.code)
|
||||
subscriber = subscribe.subscriber
|
||||
else:
|
||||
country_code = country.code
|
||||
|
||||
socials_for_subscriber = socials.get(country_code)
|
||||
|
||||
try:
|
||||
for new in sent_news:
|
||||
if new.country.code != country_code:
|
||||
continue
|
||||
|
||||
context = {
|
||||
"title": new.title.get(subscriber.locale),
|
||||
"subtitle": new.subtitle.get(subscriber.locale),
|
||||
|
|
@ -43,7 +51,7 @@ def send_email_with_news(news_ids):
|
|||
"code": subscriber.update_code,
|
||||
"image_url": new.image_url if new.image_url not in EMPTY_VALUES else None,
|
||||
"domain_uri": settings.DOMAIN_URI,
|
||||
"slug": new.slug,
|
||||
"slug": new.slugs.get(subscriber.locale),
|
||||
"country_code": subscriber.country_code,
|
||||
"twitter_page_url": socials_for_subscriber.twitter_page_url if socials_for_subscriber else '#',
|
||||
"instagram_page_url": socials_for_subscriber.instagram_page_url if socials_for_subscriber else '#',
|
||||
|
|
@ -52,9 +60,12 @@ def send_email_with_news(news_ids):
|
|||
"year": year
|
||||
}
|
||||
send_mail(
|
||||
"G&M News", render_to_string(settings.NEWS_EMAIL_TEMPLATE, context),
|
||||
settings.EMAIL_HOST_USER, [subscriber.send_to], fail_silently=False,
|
||||
html_message=htmly.render(context)
|
||||
subject="G&M News",
|
||||
message=render_to_string(settings.NEWS_EMAIL_TEMPLATE, context),
|
||||
from_email=settings.EMAIL_HOST_USER,
|
||||
recipient_list=[subscriber.send_to],
|
||||
fail_silently=False,
|
||||
html_message=html_template.render(context)
|
||||
)
|
||||
except SMTPException:
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -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,20 +37,23 @@ 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', )
|
||||
|
||||
for news_type in migrated_news_types:
|
||||
news_type_obj, _ = NewsType.objects.get_or_create(
|
||||
name=transform_camelcase_to_underscore(news_type))
|
||||
|
||||
queryset = PageTexts.objects.filter(
|
||||
page__type='News',
|
||||
page__type=news_type,
|
||||
).annotate(
|
||||
page__id=F('page__id'),
|
||||
news_type_id=Value(news_type.id, output_field=IntegerField()),
|
||||
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'),
|
||||
|
|
@ -166,5 +170,5 @@ data_types = {
|
|||
update_en_gb_locales,
|
||||
add_views_count,
|
||||
add_tags,
|
||||
]
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from account.models import User
|
|||
from location.models import Country
|
||||
from utils.methods import generate_string_code
|
||||
from utils.models import ProjectBaseMixin, TJSONField, TranslatedFieldsMixin
|
||||
from notification.tasks import send_unsubscribe_email
|
||||
|
||||
|
||||
class SubscriptionType(ProjectBaseMixin, TranslatedFieldsMixin):
|
||||
|
|
@ -116,6 +117,11 @@ class Subscriber(ProjectBaseMixin):
|
|||
"""Unsubscribe user."""
|
||||
self.subscribe_set.update(unsubscribe_date=now())
|
||||
|
||||
if settings.USE_CELERY:
|
||||
send_unsubscribe_email.delay(self.pk)
|
||||
else:
|
||||
send_unsubscribe_email(self.pk)
|
||||
|
||||
@property
|
||||
def send_to(self):
|
||||
"""Actual email."""
|
||||
|
|
@ -135,6 +141,13 @@ class Subscriber(ProjectBaseMixin):
|
|||
return self.subscription_types.exclude(subscriber__subscribe__unsubscribe_date__isnull=False)
|
||||
|
||||
|
||||
class SubscribeQuerySet(models.QuerySet):
|
||||
|
||||
def active(self, switcher=True):
|
||||
"""Fetches active subscriptions."""
|
||||
return self.exclude(unsubscribe_date__isnull=not switcher)
|
||||
|
||||
|
||||
class Subscribe(ProjectBaseMixin):
|
||||
"""Subscribe model."""
|
||||
|
||||
|
|
@ -144,6 +157,8 @@ class Subscribe(ProjectBaseMixin):
|
|||
subscriber = models.ForeignKey(Subscriber, on_delete=models.CASCADE)
|
||||
subscription_type = models.ForeignKey(SubscriptionType, on_delete=models.CASCADE)
|
||||
|
||||
objects = SubscribeQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
"""Meta class."""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
"""Notification app serializers."""
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from location.serializers import CountrySimpleSerializer
|
||||
from notification import models
|
||||
from notification.tasks import send_subscribes_update_email
|
||||
from utils.methods import get_user_ip
|
||||
from utils.serializers import TranslatedField
|
||||
|
||||
|
|
@ -28,10 +29,11 @@ class SubscriptionTypeSerializer(serializers.ModelSerializer):
|
|||
|
||||
|
||||
class CreateSubscribeSerializer(serializers.ModelSerializer):
|
||||
"""Create Subscribe serializer."""
|
||||
"""Create and Update Subscribe serializer."""
|
||||
|
||||
email = serializers.EmailField(required=False, source='send_to')
|
||||
subscription_types = serializers.PrimaryKeyRelatedField(many=True, queryset=models.SubscriptionType.objects.all())
|
||||
country_code = serializers.CharField(required=False, allow_blank=True)
|
||||
|
||||
class Meta:
|
||||
"""Meta class."""
|
||||
|
|
@ -41,7 +43,10 @@ class CreateSubscribeSerializer(serializers.ModelSerializer):
|
|||
'email',
|
||||
'subscription_types',
|
||||
'link_to_unsubscribe',
|
||||
'country_code',
|
||||
'update_code'
|
||||
)
|
||||
read_only_fields = ('link_to_unsubscribe', 'update_code')
|
||||
|
||||
def validate(self, attrs):
|
||||
"""Validate attrs."""
|
||||
|
|
@ -63,7 +68,13 @@ class CreateSubscribeSerializer(serializers.ModelSerializer):
|
|||
|
||||
# append info
|
||||
attrs['email'] = email
|
||||
|
||||
if request.country_code:
|
||||
attrs['country_code'] = request.country_code
|
||||
|
||||
else:
|
||||
attrs['country_code'] = attrs.get('country_code')
|
||||
|
||||
attrs['locale'] = request.locale
|
||||
attrs['ip_address'] = get_user_ip(request)
|
||||
|
||||
|
|
@ -74,7 +85,67 @@ class CreateSubscribeSerializer(serializers.ModelSerializer):
|
|||
|
||||
def create(self, validated_data):
|
||||
"""Create obj."""
|
||||
return models.Subscriber.objects.make_subscriber(**validated_data)
|
||||
|
||||
subscriber = models.Subscriber.objects.make_subscriber(**validated_data)
|
||||
|
||||
if settings.USE_CELERY:
|
||||
send_subscribes_update_email.delay(subscriber.pk)
|
||||
else:
|
||||
send_subscribes_update_email(subscriber.pk)
|
||||
|
||||
return subscriber
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
if settings.USE_CELERY:
|
||||
send_subscribes_update_email.delay(instance.pk)
|
||||
else:
|
||||
send_subscribes_update_email(instance.pk)
|
||||
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class UpdateSubscribeSerializer(serializers.ModelSerializer):
|
||||
"""Update with code Subscribe serializer."""
|
||||
|
||||
subscription_types = serializers.PrimaryKeyRelatedField(many=True, queryset=models.SubscriptionType.objects.all())
|
||||
|
||||
class Meta:
|
||||
"""Meta class."""
|
||||
|
||||
model = models.Subscriber
|
||||
fields = (
|
||||
'subscription_types',
|
||||
'link_to_unsubscribe',
|
||||
'update_code'
|
||||
)
|
||||
read_only_fields = ('link_to_unsubscribe', 'update_code')
|
||||
|
||||
def validate(self, attrs):
|
||||
"""Validate attrs."""
|
||||
request = self.context.get('request')
|
||||
user = request.user
|
||||
|
||||
if request.country_code:
|
||||
attrs['country_code'] = request.country_code
|
||||
|
||||
else:
|
||||
attrs['country_code'] = attrs.get('country_code')
|
||||
|
||||
attrs['locale'] = request.locale
|
||||
attrs['ip_address'] = get_user_ip(request)
|
||||
|
||||
if user.is_authenticated:
|
||||
attrs['user'] = user
|
||||
|
||||
return attrs
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
if settings.USE_CELERY:
|
||||
send_subscribes_update_email.delay(instance.pk)
|
||||
else:
|
||||
send_subscribes_update_email(instance.pk)
|
||||
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class SubscribeObjectSerializer(serializers.ModelSerializer):
|
||||
|
|
|
|||
84
apps/notification/tasks.py
Normal file
84
apps/notification/tasks.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
from datetime import datetime
|
||||
|
||||
from celery import shared_task
|
||||
from django.conf import settings
|
||||
from django.core.mail import send_mail
|
||||
from django.template.loader import get_template, render_to_string
|
||||
|
||||
from main.models import SiteSettings
|
||||
from notification import models
|
||||
|
||||
|
||||
@shared_task
|
||||
def send_subscribes_update_email(subscriber_id):
|
||||
subscriber = models.Subscriber.objects.get(id=subscriber_id)
|
||||
|
||||
country_code = subscriber.country_code
|
||||
|
||||
html_template = get_template(settings.NOTIFICATION_SUBSCRIBE_TEMPLATE)
|
||||
year = datetime.now().year
|
||||
|
||||
socials = list(SiteSettings.objects.with_country().select_related('country'))
|
||||
socials = dict(zip(map(lambda social: social.country.code, socials), socials))
|
||||
|
||||
socials_for_subscriber = socials.get(country_code)
|
||||
|
||||
context = {
|
||||
"title": "You have subscribed on news G&M",
|
||||
"description": "<br>".join([
|
||||
name.get(subscriber.locale)
|
||||
for name in subscriber.subscription_types.values_list('name', flat=True)
|
||||
]),
|
||||
"code": subscriber.update_code,
|
||||
"link_to_unsubscribe": subscriber.link_to_unsubscribe,
|
||||
"twitter_page_url": socials_for_subscriber.twitter_page_url if socials_for_subscriber else '#',
|
||||
"instagram_page_url": socials_for_subscriber.instagram_page_url if socials_for_subscriber else '#',
|
||||
"facebook_page_url": socials_for_subscriber.facebook_page_url if socials_for_subscriber else '#',
|
||||
"send_to": subscriber.send_to,
|
||||
"year": year
|
||||
}
|
||||
|
||||
send_mail(
|
||||
subject="G&M Subscriptions",
|
||||
message=render_to_string(settings.NOTIFICATION_SUBSCRIBE_TEMPLATE, context),
|
||||
from_email=settings.EMAIL_HOST_USER,
|
||||
recipient_list=[subscriber.send_to],
|
||||
fail_silently=False,
|
||||
html_message=html_template.render(context)
|
||||
)
|
||||
|
||||
|
||||
@shared_task
|
||||
def send_unsubscribe_email(subscriber_id):
|
||||
subscriber = models.Subscriber.objects.get(id=subscriber_id)
|
||||
|
||||
country_code = subscriber.country_code
|
||||
|
||||
html_template = get_template(settings.NOTIFICATION_SUBSCRIBE_TEMPLATE)
|
||||
year = datetime.now().year
|
||||
|
||||
socials = list(SiteSettings.objects.with_country().select_related('country'))
|
||||
socials = dict(zip(map(lambda social: social.country.code, socials), socials))
|
||||
|
||||
socials_for_subscriber = socials.get(country_code)
|
||||
|
||||
context = {
|
||||
"title": "You have successfully unsubscribed from G&M news",
|
||||
"description": "",
|
||||
"code": subscriber.update_code,
|
||||
"link_to_unsubscribe": subscriber.link_to_unsubscribe,
|
||||
"twitter_page_url": socials_for_subscriber.twitter_page_url if socials_for_subscriber else '#',
|
||||
"instagram_page_url": socials_for_subscriber.instagram_page_url if socials_for_subscriber else '#',
|
||||
"facebook_page_url": socials_for_subscriber.facebook_page_url if socials_for_subscriber else '#',
|
||||
"send_to": subscriber.send_to,
|
||||
"year": year
|
||||
}
|
||||
|
||||
send_mail(
|
||||
subject="G&M Subscriptions",
|
||||
message=render_to_string(settings.NOTIFICATION_SUBSCRIBE_TEMPLATE, context),
|
||||
from_email=settings.EMAIL_HOST_USER,
|
||||
recipient_list=[subscriber.send_to],
|
||||
fail_silently=False,
|
||||
html_message=html_template.render(context)
|
||||
)
|
||||
|
|
@ -6,6 +6,7 @@ app_name = "notification"
|
|||
|
||||
urlpatterns = [
|
||||
path('subscribe/', common.CreateSubscribeView.as_view(), name='create-subscribe'),
|
||||
path('subscribe/<code>/', common.UpdateSubscribeView.as_view(), name='update-subscribe'),
|
||||
path('subscribe-info/', common.SubscribeInfoAuthUserView.as_view(), name='check-code-auth'),
|
||||
path('subscribe-info/<code>/', common.SubscribeInfoView.as_view(), name='check-code'),
|
||||
path('unsubscribe/', common.UnsubscribeAuthUserView.as_view(), name='unsubscribe-auth'),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from rest_framework.response import Response
|
|||
|
||||
from notification import models
|
||||
from notification.serializers import common as serializers
|
||||
from utils.methods import get_user_ip
|
||||
|
||||
|
||||
class CreateSubscribeView(generics.CreateAPIView):
|
||||
|
|
@ -15,6 +16,16 @@ class CreateSubscribeView(generics.CreateAPIView):
|
|||
serializer_class = serializers.CreateSubscribeSerializer
|
||||
|
||||
|
||||
class UpdateSubscribeView(generics.UpdateAPIView):
|
||||
"""Subscribe info view."""
|
||||
|
||||
lookup_field = 'update_code'
|
||||
lookup_url_kwarg = 'code'
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
queryset = models.Subscriber.objects.all()
|
||||
serializer_class = serializers.UpdateSubscribeSerializer
|
||||
|
||||
|
||||
class SubscribeInfoView(generics.RetrieveAPIView):
|
||||
"""Subscribe info view."""
|
||||
|
||||
|
|
@ -33,7 +44,22 @@ class SubscribeInfoAuthUserView(generics.RetrieveAPIView):
|
|||
lookup_field = None
|
||||
|
||||
def get_object(self):
|
||||
return get_object_or_404(models.Subscriber, user=self.request.user)
|
||||
user = self.request.user
|
||||
|
||||
if user.is_authenticated:
|
||||
try:
|
||||
subscriber = models.Subscriber.objects.get(user=user)
|
||||
|
||||
except models.Subscriber.DoesNotExist:
|
||||
subscriber = models.Subscriber.objects.make_subscriber(
|
||||
email=user.email, user=user, ip_address=get_user_ip(self.request),
|
||||
country_code=self.request.country_code, locale=self.request.locale
|
||||
)
|
||||
|
||||
else:
|
||||
return get_object_or_404(models.Subscriber, user=user)
|
||||
|
||||
return subscriber
|
||||
|
||||
|
||||
class UnsubscribeView(generics.UpdateAPIView):
|
||||
|
|
|
|||
|
|
@ -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,6 +77,14 @@ class EstablishmentDocument(Document):
|
|||
'value': fields.KeywordField(),
|
||||
},
|
||||
multi=True, attr='artisan_category_indexing')
|
||||
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, attr='distillery_type_indexing')
|
||||
visible_tags = fields.ObjectField(
|
||||
properties={
|
||||
'id': fields.IntegerField(attr='id'),
|
||||
|
|
@ -145,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,6 +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_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)
|
||||
|
|
@ -310,6 +311,16 @@ class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer):
|
|||
# 'collections',
|
||||
'type',
|
||||
'subtypes',
|
||||
'distillery_type',
|
||||
)
|
||||
|
||||
|
||||
class EstablishmentBackOfficeDocumentSerializer(EstablishmentDocumentSerializer):
|
||||
|
||||
class Meta(EstablishmentDocumentSerializer.Meta):
|
||||
document = EstablishmentDocumentSerializer.Meta.document
|
||||
fields = EstablishmentDocumentSerializer.Meta.fields + (
|
||||
'created',
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"""Search indexes app views."""
|
||||
from django_elasticsearch_dsl_drf import constants
|
||||
from django_elasticsearch_dsl_drf import constants, pagination
|
||||
from django_elasticsearch_dsl_drf.filter_backends import (
|
||||
FilteringFilterBackend,
|
||||
GeoSpatialOrderingFilterBackend,
|
||||
|
|
@ -321,14 +321,26 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
|
|||
|
||||
class EstablishmentBackOfficeViewSet(EstablishmentDocumentViewSet):
|
||||
|
||||
filter_backends = EstablishmentDocumentViewSet.filter_backends + [
|
||||
OrderingFilterBackend
|
||||
]
|
||||
def get_queryset(self):
|
||||
if not self.request.query_params.get('search'):
|
||||
self.request.GET._mutable = True
|
||||
self.request.query_params.update({
|
||||
'ordering': '-created',
|
||||
})
|
||||
self.request.GET._mutable = False
|
||||
return super(EstablishmentBackOfficeViewSet, self).get_queryset()
|
||||
|
||||
serializer_class = serializers.EstablishmentBackOfficeDocumentSerializer
|
||||
pagination_class = pagination.PageNumberPagination
|
||||
filter_backends = [
|
||||
FilteringFilterBackend,
|
||||
filters.CustomSearchFilterBackend,
|
||||
filters.CustomGeoSpatialFilteringFilterBackend,
|
||||
GeoSpatialOrderingFilterBackend,
|
||||
OrderingFilterBackend,
|
||||
]
|
||||
ordering_fields = {
|
||||
'created': {
|
||||
'field': 'created'
|
||||
}
|
||||
'created': 'created',
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ class TagsBaseFilterSet(filters.FilterSet):
|
|||
type = filters.MultipleChoiceFilter(choices=TYPE_CHOICES,
|
||||
method='filter_by_type')
|
||||
|
||||
|
||||
def filter_by_type(self, queryset, name, value):
|
||||
if self.NEWS in value:
|
||||
queryset = queryset.for_news()
|
||||
|
|
|
|||
19
apps/tag/migrations/0018_auto_20200113_1357.py
Normal file
19
apps/tag/migrations/0018_auto_20200113_1357.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 2.2.7 on 2020-01-13 13:57
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tag', '0017_auto_20191220_1623'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='tagcategory',
|
||||
name='country',
|
||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='location.Country'),
|
||||
),
|
||||
]
|
||||
26
apps/tag/migrations/0018_chosentag.py
Normal file
26
apps/tag/migrations/0018_chosentag.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Generated by Django 2.2.7 on 2020-01-13 08:36
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0045_carousel_is_international'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('tag', '0017_auto_20191220_1623'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ChosenTag',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('object_id', models.PositiveIntegerField()),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
|
||||
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.SiteSettings', verbose_name='site')),
|
||||
('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chosen_tags', to='tag.Tag', verbose_name='tag')),
|
||||
],
|
||||
),
|
||||
]
|
||||
14
apps/tag/migrations/0019_merge_20200114_0756.py
Normal file
14
apps/tag/migrations/0019_merge_20200114_0756.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Generated by Django 2.2.7 on 2020-01-14 07:56
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tag', '0018_auto_20200113_1357'),
|
||||
('tag', '0018_chosentag'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
"""Tag app models."""
|
||||
from django.contrib.contenttypes import fields as generic
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
|
@ -145,8 +146,8 @@ class TagCategory(models.Model):
|
|||
(BOOLEAN, _('boolean')),
|
||||
)
|
||||
country = models.ForeignKey('location.Country',
|
||||
on_delete=models.SET_NULL, null=True,
|
||||
default=None)
|
||||
on_delete=models.SET_NULL,
|
||||
blank=True, null=True, default=None)
|
||||
public = models.BooleanField(default=False)
|
||||
index_name = models.CharField(max_length=255, blank=True, null=True,
|
||||
verbose_name=_('indexing name'), unique=True)
|
||||
|
|
@ -154,7 +155,8 @@ class TagCategory(models.Model):
|
|||
value_type = models.CharField(_('value type'), max_length=255,
|
||||
choices=VALUE_TYPE_CHOICES, default=LIST, )
|
||||
old_id = models.IntegerField(blank=True, null=True)
|
||||
translation = models.OneToOneField('translation.SiteInterfaceDictionary', on_delete=models.SET_NULL,
|
||||
translation = models.OneToOneField(
|
||||
'translation.SiteInterfaceDictionary', on_delete=models.SET_NULL,
|
||||
null=True, related_name='tag_category', verbose_name=_('Translation'))
|
||||
|
||||
@property
|
||||
|
|
@ -175,3 +177,20 @@ class TagCategory(models.Model):
|
|||
|
||||
def __str__(self):
|
||||
return self.index_name
|
||||
|
||||
|
||||
class ChosenTag(models.Model):
|
||||
"""Chosen tag for type."""
|
||||
tag = models.ForeignKey(
|
||||
'Tag', verbose_name=_('tag'), related_name='chosen_tags',
|
||||
on_delete=models.CASCADE)
|
||||
site = models.ForeignKey(
|
||||
'main.SiteSettings', verbose_name=_('site'), on_delete=models.CASCADE)
|
||||
|
||||
content_type = models.ForeignKey(
|
||||
generic.ContentType, on_delete=models.CASCADE)
|
||||
object_id = models.PositiveIntegerField()
|
||||
content_object = generic.GenericForeignKey('content_type', 'object_id')
|
||||
|
||||
def __str__(self):
|
||||
return f'chosen_tag:{self.tag}'
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
@ -56,7 +57,8 @@ class TagBackOfficeSerializer(TagBaseSerializer):
|
|||
|
||||
fields = TagBaseSerializer.Meta.fields + (
|
||||
'label',
|
||||
'category'
|
||||
'category',
|
||||
'value',
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -191,7 +193,7 @@ class TagCategoryBackOfficeDetailSerializer(TagCategoryBaseSerializer):
|
|||
|
||||
|
||||
class TagBindObjectSerializer(serializers.Serializer):
|
||||
"""Serializer for binding tag category and objects"""
|
||||
"""Serializer for binding tag category and objects."""
|
||||
|
||||
ESTABLISHMENT = 'establishment'
|
||||
NEWS = 'news'
|
||||
|
|
@ -216,15 +218,20 @@ class TagBindObjectSerializer(serializers.Serializer):
|
|||
|
||||
if obj_type == self.ESTABLISHMENT:
|
||||
establishment = Establishment.objects.filter(pk=obj_id).first()
|
||||
|
||||
if not establishment:
|
||||
raise BindingObjectNotFound()
|
||||
|
||||
if request.method == 'POST' and tag.establishments.filter(
|
||||
pk=establishment.pk).exists():
|
||||
raise ObjectAlreadyAdded()
|
||||
|
||||
if request.method == 'DELETE' and not tag.establishments.filter(
|
||||
pk=establishment.pk).exists():
|
||||
raise RemovedBindingObjectNotFound()
|
||||
|
||||
attrs['related_object'] = establishment
|
||||
|
||||
elif obj_type == self.NEWS:
|
||||
news = News.objects.filter(pk=obj_id).first()
|
||||
if not news:
|
||||
|
|
@ -287,3 +294,41 @@ class TagCategoryBindObjectSerializer(serializers.Serializer):
|
|||
raise RemovedBindingObjectNotFound()
|
||||
attrs['related_object'] = news_type
|
||||
return attrs
|
||||
|
||||
|
||||
class ChosenTagSerializer(serializers.ModelSerializer):
|
||||
tag = TagBackOfficeSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.ChosenTag
|
||||
fields = [
|
||||
'id',
|
||||
'tag',
|
||||
]
|
||||
|
||||
|
||||
class ChosenTagBindObjectSerializer(serializers.Serializer):
|
||||
"""Serializer for binding chosen tag and objects"""
|
||||
|
||||
feature_id = serializers.IntegerField()
|
||||
|
||||
def validate(self, attrs):
|
||||
view = self.context.get('view')
|
||||
request = self.context.get('request')
|
||||
|
||||
obj_id = attrs.get('feature_id')
|
||||
|
||||
tag = view.get_object()
|
||||
attrs['tag'] = tag
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
"""Tag views."""
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import generics, mixins, permissions, status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from search_indexes import views as search_views
|
||||
|
||||
from location.models import WineRegion
|
||||
from product.models import ProductType
|
||||
from search_indexes import views as search_views
|
||||
from tag import filters, models, serializers
|
||||
|
||||
|
||||
|
|
@ -89,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 = []
|
||||
|
|
@ -154,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)
|
||||
|
|
@ -169,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)
|
||||
|
|
@ -192,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)
|
||||
|
|
@ -292,6 +294,8 @@ class BindObjectMixin:
|
|||
def get_serializer_class(self):
|
||||
if self.action == 'bind_object':
|
||||
return self.bind_object_serializer_class
|
||||
elif self.action == 'chosen':
|
||||
return self.chosen_serializer_class
|
||||
return self.serializer_class
|
||||
|
||||
def perform_binding(self, serializer):
|
||||
|
|
@ -311,6 +315,17 @@ class BindObjectMixin:
|
|||
self.perform_unbinding(serializer)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@action(methods=['post', 'delete'], detail=True, url_path='chosen')
|
||||
def chosen(self, request, pk=None):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
if request.method == 'POST':
|
||||
self.perform_binding(serializer)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
elif request.method == 'DELETE':
|
||||
self.perform_unbinding(serializer)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class TagBackOfficeViewSet(mixins.ListModelMixin, mixins.CreateModelMixin,
|
||||
mixins.UpdateModelMixin, mixins.DestroyModelMixin,
|
||||
|
|
@ -322,12 +337,27 @@ class TagBackOfficeViewSet(mixins.ListModelMixin, mixins.CreateModelMixin,
|
|||
queryset = models.Tag.objects.all()
|
||||
serializer_class = serializers.TagBackOfficeSerializer
|
||||
bind_object_serializer_class = serializers.TagBindObjectSerializer
|
||||
chosen_serializer_class = serializers.ChosenTagBindObjectSerializer
|
||||
|
||||
def perform_binding(self, serializer):
|
||||
data = serializer.validated_data
|
||||
tag = data.pop('tag')
|
||||
obj_type = data.get('type')
|
||||
related_object = data.get('related_object')
|
||||
|
||||
# for compatible exist code
|
||||
if self.action == 'chosen':
|
||||
obj_type = ContentType.objects.get_for_model(models.ChosenTag)
|
||||
models.ChosenTag.objects.update_or_create(
|
||||
tag=tag,
|
||||
content_type=obj_type,
|
||||
object_id=related_object.id,
|
||||
defaults={
|
||||
"content_object": related_object,
|
||||
"site": self.request.user.last_country
|
||||
},
|
||||
)
|
||||
|
||||
if obj_type == self.bind_object_serializer_class.ESTABLISHMENT:
|
||||
tag.establishments.add(related_object)
|
||||
elif obj_type == self.bind_object_serializer_class.NEWS:
|
||||
|
|
@ -338,6 +368,11 @@ class TagBackOfficeViewSet(mixins.ListModelMixin, mixins.CreateModelMixin,
|
|||
tag = data.pop('tag')
|
||||
obj_type = data.get('type')
|
||||
related_object = data.get('related_object')
|
||||
|
||||
# for compatible exist code
|
||||
if self.action == 'chosen':
|
||||
related_object.chosen_tags.filter(tag=tag).delete()
|
||||
|
||||
if obj_type == self.bind_object_serializer_class.ESTABLISHMENT:
|
||||
tag.establishments.remove(related_object)
|
||||
elif obj_type == self.bind_object_serializer_class.NEWS:
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'[^\"\'=\s]+\.jpe?g|png|gif|svg', 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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -28,3 +28,6 @@
|
|||
./manage.py transfer --inquiries
|
||||
./manage.py transfer --product_review
|
||||
./manage.py transfer --transfer_text_review
|
||||
|
||||
# оптимизация изображений
|
||||
/manage.py news_optimize_images # сжимает картинки в описаниях новостей
|
||||
|
|
@ -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}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -454,6 +457,7 @@ CHANGE_EMAIL_TEMPLATE = 'account/change_email.html'
|
|||
CONFIRM_EMAIL_TEMPLATE = 'authorization/confirm_email.html'
|
||||
NEWS_EMAIL_TEMPLATE = 'news/news_email.html'
|
||||
NOTIFICATION_PASSWORD_TEMPLATE = 'account/password_change_email.html'
|
||||
NOTIFICATION_SUBSCRIBE_TEMPLATE = 'notification/update_email.html'
|
||||
|
||||
|
||||
# COOKIES
|
||||
|
|
@ -532,3 +536,5 @@ COOKIE_DOMAIN = None
|
|||
|
||||
ELASTICSEARCH_DSL = {}
|
||||
ELASTICSEARCH_INDEX_NAMES = {}
|
||||
|
||||
THUMBNAIL_FORCE_OVERWRITE = True
|
||||
|
|
|
|||
|
|
@ -74,3 +74,10 @@ CELERY_RESULT_BACKEND = BROKER_URL
|
|||
CELERY_BROKER_URL = BROKER_URL
|
||||
|
||||
COOKIE_DOMAIN = '.id-east.ru'
|
||||
|
||||
# Email settings
|
||||
EMAIL_USE_TLS = True
|
||||
EMAIL_HOST = 'smtp.gmail.com'
|
||||
EMAIL_HOST_USER = 'anatolyfeteleu@gmail.com'
|
||||
EMAIL_HOST_PASSWORD = 'nggrlnbehzksgmbt'
|
||||
EMAIL_PORT = 587
|
||||
|
|
@ -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 %}
|
||||
|
|
|
|||
75
project/templates/notification/update_email.html
Normal file
75
project/templates/notification/update_email.html
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" style="box-sizing: border-box;margin: 0;">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:400,400i,700|PT+Serif&display=swap&subset=cyrillic" type="text/css">
|
||||
<title>{{ title }}</title>
|
||||
</head>
|
||||
<body style="box-sizing: border-box;margin: 0;font-family: "Open Sans", sans-serif;font-size: 0.875rem;">
|
||||
<div style="dispaly: none">
|
||||
|
||||
</div>
|
||||
|
||||
<div style="margin: 0 auto; max-width:38.25rem;" class="letter">
|
||||
<div class="letter__wrapper">
|
||||
<div class="letter__inner">
|
||||
<div class="letter__content" style="position: relative;margin: 0 16px 40px;padding: 0 0 1px;">
|
||||
<div class="letter__header" style="margin: 1.875rem 0 2.875rem;text-align: center;">
|
||||
<div class="letter__logo" style="display: block;width: 7.9375rem;height: 4.6875rem;margin: 0 auto 14px auto;">
|
||||
<a href="https://{{ country_code }}.{{ domain_uri }}/" class="letter__logo-photo" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: block;border: none;">
|
||||
<img alt="" style="width:100%;" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/1.png" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="letter__sublogo" style="font-size: 21px;line-height: 1;letter-spacing: 0;color: #bcbcbc;text-transform: uppercase;">france</div>
|
||||
</div>
|
||||
<div class="letter__title" style="font-family:"Open-Sans",sans-serif; font-size: 1.5rem;margin: 0 0 10px;padding: 0 0 6px;border-bottom: 4px solid #ffee29;">
|
||||
<span class="letter__title-txt">{{ title }}</span>
|
||||
</div>
|
||||
{% if not image_url is None %}
|
||||
<div class="letter__image" style="width: 100%;margin: 0 0 1.25rem;max-height:260px;">
|
||||
<img src="{{ image_url }}" alt="" class="letter__photo" style="display: block; width: 100%;height: 100%;">
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="letter__text" style="margin: 0 0 30px; font-family:"Open-Sans",sans-serif; font-size: 14px; line-height: 21px;letter-spacing: -0.34px; overflow-x: hidden;">
|
||||
{{ description | safe }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="letter__follow" style="padding: 8px;margin: 0 auto 40px auto;background: #ffee29; max-width: 400px;">
|
||||
<div class="letter__follow-content" style="padding: 1.25rem 0;background: #fff;text-align: center;">
|
||||
<div class="letter__follow-header" style="display: inline-block;margin: 0 0 18px;font-family: "PT Serif", sans-serif;font-size: 1.25rem;text-transform: uppercase;">
|
||||
<img alt="thumb" style="width:30px;vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/2.png" />
|
||||
<span class="letter__follow-title">Follow us</span>
|
||||
</div>
|
||||
<div class="letter__follow-text" style="display: block;margin: 0 0 30px;font-size: 12px;font-style: italic;">You can also us on our social network below
|
||||
</div>
|
||||
<div class="letter__follow-social">
|
||||
<a href="{{ facebook_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0 1rem 0 0;background: #ffee29;border: none;">
|
||||
<img alt="facebook" style="width: 30px; vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/3.png" />
|
||||
</a>
|
||||
<a href="{{ instagram_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0 1rem 0 0;background: #ffee29;border: none;">
|
||||
<img alt="instagram" style="width:30px;vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/4.png" />
|
||||
</a>
|
||||
<a href="{{ twitter_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0;background: #ffee29;border: none;">
|
||||
<img alt="twitter" style="width:30px;vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/5.png" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="letter__unsubscribe" style="margin: 0 0 1.25rem;font-size: 12px;text-align: center;">
|
||||
<span class="letter__unsubscribe-dscr" style="display: inline-block;">This email has been sent to {{ send_to }} ,</span>
|
||||
<a href="{{ link_to_unsubscribe }}" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;">click here to unsubscribe</a>
|
||||
</div>
|
||||
<div class="letter__footer" style="padding: 24px 0 15px;text-align: center;background: #ffee29;">
|
||||
<div class="letter__footer-logo" style="width: 71px;height: 42px;margin: 0 auto 14px auto;">
|
||||
<a href="#" class="letter__footer-logo-photo" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;">
|
||||
<img alt="" style="width: 100%;" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/6.png" /></a>
|
||||
</div>
|
||||
<div class="letter__copyright">GaultMillau © {{ year }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -49,7 +49,6 @@ django-storages==1.7.2
|
|||
|
||||
sorl-thumbnail==12.5.0
|
||||
|
||||
|
||||
PyYAML==5.1.2
|
||||
|
||||
# temp solution
|
||||
|
|
|
|||
|
|
@ -2,3 +2,7 @@
|
|||
ipdb
|
||||
ipython
|
||||
mysqlclient==1.4.4
|
||||
|
||||
pyparsing
|
||||
graphviz
|
||||
pydot
|
||||
Loading…
Reference in New Issue
Block a user