Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
alex 2020-02-05 17:32:45 +03:00
commit 590292919b
47 changed files with 823 additions and 99 deletions

View File

@ -388,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,
} }
@ -430,6 +431,16 @@ class User(PhoneModelMixin, AbstractUser):
template_name=settings.EXISTING_USER_FOR_ESTABLISHMENT_TEAM_TEMPLATE, template_name=settings.EXISTING_USER_FOR_ESTABLISHMENT_TEAM_TEMPLATE,
context=context), get_template(settings.EXISTING_USER_FOR_ESTABLISHMENT_TEAM_TEMPLATE).render(context) 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 = {'country_code': country_code} context = {'country_code': country_code}

View File

@ -40,9 +40,15 @@ def send_team_invite_to_new_user(user_id, country_code, user_role_id, restaurant
subject = _(f'GAULT&MILLAU INVITES YOU TO MANAGE {restaurant_name}') subject = _(f'GAULT&MILLAU INVITES YOU TO MANAGE {restaurant_name}')
message = user.invite_new_establishment_member_template(country_code, user.username, message = user.invite_new_establishment_member_template(country_code, user.username,
subject, restaurant_name, user_role_id) subject, restaurant_name, user_role_id)
user.send_email(subject=subject, try:
message=message, user.send_email(subject=subject,
emails=None) 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
@ -52,9 +58,32 @@ def send_team_invite_to_existing_user(user_id, country_code, user_role_id, resta
subject = _(f'GAULT&MILLAU INVITES YOU TO MANAGE {restaurant_name}') subject = _(f'GAULT&MILLAU INVITES YOU TO MANAGE {restaurant_name}')
message = user.invite_establishment_member_template(country_code, user.username, message = user.invite_establishment_member_template(country_code, user.username,
subject, restaurant_name, user_role_id) subject, restaurant_name, user_role_id)
user.send_email(subject=subject, try:
message=message, user.send_email(subject=subject,
emails=None) 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

View File

@ -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 + \

View File

@ -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))

View File

@ -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):

View File

@ -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},

View File

@ -9,28 +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, guide):
data = {
'restaurant_section_node_id': section_node.id,
'guide_id': guide.id,
'establishment_id': establishment.id,
}
if establishment.last_published_review:
data.update({'review_id': establishment.last_published_review.id})
return data
def get_additional_product_data(section_node, product, guide):
data = {
'color_wine_section_node_id': section_node.id,
'wine_id': product.id,
'guide_id': guide.id,
}
if product.last_published_review:
data.update({'review_id': 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."""
@ -38,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:
@ -59,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()
@ -72,13 +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, 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,
guide=guide)) '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'
@ -108,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()
@ -144,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)
@ -157,26 +139,29 @@ def generate_product_guide_elements(guide_id: int, filter_set: dict):
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) 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(
wine.id, wine.id,
wine_region_node.id, wine_region_node.id,
guide_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_qs.first().value, wine_color_qs.first().value,
yard_node.id, yard_node.id,
guide_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,
wine_color_section, 'wine_id': wine.id,
wine, 'guide_id': guide.id,
guide)) }
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'

View File

@ -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):

View File

@ -34,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
@ -549,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
@ -1149,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,
@ -1225,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"""

View File

@ -21,6 +21,8 @@ from utils.decorators import with_base_attributes
from utils.methods import string_random 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):
@ -948,3 +950,24 @@ class TeamMemberSerializer(serializers.ModelSerializer):
'username', 'username',
'email', '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

View File

@ -83,5 +83,10 @@ urlpatterns = [
path('team/<int:establishment_id>', views.TeamMemberListView.as_view(), name='establishment-team-members-list'), 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(), path('team/<int:establishment_id>/<int:user_id>', views.TeamMemberDeleteView.as_view(),
name='establishment-team-member-delete'), 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',)
] ]

View File

