Merge branch 'develop' into features/migrate-wine

# Conflicts:
#	apps/transfer/management/commands/transfer.py
#	apps/transfer/models.py
#	project/settings/base.py
This commit is contained in:
Anatoly 2019-11-11 11:35:03 +03:00
commit c67109d9f8
62 changed files with 1157 additions and 264 deletions

View File

@ -0,0 +1,43 @@
from django.core.management.base import BaseCommand
from django.db import connections
from django.db.models import Q
from establishment.management.commands.add_position import namedtuplefetchall
from account.models import User
class Command(BaseCommand):
help = 'Add account from old db to new db'
def account_sql(self):
with connections['legacy'].cursor() as cursor:
cursor.execute('''
select a.email, a.id as account_id, a.encrypted_password
from accounts as a
where a.email is not null
and a.email not in ('cyril@tomatic.net',
'cyril2@tomatic.net',
'd.sadykova@id-east.ru',
'd.sadykova@octopod.ru',
'n.yurchenko@id-east.ru'
)
and a.confirmed_at is not null
''')
return namedtuplefetchall(cursor)
def handle(self, *args, **kwargs):
objects = []
for a in self.account_sql():
count = User.objects.filter(Q(email=a.email) | Q(old_id=a.account_id)).count()
if count == 0:
objects.append(User(email=a.email,
unconfirmed_email=False,
email_confirmed=True,
old_id=a.account_id,
password='bcrypt$'+a.encrypted_password
))
else:
user = User.objects.filter(Q(email=a.email) | Q(old_id=a.account_id))
user.update(password='bcrypt$'+a.encrypted_password)
User.objects.bulk_create(objects)
self.stdout.write(self.style.WARNING(f'Created accounts objects.'))

View File

@ -0,0 +1,31 @@
from django.core.management.base import BaseCommand
from django.db import connections
from django.db.models import Q
from establishment.management.commands.add_position import namedtuplefetchall
from account.models import User
class Command(BaseCommand):
help = 'Update accounts image from old db to new db'
def account_sql(self):
with connections['legacy'].cursor() as cursor:
cursor.execute('''
select
url as image_url,
t.account_id
from
(
select a.account_id, a.attachment_file_name,
trim(CONCAT(u.url, a.attachment_suffix_url)) as url
from account_pictures a,
(select 'https://s3.eu-central-1.amazonaws.com/gm-test.com/media/' as url) u
) t
''')
return namedtuplefetchall(cursor)
def handle(self, *args, **kwargs):
for a in self.account_sql():
users = User.objects.filter(old_id=a.account_id)
users.update(image_url= a.image_url)
self.stdout.write(self.style.WARNING(f'Update accounts image url.'))

View File

@ -0,0 +1,51 @@
from django.core.management.base import BaseCommand
from django.db import connections
from django.db.models import Q
from social_django.models import UserSocialAuth
from establishment.management.commands.add_position import namedtuplefetchall
from account.models import User
class Command(BaseCommand):
help = '''Add account social networks from old db to new db.
Run after add_account!!!'''
def social_sql(self):
with connections['legacy'].cursor() as cursor:
cursor.execute('''
select
DISTINCT
i.account_id, i.provider, i.uid
from
(
select a.email, a.id as account_id
from accounts as a
where a.email is not null
and a.email not in ('cyril@tomatic.net',
'cyril2@tomatic.net',
'd.sadykova@id-east.ru',
'd.sadykova@octopod.ru',
'n.yurchenko@id-east.ru'
)
and a.confirmed_at is not null
) t
join identities i on i.account_id = t.account_id
''')
return namedtuplefetchall(cursor)
def handle(self, *args, **kwargs):
objects = []
for s in self.social_sql():
user = User.objects.filter(old_id=s.account_id)
if user.count() > 0:
social = UserSocialAuth.objects.filter(user=user.first(),
provider=s.provider,
uid=s.uid)
if social.count() == 0:
objects.append(UserSocialAuth(user=user.first(), provider=s.provider,
uid=s.uid)
)
print('INSERT')
UserSocialAuth.objects.bulk_create(objects)
self.stdout.write(self.style.WARNING(f'Created social objects.'))

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-11-08 08:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0018_user_old_id'),
]
operations = [
migrations.AlterField(
model_name='user',
name='image_url',
field=models.URLField(blank=True, default=None, max_length=500, null=True, verbose_name='Image URL path'),
),
]

View File

@ -1,10 +1,12 @@
"""Account models"""
from datetime import datetime
from django.conf import settings
from django.contrib.auth.models import AbstractUser, UserManager as BaseUserManager
from django.contrib.auth.tokens import default_token_generator as password_token_generator
from django.core.mail import send_mail
from django.db import models
from django.template.loader import render_to_string
from django.template.loader import render_to_string, get_template
from django.utils import timezone
from django.utils.encoding import force_bytes
from django.utils.html import mark_safe
@ -15,6 +17,7 @@ from rest_framework.authtoken.models import Token
from authorization.models import Application
from establishment.models import Establishment
from location.models import Country
from main.models import SiteSettings
from utils.models import GMTokenGenerator
from utils.models import ImageMixin, ProjectBaseMixin, PlatformMixin
from utils.tokens import GMRefreshToken
@ -86,7 +89,8 @@ class UserQuerySet(models.QuerySet):
class User(AbstractUser):
"""Base user model."""
image_url = models.URLField(verbose_name=_('Image URL path'),
blank=True, null=True, default=None)
blank=True, null=True, default=None,
max_length=500)
cropped_image_url = models.URLField(verbose_name=_('Cropped image URL path'),
blank=True, null=True, default=None)
email = models.EmailField(_('email address'), unique=True,
@ -158,19 +162,21 @@ class User(AbstractUser):
self.is_active = True
self.save()
def get_body_email_message(self, subject: str, message: str):
def get_body_email_message(self, subject: str, message: str, emails=None):
"""Prepare the body of the email message"""
return {
'subject': subject,
'message': str(message),
'message': str(message[0]),
'html_message': message[1],
'from_email': settings.EMAIL_HOST_USER,
'recipient_list': [self.email, ]
'recipient_list': emails if emails else [self.email, ],
}
def send_email(self, subject: str, message: str):
def send_email(self, subject: str, message: str, emails=None):
"""Send an email to reset user password"""
send_mail(**self.get_body_email_message(subject=subject,
message=message))
message=message,
emails=emails))
@property
def confirm_email_token(self):
@ -192,12 +198,20 @@ class User(AbstractUser):
"""Get base64 value for user by primary key identifier"""
return urlsafe_base64_encode(force_bytes(self.pk))
@property
def base_template(self):
def base_template(self, country_code='www', username='', subject=''):
"""Base email template"""
return {'domain_uri': settings.DOMAIN_URI,
'uidb64': self.get_user_uidb64,
'site_name': settings.SITE_NAME}
socials = SiteSettings.objects.by_country_code(country_code).first()
return {
'title': subject,
'domain_uri': settings.DOMAIN_URI,
'uidb64': self.get_user_uidb64,
'site_name': settings.SITE_NAME,
'year': datetime.now().year,
'twitter_page_url': socials.twitter_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 '#',
'send_to': username,
}
@property
def image_tag(self):
@ -207,41 +221,41 @@ class User(AbstractUser):
def cropped_image_tag(self):
return mark_safe(f'<img src="{self.cropped_image_url}" />')
def reset_password_template(self, country_code):
def reset_password_template(self, country_code, username, subject):
"""Get reset password template"""
context = {'token': self.reset_password_token,
'country_code': country_code}
context.update(self.base_template)
context.update(self.base_template(country_code, username, subject))
return render_to_string(
template_name=settings.RESETTING_TOKEN_TEMPLATE,
context=context)
context=context), get_template(settings.RESETTING_TOKEN_TEMPLATE).render(context)
def notify_password_changed_template(self, country_code):
def notify_password_changed_template(self, country_code, username, subject):
"""Get notification email template"""
context = {'contry_code': country_code}
context.update(self.base_template)
context.update(self.base_template(country_code, username, subject))
return render_to_string(
template_name=settings.NOTIFICATION_PASSWORD_TEMPLATE,
context=context,
)
), get_template(settings.NOTIFICATION_PASSWORD_TEMPLATE).render(context)
def confirm_email_template(self, country_code):
def confirm_email_template(self, country_code, username, subject):
"""Get confirm email template"""
context = {'token': self.confirm_email_token,
'country_code': country_code}
context.update(self.base_template)
context.update(self.base_template(country_code, username, subject))
return render_to_string(
template_name=settings.CONFIRM_EMAIL_TEMPLATE,
context=context)
context=context), get_template(settings.CONFIRM_EMAIL_TEMPLATE).render(context)
def change_email_template(self, country_code):
def change_email_template(self, country_code, username, subject):
"""Get change email template"""
context = {'token': self.change_email_token,
'country_code': country_code}
context.update(self.base_template)
context.update(self.base_template(country_code, username, subject))
return render_to_string(
template_name=settings.CHANGE_EMAIL_TEMPLATE,
context=context)
context=context), get_template(settings.CHANGE_EMAIL_TEMPLATE).render(context)
@property
def favorite_establishment_ids(self):

