Merge branch 'develop' into 'feature/bind-award-for-establishment'

# Conflicts:
#   apps/establishment/views/back.py
This commit is contained in:
Anton Gorbunov 2020-02-05 12:44:28 +00:00
commit b778661564
17 changed files with 304 additions and 90 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)
try:
user.send_email(subject=subject, user.send_email(subject=subject,
message=message, message=message,
emails=None) 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,33 @@ 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)
try:
user.send_email(subject=subject, user.send_email(subject=subject,
message=message, message=message,
emails=None) 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()
with transaction.atomic():
serializer = self.get_serializer(instance=instance, serializer = self.get_serializer(instance=instance,
data=request.data) data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
serializer.save() 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

@ -1,4 +1,5 @@
"""Establishment app views.""" """Establishment app views."""
from django.conf import settings
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
@ -941,24 +942,11 @@ class TeamMemberDeleteView(generics.DestroyAPIView):
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):
class EstablishmentAwardCreateAndBind(generics.CreateAPIView, generics.DestroyAPIView): instance.delete()
queryset = main_models.Award.objects.with_base_related().all() from account.tasks import team_role_revoked
permission_classes = get_permission_classes() establishment = models.Establishment.objects.get(pk=self.kwargs['establishment_id'])
serializer_class = serializers.BackEstablishmentAwardCreateSerializer if settings.USE_CELERY:
team_role_revoked.delay(self.kwargs['user_id'], self.request.country_code, establishment.name)
def _award_list_for_establishment(self, establishment_id: int, status: int) -> Response: else:
awards = main_models.Award.objects.with_base_related().filter(object_id=establishment_id) team_role_revoked(self.kwargs['user_id'], self.request.country_code, establishment.name)
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

@ -455,6 +455,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'

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

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;">