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:
commit
c67109d9f8
43
apps/account/management/commands/add_account.py
Normal file
43
apps/account/management/commands/add_account.py
Normal 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.'))
|
||||
31
apps/account/management/commands/add_image.py
Normal file
31
apps/account/management/commands/add_image.py
Normal 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.'))
|
||||
51
apps/account/management/commands/add_social.py
Normal file
51
apps/account/management/commands/add_social.py
Normal 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.'))
|
||||
18
apps/account/migrations/0019_auto_20191108_0827.py
Normal file
18
apps/account/migrations/0019_auto_20191108_0827.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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}')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
]
|
||||
}
|
||||
|
|
|
|||
57
apps/establishment/management/commands/add_empl_position.py
Normal file
57
apps/establishment/management/commands/add_empl_position.py
Normal 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.'))
|
||||
38
apps/establishment/management/commands/add_employee.py
Normal file
38
apps/establishment/management/commands/add_employee.py
Normal 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.'))
|
||||
34
apps/establishment/management/commands/add_position.py
Normal file
34
apps/establishment/management/commands/add_position.py
Normal 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.'))
|
||||
28
apps/establishment/migrations/0055_auto_20191106_0740.py
Normal file
28
apps/establishment/migrations/0055_auto_20191106_0740.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
18
apps/establishment/migrations/0055_employee_old_id.py
Normal file
18
apps/establishment/migrations/0055_employee_old_id.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
19
apps/establishment/migrations/0056_auto_20191105_1401.py
Normal file
19
apps/establishment/migrations/0056_auto_20191105_1401.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
14
apps/establishment/migrations/0058_merge_20191106_0921.py
Normal file
14
apps/establishment/migrations/0058_merge_20191106_0921.py
Normal 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 = [
|
||||
]
|
||||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -196,7 +196,8 @@ class EstablishmentBaseSerializer(ProjectModelSerializer):
|
|||
fields = [
|
||||
'id',
|
||||
'name',
|
||||
'name_translated',
|
||||
'transliterated_name',
|
||||
'index_name',
|
||||
'price_level',
|
||||
'toque_number',
|
||||
'public_mark',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
from pprint import pprint
|
||||
|
||||
import requests
|
||||
from django.db.models import Q, F
|
||||
|
||||
from establishment.models import Establishment
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
0
apps/main/management/__init__.py
Normal file
0
apps/main/management/__init__.py
Normal file
0
apps/main/management/commands/__init__.py
Normal file
0
apps/main/management/commands/__init__.py
Normal file
40
apps/main/management/commands/add_award.py
Normal file
40
apps/main/management/commands/add_award.py
Normal 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.'))
|
||||
|
||||
|
||||
34
apps/main/management/commands/add_award_type.py
Normal file
34
apps/main/management/commands/add_award_type.py
Normal 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.'))
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
18
apps/main/migrations/0032_awardtype_old_id.py
Normal file
18
apps/main/migrations/0032_awardtype_old_id.py
Normal 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),
|
||||
),
|
||||
]
|
||||
23
apps/main/migrations/0033_auto_20191106_0744.py
Normal file
23
apps/main/migrations/0033_auto_20191106_0744.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -90,7 +90,8 @@ class EstablishmentDocument(Document):
|
|||
fields = (
|
||||
'id',
|
||||
'name',
|
||||
'name_translated',
|
||||
'transliterated_name',
|
||||
'index_name',
|
||||
'is_publish',
|
||||
'price_level',
|
||||
'toque_number',
|
||||
|
|
|
|||
|
|
@ -96,7 +96,8 @@ class EstablishmentDocumentSerializer(DocumentSerializer):
|
|||
fields = (
|
||||
'id',
|
||||
'name',
|
||||
'name_translated',
|
||||
'transliterated_name',
|
||||
'index_name',
|
||||
'price_level',
|
||||
'toque_number',
|
||||
'public_mark',
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
30
apps/transfer/serializers/user_social_auth.py
Normal file
30
apps/transfer/serializers/user_social_auth.py
Normal 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
|
||||
|
|
@ -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']
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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': {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
@ -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: "Open Sans", 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:"Open-Sans",sans-serif; font-size: 1.5rem;margin: 0 0 10px;padding: 0 0 6px;border-bottom: 4px solid #ffee29;">
|
||||
<span class="letter__title-txt">{{ title }}</span>
|
||||
</div>
|
||||
<div class="letter__text" style="margin: 0 0 30px; font-family:"Open-Sans",sans-serif; font-size: 14px; line-height: 21px;letter-spacing: -0.34px; overflow-x: hidden;">
|
||||
{% 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: "PT Serif", sans-serif;font-size: 1.25rem;text-transform: uppercase;">
|
||||
<img alt="thumb" style="width:30px;vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/2.png" />
|
||||
<span class="letter__follow-title">Follow us</span>
|
||||
</div>
|
||||
<div class="letter__follow-text" style="display: block;margin: 0 0 30px;font-size: 12px;font-style: italic;">You can also us on our social network below
|
||||
</div>
|
||||
<div class="letter__follow-social">
|
||||
<a href="{{ facebook_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0 1rem 0 0;background: #ffee29;border: none;">
|
||||
<img alt="facebook" style="width: 30px; vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/3.png" />
|
||||
</a>
|
||||
<a href="{{ instagram_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0 1rem 0 0;background: #ffee29;border: none;">
|
||||
<img alt="instagram" style="width:30px;vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/4.png" />
|
||||
</a>
|
||||
<a href="{{ twitter_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0;background: #ffee29;border: none;">
|
||||
<img alt="twitter" style="width:30px;vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/5.png" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="letter__unsubscribe" style="margin: 0 0 1.25rem;font-size: 12px;text-align: center;">
|
||||
<span class="letter__unsubscribe-dscr" style="display: inline-block;">This email has been sent to {{ send_to }} ,</span>
|
||||
</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 %}
|
||||
|
|
@ -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: "Open Sans", 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:"Open-Sans",sans-serif; font-size: 1.5rem;margin: 0 0 10px;padding: 0 0 6px;border-bottom: 4px solid #ffee29;">
|
||||
<span class="letter__title-txt">{{ title }}</span>
|
||||
</div>
|
||||
<div class="letter__text" style="margin: 0 0 30px; font-family:"Open-Sans",sans-serif; font-size: 14px; line-height: 21px;letter-spacing: -0.34px; overflow-x: hidden;">
|
||||
<br>
|
||||
{% blocktrans %}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: "PT Serif", sans-serif;font-size: 1.25rem;text-transform: uppercase;">
|
||||
<img alt="thumb" style="width:30px;vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/2.png" />
|
||||
<span class="letter__follow-title">Follow us</span>
|
||||
</div>
|
||||
<div class="letter__follow-text" style="display: block;margin: 0 0 30px;font-size: 12px;font-style: italic;">You can also us on our social network below
|
||||
</div>
|
||||
<div class="letter__follow-social">
|
||||
<a href="{{ facebook_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0 1rem 0 0;background: #ffee29;border: none;">
|
||||
<img alt="facebook" style="width: 30px; vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/3.png" />
|
||||
</a>
|
||||
<a href="{{ instagram_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0 1rem 0 0;background: #ffee29;border: none;">
|
||||
<img alt="instagram" style="width:30px;vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/4.png" />
|
||||
</a>
|
||||
<a href="{{ twitter_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0;background: #ffee29;border: none;">
|
||||
<img alt="twitter" style="width:30px;vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/5.png" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="letter__unsubscribe" style="margin: 0 0 1.25rem;font-size: 12px;text-align: center;">
|
||||
<span class="letter__unsubscribe-dscr" style="display: inline-block;">This email has been sent to {{ send_to }} ,</span>
|
||||
</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 %}
|
||||
|
|
@ -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: "Open Sans", 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:"Open-Sans",sans-serif; font-size: 1.5rem;margin: 0 0 10px;padding: 0 0 6px;border-bottom: 4px solid #ffee29;">
|
||||
<span class="letter__title-txt">{{ title }}</span>
|
||||
</div>
|
||||
<div class="letter__text" style="margin: 0 0 30px; font-family:"Open-Sans",sans-serif; font-size: 14px; line-height: 21px;letter-spacing: -0.34px; overflow-x: hidden;">
|
||||
{% 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: "PT Serif", sans-serif;font-size: 1.25rem;text-transform: uppercase;">
|
||||
<img alt="thumb" style="width:30px;vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/2.png" />
|
||||
<span class="letter__follow-title">Follow us</span>
|
||||
</div>
|
||||
<div class="letter__follow-text" style="display: block;margin: 0 0 30px;font-size: 12px;font-style: italic;">You can also us on our social network below
|
||||
</div>
|
||||
<div class="letter__follow-social">
|
||||
<a href="{{ facebook_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0 1rem 0 0;background: #ffee29;border: none;">
|
||||
<img alt="facebook" style="width: 30px; vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/3.png" />
|
||||
</a>
|
||||
<a href="{{ instagram_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0 1rem 0 0;background: #ffee29;border: none;">
|
||||
<img alt="instagram" style="width:30px;vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/4.png" />
|
||||
</a>
|
||||
<a href="{{ twitter_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0;background: #ffee29;border: none;">
|
||||
<img alt="twitter" style="width:30px;vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/5.png" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="letter__unsubscribe" style="margin: 0 0 1.25rem;font-size: 12px;text-align: center;">
|
||||
<span class="letter__unsubscribe-dscr" style="display: inline-block;">This email has been sent to {{ send_to }} ,</span>
|
||||
</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 %}
|
||||
|
|
@ -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: "Open Sans", 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:"Open-Sans",sans-serif; font-size: 1.5rem;margin: 0 0 10px;padding: 0 0 6px;border-bottom: 4px solid #ffee29;">
|
||||
<span class="letter__title-txt">{{ title }}</span>
|
||||
</div>
|
||||
<div class="letter__text" style="margin: 0 0 30px; font-family:"Open-Sans",sans-serif; font-size: 14px; line-height: 21px;letter-spacing: -0.34px; overflow-x: hidden;">
|
||||
<br>
|
||||
{% blocktrans %}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: "PT Serif", sans-serif;font-size: 1.25rem;text-transform: uppercase;">
|
||||
<img alt="thumb" style="width:30px;vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/2.png" />
|
||||
<span class="letter__follow-title">Follow us</span>
|
||||
</div>
|
||||
<div class="letter__follow-text" style="display: block;margin: 0 0 30px;font-size: 12px;font-style: italic;">You can also us on our social network below
|
||||
</div>
|
||||
<div class="letter__follow-social">
|
||||
<a href="{{ facebook_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0 1rem 0 0;background: #ffee29;border: none;">
|
||||
<img alt="facebook" style="width: 30px; vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/3.png" />
|
||||
</a>
|
||||
<a href="{{ instagram_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0 1rem 0 0;background: #ffee29;border: none;">
|
||||
<img alt="instagram" style="width:30px;vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/4.png" />
|
||||
</a>
|
||||
<a href="{{ twitter_page_url }}" class="letter__follow-link" target="_blank" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: inline-block;width: 30px;height: 30px;margin: 0;background: #ffee29;border: none;">
|
||||
<img alt="twitter" style="width:30px;vertical-align: sub; display: inline-block" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/5.png" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="letter__unsubscribe" style="margin: 0 0 1.25rem;font-size: 12px;text-align: center;">
|
||||
<span class="letter__unsubscribe-dscr" style="display: inline-block;">This email has been sent to {{ send_to }} ,</span>
|
||||
</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
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user