View File

@ -72,11 +72,13 @@ class UserSerializer(serializers.ModelSerializer):
if settings.USE_CELERY:
tasks.change_email_address.delay(
user_id=instance.id,
country_code=self.context.get('request').country_code)
country_code=self.context.get('request').country_code,
emails=[validated_data['email'],])
else:
tasks.change_email_address(
user_id=instance.id,
country_code=self.context.get('request').country_code)
country_code=self.context.get('request').country_code,
emails=[validated_data['email'],])
return instance

View File

@ -10,11 +10,12 @@ from account.models import User
logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO)
logger = logging.getLogger(__name__)
def send_email(user_id: int, subject: str, message_prop: str, country_code: str):
def send_email(user_id: int, subject: str, message_prop: str, country_code: str, emails=None):
try:
user = User.objects.get(id=user_id)
user.send_email(subject=_(subject),
message=getattr(user, message_prop, lambda _: '')(country_code))
message=getattr(user, message_prop, lambda _: '')(country_code, user.username, _(subject)),
emails=emails)
except:
cur_frame = inspect.currentframe()
cal_frame = inspect.getouterframes(cur_frame, 2)
@ -35,9 +36,9 @@ def confirm_new_email_address(user_id, country_code):
@shared_task
def change_email_address(user_id, country_code):
def change_email_address(user_id, country_code, emails=None):
"""Send email to user new email."""
send_email(user_id, 'Validate new email address', 'change_email_template', country_code)
send_email(user_id, 'Validate new email address', 'change_email_template', country_code, emails)
@shared_task

View File

@ -1,28 +1,55 @@
from django.db.models import Value, IntegerField, F
from pprint import pprint
from transfer.models import Profiles, Accounts
from django.db.models import Q
from transfer.models import Accounts, Identities
from transfer.serializers.account import UserSerializer
from transfer.serializers.user_social_auth import UserSocialAuthSerializer
STOP_LIST = (
'cyril@tomatic.net',
'cyril2@tomatic.net',
'd.sadykova@id-east.ru',
'd.sadykova@octopod.ru',
'n.yurchenko@id-east.ru',
)
def transfer_user():
# queryset = Profiles.objects.all()
# queryset = queryset.annotate(nickname=F("account__nickname"))
# queryset = queryset.annotate(email=F("account__email"))
stop_list = ['cyril@tomatic.net',
'cyril2@tomatic.net',
'd.sadykova@id-east.ru',
'd.sadykova@octopod.ru',
'n.yurchenko@id-east.ru']
queryset = Accounts.objects.filter(confirmed_at__isnull=False).exclude(email__in=stop_list)
# queryset = queryset.annotate(nickname=F('account__nickname'))
# queryset = queryset.annotate(email=F('account__email'))
queryset = Accounts.objects.filter(confirmed_at__isnull=False).exclude(email__in=STOP_LIST)
serialized_data = UserSerializer(data=list(queryset.values()), many=True)
if serialized_data.is_valid():
serialized_data.save()
else:
pprint(f"News serializer errors: {serialized_data.errors}")
pprint(f'News serializer errors: {serialized_data.errors}')
def transfer_identities():
queryset = Identities.objects.exclude(
Q(account_id__isnull=True) |
Q(account__confirmed_at__isnull=True) |
Q(account__email__in=STOP_LIST)
).values_list(
'account_id',
'provider',
'uid',
)
serialized_data = UserSocialAuthSerializer(data=list(queryset.values()), many=True)
if serialized_data.is_valid():
serialized_data.save()
else:
pprint(f'UserSocialAuth serializer errors: {serialized_data.errors}')
data_types = {
"account": [transfer_user]
'account': [transfer_user],
'identities': [transfer_identities],
}

View File

@ -33,7 +33,7 @@ class Application(PlatformMixin, AbstractApplication):
swappable = "OAUTH2_PROVIDER_APPLICATION_MODEL"
def natural_key(self):
return (self.client_id,)
return self.client_id
class JWTAccessTokenManager(models.Manager):

View File

@ -38,13 +38,13 @@ class SignupSerializer(serializers.ModelSerializer):
valid = utils_methods.username_validator(username=value)
if not valid:
raise utils_exceptions.NotValidUsernameError()
if account_models.User.objects.filter(username__icontains=value).exists():
if account_models.User.objects.filter(username__iexact=value).exists():
raise serializers.ValidationError()
return value
def validate_email(self, value):
"""Validate email"""
if account_models.User.objects.filter(email__icontains=value).exists():
if account_models.User.objects.filter(email__iexact=value).exists():
raise serializers.ValidationError()
return value

View File

@ -16,7 +16,7 @@ def send_confirm_email(user_id: int, country_code: str):
try:
obj = account_models.User.objects.get(id=user_id)
obj.send_email(subject=_('Email confirmation'),
message=obj.confirm_email_template(country_code))
message=obj.confirm_email_template(country_code, obj.username, _('Email confirmation')))
except Exception as e:
logger.error(f'METHOD_NAME: {send_confirm_email.__name__}\n'
f'DETAIL: user {user_id}, - {e}')

View File

