Merge branch 'develop' into feature/gm-192

This commit is contained in:
evgeniy-st 2019-10-11 16:44:54 +03:00
commit 0d7104eace
17 changed files with 149 additions and 22 deletions

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-10 14:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0008_auto_20190912_1325'),
]
operations = [
migrations.AddField(
model_name='user',
name='unconfirmed_email',
field=models.EmailField(blank=True, default=None, max_length=254, null=True, verbose_name='unconfirmed email'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-10 17:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0009_user_unconfirmed_email'),
]
operations = [
migrations.AddField(
model_name='user',
name='password_confirmed',
field=models.BooleanField(default=True, verbose_name='is new password confirmed'),
),
]

View File

@ -60,8 +60,10 @@ class User(AbstractUser):
blank=True, null=True, default=None)
email = models.EmailField(_('email address'), blank=True,
null=True, default=None)
unconfirmed_email = models.EmailField(_('unconfirmed email'), blank=True, null=True, default=None)
email_confirmed = models.BooleanField(_('email status'), default=False)
newsletter = models.NullBooleanField(default=True)
password_confirmed = models.BooleanField(_('is new password confirmed'), default=True, null=False)
EMAIL_FIELD = 'email'
USERNAME_FIELD = 'username'
@ -112,9 +114,15 @@ class User(AbstractUser):
def confirm_email(self):
"""Method to confirm user email address"""
self.email = self.unconfirmed_email
self.unconfirmed_email = None
self.email_confirmed = True
self.save()
def confirm_password(self):
self.password_confirmed = True
self.save()
def approve(self):
"""Set user is_active status to True"""
self.is_active = True
@ -149,6 +157,11 @@ class User(AbstractUser):
"""Make a token for finish signup."""
return password_token_generator.make_token(self)
@property
def confirm_password_token(self):
"""Make a token for new password confirmation """
return GMTokenGenerator(purpose=GMTokenGenerator.CONFIRM_PASSWORD).make_token(self)
@property
def get_user_uidb64(self):
"""Get base64 value for user by primary key identifier"""
@ -178,6 +191,16 @@ class User(AbstractUser):
template_name=settings.RESETTING_TOKEN_TEMPLATE,
context=context)
def confirm_password_template(self, country_code):
"""Get confirm password template"""
context = {'token': self.confirm_password_token,
'country_code': country_code}
context.update(self.base_template)
return render_to_string(
template_name=settings.CONFIRM_PASSWORD_TEMPLATE,
context=context,
)
def confirm_email_template(self, country_code):
"""Get confirm email template"""
context = {'token': self.confirm_email_token,

View File

@ -61,9 +61,12 @@ class UserSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
"""Override update method"""
old_email = instance.email
instance = super().update(instance, validated_data)
if 'email' in validated_data:
instance.email_confirmed = False
instance.email = old_email
instance.unconfirmed_email = validated_data['email']
instance.save()
# Send verification link on user email for change email address
if settings.USE_CELERY:

View File

@ -1,8 +1,10 @@
"""Serializers for account web"""
from django.contrib.auth import password_validation as password_validators
from django.conf import settings
from rest_framework import serializers
from account import models
from account import tasks
from utils import exceptions as utils_exceptions
from utils.methods import username_validator
@ -67,5 +69,16 @@ class PasswordResetConfirmSerializer(serializers.ModelSerializer):
"""Override update method"""
# Update user password from instance
instance.set_password(validated_data.get('password'))
instance.password_confirmed = False
instance.save()
if settings.USE_CELERY:
tasks.send_reset_password_confirm.delay(
user=instance,
country_code=self.context.get('request').country_code,
)
else:
tasks.send_reset_password_confirm(
user=instance,
country_code=self.context.get('request').country_code,
)
return instance

View File

@ -22,6 +22,17 @@ def send_reset_password_email(user_id, country_code):
f'DETAIL: Exception occurred for reset password: '
f'{user_id}')
@shared_task
def send_reset_password_confirm(user: models.User, country_code):
""" Send email to user for applying new password. """
try:
user.send_email(subject=_('New password confirmation'),
message=user.confirm_password_template(country_code))
except:
logger.error(f'METHOD_NAME: {send_reset_password_confirm.__name__}\n'
f'DETAIL: Exception occured for new passwordconfirmation',
f'{user.id}')
@shared_task
def confirm_new_email_address(user_id, country_code):

