diff --git a/apps/account/management/commands/add_account.py b/apps/account/management/commands/add_account.py new file mode 100644 index 00000000..2b72767c --- /dev/null +++ b/apps/account/management/commands/add_account.py @@ -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.')) \ No newline at end of file diff --git a/apps/account/management/commands/add_image.py b/apps/account/management/commands/add_image.py new file mode 100644 index 00000000..07efe53b --- /dev/null +++ b/apps/account/management/commands/add_image.py @@ -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.')) \ No newline at end of file diff --git a/apps/account/management/commands/add_social.py b/apps/account/management/commands/add_social.py new file mode 100644 index 00000000..c7894c32 --- /dev/null +++ b/apps/account/management/commands/add_social.py @@ -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.')) \ No newline at end of file diff --git a/apps/account/migrations/0019_auto_20191108_0827.py b/apps/account/migrations/0019_auto_20191108_0827.py new file mode 100644 index 00000000..1a3bb2ac --- /dev/null +++ b/apps/account/migrations/0019_auto_20191108_0827.py @@ -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'), + ), + ] diff --git a/apps/account/models.py b/apps/account/models.py index 3eb07722..9573c92e 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -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'') - 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): diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index d68cfe56..20016297 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -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 diff --git a/apps/account/tasks.py b/apps/account/tasks.py index d9fa7bb7..8afe1545 100644 --- a/apps/account/tasks.py +++ b/apps/account/tasks.py @@ -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 diff --git a/apps/account/transfer_data.py b/apps/account/transfer_data.py index c39e0ac1..75e0d8ee 100644 --- a/apps/account/transfer_data.py +++ b/apps/account/transfer_data.py @@ -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], } diff --git a/apps/authorization/models.py b/apps/authorization/models.py index c295329c..f6476db5 100644 --- a/apps/authorization/models.py +++ b/apps/authorization/models.py @@ -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): diff --git a/apps/authorization/serializers/common.py b/apps/authorization/serializers/common.py index 13121f78..1d61c573 100644 --- a/apps/authorization/serializers/common.py +++ b/apps/authorization/serializers/common.py @@ -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 diff --git a/apps/authorization/tasks.py b/apps/authorization/tasks.py index cb186142..d44c2b5c 100644 --- a/apps/authorization/tasks.py +++ b/apps/authorization/tasks.py @@ -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}') diff --git a/apps/authorization/views/common.py b/apps/authorization/views/common.py index a61981c3..9d2069f2 100644 --- a/apps/authorization/views/common.py +++ b/apps/authorization/views/common.py @@ -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 diff --git a/apps/comment/admin.py b/apps/comment/admin.py index 855f6b3e..061c9c8d 100644 --- a/apps/comment/admin.py +++ b/apps/comment/admin.py @@ -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') diff --git a/apps/comment/models.py b/apps/comment/models.py index 29021677..3a8c8c37 100644 --- a/apps/comment/models.py +++ b/apps/comment/models.py @@ -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""" diff --git a/apps/comment/transfer_data.py b/apps/comment/transfer_data.py index 3a291b86..843da78f 100644 --- a/apps/comment/transfer_data.py +++ b/apps/comment/transfer_data.py @@ -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 ] } diff --git a/apps/establishment/management/commands/add_empl_position.py b/apps/establishment/management/commands/add_empl_position.py new file mode 100644 index 00000000..acaeb324 --- /dev/null +++ b/apps/establishment/management/commands/add_empl_position.py @@ -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.')) diff --git a/apps/establishment/management/commands/add_employee.py b/apps/establishment/management/commands/add_employee.py new file mode 100644 index 00000000..4abf588a --- /dev/null +++ b/apps/establishment/management/commands/add_employee.py @@ -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.')) diff --git a/apps/establishment/management/commands/add_position.py b/apps/establishment/management/commands/add_position.py new file mode 100644 index 00000000..18e8f05c --- /dev/null +++ b/apps/establishment/management/commands/add_position.py @@ -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.')) diff --git a/apps/establishment/migrations/0055_auto_20191106_0740.py b/apps/establishment/migrations/0055_auto_20191106_0740.py new file mode 100644 index 00000000..78b1c027 --- /dev/null +++ b/apps/establishment/migrations/0055_auto_20191106_0740.py @@ -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'), + ), + ] diff --git a/apps/establishment/migrations/0055_employee_old_id.py b/apps/establishment/migrations/0055_employee_old_id.py new file mode 100644 index 00000000..f13bff43 --- /dev/null +++ b/apps/establishment/migrations/0055_employee_old_id.py @@ -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'), + ), + ] diff --git a/apps/establishment/migrations/0056_auto_20191105_1401.py b/apps/establishment/migrations/0056_auto_20191105_1401.py new file mode 100644 index 00000000..4b845dd7 --- /dev/null +++ b/apps/establishment/migrations/0056_auto_20191105_1401.py @@ -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'), + ), + ] diff --git a/apps/establishment/migrations/0057_establishmentemployee_old_id.py b/apps/establishment/migrations/0057_establishmentemployee_old_id.py new file mode 100644 index 00000000..a1a66517 --- /dev/null +++ b/apps/establishment/migrations/0057_establishmentemployee_old_id.py @@ -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'), + ), + ] diff --git a/apps/establishment/migrations/0058_merge_20191106_0921.py b/apps/establishment/migrations/0058_merge_20191106_0921.py new file mode 100644 index 00000000..61f686b9 --- /dev/null +++ b/apps/establishment/migrations/0058_merge_20191106_0921.py @@ -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 = [ + ] diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 8ea4601b..75d3f6e9 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -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.""" diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index f1fb40a5..36b3df99 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -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', ] diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 50a7acbe..108f8dd0 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -196,7 +196,8 @@ class EstablishmentBaseSerializer(ProjectModelSerializer): fields = [ 'id', 'name', - 'name_translated', + 'transliterated_name', + 'index_name', 'price_level', 'toque_number', 'public_mark', diff --git a/apps/establishment/transfer_data.py b/apps/establishment/transfer_data.py index ced64902..a8c5ca58 100644 --- a/apps/establishment/transfer_data.py +++ b/apps/establishment/transfer_data.py @@ -1,6 +1,5 @@ from pprint import pprint -import requests from django.db.models import Q, F from establishment.models import Establishment diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index e490d576..f6d4d63a 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -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): diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index 05391827..b01c8c49 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -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}) diff --git a/apps/location/serializers/common.py b/apps/location/serializers/common.py index 5ba26435..378e4912 100644 --- a/apps/location/serializers/common.py +++ b/apps/location/serializers/common.py @@ -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', ] diff --git a/apps/location/views/back.py b/apps/location/views/back.py index bb64ff72..a4eee929 100644 --- a/apps/location/views/back.py +++ b/apps/location/views/back.py @@ -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] diff --git a/apps/main/management/__init__.py b/apps/main/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/main/management/commands/__init__.py b/apps/main/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/main/management/commands/add_award.py b/apps/main/management/commands/add_award.py new file mode 100644 index 00000000..85a613cf --- /dev/null +++ b/apps/main/management/commands/add_award.py @@ -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.')) + + diff --git a/apps/main/management/commands/add_award_type.py b/apps/main/management/commands/add_award_type.py new file mode 100644 index 00000000..63aa6546 --- /dev/null +++ b/apps/main/management/commands/add_award_type.py @@ -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.')) \ No newline at end of file diff --git a/apps/main/methods.py b/apps/main/methods.py index 845a99a4..d5f307eb 100644 --- a/apps/main/methods.py +++ b/apps/main/methods.py @@ -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) diff --git a/apps/main/migrations/0032_awardtype_old_id.py b/apps/main/migrations/0032_awardtype_old_id.py new file mode 100644 index 00000000..7a9e9913 --- /dev/null +++ b/apps/main/migrations/0032_awardtype_old_id.py @@ -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), + ), + ] diff --git a/apps/main/migrations/0033_auto_20191106_0744.py b/apps/main/migrations/0033_auto_20191106_0744.py new file mode 100644 index 00000000..726fa27f --- /dev/null +++ b/apps/main/migrations/0033_auto_20191106_0744.py @@ -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'), + ), + ] diff --git a/apps/main/models.py b/apps/main/models.py index 97fe53c1..9fee60b6 100644 --- a/apps/main/models.py +++ b/apps/main/models.py @@ -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 diff --git a/apps/main/views/common.py b/apps/main/views/common.py index d509f80e..0c2ef6d4 100644 --- a/apps/main/views/common.py +++ b/apps/main/views/common.py @@ -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 diff --git a/apps/news/views.py b/apps/news/views.py index 990665dd..c65c5297 100644 --- a/apps/news/views.py +++ b/apps/news/views.py @@ -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): diff --git a/apps/search_indexes/documents/establishment.py b/apps/search_indexes/documents/establishment.py index 7eac2d6c..19d419aa 100644 --- a/apps/search_indexes/documents/establishment.py +++ b/apps/search_indexes/documents/establishment.py @@ -90,7 +90,8 @@ class EstablishmentDocument(Document): fields = ( 'id', 'name', - 'name_translated', + 'transliterated_name', + 'index_name', 'is_publish', 'price_level', 'toque_number', diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index a356f99b..bbc61735 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -96,7 +96,8 @@ class EstablishmentDocumentSerializer(DocumentSerializer): fields = ( 'id', 'name', - 'name_translated', + 'transliterated_name', + 'index_name', 'price_level', 'toque_number', 'public_mark', diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 29905125..87b0ba64 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -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 = ( diff --git a/apps/tag/filters.py b/apps/tag/filters.py index 2dc3a775..e8263e0d 100644 --- a/apps/tag/filters.py +++ b/apps/tag/filters.py @@ -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 \ No newline at end of file diff --git a/apps/tag/views.py b/apps/tag/views.py index a4beaaa0..c55834e0 100644 --- a/apps/tag/views.py +++ b/apps/tag/views.py @@ -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): diff --git a/apps/timetable/models.py b/apps/timetable/models.py index 35469c32..cf7f8d94 100644 --- a/apps/timetable/models.py +++ b/apps/timetable/models.py @@ -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.""" diff --git a/apps/transfer/management/commands/transfer.py b/apps/transfer/management/commands/transfer.py index a3726dd7..c606d2cf 100644 --- a/apps/transfer/management/commands/transfer.py +++ b/apps/transfer/management/commands/transfer.py @@ -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): diff --git a/apps/transfer/models.py b/apps/transfer/models.py index 711863a2..9b108885 100644 --- a/apps/transfer/models.py +++ b/apps/transfer/models.py @@ -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' diff --git a/apps/transfer/serializers/comments.py b/apps/transfer/serializers/comments.py index 584370e6..cd4233d1 100644 --- a/apps/transfer/serializers/comments.py +++ b/apps/transfer/serializers/comments.py @@ -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 diff --git a/apps/transfer/serializers/user_social_auth.py b/apps/transfer/serializers/user_social_auth.py new file mode 100644 index 00000000..dc260fa0 --- /dev/null +++ b/apps/transfer/serializers/user_social_auth.py @@ -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 diff --git a/project/settings/base.py b/project/settings/base.py index 0e410537..89df2d69 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -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'] \ No newline at end of file diff --git a/project/settings/development.py b/project/settings/development.py index c6f80f9e..fd832329 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -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' diff --git a/project/settings/local.py b/project/settings/local.py index eba35ebf..f3c206d7 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -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': { diff --git a/project/settings/production.py b/project/settings/production.py index 4f9e90b7..a531a2ae 100644 --- a/project/settings/production.py +++ b/project/settings/production.py @@ -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 diff --git a/project/settings/stage.py b/project/settings/stage.py index b70563c9..49a7ae0f 100644 --- a/project/settings/stage.py +++ b/project/settings/stage.py @@ -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] \ No newline at end of file diff --git a/project/templates/account/change_email.html b/project/templates/account/change_email.html index 6b74d970..efe92766 100644 --- a/project/templates/account/change_email.html +++ b/project/templates/account/change_email.html @@ -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 %} + + + + + + + + + + +
-{% trans "Please go to the following page for confirmation new email address:" %} +
-https://{{ country_code }}.{{ domain_uri }}/change-email-confirm/{{ uidb64 }}/{{ token }}/ - -{% trans "Thanks for using our site!" %} - -{% blocktrans %}The {{ site_name }} team{% endblocktrans %} +
+
+
+
+
+ + +
+
+
+ {{ title }} +
+
+ {% blocktrans %}You're receiving this email because you want to change email address at {{ site_name }}.{% endblocktrans %} +
+ {% trans "Please go to the following page for confirmation new email address:" %} +
+ https://{{ country_code }}.{{ domain_uri }}/change-email-confirm/{{ uidb64 }}/{{ token }}/ +
+ {% trans "Thanks for using our site!" %} +
+
+ {% blocktrans %}The {{ site_name }} team{% endblocktrans %} +
+ +
+ This email has been sent to {{ send_to }} , +
+ +
+
+
+ + {% endautoescape %} \ No newline at end of file diff --git a/project/templates/account/password_change_email.html b/project/templates/account/password_change_email.html index 30dd2aac..77cad83f 100644 --- a/project/templates/account/password_change_email.html +++ b/project/templates/account/password_change_email.html @@ -1,7 +1,77 @@ {% load i18n %}{% autoescape off %} -{% blocktrans %}You're receiving this email because your account's password address at {{ site_name }}.{% endblocktrans %} + + + + + + + + + + +
-{% trans "Thanks for using our site!" %} +
-{% blocktrans %}The {{ site_name }} team{% endblocktrans %} +
+
+
+
+
+ + +
+
+
+ {{ title }} +
+
+
+ {% blocktrans %}You're receiving this email because your account's password address at {{ site_name }}.{% endblocktrans %} +
+ {% trans "Thanks for using our site!" %} +
+
+ {% blocktrans %}The {{ site_name }} team{% endblocktrans %} +
+ +
+ This email has been sent to {{ send_to }} , +
+ +
+
+
+ + {% endautoescape %} \ No newline at end of file diff --git a/project/templates/account/password_reset_email.html b/project/templates/account/password_reset_email.html index 9fb3fbf3..4d61147d 100644 --- a/project/templates/account/password_reset_email.html +++ b/project/templates/account/password_reset_email.html @@ -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 %} + + + + + + + + + + +
-{% trans "Please go to the following page and choose a new password:" %} +
-https://{{ country_code }}.{{ domain_uri }}/recovery/{{ uidb64 }}/{{ token }}/ - -{% trans "Thanks for using our site!" %} - -{% blocktrans %}The {{ site_name }} team{% endblocktrans %} +
+
+
+
+
+ + +
+
+
+ {{ title }} +
+
+ {% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %} +
+ {% trans "Please go to the following page and choose a new password:" %} +
+ https://{{ country_code }}.{{ domain_uri }}/recovery/{{ uidb64 }}/{{ token }}/ +
+ {% trans "Thanks for using our site!" %} +