@ -30,24 +30,17 @@ from utils.views import JWTGenericViewMixin
# OAuth2
class BaseOAuth2ViewMixin(generics.GenericAPIView):
"""BaseMixin for classic auth views"""
def get_client_id(self, source) -> str:
"""Get application client id"""
qs = Application.objects.by_source(source=source)
if qs.exists():
return qs.first().client_id
else:
raise utils_exceptions.ServiceError(data={
'detail': _('Application is not found')})
def get_client_secret(self, source) -> str:
"""Get application client id"""
if source == Application.MOBILE:
qs = Application.objects.by_source(source=source)
if qs.exists:
return qs.first().client_secret
else:
raise utils_exceptions.ServiceError(data={
'detail': _('Not found an application with this source')})
def get_client_credentials(self, source) -> dict:
"""Get application credentials by source."""
credentials = {}
qs = Application.objects.filter(authorization_grant_type=Application.GRANT_PASSWORD,
source=source)
if qs.exists():
application = qs.first()
credentials = dict(client_id=application.client_id,
client_secret=application.client_secret)
return credentials
class OAuth2ViewMixin(CsrfExemptMixin, OAuthLibMixin, BaseOAuth2ViewMixin):
@ -59,20 +52,22 @@ class OAuth2ViewMixin(CsrfExemptMixin, OAuthLibMixin, BaseOAuth2ViewMixin):
def prepare_request_data(self, validated_data: dict) -> dict:
"""Preparing request data"""
source = validated_data.get('source')
# Set OAuth2 request parameters
_request_data = {
'client_id': self.get_client_id(source)
}
# Fill client secret parameter by platform
if validated_data.get('source') == Application.MOBILE:
_request_data['client_secret'] = self.get_client_secret(source)
# Fill token parameter if transfer
if validated_data.get('token'):
_request_data['token'] = validated_data.get('token')
if _request_data:
return _request_data
credentials = self.get_client_credentials(source=source)
client_id = credentials.get('client_id')
client_secret = credentials.get('client_secret')
token = validated_data.get('token')
if client_id and client_secret and token:
return {
'client_id': client_id,
'client_secret': client_secret,
'token': token,
}
else:
raise utils_exceptions.ServiceError()
raise utils_exceptions.ServiceError(data={
'detail': _('Validation OAuth2 request data error')
})
# Sign in via Facebook

View File

@ -6,3 +6,4 @@ from . import models
@admin.register(models.Comment)
class CommentModelAdmin(admin.ModelAdmin):
"""Model admin for model Comment"""
raw_id_fields = ('user', 'country')

View File