View File

@ -8,6 +8,7 @@ app_name = 'account'
urlpatterns = [
path('user/', views.UserRetrieveUpdateView.as_view(), name='user-retrieve-update'),
path('change-password/', views.ChangePasswordView.as_view(), name='change-password'),
path('change-password-confirm/<uuid64>/<token>/', views.ConfirmPasswordView.as_view(), name='change-password'),
path('email/confirm/', views.SendConfirmationEmailView.as_view(), name='send-confirm-email'),
path('email/confirm/<uidb64>/<token>/', views.ConfirmEmailView.as_view(), name='confirm-email'),
]

View File

@ -91,6 +91,32 @@ class ConfirmEmailView(JWTGenericViewMixin):
else:
raise utils_exceptions.UserNotFoundError()
class ConfirmPasswordView(JWTGenericViewMixin):
"""View for applying newly set password"""
permission_classes = (permissions.AllowAny,)
def get(self, request, *args, **kwargs):
uidb64 = kwargs.get('uidb64')
token = kwargs.get('token')
uid = force_text(urlsafe_base64_decode(uidb64))
user_qs = models.User.objects.filter(pk=uid)
if user_qs.exists():
user = user_qs.first()
if not GMTokenGenerator(GMTokenGenerator.CONFIRM_PASSWORD).check_token(
user, token):
raise utils_exceptions.NotValidTokenError()
user.confirm_password()
tokens = user.create_jwt_tokens()
return self._put_cookies_in_response(
cookies=self._put_data_in_cookies(
access_token=tokens.get('access_token'),
refresh_token=tokens.get('refresh_token')),
response=Response(status=status.HTTP_200_OK))
else:
raise utils_exceptions.UserNotFoundError()
# Firebase Cloud Messaging
class FCMDeviceViewSet(generics.GenericAPIView):

View File

@ -108,8 +108,8 @@ class LoginByUsernameOrEmailSerializer(SourceSerializerMixin,
"""Override validate method"""
username_or_email = attrs.pop('username_or_email')
password = attrs.pop('password')
user_qs = account_models.User.objects.filter(Q(username=username_or_email) |
Q(email=username_or_email))
user_qs = account_models.User.objects.filter(password_confirmed=True)\
.filter(Q(username=username_or_email) | Q(email=username_or_email))
if not user_qs.exists():
raise utils_exceptions.WrongAuthCredentials()
else:

View File

@ -141,7 +141,7 @@ class LastableService(AbstractBookingService):
super().check_whether_booking_available(restaurant_id, date)
url = f'{self.url}v1/restaurant/{restaurant_id}/offers'
r = requests.get(url, headers=self.get_common_headers(), proxies=self.proxies)
response = json.loads(r.content)['data']
response = json.loads(r.content).get('data')
if not status.is_success(r.status_code) or not response:
return False
self.response = response

View File

@ -35,9 +35,9 @@ class CheckWhetherBookingAvailable(generics.GenericAPIView):
response = {
'available': is_booking_available,
'type': service.service,
'type': service.service if service else None,
}
response.update({'details': service.response} if service.response else {})
response.update({'details': service.response} if service and service.response else {})
return Response(data=response, status=200)

View File

@ -11,13 +11,11 @@ from django.db import models
from django.db.models import When, Case, F, ExpressionWrapper, Subquery, Q
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from elasticsearch_dsl import Q
from phonenumber_field.modelfields import PhoneNumberField
from collection.models import Collection
from tag.models import Tag, TagCategory
from main.models import Award, MetaDataContent
from location.models import Address
from main.models import Award
from review.models import Review
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
TranslatedFieldsMixin, BaseAttributes)
@ -101,15 +99,15 @@ class EstablishmentQuerySet(models.QuerySet):
else:
return self.none()
def es_search(self, value, locale=None):
"""Search text via ElasticSearch."""
from search_indexes.documents import EstablishmentDocument
search = EstablishmentDocument.search().filter(
Q('match', name=value) |
Q('match', **{f'description.{locale}': value})
).execute()
ids = [result.meta.id for result in search]
return self.filter(id__in=ids)
# def es_search(self, value, locale=None):
# """Search text via ElasticSearch."""
# from search_indexes.documents import EstablishmentDocument
# search = EstablishmentDocument.search().filter(
# Elastic_Q('match', name=value) |
# Elastic_Q('match', **{f'description.{locale}': value})
# ).execute()
# ids = [result.meta.id for result in search]
# return self.filter(id__in=ids)
def by_country_code(self, code):
"""Return establishments by country code"""
@ -183,7 +181,8 @@ class EstablishmentQuerySet(models.QuerySet):
return self.filter(id__in=subquery_filter_by_distance) \
.annotate_intermediate_public_mark() \
.annotate_mark_similarity(mark=establishment.public_mark) \
.order_by('mark_similarity')
.order_by('mark_similarity') \
.distinct('mark_similarity', 'id')
else:
return self.none()