@ -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
@ -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:
@ -102,6 +106,36 @@ class EmployeePositionsListView(generics.ListAPIView):
class EstablishmentRUDView(EstablishmentMixinViews, generics.RetrieveUpdateDestroyAPIView): class EstablishmentRUDView(EstablishmentMixinViews, generics.RetrieveUpdateDestroyAPIView):
"""
Establishment by slug
Implements get, update, delete methods
**GET**
```
EstablishmentRUDView GET method
Implement getting Establishment by slug
```
**PUT**
```
EstablishmentRUDView PUT method
Implement update of Establishment by slug
```
**PATCH**
```
EstablishmentRUDView PATCH method
Implement partial update of Establishment by slug
```
**DELETE**
```
EstablishmentRUDView DELETE method
Implement delete Establishment by slug
```
"""
lookup_field = 'slug' lookup_field = 'slug'
serializer_class = serializers.EstablishmentRUDSerializer serializer_class = serializers.EstablishmentRUDSerializer
permission_classes = get_permission_classes( permission_classes = get_permission_classes(
@ -938,3 +972,37 @@ class TeamMemberDeleteView(generics.DestroyAPIView):
def get_object(self): def get_object(self):
return UserRole.objects.get(role__role=Role.ESTABLISHMENT_ADMINISTRATOR, user_id=self.kwargs['user_id'], return UserRole.objects.get(role__role=Role.ESTABLISHMENT_ADMINISTRATOR, user_id=self.kwargs['user_id'],
establishment_id=self.kwargs['establishment_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)

File diff suppressed because one or more lines are too long

View File

@ -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

View 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'),
),
]

View File

@ -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
) )

View File

@ -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']:

0
apps/report/__init__.py Normal file
View File

8
apps/report/admin.py Normal file
View 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
View 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
View 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',
)

View 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',
},
),
]

View File

84
apps/report/models.py Normal file
View 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())

View 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
View 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
View File

@ -0,0 +1 @@
# Create your tests here.

View File

10
apps/report/urls/back.py Normal file
View 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'),
]

View 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')
]

View File

@ -0,0 +1,5 @@
"""Mobile URL patterns for application report."""
app_name = 'report'
urlpatterns = []

11
apps/report/urls/web.py Normal file
View 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'),
]

View File

39
apps/report/views/back.py Normal file
View 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

View 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
View 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

View File

@ -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)

View File

@ -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'

View File

@ -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)

View File

@ -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 = [
@ -455,6 +456,7 @@ PASSWORD_RESET_TIMEOUT_DAYS = 1
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' 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' 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'
@ -565,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'

View File

@ -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,
},
}
}

View File

@ -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'

View 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: &quot;Open Sans&quot;, 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:&quot;Open-Sans&quot;,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:&quot;Open-Sans&quot;,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: &quot;PT Serif&quot;, 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 %}

View File

@ -31,7 +31,7 @@
</div> </div>
<div class="letter__text" style="margin: 0 0 30px; font-family:&quot;Open-Sans&quot;,sans-serif; font-size: 14px; line-height: 21px;letter-spacing: -0.34px; overflow-x: hidden;"> <div class="letter__text" style="margin: 0 0 30px; font-family:&quot;Open-Sans&quot;,sans-serif; font-size: 14px; line-height: 21px;letter-spacing: -0.34px; overflow-x: hidden;">
<br> <br>
{% blocktrans %}Hi, you are receiving this email because you have been granted to manage the establishment <b>%restaurant_name%</b>. {% 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 %} From now, you can manage information about this place and have it published in {{ site_name }}.{% endblocktrans %}
<br> <br>
<br> <br>
@ -66,7 +66,7 @@ From now, you can manage information about this place and have it published in {
<div class="letter__unsubscribe" style="margin: 0 0 1.25rem;font-size: 12px;text-align: center;"> <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. <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. 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 %license_contact_email% if you have any questions.</span> 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>
<div class="letter__footer" style="padding: 24px 0 15px;text-align: center;background: #ffee29;"> <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;"> <div class="letter__footer-logo" style="width: 71px;height: 42px;margin: 0 auto 14px auto;">

View File

@ -66,7 +66,7 @@ From now, you can manage information about this place and have it published in {
<div class="letter__unsubscribe" style="margin: 0 0 1.25rem;font-size: 12px;text-align: center;"> <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. <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. 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 %license_contact_email% if you have any questions. </span> 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>
<div class="letter__footer" style="padding: 24px 0 15px;text-align: center;background: #ffee29;"> <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;"> <div class="letter__footer-logo" style="width: 71px;height: 42px;margin: 0 auto 14px auto;">

View File

@ -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')),
] ]

View File

@ -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')),
] ]