@ -32,21 +32,16 @@ class CommentQuerySet(ContentTypeQuerySetMixin):
class Comment(ProjectBaseMixin):
"""Comment model."""
text = models.TextField(verbose_name=_('Comment text'))
mark = models.PositiveIntegerField(blank=True, null=True, default=None,
verbose_name=_('Mark'))
user = models.ForeignKey('account.User',
related_name='comments',
on_delete=models.CASCADE,
verbose_name=_('User'))
mark = models.PositiveIntegerField(blank=True, null=True, default=None, verbose_name=_('Mark'))
user = models.ForeignKey('account.User', related_name='comments', on_delete=models.CASCADE, verbose_name=_('User'))
country = models.ForeignKey(Country, verbose_name=_('Country'), on_delete=models.SET_NULL, null=True)
old_id = models.IntegerField(null=True, blank=True, default=None)
content_type = models.ForeignKey(generic.ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey('content_type', 'object_id')
objects = CommentQuerySet.as_manager()
country = models.ForeignKey(Country, verbose_name=_('Country'),
on_delete=models.SET_NULL, null=True)
old_id = models.IntegerField(null=True, blank=True, default=None)
class Meta:
"""Meta class"""

View File

@ -1,11 +1,31 @@
from pprint import pprint
from django.db.models import Q
from account.transfer_data import STOP_LIST
from transfer.models import Comments
from transfer.serializers.comments import CommentSerializer
def transfer_comments():
queryset = Comments.objects.filter(account__isnull=False, mark__isnull=False)\
.only("id", "comment", "mark", "locale", "establishment_id", "account_id")
# В queryset исключены объекты по условию в связанные моделях
# см. transfer_establishment() и transfer_user()
queryset = Comments.objects.exclude(
Q(establishment__type='Wineyard') |
Q(establishment__location__timezone__isnull=True) |
Q(account__confirmed_at__isnull=True) |
Q(account__email__in=STOP_LIST)
).filter(
account__isnull=False,
mark__isnull=False
).only(
'id',
'comment',
'mark',
'locale',
'establishment_id',
'account_id',
)
serialized_data = CommentSerializer(data=list(queryset.values()), many=True)
if serialized_data.is_valid():
@ -15,7 +35,7 @@ def transfer_comments():
data_types = {
"tmp": [
# transfer_comments
'comment': [
transfer_comments
]
}

View File

@ -0,0 +1,57 @@
from django.core.management.base import BaseCommand
from django.db import connections
from establishment.management.commands.add_position import namedtuplefetchall
from establishment.models import Establishment, Position, Employee, EstablishmentEmployee
class Command(BaseCommand):
help = '''Add employee position from old db to new db.
Run after add_position and add_employee!'''
def empl_position_sql(self):
with connections['legacy'].cursor() as cursor:
cursor.execute('''
select t.id,
t.profile_id,
t.establishment_id,
t.role,
t.start_date,
t.end_date
from
(
select
DISTINCT
a.id,
a.profile_id,
a.establishment_id,
a.role,
a.start_date,
a.end_date,
trim(CONCAT(p.firstname, ' ', p.lastname, ' ',
p.email,'')
) as name
from affiliations as a
join profiles p on p.id = a.profile_id
) t
where t.name is not null
''')
return namedtuplefetchall(cursor)
def handle(self, *args, **options):
objects = []
for e in self.empl_position_sql():
pos = Position.objects.filter(name={"en-GB": e.role}).first()
empl = Employee.objects.filter(old_id=e.profile_id).first()
est = Establishment.objects.filter(old_id=e.establishment_id).first()
if pos and empl and est:
est_empl = EstablishmentEmployee(
from_date=e.start_date, to_date=e.end_date,
old_id=e.id
)
est_empl.establishment = est
est_empl.employee = empl
est_empl.position = pos
objects.append(est_empl)
ee = EstablishmentEmployee.objects.bulk_create(objects)
self.stdout.write(self.style.WARNING(f'Created employee position objects.'))

View File

@ -0,0 +1,38 @@
from django.core.management.base import BaseCommand
from django.db import connections
from establishment.management.commands.add_position import namedtuplefetchall
from establishment.models import Employee
from django.db.models import Q
class Command(BaseCommand):
help = 'Add employee from old db to new db.'
def employees_sql(self):
with connections['legacy'].cursor() as cursor:
cursor.execute('''
select t.profile_id, t.name
from
(
select
DISTINCT
a.profile_id,
trim(CONCAT(p.firstname, ' ', p.lastname, ' ',
p.email,'')
) as name
from affiliations as a
join profiles p on p.id = a.profile_id
) t
where t.name is not null
''')
return namedtuplefetchall(cursor)
def handle(self, *args, **options):
objects = []
for e in self.employees_sql():
count = Employee.objects.filter(Q(old_id=e.profile_id) | Q(name=e.name)).count()
if count == 0:
objects.append(Employee(name=e.name, old_id=e.profile_id))
print(e.name)
empls = Employee.objects.bulk_create(objects)
self.stdout.write(self.style.WARNING(f'Created employee objects.'))

View File

@ -0,0 +1,34 @@
from django.core.management.base import BaseCommand
from django.db import connections
from collections import namedtuple
from establishment.models import Position
def namedtuplefetchall(cursor):
"Return all rows from a cursor as a namedtuple"
desc = cursor.description
nt_result = namedtuple('Result', [col[0] for col in desc])
return [nt_result(*row) for row in cursor.fetchall()]
class Command(BaseCommand):
help = 'Add position from old db to new db'
def position_sql(self):
with connections['legacy'].cursor() as cursor:
cursor.execute('''
select
DISTINCT a.`role` as position_name
from affiliations as a
''')
return namedtuplefetchall(cursor)
def handle(self, *args, **kwargs):
objects = []
for p in self.position_sql():
count = Position.objects.filter(name={"en-GB": p.position_name}).count()
if count == 0:
objects.append(Position(name={"en-GB": p.position_name}))
p = Position.objects.bulk_create(objects)
self.stdout.write(self.style.WARNING(f'Created positions objects.'))

View File

@ -0,0 +1,28 @@
# Generated by Django 2.2.4 on 2019-11-06 07:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0054_auto_20191103_2117'),
]
operations = [
migrations.RenameField(
model_name='establishment',
old_name='name_translated',
new_name='index_name',
),
migrations.AlterField(
model_name='establishment',
name='index_name',
field=models.CharField(default='', max_length=255, verbose_name='Index name'),
),
migrations.AddField(
model_name='establishment',
name='transliterated_name',
field=models.CharField(default='', max_length=255, verbose_name='Transliterated name'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-11-05 13:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0054_auto_20191103_2117'),
]
operations = [
migrations.AddField(
model_name='employee',
name='old_id',
field=models.IntegerField(blank=True, null=True, verbose_name='Old id'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.4 on 2019-11-05 14:01
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('establishment', '0055_employee_old_id'),
]
operations = [
migrations.AlterField(
model_name='establishmentemployee',
name='from_date',
field=models.DateTimeField(blank=True, default=django.utils.timezone.now, null=True, verbose_name='From date'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-11-05 14:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0056_auto_20191105_1401'),
]
operations = [
migrations.AddField(
model_name='establishmentemployee',
name='old_id',
field=models.IntegerField(blank=True, null=True, verbose_name='Old id'),
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-11-06 09:21
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('establishment', '0055_auto_20191106_0740'),
('establishment', '0057_establishmentemployee_old_id'),
]
operations = [
]

View File

@ -301,8 +301,9 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
name = models.CharField(_('name'), max_length=255, default='')
name_translated = models.CharField(_('Transliterated name'),
max_length=255, default='')
transliterated_name = models.CharField(default='', max_length=255,
verbose_name=_('Transliterated name'))
index_name = models.CharField(_('Index name'), max_length=255, default='')
description = TJSONField(blank=True, null=True, default=None,
verbose_name=_('description'),
help_text='{"en-GB":"some text"}')
@ -471,10 +472,10 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
now_at_est_tz = datetime.now(tz=self.tz)
current_week = now_at_est_tz.weekday()
schedule_for_today = self.schedule.filter(weekday=current_week).first()
if schedule_for_today is None or schedule_for_today.closed_at is None or schedule_for_today.opening_at is None:
if schedule_for_today is None or schedule_for_today.opening_time is None or schedule_for_today.ending_time is None:
return False
time_at_est_tz = now_at_est_tz.time()
return schedule_for_today.closed_at > time_at_est_tz > schedule_for_today.opening_at
return schedule_for_today.ending_time > time_at_est_tz > schedule_for_today.opening_time
@property
def tags_indexing(self):
@ -557,11 +558,14 @@ class EstablishmentEmployee(BaseAttributes):
verbose_name=_('Establishment'))
employee = models.ForeignKey('establishment.Employee', on_delete=models.PROTECT,
verbose_name=_('Employee'))
from_date = models.DateTimeField(default=timezone.now, verbose_name=_('From date'))
from_date = models.DateTimeField(default=timezone.now, verbose_name=_('From date'),
null=True, blank=True)
to_date = models.DateTimeField(blank=True, null=True, default=None,
verbose_name=_('To date'))
position = models.ForeignKey(Position, on_delete=models.PROTECT,
verbose_name=_('Position'))
# old_id = affiliations_id
old_id = models.IntegerField(verbose_name=_('Old id'), null=True, blank=True)
objects = EstablishmentEmployeeQuerySet.as_manager()
@ -578,6 +582,8 @@ class Employee(BaseAttributes):
awards = generic.GenericRelation(to='main.Award', related_query_name='employees')
tags = models.ManyToManyField('tag.Tag', related_name='employees',
verbose_name=_('Tags'))
# old_id = profile_id
old_id = models.IntegerField(verbose_name=_('Old id'), null=True, blank=True)
class Meta:
"""Meta class."""

View File

@ -5,6 +5,7 @@ from establishment.serializers import (
EstablishmentBaseSerializer, PlateSerializer, ContactEmailsSerializer,
ContactPhonesSerializer, SocialNetworkRelatedSerializers,
EstablishmentTypeBaseSerializer)
from location.serializers import AddressDetailSerializer
from main.models import Currency
from utils.decorators import with_base_attributes
from utils.serializers import TimeZoneChoiceField
@ -28,6 +29,8 @@ class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
fields = [
'id',
'name',
'transliterated_name',
'index_name',
'website',
'phones',
'emails',
@ -42,6 +45,7 @@ class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
'is_publish',
'guestonline_id',
'lastable_id',
'tags',
'tz',
]
@ -53,6 +57,7 @@ class EstablishmentRUDSerializer(EstablishmentBaseSerializer):
source='establishment_type',
queryset=models.EstablishmentType.objects.all(), write_only=True
)
address = AddressDetailSerializer()
phones = ContactPhonesSerializer(read_only=False, many=True, )
emails = ContactEmailsSerializer(read_only=False, many=True, )
socials = SocialNetworkRelatedSerializers(read_only=False, many=True, )
@ -73,7 +78,9 @@ class EstablishmentRUDSerializer(EstablishmentBaseSerializer):
'socials',
'image_url',
# TODO: check in admin filters
'is_publish'
'is_publish',
'address',
'tags',
]

View File

@ -196,7 +196,8 @@ class EstablishmentBaseSerializer(ProjectModelSerializer):
fields = [
'id',
'name',
'name_translated',
'transliterated_name',
'index_name',
'price_level',
'toque_number',
'public_mark',

View File

@ -1,6 +1,5 @@
from pprint import pprint
import requests
from django.db.models import Q, F
from establishment.models import Establishment

View File

@ -3,7 +3,7 @@ from django.shortcuts import get_object_or_404
from rest_framework import generics
from utils.permissions import IsCountryAdmin, IsEstablishmentManager
from establishment import models, serializers
from establishment import filters, models, serializers
from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer
@ -17,9 +17,11 @@ class EstablishmentMixinViews:
class EstablishmentListCreateView(EstablishmentMixinViews, generics.ListCreateAPIView):
"""Establishment list/create view."""
filter_class = filters.EstablishmentFilter
permission_classes = [IsCountryAdmin | IsEstablishmentManager]
queryset = models.Establishment.objects.all()
serializer_class = serializers.EstablishmentListCreateSerializer
permission_classes = [IsCountryAdmin|IsEstablishmentManager]
class EstablishmentRUDView(generics.RetrieveUpdateDestroyAPIView):

View File

@ -162,7 +162,7 @@ class EstablishmentNearestRetrieveView(EstablishmentListView, generics.ListAPIVi
qs = super(EstablishmentNearestRetrieveView, self).get_queryset()
if lat and lon and radius and unit:
center = Point(float(lat), float(lon))
center = Point(float(lon), float(lat))
filter_kwargs = {'center': center, 'radius': float(radius), 'unit': unit}
return qs.by_distance_from_point(**{k: v for k, v in filter_kwargs.items()
if v is not None})

View File

@ -69,6 +69,7 @@ class CitySerializer(serializers.ModelSerializer):
queryset=models.Country.objects.all(),
write_only=True
)
country = CountrySerializer()
class Meta:
model = models.City
@ -79,6 +80,7 @@ class CitySerializer(serializers.ModelSerializer):
'region',
'region_id',
'country_id',
'country',
'postal_code',
'is_island',
]

View File

@ -38,6 +38,7 @@ class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView):
# Region
class RegionListCreateView(common.RegionViewMixin, generics.ListCreateAPIView):
"""Create view for model Region"""
pagination_class = None
serializer_class = serializers.RegionSerializer
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]

View File

View File

@ -0,0 +1,40 @@
from django.core.management.base import BaseCommand
from django.db import connections
from establishment.management.commands.add_position import namedtuplefetchall
from main.models import Award, AwardType
from establishment.models import Employee
class Command(BaseCommand):
help = '''Add award from old db to new db.
Run after command add_award_type!'''
def award_sql(self):
with connections['legacy'].cursor() as cursor:
cursor.execute('''
select
DISTINCT
a.id, a.profile_id, a.title,
a.`year` as vintage_year, a.state,
t.id as award_type
from awards as a
join award_types t on t.id = a.award_type_id
join profiles p on p.id = a.profile_id
''')
return namedtuplefetchall(cursor)
def handle(self, *args, **kwargs):
objects =[]
for a in self.award_sql():
profile = Employee.objects.filter(old_id=a.profile_id).first()
type = AwardType.objects.filter(old_id=a.award_type).first()
state = Award.PUBLISHED if a.state == 'published' else Award.WAITING
if profile and type:
award = Award(award_type=type, vintage_year=a.vintage_year,
title={"en-GB": a.title}, state=state,
content_object=profile, old_id=a.id)
objects.append(award)
awards = Award.objects.bulk_create(objects)
self.stdout.write(self.style.WARNING(f'Created awards objects.'))

View File

@ -0,0 +1,34 @@
from django.core.management.base import BaseCommand
from django.db import connections
from establishment.management.commands.add_position import namedtuplefetchall
from main.models import AwardType
from location.models import Country
class Command(BaseCommand):
help = '''Add award types from old db to new db.
Run after migrate country code!'''
def award_types_sql(self):
with connections['legacy'].cursor() as cursor:
cursor.execute('''
SELECT
DISTINCT
at.id, TRIM(at.title) AS name,
s.country_code_2 AS country_code
FROM award_types as at
JOIN sites s on s.id = at.site_id
WHERE LENGTH(TRIM(at.title))>0
''')
return namedtuplefetchall(cursor)
def handle(self, *args, **kwargs):
objects =[]
for a in self.award_types_sql():
country = Country.objects.filter(code=a.country_code).first()
if country:
type = AwardType(name=a.name, old_id=a.id)
type.country = country
objects.append(type)
types = AwardType.objects.bulk_create(objects)
self.stdout.write(self.style.WARNING(f'Created award types objects.'))

View File

@ -1,11 +1,13 @@
"""Main app methods."""
import logging
from typing import Tuple, Optional
from django.conf import settings
from django.contrib.gis.geoip2 import GeoIP2, GeoIP2Exception
from geoip2.models import City
from typing import Union, Tuple, Optional
import pycountry
from main import models
logger = logging.getLogger(__name__)
@ -83,3 +85,16 @@ def determine_user_city(ip_addr: str) -> Optional[City]:
except Exception as ex:
logger.warning(f'GEOIP Base exception: {ex}')
return None
def determine_subdivision(
country_alpha2_code: str,
subdivision_code: Union[str, int]
) -> pycountry.Subdivision:
"""
:param country_alpha2_code: country code according to ISO 3166-1 alpha-2 standard
:param subdivision_code: subdivision code according to ISO 3166-2 without country code prefix
:return: subdivision
"""
iso3166_2_subdivision_code = f'{country_alpha2_code}-{subdivision_code}'
return pycountry.subdivisions.get(code=iso3166_2_subdivision_code)

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-11-06 07:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0031_auto_20191103_2037'),
]
operations = [
migrations.AddField(
model_name='awardtype',
name='old_id',
field=models.IntegerField(blank=True, null=True),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 2.2.4 on 2019-11-06 07:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0032_awardtype_old_id'),
]
operations = [
migrations.AddField(
model_name='award',
name='old_id',
field=models.IntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='award',
name='state',
field=models.PositiveSmallIntegerField(choices=[(0, 'waiting'), (1, 'published')], default=0, verbose_name='State'),
),
]

View File

@ -42,6 +42,9 @@ class SiteSettingsQuerySet(models.QuerySet):
def with_country(self):
return self.filter(country__isnull=False)
def by_country_code(self, code):
return self.filter(country__code=code)
class SiteSettings(ProjectBaseMixin):
subdomain = models.CharField(max_length=255, db_index=True, unique=True,
@ -161,6 +164,14 @@ class SiteFeature(ProjectBaseMixin):
class Award(TranslatedFieldsMixin, URLImageMixin, models.Model):
"""Award model."""
WAITING = 0
PUBLISHED = 1
STATE_CHOICES = (
(WAITING,'waiting'),
(PUBLISHED, 'published')
)
award_type = models.ForeignKey('main.AwardType', on_delete=models.CASCADE)
title = TJSONField(
_('title'), null=True, blank=True,
@ -171,6 +182,11 @@ class Award(TranslatedFieldsMixin, URLImageMixin, models.Model):
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey('content_type', 'object_id')
state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES,
verbose_name=_('State'))
old_id = models.IntegerField(null=True, blank=True)
def __str__(self):
title = 'None'
lang = TranslationSettings.get_solo().default_language
@ -184,6 +200,7 @@ class AwardType(models.Model):
country = models.ForeignKey(
'location.Country', verbose_name=_('country'), on_delete=models.CASCADE)
name = models.CharField(_('name'), max_length=255)
old_id = models.IntegerField(null=True, blank=True)
def __str__(self):
return self.name

View File

@ -1,5 +1,6 @@
"""Main app views."""
from django.http import Http404
from django.conf import settings
from rest_framework import generics, permissions
from rest_framework.response import Response

View File

@ -18,29 +18,12 @@ class NewsMixinView:
serializer_class = serializers.NewsBaseSerializer
def get_queryset(self, *args, **kwargs):
from django.conf import settings
"""Override get_queryset method."""
qs = models.News.objects.published() \
.with_base_related() \
.order_by('-is_highlighted', '-created')
country_code = self.request.country_code
if country_code:
# temp code
# Temporary stub for international news logic
# (по договорённости с заказчиком на демонстрации 4 ноября
# здесь будет 6 фиксированных новостей)
# TODO replace this stub with actual logic
if hasattr(settings, 'HARDCODED_INTERNATIONAL_NEWS_IDS'):
if country_code and country_code != 'www' and country_code != 'main':
qs = qs.by_country_code(country_code)
else:
qs = models.News.objects.filter(
old_id__in=settings.HARDCODED_INTERNATIONAL_NEWS_IDS)
return qs
# temp code
qs = qs.by_country_code(country_code)
return qs
@ -58,9 +41,10 @@ class NewsDetailView(NewsMixinView, generics.RetrieveAPIView):
lookup_field = 'slug'
serializer_class = serializers.NewsDetailWebSerializer
queryset = models.News.objects.all()
def get_queryset(self):
"""Override get_queryset method."""
return super().get_queryset().with_extended_related()
return self.queryset
class NewsTypeListView(generics.ListAPIView):

View File

@ -90,7 +90,8 @@ class EstablishmentDocument(Document):
fields = (
'id',
'name',
'name_translated',
'transliterated_name',
'index_name',
'is_publish',
'price_level',
'toque_number',

View File

@ -96,7 +96,8 @@ class EstablishmentDocumentSerializer(DocumentSerializer):
fields = (
'id',
'name',
'name_translated',
'transliterated_name',
'index_name',
'price_level',
'toque_number',
'public_mark',

View File

@ -46,6 +46,12 @@ class NewsDocumentViewSet(BaseDocumentViewSet):
]
},
'slug': 'slug',
'country_id': {
'field': 'country.id'
},
'country': {
'field': 'country.code'
},
}
@ -73,8 +79,10 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
search_fields = {
'name': {'fuzziness': 'auto:3,4',
'boost': '2'},
'name_translated': {'fuzziness': 'auto:3,4',
'boost': '2'},
'transliterated_name': {'fuzziness': 'auto:3,4',
'boost': '2'},
'index_name': {'fuzziness': 'auto:3,4',
'boost': '2'},
'description': {'fuzziness': 'auto'},
}
translated_search_fields = (

View File

@ -54,18 +54,3 @@ class TagsFilterSet(TagsBaseFilterSet):
model = models.Tag
fields = ('type',)
# TMP TODO remove it later
# Временный хардкод для демонстрации 4 ноября, потом удалить!
def filter_by_type(self, queryset, name, value):
""" Overrides base filter. Temporary decision"""
if not (settings.NEWS_CHOSEN_TAGS and settings.ESTABLISHMENT_CHOSEN_TAGS):
return super().filter_by_type(queryset, name, value)
queryset = models.Tag.objects
if self.NEWS in value:
queryset = queryset.for_news().filter(value__in=settings.NEWS_CHOSEN_TAGS).distinct('value')
if self.ESTABLISHMENT in value:
queryset = queryset.for_establishments().filter(value__in=settings.ESTABLISHMENT_CHOSEN_TAGS).distinct(
'value')
return queryset

View File

@ -23,23 +23,6 @@ class ChosenTagsView(generics.ListAPIView, viewsets.GenericViewSet):
.filter(id__in=result_tags_ids) \
.order_by_priority()
def list(self, request, *args, **kwargs):
# TMP TODO remove it later
# Временный хардкод для демонстрации 4 ноября, потом удалить!
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
result_list = serializer.data
if request.query_params.get('type') and (settings.ESTABLISHMENT_CHOSEN_TAGS or settings.NEWS_CHOSEN_TAGS):
ordered_list = settings.ESTABLISHMENT_CHOSEN_TAGS if request.query_params.get('type') == 'establishment' else settings.NEWS_CHOSEN_TAGS
result_list = sorted(result_list, key=lambda x: ordered_list.index(x['index_name']))
return Response(result_list)
# User`s views & viewsets
class TagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):

View File

@ -39,13 +39,21 @@ class Timetable(ProjectBaseMixin):
def closed_at_str(self):
return str(self.closed_at) if self.closed_at else None
@property
def opening_time(self):
return self.opening_at or self.lunch_start or self.dinner_start
@property
def ending_time(self):
return self.closed_at or self.dinner_end or self.lunch_end
@property
def works_at_noon(self):
return bool(self.closed_at and self.closed_at <= self.NOON)
return bool(self.opening_time and self.opening_time <= self.NOON)
@property
def works_at_afternoon(self):
return bool(self.closed_at and self.closed_at > self.NOON)
return bool(self.ending_time and self.ending_time > self.NOON)
class Meta:
"""Meta class."""

View File

@ -23,12 +23,14 @@ class Command(BaseCommand):
'menu',
'location_establishment',
'whirligig',
'identities',
]
LONG_DATA_TYPES = [
'update_country_flag',
'wine_characteristics',
'product',
'comment',
]
def handle(self, *args, **options):

View File

@ -1018,6 +1018,20 @@ class MetadatumAliases(MigrateMixin):
db_table = 'metadatum_aliases'
class Identities(MigrateMixin):
using = 'legacy'
account = models.ForeignKey(Accounts, models.DO_NOTHING, blank=True, null=True)
provider = models.CharField(max_length=255, blank=True, null=True)
uid = models.CharField(max_length=255, blank=True, null=True)
created_at = models.DateTimeField()
updated_at = models.DateTimeField()
class Meta:
managed = False
db_table = 'identities'
class WineLocations(MigrateMixin):
using = 'legacy'

View File

@ -1,9 +1,10 @@
from rest_framework import serializers
from comment.models import Comment, User
from establishment.models import Establishment
from location.models import Country
class CommentSerializer(serializers.ModelSerializer):
class CommentSerializer(serializers.Serializer):
id = serializers.IntegerField()
comment = serializers.CharField()
mark = serializers.DecimalField(max_digits=4, decimal_places=2)
@ -11,54 +12,45 @@ class CommentSerializer(serializers.ModelSerializer):
account_id = serializers.IntegerField()
establishment_id = serializers.CharField()
class Meta:
model = Comment
fields = (
"id",
"comment",
"mark",
"locale",
"account_id",
"establishment_id"
)
def validate(self, data):
data = self.set_old_id(data)
data = self.set_text(data)
data = self.set_mark(data)
data = self.set_establishment(data)
data = self.set_account(data)
data.update({
'old_id': data.pop('id'),
'text': data.pop('comment'),
'mark': data['mark'] * -1 if data['mark'] < 0 else data['mark'],
'content_object': self.get_content_object(data),
'user': self.get_account(data),
'country': self.get_country(data),
})
data.pop('establishment_id')
data.pop('account_id')
data.pop('locale')
return data
def set_text(self, data):
data['text'] = data.pop('comment')
return data
def set_mark(self, data):
if data['mark'] < 0:
data['mark'] = data['mark'] * -1
return data
def set_account(self, data):
def create(self, validated_data):
try:
data['account'] = User.objects.filter(old_id=data['account_id']).first()
except User.DoesNotExist as e:
raise ValueError(f"User account not found with {data}: {e}")
return Comment.objects.create(**validated_data)
except Exception as e:
raise ValueError(f"Error creating comment with {validated_data}: {e}")
del(data['account_id'])
@staticmethod
def get_content_object(data):
establishment = Establishment.objects.filter(old_id=data['establishment_id']).first()
if not establishment:
raise ValueError(f"Establishment not found with old_id {data['establishment_id']}: ")
return establishment
return data
@staticmethod
def get_account(data):
user = User.objects.filter(old_id=data['account_id']).first()
if not user:
raise ValueError(f"User account not found with old_id {data['account_id']}")
return user
def set_establishment(self, data):
try:
data['establishment'] = Establishment.objects.filter(old_id=data['account_id']).first()
except Establishment.DoesNotExist as e:
raise ValueError(f"Establishment not found with {data}: {e}")
del(data['establishment_id'])
return data
def set_old_id(self, data):
data['old_id'] = data.pop("id")
return data
@staticmethod
def get_country(data):
locale = data['locale']
country_code = locale[:locale.index("-")] if len(locale) > 2 else locale
country = Country.objects.filter(code=country_code).first()
if not country:
raise ValueError(f"Country not found with code {country_code}")
return country

View File

@ -0,0 +1,30 @@
from rest_framework import serializers
from social_django.models import UserSocialAuth
from account.models import User
class UserSocialAuthSerializer(serializers.Serializer):
account_id = serializers.IntegerField()
provider = serializers.CharField()
uid = serializers.CharField()
def validate(self, data):
data.update({
'user': self.get_account(data),
})
data.pop('account_id')
return data
def create(self, validated_data):
try:
return UserSocialAuth.objects.create(**validated_data)
except Exception as e:
raise ValueError(f"Error creating UserSocialAuth with {validated_data}: {e}")
@staticmethod
def get_account(data):
user = User.objects.filter(old_id=data['account_id']).first()
if not user:
raise ValueError(f"User account not found with old_id {data['account_id']}")
return user

View File

@ -74,6 +74,7 @@ PROJECT_APPS = [
'comment.apps.CommentConfig',
'favorites.apps.FavoritesConfig',
'rating.apps.RatingConfig',
'transfer.apps.TransferConfig',
'tag.apps.TagConfig',
'product.apps.ProductConfig',
]
@ -177,6 +178,14 @@ AUTH_PASSWORD_VALIDATORS = [
},
]
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.BCryptPasswordHasher',
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
]
# Account settings
AUTH_USER_MODEL = 'account.User'
LOGIN_URL = 'admin:login'
@ -216,7 +225,7 @@ REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
# JWT
'utils.authentication.GMJWTAuthentication',
'rest_framework.authentication.SessionAuthentication',
# 'rest_framework.authentication.SessionAuthentication',
),
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning',
'DEFAULT_VERSION': (AVAILABLE_VERSIONS['current'],),
@ -479,8 +488,3 @@ PHONENUMBER_DB_FORMAT = 'NATIONAL'
PHONENUMBER_DEFAULT_REGION = "FR"
FALLBACK_LOCALE = 'en-GB'
# TMP TODO remove it later
# Временный хардкод для демонстрации 4 ноября, потом удалить!
ESTABLISHMENT_CHOSEN_TAGS = ['gastronomic', 'en_vogue', 'terrace', 'streetfood', 'business', 'bar_cocktail', 'brunch', 'pop']
NEWS_CHOSEN_TAGS = ['eat', 'drink', 'cook', 'style', 'international', 'event', 'partnership']