+ {% blocktrans %}The {{ site_name }} team{% endblocktrans %} +
+ +
+ This email has been sent to {{ send_to }} , +
+ +
+
+
+ + {% endautoescape %} \ No newline at end of file diff --git a/project/templates/authorization/confirm_email.html b/project/templates/authorization/confirm_email.html index 3e51add7..c05c85b0 100644 --- a/project/templates/authorization/confirm_email.html +++ b/project/templates/authorization/confirm_email.html @@ -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 %} + + + + + + + + + + +
-{% trans "Please confirm your email address to complete the registration:" %} -https://{{ country_code }}.{{ domain_uri }}/registered/{{ uidb64 }}/{{ token }}/ +
-{% trans "Thanks for using our site!" %} - -{% blocktrans %}The {{ site_name }} team{% endblocktrans %} +
+
+
+
+
+ + +
+
+
+ {{ title }} +
+
+
+ {% blocktrans %}You're receiving this email because you trying to register new account at {{ site_name }}.{% endblocktrans %} +
+ {% trans "Please confirm your email address to complete the registration:" %} + https://{{ country_code }}.{{ domain_uri }}/registered/{{ uidb64 }}/{{ token }}/ +
+ {% trans "Thanks for using our site!" %} +

+ {% blocktrans %}The {{ site_name }} team{% endblocktrans %} +
+ +
+ This email has been sent to {{ send_to }} , +
+ +
+
+
+ + {% endautoescape %} \ No newline at end of file diff --git a/project/templates/news/news_email.html b/project/templates/news/news_email.html index 0227b9de..c2ae227c 100644 --- a/project/templates/news/news_email.html +++ b/project/templates/news/news_email.html @@ -19,7 +19,7 @@
@@ -35,7 +35,7 @@
{{ description | safe }}
- +
Go to news
@@ -44,20 +44,20 @@
@@ -69,9 +69,9 @@
diff --git a/requirements/base.txt b/requirements/base.txt index 7bd55d12..a1408fc5 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -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