View File

@ -258,12 +258,14 @@ class GMTokenGenerator(PasswordResetTokenGenerator):
RESET_PASSWORD = 1
CHANGE_PASSWORD = 2
CONFIRM_EMAIL = 3
CONFIRM_PASSWORD = 4
TOKEN_CHOICES = (
CHANGE_EMAIL,
RESET_PASSWORD,
CHANGE_PASSWORD,
CONFIRM_EMAIL
CONFIRM_EMAIL,
CONFIRM_PASSWORD,
)
def __init__(self, purpose: int):
@ -279,7 +281,8 @@ class GMTokenGenerator(PasswordResetTokenGenerator):
self.purpose == self.CONFIRM_EMAIL:
fields.extend([str(user.email_confirmed), str(user.email)])
elif self.purpose == self.RESET_PASSWORD or \
self.purpose == self.CHANGE_PASSWORD:
self.purpose == self.CHANGE_PASSWORD or \
self.purpose == self.CONFIRM_PASSWORD:
fields.append(str(user.password))
return fields

View File

@ -406,6 +406,7 @@ PASSWORD_RESET_TIMEOUT_DAYS = 1
# TEMPLATES
RESETTING_TOKEN_TEMPLATE = 'account/password_reset_email.html'
CHANGE_EMAIL_TEMPLATE = 'account/change_email.html'
CONFIRM_PASSWORD_TEMPLATE = 'account/password_confirm_email.html'
CONFIRM_EMAIL_TEMPLATE = 'authorization/confirm_email.html'
NEWS_EMAIL_TEMPLATE = "news/news_email.html"

View File

@ -3,7 +3,7 @@
{% trans "Please go to the following page for confirmation new email address:" %}
https://{{ country_code }}.{{ domain_uri }}/registered/{{ uidb64 }}/{{ token }}/
https://{{ country_code }}.{{ domain_uri }}/change-email-confirm/{{ uidb64 }}/{{ token }}/
{% trans "Thanks for using our site!" %}

View File

@ -0,0 +1,11 @@
{% load i18n %}{% autoescape off %}
{% blocktrans %}Confirm a password reset for your user account at {{ site_name }}.{% endblocktrans %}
{% trans "Please go to the following page:" %}
https://{{ country_code }}.{{ domain_uri }}/confirm-new-password/{{ uidb64 }}/{{ token }}/
{% trans "Thanks for using our site!" %}
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
{% endautoescape %}

View File

@ -36,7 +36,7 @@
<div class="letter__text" style="margin: 0 0 30px;line-height: 21px;letter-spacing: -0.34px; overflow-x: hidden;">
{{ description | safe }}
</div>
<a href="https://{{ country_code }}.{{ domain_uri }}{% url 'web:news:rud' slug %}" style="text-decoration: none;" target="_blank">
<a href="https://{{ country_code }}.{{ domain_uri }}/news/{{ slug }}" style="text-decoration: none;" target="_blank">
<button class="letter__button" style="display: block;margin: 30px auto;padding: 0;font-family: &quot;pt serif&quot: ;, sans-serif: ;font-size: 1.25rem;letter-spacing: 1px;text-align: center;color: #000;background: #ffee29;border: 2px solid #ffee29;text-transform: uppercase;min-width: 238px;height: 46px;background-color: #ffee29;">Go to news</button>
</a>
</div>