View File

@ -38,10 +38,6 @@ sentry_sdk.init(
integrations=[DjangoIntegration()]
)
# TMP ( TODO remove it later)
# Временный хардкод для демонстрации 4 ноября, потом удалить!
HARDCODED_INTERNATIONAL_NEWS_IDS = [1460, 1471, 1482, 1484, 1611, 1612]
# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
@ -54,14 +50,14 @@ DATABASES = {
'HOST': os.environ.get('DB_HOSTNAME'),
'PORT': os.environ.get('DB_PORT'),
},
# 'legacy': {
# 'ENGINE': 'django.db.backends.mysql',
# 'HOST': os.environ.get('MYSQL_HOSTNAME'),
# 'PORT': os.environ.get('MYSQL_PORT'),
# 'NAME': os.environ.get('MYSQL_DATABASE'),
# 'USER': os.environ.get('MYSQL_USER'),
# 'PASSWORD': os.environ.get('MYSQL_PASSWORD')
# }
'legacy': {
'ENGINE': 'django.db.backends.mysql',
'HOST': os.environ.get('MYSQL_HOSTNAME'),
'PORT': os.environ.get('MYSQL_PORT'),
'NAME': os.environ.get('MYSQL_DATABASE'),
'USER': os.environ.get('MYSQL_USER'),
'PASSWORD': os.environ.get('MYSQL_PASSWORD')
}
}
BROKER_URL = 'redis://localhost:6379/1'

