Merge branch 'develop' of ssh://gl.id-east.ru:222/gm/gm-backend into develop
This commit is contained in:
commit
b859792b8c
18
apps/account/migrations/0035_userrole_for_team.py
Normal file
18
apps/account/migrations/0035_userrole_for_team.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.7 on 2020-02-04 15:19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('account', '0034_auto_20200131_0548'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='userrole',
|
||||||
|
name='for_team',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='is this role for team membership'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -103,16 +103,46 @@ class UserManager(BaseUserManager):
|
||||||
|
|
||||||
use_in_migrations = False
|
use_in_migrations = False
|
||||||
|
|
||||||
def make(self, email: str, password: str, newsletter: bool, username: str = '') -> object:
|
def make(self, email: str, password: str, newsletter: bool, username: str = '', email_confirmed=False) -> object:
|
||||||
"""Register new user"""
|
"""Register new user"""
|
||||||
obj = self.model(
|
obj = self.model(
|
||||||
username=username,
|
username=username,
|
||||||
email=email.lower(),
|
email=email.lower(),
|
||||||
newsletter=newsletter)
|
newsletter=newsletter,
|
||||||
|
email_confirmed=email_confirmed)
|
||||||
obj.set_password(password)
|
obj.set_password(password)
|
||||||
obj.save()
|
obj.save()
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
def invite_for_team(self, email: str, establishment: Establishment, country_code: str):
|
||||||
|
created = False
|
||||||
|
user = User.objects.filter(email=email).first()
|
||||||
|
if user is None:
|
||||||
|
from utils.methods import string_random
|
||||||
|
user = self.make(email, string_random(), True, string_random(), email_confirmed=True)
|
||||||
|
created = True
|
||||||
|
user_role_defaults = {'for_team': True, 'state': UserRole.PENDING}
|
||||||
|
role, is_role_created = Role.objects.get_or_create(role=Role.ESTABLISHMENT_ADMINISTRATOR)
|
||||||
|
user_role, is_user_role_created = UserRole.objects.get_or_create(user=user, establishment=establishment,
|
||||||
|
role=role, defaults=user_role_defaults)
|
||||||
|
if not is_user_role_created:
|
||||||
|
from rest_framework.serializers import ValidationError
|
||||||
|
raise ValidationError({'detail': f'User with this role already exists. State: {user_role.state}'})
|
||||||
|
|
||||||
|
if created:
|
||||||
|
from account.tasks import send_team_invite_to_new_user
|
||||||
|
if settings.USE_CELERY:
|
||||||
|
send_team_invite_to_new_user.delay(user.pk, country_code, user_role.pk, establishment.name)
|
||||||
|
else:
|
||||||
|
send_team_invite_to_new_user(user.pk, country_code, user_role.pk, establishment.name)
|
||||||
|
else:
|
||||||
|
from account.tasks import send_team_invite_to_existing_user
|
||||||
|
if settings.USE_CELERY:
|
||||||
|
send_team_invite_to_existing_user.delay(user.pk, country_code, user_role.pk, establishment.name)
|
||||||
|
else:
|
||||||
|
send_team_invite_to_existing_user(user.pk, country_code, user_role.pk, establishment.name)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
class UserQuerySet(models.QuerySet):
|
class UserQuerySet(models.QuerySet):
|
||||||
"""Extended queryset for User model."""
|
"""Extended queryset for User model."""
|
||||||
|
|
@ -358,6 +388,7 @@ class User(PhoneModelMixin, AbstractUser):
|
||||||
'twitter_page_url': socials.twitter_page_url if socials else '#',
|
'twitter_page_url': socials.twitter_page_url if socials else '#',
|
||||||
'instagram_page_url': socials.instagram_page_url if socials else '#',
|
'instagram_page_url': socials.instagram_page_url if socials else '#',
|
||||||
'facebook_page_url': socials.facebook_page_url if socials else '#',
|
'facebook_page_url': socials.facebook_page_url if socials else '#',
|
||||||
|
'contact_email': socials.contact_email if socials else '-',
|
||||||
'send_to': username,
|
'send_to': username,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -378,9 +409,41 @@ class User(PhoneModelMixin, AbstractUser):
|
||||||
template_name=settings.RESETTING_TOKEN_TEMPLATE,
|
template_name=settings.RESETTING_TOKEN_TEMPLATE,
|
||||||
context=context), get_template(settings.RESETTING_TOKEN_TEMPLATE).render(context)
|
context=context), get_template(settings.RESETTING_TOKEN_TEMPLATE).render(context)
|
||||||
|
|
||||||
|
def invite_new_establishment_member_template(self, country_code, username, subject, restaurant_name, user_role_id):
|
||||||
|
"""Template for newly created user establishment team invite"""
|
||||||
|
context = {'token': self.reset_password_token,
|
||||||
|
'country_code': country_code,
|
||||||
|
'restaurant_name': restaurant_name,
|
||||||
|
'user_role_id': user_role_id}
|
||||||
|
context.update(self.base_template(country_code, username, subject))
|
||||||
|
return render_to_string(
|
||||||
|
template_name=settings.NEW_USER_FOR_ESTABLISHMENT_TEAM_TEMPLATE,
|
||||||
|
context=context), get_template(settings.NEW_USER_FOR_ESTABLISHMENT_TEAM_TEMPLATE).render(context)
|
||||||
|
|
||||||
|
def invite_establishment_member_template(self, country_code, username, subject, restaurant_name, user_role_id):
|
||||||
|
"""Template existing user establishment team invite"""
|
||||||
|
context = {'token': self.reset_password_token,
|
||||||
|
'country_code': country_code,
|
||||||
|
'restaurant_name': restaurant_name,
|
||||||
|
'user_role_id': user_role_id}
|
||||||
|
context.update(self.base_template(country_code, username, subject))
|
||||||
|
return render_to_string(
|
||||||
|
template_name=settings.EXISTING_USER_FOR_ESTABLISHMENT_TEAM_TEMPLATE,
|
||||||
|
context=context), get_template(settings.EXISTING_USER_FOR_ESTABLISHMENT_TEAM_TEMPLATE).render(context)
|
||||||
|
|
||||||
|
def establishment_team_role_revoked(self, country_code, username, subject, restaurant_name):
|
||||||
|
"""Template to notify user that his/her establishment role is revoked"""
|
||||||
|
context = {'token': self.reset_password_token,
|
||||||
|
'country_code': country_code,
|
||||||
|
'restaurant_name': restaurant_name}
|
||||||
|
context.update(self.base_template(country_code, username, subject))
|
||||||
|
return render_to_string(
|
||||||
|
template_name=settings.ESTABLISHMENT_TEAM_ROLE_REVOKED_TEMPLATE,
|
||||||
|
context=context), get_template(settings.ESTABLISHMENT_TEAM_ROLE_REVOKED_TEMPLATE).render(context)
|
||||||
|
|
||||||
def notify_password_changed_template(self, country_code, username, subject):
|
def notify_password_changed_template(self, country_code, username, subject):
|
||||||
"""Get notification email template"""
|
"""Get notification email template"""
|
||||||
context = {'contry_code': country_code}
|
context = {'country_code': country_code}
|
||||||
context.update(self.base_template(country_code, username, subject))
|
context.update(self.base_template(country_code, username, subject))
|
||||||
return render_to_string(
|
return render_to_string(
|
||||||
template_name=settings.NOTIFICATION_PASSWORD_TEMPLATE,
|
template_name=settings.NOTIFICATION_PASSWORD_TEMPLATE,
|
||||||
|
|
@ -595,6 +658,8 @@ class UserRole(ProjectBaseMixin):
|
||||||
help_text='A user (REQUESTER) who requests a '
|
help_text='A user (REQUESTER) who requests a '
|
||||||
'role change for a USER')
|
'role change for a USER')
|
||||||
|
|
||||||
|
for_team = models.BooleanField(verbose_name=_('is this role for team membership'), default=False)
|
||||||
|
|
||||||
objects = UserRoleQueryset.as_manager()
|
objects = UserRoleQueryset.as_manager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"""Back account serializers"""
|
"""Back account serializers"""
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
from account import models
|
from account import models
|
||||||
from account.serializers import RoleBaseSerializer, UserSerializer, subscriptions_handler
|
from account.serializers import RoleBaseSerializer, UserSerializer, subscriptions_handler
|
||||||
|
|
@ -174,3 +175,20 @@ class UserCSVSerializer(serializers.ModelSerializer):
|
||||||
'last_ip',
|
'last_ip',
|
||||||
'roles_list',
|
'roles_list',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class InviteTeamSerializer(serializers.ModelSerializer):
|
||||||
|
email = serializers.EmailField(required=True, allow_blank=False, allow_null=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.User
|
||||||
|
fields = (
|
||||||
|
'id',
|
||||||
|
'email',
|
||||||
|
)
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
with transaction.atomic():
|
||||||
|
establishment = self.context['view'].kwargs['establishment']
|
||||||
|
country_code = self.context['request'].country_code
|
||||||
|
return models.User.objects.invite_for_team(validated_data['email'], establishment, country_code)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
"""Account app celery tasks."""
|
"""Account app celery tasks."""
|
||||||
|
import datetime
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from celery.schedules import crontab
|
||||||
|
from celery.task import periodic_task
|
||||||
|
|
||||||
from account.models import User
|
from account.models import User, UserRole, Role
|
||||||
|
|
||||||
logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO)
|
logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -30,6 +33,59 @@ def send_reset_password_email(user_id, country_code):
|
||||||
send_email(user_id, 'Password_resetting', 'reset_password_template', country_code)
|
send_email(user_id, 'Password_resetting', 'reset_password_template', country_code)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def send_team_invite_to_new_user(user_id, country_code, user_role_id, restaurant_name):
|
||||||
|
"""Send email to establishment team member with resetting password and accepting role link"""
|
||||||
|
user = User.objects.get(id=user_id)
|
||||||
|
subject = _(f'GAULT&MILLAU INVITES YOU TO MANAGE {restaurant_name}')
|
||||||
|
message = user.invite_new_establishment_member_template(country_code, user.username,
|
||||||
|
subject, restaurant_name, user_role_id)
|
||||||
|
try:
|
||||||
|
user.send_email(subject=subject,
|
||||||
|
message=message,
|
||||||
|
emails=None)
|
||||||
|
except:
|
||||||
|
cur_frame = inspect.currentframe()
|
||||||
|
cal_frame = inspect.getouterframes(cur_frame, 2)
|
||||||
|
logger.error(f'METHOD_NAME: {cal_frame[1][3]}\n'
|
||||||
|
f'DETAIL: Exception occurred for user: {user_id}')
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def send_team_invite_to_existing_user(user_id, country_code, user_role_id, restaurant_name):
|
||||||
|
"""Send email to establishment team member with role acceptance link"""
|
||||||
|
user = User.objects.get(id=user_id)
|
||||||
|
subject = _(f'GAULT&MILLAU INVITES YOU TO MANAGE {restaurant_name}')
|
||||||
|
message = user.invite_establishment_member_template(country_code, user.username,
|
||||||
|
subject, restaurant_name, user_role_id)
|
||||||
|
try:
|
||||||
|
user.send_email(subject=subject,
|
||||||
|
message=message,
|
||||||
|
emails=None)
|
||||||
|
except:
|
||||||
|
cur_frame = inspect.currentframe()
|
||||||
|
cal_frame = inspect.getouterframes(cur_frame, 2)
|
||||||
|
logger.error(f'METHOD_NAME: {cal_frame[1][3]}\n'
|
||||||
|
f'DETAIL: Exception occurred for user: {user_id}')
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def team_role_revoked(user_id, country_code, restaurant_name):
|
||||||
|
"""Send email to establishment team member with role acceptance link"""
|
||||||
|
user = User.objects.get(id=user_id)
|
||||||
|
subject = _(f'Your GAULT&MILLAU privileges to manage {restaurant_name} have been revoked.')
|
||||||
|
message = user.establishment_team_role_revoked(country_code, user.username, subject, restaurant_name)
|
||||||
|
try:
|
||||||
|
user.send_email(subject=subject,
|
||||||
|
message=message,
|
||||||
|
emails=None)
|
||||||
|
except:
|
||||||
|
cur_frame = inspect.currentframe()
|
||||||
|
cal_frame = inspect.getouterframes(cur_frame, 2)
|
||||||
|
logger.error(f'METHOD_NAME: {cal_frame[1][3]}\n'
|
||||||
|
f'DETAIL: Exception occurred for user: {user_id}')
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def confirm_new_email_address(user_id, country_code):
|
def confirm_new_email_address(user_id, country_code):
|
||||||
"""Send email to user new email."""
|
"""Send email to user new email."""
|
||||||
|
|
@ -46,3 +102,17 @@ def change_email_address(user_id, country_code, emails=None):
|
||||||
def send_password_changed_email(user_id, country_code):
|
def send_password_changed_email(user_id, country_code):
|
||||||
"""Send email which notifies user that his password had changed"""
|
"""Send email which notifies user that his password had changed"""
|
||||||
send_email(user_id, 'Notify password changed', 'notify_password_changed_template', country_code)
|
send_email(user_id, 'Notify password changed', 'notify_password_changed_template', country_code)
|
||||||
|
|
||||||
|
|
||||||
|
@periodic_task(run_every=crontab(hour='*/3'))
|
||||||
|
def clean_unaccepted_establishment_team_invites():
|
||||||
|
"""This task cleans unaccepted (for 7 days) establishment team membership invites."""
|
||||||
|
week_ago = datetime.datetime.now() - datetime.timedelta(days=7)
|
||||||
|
roles_to_remove = UserRole.objects.filter(role__role=Role.ESTABLISHMENT_ADMINISTRATOR,
|
||||||
|
for_team=True, created__lte=week_ago)\
|
||||||
|
.exclude(state=UserRole.VALIDATED).prefetch_related('user', 'role')
|
||||||
|
for user_role in roles_to_remove:
|
||||||
|
user = user_role.user
|
||||||
|
user_role.delete()
|
||||||
|
if user.last_login is None:
|
||||||
|
user.delete()
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,6 @@ urlpatterns = [
|
||||||
path('user/<int:id>/', views.UserRUDView.as_view(), name='user-rud'),
|
path('user/<int:id>/', views.UserRUDView.as_view(), name='user-rud'),
|
||||||
path('user/<int:id>/csv/', views.get_user_csv, name='user-csv'),
|
path('user/<int:id>/csv/', views.get_user_csv, name='user-csv'),
|
||||||
path('user/csv/', views.UserCSVViewSet.as_view({'get': 'to_csv'}), name='user-csv-list'),
|
path('user/csv/', views.UserCSVViewSet.as_view({'get': 'to_csv'}), name='user-csv-list'),
|
||||||
|
path('team/invite/<int:establishment_id>', views.EstablishmentTeamInviteView.as_view(),
|
||||||
|
name='invite-to-establishment-team'),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ urlpatterns_api = [
|
||||||
path('reset-password/', views.PasswordResetView.as_view(), name='password-reset'),
|
path('reset-password/', views.PasswordResetView.as_view(), name='password-reset'),
|
||||||
path('reset-password/confirm/<uidb64>/<token>/', views.PasswordResetConfirmView.as_view(),
|
path('reset-password/confirm/<uidb64>/<token>/', views.PasswordResetConfirmView.as_view(),
|
||||||
name='password-reset-confirm'),
|
name='password-reset-confirm'),
|
||||||
|
path('join-establishment-team/<uidb64>/<token>/<int:role_id>', views.ApplyUserEstablishmentRole.as_view(),
|
||||||
|
name='join-establishment-team'),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns = urlpatterns_api + \
|
urlpatterns = urlpatterns_api + \
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from datetime import datetime
|
||||||
|
|
||||||
from django.http import HttpResponse, HttpResponseNotFound
|
from django.http import HttpResponse, HttpResponseNotFound
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from rest_framework import generics, status
|
from rest_framework import generics, status
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
|
|
@ -14,6 +15,7 @@ from account import models, filters
|
||||||
from account.models import User
|
from account.models import User
|
||||||
from account.serializers import back as serializers
|
from account.serializers import back as serializers
|
||||||
from account.serializers.common import RoleBaseSerializer
|
from account.serializers.common import RoleBaseSerializer
|
||||||
|
from establishment.models import Establishment
|
||||||
from utils.methods import get_permission_classes
|
from utils.methods import get_permission_classes
|
||||||
from utils.permissions import IsReviewManager
|
from utils.permissions import IsReviewManager
|
||||||
|
|
||||||
|
|
@ -187,3 +189,16 @@ class UserCSVViewSet(ModelViewSet):
|
||||||
writer.writerow(item.values())
|
writer.writerow(item.values())
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentTeamInviteView(generics.CreateAPIView):
|
||||||
|
"""View to invite new team member by email"""
|
||||||
|
queryset = User.objects.all()
|
||||||
|
# permission_classes = get_permission_classes()
|
||||||
|
from rest_framework.permissions import AllowAny
|
||||||
|
permission_classes = (AllowAny, )
|
||||||
|
serializer_class = serializers.InviteTeamSerializer
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
self.kwargs['establishment'] = get_object_or_404(klass=Establishment, pk=self.kwargs['establishment_id'])
|
||||||
|
return super().create(request, *args, **kwargs)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"""Web account views"""
|
"""Web account views"""
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.db import transaction
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.utils.http import urlsafe_base64_decode
|
from django.utils.http import urlsafe_base64_decode
|
||||||
|
|
@ -61,10 +62,18 @@ class PasswordResetConfirmView(JWTGenericViewMixin, generics.GenericAPIView):
|
||||||
def patch(self, request, *args, **kwargs):
|
def patch(self, request, *args, **kwargs):
|
||||||
"""Implement PATCH method"""
|
"""Implement PATCH method"""
|
||||||
instance = self.get_object()
|
instance = self.get_object()
|
||||||
serializer = self.get_serializer(instance=instance,
|
with transaction.atomic():
|
||||||
data=request.data)
|
serializer = self.get_serializer(instance=instance,
|
||||||
serializer.is_valid(raise_exception=True)
|
data=request.data)
|
||||||
serializer.save()
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
|
# apply requested user_role if 'role' in query_params
|
||||||
|
if 'role' in request.query_params:
|
||||||
|
user_role = get_object_or_404(klass=models.UserRole, pk=request.query_params['role'])
|
||||||
|
user_role.state = models.UserRole.VALIDATED
|
||||||
|
user_role.save()
|
||||||
|
|
||||||
# Create tokens
|
# Create tokens
|
||||||
tokens = instance.create_jwt_tokens()
|
tokens = instance.create_jwt_tokens()
|
||||||
return self._put_cookies_in_response(
|
return self._put_cookies_in_response(
|
||||||
|
|
@ -72,3 +81,40 @@ class PasswordResetConfirmView(JWTGenericViewMixin, generics.GenericAPIView):
|
||||||
access_token=tokens.get('access_token'),
|
access_token=tokens.get('access_token'),
|
||||||
refresh_token=tokens.get('refresh_token')),
|
refresh_token=tokens.get('refresh_token')),
|
||||||
response=Response(status=status.HTTP_200_OK))
|
response=Response(status=status.HTTP_200_OK))
|
||||||
|
|
||||||
|
|
||||||
|
class ApplyUserEstablishmentRole(JWTGenericViewMixin, generics.GenericAPIView):
|
||||||
|
|
||||||
|
queryset = models.User.objects.all()
|
||||||
|
permission_classes = (permissions.AllowAny,)
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
"""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')
|
||||||
|
|
||||||
|
user = get_object_or_404(queryset, id=user_id)
|
||||||
|
|
||||||
|
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, user)
|
||||||
|
|
||||||
|
return get_object_or_404(klass=models.UserRole, pk=self.kwargs['role_id'], user=user), user
|
||||||
|
|
||||||
|
def patch(self, request, *args, **kwargs):
|
||||||
|
instance, user = self.get_object()
|
||||||
|
instance.state = models.UserRole.VALIDATED
|
||||||
|
instance.save()
|
||||||
|
# Create tokens
|
||||||
|
tokens = user.create_jwt_tokens()
|
||||||
|
return self._put_cookies_in_response(
|
||||||
|
cookies=self._put_data_in_cookies(
|
||||||
|
access_token=tokens.get('access_token'),
|
||||||
|
refresh_token=tokens.get('refresh_token')),
|
||||||
|
response=Response(status=status.HTTP_200_OK))
|
||||||
|
|
|
||||||
|
|
@ -182,7 +182,7 @@ class LoginByUsernameOrEmailView(JWTGenericViewMixin, generics.GenericAPIView):
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
## Login view.
|
## Login view.
|
||||||
POST-request data
|
### POST-request data
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
"username_or_email": <str>,
|
"username_or_email": <str>,
|
||||||
|
|
@ -191,11 +191,12 @@ class LoginByUsernameOrEmailView(JWTGenericViewMixin, generics.GenericAPIView):
|
||||||
"source": <int> # 0 - Mobile, 1 - Web, 2 - All (by default used: 1)
|
"source": <int> # 0 - Mobile, 1 - Web, 2 - All (by default used: 1)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
## Response
|
### Response
|
||||||
After a successful login, server side set up access_token and refresh token to cookies.
|
After a successful login, server side set up access_token and refresh token to cookies.
|
||||||
In a payload of access token, the following information is being embed:
|
In a payload of access token, the following information is being embed:
|
||||||
see `User().get_user_info()`.
|
see `User().get_user_info()`.
|
||||||
|
|
||||||
|
### Description
|
||||||
COOKIE Max-age are determined by `remember` flag:
|
COOKIE Max-age are determined by `remember` flag:
|
||||||
if `remember` is `True` then `Max-age` parameter taken from `settings.COOKIES_MAX_AGE`
|
if `remember` is `True` then `Max-age` parameter taken from `settings.COOKIES_MAX_AGE`
|
||||||
otherwise using session COOKIE Max-age.
|
otherwise using session COOKIE Max-age.
|
||||||
|
|
@ -221,7 +222,23 @@ class LogoutView(JWTGenericViewMixin, generics.GenericAPIView):
|
||||||
permission_classes = (IsAuthenticatedAndTokenIsValid, )
|
permission_classes = (IsAuthenticatedAndTokenIsValid, )
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
"""Override create method"""
|
"""
|
||||||
|
## Logout view.
|
||||||
|
### POST-request data
|
||||||
|
```
|
||||||
|
{}
|
||||||
|
```
|
||||||
|
### Response
|
||||||
|
If user has *valid* `access_token` in COOKIES, then response return
|
||||||
|
blank response data with `HTTP_STATUS_CODE` *204*.
|
||||||
|
|
||||||
|
### Description
|
||||||
|
For complete logout, user must provide *valid* `access_token`
|
||||||
|
(`access_token` must be kept in `COOKIES`).
|
||||||
|
After successful request with valid access_token, token would be expired,
|
||||||
|
for reuse protection.
|
||||||
|
"""
|
||||||
|
|
||||||
# Get access token objs by JTI
|
# Get access token objs by JTI
|
||||||
access_token = AccessToken(request.COOKIES.get('access_token'))
|
access_token = AccessToken(request.COOKIES.get('access_token'))
|
||||||
access_token_obj = JWTAccessToken.objects.get(jti=access_token.payload.get('jti'))
|
access_token_obj = JWTAccessToken.objects.get(jti=access_token.payload.get('jti'))
|
||||||
|
|
|
||||||
|
|
@ -41,12 +41,18 @@ class GuideFilterSet(filters.FilterSet):
|
||||||
'Use for Establishment detail\'s sheet to content display within '
|
'Use for Establishment detail\'s sheet to content display within '
|
||||||
'"Collections & Guides" tab.'
|
'"Collections & Guides" tab.'
|
||||||
)
|
)
|
||||||
|
guide_type = filters.ChoiceFilter(
|
||||||
|
choices=models.Guide.GUIDE_TYPE_CHOICES,
|
||||||
|
help_text=f'It allows filtering guide list by type.'
|
||||||
|
f'Enum: {dict(models.Guide.GUIDE_TYPE_CHOICES)}'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
model = models.Guide
|
model = models.Guide
|
||||||
fields = (
|
fields = (
|
||||||
'establishment_id',
|
'establishment_id',
|
||||||
|
'guide_type',
|
||||||
)
|
)
|
||||||
|
|
||||||
def by_establishment_id(self, queryset, name, value):
|
def by_establishment_id(self, queryset, name, value):
|
||||||
|
|
|
||||||
|
|
@ -866,19 +866,22 @@ class GuideElementManager(models.Manager):
|
||||||
city=city_qs.first())
|
city=city_qs.first())
|
||||||
return None, False
|
return None, False
|
||||||
|
|
||||||
def get_or_create_establishment_section_node(self, city_node: int, establishment_node_name: str):
|
def get_or_create_establishment_section_node(self, city_node_id: int, establishment_node_name: str,
|
||||||
|
guide_id: int):
|
||||||
"""Get or Create (Restaurant|Shop...)SectionNode."""
|
"""Get or Create (Restaurant|Shop...)SectionNode."""
|
||||||
parent_node_qs = GuideElement.objects.filter(id=city_node)
|
parent_node_qs = GuideElement.objects.filter(id=city_node_id)
|
||||||
guide_element_type_qs = GuideElementType.objects.filter(name__iexact=establishment_node_name)
|
guide_element_type_qs = GuideElementType.objects.filter(name__iexact=establishment_node_name)
|
||||||
|
guide_qs = Guide.objects.filter(id=guide_id)
|
||||||
|
|
||||||
if parent_node_qs.exists() and guide_element_type_qs.exists():
|
if parent_node_qs.exists() and guide_element_type_qs.exists() and guide_qs.exists():
|
||||||
parent_node = parent_node_qs.first()
|
parent_node = parent_node_qs.first()
|
||||||
|
|
||||||
return self.get_or_create(guide_element_type=guide_element_type_qs.first(),
|
return self.get_or_create(guide_element_type=guide_element_type_qs.first(),
|
||||||
parent=parent_node,
|
parent=parent_node,
|
||||||
guide=parent_node.get_root().guide)
|
guide=guide_qs.first())
|
||||||
return None, False
|
return None, False
|
||||||
|
|
||||||
def get_or_create_establishment_node(self, restaurant_section_node_id: int,
|
def get_or_create_establishment_node(self, restaurant_section_node_id: int, guide_id: int,
|
||||||
establishment_id: int, review_id: int = None):
|
establishment_id: int, review_id: int = None):
|
||||||
"""Get or Create EstablishmentNode."""
|
"""Get or Create EstablishmentNode."""
|
||||||
from establishment.models import Establishment
|
from establishment.models import Establishment
|
||||||
|
|
@ -887,14 +890,16 @@ class GuideElementManager(models.Manager):
|
||||||
guide_element_type_qs = GuideElementType.objects.filter(name='EstablishmentNode')
|
guide_element_type_qs = GuideElementType.objects.filter(name='EstablishmentNode')
|
||||||
parent_node_qs = GuideElement.objects.filter(id=restaurant_section_node_id)
|
parent_node_qs = GuideElement.objects.filter(id=restaurant_section_node_id)
|
||||||
establishment_qs = Establishment.objects.filter(id=establishment_id)
|
establishment_qs = Establishment.objects.filter(id=establishment_id)
|
||||||
|
guide_qs = Guide.objects.filter(id=guide_id)
|
||||||
|
|
||||||
if parent_node_qs.exists() and establishment_qs.exists() and guide_element_type_qs.exists():
|
if (parent_node_qs.exists() and establishment_qs.exists()
|
||||||
|
and guide_element_type_qs.exists() and guide_qs.exists()):
|
||||||
establishment = establishment_qs.first()
|
establishment = establishment_qs.first()
|
||||||
parent_node = parent_node_qs.first()
|
parent_node = parent_node_qs.first()
|
||||||
data.update({
|
data.update({
|
||||||
'guide_element_type': guide_element_type_qs.first(),
|
'guide_element_type': guide_element_type_qs.first(),
|
||||||
'parent': parent_node,
|
'parent': parent_node,
|
||||||
'guide': parent_node.get_root().guide,
|
'guide': guide_qs.first(),
|
||||||
'establishment': establishment
|
'establishment': establishment
|
||||||
})
|
})
|
||||||
if review_id:
|
if review_id:
|
||||||
|
|
@ -903,44 +908,47 @@ class GuideElementManager(models.Manager):
|
||||||
return self.get_or_create(**data)
|
return self.get_or_create(**data)
|
||||||
return None, False
|
return None, False
|
||||||
|
|
||||||
def get_or_create_wine_region_node(self, root_node_id: int, wine_region_id: int):
|
def get_or_create_wine_region_node(self, root_node_id: int, wine_region_id: int, guide_id: int):
|
||||||
"""Get or Create WineRegionNode."""
|
"""Get or Create WineRegionNode."""
|
||||||
guide_element_type_qs = GuideElementType.objects.filter(name='RegionNode')
|
guide_element_type_qs = GuideElementType.objects.filter(name='RegionNode')
|
||||||
parent_node_qs = GuideElement.objects.filter(id=root_node_id)
|
parent_node_qs = GuideElement.objects.filter(id=root_node_id)
|
||||||
wine_region_qs = WineRegion.objects.filter(id=wine_region_id)
|
wine_region_qs = WineRegion.objects.filter(id=wine_region_id)
|
||||||
|
guide_qs = Guide.objects.filter(id=guide_id)
|
||||||
|
|
||||||
if parent_node_qs.exists() and parent_node_qs.first().guide and wine_region_qs.exists() and guide_element_type_qs.exists():
|
if (parent_node_qs.exists() and parent_node_qs.first().guide and
|
||||||
|
wine_region_qs.exists() and guide_element_type_qs.exists() and
|
||||||
|
guide_qs.exists()):
|
||||||
root_node = parent_node_qs.first()
|
root_node = parent_node_qs.first()
|
||||||
return self.get_or_create(guide_element_type=guide_element_type_qs.first(),
|
return self.get_or_create(guide_element_type=guide_element_type_qs.first(),
|
||||||
parent=root_node,
|
parent=root_node,
|
||||||
guide=root_node.guide,
|
guide=guide_qs.first(),
|
||||||
wine_region=wine_region_qs.first())
|
wine_region=wine_region_qs.first())
|
||||||
return None, False
|
return None, False
|
||||||
|
|
||||||
def get_or_create_yard_node(self, product_id: int, wine_region_node_id: int):
|
def get_or_create_yard_node(self, product_id: int, wine_region_node_id: int, guide_id: int):
|
||||||
"""Make YardNode."""
|
"""Make YardNode."""
|
||||||
from establishment.models import Establishment
|
from establishment.models import Establishment
|
||||||
|
|
||||||
guide_element_type_qs = GuideElementType.objects.filter(name='YardNode')
|
guide_element_type_qs = GuideElementType.objects.filter(name='YardNode')
|
||||||
wine_region_node_qs = GuideElement.objects.filter(id=wine_region_node_id)
|
wine_region_node_qs = GuideElement.objects.filter(id=wine_region_node_id)
|
||||||
product_qs = Product.objects.filter(id=product_id)
|
product_qs = Product.objects.filter(id=product_id)
|
||||||
|
guide_qs = Guide.objects.filter(id=guide_id)
|
||||||
|
|
||||||
if product_qs.exists() and wine_region_node_qs.exists():
|
if product_qs.exists() and wine_region_node_qs.exists() and guide_qs.exists():
|
||||||
wine_region_node = wine_region_node_qs.first()
|
wine_region_node = wine_region_node_qs.first()
|
||||||
root_node = wine_region_node.get_root()
|
|
||||||
product = product_qs.first()
|
product = product_qs.first()
|
||||||
|
return self.get_or_create(guide_element_type=guide_element_type_qs.first(),
|
||||||
if product.establishment:
|
parent=wine_region_node,
|
||||||
return self.get_or_create(guide_element_type=guide_element_type_qs.first(),
|
guide=guide_qs.first(),
|
||||||
parent=wine_region_node,
|
establishment=product.establishment)
|
||||||
guide=root_node.guide,
|
|
||||||
establishment=product.establishment)
|
|
||||||
return None, False
|
return None, False
|
||||||
|
|
||||||
def get_or_create_color_wine_section_node(self, wine_color_name: str, yard_node_id: int):
|
def get_or_create_color_wine_section_node(self, wine_color_name: str,
|
||||||
|
yard_node_id: int, guide_id: int):
|
||||||
"""Get or Create WineSectionNode."""
|
"""Get or Create WineSectionNode."""
|
||||||
guide_element_type_qs = GuideElementType.objects.filter(name='ColorWineSectionNode')
|
guide_element_type_qs = GuideElementType.objects.filter(name='ColorWineSectionNode')
|
||||||
parent_node_qs = GuideElement.objects.filter(id=yard_node_id)
|
parent_node_qs = GuideElement.objects.filter(id=yard_node_id)
|
||||||
|
guide_qs = Guide.objects.filter(id=guide_id)
|
||||||
|
|
||||||
if not wine_color_name.endswith('SectionNode'):
|
if not wine_color_name.endswith('SectionNode'):
|
||||||
wine_color_name = transform_into_section_name(wine_color_name)
|
wine_color_name = transform_into_section_name(wine_color_name)
|
||||||
|
|
@ -951,27 +959,30 @@ class GuideElementManager(models.Manager):
|
||||||
'name': wine_color_name
|
'name': wine_color_name
|
||||||
})
|
})
|
||||||
|
|
||||||
if parent_node_qs.exists() and guide_element_type_qs.exists():
|
if parent_node_qs.exists() and guide_element_type_qs.exists() and guide_qs.exists():
|
||||||
root_node = parent_node_qs.first()
|
root_node = parent_node_qs.first()
|
||||||
return self.get_or_create(guide_element_type=guide_element_type_qs.first(),
|
return self.get_or_create(guide_element_type=guide_element_type_qs.first(),
|
||||||
parent=root_node,
|
parent=root_node,
|
||||||
wine_color_section=wine_color_section,
|
wine_color_section=wine_color_section,
|
||||||
guide=root_node.guide)
|
guide=guide_qs.first())
|
||||||
return None, False
|
return None, False
|
||||||
|
|
||||||
def get_or_create_wine_node(self, color_wine_section_node_id: int, wine_id: int, review_id: int):
|
def get_or_create_wine_node(self, color_wine_section_node_id: int,
|
||||||
|
wine_id: int, review_id: int, guide_id: int):
|
||||||
"""Get or Create WineNode."""
|
"""Get or Create WineNode."""
|
||||||
guide_element_type_qs = GuideElementType.objects.filter(name='WineNode')
|
guide_element_type_qs = GuideElementType.objects.filter(name='WineNode')
|
||||||
parent_node_qs = GuideElement.objects.filter(id=color_wine_section_node_id)
|
parent_node_qs = GuideElement.objects.filter(id=color_wine_section_node_id)
|
||||||
wine_qs = Product.objects.wines().filter(id=wine_id)
|
wine_qs = Product.objects.wines().filter(id=wine_id)
|
||||||
review_qs = Review.objects.filter(id=review_id)
|
review_qs = Review.objects.filter(id=review_id)
|
||||||
|
guide_qs = Guide.objects.filter(id=guide_id)
|
||||||
|
|
||||||
if parent_node_qs.exists() and wine_qs.exists() and review_qs.exists() and guide_element_type_qs.exists():
|
if (parent_node_qs.exists() and wine_qs.exists() and
|
||||||
|
review_qs.exists() and guide_element_type_qs.exists() and guide_qs.exists()):
|
||||||
root_node = parent_node_qs.first()
|
root_node = parent_node_qs.first()
|
||||||
return self.get_or_create(guide_element_type=guide_element_type_qs.first(),
|
return self.get_or_create(guide_element_type=guide_element_type_qs.first(),
|
||||||
parent=root_node,
|
parent=root_node,
|
||||||
product=wine_qs.first(),
|
product=wine_qs.first(),
|
||||||
guide=root_node.guide,
|
guide=guide_qs.first(),
|
||||||
review=review_qs.first())
|
review=review_qs.first())
|
||||||
return None, False
|
return None, False
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -137,8 +137,8 @@ class GuideBaseSerializer(serializers.ModelSerializer):
|
||||||
'count_objects_during_init',
|
'count_objects_during_init',
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'guide_type': {'write_only': True},
|
'guide_type': {'write_only': True, 'required': True},
|
||||||
'site': {'write_only': True},
|
'site': {'write_only': True, 'required': True},
|
||||||
'state': {'write_only': True},
|
'state': {'write_only': True},
|
||||||
'start': {'required': True},
|
'start': {'required': True},
|
||||||
'slug': {'required': False},
|
'slug': {'required': False},
|
||||||
|
|
|
||||||
|
|
@ -9,26 +9,6 @@ logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_additional_establishment_data(section_node, establishment):
|
|
||||||
data = [
|
|
||||||
section_node.id,
|
|
||||||
establishment.id,
|
|
||||||
]
|
|
||||||
if establishment.last_published_review:
|
|
||||||
data.append(establishment.last_published_review.id)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def get_additional_product_data(section_node, product):
|
|
||||||
data = [
|
|
||||||
section_node.id,
|
|
||||||
product.id,
|
|
||||||
]
|
|
||||||
if product.last_published_review:
|
|
||||||
data.append(product.last_published_review.id)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def generate_establishment_guide_elements(guide_id: int, filter_set: dict):
|
def generate_establishment_guide_elements(guide_id: int, filter_set: dict):
|
||||||
"""Generate guide elements."""
|
"""Generate guide elements."""
|
||||||
|
|
@ -36,9 +16,9 @@ def generate_establishment_guide_elements(guide_id: int, filter_set: dict):
|
||||||
from establishment.models import Establishment
|
from establishment.models import Establishment
|
||||||
|
|
||||||
guide = Guide.objects.get(id=guide_id)
|
guide = Guide.objects.get(id=guide_id)
|
||||||
guide.change_state(Guide.BUILDING)
|
|
||||||
queryset_values = Establishment.objects.filter(**filter_set).values()
|
queryset_values = Establishment.objects.filter(**filter_set).values()
|
||||||
try:
|
try:
|
||||||
|
guide.change_state(Guide.BUILDING)
|
||||||
for instance in queryset_values:
|
for instance in queryset_values:
|
||||||
populate_establishment_guide(guide_id, instance.get('id'))
|
populate_establishment_guide(guide_id, instance.get('id'))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -57,8 +37,8 @@ def populate_establishment_guide(guide_id: int, establishment_id: int):
|
||||||
from establishment.models import Establishment
|
from establishment.models import Establishment
|
||||||
|
|
||||||
guide = Guide.objects.get(id=guide_id)
|
guide = Guide.objects.get(id=guide_id)
|
||||||
guide.change_state(Guide.BUILDING)
|
|
||||||
try:
|
try:
|
||||||
|
guide.change_state(Guide.BUILDING)
|
||||||
establishment_qs = Establishment.objects.filter(id=establishment_id)
|
establishment_qs = Establishment.objects.filter(id=establishment_id)
|
||||||
if establishment_qs.exists():
|
if establishment_qs.exists():
|
||||||
establishment = establishment_qs.first()
|
establishment = establishment_qs.first()
|
||||||
|
|
@ -70,11 +50,17 @@ def populate_establishment_guide(guide_id: int, establishment_id: int):
|
||||||
section_node, _ = GuideElement.objects.get_or_create_establishment_section_node(
|
section_node, _ = GuideElement.objects.get_or_create_establishment_section_node(
|
||||||
city_node.id,
|
city_node.id,
|
||||||
transform_into_section_name(establishment.establishment_type.index_name),
|
transform_into_section_name(establishment.establishment_type.index_name),
|
||||||
|
guide.id,
|
||||||
)
|
)
|
||||||
if section_node:
|
if section_node:
|
||||||
GuideElement.objects.get_or_create_establishment_node(
|
params = {
|
||||||
*get_additional_establishment_data(section_node=section_node,
|
'restaurant_section_node_id': section_node.id,
|
||||||
establishment=establishment))
|
'guide_id': guide.id,
|
||||||
|
'establishment_id': establishment.id,
|
||||||
|
}
|
||||||
|
if establishment.last_published_review:
|
||||||
|
params.update({'review_id': establishment.last_published_review.id})
|
||||||
|
GuideElement.objects.get_or_create_establishment_node(**params)
|
||||||
else:
|
else:
|
||||||
logger.error(
|
logger.error(
|
||||||
f'METHOD_NAME: {generate_establishment_guide_elements.__name__}\n'
|
f'METHOD_NAME: {generate_establishment_guide_elements.__name__}\n'
|
||||||
|
|
@ -87,7 +73,7 @@ def populate_establishment_guide(guide_id: int, establishment_id: int):
|
||||||
f'DETAIL: Guide ID {guide_id} - RootNode is not exists.')
|
f'DETAIL: Guide ID {guide_id} - RootNode is not exists.')
|
||||||
else:
|
else:
|
||||||
logger.error(f'METHOD_NAME: {generate_establishment_guide_elements.__name__}\n'
|
logger.error(f'METHOD_NAME: {generate_establishment_guide_elements.__name__}\n'
|
||||||
f'DETAIL: Guide ID {guide_id} - Establishment {establishment_id} id is not exists.')
|
f'DETAIL: Guide ID {guide_id} - Establishment {establishment_id} id is not exists.')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
guide.change_state(Guide.WAITING)
|
guide.change_state(Guide.WAITING)
|
||||||
logger.error(f'METHOD_NAME: {generate_establishment_guide_elements.__name__}\n'
|
logger.error(f'METHOD_NAME: {generate_establishment_guide_elements.__name__}\n'
|
||||||
|
|
@ -104,8 +90,8 @@ def remove_establishment_guide(guide_id: int, establishment_id: int):
|
||||||
from establishment.models import Establishment
|
from establishment.models import Establishment
|
||||||
|
|
||||||
guide = Guide.objects.get(id=guide_id)
|
guide = Guide.objects.get(id=guide_id)
|
||||||
guide.change_state(Guide.REMOVING)
|
|
||||||
try:
|
try:
|
||||||
|
guide.change_state(Guide.REMOVING)
|
||||||
establishment_qs = Establishment.objects.filter(id=establishment_id)
|
establishment_qs = Establishment.objects.filter(id=establishment_id)
|
||||||
if establishment_qs.exists():
|
if establishment_qs.exists():
|
||||||
establishment = establishment_qs.first()
|
establishment = establishment_qs.first()
|
||||||
|
|
@ -140,9 +126,9 @@ def generate_product_guide_elements(guide_id: int, filter_set: dict):
|
||||||
from product.models import Product
|
from product.models import Product
|
||||||
|
|
||||||
guide = Guide.objects.get(id=guide_id)
|
guide = Guide.objects.get(id=guide_id)
|
||||||
guide.change_state(Guide.BUILDING)
|
|
||||||
queryset_values = Product.objects.filter(**filter_set).values()
|
queryset_values = Product.objects.filter(**filter_set).values()
|
||||||
try:
|
try:
|
||||||
|
guide.change_state(Guide.BUILDING)
|
||||||
for instance in queryset_values:
|
for instance in queryset_values:
|
||||||
wine_id = instance.get('id')
|
wine_id = instance.get('id')
|
||||||
wine_qs = Product.objects.filter(id=wine_id)
|
wine_qs = Product.objects.filter(id=wine_id)
|
||||||
|
|
@ -152,24 +138,30 @@ def generate_product_guide_elements(guide_id: int, filter_set: dict):
|
||||||
if root_node:
|
if root_node:
|
||||||
wine_region_node, _ = GuideElement.objects.get_or_create_wine_region_node(
|
wine_region_node, _ = GuideElement.objects.get_or_create_wine_region_node(
|
||||||
root_node.id,
|
root_node.id,
|
||||||
wine.wine_region.id)
|
wine.wine_region.id,
|
||||||
|
guide.id)
|
||||||
if wine_region_node:
|
if wine_region_node:
|
||||||
yard_node, _ = GuideElement.objects.get_or_create_yard_node(
|
yard_node, _ = GuideElement.objects.get_or_create_yard_node(
|
||||||
product_id=wine.id,
|
wine.id,
|
||||||
wine_region_node_id=wine_region_node.id
|
wine_region_node.id,
|
||||||
)
|
guide.id)
|
||||||
if yard_node:
|
if yard_node:
|
||||||
wine_color_qs = wine.wine_colors
|
wine_color_qs = wine.wine_colors
|
||||||
if wine_color_qs.exists():
|
if wine_color_qs.exists():
|
||||||
wine_color_section, _ = GuideElement.objects.get_or_create_color_wine_section_node(
|
wine_color_section, _ = GuideElement.objects.get_or_create_color_wine_section_node(
|
||||||
wine_color_name=wine_color_qs.first().value,
|
wine_color_qs.first().value,
|
||||||
yard_node_id=yard_node.id
|
yard_node.id,
|
||||||
|
guide.id
|
||||||
)
|
)
|
||||||
if wine_color_section:
|
if wine_color_section:
|
||||||
GuideElement.objects.get_or_create_wine_node(
|
params = {
|
||||||
*get_additional_product_data(
|
'color_wine_section_node_id': wine_color_section.id,
|
||||||
section_node=wine_color_section,
|
'wine_id': wine.id,
|
||||||
product=wine))
|
'guide_id': guide.id,
|
||||||
|
}
|
||||||
|
if wine.last_published_review:
|
||||||
|
params.update({'review_id': wine.last_published_review.id})
|
||||||
|
GuideElement.objects.get_or_create_wine_node(**params)
|
||||||
else:
|
else:
|
||||||
logger.error(
|
logger.error(
|
||||||
f'METHOD_NAME: {generate_product_guide_elements.__name__}\n'
|
f'METHOD_NAME: {generate_product_guide_elements.__name__}\n'
|
||||||
|
|
|
||||||
|
|
@ -100,9 +100,48 @@ class CollectionBackOfficeList(CollectionBackOfficeViewSet):
|
||||||
pagination_class = None
|
pagination_class = None
|
||||||
|
|
||||||
|
|
||||||
class GuideListCreateView(GuideBaseView,
|
class GuideListCreateView(GuideBaseView, generics.ListCreateAPIView):
|
||||||
generics.ListCreateAPIView):
|
"""
|
||||||
"""View for Guides list for BackOffice users and Guide create."""
|
## Guide list/create view.
|
||||||
|
### Request
|
||||||
|
For creating new instance of Guide model, need to pass the following data in the request:
|
||||||
|
required fields:
|
||||||
|
* name (str) - guide name
|
||||||
|
* start (str) - guide start date (datetime in a format `ISO-8601`)
|
||||||
|
* vintage (str) - valid year
|
||||||
|
* guide_type (int) - guide type enum: `0 (Restaurant), 1 (Artisan), 2 (Wine)`
|
||||||
|
* site (int) - identifier of site
|
||||||
|
non-required fields:
|
||||||
|
* slug - generated automatically if not provided
|
||||||
|
* state - state enum`: `0 (Built), 1 (Waiting), 2 (Removing), 3 (Building)` (service states)
|
||||||
|
* end - guide end date (datetime in a format `ISO-8601`)
|
||||||
|
|
||||||
|
### Response
|
||||||
|
Return paginated list of guides.
|
||||||
|
I.e.:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"count": 58,
|
||||||
|
"next": 2,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Description
|
||||||
|
*GET*
|
||||||
|
Return paginated list of guides with the opportunity of filtering by next fields:
|
||||||
|
* establishment_id (int) - identifier of establishment,
|
||||||
|
* guide_type (int) - guide type enum: `0 (Restaurant), 1 (Artisan), 2 (Wine)`
|
||||||
|
|
||||||
|
*POST*
|
||||||
|
Create a new instance of guide.
|
||||||
|
"""
|
||||||
filter_class = filters.GuideFilterSet
|
filter_class = filters.GuideFilterSet
|
||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
|
|
|
||||||
19
apps/comment/migrations/0009_auto_20200204_1205.py
Normal file
19
apps/comment/migrations/0009_auto_20200204_1205.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Generated by Django 2.2.7 on 2020-02-04 12:05
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('comment', '0008_comment_status'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='comment',
|
||||||
|
name='mark',
|
||||||
|
field=models.PositiveIntegerField(blank=True, default=None, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(10)], verbose_name='Mark'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -17,7 +17,10 @@ class CommentLstView(generics.ListCreateAPIView):
|
||||||
"establishment": Establishment.__name__.lower()
|
"establishment": Establishment.__name__.lower()
|
||||||
}
|
}
|
||||||
|
|
||||||
qs = models.Comment.objects.with_base_related().filter(status=models.Comment.WAITING)
|
qs = models.Comment.objects.with_base_related()
|
||||||
|
|
||||||
|
if "object" not in self.kwargs and "type" not in self.kwargs:
|
||||||
|
qs = qs.filter(status=models.Comment.WAITING)
|
||||||
|
|
||||||
if "object" in self.kwargs:
|
if "object" in self.kwargs:
|
||||||
qs = qs.by_object_id(self.kwargs["object"])
|
qs = qs.by_object_id(self.kwargs["object"])
|
||||||
|
|
|
||||||
|
|
@ -173,5 +173,5 @@ class PositionsByEstablishmentFilter(filters.FilterSet):
|
||||||
def by_subtype(self, queryset, name, value):
|
def by_subtype(self, queryset, name, value):
|
||||||
"""filter by establishment subtype"""
|
"""filter by establishment subtype"""
|
||||||
if value not in EMPTY_VALUES:
|
if value not in EMPTY_VALUES:
|
||||||
return queryset.by_establishment_subtype(value)
|
return queryset.by_establishment_subtypes(value.split('__'))
|
||||||
return queryset
|
return queryset
|
||||||
|
|
|
||||||
18
apps/establishment/migrations/0093_auto_20200204_1120.py
Executable file
18
apps/establishment/migrations/0093_auto_20200204_1120.py
Executable file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.7 on 2020-02-04 11:20
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('establishment', '0092_merge_20200131_0835'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='establishment',
|
||||||
|
name='status',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[(0, 'Abandoned'), (1, 'Closed'), (2, 'Published'), (3, 'Unpicked'), (4, 'Waiting'), (5, 'Hidden'), (6, 'Deleted'), (7, 'Out of selection'), (8, 'Unpublished')], default=4, verbose_name='Status'),
|
||||||
|
),
|
||||||
|
]
|
||||||
14
apps/establishment/migrations/0097_merge_20200204_1135.py
Normal file
14
apps/establishment/migrations/0097_merge_20200204_1135.py
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Generated by Django 2.2.7 on 2020-02-04 11:35
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('establishment', '0093_auto_20200204_1120'),
|
||||||
|
('establishment', '0096_auto_20200204_0952'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
]
|
||||||
24
apps/establishment/migrations/0098_auto_20200204_1205.py
Normal file
24
apps/establishment/migrations/0098_auto_20200204_1205.py
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Generated by Django 2.2.7 on 2020-02-04 12:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('establishment', '0097_merge_20200204_1135'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='company',
|
||||||
|
name='establishment',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='companies', to='establishment.Establishment', verbose_name='establishment'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='establishmentnote',
|
||||||
|
name='establishment',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='establishment.Establishment', verbose_name='establishment'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -3,7 +3,6 @@ from datetime import datetime
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from operator import or_
|
from operator import or_
|
||||||
from typing import List
|
from typing import List
|
||||||
from slugify import slugify
|
|
||||||
|
|
||||||
import elasticsearch_dsl
|
import elasticsearch_dsl
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
@ -35,7 +34,7 @@ from utils.models import (
|
||||||
BaseAttributes, FavoritesMixin, FileMixin, GalleryMixin, HasTagsMixin,
|
BaseAttributes, FavoritesMixin, FileMixin, GalleryMixin, HasTagsMixin,
|
||||||
IntermediateGalleryModelMixin, ProjectBaseMixin, TJSONField, TranslatedFieldsMixin,
|
IntermediateGalleryModelMixin, ProjectBaseMixin, TJSONField, TranslatedFieldsMixin,
|
||||||
TypeDefaultImageMixin, URLImageMixin, default_menu_bool_array, PhoneModelMixin,
|
TypeDefaultImageMixin, URLImageMixin, default_menu_bool_array, PhoneModelMixin,
|
||||||
)
|
AwardsModelMixin)
|
||||||
|
|
||||||
|
|
||||||
# todo: establishment type&subtypes check
|
# todo: establishment type&subtypes check
|
||||||
|
|
@ -129,12 +128,16 @@ class EstablishmentQuerySet(models.QuerySet):
|
||||||
def with_base_related(self):
|
def with_base_related(self):
|
||||||
"""Return qs with related objects."""
|
"""Return qs with related objects."""
|
||||||
return self.select_related('address', 'establishment_type'). \
|
return self.select_related('address', 'establishment_type'). \
|
||||||
prefetch_related('tags', 'tags__translation').with_main_image()
|
prefetch_related('tags', 'tags__translation', 'establishment_subtypes').with_main_image()
|
||||||
|
|
||||||
def with_schedule(self):
|
def with_schedule(self):
|
||||||
"""Return qs with related schedule."""
|
"""Return qs with related schedule."""
|
||||||
return self.prefetch_related('schedule')
|
return self.prefetch_related('schedule')
|
||||||
|
|
||||||
|
def with_reviews(self):
|
||||||
|
"""Return qs with related reviews."""
|
||||||
|
return self.prefetch_related('reviews')
|
||||||
|
|
||||||
def with_currency_related(self):
|
def with_currency_related(self):
|
||||||
"""Return qs with related """
|
"""Return qs with related """
|
||||||
return self.prefetch_related('currency')
|
return self.prefetch_related('currency')
|
||||||
|
|
@ -546,7 +549,7 @@ class EstablishmentQuerySet(models.QuerySet):
|
||||||
|
|
||||||
|
|
||||||
class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
|
class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
|
||||||
TranslatedFieldsMixin, HasTagsMixin, FavoritesMixin):
|
TranslatedFieldsMixin, HasTagsMixin, FavoritesMixin, AwardsModelMixin):
|
||||||
"""Establishment model."""
|
"""Establishment model."""
|
||||||
|
|
||||||
ABANDONED = 0
|
ABANDONED = 0
|
||||||
|
|
@ -554,6 +557,10 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
|
||||||
PUBLISHED = 2
|
PUBLISHED = 2
|
||||||
UNPICKED = 3
|
UNPICKED = 3
|
||||||
WAITING = 4
|
WAITING = 4
|
||||||
|
HIDDEN = 5
|
||||||
|
DELETED = 6
|
||||||
|
OUT_OF_SELECTION = 7
|
||||||
|
UNPUBLISHED = 8
|
||||||
|
|
||||||
STATUS_CHOICES = (
|
STATUS_CHOICES = (
|
||||||
(ABANDONED, _('Abandoned')),
|
(ABANDONED, _('Abandoned')),
|
||||||
|
|
@ -561,6 +568,10 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
|
||||||
(PUBLISHED, _('Published')),
|
(PUBLISHED, _('Published')),
|
||||||
(UNPICKED, _('Unpicked')),
|
(UNPICKED, _('Unpicked')),
|
||||||
(WAITING, _('Waiting')),
|
(WAITING, _('Waiting')),
|
||||||
|
(HIDDEN, _('Hidden')),
|
||||||
|
(DELETED, _('Deleted')),
|
||||||
|
(OUT_OF_SELECTION, _('Out of selection')),
|
||||||
|
(UNPUBLISHED, _('Unpublished')),
|
||||||
)
|
)
|
||||||
|
|
||||||
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
|
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
|
||||||
|
|
@ -656,24 +667,6 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'id:{self.id}-{self.name}'
|
return f'id:{self.id}-{self.name}'
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
slugify_slug = slugify(
|
|
||||||
self.index_name,
|
|
||||||
word_boundary=True
|
|
||||||
)
|
|
||||||
self.slug = slugify_slug
|
|
||||||
super(Establishment, self).save(*args, **kwargs)
|
|
||||||
|
|
||||||
def delete(self, using=None, keep_parents=False):
|
|
||||||
"""Overridden delete method"""
|
|
||||||
# TODO: If this does not contradict the plan,
|
|
||||||
# it is better to change it.
|
|
||||||
# Just add CASCADE to Company and Note in establishment fk field.
|
|
||||||
# Delete all related companies
|
|
||||||
self.companies.all().delete()
|
|
||||||
# Delete all related notes
|
|
||||||
self.notes.all().delete()
|
|
||||||
return super().delete(using, keep_parents)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def visible_tags(self):
|
def visible_tags(self):
|
||||||
|
|
@ -853,6 +846,10 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
|
||||||
def distillery_type_indexing(self):
|
def distillery_type_indexing(self):
|
||||||
return self.tags.filter(category__index_name='distillery_type')
|
return self.tags.filter(category__index_name='distillery_type')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def food_producer_indexing(self):
|
||||||
|
return self.tags.filter(category__index_name='producer_type')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def last_comment(self):
|
def last_comment(self):
|
||||||
if hasattr(self, 'comments_prefetched') and len(self.comments_prefetched):
|
if hasattr(self, 'comments_prefetched') and len(self.comments_prefetched):
|
||||||
|
|
@ -937,7 +934,7 @@ class EstablishmentNote(ProjectBaseMixin):
|
||||||
"""Note model for Establishment entity."""
|
"""Note model for Establishment entity."""
|
||||||
old_id = models.PositiveIntegerField(null=True, blank=True)
|
old_id = models.PositiveIntegerField(null=True, blank=True)
|
||||||
text = models.TextField(verbose_name=_('text'))
|
text = models.TextField(verbose_name=_('text'))
|
||||||
establishment = models.ForeignKey(Establishment, on_delete=models.PROTECT,
|
establishment = models.ForeignKey(Establishment, on_delete=models.CASCADE,
|
||||||
related_name='notes',
|
related_name='notes',
|
||||||
verbose_name=_('establishment'))
|
verbose_name=_('establishment'))
|
||||||
user = models.ForeignKey('account.User', on_delete=models.PROTECT,
|
user = models.ForeignKey('account.User', on_delete=models.PROTECT,
|
||||||
|
|
@ -973,10 +970,12 @@ class EstablishmentGallery(IntermediateGalleryModelMixin):
|
||||||
class PositionQuerySet(models.QuerySet):
|
class PositionQuerySet(models.QuerySet):
|
||||||
|
|
||||||
def by_establishment_type(self, value: str):
|
def by_establishment_type(self, value: str):
|
||||||
return self.filter(establishment_type__index_name=value)
|
return self.filter(Q(establishment_type__index_name=value) |
|
||||||
|
Q(establishment_type__isnull=True, establishment_subtype__isnull=True))
|
||||||
|
|
||||||
def by_establishment_subtype(self, value: str):
|
def by_establishment_subtypes(self, value: List[str]):
|
||||||
return self.filter(establishment_subtype__index_name=value)
|
return self.filter(Q(establishment_subtype__index_name__in=value) |
|
||||||
|
Q(establishment_type__isnull=True, establishment_subtype__isnull=True))
|
||||||
|
|
||||||
|
|
||||||
class Position(BaseAttributes, TranslatedFieldsMixin):
|
class Position(BaseAttributes, TranslatedFieldsMixin):
|
||||||
|
|
@ -1150,7 +1149,7 @@ class EmployeeQuerySet(models.QuerySet):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Employee(PhoneModelMixin, BaseAttributes):
|
class Employee(PhoneModelMixin, AwardsModelMixin, BaseAttributes):
|
||||||
"""Employee model."""
|
"""Employee model."""
|
||||||
|
|
||||||
user = models.OneToOneField('account.User', on_delete=models.PROTECT,
|
user = models.OneToOneField('account.User', on_delete=models.PROTECT,
|
||||||
|
|
@ -1226,11 +1225,6 @@ class Employee(PhoneModelMixin, BaseAttributes):
|
||||||
)
|
)
|
||||||
return image_property
|
return image_property
|
||||||
|
|
||||||
def remove_award(self, award_id: int):
|
|
||||||
from main.models import Award
|
|
||||||
award = get_object_or_404(Award, pk=award_id)
|
|
||||||
self.awards.remove(award)
|
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentScheduleQuerySet(models.QuerySet):
|
class EstablishmentScheduleQuerySet(models.QuerySet):
|
||||||
"""QuerySet for model EstablishmentSchedule"""
|
"""QuerySet for model EstablishmentSchedule"""
|
||||||
|
|
@ -1547,7 +1541,7 @@ class CompanyQuerySet(models.QuerySet):
|
||||||
class Company(ProjectBaseMixin):
|
class Company(ProjectBaseMixin):
|
||||||
"""Establishment company model."""
|
"""Establishment company model."""
|
||||||
|
|
||||||
establishment = models.ForeignKey(Establishment, on_delete=models.PROTECT,
|
establishment = models.ForeignKey(Establishment, on_delete=models.CASCADE,
|
||||||
related_name='companies',
|
related_name='companies',
|
||||||
verbose_name=_('establishment'))
|
verbose_name=_('establishment'))
|
||||||
name = models.CharField(max_length=255, verbose_name=_('name'))
|
name = models.CharField(max_length=255, verbose_name=_('name'))
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,11 @@ from functools import lru_cache
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from phonenumber_field.serializerfields import PhoneNumberField
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from slugify import slugify
|
||||||
|
|
||||||
|
from account import models as account_models
|
||||||
from account.serializers.common import UserShortSerializer
|
from account.serializers.common import UserShortSerializer
|
||||||
from collection.models import Guide
|
from collection.models import Guide
|
||||||
from establishment import models, serializers as model_serializers
|
from establishment import models, serializers as model_serializers
|
||||||
|
|
@ -15,8 +18,11 @@ from location.serializers import AddressDetailSerializer, TranslatedField
|
||||||
from main.models import Currency
|
from main.models import Currency
|
||||||
from main.serializers import AwardSerializer
|
from main.serializers import AwardSerializer
|
||||||
from utils.decorators import with_base_attributes
|
from utils.decorators import with_base_attributes
|
||||||
|
from utils.methods import string_random
|
||||||
from utils.serializers import ImageBaseSerializer, ProjectModelSerializer, TimeZoneChoiceField, \
|
from utils.serializers import ImageBaseSerializer, ProjectModelSerializer, TimeZoneChoiceField, \
|
||||||
PhoneMixinSerializer
|
PhoneMixinSerializer
|
||||||
|
from main import models as main_models
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
|
|
||||||
def phones_handler(phones_list, establishment):
|
def phones_handler(phones_list, establishment):
|
||||||
|
|
@ -68,7 +74,7 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria
|
||||||
source='contact_phones',
|
source='contact_phones',
|
||||||
allow_null=True,
|
allow_null=True,
|
||||||
allow_empty=True,
|
allow_empty=True,
|
||||||
child=serializers.CharField(max_length=128),
|
child=PhoneNumberField(),
|
||||||
required=False,
|
required=False,
|
||||||
write_only=True,
|
write_only=True,
|
||||||
)
|
)
|
||||||
|
|
@ -80,6 +86,8 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria
|
||||||
child=serializers.CharField(max_length=128),
|
child=serializers.CharField(max_length=128),
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
|
subtypes = model_serializers.EstablishmentSubTypeBaseSerializer(source='establishment_subtypes',
|
||||||
|
read_only=True, many=True)
|
||||||
|
|
||||||
class Meta(model_serializers.EstablishmentBaseSerializer.Meta):
|
class Meta(model_serializers.EstablishmentBaseSerializer.Meta):
|
||||||
fields = [
|
fields = [
|
||||||
|
|
@ -95,6 +103,7 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria
|
||||||
'toque_number',
|
'toque_number',
|
||||||
'type_id',
|
'type_id',
|
||||||
'type',
|
'type',
|
||||||
|
'subtypes',
|
||||||
'socials',
|
'socials',
|
||||||
'image_url',
|
'image_url',
|
||||||
'slug',
|
'slug',
|
||||||
|
|
@ -126,6 +135,19 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria
|
||||||
if 'contact_emails' in validated_data:
|
if 'contact_emails' in validated_data:
|
||||||
emails_list = validated_data.pop('contact_emails')
|
emails_list = validated_data.pop('contact_emails')
|
||||||
|
|
||||||
|
index_name = validated_data.get('index_name')
|
||||||
|
if 'slug' not in validated_data and index_name:
|
||||||
|
slug = slugify(
|
||||||
|
index_name,
|
||||||
|
word_boundary=True
|
||||||
|
)
|
||||||
|
while models.Establishment.objects.filter(slug=slug).exists():
|
||||||
|
slug = slugify(
|
||||||
|
f'{index_name} {string_random()}',
|
||||||
|
word_boundary=True
|
||||||
|
)
|
||||||
|
validated_data['slug'] = slug
|
||||||
|
|
||||||
instance = super().create(validated_data)
|
instance = super().create(validated_data)
|
||||||
phones_handler(phones_list, instance)
|
phones_handler(phones_list, instance)
|
||||||
emails_handler(emails_list, instance)
|
emails_handler(emails_list, instance)
|
||||||
|
|
@ -165,7 +187,15 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer):
|
||||||
subtypes = model_serializers.EstablishmentSubTypeBaseSerializer(source='establishment_subtypes',
|
subtypes = model_serializers.EstablishmentSubTypeBaseSerializer(source='establishment_subtypes',
|
||||||
read_only=True, many=True)
|
read_only=True, many=True)
|
||||||
type = model_serializers.EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True)
|
type = model_serializers.EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True)
|
||||||
phones = ContactPhonesSerializer(read_only=True, many=True)
|
phones = serializers.ListField(
|
||||||
|
source='contact_phones',
|
||||||
|
allow_null=True,
|
||||||
|
allow_empty=True,
|
||||||
|
child=PhoneNumberField(),
|
||||||
|
required=False,
|
||||||
|
write_only=True,
|
||||||
|
)
|
||||||
|
contact_phones = ContactPhonesSerializer(source='phones', read_only=True, many=True)
|
||||||
|
|
||||||
class Meta(model_serializers.EstablishmentBaseSerializer.Meta):
|
class Meta(model_serializers.EstablishmentBaseSerializer.Meta):
|
||||||
fields = [
|
fields = [
|
||||||
|
|
@ -176,6 +206,7 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer):
|
||||||
'index_name',
|
'index_name',
|
||||||
'website',
|
'website',
|
||||||
'phones',
|
'phones',
|
||||||
|
'contact_phones',
|
||||||
'emails',
|
'emails',
|
||||||
'price_level',
|
'price_level',
|
||||||
'toque_number',
|
'toque_number',
|
||||||
|
|
@ -196,6 +227,11 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer):
|
||||||
'status_display',
|
'status_display',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def to_representation(self, instance):
|
||||||
|
data = super(EstablishmentRUDSerializer, self).to_representation(instance)
|
||||||
|
data['phones'] = data.pop('contact_phones', None)
|
||||||
|
return data
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
phones_list = []
|
phones_list = []
|
||||||
if 'contact_phones' in validated_data:
|
if 'contact_phones' in validated_data:
|
||||||
|
|
@ -397,7 +433,8 @@ class EmployeeBackSerializers(PhoneMixinSerializer, serializers.ModelSerializer)
|
||||||
'photo_id',
|
'photo_id',
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'phone': {'write_only': True}
|
'phone': {'write_only': True},
|
||||||
|
'available_for_events': {'required': False}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -891,7 +928,7 @@ class CardAndWinesPlatesSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
|
||||||
class CardAndWinesSerializer(serializers.ModelSerializer):
|
class CardAndWinesSerializer(serializers.ModelSerializer):
|
||||||
"""View to show menus(not formulas) with dishes for certain establishment"""
|
"""Serializer to show menus(not formulas) with dishes for certain establishment"""
|
||||||
|
|
||||||
wine = EstablishmentBackOfficeWineSerializer(allow_null=True, read_only=True, source='back_office_wine')
|
wine = EstablishmentBackOfficeWineSerializer(allow_null=True, read_only=True, source='back_office_wine')
|
||||||
dishes = CardAndWinesPlatesSerializer(many=True, source='card_and_wine_plates')
|
dishes = CardAndWinesPlatesSerializer(many=True, source='card_and_wine_plates')
|
||||||
|
|
@ -902,3 +939,35 @@ class CardAndWinesSerializer(serializers.ModelSerializer):
|
||||||
'wine',
|
'wine',
|
||||||
'dishes',
|
'dishes',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TeamMemberSerializer(serializers.ModelSerializer):
|
||||||
|
"""Serializer for team establishment BO section"""
|
||||||
|
class Meta:
|
||||||
|
model = account_models.User
|
||||||
|
fields = (
|
||||||
|
'id',
|
||||||
|
'username',
|
||||||
|
'email',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BackEstablishmentAwardCreateSerializer(serializers.ModelSerializer):
|
||||||
|
"""Award, The Creator."""
|
||||||
|
award_type = serializers.PrimaryKeyRelatedField(required=True, queryset=main_models.AwardType.objects.all())
|
||||||
|
title = serializers.CharField(write_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Award
|
||||||
|
fields = (
|
||||||
|
'id',
|
||||||
|
'award_type',
|
||||||
|
'title',
|
||||||
|
'vintage_year',
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
attrs['object_id'] = self.context.get('request').parser_context.get('kwargs')['establishment_id']
|
||||||
|
attrs['content_type'] = ContentType.objects.get_for_model(models.Establishment)
|
||||||
|
attrs['title'] = {self.context.get('request').locale: attrs['title']}
|
||||||
|
return attrs
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ from location.serializers import (
|
||||||
EstablishmentWineRegionBaseSerializer,
|
EstablishmentWineRegionBaseSerializer,
|
||||||
)
|
)
|
||||||
from main.serializers import AwardSerializer, CurrencySerializer
|
from main.serializers import AwardSerializer, CurrencySerializer
|
||||||
from review.serializers import ReviewShortSerializer
|
from review.serializers import ReviewShortSerializer, ReviewBaseSerializer
|
||||||
from tag.serializers import TagBaseSerializer
|
from tag.serializers import TagBaseSerializer
|
||||||
from timetable.serialziers import ScheduleRUDSerializer
|
from timetable.serialziers import ScheduleRUDSerializer
|
||||||
from utils import exceptions as utils_exceptions
|
from utils import exceptions as utils_exceptions
|
||||||
|
|
@ -408,6 +408,8 @@ class EstablishmentListRetrieveSerializer(EstablishmentBaseSerializer):
|
||||||
restaurant_cuisine = TagBaseSerializer(read_only=True, many=True, allow_null=True)
|
restaurant_cuisine = TagBaseSerializer(read_only=True, many=True, allow_null=True)
|
||||||
artisan_category = TagBaseSerializer(read_only=True, many=True, allow_null=True)
|
artisan_category = TagBaseSerializer(read_only=True, many=True, allow_null=True)
|
||||||
distillery_type = TagBaseSerializer(read_only=True, many=True, allow_null=True)
|
distillery_type = TagBaseSerializer(read_only=True, many=True, allow_null=True)
|
||||||
|
food_producer = TagBaseSerializer(read_only=True, many=True, allow_null=True)
|
||||||
|
reviews = ReviewBaseSerializer(read_only=True, many=True)
|
||||||
|
|
||||||
class Meta(EstablishmentBaseSerializer.Meta):
|
class Meta(EstablishmentBaseSerializer.Meta):
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -418,6 +420,11 @@ class EstablishmentListRetrieveSerializer(EstablishmentBaseSerializer):
|
||||||
'restaurant_cuisine',
|
'restaurant_cuisine',
|
||||||
'artisan_category',
|
'artisan_category',
|
||||||
'distillery_type',
|
'distillery_type',
|
||||||
|
'food_producer',
|
||||||
|
'vintage_year',
|
||||||
|
'reviews',
|
||||||
|
'contact_phones',
|
||||||
|
'public_mark'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -706,3 +713,8 @@ class EstablishmentGuideElementSerializer(serializers.ModelSerializer):
|
||||||
'range_price_carte',
|
'range_price_carte',
|
||||||
'currency',
|
'currency',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentStatusesSerializer(serializers.Serializer):
|
||||||
|
value = serializers.IntegerField()
|
||||||
|
state_translated = serializers.CharField()
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,16 @@ urlpatterns = [
|
||||||
name='employee-positions-list'),
|
name='employee-positions-list'),
|
||||||
path('employee_establishments/<int:pk>/', views.EmployeeEstablishmentsListView.as_view(),
|
path('employee_establishments/<int:pk>/', views.EmployeeEstablishmentsListView.as_view(),
|
||||||
name='employee-establishments-list'),
|
name='employee-establishments-list'),
|
||||||
|
path('available_statuses/', views.StatusesListView.as_view(), name='statuses-list'),
|
||||||
path('employee_establishment_positions/<int:pk>/', views.EmployeeEstablishmentPositionsView.as_view(),
|
path('employee_establishment_positions/<int:pk>/', views.EmployeeEstablishmentPositionsView.as_view(),
|
||||||
name='employee-establishment-positions')
|
name='employee-establishment-positions'),
|
||||||
|
path('team/<int:establishment_id>', views.TeamMemberListView.as_view(), name='establishment-team-members-list'),
|
||||||
|
path('team/<int:establishment_id>/<int:user_id>', views.TeamMemberDeleteView.as_view(),
|
||||||
|
name='establishment-team-member-delete'),
|
||||||
|
path('awards/create-and-bind/<int:establishment_id>/',
|
||||||
|
views.EstablishmentAwardCreateAndBind.as_view(),
|
||||||
|
name='establishment-awards-create-and-bind'),
|
||||||
|
path('awards/create-and-bind/<int:establishment_id>/<int:award_id>/',
|
||||||
|
views.EstablishmentAwardCreateAndBind.as_view(),
|
||||||
|
name='establishment-awards-create-and-bind',)
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
"""Establishment app views."""
|
"""Establishment app views."""
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models.query_utils import Q
|
from django.db.models.query_utils import Q
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
|
|
@ -7,7 +9,7 @@ from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from rest_framework import generics, response, status
|
from rest_framework import generics, response, status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from account.models import User
|
from account.models import User, Role, UserRole
|
||||||
from collection.models import Guide
|
from collection.models import Guide
|
||||||
from establishment import filters, models, serializers
|
from establishment import filters, models, serializers
|
||||||
from establishment.models import EstablishmentEmployee, Menu
|
from establishment.models import EstablishmentEmployee, Menu
|
||||||
|
|
@ -16,6 +18,8 @@ from timetable.serialziers import ScheduleCreateSerializer, ScheduleRUDSerialize
|
||||||
from utils.methods import get_permission_classes
|
from utils.methods import get_permission_classes
|
||||||
from utils.permissions import (IsEstablishmentAdministrator, IsEstablishmentManager)
|
from utils.permissions import (IsEstablishmentAdministrator, IsEstablishmentManager)
|
||||||
from utils.views import CreateDestroyGalleryViewMixin
|
from utils.views import CreateDestroyGalleryViewMixin
|
||||||
|
from main import models as main_models
|
||||||
|
from main import serializers as main_serializers
|
||||||
|
|
||||||
|
|
||||||
class MenuRUDMixinViews:
|
class MenuRUDMixinViews:
|
||||||
|
|
@ -795,7 +799,7 @@ class EstablishmentGuideCreateDestroyView(generics.GenericAPIView):
|
||||||
# Perform the lookup filtering.
|
# Perform the lookup filtering.
|
||||||
lookup_url_kwarg = getattr(self, 'establishment_lookup_url_kwarg', None)
|
lookup_url_kwarg = getattr(self, 'establishment_lookup_url_kwarg', None)
|
||||||
|
|
||||||
assert lookup_url_kwarg and lookup_url_kwarg in self.kwargs, (
|
assert lookup_url_kwarg in self.kwargs, (
|
||||||
'Expected view %s to be called with a URL keyword argument '
|
'Expected view %s to be called with a URL keyword argument '
|
||||||
'named "%s". Fix your URL conf, or set the `.lookup_field` '
|
'named "%s". Fix your URL conf, or set the `.lookup_field` '
|
||||||
'attribute on the view correctly.' %
|
'attribute on the view correctly.' %
|
||||||
|
|
@ -816,7 +820,7 @@ class EstablishmentGuideCreateDestroyView(generics.GenericAPIView):
|
||||||
# Perform the lookup filtering.
|
# Perform the lookup filtering.
|
||||||
lookup_url_kwarg = getattr(self, 'guide_lookup_url_kwarg', None)
|
lookup_url_kwarg = getattr(self, 'guide_lookup_url_kwarg', None)
|
||||||
|
|
||||||
assert lookup_url_kwarg and lookup_url_kwarg in self.kwargs, (
|
assert lookup_url_kwarg in self.kwargs, (
|
||||||
'Expected view %s to be called with a URL keyword argument '
|
'Expected view %s to be called with a URL keyword argument '
|
||||||
'named "%s". Fix your URL conf, or set the `.lookup_field` '
|
'named "%s". Fix your URL conf, or set the `.lookup_field` '
|
||||||
'attribute on the view correctly.' %
|
'attribute on the view correctly.' %
|
||||||
|
|
@ -923,3 +927,82 @@ class MenuGalleryCreateDestroyView(CreateDestroyGalleryViewMixin):
|
||||||
self.check_object_permissions(self.request, gallery)
|
self.check_object_permissions(self.request, gallery)
|
||||||
|
|
||||||
return gallery
|
return gallery
|
||||||
|
|
||||||
|
|
||||||
|
class StatusesListView(generics.ListAPIView):
|
||||||
|
"""Possible project establishment statuses"""
|
||||||
|
pagination_class = None
|
||||||
|
serializer_class = serializers.EstablishmentStatusesSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
mutated_for_serializer = [{
|
||||||
|
'value': state[0],
|
||||||
|
'state_translated': state[1],
|
||||||
|
} for state in models.Establishment.STATUS_CHOICES]
|
||||||
|
serializer = self.get_serializer(mutated_for_serializer, many=True)
|
||||||
|
return response.Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
class TeamMemberListView(generics.ListAPIView):
|
||||||
|
"""Show team for certain establishment"""
|
||||||
|
pagination_class = None
|
||||||
|
serializer_class = serializers.TeamMemberSerializer
|
||||||
|
queryset = User.objects.all()
|
||||||
|
permission_classes = get_permission_classes(
|
||||||
|
IsEstablishmentManager,
|
||||||
|
IsEstablishmentAdministrator
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
establishment = get_object_or_404(klass=models.Establishment, pk=self.kwargs['establishment_id'])
|
||||||
|
return super().get_queryset().filter(roles__role=Role.ESTABLISHMENT_ADMINISTRATOR,
|
||||||
|
userrole__establishment=establishment, userrole__state=UserRole.VALIDATED)
|
||||||
|
|
||||||
|
|
||||||
|
class TeamMemberDeleteView(generics.DestroyAPIView):
|
||||||
|
"""Delete user from team"""
|
||||||
|
permission_classes = get_permission_classes(
|
||||||
|
IsEstablishmentManager,
|
||||||
|
IsEstablishmentAdministrator
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
return UserRole.objects.get(role__role=Role.ESTABLISHMENT_ADMINISTRATOR, user_id=self.kwargs['user_id'],
|
||||||
|
establishment_id=self.kwargs['establishment_id'])
|
||||||
|
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
instance.delete()
|
||||||
|
from account.tasks import team_role_revoked
|
||||||
|
establishment = models.Establishment.objects.get(pk=self.kwargs['establishment_id'])
|
||||||
|
if settings.USE_CELERY:
|
||||||
|
team_role_revoked.delay(self.kwargs['user_id'], self.request.country_code, establishment.name)
|
||||||
|
else:
|
||||||
|
team_role_revoked(self.kwargs['user_id'], self.request.country_code, establishment.name)
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentAwardCreateAndBind(generics.CreateAPIView, generics.DestroyAPIView):
|
||||||
|
queryset = main_models.Award.objects.with_base_related().all()
|
||||||
|
permission_classes = get_permission_classes()
|
||||||
|
serializer_class = serializers.BackEstablishmentAwardCreateSerializer
|
||||||
|
|
||||||
|
def _award_list_for_establishment(self, establishment_id: int, status: int) -> Response:
|
||||||
|
awards = main_models.Award.objects.with_base_related().filter(
|
||||||
|
object_id=establishment_id,
|
||||||
|
content_type=ContentType.objects.get_for_model(models.Establishment)
|
||||||
|
)
|
||||||
|
response_serializer = main_serializers.AwardBaseSerializer(awards, many=True)
|
||||||
|
headers = self.get_success_headers(response_serializer.data)
|
||||||
|
return Response(response_serializer.data, status=status, headers=headers)
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
"""Overridden create method."""
|
||||||
|
super().create(request, args, kwargs)
|
||||||
|
return self._award_list_for_establishment(kwargs['establishment_id'], status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
def delete(self, request, *args, **kwargs):
|
||||||
|
establishment = get_object_or_404(models.Establishment, id=kwargs['establishment_id'])
|
||||||
|
establishment.remove_award(kwargs['award_id'])
|
||||||
|
return self._award_list_for_establishment(kwargs['establishment_id'], status.HTTP_200_OK)
|
||||||
|
|
@ -33,12 +33,13 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
|
||||||
serializer_class = serializers.EstablishmentListRetrieveSerializer
|
serializer_class = serializers.EstablishmentListRetrieveSerializer
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return super().get_queryset().with_schedule() \
|
return super().get_queryset().with_schedule().with_reviews() \
|
||||||
.with_extended_address_related().with_currency_related() \
|
.with_extended_address_related().with_currency_related() \
|
||||||
.with_certain_tag_category_related('category', 'restaurant_category') \
|
.with_certain_tag_category_related('category', 'restaurant_category') \
|
||||||
.with_certain_tag_category_related('cuisine', 'restaurant_cuisine') \
|
.with_certain_tag_category_related('cuisine', 'restaurant_cuisine') \
|
||||||
.with_certain_tag_category_related('shop_category', 'artisan_category') \
|
.with_certain_tag_category_related('shop_category', 'artisan_category') \
|
||||||
.with_certain_tag_category_related('distillery_type', 'distillery_type')
|
.with_certain_tag_category_related('distillery_type', 'distillery_type') \
|
||||||
|
.with_certain_tag_category_related('food_producer', 'producer_type')
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentSimilarView(EstablishmentListView):
|
class EstablishmentSimilarView(EstablishmentListView):
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,22 @@ class CountryQuerySet(models.QuerySet):
|
||||||
"""Filter only active users."""
|
"""Filter only active users."""
|
||||||
return self.filter(is_active=switcher)
|
return self.filter(is_active=switcher)
|
||||||
|
|
||||||
|
def by_country_code(self, code: str):
|
||||||
|
"""Filter QuerySet by country code."""
|
||||||
|
return self.filter(code__iexact=code)
|
||||||
|
|
||||||
|
def aggregate_country_codes(self):
|
||||||
|
"""Aggregate country codes."""
|
||||||
|
calling_codes = list(
|
||||||
|
self.model.objects.exclude(calling_code__isnull=True)
|
||||||
|
.exclude(code__iexact='aa')
|
||||||
|
.distinct()
|
||||||
|
.values_list('calling_code', flat=True)
|
||||||
|
)
|
||||||
|
# extend country calling code hardcoded codes
|
||||||
|
calling_codes.extend(settings.CALLING_CODES_ANTILLES_GUYANE_WEST_INDIES)
|
||||||
|
return [self.model.CALLING_NUMBER_MASK % i for i in calling_codes]
|
||||||
|
|
||||||
|
|
||||||
class Country(TranslatedFieldsMixin,
|
class Country(TranslatedFieldsMixin,
|
||||||
SVGImageMixin, ProjectBaseMixin):
|
SVGImageMixin, ProjectBaseMixin):
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -144,8 +144,8 @@ def transfer_addresses():
|
||||||
def transfer_wine_region():
|
def transfer_wine_region():
|
||||||
queryset = transfer_models.WineLocations.objects.filter(type='WineRegion')
|
queryset = transfer_models.WineLocations.objects.filter(type='WineRegion')
|
||||||
serialized_data = location_serializers.WineRegionSerializer(
|
serialized_data = location_serializers.WineRegionSerializer(
|
||||||
data=list(queryset.values()),
|
data=list(queryset.values()),
|
||||||
many=True)
|
many=True)
|
||||||
if serialized_data.is_valid():
|
if serialized_data.is_valid():
|
||||||
serialized_data.save()
|
serialized_data.save()
|
||||||
else:
|
else:
|
||||||
|
|
@ -155,8 +155,8 @@ def transfer_wine_region():
|
||||||
def transfer_wine_sub_region():
|
def transfer_wine_sub_region():
|
||||||
queryset = transfer_models.WineLocations.objects.filter(type='WineSubRegion')
|
queryset = transfer_models.WineLocations.objects.filter(type='WineSubRegion')
|
||||||
serialized_data = location_serializers.WineSubRegionSerializer(
|
serialized_data = location_serializers.WineSubRegionSerializer(
|
||||||
data=list(queryset.values()),
|
data=list(queryset.values()),
|
||||||
many=True)
|
many=True)
|
||||||
if serialized_data.is_valid():
|
if serialized_data.is_valid():
|
||||||
serialized_data.save()
|
serialized_data.save()
|
||||||
else:
|
else:
|
||||||
|
|
@ -166,8 +166,8 @@ def transfer_wine_sub_region():
|
||||||
def transfer_wine_village():
|
def transfer_wine_village():
|
||||||
queryset = transfer_models.WineLocations.objects.filter(type='Village')
|
queryset = transfer_models.WineLocations.objects.filter(type='Village')
|
||||||
serialized_data = location_serializers.WineVillageSerializer(
|
serialized_data = location_serializers.WineVillageSerializer(
|
||||||
data=list(queryset.values()),
|
data=list(queryset.values()),
|
||||||
many=True)
|
many=True)
|
||||||
if serialized_data.is_valid():
|
if serialized_data.is_valid():
|
||||||
serialized_data.save()
|
serialized_data.save()
|
||||||
else:
|
else:
|
||||||
|
|
@ -247,19 +247,19 @@ def get_ruby_data():
|
||||||
|
|
||||||
ruby_params = {}
|
ruby_params = {}
|
||||||
|
|
||||||
if mysql_city.country_code is not None:
|
if mysql_city.country_code:
|
||||||
ruby_params['country_code'] = mysql_city.country_code
|
ruby_params['country_code'] = mysql_city.country_code
|
||||||
|
|
||||||
if mysql_city.country_code_2 is not None:
|
if mysql_city.country_code_2:
|
||||||
ruby_params['country_code_2'] = mysql_city.country_code_2
|
ruby_params['country_code_2'] = mysql_city.country_code_2
|
||||||
|
|
||||||
if mysql_city.region_code is not None:
|
if mysql_city.region_code:
|
||||||
ruby_params['region_code'] = mysql_city.region_code
|
ruby_params['region_code'] = mysql_city.region_code
|
||||||
|
|
||||||
if mysql_city.subregion_code is not None:
|
if mysql_city.subregion_code:
|
||||||
ruby_params['subregion_code'] = mysql_city.subregion_code
|
ruby_params['subregion_code'] = mysql_city.subregion_code
|
||||||
|
|
||||||
ruby_response = get_ruby_socket(ruby_params)
|
ruby_response = get_ruby_socket(ruby_params) if ruby_params else None
|
||||||
|
|
||||||
if ruby_response is None:
|
if ruby_response is None:
|
||||||
continue
|
continue
|
||||||
|
|
@ -332,7 +332,7 @@ def get_unused_data():
|
||||||
# Add correct objects of Country, Region and City with mysql_ids array (Country, Region) and mysql_id (City)
|
# Add correct objects of Country, Region and City with mysql_ids array (Country, Region) and mysql_id (City)
|
||||||
def add_correct_location_models(ruby_data):
|
def add_correct_location_models(ruby_data):
|
||||||
for mysql_id, city_object in tqdm(ruby_data.items()):
|
for mysql_id, city_object in tqdm(ruby_data.items()):
|
||||||
country_data = city_object["country"]
|
country_data = city_object['country']
|
||||||
country_code = country_data['code'] # i.e. fr
|
country_code = country_data['code'] # i.e. fr
|
||||||
try:
|
try:
|
||||||
country = Country.objects.get(
|
country = Country.objects.get(
|
||||||
|
|
@ -377,7 +377,7 @@ def add_correct_location_models(ruby_data):
|
||||||
city_object['region'] = region
|
city_object['region'] = region
|
||||||
|
|
||||||
if "subregion" in city_object:
|
if "subregion" in city_object:
|
||||||
del(city_object['subregion'])
|
del (city_object['subregion'])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
City.objects.create(**city_object)
|
City.objects.create(**city_object)
|
||||||
|
|
@ -404,7 +404,7 @@ def fix_location_collection():
|
||||||
collections = Collection.objects.filter(old_id__isnull=False)
|
collections = Collection.objects.filter(old_id__isnull=False)
|
||||||
for collection in collections:
|
for collection in collections:
|
||||||
try:
|
try:
|
||||||
mysql_collection = transfer_models.Collections.objects.\
|
mysql_collection = transfer_models.Collections.objects. \
|
||||||
raw(f"""select
|
raw(f"""select
|
||||||
s.country_code_2,
|
s.country_code_2,
|
||||||
c.id
|
c.id
|
||||||
|
|
@ -425,7 +425,7 @@ def fix_location_collection():
|
||||||
def fix_award_type():
|
def fix_award_type():
|
||||||
award_types = AwardType.objects.filter(old_id__isnull=False)
|
award_types = AwardType.objects.filter(old_id__isnull=False)
|
||||||
for award_type in award_types:
|
for award_type in award_types:
|
||||||
mysql_award_type = transfer_models.AwardTypes.objects.\
|
mysql_award_type = transfer_models.AwardTypes.objects. \
|
||||||
raw(f"""SELECT at.id, s.country_code_2 AS country_code
|
raw(f"""SELECT at.id, s.country_code_2 AS country_code
|
||||||
FROM award_types as at
|
FROM award_types as at
|
||||||
JOIN sites s on s.id = at.site_id
|
JOIN sites s on s.id = at.site_id
|
||||||
|
|
@ -501,7 +501,6 @@ def fix_chosen_tag():
|
||||||
|
|
||||||
|
|
||||||
def fix_location_models():
|
def fix_location_models():
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ruby_data_file = open(f"{settings.PROJECT_ROOT}/apps/location/ruby_data.py", "r")
|
ruby_data_file = open(f"{settings.PROJECT_ROOT}/apps/location/ruby_data.py", "r")
|
||||||
ruby_data = json.loads(ruby_data_file.read())
|
ruby_data = json.loads(ruby_data_file.read())
|
||||||
|
|
@ -541,11 +540,11 @@ def transfer_city_photos():
|
||||||
cities_has_same_image = 0
|
cities_has_same_image = 0
|
||||||
|
|
||||||
city_gallery = transfer_models.CityPhotos.objects.exclude(city__isnull=True) \
|
city_gallery = transfer_models.CityPhotos.objects.exclude(city__isnull=True) \
|
||||||
.exclude(city__country_code_2__isnull=True) \
|
.exclude(city__country_code_2__isnull=True) \
|
||||||
.exclude(city__country_code_2__iexact='') \
|
.exclude(city__country_code_2__iexact='') \
|
||||||
.exclude(city__region_code__isnull=True) \
|
.exclude(city__region_code__isnull=True) \
|
||||||
.exclude(city__region_code__iexact='') \
|
.exclude(city__region_code__iexact='') \
|
||||||
.values_list('city_id', 'attachment_suffix_url')
|
.values_list('city_id', 'attachment_suffix_url')
|
||||||
for old_city_id, image_suffix_url in tqdm(city_gallery):
|
for old_city_id, image_suffix_url in tqdm(city_gallery):
|
||||||
city = City.objects.filter(old_id=old_city_id)
|
city = City.objects.filter(old_id=old_city_id)
|
||||||
if city.exists():
|
if city.exists():
|
||||||
|
|
@ -639,7 +638,7 @@ def add_fake_country():
|
||||||
for region_data in regions_data:
|
for region_data in regions_data:
|
||||||
if "subregions" in region_data:
|
if "subregions" in region_data:
|
||||||
subregions = region_data['subregions']
|
subregions = region_data['subregions']
|
||||||
del(region_data['subregions'])
|
del (region_data['subregions'])
|
||||||
else:
|
else:
|
||||||
subregions = False
|
subregions = False
|
||||||
|
|
||||||
|
|
@ -665,16 +664,16 @@ def add_fake_country():
|
||||||
file = open(f"{settings.PROJECT_ROOT}/apps/location/csv/aa_cities.csv")
|
file = open(f"{settings.PROJECT_ROOT}/apps/location/csv/aa_cities.csv")
|
||||||
reader = csv.DictReader(file, delimiter=',')
|
reader = csv.DictReader(file, delimiter=',')
|
||||||
for city_data in reader:
|
for city_data in reader:
|
||||||
del(city_data[''])
|
del (city_data[''])
|
||||||
|
|
||||||
city_data['mysql_id'] = city_data['old_id']
|
city_data['mysql_id'] = city_data['old_id']
|
||||||
del(city_data['old_id'])
|
del (city_data['old_id'])
|
||||||
|
|
||||||
city_data['postal_code'] = city_data['zip_code']
|
city_data['postal_code'] = city_data['zip_code']
|
||||||
del(city_data['zip_code'])
|
del (city_data['zip_code'])
|
||||||
|
|
||||||
if city_data['postal_code'] == 'null' or city_data['postal_code'] == '':
|
if city_data['postal_code'] == 'null' or city_data['postal_code'] == '':
|
||||||
del(city_data['postal_code'])
|
del (city_data['postal_code'])
|
||||||
|
|
||||||
city_data["country"] = country
|
city_data["country"] = country
|
||||||
|
|
||||||
|
|
@ -683,10 +682,10 @@ def add_fake_country():
|
||||||
elif city_data['region'] != 'null':
|
elif city_data['region'] != 'null':
|
||||||
region = regions[city_data['region']]
|
region = regions[city_data['region']]
|
||||||
else:
|
else:
|
||||||
del(city_data['region'])
|
del (city_data['region'])
|
||||||
region = None
|
region = None
|
||||||
|
|
||||||
del(city_data['subregion'])
|
del (city_data['subregion'])
|
||||||
|
|
||||||
city_data['region'] = region
|
city_data['region'] = region
|
||||||
|
|
||||||
|
|
@ -700,7 +699,7 @@ def add_fake_country():
|
||||||
country.mysql_ids.append(city_data['mysql_id'])
|
country.mysql_ids.append(city_data['mysql_id'])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
mysql_data = transfer_models.Cities.objects.\
|
mysql_data = transfer_models.Cities.objects. \
|
||||||
only("map1", "map2", "map_ref", "situation").get(id=city_data['mysql_id'])
|
only("map1", "map2", "map_ref", "situation").get(id=city_data['mysql_id'])
|
||||||
city_data['map1'] = mysql_data.map1
|
city_data['map1'] = mysql_data.map1
|
||||||
city_data['map2'] = mysql_data.map2
|
city_data['map2'] = mysql_data.map2
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ urlpatterns = [
|
||||||
path('cities/<int:pk>/', views.CityRUDView.as_view(), name='city-retrieve'),
|
path('cities/<int:pk>/', views.CityRUDView.as_view(), name='city-retrieve'),
|
||||||
|
|
||||||
path('countries/', views.CountryListCreateView.as_view(), name='country-list-create'),
|
path('countries/', views.CountryListCreateView.as_view(), name='country-list-create'),
|
||||||
|
path('countries/calling-codes/', views.CountryCallingCodeListView.as_view(),
|
||||||
|
name='country-calling-code-list'),
|
||||||
path('countries/<int:pk>/', views.CountryRUDView.as_view(), name='country-retrieve'),
|
path('countries/<int:pk>/', views.CountryRUDView.as_view(), name='country-retrieve'),
|
||||||
|
|
||||||
path('regions/', views.RegionListCreateView.as_view(), name='region-list-create'),
|
path('regions/', views.RegionListCreateView.as_view(), name='region-list-create'),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
"""Location app views."""
|
"""Location app views."""
|
||||||
from django.contrib.postgres.fields.jsonb import KeyTextTransform
|
from django.contrib.postgres.fields.jsonb import KeyTextTransform
|
||||||
from rest_framework import generics
|
from rest_framework import generics, status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from location import filters
|
from location import filters
|
||||||
from location import models, serializers
|
from location import models, serializers
|
||||||
|
|
@ -107,26 +109,63 @@ class RegionRUDView(common.RegionViewMixin, generics.RetrieveUpdateDestroyAPIVie
|
||||||
|
|
||||||
|
|
||||||
# Country
|
# Country
|
||||||
class CountryListCreateView(generics.ListCreateAPIView):
|
class CountryBaseViewMixin:
|
||||||
|
"""Mixin for Country views."""
|
||||||
|
queryset = models.Country.objects.all()
|
||||||
|
permission_classes = get_permission_classes(
|
||||||
|
IsEstablishmentManager,
|
||||||
|
IsEstablishmentAdministrator,
|
||||||
|
IsGuest,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CountryListCreateView(CountryBaseViewMixin, generics.ListCreateAPIView):
|
||||||
"""List/Create view for model Country."""
|
"""List/Create view for model Country."""
|
||||||
queryset = models.Country.objects.all()\
|
queryset = (models.Country.objects.annotate(
|
||||||
.annotate(locale_name=KeyTextTransform(get_current_locale(), 'name'))\
|
locale_name=KeyTextTransform(get_current_locale(), 'name')).order_by('locale_name'))
|
||||||
.order_by('locale_name')
|
|
||||||
serializer_class = serializers.CountryBackSerializer
|
serializer_class = serializers.CountryBackSerializer
|
||||||
pagination_class = None
|
pagination_class = None
|
||||||
|
|
||||||
|
|
||||||
|
class CountryRUDView(CountryBaseViewMixin, generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
"""RUD view for model Country."""
|
||||||
|
serializer_class = serializers.CountryBackSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class CountryCallingCodeListView(APIView):
|
||||||
|
"""
|
||||||
|
## Country codes view.
|
||||||
|
### Response
|
||||||
|
```
|
||||||
|
[
|
||||||
|
"+7",
|
||||||
|
"+386",
|
||||||
|
"+352"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
### Description
|
||||||
|
Return an array of unique country code for all countries in a database.
|
||||||
|
"""
|
||||||
|
pagination_class = None
|
||||||
permission_classes = get_permission_classes(
|
permission_classes = get_permission_classes(
|
||||||
IsEstablishmentManager,
|
IsEstablishmentManager,
|
||||||
IsEstablishmentAdministrator,
|
IsEstablishmentAdministrator,
|
||||||
IsGuest,
|
IsGuest,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
class CountryRUDView(generics.RetrieveUpdateDestroyAPIView):
|
"""
|
||||||
"""RUD view for model Country."""
|
## Country codes view.
|
||||||
serializer_class = serializers.CountryBackSerializer
|
### Response
|
||||||
queryset = models.Country.objects.all()
|
```
|
||||||
permission_classes = get_permission_classes(
|
[
|
||||||
IsEstablishmentManager,
|
"+7",
|
||||||
IsEstablishmentAdministrator,
|
"+386",
|
||||||
IsGuest,
|
"+352"
|
||||||
)
|
]
|
||||||
|
```
|
||||||
|
### Description
|
||||||
|
Return an array of unique country code for all countries in a database.
|
||||||
|
"""
|
||||||
|
return Response(data=models.Country.objects.aggregate_country_codes(),
|
||||||
|
status=status.HTTP_200_OK)
|
||||||
|
|
|
||||||
20
apps/main/migrations/0050_awardtype_years.py
Normal file
20
apps/main/migrations/0050_awardtype_years.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 2.2.7 on 2020-02-04 14:06
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0049_remove_navigationbarpermission_section'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='awardtype',
|
||||||
|
name='years',
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(validators=[django.core.validators.MinValueValidator(1980)]), blank=True, null=True, size=None, verbose_name='years'),
|
||||||
|
),
|
||||||
|
]
|
||||||
22
apps/main/migrations/0051_auto_20200204_1409.py
Normal file
22
apps/main/migrations/0051_auto_20200204_1409.py
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Generated by Django 2.2.7 on 2020-02-04 14:09
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def fill_award_type_ears(apps, schemaeditor):
|
||||||
|
AwardType = apps.get_model('main', 'AwardType')
|
||||||
|
for aw in AwardType.objects.all():
|
||||||
|
aw.years = list(range(1980, datetime.date.today().year+1))
|
||||||
|
aw.save()
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0050_awardtype_years'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(fill_award_type_ears),
|
||||||
|
]
|
||||||
|
|
@ -4,8 +4,8 @@ from typing import Iterable
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes import fields as generic
|
from django.contrib.contenttypes import fields as generic
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.postgres.fields import JSONField
|
from django.contrib.postgres.fields import JSONField, ArrayField
|
||||||
from django.core.validators import EMPTY_VALUES
|
from django.core.validators import EMPTY_VALUES, MinValueValidator
|
||||||
from django.db import connections
|
from django.db import connections
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
@ -230,6 +230,12 @@ class AwardType(models.Model):
|
||||||
'location.Country', verbose_name=_('country'), on_delete=models.CASCADE)
|
'location.Country', verbose_name=_('country'), on_delete=models.CASCADE)
|
||||||
name = models.CharField(_('name'), max_length=255)
|
name = models.CharField(_('name'), max_length=255)
|
||||||
old_id = models.IntegerField(null=True, blank=True)
|
old_id = models.IntegerField(null=True, blank=True)
|
||||||
|
years = ArrayField(
|
||||||
|
models.IntegerField(validators=[MinValueValidator(1980)]),
|
||||||
|
verbose_name=_('years'),
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
objects = AwardTypeQuerySet.as_manager()
|
objects = AwardTypeQuerySet.as_manager()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,7 @@ class AwardTypeBaseSerializer(serializers.ModelSerializer):
|
||||||
fields = (
|
fields = (
|
||||||
'id',
|
'id',
|
||||||
'name',
|
'name',
|
||||||
|
'years',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,13 @@ class AwardTypesListView(generics.ListAPIView):
|
||||||
pagination_class = None
|
pagination_class = None
|
||||||
serializer_class = serializers.AwardTypeBaseSerializer
|
serializer_class = serializers.AwardTypeBaseSerializer
|
||||||
permission_classes = get_permission_classes()
|
permission_classes = get_permission_classes()
|
||||||
|
filter_backends = (DjangoFilterBackend,)
|
||||||
|
ordering_fields = '__all__'
|
||||||
|
lookup_field = 'id'
|
||||||
|
filterset_fields = (
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Overridden get_queryset method."""
|
"""Overridden get_queryset method."""
|
||||||
|
|
@ -66,7 +73,7 @@ class ContentTypeView(generics.ListAPIView):
|
||||||
queryset = ContentType.objects.all()
|
queryset = ContentType.objects.all()
|
||||||
serializer_class = serializers.ContentTypeBackSerializer
|
serializer_class = serializers.ContentTypeBackSerializer
|
||||||
permission_classes = get_permission_classes()
|
permission_classes = get_permission_classes()
|
||||||
filter_backends = (DjangoFilterBackend, )
|
filter_backends = (DjangoFilterBackend,)
|
||||||
ordering_fields = '__all__'
|
ordering_fields = '__all__'
|
||||||
lookup_field = 'id'
|
lookup_field = 'id'
|
||||||
filterset_fields = (
|
filterset_fields = (
|
||||||
|
|
|
||||||
18
apps/news/migrations/0056_auto_20200205_1310.py
Normal file
18
apps/news/migrations/0056_auto_20200205_1310.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.7 on 2020-02-05 13:10
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('news', '0055_auto_20200131_1232'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='news',
|
||||||
|
name='state',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[(0, 'published'), (1, 'not published')], default=1, verbose_name='State'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -105,10 +105,6 @@ class NewsQuerySet(TranslationQuerysetMixin):
|
||||||
"""Return qs with related objects."""
|
"""Return qs with related objects."""
|
||||||
return self.select_related('created_by', 'agenda', 'banner')
|
return self.select_related('created_by', 'agenda', 'banner')
|
||||||
|
|
||||||
def visible(self):
|
|
||||||
"""Narrows qs by excluding invisible for API (at all) news"""
|
|
||||||
return self.exclude(state=self.model.REMOVE)
|
|
||||||
|
|
||||||
def by_type(self, news_type):
|
def by_type(self, news_type):
|
||||||
"""Filter News by type"""
|
"""Filter News by type"""
|
||||||
return self.filter(news_type__name=news_type)
|
return self.filter(news_type__name=news_type)
|
||||||
|
|
@ -278,16 +274,12 @@ class News(GalleryMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin,
|
||||||
)
|
)
|
||||||
|
|
||||||
# STATE CHOICES
|
# STATE CHOICES
|
||||||
REMOVE = 0
|
PUBLISHED = 0
|
||||||
HIDDEN = 1
|
UNPUBLISHED = 1
|
||||||
PUBLISHED = 2
|
|
||||||
UNPUBLISHED = 3
|
|
||||||
|
|
||||||
PUBLISHED_STATES = [PUBLISHED]
|
PUBLISHED_STATES = [PUBLISHED]
|
||||||
|
|
||||||
STATE_CHOICES = (
|
STATE_CHOICES = (
|
||||||
(REMOVE, _('remove')), # simply stored in DB news. not shown anywhere
|
|
||||||
(HIDDEN, _('hidden')), # not shown in api/web or api/mobile
|
|
||||||
(PUBLISHED, _('published')), # shown everywhere
|
(PUBLISHED, _('published')), # shown everywhere
|
||||||
(UNPUBLISHED, _('not published')), # newly created news
|
(UNPUBLISHED, _('not published')), # newly created news
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ class NewsMixinView:
|
||||||
"""Override get_queryset method."""
|
"""Override get_queryset method."""
|
||||||
qs = models.News.objects.published() \
|
qs = models.News.objects.published() \
|
||||||
.with_base_related() \
|
.with_base_related() \
|
||||||
.visible() \
|
|
||||||
.annotate_in_favorites(self.request.user) \
|
.annotate_in_favorites(self.request.user) \
|
||||||
.order_by('-is_highlighted', '-publication_date', '-publication_time')
|
.order_by('-is_highlighted', '-publication_date', '-publication_time')
|
||||||
|
|
||||||
|
|
@ -44,7 +43,7 @@ class NewsMixinView:
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
instance = self.get_queryset().visible().with_base_related().filter(
|
instance = self.get_queryset().with_base_related().filter(
|
||||||
slugs__values__contains=[self.kwargs['slug']]
|
slugs__values__contains=[self.kwargs['slug']]
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
|
|
@ -162,7 +161,7 @@ class NewsBackOfficeLCView(NewsBackOfficeMixinView,
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Override get_queryset method."""
|
"""Override get_queryset method."""
|
||||||
qs = super().get_queryset().with_extended_related().visible()
|
qs = super().get_queryset().with_extended_related()
|
||||||
if 'ordering' in self.request.query_params:
|
if 'ordering' in self.request.query_params:
|
||||||
self.request.GET._mutable = True
|
self.request.GET._mutable = True
|
||||||
if '-publication_datetime' in self.request.query_params['ordering']:
|
if '-publication_datetime' in self.request.query_params['ordering']:
|
||||||
|
|
|
||||||
19
apps/product/migrations/0026_auto_20200204_1205.py
Normal file
19
apps/product/migrations/0026_auto_20200204_1205.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Generated by Django 2.2.7 on 2020-02-04 12:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('product', '0025_auto_20191227_1443'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='productnote',
|
||||||
|
name='product',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='product.Product', verbose_name='product'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -344,12 +344,6 @@ class Product(GalleryMixin, TranslatedFieldsMixin, BaseAttributes,
|
||||||
"""Override str dunder method."""
|
"""Override str dunder method."""
|
||||||
return f'{self.name}'
|
return f'{self.name}'
|
||||||
|
|
||||||
def delete(self, using=None, keep_parents=False):
|
|
||||||
"""Overridden delete method"""
|
|
||||||
# Delete all related notes
|
|
||||||
self.notes.all().delete()
|
|
||||||
return super().delete(using, keep_parents)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def product_type_translated_name(self):
|
def product_type_translated_name(self):
|
||||||
"""Get translated name of product type."""
|
"""Get translated name of product type."""
|
||||||
|
|
@ -624,7 +618,7 @@ class ProductNote(ProjectBaseMixin):
|
||||||
"""Note model for Product entity."""
|
"""Note model for Product entity."""
|
||||||
old_id = models.PositiveIntegerField(null=True, blank=True)
|
old_id = models.PositiveIntegerField(null=True, blank=True)
|
||||||
text = models.TextField(verbose_name=_('text'))
|
text = models.TextField(verbose_name=_('text'))
|
||||||
product = models.ForeignKey(Product, on_delete=models.PROTECT,
|
product = models.ForeignKey(Product, on_delete=models.CASCADE,
|
||||||
related_name='notes',
|
related_name='notes',
|
||||||
verbose_name=_('product'))
|
verbose_name=_('product'))
|
||||||
user = models.ForeignKey('account.User', on_delete=models.PROTECT,
|
user = models.ForeignKey('account.User', on_delete=models.PROTECT,
|
||||||
|
|
|
||||||
0
apps/report/__init__.py
Normal file
0
apps/report/__init__.py
Normal file
8
apps/report/admin.py
Normal file
8
apps/report/admin.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from report.models import Report
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Report)
|
||||||
|
class ReportAdmin(admin.ModelAdmin):
|
||||||
|
"""Report admin model."""
|
||||||
8
apps/report/apps.py
Normal file
8
apps/report/apps.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
from django.utils.text import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class ReportConfig(AppConfig):
|
||||||
|
name = 'report'
|
||||||
|
verbose_name = _('Report')
|
||||||
|
|
||||||
26
apps/report/filters.py
Normal file
26
apps/report/filters.py
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
"""Filters for application report."""
|
||||||
|
from django_filters import rest_framework as filters
|
||||||
|
|
||||||
|
from report.models import Report
|
||||||
|
|
||||||
|
|
||||||
|
class ReportFilterSet(filters.FilterSet):
|
||||||
|
"""Report filter set."""
|
||||||
|
source = filters.ChoiceFilter(
|
||||||
|
choices=Report.SOURCE_CHOICES,
|
||||||
|
help_text='Filter allow filtering QuerySet by a field - source.'
|
||||||
|
'Choices - 0 (Back office), 1 (Web), 2 (Mobile)'
|
||||||
|
)
|
||||||
|
category = filters.ChoiceFilter(
|
||||||
|
choices=Report.CATEGORY_CHOICES,
|
||||||
|
help_text='Filter allow filtering QuerySet by a field - category.'
|
||||||
|
'Choices - 0 (Bug), 1 (Suggestion/improvement)'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
model = Report
|
||||||
|
fields = (
|
||||||
|
'source',
|
||||||
|
'category',
|
||||||
|
)
|
||||||
31
apps/report/migrations/0001_initial.py
Normal file
31
apps/report/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Generated by Django 2.2.7 on 2020-02-05 12:16
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Report',
|
||||||
|
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')),
|
||||||
|
('source', models.PositiveSmallIntegerField(choices=[(0, 'Back office'), (1, 'Web'), (2, 'Mobile')], verbose_name='source')),
|
||||||
|
('category', models.PositiveSmallIntegerField(choices=[(0, 'Bug'), (1, 'Suggestion/improvement')], verbose_name='category')),
|
||||||
|
('url', models.URLField(verbose_name='URL')),
|
||||||
|
('description', models.TextField(verbose_name='description')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Report',
|
||||||
|
'verbose_name_plural': 'Reports',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
apps/report/migrations/__init__.py
Normal file
0
apps/report/migrations/__init__.py
Normal file
84
apps/report/models.py
Normal file
84
apps/report/models.py
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.mail import send_mail
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.text import gettext_lazy as _
|
||||||
|
|
||||||
|
from report.tasks import send_report_task
|
||||||
|
from utils.models import ProjectBaseMixin
|
||||||
|
|
||||||
|
|
||||||
|
class ReportManager(models.Manager):
|
||||||
|
"""Manager for model Report."""
|
||||||
|
|
||||||
|
def make(self, source: int, category, url: str, description: str):
|
||||||
|
"""Make object."""
|
||||||
|
obj = self.create(
|
||||||
|
source=source,
|
||||||
|
category=category,
|
||||||
|
url=url,
|
||||||
|
description=description
|
||||||
|
)
|
||||||
|
if settings.USE_CELERY:
|
||||||
|
send_report_task.delay(obj.id)
|
||||||
|
else:
|
||||||
|
send_report_task(obj.id)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
class ReportQuerySet(models.QuerySet):
|
||||||
|
"""QuerySet for model Report."""
|
||||||
|
|
||||||
|
def by_source(self, source: int):
|
||||||
|
"""Return QuerySet filtered by a source."""
|
||||||
|
return self.filter(source=source)
|
||||||
|
|
||||||
|
|
||||||
|
class Report(ProjectBaseMixin):
|
||||||
|
"""Report model."""
|
||||||
|
|
||||||
|
BACK_OFFICE = 0
|
||||||
|
WEB = 1
|
||||||
|
MOBILE = 2
|
||||||
|
|
||||||
|
SOURCE_CHOICES = (
|
||||||
|
(BACK_OFFICE, _('Back office')),
|
||||||
|
(WEB, _('Web')),
|
||||||
|
(MOBILE, _('Mobile')),
|
||||||
|
)
|
||||||
|
|
||||||
|
BUG = 0
|
||||||
|
SUGGESTION_IMPROVEMENT = 1
|
||||||
|
|
||||||
|
CATEGORY_CHOICES = (
|
||||||
|
(BUG, _('Bug')),
|
||||||
|
(SUGGESTION_IMPROVEMENT, _('Suggestion/improvement')),
|
||||||
|
)
|
||||||
|
|
||||||
|
source = models.PositiveSmallIntegerField(choices=SOURCE_CHOICES,
|
||||||
|
verbose_name=_('source'))
|
||||||
|
category = models.PositiveSmallIntegerField(choices=CATEGORY_CHOICES,
|
||||||
|
verbose_name=_('category'))
|
||||||
|
url = models.URLField(verbose_name=_('URL'))
|
||||||
|
description = models.TextField(verbose_name=_('description'))
|
||||||
|
|
||||||
|
objects = ReportManager.from_queryset(ReportQuerySet)()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
verbose_name = _('Report')
|
||||||
|
verbose_name_plural = _('Reports')
|
||||||
|
|
||||||
|
def get_body_email_message(self):
|
||||||
|
"""Prepare the body of the email message"""
|
||||||
|
return {
|
||||||
|
'subject': self.get_category_display(),
|
||||||
|
'message': str(self.description),
|
||||||
|
'html_message': self.description,
|
||||||
|
'from_email': settings.EMAIL_HOST_USER,
|
||||||
|
'recipient_list': [settings.EMAIL_TECHNICAL_SUPPORT, ],
|
||||||
|
}
|
||||||
|
|
||||||
|
def send_email(self):
|
||||||
|
"""Send an email reset user password"""
|
||||||
|
send_mail(**self.get_body_email_message())
|
||||||
|
|
||||||
34
apps/report/serializers.py
Normal file
34
apps/report/serializers.py
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
"""DRF-serializers for application report."""
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from report.models import Report
|
||||||
|
|
||||||
|
|
||||||
|
class ReportBaseSerializer(serializers.ModelSerializer):
|
||||||
|
"""Serializer for model Report."""
|
||||||
|
|
||||||
|
category_display = serializers.CharField(source='get_category_display', read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
model = Report
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'category',
|
||||||
|
'category_display',
|
||||||
|
'url',
|
||||||
|
'description',
|
||||||
|
]
|
||||||
|
extra_kwargs = {
|
||||||
|
'source': {'required': False},
|
||||||
|
'category': {'write_only': True}
|
||||||
|
}
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
"""An overridden validate method."""
|
||||||
|
attrs['source'] = self.context.get('view').get_source()
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
"""An overridden create method."""
|
||||||
|
return self.Meta.model.objects.make(**validated_data)
|
||||||
18
apps/report/tasks.py
Normal file
18
apps/report/tasks.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
"""Report app tasks."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from celery import shared_task
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def send_report_task(report_id: int):
|
||||||
|
from report.models import Report
|
||||||
|
|
||||||
|
report_qs = Report.objects.filter(id=report_id)
|
||||||
|
if report_qs.exists():
|
||||||
|
report = report_qs.first()
|
||||||
|
report.send_email()
|
||||||
|
else:
|
||||||
|
logger.error(f'Error sending report {report_id}')
|
||||||
1
apps/report/tests.py
Normal file
1
apps/report/tests.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Create your tests here.
|
||||||
0
apps/report/urls/__init__.py
Normal file
0
apps/report/urls/__init__.py
Normal file
10
apps/report/urls/back.py
Normal file
10
apps/report/urls/back.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
"""Back office URL patterns for application report."""
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from report.views import back as views
|
||||||
|
|
||||||
|
app_name = 'report'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', views.ReportListCreateView.as_view(), name='report-list-create'),
|
||||||
|
]
|
||||||
9
apps/report/urls/common.py
Normal file
9
apps/report/urls/common.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
"""Common URL patterns for application report."""
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from report.views import common as views
|
||||||
|
|
||||||
|
app_name = 'report'
|
||||||
|
urlpatterns = [
|
||||||
|
path('<int:pk>/', views.ReportRetrieveView.as_view(), name='report-retrieve')
|
||||||
|
]
|
||||||
5
apps/report/urls/mobile.py
Normal file
5
apps/report/urls/mobile.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
"""Mobile URL patterns for application report."""
|
||||||
|
|
||||||
|
app_name = 'report'
|
||||||
|
|
||||||
|
urlpatterns = []
|
||||||
11
apps/report/urls/web.py
Normal file
11
apps/report/urls/web.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
"""Web URL patterns for application report."""
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from report.urls.common import urlpatterns as common_urlpatterns
|
||||||
|
from report.views import web as views
|
||||||
|
|
||||||
|
app_name = 'report'
|
||||||
|
|
||||||
|
urlpatterns = common_urlpatterns + [
|
||||||
|
path('', views.ReportListCreateView.as_view(), name='report-list-create'),
|
||||||
|
]
|
||||||
0
apps/report/views/__init__.py
Normal file
0
apps/report/views/__init__.py
Normal file
39
apps/report/views/back.py
Normal file
39
apps/report/views/back.py
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
"""Views for application report."""
|
||||||
|
from rest_framework.generics import ListCreateAPIView
|
||||||
|
|
||||||
|
from report.models import Report
|
||||||
|
from report.views.common import ReportBaseView
|
||||||
|
|
||||||
|
|
||||||
|
class ReportListCreateView(ReportBaseView, ListCreateAPIView):
|
||||||
|
"""
|
||||||
|
## View for getting list of reports or create a new one.
|
||||||
|
### POST-request data
|
||||||
|
Request data attributes:
|
||||||
|
* category: integer (0 - Bug, 1 - Suggestion improvement)
|
||||||
|
* url: char (URL)
|
||||||
|
* description: text (problem description)
|
||||||
|
I.e.:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"category": 1,
|
||||||
|
"url": "http://google.com",
|
||||||
|
"description": "Description"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response
|
||||||
|
*GET*
|
||||||
|
Return paginated list of reports.
|
||||||
|
|
||||||
|
*POST*
|
||||||
|
Creates a new report with a source - `BACK_OFFICE`, and returns a serialized object.
|
||||||
|
|
||||||
|
### Description
|
||||||
|
Method that allows getting list of reports or create a new one and return serialized object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_source():
|
||||||
|
"""Return a source for view."""
|
||||||
|
return Report.BACK_OFFICE
|
||||||
58
apps/report/views/common.py
Normal file
58
apps/report/views/common.py
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
"""Common views for application report."""
|
||||||
|
from rest_framework import generics
|
||||||
|
|
||||||
|
from report.filters import ReportFilterSet
|
||||||
|
from report.models import Report
|
||||||
|
from report.serializers import ReportBaseSerializer
|
||||||
|
from utils.methods import get_permission_classes
|
||||||
|
from utils.permissions import (
|
||||||
|
IsEstablishmentManager, IsContentPageManager, IsReviewManager,
|
||||||
|
IsModerator, IsRestaurantInspector, IsArtisanInspector,
|
||||||
|
IsWineryWineInspector, IsDistilleryLiquorInspector, IsProducerFoodInspector,
|
||||||
|
IsEstablishmentAdministrator
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ReportBaseView(generics.GenericAPIView):
|
||||||
|
"""
|
||||||
|
## Report base view.
|
||||||
|
"""
|
||||||
|
queryset = Report.objects.all()
|
||||||
|
serializer_class = ReportBaseSerializer
|
||||||
|
filter_class = ReportFilterSet
|
||||||
|
permission_classes = get_permission_classes(
|
||||||
|
IsEstablishmentManager, IsContentPageManager, IsReviewManager,
|
||||||
|
IsModerator, IsRestaurantInspector, IsArtisanInspector,
|
||||||
|
IsWineryWineInspector, IsDistilleryLiquorInspector, IsProducerFoodInspector,
|
||||||
|
IsEstablishmentAdministrator
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_source():
|
||||||
|
"""Return a source for view."""
|
||||||
|
return NotImplementedError('You must implement "get_source" method')
|
||||||
|
|
||||||
|
|
||||||
|
class ReportRetrieveView(ReportBaseView, generics.RetrieveAPIView):
|
||||||
|
"""
|
||||||
|
## View for retrieving serialized instance.
|
||||||
|
### Response
|
||||||
|
Return serialized object.
|
||||||
|
I.e.:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"count": 7,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Description
|
||||||
|
Method that allows retrieving serialized report object.
|
||||||
|
"""
|
||||||
39
apps/report/views/web.py
Normal file
39
apps/report/views/web.py
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
"""Views for application report."""
|
||||||
|
from rest_framework.generics import ListCreateAPIView
|
||||||
|
|
||||||
|
from report.models import Report
|
||||||
|
from report.views.common import ReportBaseView
|
||||||
|
|
||||||
|
|
||||||
|
class ReportListCreateView(ReportBaseView, ListCreateAPIView):
|
||||||
|
"""
|
||||||
|
## View for getting list of reports or create a new one.
|
||||||
|
### POST-request data
|
||||||
|
Request data attributes:
|
||||||
|
* category: integer (0 - Bug, 1 - Suggestion improvement)
|
||||||
|
* url: char (URL)
|
||||||
|
* description: text (problem description)
|
||||||
|
I.e.:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"category": 1,
|
||||||
|
"url": "http://google.com",
|
||||||
|
"description": "Description"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response
|
||||||
|
*GET*
|
||||||
|
Return paginated list of reports.
|
||||||
|
|
||||||
|
*POST*
|
||||||
|
Creates a new report with a source - `WEB`, and returns a serialized object.
|
||||||
|
|
||||||
|
### Description
|
||||||
|
Method that allows getting list of reports or create a new one and return serialized object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_source():
|
||||||
|
"""Return a source for view."""
|
||||||
|
return Report.WEB
|
||||||
|
|
@ -92,9 +92,9 @@ class NewsSerializer(serializers.Serializer):
|
||||||
states = {
|
states = {
|
||||||
'new': News.UNPUBLISHED,
|
'new': News.UNPUBLISHED,
|
||||||
'published': News.PUBLISHED,
|
'published': News.PUBLISHED,
|
||||||
'hidden': News.HIDDEN,
|
'hidden': News.UNPUBLISHED,
|
||||||
'published_exclusive': News.PUBLISHED,
|
'published_exclusive': News.UNPUBLISHED,
|
||||||
'scheduled_exclusively': News.PUBLISHED,
|
'scheduled_exclusively': News.UNPUBLISHED,
|
||||||
}
|
}
|
||||||
return states.get(data['page__state'], News.UNPUBLISHED)
|
return states.get(data['page__state'], News.UNPUBLISHED)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
"""Translation app models."""
|
"""Translation app models."""
|
||||||
|
from django.apps import apps
|
||||||
from django.contrib.postgres.fields import JSONField
|
from django.contrib.postgres.fields import JSONField
|
||||||
from django.contrib.postgres.indexes import GinIndex
|
from django.contrib.postgres.indexes import GinIndex
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.apps import apps
|
|
||||||
from utils.models import ProjectBaseMixin, LocaleManagerMixin
|
from utils.models import ProjectBaseMixin, LocaleManagerMixin
|
||||||
|
|
||||||
|
|
||||||
class LanguageQuerySet(models.QuerySet):
|
class LanguageQuerySet(models.QuerySet):
|
||||||
"""QuerySet for model Language"""
|
"""QuerySet for model Language"""
|
||||||
|
|
||||||
|
|
@ -55,7 +57,7 @@ class SiteInterfaceDictionaryManager(LocaleManagerMixin):
|
||||||
Tag = apps.get_model('tag', 'Tag')
|
Tag = apps.get_model('tag', 'Tag')
|
||||||
"""Creates or updates translation for EXISTING in DB Tag"""
|
"""Creates or updates translation for EXISTING in DB Tag"""
|
||||||
if not tag.pk or not isinstance(tag, Tag):
|
if not tag.pk or not isinstance(tag, Tag):
|
||||||
raise NotImplementedError
|
raise NotImplementedError()
|
||||||
if tag.translation:
|
if tag.translation:
|
||||||
tag.translation.text = translations
|
tag.translation.text = translations
|
||||||
tag.translation.page = 'tag'
|
tag.translation.page = 'tag'
|
||||||
|
|
@ -74,7 +76,7 @@ class SiteInterfaceDictionaryManager(LocaleManagerMixin):
|
||||||
"""Creates or updates translation for EXISTING in DB TagCategory"""
|
"""Creates or updates translation for EXISTING in DB TagCategory"""
|
||||||
TagCategory = apps.get_model('tag', 'TagCategory')
|
TagCategory = apps.get_model('tag', 'TagCategory')
|
||||||
if not tag_category.pk or not isinstance(tag_category, TagCategory):
|
if not tag_category.pk or not isinstance(tag_category, TagCategory):
|
||||||
raise NotImplementedError
|
raise NotImplementedError()
|
||||||
if tag_category.translation:
|
if tag_category.translation:
|
||||||
tag_category.translation.text = translations
|
tag_category.translation.text = translations
|
||||||
tag_category.translation.page = 'tag'
|
tag_category.translation.page = 'tag'
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ from django.contrib.postgres.aggregates import ArrayAgg
|
||||||
from django.contrib.postgres.fields import JSONField
|
from django.contrib.postgres.fields import JSONField
|
||||||
from django.contrib.postgres.fields.jsonb import KeyTextTransform
|
from django.contrib.postgres.fields.jsonb import KeyTextTransform
|
||||||
from django.core.validators import FileExtensionValidator
|
from django.core.validators import FileExtensionValidator
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.html import mark_safe
|
from django.utils.html import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _, get_language
|
from django.utils.translation import ugettext_lazy as _, get_language
|
||||||
|
|
@ -142,7 +143,7 @@ class OAuthProjectMixin:
|
||||||
|
|
||||||
def get_source(self):
|
def get_source(self):
|
||||||
"""Method to get of platform"""
|
"""Method to get of platform"""
|
||||||
return NotImplementedError
|
return NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
class BaseAttributes(ProjectBaseMixin):
|
class BaseAttributes(ProjectBaseMixin):
|
||||||
|
|
@ -526,3 +527,12 @@ class PhoneModelMixin:
|
||||||
"""Return phone national number from from PhonеNumberField."""
|
"""Return phone national number from from PhonеNumberField."""
|
||||||
if hasattr(self, 'phone') and (self.phone and hasattr(self.phone, 'national_number')):
|
if hasattr(self, 'phone') and (self.phone and hasattr(self.phone, 'national_number')):
|
||||||
return self.phone.national_number
|
return self.phone.national_number
|
||||||
|
|
||||||
|
|
||||||
|
class AwardsModelMixin:
|
||||||
|
def remove_award(self, award_id: int):
|
||||||
|
from main.models import Award
|
||||||
|
award = get_object_or_404(Award, pk=award_id)
|
||||||
|
|
||||||
|
if hasattr(self, 'awards'):
|
||||||
|
self.awards.remove(award)
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ PROJECT_APPS = [
|
||||||
'favorites.apps.FavoritesConfig',
|
'favorites.apps.FavoritesConfig',
|
||||||
'rating.apps.RatingConfig',
|
'rating.apps.RatingConfig',
|
||||||
'tag.apps.TagConfig',
|
'tag.apps.TagConfig',
|
||||||
|
'report.apps.ReportConfig',
|
||||||
]
|
]
|
||||||
|
|
||||||
EXTERNAL_APPS = [
|
EXTERNAL_APPS = [
|
||||||
|
|
@ -453,6 +454,9 @@ PASSWORD_RESET_TIMEOUT_DAYS = 1
|
||||||
|
|
||||||
# TEMPLATES
|
# TEMPLATES
|
||||||
RESETTING_TOKEN_TEMPLATE = 'account/password_reset_email.html'
|
RESETTING_TOKEN_TEMPLATE = 'account/password_reset_email.html'
|
||||||
|
NEW_USER_FOR_ESTABLISHMENT_TEAM_TEMPLATE = 'account/invite_est_team_new_user.html'
|
||||||
|
EXISTING_USER_FOR_ESTABLISHMENT_TEAM_TEMPLATE = 'account/invite_est_team_existing_user.html'
|
||||||
|
ESTABLISHMENT_TEAM_ROLE_REVOKED_TEMPLATE = 'account/est_team_role_revoked.html'
|
||||||
CHANGE_EMAIL_TEMPLATE = 'account/change_email.html'
|
CHANGE_EMAIL_TEMPLATE = 'account/change_email.html'
|
||||||
CONFIRM_EMAIL_TEMPLATE = 'authorization/confirm_email.html'
|
CONFIRM_EMAIL_TEMPLATE = 'authorization/confirm_email.html'
|
||||||
NEWS_EMAIL_TEMPLATE = 'news/news_email.html'
|
NEWS_EMAIL_TEMPLATE = 'news/news_email.html'
|
||||||
|
|
@ -563,3 +567,5 @@ COUNTRY_CALLING_CODES = {
|
||||||
|
|
||||||
CALLING_CODES_ANTILLES_GUYANE_WEST_INDIES = [590, 594, 1758, 596]
|
CALLING_CODES_ANTILLES_GUYANE_WEST_INDIES = [590, 594, 1758, 596]
|
||||||
DEFAULT_CALLING_CODE_ANTILLES_GUYANE_WEST_INDIES = 590
|
DEFAULT_CALLING_CODE_ANTILLES_GUYANE_WEST_INDIES = 590
|
||||||
|
|
||||||
|
EMAIL_TECHNICAL_SUPPORT = 'it-report@gaultmillau.com'
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"""Development settings."""
|
"""Development settings."""
|
||||||
from .base import *
|
|
||||||
from .amazon_s3 import *
|
from .amazon_s3 import *
|
||||||
|
from .base import *
|
||||||
|
|
||||||
ALLOWED_HOSTS = ['gm.id-east.ru', '95.213.204.126', '0.0.0.0']
|
ALLOWED_HOSTS = ['gm.id-east.ru', '95.213.204.126', '0.0.0.0']
|
||||||
|
|
||||||
|
|
@ -77,3 +77,33 @@ EMAIL_PORT = 587
|
||||||
|
|
||||||
|
|
||||||
MIDDLEWARE.append('utils.middleware.log_db_queries_per_API_request')
|
MIDDLEWARE.append('utils.middleware.log_db_queries_per_API_request')
|
||||||
|
|
||||||
|
LOGGING = {
|
||||||
|
'version': 1,
|
||||||
|
'disable_existing_loggers': False,
|
||||||
|
'filters': {
|
||||||
|
'require_debug_false': {
|
||||||
|
'()': 'django.utils.log.RequireDebugFalse',
|
||||||
|
},
|
||||||
|
'require_debug_true': {
|
||||||
|
'()': 'django.utils.log.RequireDebugTrue',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'handlers': {
|
||||||
|
'console': {
|
||||||
|
'level': 'DEBUG',
|
||||||
|
'filters': ['require_debug_true'],
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
},
|
||||||
|
'null': {
|
||||||
|
'class': 'logging.NullHandler',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'loggers': {
|
||||||
|
'django.db.backends': {
|
||||||
|
'handlers': ['console', ],
|
||||||
|
'level': 'ERROR',
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,11 +86,11 @@ LOGGING = {
|
||||||
'py.warnings': {
|
'py.warnings': {
|
||||||
'handlers': ['console'],
|
'handlers': ['console'],
|
||||||
},
|
},
|
||||||
# 'django.db.backends': {
|
'django.db.backends': {
|
||||||
# 'handlers': ['console', ],
|
'handlers': ['console', ],
|
||||||
# 'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
# 'propagate': False,
|
'propagate': False,
|
||||||
# },
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,3 +123,5 @@ EMAIL_PORT = 587
|
||||||
|
|
||||||
# ADD TRANSFER TO INSTALLED APPS
|
# ADD TRANSFER TO INSTALLED APPS
|
||||||
INSTALLED_APPS.append('transfer.apps.TransferConfig')
|
INSTALLED_APPS.append('transfer.apps.TransferConfig')
|
||||||
|
|
||||||
|
EMAIL_TECHNICAL_SUPPORT = 'a.feteleu@spider.ru'
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@
|
||||||
<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" />
|
<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>
|
<span class="letter__follow-title">Follow us</span>
|
||||||
</div>
|
</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 class="letter__follow-text" style="display: block;margin: 0 0 30px;font-size: 12px;font-style: italic;">You can also find us on our social network below
|
||||||
</div>
|
</div>
|
||||||
<div class="letter__follow-social">
|
<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;">
|
<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;">
|
||||||
|
|
|
||||||
76
project/templates/account/est_team_role_revoked.html
Normal file
76
project/templates/account/est_team_role_revoked.html
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
{% load i18n %}{% autoescape off %}
|
||||||
|
<!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>
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
<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;">
|
||||||
|
<br>
|
||||||
|
{% blocktrans %}Hello, you are receiving this email because your admin privileges on <b>{{ restaurant_name }}</b> have been revoked by another administrator.
|
||||||
|
If you think this is a mistake, you should contact {{ contact_email }}.{% endblocktrans %}
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
|
||||||
|
</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 find 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;">Please contact {{ contact_email }} if you have any questions.</span>
|
||||||
|
</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>
|
||||||
|
{% endautoescape %}
|
||||||
83
project/templates/account/invite_est_team_existing_user.html
Normal file
83
project/templates/account/invite_est_team_existing_user.html
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
{% load i18n %}{% autoescape off %}
|
||||||
|
<!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>
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
<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;">
|
||||||
|
<br>
|
||||||
|
{% blocktrans %}Hi, you are receiving this email because you have been granted to manage the establishment <b>{{ restaurant_name }}</b>.
|
||||||
|
From now, you can manage information about this place and have it published in {{ site_name }}.{% endblocktrans %}
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
{% trans "Please go to the following page to apply the appointment:" %}
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
https://{{ country_code }}.{{ domain_uri }}/join-establishment-team/{{ uidb64 }}/{{ token }}/?role={{ user_role_id }}
|
||||||
|
<br>
|
||||||
|
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
|
||||||
|
</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 find 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;"> In case that you were not already a Gault&Millau user, a temporary account with your email has been temporarily created by a Gault&Millau administrator.
|
||||||
|
By completing the creation process of your account, you agree to have this account permanently created.
|
||||||
|
This temporary account will be deleted after 7 days if you don't complete the registration process. Please contact {{ contact_email }} if you have any questions.</span>
|
||||||
|
</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>
|
||||||
|
{% endautoescape %}
|
||||||
83
project/templates/account/invite_est_team_new_user.html
Normal file
83
project/templates/account/invite_est_team_new_user.html
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
{% load i18n %}{% autoescape off %}
|
||||||
|
<!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>
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
<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;">
|
||||||
|
<br>
|
||||||
|
{% blocktrans %}Hi, you are receiving this email because you have been granted to manage the establishment <b>{{ restaurant_name }}</b>.
|
||||||
|
From now, you can manage information about this place and have it published in {{ site_name }}.{% endblocktrans %}
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
{% trans "Please go to the following page and choose a new password:" %}
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
https://{{ country_code }}.{{ domain_uri }}/recovery/{{ uidb64 }}/{{ token }}/?role={{ user_role_id }}
|
||||||
|
<br>
|
||||||
|
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
|
||||||
|
</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 find 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;">In case that you were not already a Gault&Millau user, a temporary account with your email has been temporarily created by a Gault&Millau administrator.
|
||||||
|
By completing the creation process of your account, you agree to have this account permanently created.
|
||||||
|
This temporary account will be deleted after 7 days if you don't complete the registration process. Please contact {{ contact_email }} if you have any questions. </span>
|
||||||
|
</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>
|
||||||
|
{% endautoescape %}
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
<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" />
|
<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>
|
<span class="letter__follow-title">Follow us</span>
|
||||||
</div>
|
</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 class="letter__follow-text" style="display: block;margin: 0 0 30px;font-size: 12px;font-style: italic;">You can also find us on our social network below
|
||||||
</div>
|
</div>
|
||||||
<div class="letter__follow-social">
|
<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;">
|
<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;">
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
<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" />
|
<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>
|
<span class="letter__follow-title">Follow us</span>
|
||||||
</div>
|
</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 class="letter__follow-text" style="display: block;margin: 0 0 30px;font-size: 12px;font-style: italic;">You can also find us on our social network below
|
||||||
</div>
|
</div>
|
||||||
<div class="letter__follow-social">
|
<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;">
|
<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;">
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
<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" />
|
<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>
|
<span class="letter__follow-title">Follow us</span>
|
||||||
</div>
|
</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 class="letter__follow-text" style="display: block;margin: 0 0 30px;font-size: 12px;font-style: italic;">You can also find us on our social network below
|
||||||
</div>
|
</div>
|
||||||
<div class="letter__follow-social">
|
<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;">
|
<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;">
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@
|
||||||
<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" />
|
<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>
|
<span class="letter__follow-title">Follow us</span>
|
||||||
</div>
|
</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 class="letter__follow-text" style="display: block;margin: 0 0 30px;font-size: 12px;font-style: italic;">You can also find us on our social network below
|
||||||
</div>
|
</div>
|
||||||
<div class="letter__follow-social">
|
<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;">
|
<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;">
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@
|
||||||
<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" />
|
<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">{% trans "Follow us" %}</span>
|
<span class="letter__follow-title">{% trans "Follow us" %}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="letter__follow-text" style="display: block;margin: 0 0 30px;font-size: 12px;font-style: italic;">{% trans "You can also us on our social network below" %}
|
<div class="letter__follow-text" style="display: block;margin: 0 0 30px;font-size: 12px;font-style: italic;">{% trans "You can also find us on our social network below" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="letter__follow-social">
|
<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;">
|
<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;">
|
||||||
|
|
|
||||||
|
|
@ -18,4 +18,5 @@ urlpatterns = [
|
||||||
namespace='advertisement')),
|
namespace='advertisement')),
|
||||||
path('main/', include('main.urls.back')),
|
path('main/', include('main.urls.back')),
|
||||||
path('partner/', include('partner.urls.back')),
|
path('partner/', include('partner.urls.back')),
|
||||||
|
path('report/', include('report.urls.back')),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -36,5 +36,5 @@ urlpatterns = [
|
||||||
path('favorites/', include('favorites.urls')),
|
path('favorites/', include('favorites.urls')),
|
||||||
path('timetables/', include('timetable.urls.web')),
|
path('timetables/', include('timetable.urls.web')),
|
||||||
path('products/', include('product.urls.web')),
|
path('products/', include('product.urls.web')),
|
||||||
|
path('report/', include('report.urls.web')),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user