Merge branch 'develop' into feature/elasticsearch
This commit is contained in:
commit
8e864d32b9
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -19,3 +19,6 @@ logs/
|
||||||
/datadir/
|
/datadir/
|
||||||
/_files/
|
/_files/
|
||||||
/geoip_db/
|
/geoip_db/
|
||||||
|
|
||||||
|
# dev
|
||||||
|
./docker-compose.override.yml
|
||||||
|
|
@ -11,14 +11,17 @@ class UserAdmin(BaseUserAdmin):
|
||||||
"""User model admin settings."""
|
"""User model admin settings."""
|
||||||
|
|
||||||
list_display = ('id', 'username', 'short_name', 'date_joined', 'is_active',
|
list_display = ('id', 'username', 'short_name', 'date_joined', 'is_active',
|
||||||
'is_staff', 'is_superuser',)
|
'is_staff', 'is_superuser', 'email_confirmed')
|
||||||
list_filter = ('is_active', 'is_staff', 'is_superuser', 'groups',)
|
list_filter = ('is_active', 'is_staff', 'is_superuser', 'email_confirmed',
|
||||||
|
'groups',)
|
||||||
search_fields = ('email', 'first_name', 'last_name')
|
search_fields = ('email', 'first_name', 'last_name')
|
||||||
readonly_fields = ('last_login', 'date_joined',)
|
readonly_fields = ('last_login', 'date_joined', 'image_preview', 'cropped_image_preview')
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {'fields': ('email', 'password',)}),
|
(None, {'fields': ('email', 'password',)}),
|
||||||
(_('Personal info'), {
|
(_('Personal info'), {
|
||||||
'fields': ('username', 'first_name', 'last_name', 'image')}),
|
'fields': ('username', 'first_name', 'last_name',
|
||||||
|
'image_url', 'image_preview',
|
||||||
|
'cropped_image_url', 'cropped_image_preview',)}),
|
||||||
(_('Subscription'), {
|
(_('Subscription'), {
|
||||||
'fields': (
|
'fields': (
|
||||||
'newsletter',
|
'newsletter',
|
||||||
|
|
@ -46,11 +49,14 @@ class UserAdmin(BaseUserAdmin):
|
||||||
|
|
||||||
short_name.short_description = _('Name')
|
short_name.short_description = _('Name')
|
||||||
|
|
||||||
|
def image_preview(self, obj):
|
||||||
|
"""Get user image preview"""
|
||||||
|
return obj.image_tag
|
||||||
|
|
||||||
@admin.register(models.ResetPasswordToken)
|
image_preview.short_description = 'Image preview'
|
||||||
class ResetPasswordToken(admin.ModelAdmin):
|
|
||||||
"""Model admin for ResetPasswordToken"""
|
def cropped_image_preview(self, obj):
|
||||||
list_display = ('id', 'user', 'expiry_datetime')
|
"""Get user cropped image preview"""
|
||||||
list_filter = ('expiry_datetime', 'user')
|
return obj.cropped_image_tag
|
||||||
search_fields = ('user', )
|
|
||||||
readonly_fields = ('user', 'key', )
|
cropped_image_preview.short_description = 'Cropped image preview'
|
||||||
|
|
|
||||||
20
apps/account/migrations/0005_user_cropped_image.py
Normal file
20
apps/account/migrations/0005_user_cropped_image.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 2.2.4 on 2019-09-06 10:45
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import easy_thumbnails.fields
|
||||||
|
import utils.methods
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('account', '0004_user_email_confirmed'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='cropped_image',
|
||||||
|
field=easy_thumbnails.fields.ThumbnailerImageField(blank=True, default=None, null=True, upload_to=utils.methods.image_path, verbose_name='Crop image'),
|
||||||
|
),
|
||||||
|
]
|
||||||
16
apps/account/migrations/0006_delete_resetpasswordtoken.py
Normal file
16
apps/account/migrations/0006_delete_resetpasswordtoken.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Generated by Django 2.2.4 on 2019-09-12 11:42
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('account', '0005_user_cropped_image'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='ResetPasswordToken',
|
||||||
|
),
|
||||||
|
]
|
||||||
21
apps/account/migrations/0007_auto_20190912_1323.py
Normal file
21
apps/account/migrations/0007_auto_20190912_1323.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Generated by Django 2.2.4 on 2019-09-12 13:23
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('account', '0006_delete_resetpasswordtoken'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='cropped_image',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='image',
|
||||||
|
),
|
||||||
|
]
|
||||||
23
apps/account/migrations/0008_auto_20190912_1325.py
Normal file
23
apps/account/migrations/0008_auto_20190912_1325.py
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 2.2.4 on 2019-09-12 13:25
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('account', '0007_auto_20190912_1323'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='cropped_image_url',
|
||||||
|
field=models.URLField(blank=True, default=None, null=True, verbose_name='Cropped image URL path'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='image_url',
|
||||||
|
field=models.URLField(blank=True, default=None, null=True, verbose_name='Image URL path'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
"""Account models"""
|
"""Account models"""
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import AbstractUser, UserManager as BaseUserManager
|
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.contrib.auth.tokens import default_token_generator as password_token_generator
|
||||||
|
|
@ -8,6 +7,7 @@ from django.db import models
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.encoding import force_bytes
|
from django.utils.encoding import force_bytes
|
||||||
|
from django.utils.html import mark_safe
|
||||||
from django.utils.http import urlsafe_base64_encode
|
from django.utils.http import urlsafe_base64_encode
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
|
|
@ -23,15 +23,12 @@ class UserManager(BaseUserManager):
|
||||||
|
|
||||||
use_in_migrations = False
|
use_in_migrations = False
|
||||||
|
|
||||||
def make(self, username: str, email: str, password: str,
|
def make(self, username: str, email: str, password: str, newsletter: bool) -> object:
|
||||||
newsletter: bool, is_active: bool = False) -> object:
|
|
||||||
"""Register new user"""
|
"""Register new user"""
|
||||||
obj = self.model(
|
obj = self.model(
|
||||||
username=username,
|
username=username,
|
||||||
email=email,
|
email=email,
|
||||||
newsletter=newsletter,
|
newsletter=newsletter)
|
||||||
is_active=is_active
|
|
||||||
)
|
|
||||||
obj.set_password(password)
|
obj.set_password(password)
|
||||||
obj.save()
|
obj.save()
|
||||||
return obj
|
return obj
|
||||||
|
|
@ -45,18 +42,22 @@ class UserQuerySet(models.QuerySet):
|
||||||
return self.filter(is_active=switcher)
|
return self.filter(is_active=switcher)
|
||||||
|
|
||||||
def by_oauth2_access_token(self, token):
|
def by_oauth2_access_token(self, token):
|
||||||
"""Find user by access token"""
|
"""Find users by access token"""
|
||||||
return self.filter(oauth2_provider_accesstoken__token=token,
|
return self.filter(oauth2_provider_accesstoken__token=token,
|
||||||
oauth2_provider_accesstoken__expires__gt=timezone.now())
|
oauth2_provider_accesstoken__expires__gt=timezone.now())
|
||||||
|
|
||||||
def by_oauth2_refresh_token(self, token):
|
def by_oauth2_refresh_token(self, token):
|
||||||
"""Find user by access token"""
|
"""Find users by access token"""
|
||||||
return self.filter(oauth2_provider_refreshtoken__token=token,
|
return self.filter(oauth2_provider_refreshtoken__token=token,
|
||||||
oauth2_provider_refreshtoken__expires__gt=timezone.now())
|
oauth2_provider_refreshtoken__expires__gt=timezone.now())
|
||||||
|
|
||||||
|
|
||||||
class User(ImageMixin, AbstractUser):
|
class User(AbstractUser):
|
||||||
"""Base user model."""
|
"""Base user model."""
|
||||||
|
image_url = models.URLField(verbose_name=_('Image URL path'),
|
||||||
|
blank=True, null=True, default=None)
|
||||||
|
cropped_image_url = models.URLField(verbose_name=_('Cropped image URL path'),
|
||||||
|
blank=True, null=True, default=None)
|
||||||
email = models.EmailField(_('email address'), blank=True,
|
email = models.EmailField(_('email address'), blank=True,
|
||||||
null=True, default=None)
|
null=True, default=None)
|
||||||
email_confirmed = models.BooleanField(_('email status'), default=False)
|
email_confirmed = models.BooleanField(_('email status'), default=False)
|
||||||
|
|
@ -93,9 +94,9 @@ class User(ImageMixin, AbstractUser):
|
||||||
self.is_active = switcher
|
self.is_active = switcher
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def create_jwt_tokens(self, source: int):
|
def create_jwt_tokens(self, source: int = None):
|
||||||
"""Create JWT tokens for user"""
|
"""Create JWT tokens for user"""
|
||||||
token = GMRefreshToken.for_user_by_source(self, source)
|
token = GMRefreshToken.for_user(self, source)
|
||||||
return {
|
return {
|
||||||
'access_token': str(token.access_token),
|
'access_token': str(token.access_token),
|
||||||
'refresh_token': str(token),
|
'refresh_token': str(token),
|
||||||
|
|
@ -146,7 +147,7 @@ class User(ImageMixin, AbstractUser):
|
||||||
@property
|
@property
|
||||||
def reset_password_token(self):
|
def reset_password_token(self):
|
||||||
"""Make a token for finish signup."""
|
"""Make a token for finish signup."""
|
||||||
return GMTokenGenerator(purpose=GMTokenGenerator.RESET_PASSWORD).make_token(self)
|
return password_token_generator.make_token(self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def get_user_uidb64(self):
|
def get_user_uidb64(self):
|
||||||
|
|
@ -154,112 +155,43 @@ class User(ImageMixin, AbstractUser):
|
||||||
return urlsafe_base64_encode(force_bytes(self.pk))
|
return urlsafe_base64_encode(force_bytes(self.pk))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def confirm_email_template(self):
|
def base_template(self):
|
||||||
"""Get confirm email template"""
|
"""Base email template"""
|
||||||
return render_to_string(
|
return {'domain_uri': settings.DOMAIN_URI,
|
||||||
template_name=settings.CONFIRM_EMAIL_TEMPLATE,
|
'uidb64': self.get_user_uidb64,
|
||||||
context={'token': self.confirm_email_token,
|
'site_name': settings.SITE_NAME}
|
||||||
'uidb64': self.get_user_uidb64,
|
|
||||||
'domain_uri': settings.DOMAIN_URI,
|
|
||||||
'site_name': settings.SITE_NAME})
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def change_email_template(self):
|
def image_tag(self):
|
||||||
"""Get change email template"""
|
return mark_safe(f'<img src="{self.image_url}" />')
|
||||||
return render_to_string(
|
|
||||||
template_name=settings.CHANGE_EMAIL_TEMPLATE,
|
|
||||||
context={'token': self.change_email_token,
|
|
||||||
'uidb64': self.get_user_uidb64,
|
|
||||||
'domain_uri': settings.DOMAIN_URI,
|
|
||||||
'site_name': settings.SITE_NAME})
|
|
||||||
|
|
||||||
|
|
||||||
class ResetPasswordTokenQuerySet(models.QuerySet):
|
|
||||||
"""Reset password token query set"""
|
|
||||||
|
|
||||||
def expired(self):
|
|
||||||
"""Show only expired"""
|
|
||||||
return self.filter(expiry_datetime__lt=timezone.now())
|
|
||||||
|
|
||||||
def valid(self):
|
|
||||||
"""Show only valid"""
|
|
||||||
return self.filter(expiry_datetime__gt=timezone.now())
|
|
||||||
|
|
||||||
def by_user(self, user):
|
|
||||||
"""Show obj by user"""
|
|
||||||
return self.filter(user=user)
|
|
||||||
|
|
||||||
|
|
||||||
class ResetPasswordToken(PlatformMixin, ProjectBaseMixin):
|
|
||||||
"""Reset password model"""
|
|
||||||
|
|
||||||
user = models.ForeignKey(User,
|
|
||||||
related_name='password_reset_tokens',
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
verbose_name=_('The User which is associated to '
|
|
||||||
'this password reset token'))
|
|
||||||
# Key field, though it is not the primary key of the model
|
|
||||||
key = models.CharField(max_length=255,
|
|
||||||
verbose_name=_('Key'))
|
|
||||||
|
|
||||||
ip_address = models.GenericIPAddressField(default='',
|
|
||||||
blank=True, null=True,
|
|
||||||
verbose_name=_('The IP address of this session'))
|
|
||||||
|
|
||||||
expiry_datetime = models.DateTimeField(blank=True, null=True,
|
|
||||||
verbose_name=_('Expiration datetime'))
|
|
||||||
|
|
||||||
objects = ResetPasswordTokenQuerySet.as_manager()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("Password Reset Token")
|
|
||||||
verbose_name_plural = _("Password Reset Tokens")
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "Password reset token for user {user}".format(user=self.user)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
"""Override save method"""
|
|
||||||
if not self.expiry_datetime:
|
|
||||||
self.expiry_datetime = (
|
|
||||||
timezone.now() +
|
|
||||||
timezone.timedelta(hours=self.get_resetting_token_expiration)
|
|
||||||
)
|
|
||||||
if not self.key:
|
|
||||||
self.key = self.generate_token
|
|
||||||
return super(ResetPasswordToken, self).save(*args, **kwargs)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def get_resetting_token_expiration(self):
|
def cropped_image_tag(self):
|
||||||
"""Get resetting token expiration"""
|
return mark_safe(f'<img src="{self.cropped_image_url}" />')
|
||||||
return settings.RESETTING_TOKEN_EXPIRATION
|
|
||||||
|
|
||||||
@property
|
def reset_password_template(self, country_code):
|
||||||
def is_valid(self):
|
|
||||||
"""Check if valid token or not"""
|
|
||||||
return timezone.now() > self.expiry_datetime
|
|
||||||
|
|
||||||
@property
|
|
||||||
def generate_token(self):
|
|
||||||
"""Generates a pseudo random code"""
|
|
||||||
return password_token_generator.make_token(self.user)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def reset_password_template(self):
|
|
||||||
"""Get reset password template"""
|
"""Get reset password template"""
|
||||||
|
context = {'token': self.reset_password_token,
|
||||||
|
'country_code': country_code}
|
||||||
|
context.update(self.base_template)
|
||||||
return render_to_string(
|
return render_to_string(
|
||||||
template_name=settings.RESETTING_TOKEN_TEMPLATE,
|
template_name=settings.RESETTING_TOKEN_TEMPLATE,
|
||||||
context={'token': self.key,
|
context=context)
|
||||||
'uidb64': self.user.get_user_uidb64,
|
|
||||||
'domain_uri': settings.DOMAIN_URI,
|
|
||||||
'site_name': settings.SITE_NAME})
|
|
||||||
|
|
||||||
@staticmethod
|
def confirm_email_template(self, country_code):
|
||||||
def token_is_valid(user, token):
|
"""Get confirm email template"""
|
||||||
"""Check if token is valid"""
|
context = {'token': self.confirm_email_token,
|
||||||
return password_token_generator.check_token(user, token)
|
'country_code': country_code}
|
||||||
|
context.update(self.base_template)
|
||||||
|
return render_to_string(
|
||||||
|
template_name=settings.CONFIRM_EMAIL_TEMPLATE,
|
||||||
|
context=context)
|
||||||
|
|
||||||
def overdue(self):
|
def change_email_template(self, country_code):
|
||||||
"""Overdue instance"""
|
"""Get change email template"""
|
||||||
self.expiry_datetime = timezone.now()
|
context = {'token': self.change_email_token,
|
||||||
self.save()
|
'country_code': country_code}
|
||||||
|
context.update(self.base_template)
|
||||||
|
return render_to_string(
|
||||||
|
template_name=settings.CHANGE_EMAIL_TEMPLATE,
|
||||||
|
context=context)
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,179 @@
|
||||||
"""Common account serializers"""
|
"""Common account serializers"""
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth import password_validation as password_validators
|
||||||
from fcm_django.models import FCMDevice
|
from fcm_django.models import FCMDevice
|
||||||
from rest_framework import serializers, exceptions
|
from rest_framework import exceptions
|
||||||
|
from rest_framework import serializers
|
||||||
|
from rest_framework import validators as rest_validators
|
||||||
|
|
||||||
from account import models
|
from account import models, tasks
|
||||||
|
from utils import exceptions as utils_exceptions
|
||||||
|
from utils import methods as utils_methods
|
||||||
|
|
||||||
|
|
||||||
# User serializers
|
# User serializers
|
||||||
class UserSerializer(serializers.ModelSerializer):
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
"""User serializer."""
|
"""User serializer."""
|
||||||
|
# RESPONSE
|
||||||
|
email_confirmed = serializers.BooleanField(read_only=True)
|
||||||
|
fullname = serializers.CharField(source='get_full_name', read_only=True)
|
||||||
|
|
||||||
|
# REQUEST
|
||||||
|
username = serializers.CharField(
|
||||||
|
validators=(rest_validators.UniqueValidator(queryset=models.User.objects.all()),),
|
||||||
|
required=False)
|
||||||
|
first_name = serializers.CharField(required=False, write_only=True)
|
||||||
|
last_name = serializers.CharField(required=False, write_only=True)
|
||||||
|
image_url = serializers.URLField(required=False)
|
||||||
|
cropped_image_url = serializers.URLField(required=False)
|
||||||
|
email = serializers.EmailField(
|
||||||
|
validators=(rest_validators.UniqueValidator(queryset=models.User.objects.all()),),
|
||||||
|
required=False)
|
||||||
|
newsletter = serializers.BooleanField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.User
|
model = models.User
|
||||||
fields = [
|
fields = [
|
||||||
|
'username',
|
||||||
'first_name',
|
'first_name',
|
||||||
'last_name',
|
'last_name',
|
||||||
|
'fullname',
|
||||||
|
'cropped_image_url',
|
||||||
|
'image_url',
|
||||||
|
'email',
|
||||||
|
'email_confirmed',
|
||||||
|
'newsletter',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def validate_email(self, value):
|
||||||
|
"""Validate email value"""
|
||||||
|
if value == self.instance.email:
|
||||||
|
raise serializers.ValidationError(detail='Equal email address.')
|
||||||
|
return value
|
||||||
|
|
||||||
|
def validate_username(self, value):
|
||||||
|
"""Custom username validation"""
|
||||||
|
valid = utils_methods.username_validator(username=value)
|
||||||
|
if not valid:
|
||||||
|
raise utils_exceptions.NotValidUsernameError()
|
||||||
|
return value
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
"""Override update method"""
|
||||||
|
instance = super().update(instance, validated_data)
|
||||||
|
if 'email' in validated_data:
|
||||||
|
instance.email_confirmed = False
|
||||||
|
instance.save()
|
||||||
|
# Send verification link on user email for change email address
|
||||||
|
if settings.USE_CELERY:
|
||||||
|
tasks.change_email_address.delay(
|
||||||
|
user_id=instance.id,
|
||||||
|
country_code=self.context.get('request').country_code)
|
||||||
|
else:
|
||||||
|
tasks.change_email_address(
|
||||||
|
user_id=instance.id,
|
||||||
|
country_code=self.context.get('request').country_code)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class ChangePasswordSerializer(serializers.ModelSerializer):
|
||||||
|
"""Serializer for model User."""
|
||||||
|
|
||||||
|
password = serializers.CharField(write_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class"""
|
||||||
|
model = models.User
|
||||||
|
fields = ('password', )
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
"""Override validate method"""
|
||||||
|
password = attrs.get('password')
|
||||||
|
try:
|
||||||
|
# Compare new password with the old ones
|
||||||
|
if self.instance.check_password(raw_password=password):
|
||||||
|
raise utils_exceptions.PasswordsAreEqual()
|
||||||
|
# Validate password
|
||||||
|
password_validators.validate_password(password=password)
|
||||||
|
except serializers.ValidationError as e:
|
||||||
|
raise serializers.ValidationError(str(e))
|
||||||
|
else:
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
"""Override update method"""
|
||||||
|
# Update user password from instance
|
||||||
|
instance.set_password(validated_data.get('password'))
|
||||||
|
instance.save()
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeEmailSerializer(serializers.ModelSerializer):
|
||||||
|
"""Change user email serializer"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class"""
|
||||||
|
model = models.User
|
||||||
|
fields = (
|
||||||
|
'email',
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate_email(self, value):
|
||||||
|
"""Validate email value"""
|
||||||
|
if value == self.instance.email:
|
||||||
|
raise serializers.ValidationError()
|
||||||
|
return value
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
"""Override validate method"""
|
||||||
|
email_confirmed = self.instance.email_confirmed
|
||||||
|
if not email_confirmed:
|
||||||
|
raise serializers.ValidationError()
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
"""
|
||||||
|
Override update method
|
||||||
|
"""
|
||||||
|
instance.email = validated_data.get('email')
|
||||||
|
instance.email_confirmed = False
|
||||||
|
instance.save()
|
||||||
|
# Send verification link on user email for change email address
|
||||||
|
if settings.USE_CELERY:
|
||||||
|
tasks.confirm_new_email_address.delay(instance.id)
|
||||||
|
else:
|
||||||
|
tasks.confirm_new_email_address(instance.id)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class ConfirmEmailSerializer(serializers.ModelSerializer):
|
||||||
|
"""Confirm user email serializer"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class"""
|
||||||
|
model = models.User
|
||||||
|
fields = (
|
||||||
|
'email',
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
"""Override validate method"""
|
||||||
|
email_confirmed = self.instance.email_confirmed
|
||||||
|
if email_confirmed:
|
||||||
|
raise utils_exceptions.EmailConfirmedError()
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
"""
|
||||||
|
Override update method
|
||||||
|
"""
|
||||||
|
# Send verification link on user email for change email address
|
||||||
|
if settings.USE_CELERY:
|
||||||
|
tasks.confirm_new_email_address.delay(instance.id)
|
||||||
|
else:
|
||||||
|
tasks.confirm_new_email_address(instance.id)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
# Firebase Cloud Messaging serializers
|
# Firebase Cloud Messaging serializers
|
||||||
class FCMDeviceSerializer(serializers.ModelSerializer):
|
class FCMDeviceSerializer(serializers.ModelSerializer):
|
||||||
|
|
|
||||||
|
|
@ -1,237 +1,75 @@
|
||||||
"""Serializers for account web"""
|
"""Serializers for account web"""
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth import password_validation as password_validators
|
from django.contrib.auth import password_validation as password_validators
|
||||||
from django.db.models import Q
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from account import models, tasks
|
from account import models
|
||||||
from authorization.models import JWTRefreshToken
|
|
||||||
from utils import exceptions as utils_exceptions
|
from utils import exceptions as utils_exceptions
|
||||||
from utils.serializers import SourceSerializerMixin
|
from utils.methods import username_validator
|
||||||
from utils.tokens import GMRefreshToken
|
|
||||||
|
|
||||||
|
|
||||||
class PasswordResetSerializer(serializers.ModelSerializer):
|
class PasswordResetSerializer(serializers.Serializer):
|
||||||
"""Serializer from model PasswordReset"""
|
"""Serializer from model PasswordReset"""
|
||||||
username_or_email = serializers.CharField(required=False,
|
username_or_email = serializers.CharField(required=False,
|
||||||
write_only=True,)
|
write_only=True,)
|
||||||
|
|
||||||
class Meta:
|
@property
|
||||||
"""Meta class"""
|
def request(self):
|
||||||
model = models.ResetPasswordToken
|
"""Get request from context"""
|
||||||
fields = (
|
return self.context.get('request')
|
||||||
'username_or_email',
|
|
||||||
)
|
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
"""Override validate method"""
|
"""Override validate method"""
|
||||||
user = self.context.get('request').user
|
user = self.request.user
|
||||||
|
username_or_email = attrs.get('username_or_email')
|
||||||
|
|
||||||
if user.is_anonymous:
|
if not user.is_authenticated:
|
||||||
username_or_email = attrs.get('username_or_email')
|
|
||||||
if not username_or_email:
|
if not username_or_email:
|
||||||
raise serializers.ValidationError(_('Username or Email not requested'))
|
raise serializers.ValidationError(_('username or email not in request body.'))
|
||||||
# Check user in DB
|
|
||||||
user_qs = models.User.objects.filter(Q(email=username_or_email) |
|
filters = {}
|
||||||
Q(username=username_or_email))
|
if username_validator(username_or_email):
|
||||||
if user_qs.exists():
|
filters.update({'username__icontains': username_or_email})
|
||||||
attrs['user'] = user_qs.first()
|
|
||||||
else:
|
else:
|
||||||
raise utils_exceptions.UserNotFoundError()
|
filters.update({'email__icontains': username_or_email})
|
||||||
else:
|
|
||||||
attrs['user'] = user
|
if filters:
|
||||||
|
filters.update({'is_active': True})
|
||||||
|
user_qs = models.User.objects.filter(**filters)
|
||||||
|
|
||||||
|
if not user_qs.exists():
|
||||||
|
raise utils_exceptions.UserNotFoundError()
|
||||||
|
user = user_qs.first()
|
||||||
|
|
||||||
|
attrs['user'] = user
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
def create(self, validated_data, *args, **kwargs):
|
|
||||||
"""Override create method"""
|
|
||||||
user = validated_data.pop('user')
|
|
||||||
ip_address = self.context.get('request').META.get('REMOTE_ADDR')
|
|
||||||
|
|
||||||
obj = models.ResetPasswordToken.objects.create(
|
|
||||||
user=user,
|
|
||||||
ip_address=ip_address,
|
|
||||||
source=models.ResetPasswordToken.WEB
|
|
||||||
)
|
|
||||||
if settings.USE_CELERY:
|
|
||||||
tasks.send_reset_password_email.delay(obj.id)
|
|
||||||
else:
|
|
||||||
tasks.send_reset_password_email(obj.id)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
class PasswordResetConfirmSerializer(serializers.ModelSerializer):
|
class PasswordResetConfirmSerializer(serializers.ModelSerializer):
|
||||||
"""Serializer for model User"""
|
"""Serializer for model User"""
|
||||||
|
|
||||||
password = serializers.CharField(write_only=True)
|
password = serializers.CharField(write_only=True)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
"""Meta class"""
|
|
||||||
model = models.ResetPasswordToken
|
|
||||||
fields = ('password', )
|
|
||||||
|
|
||||||
def validate(self, attrs):
|
|
||||||
"""Override validate method"""
|
|
||||||
user = self.instance.user
|
|
||||||
password = attrs.get('password')
|
|
||||||
try:
|
|
||||||
# Compare new password with the old ones
|
|
||||||
if user.check_password(raw_password=password):
|
|
||||||
raise utils_exceptions.PasswordsAreEqual()
|
|
||||||
# Validate password
|
|
||||||
password_validators.validate_password(password=password)
|
|
||||||
except serializers.ValidationError as e:
|
|
||||||
raise serializers.ValidationError(str(e))
|
|
||||||
else:
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
|
||||||
"""Override update method"""
|
|
||||||
# Update user password from instance
|
|
||||||
instance.user.set_password(validated_data.get('password'))
|
|
||||||
instance.user.save()
|
|
||||||
|
|
||||||
# Overdue instance
|
|
||||||
instance.overdue()
|
|
||||||
return instance
|
|
||||||
|
|
||||||
|
|
||||||
class ChangePasswordSerializer(serializers.ModelSerializer):
|
|
||||||
"""Serializer for model User."""
|
|
||||||
|
|
||||||
password = serializers.CharField(write_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class"""
|
"""Meta class"""
|
||||||
model = models.User
|
model = models.User
|
||||||
fields = ('password', )
|
fields = ('password', )
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate_password(self, value):
|
||||||
"""Override validate method"""
|
"""Password validation method."""
|
||||||
password = attrs.get('password')
|
|
||||||
try:
|
try:
|
||||||
# Compare new password with the old ones
|
# Compare new password with the old ones
|
||||||
if self.instance.check_password(raw_password=password):
|
if self.instance.check_password(raw_password=value):
|
||||||
raise utils_exceptions.PasswordsAreEqual()
|
raise utils_exceptions.PasswordsAreEqual()
|
||||||
# Validate password
|
# Validate password
|
||||||
password_validators.validate_password(password=password)
|
password_validators.validate_password(password=value)
|
||||||
except serializers.ValidationError as e:
|
except serializers.ValidationError as e:
|
||||||
raise serializers.ValidationError(str(e))
|
raise serializers.ValidationError(str(e))
|
||||||
else:
|
return value
|
||||||
return attrs
|
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
"""Override update method"""
|
"""Override update method"""
|
||||||
# Update user password from instance
|
# Update user password from instance
|
||||||
instance.set_password(validated_data.get('password'))
|
instance.set_password(validated_data.get('password'))
|
||||||
instance.save()
|
instance.save()
|
||||||
|
|
||||||
# Expire tokens
|
|
||||||
instance.expire_access_tokens()
|
|
||||||
instance.expire_refresh_tokens()
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
class ChangeEmailSerializer(serializers.ModelSerializer):
|
|
||||||
"""Change user email serializer"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
"""Meta class"""
|
|
||||||
model = models.User
|
|
||||||
fields = (
|
|
||||||
'email',
|
|
||||||
)
|
|
||||||
|
|
||||||
def validate_email(self, value):
|
|
||||||
"""Validate email value"""
|
|
||||||
if value == self.instance.email:
|
|
||||||
raise serializers.ValidationError()
|
|
||||||
return value
|
|
||||||
|
|
||||||
def validate(self, attrs):
|
|
||||||
"""Override validate method"""
|
|
||||||
email_confirmed = self.instance.email_confirmed
|
|
||||||
if not email_confirmed:
|
|
||||||
raise serializers.ValidationError()
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
|
||||||
"""
|
|
||||||
Override update method
|
|
||||||
"""
|
|
||||||
instance.email = validated_data.get('email')
|
|
||||||
instance.email_confirmed = False
|
|
||||||
instance.save()
|
|
||||||
# Send verification link on user email for change email address
|
|
||||||
if settings.USE_CELERY:
|
|
||||||
tasks.confirm_new_email_address.delay(instance.id)
|
|
||||||
else:
|
|
||||||
tasks.confirm_new_email_address(instance.id)
|
|
||||||
return instance
|
|
||||||
|
|
||||||
|
|
||||||
class ConfirmEmailSerializer(serializers.ModelSerializer):
|
|
||||||
"""Confirm user email serializer"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
"""Meta class"""
|
|
||||||
model = models.User
|
|
||||||
fields = (
|
|
||||||
'email',
|
|
||||||
)
|
|
||||||
|
|
||||||
def validate(self, attrs):
|
|
||||||
"""Override validate method"""
|
|
||||||
email_confirmed = self.instance.email_confirmed
|
|
||||||
if email_confirmed:
|
|
||||||
raise serializers.ValidationError()
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
|
||||||
"""
|
|
||||||
Override update method
|
|
||||||
"""
|
|
||||||
# Send verification link on user email for change email address
|
|
||||||
if settings.USE_CELERY:
|
|
||||||
tasks.confirm_new_email_address.delay(instance.id)
|
|
||||||
else:
|
|
||||||
tasks.confirm_new_email_address(instance.id)
|
|
||||||
return instance
|
|
||||||
|
|
||||||
|
|
||||||
class RefreshTokenSerializer(SourceSerializerMixin):
|
|
||||||
"""Serializer for refresh token view"""
|
|
||||||
refresh_token = serializers.CharField(read_only=True)
|
|
||||||
access_token = serializers.CharField(read_only=True)
|
|
||||||
|
|
||||||
def get_request(self):
|
|
||||||
"""Return request"""
|
|
||||||
return self.context.get('request')
|
|
||||||
|
|
||||||
def validate(self, attrs):
|
|
||||||
"""Override validate method"""
|
|
||||||
|
|
||||||
cookie_refresh_token = self.get_request().COOKIES.get('refresh_token')
|
|
||||||
# Check if refresh_token in COOKIES
|
|
||||||
if not cookie_refresh_token:
|
|
||||||
raise utils_exceptions.NotValidRefreshTokenError()
|
|
||||||
|
|
||||||
refresh_token = GMRefreshToken(cookie_refresh_token)
|
|
||||||
refresh_token_qs = JWTRefreshToken.objects.valid() \
|
|
||||||
.by_jti(jti=refresh_token.payload.get('jti'))
|
|
||||||
# Check if the user has refresh token
|
|
||||||
if not refresh_token_qs.exists():
|
|
||||||
raise utils_exceptions.NotValidRefreshTokenError()
|
|
||||||
|
|
||||||
old_refresh_token = refresh_token_qs.first()
|
|
||||||
source = old_refresh_token.source
|
|
||||||
user = old_refresh_token.user
|
|
||||||
|
|
||||||
# Expire existing tokens
|
|
||||||
old_refresh_token.expire()
|
|
||||||
old_refresh_token.access_token.expire()
|
|
||||||
|
|
||||||
# Create new one for user
|
|
||||||
response = user.create_jwt_tokens(source=source)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
|
||||||
|
|
@ -11,26 +11,37 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def send_reset_password_email(request_id):
|
def send_reset_password_email(user_id, country_code):
|
||||||
"""Send email to user for reset password."""
|
"""Send email to user for reset password."""
|
||||||
try:
|
try:
|
||||||
obj = models.ResetPasswordToken.objects.get(id=request_id)
|
user = models.User.objects.get(id=user_id)
|
||||||
user = obj.user
|
|
||||||
user.send_email(subject=_('Password resetting'),
|
user.send_email(subject=_('Password resetting'),
|
||||||
message=obj.reset_password_template)
|
message=user.reset_password_template(country_code))
|
||||||
except:
|
except:
|
||||||
logger.error(f'METHOD_NAME: {send_reset_password_email.__name__}\n'
|
logger.error(f'METHOD_NAME: {send_reset_password_email.__name__}\n'
|
||||||
f'DETAIL: Exception occurred for ResetPasswordToken instance: '
|
f'DETAIL: Exception occurred for reset password: '
|
||||||
f'{request_id}')
|
f'{user_id}')
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def confirm_new_email_address(user_id):
|
def confirm_new_email_address(user_id, country_code):
|
||||||
"""Send email to user new email."""
|
"""Send email to user new email."""
|
||||||
try:
|
try:
|
||||||
user = models.User.objects.get(id=user_id)
|
user = models.User.objects.get(id=user_id)
|
||||||
user.send_email(subject=_('Validate new email address'),
|
user.send_email(subject=_('Validate new email address'),
|
||||||
message=user.change_email_template)
|
message=user.confirm_email_template(country_code))
|
||||||
except:
|
except:
|
||||||
logger.error(f'METHOD_NAME: {confirm_new_email_address.__name__}\n'
|
logger.error(f'METHOD_NAME: {confirm_new_email_address.__name__}\n'
|
||||||
f'DETAIL: Exception occurred for user: {user_id}')
|
f'DETAIL: Exception occurred for user: {user_id}')
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def change_email_address(user_id, country_code):
|
||||||
|
"""Send email to user new email."""
|
||||||
|
try:
|
||||||
|
user = models.User.objects.get(id=user_id)
|
||||||
|
user.send_email(subject=_('Validate new email address'),
|
||||||
|
message=user.change_email_template(country_code))
|
||||||
|
except:
|
||||||
|
logger.error(f'METHOD_NAME: {change_email_address.__name__}\n'
|
||||||
|
f'DETAIL: Exception occurred for user: {user_id}')
|
||||||
|
|
|
||||||
|
|
@ -6,5 +6,7 @@ from account.views import common as views
|
||||||
app_name = 'account'
|
app_name = 'account'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('user/', views.UserView.as_view(), name='user-get-update'),
|
path('user/', views.UserRetrieveUpdateView.as_view(), name='user-retrieve-update'),
|
||||||
|
path('change-password/', views.ChangePasswordView.as_view(), name='change-password'),
|
||||||
|
path('email/confirm/<uidb64>/<token>/', views.ConfirmEmailView.as_view(), name='confirm-email'),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -7,19 +7,9 @@ from account.views import web as views
|
||||||
app_name = 'account'
|
app_name = 'account'
|
||||||
|
|
||||||
urlpatterns_api = [
|
urlpatterns_api = [
|
||||||
path('change-password/', views.ChangePasswordView.as_view(), name='change-password'),
|
|
||||||
path('reset-password/', views.PasswordResetView.as_view(), name='password-reset'),
|
path('reset-password/', views.PasswordResetView.as_view(), name='password-reset'),
|
||||||
path('form/reset-password/<uidb64>/<token>/', views.FormPasswordResetConfirmView.as_view(),
|
path('reset-password/confirm/<uidb64>/<token>/', views.PasswordResetConfirmView.as_view(),
|
||||||
name='form-password-reset-confirm'),
|
name='password-reset-confirm'),
|
||||||
path('form/reset-password/success/', views.FormPasswordResetSuccessView.as_view(),
|
|
||||||
name='form-password-reset-success'),
|
|
||||||
path('refresh-token/', views.RefreshTokenView.as_view(), name='refresh-token'),
|
|
||||||
path('change-email/', views.ChangeEmailView.as_view(), name='change-email'),
|
|
||||||
path('change-email/confirm/<uidb64>/<token>/', views.ChangeEmailConfirmView.as_view(),
|
|
||||||
name='change-email-confirm'),
|
|
||||||
path('confirm-email/', views.ConfirmEmailView.as_view(), name='confirm-email'),
|
|
||||||
path('confirm-email/<uidb64>/<token>/', views.ConfirmInactiveEmailView.as_view(),
|
|
||||||
name='inactive-email-confirm'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns = urlpatterns_api + \
|
urlpatterns = urlpatterns_api + \
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,22 @@
|
||||||
"""Common account views"""
|
"""Common account views"""
|
||||||
|
from django.utils.encoding import force_text
|
||||||
|
from django.utils.http import urlsafe_base64_decode
|
||||||
from fcm_django.models import FCMDevice
|
from fcm_django.models import FCMDevice
|
||||||
from rest_framework import generics, status
|
from rest_framework import generics
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
from utils.permissions import IsAuthenticatedAndTokenIsValid
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from account import models
|
from account import models
|
||||||
from account.serializers import common as serializers
|
from account.serializers import common as serializers
|
||||||
|
from utils import exceptions as utils_exceptions
|
||||||
|
from utils.models import GMTokenGenerator
|
||||||
|
from utils.views import JWTGenericViewMixin
|
||||||
|
|
||||||
|
|
||||||
# User views
|
# User views
|
||||||
class UserView(generics.RetrieveUpdateAPIView):
|
class UserRetrieveUpdateView(generics.RetrieveUpdateAPIView):
|
||||||
"""### User update view."""
|
"""User update view."""
|
||||||
serializer_class = serializers.UserSerializer
|
serializer_class = serializers.UserSerializer
|
||||||
queryset = models.User.objects.active()
|
queryset = models.User.objects.active()
|
||||||
|
|
||||||
|
|
@ -19,6 +24,65 @@ class UserView(generics.RetrieveUpdateAPIView):
|
||||||
return self.request.user
|
return self.request.user
|
||||||
|
|
||||||
|
|
||||||
|
class ChangePasswordView(generics.GenericAPIView):
|
||||||
|
"""Change password view"""
|
||||||
|
serializer_class = serializers.ChangePasswordSerializer
|
||||||
|
queryset = models.User.objects.active()
|
||||||
|
|
||||||
|
def patch(self, request, *args, **kwargs):
|
||||||
|
"""Implement PUT method"""
|
||||||
|
serializer = self.get_serializer(instance=self.request.user,
|
||||||
|
data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
return Response(status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class SendConfirmationEmailView(JWTGenericViewMixin):
|
||||||
|
"""Confirm email view."""
|
||||||
|
serializer_class = serializers.ConfirmEmailSerializer
|
||||||
|
queryset = models.User.objects.all()
|
||||||
|
|
||||||
|
def patch(self, request, *args, **kwargs):
|
||||||
|
"""Implement PATCH-method"""
|
||||||
|
# Get user instance
|
||||||
|
instance = self.request.user
|
||||||
|
|
||||||
|
serializer = self.get_serializer(data=request.data, instance=instance)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
return Response(status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfirmEmailView(JWTGenericViewMixin):
|
||||||
|
"""View for confirm changing email"""
|
||||||
|
|
||||||
|
permission_classes = (permissions.AllowAny,)
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
"""Implement GET-method"""
|
||||||
|
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_EMAIL).check_token(
|
||||||
|
user, token):
|
||||||
|
raise utils_exceptions.NotValidTokenError()
|
||||||
|
# Approve email status
|
||||||
|
user.confirm_email()
|
||||||
|
# Create tokens
|
||||||
|
tokens = user.create_jwt_tokens()
|
||||||
|
return self._put_cookies_in_response(
|
||||||
|
cookies=self._put_data_in_cookies(
|
||||||
|
access_token=tokens.get('access_token'),
|
||||||
|
refresh_token=tokens.get('refresh_token')),
|
||||||
|
response=Response(status=status.HTTP_200_OK))
|
||||||
|
else:
|
||||||
|
raise utils_exceptions.UserNotFoundError()
|
||||||
|
|
||||||
|
|
||||||
# Firebase Cloud Messaging
|
# Firebase Cloud Messaging
|
||||||
class FCMDeviceViewSet(generics.GenericAPIView):
|
class FCMDeviceViewSet(generics.GenericAPIView):
|
||||||
"""FCMDevice registration view.
|
"""FCMDevice registration view.
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,43 @@
|
||||||
"""Web account views"""
|
"""Web account views"""
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.tokens import default_token_generator
|
from django.contrib.auth.tokens import default_token_generator as password_token_generator
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.http import HttpResponseRedirect
|
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.decorators import method_decorator
|
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.utils.http import urlsafe_base64_decode
|
from django.utils.http import urlsafe_base64_decode
|
||||||
from django.utils.translation import gettext_lazy as _
|
from rest_framework import permissions, status, generics
|
||||||
from django.views.decorators.cache import never_cache
|
|
||||||
from django.views.decorators.debug import sensitive_post_parameters
|
|
||||||
from django.views.generic.edit import FormView
|
|
||||||
from rest_framework import generics
|
|
||||||
from rest_framework import permissions
|
|
||||||
from rest_framework import status
|
|
||||||
from rest_framework import views
|
|
||||||
from rest_framework.permissions import AllowAny
|
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from account import models
|
from account import tasks, models
|
||||||
from account.forms import SetPasswordForm
|
|
||||||
from account.serializers import web as serializers
|
from account.serializers import web as serializers
|
||||||
from utils import exceptions as utils_exceptions
|
from utils import exceptions as utils_exceptions
|
||||||
from utils.models import GMTokenGenerator
|
from utils.views import JWTGenericViewMixin
|
||||||
from utils.views import (JWTCreateAPIView,
|
|
||||||
JWTUpdateAPIView,
|
|
||||||
JWTGenericViewMixin)
|
|
||||||
|
|
||||||
|
|
||||||
class PasswordResetView(JWTCreateAPIView):
|
class PasswordResetView(generics.GenericAPIView):
|
||||||
"""View for resetting user password"""
|
"""View for resetting user password"""
|
||||||
permission_classes = (permissions.AllowAny, )
|
permission_classes = (permissions.AllowAny, )
|
||||||
serializer_class = serializers.PasswordResetSerializer
|
serializer_class = serializers.PasswordResetSerializer
|
||||||
queryset = models.ResetPasswordToken.objects.valid()
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
"""Override create method"""
|
||||||
|
serializer = self.get_serializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
if serializer.validated_data.get('user'):
|
||||||
|
user = serializer.validated_data.pop('user')
|
||||||
|
if settings.USE_CELERY:
|
||||||
|
tasks.send_reset_password_email.delay(user_id=user.id,
|
||||||
|
country_code=self.request.country_code)
|
||||||
|
else:
|
||||||
|
tasks.send_reset_password_email(user_id=user.id,
|
||||||
|
country_code=self.request.country_code)
|
||||||
|
return Response(status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class PasswordResetConfirmView(JWTGenericViewMixin):
|
class PasswordResetConfirmView(JWTGenericViewMixin):
|
||||||
"""View for confirmation new password"""
|
"""View for confirmation new password"""
|
||||||
serializer_class = serializers.PasswordResetConfirmSerializer
|
serializer_class = serializers.PasswordResetConfirmSerializer
|
||||||
permission_classes = (permissions.AllowAny,)
|
permission_classes = (permissions.AllowAny,)
|
||||||
|
queryset = models.User.objects.active()
|
||||||
def get_queryset(self):
|
|
||||||
"""Override get_queryset method"""
|
|
||||||
return models.ResetPasswordToken.objects.valid()
|
|
||||||
|
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
"""Override get_object method
|
"""Override get_object method
|
||||||
|
|
@ -54,230 +48,27 @@ class PasswordResetConfirmView(JWTGenericViewMixin):
|
||||||
user_id = force_text(urlsafe_base64_decode(uidb64))
|
user_id = force_text(urlsafe_base64_decode(uidb64))
|
||||||
token = self.kwargs.get('token')
|
token = self.kwargs.get('token')
|
||||||
|
|
||||||
filter_kwargs = {'key': token, 'user_id': user_id}
|
obj = get_object_or_404(queryset, id=user_id)
|
||||||
obj = get_object_or_404(queryset, **filter_kwargs)
|
|
||||||
|
|
||||||
if not GMTokenGenerator(GMTokenGenerator.RESET_PASSWORD).check_token(
|
if not password_token_generator.check_token(user=obj, token=token):
|
||||||
user=obj.user, token=token):
|
raise utils_exceptions.NotValidTokenError()
|
||||||
raise utils_exceptions.NotValidAccessTokenError()
|
|
||||||
|
|
||||||
# May raise a permission denied
|
# May raise a permission denied
|
||||||
self.check_object_permissions(self.request, obj)
|
self.check_object_permissions(self.request, obj)
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def put(self, request, *args, **kwargs):
|
def patch(self, request, *args, **kwargs):
|
||||||
"""Implement PUT method"""
|
"""Implement PATCH method"""
|
||||||
instance = self.get_object()
|
instance = self.get_object()
|
||||||
serializer = self.get_serializer(instance=instance,
|
serializer = self.get_serializer(instance=instance,
|
||||||
data=request.data)
|
data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(status=status.HTTP_200_OK)
|
# Create tokens
|
||||||
|
tokens = instance.create_jwt_tokens()
|
||||||
|
|
||||||
class ChangePasswordView(JWTUpdateAPIView):
|
|
||||||
"""Change password view"""
|
|
||||||
serializer_class = serializers.ChangePasswordSerializer
|
|
||||||
queryset = models.User.objects.active()
|
|
||||||
|
|
||||||
def patch(self, request, *args, **kwargs):
|
|
||||||
"""Implement PUT method"""
|
|
||||||
serializer = self.get_serializer(instance=self.request.user,
|
|
||||||
data=request.data)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
serializer.save()
|
|
||||||
return Response(status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
|
|
||||||
class ChangeEmailView(JWTGenericViewMixin):
|
|
||||||
"""Change user email view."""
|
|
||||||
serializer_class = serializers.ChangeEmailSerializer
|
|
||||||
queryset = models.User.objects.all()
|
|
||||||
|
|
||||||
def patch(self, request, *args, **kwargs):
|
|
||||||
"""Implement POST-method"""
|
|
||||||
# Get user instance
|
|
||||||
instance = self.request.user
|
|
||||||
|
|
||||||
serializer = self.get_serializer(data=request.data, instance=instance)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
serializer.save()
|
|
||||||
return Response(status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
|
|
||||||
class ConfirmEmailView(ChangeEmailView):
|
|
||||||
"""Confirm email view."""
|
|
||||||
serializer_class = serializers.ConfirmEmailSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class ChangeEmailConfirmView(JWTGenericViewMixin):
|
|
||||||
"""View for confirm changing email"""
|
|
||||||
|
|
||||||
permission_classes = (permissions.AllowAny,)
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
"""Implement GET-method"""
|
|
||||||
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.CHANGE_EMAIL).check_token(
|
|
||||||
user, token):
|
|
||||||
raise utils_exceptions.NotValidTokenError()
|
|
||||||
# Approve email status
|
|
||||||
user.confirm_email()
|
|
||||||
# Expire user tokens
|
|
||||||
user.expire_access_tokens()
|
|
||||||
user.expire_refresh_tokens()
|
|
||||||
|
|
||||||
return Response(status=status.HTTP_200_OK)
|
|
||||||
else:
|
|
||||||
raise utils_exceptions.UserNotFoundError()
|
|
||||||
|
|
||||||
|
|
||||||
class ConfirmInactiveEmailView(generics.GenericAPIView):
|
|
||||||
"""View for confirm inactive email"""
|
|
||||||
|
|
||||||
permission_classes = (permissions.AllowAny,)
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
"""Implement GET-method"""
|
|
||||||
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.CHANGE_EMAIL).check_token(
|
|
||||||
user, token):
|
|
||||||
raise utils_exceptions.NotValidTokenError()
|
|
||||||
# Approve email status
|
|
||||||
user.confirm_email()
|
|
||||||
return Response(status=status.HTTP_200_OK)
|
|
||||||
else:
|
|
||||||
raise utils_exceptions.UserNotFoundError()
|
|
||||||
|
|
||||||
|
|
||||||
class RefreshTokenView(JWTGenericViewMixin):
|
|
||||||
"""Refresh access_token"""
|
|
||||||
permission_classes = (AllowAny, )
|
|
||||||
serializer_class = serializers.RefreshTokenSerializer
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
serializer = self.get_serializer(data=request.data)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
response = Response(serializer.data, status=status.HTTP_201_CREATED)
|
|
||||||
access_token = serializer.data.get('access_token')
|
|
||||||
refresh_token = serializer.data.get('refresh_token')
|
|
||||||
return self._put_cookies_in_response(
|
return self._put_cookies_in_response(
|
||||||
cookies=self._put_data_in_cookies(access_token=access_token,
|
cookies=self._put_data_in_cookies(
|
||||||
refresh_token=refresh_token),
|
access_token=tokens.get('access_token'),
|
||||||
response=response)
|
refresh_token=tokens.get('refresh_token')),
|
||||||
|
response=Response(status=status.HTTP_200_OK))
|
||||||
|
|
||||||
# Form view
|
|
||||||
class PasswordContextMixin:
|
|
||||||
extra_context = None
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
context.update({
|
|
||||||
'title': self.title,
|
|
||||||
**(self.extra_context or {})
|
|
||||||
})
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class FormPasswordResetSuccessView(views.APIView):
|
|
||||||
"""View for successful reset password"""
|
|
||||||
|
|
||||||
permission_classes = (permissions.AllowAny, )
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
"""Implement GET-method"""
|
|
||||||
return Response(status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
|
|
||||||
class FormPasswordResetConfirmView(PasswordContextMixin, FormView):
|
|
||||||
|
|
||||||
INTERNAL_RESET_URL_TOKEN = 'set-password'
|
|
||||||
INTERNAL_RESET_SESSION_TOKEN = '_password_reset_token'
|
|
||||||
|
|
||||||
form_class = SetPasswordForm
|
|
||||||
post_reset_login = False
|
|
||||||
post_reset_login_backend = None
|
|
||||||
success_url = reverse_lazy('web:account:form-password-reset-success')
|
|
||||||
template_name = settings.CONFIRMATION_PASSWORD_RESET_TEMPLATE
|
|
||||||
title = _('Enter new password')
|
|
||||||
token_generator = default_token_generator
|
|
||||||
|
|
||||||
@method_decorator(sensitive_post_parameters())
|
|
||||||
@method_decorator(never_cache)
|
|
||||||
def dispatch(self, *args, **kwargs):
|
|
||||||
assert 'uidb64' in kwargs and 'token' in kwargs
|
|
||||||
|
|
||||||
self.validlink = False
|
|
||||||
self.user = self.get_user(kwargs['uidb64'])
|
|
||||||
|
|
||||||
if self.user is not None:
|
|
||||||
token = kwargs['token']
|
|
||||||
if token == self.INTERNAL_RESET_URL_TOKEN:
|
|
||||||
session_token = self.request.session.get(self.INTERNAL_RESET_SESSION_TOKEN)
|
|
||||||
if self.token_generator.check_token(self.user, session_token):
|
|
||||||
# If the token is valid, display the password reset form.
|
|
||||||
self.validlink = True
|
|
||||||
return super().dispatch(*args, **kwargs)
|
|
||||||
else:
|
|
||||||
if self.token_generator.check_token(self.user, token):
|
|
||||||
# Store the token in the session and redirect to the
|
|
||||||
# password reset form at a URL without the token. That
|
|
||||||
# avoids the possibility of leaking the token in the
|
|
||||||
# HTTP Referer header.
|
|
||||||
self.request.session[self.INTERNAL_RESET_SESSION_TOKEN] = token
|
|
||||||
redirect_url = self.request.path.replace(token, self.INTERNAL_RESET_URL_TOKEN)
|
|
||||||
return HttpResponseRedirect(redirect_url)
|
|
||||||
|
|
||||||
# Display the "Password reset unsuccessful" page.
|
|
||||||
return self.render_to_response(self.get_context_data())
|
|
||||||
|
|
||||||
def get_user(self, uidb64):
|
|
||||||
try:
|
|
||||||
# urlsafe_base64_decode() decodes to bytestring
|
|
||||||
uid = urlsafe_base64_decode(uidb64).decode()
|
|
||||||
user = models.User.objects.get(pk=uid)
|
|
||||||
except (TypeError, ValueError, OverflowError, models.User.DoesNotExist, ValidationError):
|
|
||||||
user = None
|
|
||||||
return user
|
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
|
||||||
kwargs = super().get_form_kwargs()
|
|
||||||
kwargs['user'] = self.user
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
# Saving form
|
|
||||||
form.save()
|
|
||||||
user = form.user
|
|
||||||
|
|
||||||
# Expire user tokens
|
|
||||||
user.expire_access_tokens()
|
|
||||||
user.expire_refresh_tokens()
|
|
||||||
|
|
||||||
# Pop session token
|
|
||||||
del self.request.session[self.INTERNAL_RESET_SESSION_TOKEN]
|
|
||||||
return super().form_valid(form)
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
if self.validlink:
|
|
||||||
context['validlink'] = True
|
|
||||||
else:
|
|
||||||
context.update({
|
|
||||||
'form': None,
|
|
||||||
'title': _('Password reset unsuccessful'),
|
|
||||||
'validlink': False,
|
|
||||||
})
|
|
||||||
return context
|
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,18 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from oauth2_provider import models as oauth2_models
|
||||||
|
from rest_framework.authtoken.models import Token
|
||||||
|
from rest_framework_simplejwt.token_blacklist import models as jwt_models
|
||||||
|
from social_django import models as social_models
|
||||||
|
|
||||||
from authorization import models
|
from authorization import models
|
||||||
|
|
||||||
|
|
||||||
|
# Unregister unused models
|
||||||
|
admin.site.unregister(jwt_models.OutstandingToken)
|
||||||
|
admin.site.unregister(jwt_models.BlacklistedToken)
|
||||||
|
admin.site.unregister(oauth2_models.AccessToken)
|
||||||
|
admin.site.unregister(oauth2_models.RefreshToken)
|
||||||
|
admin.site.unregister(oauth2_models.Grant)
|
||||||
|
admin.site.unregister(social_models.Association)
|
||||||
|
admin.site.unregister(social_models.Nonce)
|
||||||
|
admin.site.unregister(Token)
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,8 @@ class Application(PlatformMixin, AbstractApplication):
|
||||||
|
|
||||||
class JWTAccessTokenManager(models.Manager):
|
class JWTAccessTokenManager(models.Manager):
|
||||||
"""Manager for AccessToken model."""
|
"""Manager for AccessToken model."""
|
||||||
def add_to_db(self, user, access_token: AccessToken,
|
|
||||||
refresh_token: RefreshToken):
|
def make(self, user, access_token: AccessToken, refresh_token: RefreshToken):
|
||||||
"""Create generated tokens to DB"""
|
"""Create generated tokens to DB"""
|
||||||
refresh_token_qs = JWTRefreshToken.objects.filter(user=user,
|
refresh_token_qs = JWTRefreshToken.objects.filter(user=user,
|
||||||
jti=refresh_token.payload.get('jti'))
|
jti=refresh_token.payload.get('jti'))
|
||||||
|
|
@ -106,18 +106,17 @@ class JWTAccessToken(ProjectBaseMixin):
|
||||||
|
|
||||||
class JWTRefreshTokenManager(models.Manager):
|
class JWTRefreshTokenManager(models.Manager):
|
||||||
"""Manager for model RefreshToken."""
|
"""Manager for model RefreshToken."""
|
||||||
|
def make(self, user, token: RefreshToken, source: int):
|
||||||
def add_to_db(self, user, token: RefreshToken, source: int):
|
"""Make method"""
|
||||||
"""Added generated refresh token to db"""
|
|
||||||
jti = token[settings.SIMPLE_JWT.get('JTI_CLAIM')]
|
jti = token[settings.SIMPLE_JWT.get('JTI_CLAIM')]
|
||||||
exp = token['exp']
|
exp = token['exp']
|
||||||
obj = self.model(
|
obj = self.model(
|
||||||
user=user,
|
user=user,
|
||||||
jti=jti,
|
jti=jti,
|
||||||
source=source,
|
|
||||||
created_at=token.current_time,
|
created_at=token.current_time,
|
||||||
expires_at=utils.datetime_from_epoch(exp),
|
expires_at=utils.datetime_from_epoch(exp))
|
||||||
)
|
if source:
|
||||||
|
obj.source = source
|
||||||
obj.save()
|
obj.save()
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,23 +8,20 @@ from rest_framework import validators as rest_validators
|
||||||
|
|
||||||
from account import models as account_models
|
from account import models as account_models
|
||||||
from authorization import tasks
|
from authorization import tasks
|
||||||
|
from authorization.models import JWTRefreshToken
|
||||||
from utils import exceptions as utils_exceptions
|
from utils import exceptions as utils_exceptions
|
||||||
from utils import methods as utils_methods
|
from utils import methods as utils_methods
|
||||||
from utils.serializers import SourceSerializerMixin
|
from utils.serializers import SourceSerializerMixin
|
||||||
|
from utils.tokens import GMRefreshToken
|
||||||
|
|
||||||
|
|
||||||
# Serializers
|
# Serializers
|
||||||
class SignupSerializer(serializers.ModelSerializer):
|
class SignupSerializer(serializers.ModelSerializer):
|
||||||
"""Signup serializer serializer mixin"""
|
"""Signup serializer serializer mixin"""
|
||||||
# REQUEST
|
# REQUEST
|
||||||
username = serializers.CharField(
|
username = serializers.CharField(write_only=True)
|
||||||
validators=(rest_validators.UniqueValidator(queryset=account_models.User.objects.all()),),
|
|
||||||
write_only=True
|
|
||||||
)
|
|
||||||
password = serializers.CharField(write_only=True)
|
password = serializers.CharField(write_only=True)
|
||||||
email = serializers.EmailField(
|
email = serializers.EmailField(write_only=True)
|
||||||
validators=(rest_validators.UniqueValidator(queryset=account_models.User.objects.all()),),
|
|
||||||
write_only=True)
|
|
||||||
newsletter = serializers.BooleanField(write_only=True)
|
newsletter = serializers.BooleanField(write_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
@ -36,34 +33,46 @@ class SignupSerializer(serializers.ModelSerializer):
|
||||||
'newsletter'
|
'newsletter'
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_username(self, data):
|
def validate_username(self, value):
|
||||||
"""Custom username validation"""
|
"""Custom username validation"""
|
||||||
valid = utils_methods.username_validator(username=data)
|
valid = utils_methods.username_validator(username=value)
|
||||||
if not valid:
|
if not valid:
|
||||||
raise utils_exceptions.NotValidUsernameError()
|
raise utils_exceptions.NotValidUsernameError()
|
||||||
return data
|
if account_models.User.objects.filter(username__icontains=value).exists():
|
||||||
|
raise serializers.ValidationError()
|
||||||
|
return value
|
||||||
|
|
||||||
def validate_password(self, data):
|
def validate_email(self, value):
|
||||||
|
"""Validate email"""
|
||||||
|
if account_models.User.objects.filter(email__icontains=value).exists():
|
||||||
|
raise serializers.ValidationError()
|
||||||
|
return value
|
||||||
|
|
||||||
|
def validate_password(self, value):
|
||||||
"""Custom password validation"""
|
"""Custom password validation"""
|
||||||
try:
|
try:
|
||||||
password_validators.validate_password(password=data)
|
password_validators.validate_password(password=value)
|
||||||
except serializers.ValidationError as e:
|
except serializers.ValidationError as e:
|
||||||
raise serializers.ValidationError(str(e))
|
raise serializers.ValidationError(str(e))
|
||||||
else:
|
else:
|
||||||
return data
|
return value
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
"""Override create method"""
|
"""Override create method"""
|
||||||
obj = account_models.User.objects.make(
|
obj = account_models.User.objects.make(
|
||||||
username=validated_data.get('username'),
|
username=validated_data.get('username'),
|
||||||
password=validated_data.get('password'),
|
password=validated_data.get('password'),
|
||||||
email=validated_data.get('email'),
|
email=validated_data.get('email').lower(),
|
||||||
newsletter=validated_data.get('newsletter'))
|
newsletter=validated_data.get('newsletter'))
|
||||||
# Send verification link on user email
|
# Send verification link on user email
|
||||||
if settings.USE_CELERY:
|
if settings.USE_CELERY:
|
||||||
tasks.send_confirm_email.delay(obj.id)
|
tasks.send_confirm_email.delay(
|
||||||
|
user_id=obj.id,
|
||||||
|
country_code=self.context.get('request').country_code)
|
||||||
else:
|
else:
|
||||||
tasks.send_confirm_email(obj.id)
|
tasks.send_confirm_email(
|
||||||
|
user_id=obj.id,
|
||||||
|
country_code=self.context.get('request').country_code)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -98,8 +107,7 @@ class LoginByUsernameOrEmailSerializer(SourceSerializerMixin,
|
||||||
username_or_email = attrs.pop('username_or_email')
|
username_or_email = attrs.pop('username_or_email')
|
||||||
password = attrs.pop('password')
|
password = attrs.pop('password')
|
||||||
user_qs = account_models.User.objects.filter(Q(username=username_or_email) |
|
user_qs = account_models.User.objects.filter(Q(username=username_or_email) |
|
||||||
(Q(email=username_or_email) &
|
(Q(email=username_or_email)))
|
||||||
Q(email_confirmed=True)))
|
|
||||||
if not user_qs.exists():
|
if not user_qs.exists():
|
||||||
raise utils_exceptions.UserNotFoundError()
|
raise utils_exceptions.UserNotFoundError()
|
||||||
else:
|
else:
|
||||||
|
|
@ -107,7 +115,7 @@ class LoginByUsernameOrEmailSerializer(SourceSerializerMixin,
|
||||||
authentication = authenticate(username=user.get_username(),
|
authentication = authenticate(username=user.get_username(),
|
||||||
password=password)
|
password=password)
|
||||||
if not authentication:
|
if not authentication:
|
||||||
raise utils_exceptions.WrongAuthCredentials()
|
raise utils_exceptions.UserNotFoundError()
|
||||||
self.instance = user
|
self.instance = user
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
@ -123,6 +131,40 @@ class LogoutSerializer(SourceSerializerMixin):
|
||||||
"""Serializer for Logout endpoint."""
|
"""Serializer for Logout endpoint."""
|
||||||
|
|
||||||
|
|
||||||
|
class RefreshTokenSerializer(SourceSerializerMixin):
|
||||||
|
"""Serializer for refresh token view"""
|
||||||
|
refresh_token = serializers.CharField(read_only=True)
|
||||||
|
access_token = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
"""Override validate method"""
|
||||||
|
|
||||||
|
cookie_refresh_token = self.context.get('request').COOKIES.get('refresh_token')
|
||||||
|
# Check if refresh_token in COOKIES
|
||||||
|
if not cookie_refresh_token:
|
||||||
|
raise utils_exceptions.NotValidRefreshTokenError()
|
||||||
|
|
||||||
|
refresh_token = GMRefreshToken(cookie_refresh_token)
|
||||||
|
refresh_token_qs = JWTRefreshToken.objects.valid() \
|
||||||
|
.by_jti(jti=refresh_token.payload.get('jti'))
|
||||||
|
# Check if the user has refresh token
|
||||||
|
if not refresh_token_qs.exists():
|
||||||
|
raise utils_exceptions.NotValidRefreshTokenError()
|
||||||
|
|
||||||
|
old_refresh_token = refresh_token_qs.first()
|
||||||
|
source = old_refresh_token.source
|
||||||
|
user = old_refresh_token.user
|
||||||
|
|
||||||
|
# Expire existing tokens
|
||||||
|
old_refresh_token.expire()
|
||||||
|
old_refresh_token.access_token.expire()
|
||||||
|
|
||||||
|
# Create new one for user
|
||||||
|
response = user.create_jwt_tokens(source=source)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
# OAuth
|
# OAuth
|
||||||
class OAuth2Serialzier(SourceSerializerMixin):
|
class OAuth2Serialzier(SourceSerializerMixin):
|
||||||
"""Serializer OAuth2 authorization"""
|
"""Serializer OAuth2 authorization"""
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,12 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def send_confirm_email(user_id):
|
def send_confirm_email(user_id, country_code):
|
||||||
"""Send verification email to user."""
|
"""Send verification email to user."""
|
||||||
try:
|
try:
|
||||||
obj = account_models.User.objects.get(id=user_id)
|
obj = account_models.User.objects.get(id=user_id)
|
||||||
obj.send_email(subject=_('Email confirmation'),
|
obj.send_email(subject=_('Email confirmation'),
|
||||||
message=obj.confirm_email_template)
|
message=obj.confirm_email_template(country_code))
|
||||||
except:
|
except:
|
||||||
logger.error(f'METHOD_NAME: {send_confirm_email.__name__}\n'
|
logger.error(f'METHOD_NAME: {send_confirm_email.__name__}\n'
|
||||||
f'DETAIL: Exception occurred for user: {user_id}')
|
f'DETAIL: Exception occurred for user: {user_id}')
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,11 @@ urlpatterns_oauth2 = [
|
||||||
|
|
||||||
urlpatterns_jwt = [
|
urlpatterns_jwt = [
|
||||||
path('signup/', views.SignUpView.as_view(), name='signup'),
|
path('signup/', views.SignUpView.as_view(), name='signup'),
|
||||||
path('signup/confirm/<uidb64>/<token>/', views.VerifyEmailConfirmView.as_view(),
|
path('signup/confirm/<uidb64>/<token>/', views.ConfirmationEmailView.as_view(),
|
||||||
name='signup-confirm'),
|
name='signup-confirm'),
|
||||||
path('login/', views.LoginByUsernameOrEmailView.as_view(), name='login'),
|
path('login/', views.LoginByUsernameOrEmailView.as_view(), name='login'),
|
||||||
path('logout/', views.LogoutView.as_view(), name="logout")
|
path('logout/', views.LogoutView.as_view(), name="logout"),
|
||||||
|
path('refresh-token/', views.RefreshTokenView.as_view(), name='refresh-token'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,12 @@ from authorization.serializers import common as serializers
|
||||||
from utils import exceptions as utils_exceptions
|
from utils import exceptions as utils_exceptions
|
||||||
from utils.models import GMTokenGenerator
|
from utils.models import GMTokenGenerator
|
||||||
from utils.permissions import IsAuthenticatedAndTokenIsValid
|
from utils.permissions import IsAuthenticatedAndTokenIsValid
|
||||||
from utils.views import (JWTGenericViewMixin,
|
from utils.views import JWTGenericViewMixin
|
||||||
JWTCreateAPIView)
|
|
||||||
|
|
||||||
|
|
||||||
# Mixins
|
# Mixins
|
||||||
# JWTAuthView mixin
|
# JWTAuthView mixin
|
||||||
class JWTAuthViewMixin(JWTCreateAPIView):
|
class JWTAuthViewMixin(JWTGenericViewMixin):
|
||||||
"""Mixin for authentication views"""
|
"""Mixin for authentication views"""
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
|
|
@ -151,7 +150,7 @@ class OAuth2SignUpView(OAuth2ViewMixin, JWTGenericViewMixin):
|
||||||
|
|
||||||
# JWT
|
# JWT
|
||||||
# Sign in via username and password
|
# Sign in via username and password
|
||||||
class SignUpView(JWTCreateAPIView):
|
class SignUpView(generics.GenericAPIView):
|
||||||
"""View for classic signup"""
|
"""View for classic signup"""
|
||||||
permission_classes = (permissions.AllowAny, )
|
permission_classes = (permissions.AllowAny, )
|
||||||
serializer_class = serializers.SignupSerializer
|
serializer_class = serializers.SignupSerializer
|
||||||
|
|
@ -164,7 +163,7 @@ class SignUpView(JWTCreateAPIView):
|
||||||
return Response(status=status.HTTP_201_CREATED)
|
return Response(status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
|
||||||
class VerifyEmailConfirmView(JWTGenericViewMixin):
|
class ConfirmationEmailView(JWTGenericViewMixin):
|
||||||
"""View for confirmation email"""
|
"""View for confirmation email"""
|
||||||
|
|
||||||
permission_classes = (permissions.AllowAny, )
|
permission_classes = (permissions.AllowAny, )
|
||||||
|
|
@ -182,9 +181,15 @@ class VerifyEmailConfirmView(JWTGenericViewMixin):
|
||||||
raise utils_exceptions.NotValidTokenError()
|
raise utils_exceptions.NotValidTokenError()
|
||||||
# Approve email status
|
# Approve email status
|
||||||
user.confirm_email()
|
user.confirm_email()
|
||||||
# Set user status as active
|
response = Response(status=status.HTTP_200_OK)
|
||||||
user.approve()
|
|
||||||
return Response(status=status.HTTP_200_OK)
|
# Create tokens
|
||||||
|
tokens = user.create_jwt_tokens()
|
||||||
|
return self._put_cookies_in_response(
|
||||||
|
cookies=self._put_data_in_cookies(
|
||||||
|
access_token=tokens.get('access_token'),
|
||||||
|
refresh_token=tokens.get('refresh_token')),
|
||||||
|
response=response)
|
||||||
else:
|
else:
|
||||||
raise utils_exceptions.UserNotFoundError()
|
raise utils_exceptions.UserNotFoundError()
|
||||||
|
|
||||||
|
|
@ -226,3 +231,23 @@ class LogoutView(JWTGenericViewMixin):
|
||||||
access_token_obj.refresh_token.expire()
|
access_token_obj.refresh_token.expire()
|
||||||
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
# Refresh token
|
||||||
|
class RefreshTokenView(JWTGenericViewMixin):
|
||||||
|
"""Refresh access_token"""
|
||||||
|
permission_classes = (permissions.AllowAny, )
|
||||||
|
serializer_class = serializers.RefreshTokenSerializer
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
serializer = self.get_serializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
response = Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
access_token = serializer.data.get('access_token')
|
||||||
|
refresh_token = serializer.data.get('refresh_token')
|
||||||
|
return self._put_cookies_in_response(
|
||||||
|
cookies=self._put_data_in_cookies(access_token=access_token,
|
||||||
|
refresh_token=refresh_token),
|
||||||
|
response=response)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
0
apps/comment/__init__.py
Normal file
0
apps/comment/__init__.py
Normal file
8
apps/comment/admin.py
Normal file
8
apps/comment/admin.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.Comment)
|
||||||
|
class CommentModelAdmin(admin.ModelAdmin):
|
||||||
|
"""Model admin for model Comment"""
|
||||||
8
apps/comment/apps.py
Normal file
8
apps/comment/apps.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class CommentConfig(AppConfig):
|
||||||
|
name = 'comment'
|
||||||
|
verbose_name = _('comment')
|
||||||
|
verbose_name_plural = _('comments')
|
||||||
36
apps/comment/migrations/0001_initial.py
Normal file
36
apps/comment/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# Generated by Django 2.2.4 on 2019-09-04 14:03
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Comment',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')),
|
||||||
|
('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||||
|
('text', models.TextField(verbose_name='Comment text')),
|
||||||
|
('mark', models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='Mark')),
|
||||||
|
('object_id', models.PositiveIntegerField()),
|
||||||
|
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Comment',
|
||||||
|
'verbose_name_plural': 'Comments',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
apps/comment/migrations/__init__.py
Normal file
0
apps/comment/migrations/__init__.py
Normal file
52
apps/comment/models.py
Normal file
52
apps/comment/models.py
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
"""Models for app comment."""
|
||||||
|
from django.contrib.contenttypes import fields as generic
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from account.models import User
|
||||||
|
from utils.models import ProjectBaseMixin
|
||||||
|
from utils.querysets import ContentTypeQuerySetMixin
|
||||||
|
|
||||||
|
|
||||||
|
class CommentQuerySet(ContentTypeQuerySetMixin):
|
||||||
|
"""QuerySets for Comment model."""
|
||||||
|
|
||||||
|
def by_user(self, user: User):
|
||||||
|
"""Return comments by author"""
|
||||||
|
return self.filter(user=user)
|
||||||
|
|
||||||
|
def annotate_is_mine_status(self, user):
|
||||||
|
"""Annotate belonging status"""
|
||||||
|
return self.annotate(is_mine=models.Case(
|
||||||
|
models.When(
|
||||||
|
models.Q(user=user if user.is_authenticated else None),
|
||||||
|
then=True
|
||||||
|
),
|
||||||
|
default=False,
|
||||||
|
output_field=models.BooleanField()
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
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'))
|
||||||
|
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()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class"""
|
||||||
|
verbose_name = _('Comment')
|
||||||
|
verbose_name_plural = _('Comments')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""String representation"""
|
||||||
|
return str(self.user)
|
||||||
0
apps/comment/serializers/__init__.py
Normal file
0
apps/comment/serializers/__init__.py
Normal file
27
apps/comment/serializers/common.py
Normal file
27
apps/comment/serializers/common.py
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
"""Common serializers for app comment."""
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from comment import models
|
||||||
|
|
||||||
|
|
||||||
|
class CommentSerializer(serializers.ModelSerializer):
|
||||||
|
"""Comment serializer"""
|
||||||
|
nickname = serializers.CharField(read_only=True,
|
||||||
|
source='user.username')
|
||||||
|
is_mine = serializers.BooleanField(read_only=True)
|
||||||
|
profile_pic = serializers.URLField(read_only=True,
|
||||||
|
source='user.cropped_image_url')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Serializer for model Comment"""
|
||||||
|
model = models.Comment
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'user_id',
|
||||||
|
'is_mine',
|
||||||
|
'created',
|
||||||
|
'text',
|
||||||
|
'mark',
|
||||||
|
'nickname',
|
||||||
|
'profile_pic'
|
||||||
|
]
|
||||||
0
apps/comment/serializers/mobile.py
Normal file
0
apps/comment/serializers/mobile.py
Normal file
1
apps/comment/serializers/web.py
Normal file
1
apps/comment/serializers/web.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""Serializers for app comment."""
|
||||||
1
apps/comment/tests.py
Normal file
1
apps/comment/tests.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Create your tests here.
|
||||||
0
apps/comment/urls/__init__.py
Normal file
0
apps/comment/urls/__init__.py
Normal file
5
apps/comment/urls/common.py
Normal file
5
apps/comment/urls/common.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
"""Comment urlpaths."""
|
||||||
|
|
||||||
|
app_name = 'comment'
|
||||||
|
|
||||||
|
urlpatterns = []
|
||||||
9
apps/comment/urls/mobile.py
Normal file
9
apps/comment/urls/mobile.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
"""Mobile urlpaths."""
|
||||||
|
from comment.urls.common import urlpatterns as common_urlpatterns
|
||||||
|
|
||||||
|
app_name = 'comment'
|
||||||
|
|
||||||
|
urlpatterns_api = []
|
||||||
|
|
||||||
|
urlpatterns = common_urlpatterns + \
|
||||||
|
urlpatterns_api
|
||||||
9
apps/comment/urls/web.py
Normal file
9
apps/comment/urls/web.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
"""Web urlpaths."""
|
||||||
|
from comment.urls.common import urlpatterns as common_urlpatterns
|
||||||
|
|
||||||
|
app_name = 'comment'
|
||||||
|
|
||||||
|
urlpatterns_api = []
|
||||||
|
|
||||||
|
urlpatterns = common_urlpatterns + \
|
||||||
|
urlpatterns_api
|
||||||
0
apps/comment/views/__init__.py
Normal file
0
apps/comment/views/__init__.py
Normal file
1
apps/comment/views/common.py
Normal file
1
apps/comment/views/common.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""Views for app comment."""
|
||||||
0
apps/comment/views/mobile.py
Normal file
0
apps/comment/views/mobile.py
Normal file
0
apps/comment/views/web.py
Normal file
0
apps/comment/views/web.py
Normal file
|
|
@ -1,10 +1,12 @@
|
||||||
"""Establishment admin conf."""
|
"""Establishment admin conf."""
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.contenttypes.admin import GenericTabularInline
|
from django.contrib.contenttypes.admin import GenericTabularInline
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from comment.models import Comment
|
||||||
from establishment import models
|
from establishment import models
|
||||||
from main.models import Award, MetaDataContent
|
from main.models import Award, MetaDataContent
|
||||||
from review import models as review_models
|
from review import models as review_models
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.EstablishmentType)
|
@admin.register(models.EstablishmentType)
|
||||||
|
|
@ -44,13 +46,18 @@ class ReviewInline(GenericTabularInline):
|
||||||
extra = 0
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
|
class CommentInline(GenericTabularInline):
|
||||||
|
model = Comment
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Establishment)
|
@admin.register(models.Establishment)
|
||||||
class EstablishmentAdmin(admin.ModelAdmin):
|
class EstablishmentAdmin(admin.ModelAdmin):
|
||||||
"""Establishment admin."""
|
"""Establishment admin."""
|
||||||
inlines = [
|
inlines = [
|
||||||
AwardInline, MetaDataContentInline,
|
AwardInline, MetaDataContentInline,
|
||||||
ContactPhoneInline, ContactEmailInline,
|
ContactPhoneInline, ContactEmailInline,
|
||||||
ReviewInline]
|
ReviewInline, CommentInline]
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.EstablishmentSchedule)
|
@admin.register(models.EstablishmentSchedule)
|
||||||
|
|
@ -58,11 +65,6 @@ class EstablishmentSchedule(admin.ModelAdmin):
|
||||||
"""Establishment schedule"""
|
"""Establishment schedule"""
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Comment)
|
|
||||||
class EstablishmentComment(admin.ModelAdmin):
|
|
||||||
"""Establishment comments."""
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Position)
|
@admin.register(models.Position)
|
||||||
class PositionAdmin(admin.ModelAdmin):
|
class PositionAdmin(admin.ModelAdmin):
|
||||||
"""Position admin."""
|
"""Position admin."""
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from establishment import models
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentFilter(filters.FilterSet):
|
class EstablishmentFilter(filters.FilterSet):
|
||||||
"""Establishment filterset."""
|
"""Establishment filter set."""
|
||||||
|
|
||||||
tag_id = filters.NumberFilter(field_name='tags__metadata__id',)
|
tag_id = filters.NumberFilter(field_name='tags__metadata__id',)
|
||||||
award_id = filters.NumberFilter(field_name='awards__id',)
|
award_id = filters.NumberFilter(field_name='awards__id',)
|
||||||
|
|
|
||||||
16
apps/establishment/migrations/0015_delete_comment.py
Normal file
16
apps/establishment/migrations/0015_delete_comment.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Generated by Django 2.2.4 on 2019-09-04 13:12
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('establishment', '0014_establishment_website'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='Comment',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 2.2.4 on 2019-09-11 12:52
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('establishment', '0015_delete_comment'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='establishment',
|
||||||
|
name='name',
|
||||||
|
),
|
||||||
|
]
|
||||||
55
apps/establishment/migrations/0017_auto_20190911_1258.py
Normal file
55
apps/establishment/migrations/0017_auto_20190911_1258.py
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
# Generated by Django 2.2.4 on 2019-09-11 12:58
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import utils.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('establishment', '0016_remove_establishment_name'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='establishment',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(default='', max_length=255, verbose_name='name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='establishment',
|
||||||
|
name='address',
|
||||||
|
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='location.Address', verbose_name='address'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='establishment',
|
||||||
|
name='description',
|
||||||
|
field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='description'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='establishment',
|
||||||
|
name='establishment_subtypes',
|
||||||
|
field=models.ManyToManyField(related_name='subtype_establishment', to='establishment.EstablishmentSubType', verbose_name='subtype'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='establishment',
|
||||||
|
name='establishment_type',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='establishment', to='establishment.EstablishmentType', verbose_name='type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='establishment',
|
||||||
|
name='price_level',
|
||||||
|
field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='price level'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='establishment',
|
||||||
|
name='public_mark',
|
||||||
|
field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='public mark'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='establishment',
|
||||||
|
name='toque_number',
|
||||||
|
field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='toque number'),
|
||||||
|
),
|
||||||
|
]
|
||||||
27
apps/establishment/migrations/0018_socialnetwork.py
Normal file
27
apps/establishment/migrations/0018_socialnetwork.py
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Generated by Django 2.2.4 on 2019-09-12 13:04
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('establishment', '0017_auto_20190911_1258'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SocialNetwork',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=255, verbose_name='title')),
|
||||||
|
('url', models.URLField(verbose_name='URL')),
|
||||||
|
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='socials', to='establishment.Establishment', verbose_name='establishment')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'social network',
|
||||||
|
'verbose_name_plural': 'social networks',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
"""Establishment models."""
|
"""Establishment models."""
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
from django.contrib.contenttypes import fields as generic
|
from django.contrib.contenttypes import fields as generic
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from phonenumber_field.modelfields import PhoneNumberField
|
from phonenumber_field.modelfields import PhoneNumberField
|
||||||
|
|
||||||
from location.models import Address
|
from location.models import Address
|
||||||
from utils.models import (ProjectBaseMixin, ImageMixin, TJSONField,
|
from utils.models import (ProjectBaseMixin, ImageMixin, TJSONField,
|
||||||
TranslatedFieldsMixin, BaseAttributes)
|
TranslatedFieldsMixin, BaseAttributes)
|
||||||
|
|
@ -86,36 +88,61 @@ class EstablishmentQuerySet(models.QuerySet):
|
||||||
'position'),
|
'position'),
|
||||||
to_attr='actual_establishment_employees'))
|
to_attr='actual_establishment_employees'))
|
||||||
|
|
||||||
|
def annotate_in_favorites(self, user):
|
||||||
|
"""Annotate flag in_favorites"""
|
||||||
|
favorite_establishments = []
|
||||||
|
if user.is_authenticated:
|
||||||
|
favorite_establishments = user.favorites.by_content_type(app_label='establishment',
|
||||||
|
model='establishment')\
|
||||||
|
.values_list('object_id', flat=True)
|
||||||
|
return self.annotate(in_favorites=models.Case(
|
||||||
|
models.When(
|
||||||
|
id__in=favorite_establishments,
|
||||||
|
then=True),
|
||||||
|
default=False,
|
||||||
|
output_field=models.BooleanField(default=False)))
|
||||||
|
|
||||||
|
def by_distance_from_point(self, center, radius, unit='m'):
|
||||||
|
"""
|
||||||
|
Returns nearest establishments
|
||||||
|
|
||||||
|
:param center: point from which to find nearby establishments
|
||||||
|
:param radius: the maximum distance within the radius of which to look for establishments
|
||||||
|
:return: all establishments within the specified radius of specified point
|
||||||
|
:param unit: length unit e.g. m, km. Default is 'm'.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.contrib.gis.measure import Distance
|
||||||
|
kwargs = {unit: radius}
|
||||||
|
return self.filter(address__coordinates__distance_lte=(center, Distance(**kwargs)))
|
||||||
|
|
||||||
|
|
||||||
class Establishment(ProjectBaseMixin, ImageMixin, TranslatedFieldsMixin):
|
class Establishment(ProjectBaseMixin, ImageMixin, TranslatedFieldsMixin):
|
||||||
"""Establishment model."""
|
"""Establishment model."""
|
||||||
|
|
||||||
STR_FIELD_NAME = 'name'
|
name = models.CharField(_('name'), max_length=255, default='')
|
||||||
|
|
||||||
name = TJSONField(blank=True, null=True, default=None,
|
|
||||||
verbose_name=_('Name'), help_text='{"en-GB":"some text"}')
|
|
||||||
description = TJSONField(blank=True, null=True, default=None,
|
description = TJSONField(blank=True, null=True, default=None,
|
||||||
verbose_name=_('Description'),
|
verbose_name=_('description'),
|
||||||
help_text='{"en-GB":"some text"}')
|
help_text='{"en-GB":"some text"}')
|
||||||
public_mark = models.PositiveIntegerField(blank=True, null=True,
|
public_mark = models.PositiveIntegerField(blank=True, null=True,
|
||||||
default=None,
|
default=None,
|
||||||
verbose_name=_('Public mark'),)
|
verbose_name=_('public mark'),)
|
||||||
toque_number = models.PositiveIntegerField(blank=True, null=True,
|
toque_number = models.PositiveIntegerField(blank=True, null=True,
|
||||||
default=None,
|
default=None,
|
||||||
verbose_name=_('Toque number'),)
|
verbose_name=_('toque number'),)
|
||||||
establishment_type = models.ForeignKey(EstablishmentType,
|
establishment_type = models.ForeignKey(EstablishmentType,
|
||||||
related_name='establishment',
|
related_name='establishment',
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
verbose_name=_('Type'))
|
verbose_name=_('type'))
|
||||||
establishment_subtypes = models.ManyToManyField(EstablishmentSubType,
|
establishment_subtypes = models.ManyToManyField(EstablishmentSubType,
|
||||||
related_name='subtype_establishment',
|
related_name='subtype_establishment',
|
||||||
verbose_name=_('Subtype'))
|
verbose_name=_('subtype'))
|
||||||
address = models.ForeignKey(Address, blank=True, null=True, default=None,
|
address = models.ForeignKey(Address, blank=True, null=True, default=None,
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
verbose_name=_('Address'))
|
verbose_name=_('address'))
|
||||||
price_level = models.PositiveIntegerField(blank=True, null=True,
|
price_level = models.PositiveIntegerField(blank=True, null=True,
|
||||||
default=None,
|
default=None,
|
||||||
verbose_name=_('Price level'))
|
verbose_name=_('price level'))
|
||||||
website = models.URLField(blank=True, null=True, default=None,
|
website = models.URLField(blank=True, null=True, default=None,
|
||||||
verbose_name=_('Web site URL'))
|
verbose_name=_('Web site URL'))
|
||||||
facebook = models.URLField(blank=True, null=True, default=None,
|
facebook = models.URLField(blank=True, null=True, default=None,
|
||||||
|
|
@ -129,6 +156,7 @@ class Establishment(ProjectBaseMixin, ImageMixin, TranslatedFieldsMixin):
|
||||||
awards = generic.GenericRelation(to='main.Award')
|
awards = generic.GenericRelation(to='main.Award')
|
||||||
tags = generic.GenericRelation(to='main.MetaDataContent')
|
tags = generic.GenericRelation(to='main.MetaDataContent')
|
||||||
reviews = generic.GenericRelation(to='review.Review')
|
reviews = generic.GenericRelation(to='review.Review')
|
||||||
|
comments = generic.GenericRelation(to='comment.Comment')
|
||||||
|
|
||||||
objects = EstablishmentQuerySet.as_manager()
|
objects = EstablishmentQuerySet.as_manager()
|
||||||
|
|
||||||
|
|
@ -138,6 +166,9 @@ class Establishment(ProjectBaseMixin, ImageMixin, TranslatedFieldsMixin):
|
||||||
verbose_name = _('Establishment')
|
verbose_name = _('Establishment')
|
||||||
verbose_name_plural = _('Establishments')
|
verbose_name_plural = _('Establishments')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'id:{self.id}-{self.name}'
|
||||||
|
|
||||||
# todo: recalculate toque_number
|
# todo: recalculate toque_number
|
||||||
def recalculate_toque_number(self):
|
def recalculate_toque_number(self):
|
||||||
self.toque_number = 4
|
self.toque_number = 4
|
||||||
|
|
@ -314,6 +345,7 @@ class ContactEmail(models.Model):
|
||||||
|
|
||||||
class Plate(TranslatedFieldsMixin, models.Model):
|
class Plate(TranslatedFieldsMixin, models.Model):
|
||||||
"""Plate model."""
|
"""Plate model."""
|
||||||
|
STR_FIELD_NAME = 'name'
|
||||||
|
|
||||||
name = TJSONField(
|
name = TJSONField(
|
||||||
blank=True, null=True, default=None, verbose_name=_('name'),
|
blank=True, null=True, default=None, verbose_name=_('name'),
|
||||||
|
|
@ -334,12 +366,12 @@ class Plate(TranslatedFieldsMixin, models.Model):
|
||||||
verbose_name = _('plate')
|
verbose_name = _('plate')
|
||||||
verbose_name_plural = _('plates')
|
verbose_name_plural = _('plates')
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f'plate_id:{self.id}'
|
|
||||||
|
|
||||||
|
|
||||||
class Menu(TranslatedFieldsMixin, BaseAttributes):
|
class Menu(TranslatedFieldsMixin, BaseAttributes):
|
||||||
"""Menu model."""
|
"""Menu model."""
|
||||||
|
|
||||||
|
STR_FIELD_NAME = 'category'
|
||||||
|
|
||||||
category = TJSONField(
|
category = TJSONField(
|
||||||
blank=True, null=True, default=None, verbose_name=_('category'),
|
blank=True, null=True, default=None, verbose_name=_('category'),
|
||||||
help_text='{"en-GB":"some text"}')
|
help_text='{"en-GB":"some text"}')
|
||||||
|
|
@ -352,36 +384,16 @@ class Menu(TranslatedFieldsMixin, BaseAttributes):
|
||||||
verbose_name_plural = _('menu')
|
verbose_name_plural = _('menu')
|
||||||
|
|
||||||
|
|
||||||
class CommentQuerySet(models.QuerySet):
|
class SocialNetwork(models.Model):
|
||||||
"""QuerySets for Comment model."""
|
establishment = models.ForeignKey(
|
||||||
|
'Establishment', verbose_name=_('establishment'),
|
||||||
def by_author(self, author):
|
related_name='socials', on_delete=models.CASCADE)
|
||||||
"""Return comments by author"""
|
title = models.CharField(_('title'), max_length=255)
|
||||||
return self.filter(author=author)
|
url = models.URLField(_('URL'))
|
||||||
|
|
||||||
|
|
||||||
class Comment(ProjectBaseMixin):
|
|
||||||
"""Comment model."""
|
|
||||||
text = models.TextField(verbose_name=_('Comment text'))
|
|
||||||
mark = models.PositiveIntegerField(blank=True, null=True,
|
|
||||||
default=None,
|
|
||||||
verbose_name=_('Mark'))
|
|
||||||
author = models.ForeignKey('account.User',
|
|
||||||
related_name='comments',
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
verbose_name=_('Author'))
|
|
||||||
establishment = models.ForeignKey(Establishment,
|
|
||||||
related_name='comments',
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
verbose_name=_('Establishment'))
|
|
||||||
|
|
||||||
objects = CommentQuerySet.as_manager()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class"""
|
verbose_name = _('social network')
|
||||||
verbose_name = _('Comment')
|
verbose_name_plural = _('social networks')
|
||||||
verbose_name_plural = _('Comments')
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""String representation"""
|
return self.title
|
||||||
return str(self.author)
|
|
||||||
|
|
|
||||||
|
|
@ -1,204 +0,0 @@
|
||||||
"""Establishment serializers."""
|
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from establishment import models
|
|
||||||
from location.serializers import AddressSerializer
|
|
||||||
from main.serializers import MetaDataContentSerializer, AwardSerializer, CurrencySerializer
|
|
||||||
from review import models as review_models
|
|
||||||
from timetable.models import Timetable
|
|
||||||
|
|
||||||
|
|
||||||
class ContactPhonesSerializer(serializers.ModelSerializer):
|
|
||||||
"""Contact phone serializer"""
|
|
||||||
class Meta:
|
|
||||||
model = models.ContactPhone
|
|
||||||
fields = [
|
|
||||||
'phone'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class ContactEmailsSerializer(serializers.ModelSerializer):
|
|
||||||
"""Contact email serializer"""
|
|
||||||
class Meta:
|
|
||||||
model = models.ContactEmail
|
|
||||||
fields = [
|
|
||||||
'email'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class PlateSerializer(serializers.ModelSerializer):
|
|
||||||
|
|
||||||
name_translated = serializers.CharField(allow_null=True)
|
|
||||||
currency = CurrencySerializer(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.Plate
|
|
||||||
fields = [
|
|
||||||
'name_translated',
|
|
||||||
'currency',
|
|
||||||
'price',
|
|
||||||
'is_signature_plate',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class MenuSerializers(serializers.ModelSerializer):
|
|
||||||
plates = PlateSerializer(read_only=True, many=True, source='plate_set')
|
|
||||||
category_translated = serializers.CharField(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.Menu
|
|
||||||
fields = [
|
|
||||||
'id',
|
|
||||||
'category_translated',
|
|
||||||
'plates'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentTypeSerializer(serializers.ModelSerializer):
|
|
||||||
"""Serializer for EstablishmentType model."""
|
|
||||||
|
|
||||||
name_translated = serializers.CharField(allow_null=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
"""Meta class."""
|
|
||||||
|
|
||||||
model = models.EstablishmentType
|
|
||||||
fields = ('id', 'name_translated')
|
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentSubTypeSerializer(serializers.ModelSerializer):
|
|
||||||
"""Serializer for EstablishmentSubType models."""
|
|
||||||
|
|
||||||
name_translated = serializers.CharField(allow_null=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
"""Meta class."""
|
|
||||||
|
|
||||||
model = models.EstablishmentSubType
|
|
||||||
fields = ('id', 'name_translated')
|
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentScheduleSerializer(serializers.ModelSerializer):
|
|
||||||
"""Serializer for Establishment model."""
|
|
||||||
weekday = serializers.CharField(source='get_weekday_display')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
"""Meta class."""
|
|
||||||
model = Timetable
|
|
||||||
fields = (
|
|
||||||
'weekday',
|
|
||||||
'lunch_start',
|
|
||||||
'lunch_end',
|
|
||||||
'dinner_start',
|
|
||||||
'dinner_end',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ReviewSerializer(serializers.ModelSerializer):
|
|
||||||
"""Serializer for model Review."""
|
|
||||||
text_translated = serializers.CharField(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
"""Meta class."""
|
|
||||||
model = review_models.Review
|
|
||||||
fields = (
|
|
||||||
'text_translated',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CommentSerializer(serializers.ModelSerializer):
|
|
||||||
"""Comment serializer"""
|
|
||||||
nickname = serializers.CharField(source='author.username')
|
|
||||||
profile_pic = serializers.ImageField(source='author.image')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
"""Serializer for model Comment"""
|
|
||||||
model = models.Comment
|
|
||||||
fields = (
|
|
||||||
'created',
|
|
||||||
'text',
|
|
||||||
'mark',
|
|
||||||
'nickname',
|
|
||||||
'profile_pic'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentEmployeeSerializer(serializers.ModelSerializer):
|
|
||||||
"""Serializer for actual employees."""
|
|
||||||
|
|
||||||
id = serializers.IntegerField(source='employee.id')
|
|
||||||
name = serializers.CharField(source='employee.name')
|
|
||||||
position_translated = serializers.CharField(source='position.name_translated')
|
|
||||||
awards = AwardSerializer(source='employee.awards', many=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
"""Meta class."""
|
|
||||||
|
|
||||||
model = models.Employee
|
|
||||||
fields = ('id', 'name', 'position_translated', 'awards')
|
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentSerializer(serializers.ModelSerializer):
|
|
||||||
"""Serializer for Establishment model."""
|
|
||||||
|
|
||||||
name_translated = serializers.CharField(allow_null=True)
|
|
||||||
description_translated = serializers.CharField(allow_null=True)
|
|
||||||
type = EstablishmentTypeSerializer(source='establishment_type')
|
|
||||||
subtypes = EstablishmentSubTypeSerializer(many=True)
|
|
||||||
address = AddressSerializer()
|
|
||||||
tags = MetaDataContentSerializer(many=True)
|
|
||||||
awards = AwardSerializer(many=True)
|
|
||||||
schedule = EstablishmentScheduleSerializer(source='schedule.schedule',
|
|
||||||
many=True,
|
|
||||||
allow_null=True)
|
|
||||||
phones = ContactPhonesSerializer(read_only=True, many=True, )
|
|
||||||
emails = ContactEmailsSerializer(read_only=True, many=True, )
|
|
||||||
reviews = ReviewSerializer(source='reviews.last', allow_null=True)
|
|
||||||
comments = CommentSerializer(many=True, allow_null=True)
|
|
||||||
employees = EstablishmentEmployeeSerializer(source='actual_establishment_employees',
|
|
||||||
many=True)
|
|
||||||
menu = MenuSerializers(source='menu_set', many=True, read_only=True)
|
|
||||||
preview_image = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
best_price_menu = serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True)
|
|
||||||
best_price_carte = serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
"""Meta class."""
|
|
||||||
|
|
||||||
model = models.Establishment
|
|
||||||
fields = (
|
|
||||||
'id',
|
|
||||||
'name_translated',
|
|
||||||
'description_translated',
|
|
||||||
'public_mark',
|
|
||||||
'price_level',
|
|
||||||
'toque_number',
|
|
||||||
'price_level',
|
|
||||||
'type',
|
|
||||||
'subtypes',
|
|
||||||
'image',
|
|
||||||
'preview_image',
|
|
||||||
'address',
|
|
||||||
'tags',
|
|
||||||
'awards',
|
|
||||||
'schedule',
|
|
||||||
'website',
|
|
||||||
'facebook',
|
|
||||||
'twitter',
|
|
||||||
'lafourchette',
|
|
||||||
'booking',
|
|
||||||
'phones',
|
|
||||||
'emails',
|
|
||||||
'reviews',
|
|
||||||
'comments',
|
|
||||||
'employees',
|
|
||||||
'menu',
|
|
||||||
'best_price_menu',
|
|
||||||
'best_price_carte'
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_preview_image(self, obj):
|
|
||||||
"""Get preview image"""
|
|
||||||
return obj.get_full_image_url(request=self.context.get('request'),
|
|
||||||
thumbnail_key='establishment_preview')
|
|
||||||
3
apps/establishment/serializers/__init__.py
Normal file
3
apps/establishment/serializers/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from establishment.serializers.common import *
|
||||||
|
from establishment.serializers.web import *
|
||||||
|
from establishment.serializers.back import *
|
||||||
85
apps/establishment/serializers/back.py
Normal file
85
apps/establishment/serializers/back.py
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from establishment import models
|
||||||
|
from establishment.serializers import (
|
||||||
|
EstablishmentBaseSerializer, PlateSerializer, ContactEmailsSerializer,
|
||||||
|
ContactPhonesSerializer, SocialNetworkRelatedSerializers)
|
||||||
|
from main.models import Currency
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
|
||||||
|
"""Establishment create serializer"""
|
||||||
|
|
||||||
|
type_id = serializers.PrimaryKeyRelatedField(
|
||||||
|
source='establishment_type',
|
||||||
|
queryset=models.EstablishmentType.objects.all(), write_only=True
|
||||||
|
)
|
||||||
|
phones = ContactPhonesSerializer(read_only=True, many=True, )
|
||||||
|
emails = ContactEmailsSerializer(read_only=True, many=True, )
|
||||||
|
socials = SocialNetworkRelatedSerializers(read_only=True, many=True, )
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Establishment
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'website',
|
||||||
|
'phones',
|
||||||
|
'emails',
|
||||||
|
'price_level',
|
||||||
|
'toque_number',
|
||||||
|
'type_id',
|
||||||
|
'type',
|
||||||
|
'socials'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class SocialNetworkSerializers(serializers.ModelSerializer):
|
||||||
|
"""Social network serializers."""
|
||||||
|
class Meta:
|
||||||
|
model = models.SocialNetwork
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'establishment',
|
||||||
|
'title',
|
||||||
|
'url',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PlatesSerializers(PlateSerializer):
|
||||||
|
"""Social network serializers."""
|
||||||
|
name = serializers.JSONField()
|
||||||
|
currency_id = serializers.PrimaryKeyRelatedField(
|
||||||
|
source='currency',
|
||||||
|
queryset=Currency.objects.all(), write_only=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Plate
|
||||||
|
fields = PlateSerializer.Meta.fields + [
|
||||||
|
'name',
|
||||||
|
'currency_id',
|
||||||
|
'menu'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ContactPhoneBackSerializers(PlateSerializer):
|
||||||
|
"""Social network serializers."""
|
||||||
|
class Meta:
|
||||||
|
model = models.ContactPhone
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'establishment',
|
||||||
|
'phone'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ContactEmailBackSerializers(PlateSerializer):
|
||||||
|
"""Social network serializers."""
|
||||||
|
class Meta:
|
||||||
|
model = models.ContactEmail
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'establishment',
|
||||||
|
'email'
|
||||||
|
]
|
||||||
365
apps/establishment/serializers/common.py
Normal file
365
apps/establishment/serializers/common.py
Normal file
|
|
@ -0,0 +1,365 @@
|
||||||
|
"""Establishment serializers."""
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from comment import models as comment_models
|
||||||
|
from comment.serializers import common as comment_serializers
|
||||||
|
from establishment import models
|
||||||
|
from favorites.models import Favorites
|
||||||
|
from location.serializers import AddressSerializer
|
||||||
|
from main.models import MetaDataContent
|
||||||
|
from main.serializers import MetaDataContentSerializer, AwardSerializer, CurrencySerializer
|
||||||
|
from review import models as review_models
|
||||||
|
from timetable.models import Timetable
|
||||||
|
from utils import exceptions as utils_exceptions
|
||||||
|
|
||||||
|
|
||||||
|
class ContactPhonesSerializer(serializers.ModelSerializer):
|
||||||
|
"""Contact phone serializer"""
|
||||||
|
class Meta:
|
||||||
|
model = models.ContactPhone
|
||||||
|
fields = [
|
||||||
|
'phone'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ContactEmailsSerializer(serializers.ModelSerializer):
|
||||||
|
"""Contact email serializer"""
|
||||||
|
class Meta:
|
||||||
|
model = models.ContactEmail
|
||||||
|
fields = [
|
||||||
|
'email'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class SocialNetworkRelatedSerializers(serializers.ModelSerializer):
|
||||||
|
"""Social network serializers."""
|
||||||
|
class Meta:
|
||||||
|
model = models.SocialNetwork
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'title',
|
||||||
|
'url',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PlateSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
name_translated = serializers.CharField(allow_null=True, read_only=True)
|
||||||
|
currency = CurrencySerializer(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Plate
|
||||||
|
fields = [
|
||||||
|
'name_translated',
|
||||||
|
'currency',
|
||||||
|
'price',
|
||||||
|
'is_signature_plate',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class MenuSerializers(serializers.ModelSerializer):
|
||||||
|
plates = PlateSerializer(read_only=True, many=True, source='plate_set')
|
||||||
|
category = serializers.JSONField()
|
||||||
|
category_translated = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Menu
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'category',
|
||||||
|
'category_translated',
|
||||||
|
'plates',
|
||||||
|
'establishment'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class MenuRUDSerializers(serializers.ModelSerializer):
|
||||||
|
plates = PlateSerializer(read_only=True, many=True, source='plate_set')
|
||||||
|
category = serializers.JSONField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Menu
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'category',
|
||||||
|
'plates',
|
||||||
|
'establishment'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentTypeSerializer(serializers.ModelSerializer):
|
||||||
|
"""Serializer for EstablishmentType model."""
|
||||||
|
|
||||||
|
name_translated = serializers.CharField(allow_null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
|
||||||
|
model = models.EstablishmentType
|
||||||
|
fields = ('id', 'name_translated')
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentSubTypeSerializer(serializers.ModelSerializer):
|
||||||
|
"""Serializer for EstablishmentSubType models."""
|
||||||
|
|
||||||
|
name_translated = serializers.CharField(allow_null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
|
||||||
|
model = models.EstablishmentSubType
|
||||||
|
fields = ('id', 'name_translated')
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentScheduleSerializer(serializers.ModelSerializer):
|
||||||
|
"""Serializer for Establishment model."""
|
||||||
|
weekday = serializers.CharField(source='get_weekday_display')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
model = Timetable
|
||||||
|
fields = (
|
||||||
|
'weekday',
|
||||||
|
'lunch_start',
|
||||||
|
'lunch_end',
|
||||||
|
'dinner_start',
|
||||||
|
'dinner_end',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewSerializer(serializers.ModelSerializer):
|
||||||
|
"""Serializer for model Review."""
|
||||||
|
text_translated = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
model = review_models.Review
|
||||||
|
fields = (
|
||||||
|
'text_translated',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentEmployeeSerializer(serializers.ModelSerializer):
|
||||||
|
"""Serializer for actual employees."""
|
||||||
|
|
||||||
|
id = serializers.IntegerField(source='employee.id')
|
||||||
|
name = serializers.CharField(source='employee.name')
|
||||||
|
position_translated = serializers.CharField(source='position.name_translated')
|
||||||
|
awards = AwardSerializer(source='employee.awards', many=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
|
||||||
|
model = models.Employee
|
||||||
|
fields = ('id', 'name', 'position_translated', 'awards')
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentBaseSerializer(serializers.ModelSerializer):
|
||||||
|
"""Base serializer for Establishment model."""
|
||||||
|
type = EstablishmentTypeSerializer(source='establishment_type', read_only=True)
|
||||||
|
subtypes = EstablishmentSubTypeSerializer(many=True)
|
||||||
|
address = AddressSerializer()
|
||||||
|
tags = MetaDataContentSerializer(many=True)
|
||||||
|
preview_image = serializers.ImageField(source='image')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
|
||||||
|
model = models.Establishment
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'price_level',
|
||||||
|
'toque_number',
|
||||||
|
'public_mark',
|
||||||
|
'type',
|
||||||
|
'subtypes',
|
||||||
|
'preview_image',
|
||||||
|
'address',
|
||||||
|
'tags',
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_preview_image(self, obj):
|
||||||
|
"""Get preview image"""
|
||||||
|
return obj.get_full_image_url(request=self.context.get('request'),
|
||||||
|
thumbnail_key='establishment_preview')
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentListSerializer(EstablishmentBaseSerializer):
|
||||||
|
"""Serializer for Establishment model."""
|
||||||
|
# Annotated fields
|
||||||
|
in_favorites = serializers.BooleanField(allow_null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
|
||||||
|
model = models.Establishment
|
||||||
|
fields = EstablishmentBaseSerializer.Meta.fields + [
|
||||||
|
'in_favorites',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentDetailSerializer(EstablishmentListSerializer):
|
||||||
|
"""Serializer for Establishment model."""
|
||||||
|
description_translated = serializers.CharField(allow_null=True)
|
||||||
|
awards = AwardSerializer(many=True)
|
||||||
|
schedule = EstablishmentScheduleSerializer(source='schedule.schedule',
|
||||||
|
many=True,
|
||||||
|
allow_null=True)
|
||||||
|
phones = ContactPhonesSerializer(read_only=True, many=True, )
|
||||||
|
emails = ContactEmailsSerializer(read_only=True, many=True, )
|
||||||
|
review = serializers.SerializerMethodField()
|
||||||
|
employees = EstablishmentEmployeeSerializer(source='actual_establishment_employees',
|
||||||
|
many=True)
|
||||||
|
menu = MenuSerializers(source='menu_set', many=True, read_only=True)
|
||||||
|
preview_image = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
best_price_menu = serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True)
|
||||||
|
best_price_carte = serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True)
|
||||||
|
|
||||||
|
in_favorites = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
|
||||||
|
model = models.Establishment
|
||||||
|
fields = EstablishmentListSerializer.Meta.fields + [
|
||||||
|
'description_translated',
|
||||||
|
'price_level',
|
||||||
|
'image',
|
||||||
|
'awards',
|
||||||
|
'schedule',
|
||||||
|
'website',
|
||||||
|
'facebook',
|
||||||
|
'twitter',
|
||||||
|
'lafourchette',
|
||||||
|
'booking',
|
||||||
|
'phones',
|
||||||
|
'emails',
|
||||||
|
'review',
|
||||||
|
'employees',
|
||||||
|
'menu',
|
||||||
|
'best_price_menu',
|
||||||
|
'best_price_carte',
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_review(self, obj):
|
||||||
|
"""Serializer method for getting last published review"""
|
||||||
|
return ReviewSerializer(obj.reviews.by_status(status=review_models.Review.READY)
|
||||||
|
.order_by('-published_at').first()).data
|
||||||
|
|
||||||
|
def get_in_favorites(self, obj):
|
||||||
|
"""Get in_favorites status flag"""
|
||||||
|
user = self.context.get('request').user
|
||||||
|
if user.is_authenticated:
|
||||||
|
return obj.id in user.favorites.by_content_type(app_label='establishment',
|
||||||
|
model='establishment')\
|
||||||
|
.values_list('object_id', flat=True)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer):
|
||||||
|
"""Create comment serializer"""
|
||||||
|
mark = serializers.IntegerField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Serializer for model Comment"""
|
||||||
|
model = comment_models.Comment
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'created',
|
||||||
|
'text',
|
||||||
|
'mark',
|
||||||
|
'nickname',
|
||||||
|
'profile_pic',
|
||||||
|
]
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
"""Override validate method"""
|
||||||
|
# Check establishment object
|
||||||
|
establishment_id = self.context.get('request').parser_context.get('kwargs').get('pk')
|
||||||
|
establishment_qs = models.Establishment.objects.filter(id=establishment_id)
|
||||||
|
if not establishment_qs.exists():
|
||||||
|
return serializers.ValidationError()
|
||||||
|
attrs['establishment'] = establishment_qs.first()
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def create(self, validated_data, *args, **kwargs):
|
||||||
|
"""Override create method"""
|
||||||
|
validated_data.update({
|
||||||
|
'user': self.context.get('request').user,
|
||||||
|
'content_object': validated_data.pop('establishment')
|
||||||
|
})
|
||||||
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentCommentRUDSerializer(comment_serializers.CommentSerializer):
|
||||||
|
"""Retrieve/Update/Destroy comment serializer."""
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
model = comment_models.Comment
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'created',
|
||||||
|
'text',
|
||||||
|
'mark',
|
||||||
|
'nickname',
|
||||||
|
'profile_pic',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentFavoritesCreateSerializer(serializers.ModelSerializer):
|
||||||
|
"""Create comment serializer"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Serializer for model Comment"""
|
||||||
|
model = Favorites
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'created',
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_user(self):
|
||||||
|
"""Get user from request"""
|
||||||
|
return self.context.get('request').user
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
"""Override validate method"""
|
||||||
|
# Check establishment object
|
||||||
|
establishment_id = self.context.get('request').parser_context.get('kwargs').get('pk')
|
||||||
|
establishment_qs = models.Establishment.objects.filter(id=establishment_id)
|
||||||
|
|
||||||
|
# Check establishment obj by pk from lookup_kwarg
|
||||||
|
if not establishment_qs.exists():
|
||||||
|
return serializers.ValidationError()
|
||||||
|
|
||||||
|
# Check existence in favorites
|
||||||
|
if self.get_user().favorites.by_content_type(app_label='establishment',
|
||||||
|
model='establishment')\
|
||||||
|
.by_object_id(object_id=establishment_id).exists():
|
||||||
|
raise utils_exceptions.FavoritesError()
|
||||||
|
|
||||||
|
attrs['establishment'] = establishment_qs.first()
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def create(self, validated_data, *args, **kwargs):
|
||||||
|
"""Override create method"""
|
||||||
|
validated_data.update({
|
||||||
|
'user': self.get_user(),
|
||||||
|
'content_object': validated_data.pop('establishment')
|
||||||
|
})
|
||||||
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentTagListSerializer(serializers.ModelSerializer):
|
||||||
|
"""List establishment tag serializer."""
|
||||||
|
label_translated = serializers.CharField(
|
||||||
|
source='metadata.label_translated', read_only=True, allow_null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
model = MetaDataContent
|
||||||
|
fields = [
|
||||||
|
'label_translated',
|
||||||
|
]
|
||||||
0
apps/establishment/serializers/web.py
Normal file
0
apps/establishment/serializers/web.py
Normal file
23
apps/establishment/urls/back.py
Normal file
23
apps/establishment/urls/back.py
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
"""Establishment url patterns for backoffice."""
|
||||||
|
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from establishment import views
|
||||||
|
|
||||||
|
app_name = 'establishment'
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', views.EstablishmentListCreateView.as_view(), name='list'),
|
||||||
|
path('<int:pk>/', views.EstablishmentRetrieveView.as_view(), name='detail'),
|
||||||
|
path('menus/', views.MenuListCreateView.as_view(), name='menu-list'),
|
||||||
|
path('menus/<int:pk>/', views.MenuRUDView.as_view(), name='menu-rud'),
|
||||||
|
path('plates/', views.PlateListCreateView.as_view(), name='plates'),
|
||||||
|
path('plates/<int:pk>/', views.PlateListCreateView.as_view(), name='plate-rud'),
|
||||||
|
path('socials/', views.SocialListCreateView.as_view(), name='socials'),
|
||||||
|
path('socials/<int:pk>/', views.SocialRUDView.as_view(), name='social-rud'),
|
||||||
|
path('phones/', views.PhonesListCreateView.as_view(), name='phones'),
|
||||||
|
path('phones/<int:pk>/', views.PhonesRUDView.as_view(), name='phones-rud'),
|
||||||
|
path('emails/', views.EmailListCreateView.as_view(), name='emails'),
|
||||||
|
path('emails/<int:pk>/', views.EmailRUDView.as_view(), name='emails-rud'),
|
||||||
|
]
|
||||||
|
|
@ -1,12 +1,19 @@
|
||||||
"""Establishment url patterns."""
|
"""Establishment url patterns."""
|
||||||
from django.urls import include, path
|
from django.urls import path
|
||||||
from establishment import views
|
|
||||||
|
|
||||||
|
from establishment import views
|
||||||
|
|
||||||
app_name = 'establishment'
|
app_name = 'establishment'
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', views.EstablishmentListView.as_view(), name='list'),
|
path('', views.EstablishmentListView.as_view(), name='list'),
|
||||||
|
path('tags/', views.EstablishmentTagListView.as_view(), name='tags'),
|
||||||
path('<int:pk>/', views.EstablishmentRetrieveView.as_view(), name='detail'),
|
path('<int:pk>/', views.EstablishmentRetrieveView.as_view(), name='detail'),
|
||||||
]
|
path('<int:pk>/comments/', views.EstablishmentCommentListView.as_view(), name='list-comments'),
|
||||||
|
path('<int:pk>/comments/create/', views.EstablishmentCommentCreateView.as_view(),
|
||||||
|
name='create-comment'),
|
||||||
|
path('<int:pk>/comments/<int:comment_id>/', views.EstablishmentCommentRUDView.as_view(),
|
||||||
|
name='rud-comment'),
|
||||||
|
path('<int:pk>/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(),
|
||||||
|
name='add-favorites')
|
||||||
|
]
|
||||||
|
|
|
||||||
11
apps/establishment/urls/mobile.py
Normal file
11
apps/establishment/urls/mobile.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
"""Establishment url patterns."""
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from establishment import views
|
||||||
|
from establishment.urls.common import urlpatterns as common_urlpatterns
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('geo/', views.EstablishmentNearestRetrieveView.as_view(), name='nearest-establishments-list')
|
||||||
|
]
|
||||||
|
|
||||||
|
urlpatterns.extend(common_urlpatterns)
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
"""Establishment app views."""
|
|
||||||
from rest_framework import generics, permissions
|
|
||||||
from establishment import models, serializers
|
|
||||||
from utils.views import JWTGenericViewMixin
|
|
||||||
from establishment import filters
|
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentMixin:
|
|
||||||
"""Establishment mixin."""
|
|
||||||
|
|
||||||
permission_classes = (permissions.AllowAny,)
|
|
||||||
serializer_class = serializers.EstablishmentSerializer
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
"""Overrided method 'get_queryset'."""
|
|
||||||
# todo: update ordering (last review)
|
|
||||||
return models.Establishment.objects.all().prefetch_actual_employees()
|
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentListView(EstablishmentMixin, JWTGenericViewMixin, generics.ListAPIView):
|
|
||||||
"""Resource for getting a list of establishments."""
|
|
||||||
|
|
||||||
filter_class = filters.EstablishmentFilter
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
"""Overrided method 'get_queryset'."""
|
|
||||||
qs = super(EstablishmentListView, self).get_queryset()
|
|
||||||
return qs.by_country_code(code=self.request.country_code)
|
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentRetrieveView(EstablishmentMixin, JWTGenericViewMixin, generics.RetrieveAPIView):
|
|
||||||
"""Resource for getting a establishment."""
|
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentTypeListView(JWTGenericViewMixin, generics.ListAPIView):
|
|
||||||
"""Resource for getting a list of establishment types."""
|
|
||||||
|
|
||||||
permission_classes = (permissions.AllowAny,)
|
|
||||||
serializer_class = serializers.EstablishmentTypeSerializer
|
|
||||||
queryset = models.EstablishmentType.objects.all()
|
|
||||||
|
|
||||||
3
apps/establishment/views/__init__.py
Normal file
3
apps/establishment/views/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from establishment.views.common import *
|
||||||
|
from establishment.views.web import *
|
||||||
|
from establishment.views.back import *
|
||||||
76
apps/establishment/views/back.py
Normal file
76
apps/establishment/views/back.py
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
"""Establishment app views."""
|
||||||
|
|
||||||
|
from rest_framework import generics
|
||||||
|
|
||||||
|
from establishment import models, serializers
|
||||||
|
from establishment.views.common import EstablishmentMixin
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentListCreateView(EstablishmentMixin, generics.ListCreateAPIView):
|
||||||
|
"""Establishment list/create view."""
|
||||||
|
queryset = models.Establishment.objects.all()
|
||||||
|
serializer_class = serializers.EstablishmentListCreateSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class MenuListCreateView(generics.ListCreateAPIView):
|
||||||
|
"""Menu list create view."""
|
||||||
|
serializer_class = serializers.MenuSerializers
|
||||||
|
queryset = models.Menu.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class MenuRUDView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
"""Menu RUD view."""
|
||||||
|
serializer_class = serializers.MenuRUDSerializers
|
||||||
|
queryset = models.Menu.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class SocialListCreateView(generics.ListCreateAPIView):
|
||||||
|
"""Social list create view."""
|
||||||
|
serializer_class = serializers.SocialNetworkSerializers
|
||||||
|
queryset = models.SocialNetwork.objects.all()
|
||||||
|
pagination_class = None
|
||||||
|
|
||||||
|
|
||||||
|
class SocialRUDView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
"""Social RUD view."""
|
||||||
|
serializer_class = serializers.SocialNetworkSerializers
|
||||||
|
queryset = models.SocialNetwork.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class PlateListCreateView(generics.ListCreateAPIView):
|
||||||
|
"""Plate list create view."""
|
||||||
|
serializer_class = serializers.PlatesSerializers
|
||||||
|
queryset = models.Plate.objects.all()
|
||||||
|
pagination_class = None
|
||||||
|
|
||||||
|
|
||||||
|
class PlateRUDView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
"""Social RUD view."""
|
||||||
|
serializer_class = serializers.PlatesSerializers
|
||||||
|
queryset = models.Plate.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class PhonesListCreateView(generics.ListCreateAPIView):
|
||||||
|
"""Plate list create view."""
|
||||||
|
serializer_class = serializers.ContactPhoneBackSerializers
|
||||||
|
queryset = models.ContactPhone.objects.all()
|
||||||
|
pagination_class = None
|
||||||
|
|
||||||
|
|
||||||
|
class PhonesRUDView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
"""Social RUD view."""
|
||||||
|
serializer_class = serializers.ContactPhoneBackSerializers
|
||||||
|
queryset = models.ContactPhone.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class EmailListCreateView(generics.ListCreateAPIView):
|
||||||
|
"""Plate list create view."""
|
||||||
|
serializer_class = serializers.ContactEmailBackSerializers
|
||||||
|
queryset = models.ContactEmail.objects.all()
|
||||||
|
pagination_class = None
|
||||||
|
|
||||||
|
|
||||||
|
class EmailRUDView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
"""Social RUD view."""
|
||||||
|
serializer_class = serializers.ContactEmailBackSerializers
|
||||||
|
queryset = models.ContactEmail.objects.all()
|
||||||
15
apps/establishment/views/common.py
Normal file
15
apps/establishment/views/common.py
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
"""Establishment app views."""
|
||||||
|
|
||||||
|
from rest_framework import permissions
|
||||||
|
|
||||||
|
from establishment import models
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentMixin:
|
||||||
|
"""Establishment mixin."""
|
||||||
|
|
||||||
|
permission_classes = (permissions.AllowAny,)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Overrided method 'get_queryset'."""
|
||||||
|
return models.Establishment.objects.all().prefetch_actual_employees()
|
||||||
142
apps/establishment/views/web.py
Normal file
142
apps/establishment/views/web.py
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
"""Establishment app views."""
|
||||||
|
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from rest_framework import generics, permissions
|
||||||
|
|
||||||
|
from comment import models as comment_models
|
||||||
|
from establishment import filters
|
||||||
|
from establishment import models, serializers
|
||||||
|
from main.models import MetaDataContent
|
||||||
|
from utils.views import JWTGenericViewMixin
|
||||||
|
from establishment.views import EstablishmentMixin
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentListView(EstablishmentMixin, JWTGenericViewMixin, generics.ListAPIView):
|
||||||
|
"""Resource for getting a list of establishments."""
|
||||||
|
serializer_class = serializers.EstablishmentListSerializer
|
||||||
|
filter_class = filters.EstablishmentFilter
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Overridden method 'get_queryset'."""
|
||||||
|
qs = super(EstablishmentListView, self).get_queryset()
|
||||||
|
return qs.by_country_code(code=self.request.country_code)\
|
||||||
|
.annotate_in_favorites(user=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentRetrieveView(EstablishmentMixin, JWTGenericViewMixin, generics.RetrieveAPIView):
|
||||||
|
"""Resource for getting a establishment."""
|
||||||
|
serializer_class = serializers.EstablishmentDetailSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentTypeListView(JWTGenericViewMixin, generics.ListAPIView):
|
||||||
|
"""Resource for getting a list of establishment types."""
|
||||||
|
|
||||||
|
permission_classes = (permissions.AllowAny,)
|
||||||
|
serializer_class = serializers.EstablishmentTypeSerializer
|
||||||
|
queryset = models.EstablishmentType.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentCommentCreateView(generics.CreateAPIView):
|
||||||
|
"""View for create new comment."""
|
||||||
|
serializer_class = serializers.EstablishmentCommentCreateSerializer
|
||||||
|
queryset = comment_models.Comment.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentCommentListView(generics.ListAPIView):
|
||||||
|
"""View for return list of establishment comments."""
|
||||||
|
permission_classes = (permissions.AllowAny,)
|
||||||
|
serializer_class = serializers.EstablishmentCommentCreateSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Override get_queryset method"""
|
||||||
|
return comment_models.Comment.objects.by_content_type(app_label='establishment',
|
||||||
|
model='establishment')\
|
||||||
|
.by_object_id(object_id=self.kwargs.get('pk'))\
|
||||||
|
.order_by('-created')
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
"""View for retrieve/update/destroy establishment comment."""
|
||||||
|
serializer_class = serializers.EstablishmentCommentRUDSerializer
|
||||||
|
queryset = models.Establishment.objects.all()
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
"""
|
||||||
|
Returns the object the view is displaying.
|
||||||
|
"""
|
||||||
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
lookup_url_kwargs = ('pk', 'comment_id')
|
||||||
|
|
||||||
|
assert lookup_url_kwargs not in self.kwargs.keys(), (
|
||||||
|
'Expected view %s to be called with a URL keyword argument '
|
||||||
|
'named "%s". Fix your URL conf, or set the `.lookup_field` '
|
||||||
|
'attribute on the view correctly.' %
|
||||||
|
(self.__class__.__name__, lookup_url_kwargs)
|
||||||
|
)
|
||||||
|
|
||||||
|
establishment_obj = get_object_or_404(queryset,
|
||||||
|
pk=self.kwargs['pk'])
|
||||||
|
comment_obj = get_object_or_404(establishment_obj.comments.by_user(self.request.user),
|
||||||
|
pk=self.kwargs['comment_id'])
|
||||||
|
|
||||||
|
# May raise a permission denied
|
||||||
|
self.check_object_permissions(self.request, comment_obj)
|
||||||
|
|
||||||
|
return comment_obj
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentFavoritesCreateDestroyView(generics.CreateAPIView, generics.DestroyAPIView):
|
||||||
|
"""View for create/destroy establishment from favorites."""
|
||||||
|
serializer_class = serializers.EstablishmentFavoritesCreateSerializer
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
"""
|
||||||
|
Returns the object the view is displaying.
|
||||||
|
"""
|
||||||
|
lookup_url_kwargs = ('pk',)
|
||||||
|
assert lookup_url_kwargs not in self.kwargs.keys(), (
|
||||||
|
'Expected view %s to be called with a URL keyword argument '
|
||||||
|
'named "%s". Fix your URL conf, or set the `.lookup_field` '
|
||||||
|
'attribute on the view correctly.' %
|
||||||
|
(self.__class__.__name__, lookup_url_kwargs)
|
||||||
|
)
|
||||||
|
|
||||||
|
obj = get_object_or_404(
|
||||||
|
self.request.user.favorites.by_user(user=self.request.user)
|
||||||
|
.by_content_type(app_label='establishment',
|
||||||
|
model='establishment')
|
||||||
|
.by_object_id(object_id=self.kwargs['pk']))
|
||||||
|
# May raise a permission denied
|
||||||
|
self.check_object_permissions(self.request, obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentNearestRetrieveView(EstablishmentMixin, JWTGenericViewMixin, generics.ListAPIView):
|
||||||
|
"""Resource for getting list of nearest establishments."""
|
||||||
|
serializer_class = serializers.EstablishmentListSerializer
|
||||||
|
filter_class = filters.EstablishmentFilter
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Overrided method 'get_queryset'."""
|
||||||
|
from django.contrib.gis.geos import Point
|
||||||
|
|
||||||
|
center = Point(float(self.request.query_params["lat"]), float(self.request.query_params["lon"]))
|
||||||
|
radius = float(self.request.query_params["radius"])
|
||||||
|
unit = self.request.query_params.get("unit", None)
|
||||||
|
by_distance_from_point_kwargs = {"center": center, "radius": radius, "unit": unit}
|
||||||
|
return super(EstablishmentNearestRetrieveView, self).get_queryset() \
|
||||||
|
.by_distance_from_point(**{k: v for k, v in by_distance_from_point_kwargs.items() if v is not None}) \
|
||||||
|
.by_country_code(code=self.request.country_code) \
|
||||||
|
.annotate_in_favorites(user=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentTagListView(generics.ListAPIView):
|
||||||
|
"""List view for establishment tags."""
|
||||||
|
serializer_class = serializers.EstablishmentTagListSerializer
|
||||||
|
permission_classes = (permissions.AllowAny,)
|
||||||
|
pagination_class = None
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Override get_queryset method"""
|
||||||
|
return MetaDataContent.objects.by_content_type(app_label='establishment',
|
||||||
|
model='establishment')
|
||||||
0
apps/favorites/__init__.py
Normal file
0
apps/favorites/__init__.py
Normal file
11
apps/favorites/admin.py
Normal file
11
apps/favorites/admin.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from favorites import models
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.Favorites)
|
||||||
|
class FavoritesModelAdmin(admin.ModelAdmin):
|
||||||
|
"""Admin model for model Favorites"""
|
||||||
|
list_display = ('id', 'user', )
|
||||||
|
list_filter = ('user', )
|
||||||
|
|
||||||
7
apps/favorites/apps.py
Normal file
7
apps/favorites/apps.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class FavoritesConfig(AppConfig):
|
||||||
|
name = 'favorites'
|
||||||
|
verbose_name = _('Favorites')
|
||||||
34
apps/favorites/migrations/0001_initial.py
Normal file
34
apps/favorites/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Generated by Django 2.2.4 on 2019-09-06 12:36
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Favorites',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')),
|
||||||
|
('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||||
|
('object_id', models.PositiveIntegerField()),
|
||||||
|
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorites', to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Favorites',
|
||||||
|
'verbose_name_plural': 'Favorites',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
apps/favorites/migrations/__init__.py
Normal file
0
apps/favorites/migrations/__init__.py
Normal file
37
apps/favorites/models.py
Normal file
37
apps/favorites/models.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
from django.contrib.contenttypes import fields as generic
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from utils.querysets import ContentTypeQuerySetMixin
|
||||||
|
from utils.models import ProjectBaseMixin
|
||||||
|
|
||||||
|
|
||||||
|
class FavoritesQuerySet(ContentTypeQuerySetMixin):
|
||||||
|
"""QuerySet for model Favorites"""
|
||||||
|
|
||||||
|
def by_user(self, user):
|
||||||
|
"""Filter by user"""
|
||||||
|
return self.filter(user=user)
|
||||||
|
|
||||||
|
|
||||||
|
class Favorites(ProjectBaseMixin):
|
||||||
|
"""Favorites model."""
|
||||||
|
|
||||||
|
user = models.ForeignKey('account.User',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='favorites',
|
||||||
|
verbose_name=_('User'))
|
||||||
|
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||||
|
object_id = models.PositiveIntegerField()
|
||||||
|
content_object = generic.GenericForeignKey('content_type', 'object_id')
|
||||||
|
|
||||||
|
objects = FavoritesQuerySet.as_manager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
verbose_name = _('Favorites')
|
||||||
|
verbose_name_plural = _('Favorites')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""String representation."""
|
||||||
|
return f'{self.id}'
|
||||||
17
apps/favorites/serializers.py
Normal file
17
apps/favorites/serializers.py
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
"""Serializers for app favorites."""
|
||||||
|
from .models import Favorites
|
||||||
|
from rest_framework import serializers
|
||||||
|
from establishment.serializers import EstablishmentBaseSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class FavoritesEstablishmentListSerializer(serializers.ModelSerializer):
|
||||||
|
"""Serializer for model Favorites"""
|
||||||
|
detail = EstablishmentBaseSerializer(source='content_object')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
model = Favorites
|
||||||
|
fields = (
|
||||||
|
'id',
|
||||||
|
'detail',
|
||||||
|
)
|
||||||
1
apps/favorites/tests.py
Normal file
1
apps/favorites/tests.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Create your tests here.
|
||||||
12
apps/favorites/urls.py
Normal file
12
apps/favorites/urls.py
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
"""Favorites urlpaths."""
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = 'favorites'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('establishments/', views.FavoritesEstablishmentListView.as_view(),
|
||||||
|
name='establishment-list'),
|
||||||
|
path('remove/<int:pk>/', views.FavoritesDestroyView.as_view(), name='remove-from-favorites'),
|
||||||
|
]
|
||||||
25
apps/favorites/views.py
Normal file
25
apps/favorites/views.py
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
"""Views for app favorites."""
|
||||||
|
from rest_framework import generics
|
||||||
|
from .serializers import FavoritesEstablishmentListSerializer
|
||||||
|
from .models import Favorites
|
||||||
|
|
||||||
|
|
||||||
|
class FavoritesBaseView(generics.GenericAPIView):
|
||||||
|
"""Base view for Favorites."""
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Override get_queryset method."""
|
||||||
|
return Favorites.objects.by_user(self.request.user)
|
||||||
|
|
||||||
|
|
||||||
|
class FavoritesEstablishmentListView(FavoritesBaseView, generics.ListAPIView):
|
||||||
|
"""List views for favorites"""
|
||||||
|
serializer_class = FavoritesEstablishmentListSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Override get_queryset method"""
|
||||||
|
return super().get_queryset().by_content_type(app_label='establishment',
|
||||||
|
model='establishment')
|
||||||
|
|
||||||
|
|
||||||
|
class FavoritesDestroyView(FavoritesBaseView, generics.DestroyAPIView):
|
||||||
|
"""Destroy view for favorites"""
|
||||||
|
|
@ -10,8 +10,8 @@ class ImageSerializer(serializers.ModelSerializer):
|
||||||
write_only=True)
|
write_only=True)
|
||||||
|
|
||||||
# RESPONSE
|
# RESPONSE
|
||||||
url = serializers.SerializerMethodField()
|
url = serializers.ImageField(source='image',
|
||||||
|
read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class"""
|
"""Meta class"""
|
||||||
|
|
@ -22,6 +22,3 @@ class ImageSerializer(serializers.ModelSerializer):
|
||||||
'url'
|
'url'
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_url(self, obj):
|
|
||||||
"""Get absolute URL path"""
|
|
||||||
return obj.get_full_image_url(request=self.context.get('request'))
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from . import models, serializers
|
||||||
|
|
||||||
class ImageUploadView(generics.CreateAPIView):
|
class ImageUploadView(generics.CreateAPIView):
|
||||||
"""Upload image to gallery"""
|
"""Upload image to gallery"""
|
||||||
permission_classes = (IsAuthenticatedAndTokenIsValid, )
|
|
||||||
model = models.Image
|
model = models.Image
|
||||||
queryset = models.Image.objects.all()
|
queryset = models.Image.objects.all()
|
||||||
serializer_class = serializers.ImageSerializer
|
serializer_class = serializers.ImageSerializer
|
||||||
|
permission_classes = (IsAuthenticatedAndTokenIsValid, )
|
||||||
|
|
|
||||||
|
|
@ -46,3 +46,6 @@ class CurrencContentAdmin(admin.ModelAdmin):
|
||||||
"""CurrencContent admin"""
|
"""CurrencContent admin"""
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.Carousel)
|
||||||
|
class CarouselAdmin(admin.ModelAdmin):
|
||||||
|
"""Carousel admin."""
|
||||||
|
|
|
||||||
27
apps/main/migrations/0014_carousel.py
Normal file
27
apps/main/migrations/0014_carousel.py
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Generated by Django 2.2.4 on 2019-09-13 11:33
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
('main', '0013_auto_20190901_1032'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Carousel',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('object_id', models.PositiveIntegerField()),
|
||||||
|
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Carousel',
|
||||||
|
'verbose_name_plural': 'Carousel',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -7,7 +7,9 @@ from django.utils.translation import gettext_lazy as _
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from location.models import Country
|
from location.models import Country
|
||||||
from main import methods
|
from main import methods
|
||||||
from utils.models import ProjectBaseMixin, TJSONField, TranslatedFieldsMixin
|
from utils.models import (ProjectBaseMixin, TJSONField,
|
||||||
|
TranslatedFieldsMixin, ImageMixin)
|
||||||
|
from utils.querysets import ContentTypeQuerySetMixin
|
||||||
from configuration.models import TranslationSettings
|
from configuration.models import TranslationSettings
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
@ -253,6 +255,10 @@ class MetaData(TranslatedFieldsMixin, models.Model):
|
||||||
return f'id:{self.id}-{label}'
|
return f'id:{self.id}-{label}'
|
||||||
|
|
||||||
|
|
||||||
|
class MetaDataContentQuerySet(ContentTypeQuerySetMixin):
|
||||||
|
"""QuerySets for MetaDataContent model."""
|
||||||
|
|
||||||
|
|
||||||
class MetaDataContent(models.Model):
|
class MetaDataContent(models.Model):
|
||||||
"""MetaDataContent model."""
|
"""MetaDataContent model."""
|
||||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||||
|
|
@ -260,6 +266,8 @@ class MetaDataContent(models.Model):
|
||||||
content_object = generic.GenericForeignKey('content_type', 'object_id')
|
content_object = generic.GenericForeignKey('content_type', 'object_id')
|
||||||
metadata = models.ForeignKey(MetaData, on_delete=models.CASCADE)
|
metadata = models.ForeignKey(MetaData, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
objects = MetaDataContentQuerySet.as_manager()
|
||||||
|
|
||||||
|
|
||||||
class Currency(models.Model):
|
class Currency(models.Model):
|
||||||
"""Currency model."""
|
"""Currency model."""
|
||||||
|
|
@ -271,3 +279,56 @@ class Currency(models.Model):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.name}'
|
return f'{self.name}'
|
||||||
|
|
||||||
|
|
||||||
|
class CarouselQuerySet(models.QuerySet):
|
||||||
|
"""Carousel QuerySet."""
|
||||||
|
|
||||||
|
|
||||||
|
class Carousel(models.Model):
|
||||||
|
"""Carousel model."""
|
||||||
|
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||||
|
object_id = models.PositiveIntegerField()
|
||||||
|
content_object = generic.GenericForeignKey('content_type', 'object_id')
|
||||||
|
|
||||||
|
objects = CarouselQuerySet.as_manager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
verbose_name = _('Carousel')
|
||||||
|
verbose_name_plural = _('Carousel')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
# Check if Generic obj has name or title
|
||||||
|
if hasattr(self.content_object, 'name'):
|
||||||
|
return self.content_object.name
|
||||||
|
if hasattr(self.content_object, 'title'):
|
||||||
|
return self.content_object.title_translated
|
||||||
|
|
||||||
|
@property
|
||||||
|
def awards(self):
|
||||||
|
if hasattr(self.content_object, 'awards'):
|
||||||
|
return self.content_object.awards
|
||||||
|
|
||||||
|
@property
|
||||||
|
def toque_number(self):
|
||||||
|
if hasattr(self.content_object, 'toque_number'):
|
||||||
|
return self.content_object.toque_number
|
||||||
|
|
||||||
|
@property
|
||||||
|
def public_mark(self):
|
||||||
|
if hasattr(self.content_object, 'public_mark'):
|
||||||
|
return self.content_object.public_mark
|
||||||
|
|
||||||
|
@property
|
||||||
|
def image(self):
|
||||||
|
# Check if Generic obj has an image
|
||||||
|
if not hasattr(self.content_object.image, 'url'):
|
||||||
|
# Check if Generic obj has a FK to gallery
|
||||||
|
return self.content_object.image.image.url
|
||||||
|
return self.content_object.image.url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model_name(self):
|
||||||
|
return self.content_object.__class__.__name__
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"""Main app serializers."""
|
"""Main app serializers."""
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from location.serializers import CountrySerializer
|
from location.serializers import CountrySerializer
|
||||||
from main import models
|
from main import models
|
||||||
|
|
||||||
|
|
@ -82,8 +83,8 @@ class SiteSerializer(serializers.ModelSerializer):
|
||||||
# )
|
# )
|
||||||
|
|
||||||
|
|
||||||
class AwardSerializer(serializers.ModelSerializer):
|
class AwardBaseSerializer(serializers.ModelSerializer):
|
||||||
"""Award serializer."""
|
"""Award base serializer."""
|
||||||
|
|
||||||
title_translated = serializers.CharField(read_only=True, allow_null=True)
|
title_translated = serializers.CharField(read_only=True, allow_null=True)
|
||||||
|
|
||||||
|
|
@ -92,11 +93,18 @@ class AwardSerializer(serializers.ModelSerializer):
|
||||||
fields = [
|
fields = [
|
||||||
'id',
|
'id',
|
||||||
'title_translated',
|
'title_translated',
|
||||||
'award_type',
|
|
||||||
'vintage_year',
|
'vintage_year',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class AwardSerializer(AwardBaseSerializer):
|
||||||
|
"""Award serializer."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Award
|
||||||
|
fields = AwardBaseSerializer.Meta.fields + ['award_type', ]
|
||||||
|
|
||||||
|
|
||||||
class MetaDataContentSerializer(serializers.ModelSerializer):
|
class MetaDataContentSerializer(serializers.ModelSerializer):
|
||||||
id = serializers.IntegerField(source='metadata.id', read_only=True,)
|
id = serializers.IntegerField(source='metadata.id', read_only=True,)
|
||||||
label_translated = serializers.CharField(
|
label_translated = serializers.CharField(
|
||||||
|
|
@ -117,4 +125,27 @@ class CurrencySerializer(serializers.ModelSerializer):
|
||||||
fields = [
|
fields = [
|
||||||
'id',
|
'id',
|
||||||
'name'
|
'name'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CarouselListSerializer(serializers.ModelSerializer):
|
||||||
|
"""Serializer for retrieving list of carousel items."""
|
||||||
|
model_name = serializers.CharField()
|
||||||
|
name = serializers.CharField()
|
||||||
|
toque_number = serializers.CharField()
|
||||||
|
public_mark = serializers.CharField()
|
||||||
|
image = serializers.URLField()
|
||||||
|
awards = AwardBaseSerializer(many=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta class."""
|
||||||
|
model = models.Carousel
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'model_name',
|
||||||
|
'name',
|
||||||
|
'awards',
|
||||||
|
'toque_number',
|
||||||
|
'public_mark',
|
||||||
|
'image',
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -10,4 +10,5 @@ urlpatterns = [
|
||||||
path('site-settings/<subdomain>/', views.SiteSettingsView.as_view(), name='site-settings'),
|
path('site-settings/<subdomain>/', views.SiteSettingsView.as_view(), name='site-settings'),
|
||||||
path('awards/', views.AwardView.as_view(), name='awards_list'),
|
path('awards/', views.AwardView.as_view(), name='awards_list'),
|
||||||
path('awards/<int:pk>/', views.AwardRetrieveView.as_view(), name='awards_retrieve'),
|
path('awards/<int:pk>/', views.AwardRetrieveView.as_view(), name='awards_retrieve'),
|
||||||
|
path('carousel/', views.CarouselListView.as_view(), name='carousel-list'),
|
||||||
]
|
]
|
||||||
|
|
@ -84,3 +84,11 @@ class AwardRetrieveView(generics.RetrieveAPIView):
|
||||||
serializer_class = serializers.AwardSerializer
|
serializer_class = serializers.AwardSerializer
|
||||||
queryset = models.Award.objects.all()
|
queryset = models.Award.objects.all()
|
||||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||||
|
|
||||||
|
|
||||||
|
class CarouselListView(generics.ListAPIView):
|
||||||
|
"""Return list of carousel items."""
|
||||||
|
queryset = models.Carousel.objects.all()
|
||||||
|
serializer_class = serializers.CarouselListSerializer
|
||||||
|
permission_classes = (permissions.AllowAny, )
|
||||||
|
pagination_class = None
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
"""News app common app."""
|
"""News app common app."""
|
||||||
from rest_framework import generics, permissions
|
from rest_framework import generics, permissions
|
||||||
|
|
||||||
from news import filters, models
|
from news import filters, models
|
||||||
from news.serializers import common as serializers
|
from news.serializers import common as serializers
|
||||||
from utils.views import JWTGenericViewMixin, JWTListAPIView
|
from utils.views import JWTGenericViewMixin
|
||||||
|
|
||||||
|
|
||||||
class NewsMixin:
|
class NewsMixin:
|
||||||
|
|
@ -18,7 +19,7 @@ class NewsMixin:
|
||||||
.order_by('-is_highlighted', '-created')
|
.order_by('-is_highlighted', '-created')
|
||||||
|
|
||||||
|
|
||||||
class NewsListView(NewsMixin, JWTListAPIView):
|
class NewsListView(NewsMixin, generics.ListAPIView):
|
||||||
"""News list view."""
|
"""News list view."""
|
||||||
|
|
||||||
filter_class = filters.NewsListFilterSet
|
filter_class = filters.NewsListFilterSet
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,10 @@ class ReviewQuerySet(models.QuerySet):
|
||||||
"""Return reviews by year"""
|
"""Return reviews by year"""
|
||||||
return self.filter(vintage=year)
|
return self.filter(vintage=year)
|
||||||
|
|
||||||
|
def by_status(self, status):
|
||||||
|
"""Filter by status"""
|
||||||
|
return self.filter(status=status)
|
||||||
|
|
||||||
|
|
||||||
class Review(BaseAttributes, TranslatedFieldsMixin):
|
class Review(BaseAttributes, TranslatedFieldsMixin):
|
||||||
"""Review model"""
|
"""Review model"""
|
||||||
|
|
|
||||||
|
|
@ -32,15 +32,6 @@ class UserNotFoundError(AuthErrorMixin, ProjectBaseException):
|
||||||
default_detail = _('User not found')
|
default_detail = _('User not found')
|
||||||
|
|
||||||
|
|
||||||
class PasswordRequestResetExists(ProjectBaseException):
|
|
||||||
"""
|
|
||||||
The exception should be thrown when request for reset password
|
|
||||||
is already exists and valid
|
|
||||||
"""
|
|
||||||
status_code = status.HTTP_400_BAD_REQUEST
|
|
||||||
default_detail = _('Password request is already exists. Please wait.')
|
|
||||||
|
|
||||||
|
|
||||||
class EmailSendingError(exceptions.APIException):
|
class EmailSendingError(exceptions.APIException):
|
||||||
"""The exception should be thrown when unable to send an email"""
|
"""The exception should be thrown when unable to send an email"""
|
||||||
status_code = status.HTTP_400_BAD_REQUEST
|
status_code = status.HTTP_400_BAD_REQUEST
|
||||||
|
|
@ -120,8 +111,34 @@ class EmailConfirmedError(exceptions.APIException):
|
||||||
default_detail = _('Email address is already confirmed')
|
default_detail = _('Email address is already confirmed')
|
||||||
|
|
||||||
|
|
||||||
|
class UserUpdateUploadImageError(exceptions.APIException):
|
||||||
|
"""
|
||||||
|
The exception should be raised when user tries upload an image without crop in request
|
||||||
|
"""
|
||||||
|
status_code = status.HTTP_400_BAD_REQUEST
|
||||||
|
default_detail = _('Image invalid input.')
|
||||||
|
|
||||||
|
|
||||||
class WrongAuthCredentials(AuthErrorMixin):
|
class WrongAuthCredentials(AuthErrorMixin):
|
||||||
"""
|
"""
|
||||||
The exception should be raised when credentials is not valid for this user
|
The exception should be raised when credentials is not valid for this user
|
||||||
"""
|
"""
|
||||||
default_detail = _('Wrong authorization credentials')
|
default_detail = _('Wrong authorization credentials')
|
||||||
|
|
||||||
|
|
||||||
|
class FavoritesError(exceptions.APIException):
|
||||||
|
"""
|
||||||
|
The exception should be thrown when item that user
|
||||||
|
want to add to favorites is already exists.
|
||||||
|
"""
|
||||||
|
status_code = status.HTTP_400_BAD_REQUEST
|
||||||
|
default_detail = _('Item is already in favorites.')
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetRequestExistedError(exceptions.APIException):
|
||||||
|
"""
|
||||||
|
The exception should be thrown when password reset request
|
||||||
|
already exists and valid.
|
||||||
|
"""
|
||||||
|
status_code = status.HTTP_400_BAD_REQUEST
|
||||||
|
default_detail = _('Password reset request is already exists and valid.')
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import re
|
||||||
import string
|
import string
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.http.request import HttpRequest
|
from django.http.request import HttpRequest
|
||||||
from django.utils.timezone import datetime
|
from django.utils.timezone import datetime
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
|
@ -78,3 +79,10 @@ def generate_string_code(size=64,
|
||||||
chars=string.ascii_lowercase + string.ascii_uppercase + string.digits):
|
chars=string.ascii_lowercase + string.ascii_uppercase + string.digits):
|
||||||
"""Generate string code."""
|
"""Generate string code."""
|
||||||
return ''.join([random.SystemRandom().choice(chars) for _ in range(size)])
|
return ''.join([random.SystemRandom().choice(chars) for _ in range(size)])
|
||||||
|
|
||||||
|
|
||||||
|
def get_contenttype(app_label: str, model: str):
|
||||||
|
"""Get ContentType instance by app_label and model"""
|
||||||
|
qs = ContentType.objects.filter(app_label=app_label, model=model)
|
||||||
|
if qs.exists():
|
||||||
|
return qs.first()
|
||||||
|
|
|
||||||
|
|
@ -32,3 +32,13 @@ def parse_cookies(get_response):
|
||||||
return response
|
return response
|
||||||
return middleware
|
return middleware
|
||||||
|
|
||||||
|
|
||||||
|
class CORSMiddleware:
|
||||||
|
"""Added parameter {Access-Control-Allow-Origin: *} to response"""
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
response = self.get_response(request)
|
||||||
|
response["Access-Control-Allow-Origin"] = '*'
|
||||||
|
return response
|
||||||
|
|
|
||||||
17
apps/utils/querysets.py
Normal file
17
apps/utils/querysets.py
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
"""Utils QuerySet Mixins"""
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from utils.methods import get_contenttype
|
||||||
|
|
||||||
|
|
||||||
|
class ContentTypeQuerySetMixin(models.QuerySet):
|
||||||
|
"""QuerySet for ContentType"""
|
||||||
|
|
||||||
|
def by_object_id(self, object_id: int):
|
||||||
|
"""Filter by object_id"""
|
||||||
|
return self.filter(object_id=object_id)
|
||||||
|
|
||||||
|
def by_content_type(self, app_label: str = 'favorites', model: str = 'favorites'):
|
||||||
|
"""Filter QuerySet by ContentType."""
|
||||||
|
return self.filter(content_type=get_contenttype(app_label=app_label,
|
||||||
|
model=model))
|
||||||
|
|
@ -33,12 +33,11 @@ class GMBlacklistMixin(BlacklistMixin):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def for_user_by_source(cls, user, source: int):
|
def for_user(cls, user, source: int = None):
|
||||||
"""Create a refresh token."""
|
"""Create a refresh token."""
|
||||||
token = super().for_user(user)
|
token = super().for_user(user)
|
||||||
token['user'] = user.get_user_info()
|
token['user'] = user.get_user_info()
|
||||||
# Create a record in DB
|
JWTRefreshToken.objects.make(user=user, token=token, source=source)
|
||||||
JWTRefreshToken.objects.add_to_db(user=user, token=token, source=source)
|
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -70,7 +69,7 @@ class GMRefreshToken(GMBlacklistMixin, GMToken, RefreshToken):
|
||||||
|
|
||||||
# Create a record in DB
|
# Create a record in DB
|
||||||
user = User.objects.get(id=self.payload.get('user_id'))
|
user = User.objects.get(id=self.payload.get('user_id'))
|
||||||
JWTAccessToken.objects.add_to_db(user=user,
|
JWTAccessToken.objects.make(user=user,
|
||||||
access_token=access_token,
|
access_token=access_token,
|
||||||
refresh_token=self)
|
refresh_token=self)
|
||||||
return access_token
|
return access_token
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from rest_framework.response import Response
|
||||||
|
|
||||||
|
|
||||||
# JWT
|
# JWT
|
||||||
# Login base view mixin
|
# Login base view mixins
|
||||||
class JWTGenericViewMixin(generics.GenericAPIView):
|
class JWTGenericViewMixin(generics.GenericAPIView):
|
||||||
"""JWT view mixin"""
|
"""JWT view mixin"""
|
||||||
|
|
||||||
|
|
@ -16,6 +16,13 @@ class JWTGenericViewMixin(generics.GenericAPIView):
|
||||||
|
|
||||||
REFRESH_TOKEN_HTTP_ONLY = False
|
REFRESH_TOKEN_HTTP_ONLY = False
|
||||||
REFRESH_TOKEN_SECURE = False
|
REFRESH_TOKEN_SECURE = False
|
||||||
|
|
||||||
|
LOCALE_HTTP_ONLY = False
|
||||||
|
LOCALE_SECURE = False
|
||||||
|
|
||||||
|
COUNTRY_CODE_HTTP_ONLY = False
|
||||||
|
COUNTRY_CODE_SECURE = False
|
||||||
|
|
||||||
COOKIE = namedtuple('COOKIE', ['key', 'value', 'http_only', 'secure', 'max_age'])
|
COOKIE = namedtuple('COOKIE', ['key', 'value', 'http_only', 'secure', 'max_age'])
|
||||||
|
|
||||||
def _put_data_in_cookies(self,
|
def _put_data_in_cookies(self,
|
||||||
|
|
@ -26,21 +33,32 @@ class JWTGenericViewMixin(generics.GenericAPIView):
|
||||||
cookies it is list that contain namedtuples
|
cookies it is list that contain namedtuples
|
||||||
cookies would contain key, value and secure parameters.
|
cookies would contain key, value and secure parameters.
|
||||||
"""
|
"""
|
||||||
COOKIES = list()
|
COOKIES = []
|
||||||
|
|
||||||
# Write to cookie access and refresh token with secure flag
|
if hasattr(self.request, 'locale'):
|
||||||
if access_token and refresh_token:
|
COOKIES.append(self.COOKIE(key='locale',
|
||||||
_access_token = self.COOKIE(key='access_token',
|
value=self.request.locale,
|
||||||
value=access_token,
|
http_only=self.ACCESS_TOKEN_HTTP_ONLY,
|
||||||
http_only=self.ACCESS_TOKEN_HTTP_ONLY,
|
secure=self.LOCALE_SECURE,
|
||||||
secure=self.ACCESS_TOKEN_SECURE,
|
max_age=settings.COOKIES_MAX_AGE if permanent else None))
|
||||||
max_age=settings.COOKIES_MAX_AGE if permanent else None)
|
if hasattr(self.request, 'country_code'):
|
||||||
_refresh_token = self.COOKIE(key='refresh_token',
|
COOKIES.append(self.COOKIE(key='country_code',
|
||||||
value=refresh_token,
|
value=self.request.country_code,
|
||||||
http_only=self.REFRESH_TOKEN_HTTP_ONLY,
|
http_only=self.COUNTRY_CODE_HTTP_ONLY,
|
||||||
secure=self.REFRESH_TOKEN_SECURE,
|
secure=self.COUNTRY_CODE_SECURE,
|
||||||
max_age=settings.COOKIES_MAX_AGE if permanent else None)
|
max_age=settings.COOKIES_MAX_AGE if permanent else None))
|
||||||
COOKIES.extend((_access_token, _refresh_token))
|
if access_token:
|
||||||
|
COOKIES.append(self.COOKIE(key='access_token',
|
||||||
|
value=access_token,
|
||||||
|
http_only=self.ACCESS_TOKEN_HTTP_ONLY,
|
||||||
|
secure=self.ACCESS_TOKEN_SECURE,
|
||||||
|
max_age=settings.COOKIES_MAX_AGE if permanent else None))
|
||||||
|
if refresh_token:
|
||||||
|
COOKIES.append(self.COOKIE(key='refresh_token',
|
||||||
|
value=refresh_token,
|
||||||
|
http_only=self.REFRESH_TOKEN_HTTP_ONLY,
|
||||||
|
secure=self.REFRESH_TOKEN_SECURE,
|
||||||
|
max_age=settings.COOKIES_MAX_AGE if permanent else None))
|
||||||
return COOKIES
|
return COOKIES
|
||||||
|
|
||||||
def _put_cookies_in_response(self, cookies: list, response: Response):
|
def _put_cookies_in_response(self, cookies: list, response: Response):
|
||||||
|
|
@ -77,102 +95,3 @@ class JWTGenericViewMixin(generics.GenericAPIView):
|
||||||
http_only=self.REFRESH_TOKEN_HTTP_ONLY,
|
http_only=self.REFRESH_TOKEN_HTTP_ONLY,
|
||||||
secure=self.REFRESH_TOKEN_SECURE,
|
secure=self.REFRESH_TOKEN_SECURE,
|
||||||
max_age=_cookies.get('max_age'))]
|
max_age=_cookies.get('max_age'))]
|
||||||
|
|
||||||
|
|
||||||
class JWTListAPIView(JWTGenericViewMixin, generics.ListAPIView):
|
|
||||||
"""
|
|
||||||
Concrete view for creating a model instance.
|
|
||||||
"""
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
|
||||||
page = self.paginate_queryset(queryset)
|
|
||||||
if page is not None:
|
|
||||||
serializer = self.get_serializer(page, many=True)
|
|
||||||
response = self.get_paginated_response(serializer.data)
|
|
||||||
else:
|
|
||||||
serializer = self.get_serializer(queryset, many=True)
|
|
||||||
response = Response(serializer.data)
|
|
||||||
access_token, refresh_token = self._get_tokens_from_cookies(request)
|
|
||||||
return self._put_cookies_in_response(
|
|
||||||
cookies=self._put_data_in_cookies(access_token=access_token.value,
|
|
||||||
refresh_token=refresh_token.value),
|
|
||||||
response=response)
|
|
||||||
|
|
||||||
|
|
||||||
class JWTCreateAPIView(JWTGenericViewMixin, generics.CreateAPIView):
|
|
||||||
"""
|
|
||||||
Concrete view for creating a model instance.
|
|
||||||
"""
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
serializer = self.get_serializer(data=request.data)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
serializer.save()
|
|
||||||
response = Response(serializer.data, status=status.HTTP_201_CREATED)
|
|
||||||
access_token, refresh_token = self._get_tokens_from_cookies(request)
|
|
||||||
return self._put_cookies_in_response(
|
|
||||||
cookies=self._put_data_in_cookies(access_token=access_token.value,
|
|
||||||
refresh_token=refresh_token.value),
|
|
||||||
response=response)
|
|
||||||
|
|
||||||
|
|
||||||
class JWTRetrieveAPIView(JWTGenericViewMixin, generics.RetrieveAPIView):
|
|
||||||
"""
|
|
||||||
Concrete view for retrieving a model instance.
|
|
||||||
"""
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
"""Implement GET method"""
|
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
|
||||||
page = self.paginate_queryset(queryset)
|
|
||||||
if page is not None:
|
|
||||||
serializer = self.get_serializer(page, many=True)
|
|
||||||
response = self.get_paginated_response(serializer.data)
|
|
||||||
else:
|
|
||||||
serializer = self.get_serializer(queryset, many=True)
|
|
||||||
response = Response(serializer.data, status.HTTP_200_OK)
|
|
||||||
access_token, refresh_token = self._get_tokens_from_cookies(request)
|
|
||||||
return self._put_cookies_in_response(
|
|
||||||
cookies=self._put_data_in_cookies(access_token=access_token,
|
|
||||||
refresh_token=refresh_token),
|
|
||||||
response=response)
|
|
||||||
|
|
||||||
|
|
||||||
class JWTDestroyAPIView(JWTGenericViewMixin, generics.DestroyAPIView):
|
|
||||||
"""
|
|
||||||
Concrete view for deleting a model instance.
|
|
||||||
"""
|
|
||||||
def delete(self, request, *args, **kwargs):
|
|
||||||
instance = self.get_object()
|
|
||||||
instance.delete()
|
|
||||||
response = Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
access_token, refresh_token = self._get_tokens_from_cookies(request)
|
|
||||||
return self._put_cookies_in_response(
|
|
||||||
cookies=self._put_data_in_cookies(access_token=access_token,
|
|
||||||
refresh_token=refresh_token),
|
|
||||||
response=response)
|
|
||||||
|
|
||||||
|
|
||||||
class JWTUpdateAPIView(JWTGenericViewMixin, generics.UpdateAPIView):
|
|
||||||
"""
|
|
||||||
Concrete view for updating a model instance.
|
|
||||||
"""
|
|
||||||
def put(self, request, *args, **kwargs):
|
|
||||||
partial = kwargs.pop('partial', False)
|
|
||||||
instance = self.get_object()
|
|
||||||
serializer = self.get_serializer(instance, data=request.data, partial=partial)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
serializer.save()
|
|
||||||
if getattr(instance, '_prefetched_objects_cache', None):
|
|
||||||
# If 'prefetch_related' has been applied to a queryset, we need to
|
|
||||||
# forcibly invalidate the prefetch cache on the instance.
|
|
||||||
instance._prefetched_objects_cache = {}
|
|
||||||
response = Response(serializer.data)
|
|
||||||
access_token, refresh_token = self._get_tokens_from_cookies(request)
|
|
||||||
return self._put_cookies_in_response(
|
|
||||||
cookies=self._put_data_in_cookies(access_token=access_token,
|
|
||||||
refresh_token=refresh_token),
|
|
||||||
response=response)
|
|
||||||
|
|
||||||
def patch(self, request, *args, **kwargs):
|
|
||||||
kwargs['partial'] = True
|
|
||||||
return self.put(request, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,9 +68,14 @@ PROJECT_APPS = [
|
||||||
'configuration.apps.ConfigurationConfig',
|
'configuration.apps.ConfigurationConfig',
|
||||||
'timetable.apps.TimetableConfig',
|
'timetable.apps.TimetableConfig',
|
||||||
'review.apps.ReviewConfig',
|
'review.apps.ReviewConfig',
|
||||||
|
'comment.apps.CommentConfig',
|
||||||
|
'favorites.apps.FavoritesConfig',
|
||||||
]
|
]
|
||||||
|
|
||||||
EXTERNAL_APPS = [
|
EXTERNAL_APPS = [
|
||||||
|
'corsheaders',
|
||||||
|
'django_elasticsearch_dsl',
|
||||||
|
'django_elasticsearch_dsl_drf',
|
||||||
'django_filters',
|
'django_filters',
|
||||||
'drf_yasg',
|
'drf_yasg',
|
||||||
'fcm_django',
|
'fcm_django',
|
||||||
|
|
@ -78,7 +83,6 @@ EXTERNAL_APPS = [
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
'rest_framework.authtoken',
|
'rest_framework.authtoken',
|
||||||
'easy_select2',
|
'easy_select2',
|
||||||
'corsheaders',
|
|
||||||
'oauth2_provider',
|
'oauth2_provider',
|
||||||
'social_django',
|
'social_django',
|
||||||
'rest_framework_social_oauth2',
|
'rest_framework_social_oauth2',
|
||||||
|
|
@ -86,12 +90,10 @@ EXTERNAL_APPS = [
|
||||||
'rest_framework_simplejwt.token_blacklist',
|
'rest_framework_simplejwt.token_blacklist',
|
||||||
'solo',
|
'solo',
|
||||||
'phonenumber_field',
|
'phonenumber_field',
|
||||||
'django_elasticsearch_dsl',
|
|
||||||
'django_elasticsearch_dsl_drf',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
INSTALLED_APPS = CONTRIB_APPS + PROJECT_APPS + EXTERNAL_APPS
|
INSTALLED_APPS = CONTRIB_APPS + EXTERNAL_APPS + PROJECT_APPS
|
||||||
|
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
|
@ -334,16 +336,14 @@ THUMBNAIL_ALIASES = {
|
||||||
# Password reset
|
# Password reset
|
||||||
RESETTING_TOKEN_EXPIRATION = 24 # hours
|
RESETTING_TOKEN_EXPIRATION = 24 # hours
|
||||||
|
|
||||||
# CORS Config
|
|
||||||
CORS_ORIGIN_ALLOW_ALL = True
|
|
||||||
CORS_ALLOW_CREDENTIALS = True
|
|
||||||
|
|
||||||
|
|
||||||
GEOIP_PATH = os.path.join(PROJECT_ROOT, 'geoip_db')
|
GEOIP_PATH = os.path.join(PROJECT_ROOT, 'geoip_db')
|
||||||
|
|
||||||
# JWT
|
# JWT
|
||||||
SIMPLE_JWT = {
|
SIMPLE_JWT = {
|
||||||
'ACCESS_TOKEN_LIFETIME': timedelta(hours=6),
|
# Increase access token lifetime b.c. front-end dev's cant send multiple
|
||||||
|
# requests to API in one HTTP request.
|
||||||
|
'ACCESS_TOKEN_LIFETIME': timedelta(days=30),
|
||||||
'ACCESS_TOKEN_LIFETIME_SECONDS': 21600, # 6 hours in seconds
|
'ACCESS_TOKEN_LIFETIME_SECONDS': 21600, # 6 hours in seconds
|
||||||
'REFRESH_TOKEN_LIFETIME': timedelta(days=30),
|
'REFRESH_TOKEN_LIFETIME': timedelta(days=30),
|
||||||
'REFRESH_TOKEN_LIFETIME_SECONDS': 2592000, # 30 days in seconds
|
'REFRESH_TOKEN_LIFETIME_SECONDS': 2592000, # 30 days in seconds
|
||||||
|
|
@ -374,7 +374,6 @@ PASSWORD_RESET_TIMEOUT_DAYS = 1
|
||||||
|
|
||||||
|
|
||||||
# TEMPLATES
|
# TEMPLATES
|
||||||
CONFIRMATION_PASSWORD_RESET_TEMPLATE = 'account/password_reset_confirm.html'
|
|
||||||
RESETTING_TOKEN_TEMPLATE = 'account/password_reset_email.html'
|
RESETTING_TOKEN_TEMPLATE = 'account/password_reset_email.html'
|
||||||
CHANGE_EMAIL_TEMPLATE = 'account/change_email.html'
|
CHANGE_EMAIL_TEMPLATE = 'account/change_email.html'
|
||||||
CONFIRM_EMAIL_TEMPLATE = 'authorization/confirm_email.html'
|
CONFIRM_EMAIL_TEMPLATE = 'authorization/confirm_email.html'
|
||||||
|
|
@ -382,6 +381,12 @@ CONFIRM_EMAIL_TEMPLATE = 'authorization/confirm_email.html'
|
||||||
|
|
||||||
# COOKIES
|
# COOKIES
|
||||||
COOKIES_MAX_AGE = 86400 # 24 hours
|
COOKIES_MAX_AGE = 86400 # 24 hours
|
||||||
|
SESSION_COOKIE_SAMESITE = None
|
||||||
|
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
CORS_ORIGIN_ALLOW_ALL = True
|
||||||
|
CORS_ALLOW_CREDENTIALS = True
|
||||||
|
|
||||||
|
|
||||||
# UPLOAD FILES
|
# UPLOAD FILES
|
||||||
|
|
|
||||||
13
project/settings/stage.py
Normal file
13
project/settings/stage.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
"""Stage settings."""
|
||||||
|
from .base import *
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = ['gm-stage.id-east.ru', '95.213.204.126']
|
||||||
|
|
||||||
|
SEND_SMS = False
|
||||||
|
SMS_CODE_SHOW = True
|
||||||
|
USE_CELERY = False
|
||||||
|
|
||||||
|
SCHEMA_URI = 'https'
|
||||||
|
DEFAULT_SUBDOMAIN = 'www'
|
||||||
|
SITE_DOMAIN_URI = 'id-east.ru'
|
||||||
|
DOMAIN_URI = 'gm-stage.id-east.ru'
|
||||||
|
|
@ -2,9 +2,8 @@
|
||||||
{% blocktrans %}You're receiving this email because you want to change email address at {{ site_name }}.{% endblocktrans %}
|
{% 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:" %}
|
{% trans "Please go to the following page for confirmation new email address:" %}
|
||||||
{% block reset_link %}
|
|
||||||
http://{{ domain_uri }}{% url 'web:account:change-email-confirm' uidb64=uidb64 token=token %}
|
<a href="https://{{ country_code }}.{{ domain_uri }}/registered/{{ uidb64 }}/{{ token }}/">https://{{ country_code }}.{{ domain_uri }}/registered/{{ uidb64 }}/{{ token }}/</a>
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% trans "Thanks for using our site!" %}
|
{% trans "Thanks for using our site!" %}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
{% load i18n static %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
{% if validlink %}
|
|
||||||
|
|
||||||
<p>{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}</p>
|
|
||||||
|
|
||||||
<form method="post">{% csrf_token %}
|
|
||||||
<fieldset class="module aligned">
|
|
||||||
<div class="form-row field-password1">
|
|
||||||
{{ form.new_password1.errors }}
|
|
||||||
<label for="id_new_password1">{% trans 'New password:' %}</label>
|
|
||||||
{{ form.new_password1 }}
|
|
||||||
</div>
|
|
||||||
<div class="form-row field-password2">
|
|
||||||
{{ form.new_password2.errors }}
|
|
||||||
<label for="id_new_password2">{% trans 'Confirm password:' %}</label>
|
|
||||||
{{ form.new_password2 }}
|
|
||||||
</div>
|
|
||||||
<input type="submit" value="{% trans 'Change my password' %}">
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
<p>{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}</p>
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -2,9 +2,8 @@
|
||||||
{% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %}
|
{% 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:" %}
|
{% trans "Please go to the following page and choose a new password:" %}
|
||||||
{% block reset_link %}
|
|
||||||
http://{{ domain_uri }}{% url 'web:account:form-password-reset-confirm' uidb64=uidb64 token=token %}
|
<a href="https://{{ country_code }}.{{ domain_uri }}/recovery/{{ uidb64 }}/{{ token }}/">https://{{ country_code }}.{{ domain_uri }}/recovery/{{ uidb64 }}/{{ token }}/</a>
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% trans "Thanks for using our site!" %}
|
{% trans "Thanks for using our site!" %}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,8 @@
|
||||||
{% blocktrans %}You're receiving this email because you trying to register new account at {{ site_name }}.{% endblocktrans %}
|
{% 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:" %}
|
{% trans "Please confirm your email address to complete the registration:" %}
|
||||||
{% block signup_confirm %}
|
|
||||||
http://{{ domain_uri }}{% url 'auth:signup-confirm' uidb64=uidb64 token=token %}
|
<a href="https://{{ country_code }}.{{ domain_uri }}/registered/{{ uidb64 }}/{{ token }}/">https://{{ country_code }}.{{ domain_uri }}/registered/{{ uidb64 }}/{{ token }}/</a>
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% trans "Thanks for using our site!" %}
|
{% trans "Thanks for using our site!" %}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ urlpatterns = [
|
||||||
# path('account/', include('account.urls.web')),
|
# path('account/', include('account.urls.web')),
|
||||||
# path('advertisement/', include('advertisement.urls.web')),
|
# path('advertisement/', include('advertisement.urls.web')),
|
||||||
# path('collection/', include('collection.urls.web')),
|
# path('collection/', include('collection.urls.web')),
|
||||||
# path('establishments/', include('establishment.urls.web')),
|
path('establishments/', include('establishment.urls.back')),
|
||||||
# path('news/', include('news.urls.web')),
|
# path('news/', include('news.urls.web')),
|
||||||
# path('partner/', include('partner.urls.web')),
|
# path('partner/', include('partner.urls.web')),
|
||||||
]
|
]
|
||||||
|
|
@ -3,10 +3,11 @@ from django.urls import path, include
|
||||||
app_name = 'mobile'
|
app_name = 'mobile'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
path('establishments/', include('establishment.urls.mobile')),
|
||||||
# path('account/', include('account.urls.web')),
|
# path('account/', include('account.urls.web')),
|
||||||
# path('advertisement/', include('advertisement.urls.web')),
|
# path('advertisement/', include('advertisement.urls.web')),
|
||||||
# path('collection/', include('collection.urls.web')),
|
# path('collection/', include('collection.urls.web')),
|
||||||
# path('establishments/', include('establishment.urls.web')),
|
# path('establishments/', include('establishment.urls.web')),
|
||||||
# path('news/', include('news.urls.web')),
|
# path('news/', include('news.urls.web')),
|
||||||
# path('partner/', include('partner.urls.web')),
|
# path('partner/', include('partner.urls.web')),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -28,4 +28,6 @@ urlpatterns = [
|
||||||
path('location/', include('location.urls')),
|
path('location/', include('location.urls')),
|
||||||
path('main/', include('main.urls')),
|
path('main/', include('main.urls')),
|
||||||
path('translation/', include('translation.urls')),
|
path('translation/', include('translation.urls')),
|
||||||
|
path('comments/', include('comment.urls.web')),
|
||||||
|
path('favorites/', include('favorites.urls')),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user