View File

@ -89,6 +89,28 @@ LOGGING = {
}
DATABASES = {
'default': {
'ENGINE': 'django.contrib.gis.db.backends.postgis',
'NAME': os.environ.get('DB_NAME'),
'USER': os.environ.get('DB_USERNAME'),
'PASSWORD': os.environ.get('DB_PASSWORD'),
'HOST': os.environ.get('DB_HOSTNAME'),
'PORT': os.environ.get('DB_PORT'),
},
'legacy': {
'ENGINE': 'django.db.backends.mysql',
# 'HOST': '172.17.0.1',
# 'HOST': '172.23.0.1',
'HOST': 'mysql_db',
'PORT': 3306,
'NAME': 'dev',
'USER': 'dev',
'PASSWORD': 'octosecret123'
}
}
# ELASTICSEARCH SETTINGS
ELASTICSEARCH_DSL = {
'default': {

View File

@ -4,6 +4,9 @@ from .amazon_s3 import *
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
ALLOWED_HOSTS = ['*.next.gaultmillau.com', 'api.gaultmillau.com']
CSRF_TRUSTED_ORIGINS = ['.next.gaultmillau.com', ]
@ -37,10 +40,6 @@ sentry_sdk.init(
integrations=[DjangoIntegration()]
)
# TMP ( TODO remove it later)
# Временный хардкод для демонстрации 4 ноября, потом удалить!
HARDCODED_INTERNATIONAL_NEWS_IDS = [8, 9, 10, 11, 15, 17]
# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
@ -55,7 +54,7 @@ DATABASES = {
},
}
BROKER_URL = 'redis://localhost:6379/1'
BROKER_URL = 'redis://redis:6379/1'
CELERY_RESULT_BACKEND = BROKER_URL
CELERY_BROKER_URL = BROKER_URL

View File

@ -26,8 +26,3 @@ ELASTICSEARCH_INDEX_NAMES = {
# 'search_indexes.documents.news': 'stage_news', #temporarily disabled
'search_indexes.documents.establishment': 'stage_establishment',
}
# TMP ( TODO remove it later)
# Временный хардкод для демонстрации 4 ноября, потом удалить!
HARDCODED_INTERNATIONAL_NEWS_IDS = [1460, 1471, 1482, 1484, 1611, 1612]

View File

@ -1,11 +1,80 @@
{% load i18n %}{% autoescape off %}
{% blocktrans %}You're receiving this email because you want to change email address at {{ site_name }}.{% endblocktrans %}
<!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">
{% trans "Please go to the following page for confirmation new email address:" %}
</div>
https://{{ country_code }}.{{ domain_uri }}/change-email-confirm/{{ uidb64 }}/{{ token }}/
{% trans "Thanks for using our site!" %}
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
<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="#" 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;">
{% blocktrans %}You're receiving this email because you want to change email address at {{ site_name }}.{% endblocktrans %}
<br>
{% trans "Please go to the following page for confirmation new email address:" %}
<br>
https://{{ country_code }}.{{ domain_uri }}/change-email-confirm/{{ uidb64 }}/{{ token }}/
<br>
{% trans "Thanks for using our site!" %}
<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 us on our social network below
</div>
<div class="letter__follow-social">
<a href="{{ facebook_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0 1rem 0 0;background: #ffee29;border: none;">
<img alt="facebook" style="width: 30px; vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/3.png" />
</a>
<a href="{{ instagram_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0 1rem 0 0;background: #ffee29;border: none;">
<img alt="instagram" style="width:30px;vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/4.png" />
</a>
<a href="{{ twitter_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0;background: #ffee29;border: none;">
<img alt="twitter" style="width:30px;vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/5.png" />
</a>
</div>
</div>
</div>
<div class="letter__unsubscribe" style="margin: 0 0 1.25rem;font-size: 12px;text-align: center;">
<span class="letter__unsubscribe-dscr" style="display: inline-block;">This email has been sent to {{ send_to }} ,</span>
</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

@ -1,7 +1,77 @@
{% load i18n %}{% autoescape off %}
{% blocktrans %}You're receiving this email because your account's password address at {{ site_name }}.{% endblocktrans %}
<!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">
{% trans "Thanks for using our site!" %}
</div>
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
<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="#" 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 %}You're receiving this email because your account's password address at {{ site_name }}.{% endblocktrans %}
<br>
{% trans "Thanks for using our site!" %}
<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 us on our social network below
</div>
<div class="letter__follow-social">
<a href="{{ facebook_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0 1rem 0 0;background: #ffee29;border: none;">
<img alt="facebook" style="width: 30px; vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/3.png" />
</a>
<a href="{{ instagram_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0 1rem 0 0;background: #ffee29;border: none;">
<img alt="instagram" style="width:30px;vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/4.png" />
</a>
<a href="{{ twitter_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0;background: #ffee29;border: none;">
<img alt="twitter" style="width:30px;vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/5.png" />
</a>
</div>
</div>
</div>
<div class="letter__unsubscribe" style="margin: 0 0 1.25rem;font-size: 12px;text-align: center;">
<span class="letter__unsubscribe-dscr" style="display: inline-block;">This email has been sent to {{ send_to }} ,</span>
</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

@ -1,11 +1,79 @@
{% load i18n %}{% autoescape off %}
{% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %}
<!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">
{% trans "Please go to the following page and choose a new password:" %}
</div>
https://{{ country_code }}.{{ domain_uri }}/recovery/{{ uidb64 }}/{{ token }}/
{% trans "Thanks for using our site!" %}
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
<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="#" 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;">
{% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %}
<br>
{% trans "Please go to the following page and choose a new password:" %}
<br>
https://{{ country_code }}.{{ domain_uri }}/recovery/{{ uidb64 }}/{{ token }}/
<br>
{% trans "Thanks for using our site!" %}
<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 us on our social network below
</div>
<div class="letter__follow-social">
<a href="{{ facebook_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0 1rem 0 0;background: #ffee29;border: none;">
<img alt="facebook" style="width: 30px; vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/3.png" />
</a>
<a href="{{ instagram_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0 1rem 0 0;background: #ffee29;border: none;">
<img alt="instagram" style="width:30px;vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/4.png" />
</a>
<a href="{{ twitter_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0;background: #ffee29;border: none;">
<img alt="twitter" style="width:30px;vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/5.png" />
</a>
</div>
</div>
</div>
<div class="letter__unsubscribe" style="margin: 0 0 1.25rem;font-size: 12px;text-align: center;">
<span class="letter__unsubscribe-dscr" style="display: inline-block;">This email has been sent to {{ send_to }} ,</span>
</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

@ -1,10 +1,79 @@
{% load i18n %}{% autoescape off %}
{% blocktrans %}You're receiving this email because you trying to register new account at {{ site_name }}.{% endblocktrans %}
<!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">
{% trans "Please confirm your email address to complete the registration:" %}
https://{{ country_code }}.{{ domain_uri }}/registered/{{ uidb64 }}/{{ token }}/
</div>
{% trans "Thanks for using our site!" %}
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
<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="#" 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 %}You're receiving this email because you trying to register new account at {{ site_name }}.{% endblocktrans %}
<br>
{% trans "Please confirm your email address to complete the registration:" %}
https://{{ country_code }}.{{ domain_uri }}/registered/{{ uidb64 }}/{{ token }}/
<br>
{% trans "Thanks for using our site!" %}
<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 us on our social network below
</div>
<div class="letter__follow-social">
<a href="{{ facebook_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0 1rem 0 0;background: #ffee29;border: none;">
<img alt="facebook" style="width: 30px; vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/3.png" />
</a>
<a href="{{ instagram_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0 1rem 0 0;background: #ffee29;border: none;">
<img alt="instagram" style="width:30px;vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/4.png" />
</a>
<a href="{{ twitter_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0;background: #ffee29;border: none;">
<img alt="twitter" style="width:30px;vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/5.png" />
</a>
</div>
</div>
</div>
<div class="letter__unsubscribe" style="margin: 0 0 1.25rem;font-size: 12px;text-align: center;">
<span class="letter__unsubscribe-dscr" style="display: inline-block;">This email has been sent to {{ send_to }} ,</span>
</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 %}

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
Django==2.2.4
Django[bcrypt]==2.2.7
psycopg2-binary==2.8.3
pytz==2019.1
sqlparse==0.3.0
@ -17,6 +17,7 @@ markdown
django-filter==2.1.0
djangorestframework-xml
geoip2==2.9.0
pycountry==19.8.18
django-phonenumber-field[phonenumbers]==2.1.0
django-timezone-field==3.1
@ -47,4 +48,6 @@ PyYAML==5.1.2
# temp solution
redis==3.2.0
amqp>=2.4.0
kombu==4.5.0
celery==4.3.0rc2