Merge branch 'develop' into feature/elasticsearch

This commit is contained in:
evgeniy-st 2019-09-16 10:58:09 +03:00
commit 8e864d32b9
94 changed files with 2079 additions and 1115 deletions

3
.gitignore vendored
View File

@ -19,3 +19,6 @@ logs/
/datadir/
/_files/
/geoip_db/
# dev
./docker-compose.override.yml

View File

@ -11,14 +11,17 @@ class UserAdmin(BaseUserAdmin):
"""User model admin settings."""
list_display = ('id', 'username', 'short_name', 'date_joined', 'is_active',
'is_staff', 'is_superuser',)
list_filter = ('is_active', 'is_staff', 'is_superuser', 'groups',)
'is_staff', 'is_superuser', 'email_confirmed')
list_filter = ('is_active', 'is_staff', 'is_superuser', 'email_confirmed',
'groups',)
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 = (
(None, {'fields': ('email', 'password',)}),
(_('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'), {
'fields': (
'newsletter',
@ -46,11 +49,14 @@ class UserAdmin(BaseUserAdmin):
short_name.short_description = _('Name')
def image_preview(self, obj):
"""Get user image preview"""
return obj.image_tag
@admin.register(models.ResetPasswordToken)
class ResetPasswordToken(admin.ModelAdmin):
"""Model admin for ResetPasswordToken"""
list_display = ('id', 'user', 'expiry_datetime')
list_filter = ('expiry_datetime', 'user')
search_fields = ('user', )
readonly_fields = ('user', 'key', )
image_preview.short_description = 'Image preview'
def cropped_image_preview(self, obj):
"""Get user cropped image preview"""
return obj.cropped_image_tag
cropped_image_preview.short_description = 'Cropped image preview'

View 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'),
),
]

View 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',
),
]

View 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',
),
]

View 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'),
),
]

View File

@ -1,5 +1,4 @@
"""Account models"""
from django.conf import settings
from django.contrib.auth.models import AbstractUser, UserManager as BaseUserManager
from django.contrib.auth.tokens import default_token_generator as password_token_generator
@ -8,6 +7,7 @@ from django.db import models
from django.template.loader import render_to_string
from django.utils import timezone
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.translation import ugettext_lazy as _
from rest_framework.authtoken.models import Token
@ -23,15 +23,12 @@ class UserManager(BaseUserManager):
use_in_migrations = False
def make(self, username: str, email: str, password: str,
newsletter: bool, is_active: bool = False) -> object:
def make(self, username: str, email: str, password: str, newsletter: bool) -> object:
"""Register new user"""
obj = self.model(
username=username,
email=email,
newsletter=newsletter,
is_active=is_active
)
newsletter=newsletter)
obj.set_password(password)
obj.save()
return obj
@ -45,18 +42,22 @@ class UserQuerySet(models.QuerySet):
return self.filter(is_active=switcher)
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,
oauth2_provider_accesstoken__expires__gt=timezone.now())
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,
oauth2_provider_refreshtoken__expires__gt=timezone.now())
class User(ImageMixin, AbstractUser):
class User(AbstractUser):
"""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,
null=True, default=None)
email_confirmed = models.BooleanField(_('email status'), default=False)
@ -93,9 +94,9 @@ class User(ImageMixin, AbstractUser):
self.is_active = switcher
self.save()
def create_jwt_tokens(self, source: int):
def create_jwt_tokens(self, source: int = None):
"""Create JWT tokens for user"""
token = GMRefreshToken.for_user_by_source(self, source)
token = GMRefreshToken.for_user(self, source)
return {
'access_token': str(token.access_token),
'refresh_token': str(token),
@ -146,7 +147,7 @@ class User(ImageMixin, AbstractUser):
@property
def reset_password_token(self):
"""Make a token for finish signup."""
return GMTokenGenerator(purpose=GMTokenGenerator.RESET_PASSWORD).make_token(self)
return password_token_generator.make_token(self)
@property
def get_user_uidb64(self):
@ -154,112 +155,43 @@ class User(ImageMixin, AbstractUser):
return urlsafe_base64_encode(force_bytes(self.pk))
@property
def confirm_email_template(self):
"""Get confirm email template"""
return render_to_string(
template_name=settings.CONFIRM_EMAIL_TEMPLATE,
context={'token': self.confirm_email_token,
'uidb64': self.get_user_uidb64,
'domain_uri': settings.DOMAIN_URI,
'site_name': settings.SITE_NAME})
def base_template(self):
"""Base email template"""
return {'domain_uri': settings.DOMAIN_URI,
'uidb64': self.get_user_uidb64,
'site_name': settings.SITE_NAME}
@property
def change_email_template(self):
"""Get change email template"""
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)
def image_tag(self):
return mark_safe(f'<img src="{self.image_url}" />')
@property
def get_resetting_token_expiration(self):
"""Get resetting token expiration"""
return settings.RESETTING_TOKEN_EXPIRATION
def cropped_image_tag(self):
return mark_safe(f'<img src="{self.cropped_image_url}" />')
@property
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):
def reset_password_template(self, country_code):
"""Get reset password template"""
context = {'token': self.reset_password_token,
'country_code': country_code}
context.update(self.base_template)
return render_to_string(
template_name=settings.RESETTING_TOKEN_TEMPLATE,
context={'token': self.key,
'uidb64': self.user.get_user_uidb64,
'domain_uri': settings.DOMAIN_URI,
'site_name': settings.SITE_NAME})
context=context)
@staticmethod
def token_is_valid(user, token):
"""Check if token is valid"""
return password_token_generator.check_token(user, token)
def confirm_email_template(self, country_code):
"""Get confirm email template"""
context = {'token': self.confirm_email_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):
"""Overdue instance"""
self.expiry_datetime = timezone.now()
self.save()
def change_email_template(self, country_code):
"""Get change email template"""
context = {'token': self.change_email_token,
'country_code': country_code}
context.update(self.base_template)
return render_to_string(
template_name=settings.CHANGE_EMAIL_TEMPLATE,
context=context)

View File

@ -1,20 +1,179 @@
"""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 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
class UserSerializer(serializers.ModelSerializer):
"""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:
model = models.User
fields = [
'username',
'first_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
class FCMDeviceSerializer(serializers.ModelSerializer):

View File

@ -1,237 +1,75 @@
"""Serializers for account web"""
from django.conf import settings
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 rest_framework import serializers
from account import models, tasks
from authorization.models import JWTRefreshToken
from account import models
from utils import exceptions as utils_exceptions
from utils.serializers import SourceSerializerMixin
from utils.tokens import GMRefreshToken
from utils.methods import username_validator
class PasswordResetSerializer(serializers.ModelSerializer):
class PasswordResetSerializer(serializers.Serializer):
"""Serializer from model PasswordReset"""
username_or_email = serializers.CharField(required=False,
write_only=True,)
class Meta:
"""Meta class"""
model = models.ResetPasswordToken
fields = (
'username_or_email',
)
@property
def request(self):
"""Get request from context"""
return self.context.get('request')
def validate(self, attrs):
"""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:
username_or_email = attrs.get('username_or_email')
if not user.is_authenticated:
if not username_or_email:
raise serializers.ValidationError(_('Username or Email not requested'))
# Check user in DB
user_qs = models.User.objects.filter(Q(email=username_or_email) |
Q(username=username_or_email))
if user_qs.exists():
attrs['user'] = user_qs.first()
raise serializers.ValidationError(_('username or email not in request body.'))
filters = {}
if username_validator(username_or_email):
filters.update({'username__icontains': username_or_email})
else:
raise utils_exceptions.UserNotFoundError()
else:
attrs['user'] = user
filters.update({'email__icontains': username_or_email})
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
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):
"""Serializer for model User"""
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:
"""Meta class"""
model = models.User
fields = ('password', )
def validate(self, attrs):
"""Override validate method"""
password = attrs.get('password')
def validate_password(self, value):
"""Password validation method."""
try:
# 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()
# Validate password
password_validators.validate_password(password=password)
password_validators.validate_password(password=value)
except serializers.ValidationError as e:
raise serializers.ValidationError(str(e))
else:
return attrs
return value
def update(self, instance, validated_data):
"""Override update method"""
# Update user password from instance
instance.set_password(validated_data.get('password'))
instance.save()
# Expire tokens
instance.expire_access_tokens()
instance.expire_refresh_tokens()
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

View File

@ -11,26 +11,37 @@ logger = logging.getLogger(__name__)
@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."""
try:
obj = models.ResetPasswordToken.objects.get(id=request_id)
user = obj.user
user = models.User.objects.get(id=user_id)
user.send_email(subject=_('Password resetting'),
message=obj.reset_password_template)
message=user.reset_password_template(country_code))
except:
logger.error(f'METHOD_NAME: {send_reset_password_email.__name__}\n'
f'DETAIL: Exception occurred for ResetPasswordToken instance: '
f'{request_id}')
f'DETAIL: Exception occurred for reset password: '
f'{user_id}')
@shared_task
def confirm_new_email_address(user_id):
def confirm_new_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)
message=user.confirm_email_template(country_code))
except:
logger.error(f'METHOD_NAME: {confirm_new_email_address.__name__}\n'
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}')

View File

@ -6,5 +6,7 @@ from account.views import common as views
app_name = 'account'
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'),
]

View File

@ -7,19 +7,9 @@ from account.views import web as views
app_name = 'account'
urlpatterns_api = [
path('change-password/', views.ChangePasswordView.as_view(), name='change-password'),
path('reset-password/', views.PasswordResetView.as_view(), name='password-reset'),
path('form/reset-password/<uidb64>/<token>/', views.FormPasswordResetConfirmView.as_view(),
name='form-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'),
path('reset-password/confirm/<uidb64>/<token>/', views.PasswordResetConfirmView.as_view(),
name='password-reset-confirm'),
]
urlpatterns = urlpatterns_api + \

View File

@ -1,17 +1,22 @@
"""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 rest_framework import generics, status
from rest_framework import generics
from rest_framework import permissions
from utils.permissions import IsAuthenticatedAndTokenIsValid
from rest_framework import status
from rest_framework.response import Response
from account import models
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
class UserView(generics.RetrieveUpdateAPIView):
"""### User update view."""
class UserRetrieveUpdateView(generics.RetrieveUpdateAPIView):
"""User update view."""
serializer_class = serializers.UserSerializer
queryset = models.User.objects.active()
@ -19,6 +24,65 @@ class UserView(generics.RetrieveUpdateAPIView):
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
class FCMDeviceViewSet(generics.GenericAPIView):
"""FCMDevice registration view.

View File

@ -1,49 +1,43 @@
"""Web account views"""
from django.conf import settings
from django.contrib.auth.tokens import default_token_generator
from django.core.exceptions import ValidationError
from django.http import HttpResponseRedirect
from django.contrib.auth.tokens import default_token_generator as password_token_generator
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.http import urlsafe_base64_decode
from django.utils.translation import gettext_lazy as _
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 import permissions, status, generics
from rest_framework.response import Response
from account import models
from account.forms import SetPasswordForm
from account import tasks, models
from account.serializers import web as serializers
from utils import exceptions as utils_exceptions
from utils.models import GMTokenGenerator
from utils.views import (JWTCreateAPIView,
JWTUpdateAPIView,
JWTGenericViewMixin)
from utils.views import JWTGenericViewMixin
class PasswordResetView(JWTCreateAPIView):
class PasswordResetView(generics.GenericAPIView):
"""View for resetting user password"""
permission_classes = (permissions.AllowAny, )
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):
"""View for confirmation new password"""
serializer_class = serializers.PasswordResetConfirmSerializer
permission_classes = (permissions.AllowAny,)
def get_queryset(self):
"""Override get_queryset method"""
return models.ResetPasswordToken.objects.valid()
queryset = models.User.objects.active()
def get_object(self):
"""Override get_object method
@ -54,230 +48,27 @@ class PasswordResetConfirmView(JWTGenericViewMixin):
user_id = force_text(urlsafe_base64_decode(uidb64))
token = self.kwargs.get('token')
filter_kwargs = {'key': token, 'user_id': user_id}
obj = get_object_or_404(queryset, **filter_kwargs)
obj = get_object_or_404(queryset, id=user_id)
if not GMTokenGenerator(GMTokenGenerator.RESET_PASSWORD).check_token(
user=obj.user, token=token):
raise utils_exceptions.NotValidAccessTokenError()
if not password_token_generator.check_token(user=obj, token=token):
raise utils_exceptions.NotValidTokenError()
# May raise a permission denied
self.check_object_permissions(self.request, obj)
return obj
def put(self, request, *args, **kwargs):
"""Implement PUT method"""
def patch(self, request, *args, **kwargs):
"""Implement PATCH method"""
instance = self.get_object()
serializer = self.get_serializer(instance=instance,
data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(status=status.HTTP_200_OK)
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')
# Create tokens
tokens = instance.create_jwt_tokens()
return self._put_cookies_in_response(
cookies=self._put_data_in_cookies(access_token=access_token,
refresh_token=refresh_token),
response=response)
# 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
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))

View File

@ -1,2 +1,18 @@
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
# 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)

View File

@ -38,8 +38,8 @@ class Application(PlatformMixin, AbstractApplication):
class JWTAccessTokenManager(models.Manager):
"""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"""
refresh_token_qs = JWTRefreshToken.objects.filter(user=user,
jti=refresh_token.payload.get('jti'))
@ -106,18 +106,17 @@ class JWTAccessToken(ProjectBaseMixin):
class JWTRefreshTokenManager(models.Manager):
"""Manager for model RefreshToken."""
def add_to_db(self, user, token: RefreshToken, source: int):
"""Added generated refresh token to db"""
def make(self, user, token: RefreshToken, source: int):
"""Make method"""
jti = token[settings.SIMPLE_JWT.get('JTI_CLAIM')]
exp = token['exp']
obj = self.model(
user=user,
jti=jti,
source=source,
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()
return obj

View File

@ -8,23 +8,20 @@ from rest_framework import validators as rest_validators
from account import models as account_models
from authorization import tasks
from authorization.models import JWTRefreshToken
from utils import exceptions as utils_exceptions
from utils import methods as utils_methods
from utils.serializers import SourceSerializerMixin
from utils.tokens import GMRefreshToken
# Serializers
class SignupSerializer(serializers.ModelSerializer):
"""Signup serializer serializer mixin"""
# REQUEST
username = serializers.CharField(
validators=(rest_validators.UniqueValidator(queryset=account_models.User.objects.all()),),
write_only=True
)
username = serializers.CharField(write_only=True)
password = serializers.CharField(write_only=True)
email = serializers.EmailField(
validators=(rest_validators.UniqueValidator(queryset=account_models.User.objects.all()),),
write_only=True)
email = serializers.EmailField(write_only=True)
newsletter = serializers.BooleanField(write_only=True)
class Meta:
@ -36,34 +33,46 @@ class SignupSerializer(serializers.ModelSerializer):
'newsletter'
)
def validate_username(self, data):
def validate_username(self, value):
"""Custom username validation"""
valid = utils_methods.username_validator(username=data)
valid = utils_methods.username_validator(username=value)
if not valid:
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"""
try:
password_validators.validate_password(password=data)
password_validators.validate_password(password=value)
except serializers.ValidationError as e:
raise serializers.ValidationError(str(e))
else:
return data
return value
def create(self, validated_data):
"""Override create method"""
obj = account_models.User.objects.make(
username=validated_data.get('username'),
password=validated_data.get('password'),
email=validated_data.get('email'),
email=validated_data.get('email').lower(),
newsletter=validated_data.get('newsletter'))
# Send verification link on user email
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:
tasks.send_confirm_email(obj.id)
tasks.send_confirm_email(
user_id=obj.id,
country_code=self.context.get('request').country_code)
return obj
@ -98,8 +107,7 @@ class LoginByUsernameOrEmailSerializer(SourceSerializerMixin,
username_or_email = attrs.pop('username_or_email')
password = attrs.pop('password')
user_qs = account_models.User.objects.filter(Q(username=username_or_email) |
(Q(email=username_or_email) &
Q(email_confirmed=True)))
(Q(email=username_or_email)))
if not user_qs.exists():
raise utils_exceptions.UserNotFoundError()
else:
@ -107,7 +115,7 @@ class LoginByUsernameOrEmailSerializer(SourceSerializerMixin,
authentication = authenticate(username=user.get_username(),
password=password)
if not authentication:
raise utils_exceptions.WrongAuthCredentials()
raise utils_exceptions.UserNotFoundError()
self.instance = user
return attrs
@ -123,6 +131,40 @@ class LogoutSerializer(SourceSerializerMixin):
"""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
class OAuth2Serialzier(SourceSerializerMixin):
"""Serializer OAuth2 authorization"""

View File

@ -10,12 +10,12 @@ logger = logging.getLogger(__name__)
@shared_task
def send_confirm_email(user_id):
def send_confirm_email(user_id, country_code):
"""Send verification email to user."""
try:
obj = account_models.User.objects.get(id=user_id)
obj.send_email(subject=_('Email confirmation'),
message=obj.confirm_email_template)
message=obj.confirm_email_template(country_code))
except:
logger.error(f'METHOD_NAME: {send_confirm_email.__name__}\n'
f'DETAIL: Exception occurred for user: {user_id}')

View File

@ -29,10 +29,11 @@ urlpatterns_oauth2 = [
urlpatterns_jwt = [
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'),
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'),
]

View File

@ -24,13 +24,12 @@ from authorization.serializers import common as serializers
from utils import exceptions as utils_exceptions
from utils.models import GMTokenGenerator
from utils.permissions import IsAuthenticatedAndTokenIsValid
from utils.views import (JWTGenericViewMixin,
JWTCreateAPIView)
from utils.views import JWTGenericViewMixin
# Mixins
# JWTAuthView mixin
class JWTAuthViewMixin(JWTCreateAPIView):
class JWTAuthViewMixin(JWTGenericViewMixin):
"""Mixin for authentication views"""
def post(self, request, *args, **kwargs):
@ -151,7 +150,7 @@ class OAuth2SignUpView(OAuth2ViewMixin, JWTGenericViewMixin):
# JWT
# Sign in via username and password
class SignUpView(JWTCreateAPIView):
class SignUpView(generics.GenericAPIView):
"""View for classic signup"""
permission_classes = (permissions.AllowAny, )
serializer_class = serializers.SignupSerializer
@ -164,7 +163,7 @@ class SignUpView(JWTCreateAPIView):
return Response(status=status.HTTP_201_CREATED)
class VerifyEmailConfirmView(JWTGenericViewMixin):
class ConfirmationEmailView(JWTGenericViewMixin):
"""View for confirmation email"""
permission_classes = (permissions.AllowAny, )
@ -182,9 +181,15 @@ class VerifyEmailConfirmView(JWTGenericViewMixin):
raise utils_exceptions.NotValidTokenError()
# Approve email status
user.confirm_email()
# Set user status as active
user.approve()
return Response(status=status.HTTP_200_OK)
response = 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:
raise utils_exceptions.UserNotFoundError()
@ -226,3 +231,23 @@ class LogoutView(JWTGenericViewMixin):
access_token_obj.refresh_token.expire()
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
View File

8
apps/comment/admin.py Normal file
View 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
View 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')

View 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',
},
),
]

View File

52
apps/comment/models.py Normal file
View 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)

View File

View 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'
]

View File

View File

@ -0,0 +1 @@
"""Serializers for app comment."""

1
apps/comment/tests.py Normal file
View File

@ -0,0 +1 @@
# Create your tests here.

View File

View File

@ -0,0 +1,5 @@
"""Comment urlpaths."""
app_name = 'comment'
urlpatterns = []

View 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
View 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

View File

View File

@ -0,0 +1 @@
"""Views for app comment."""

View File

View File

View File

@ -1,10 +1,12 @@
"""Establishment admin conf."""
from django.contrib import admin
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 main.models import Award, MetaDataContent
from review import models as review_models
from django.utils.translation import gettext_lazy as _
@admin.register(models.EstablishmentType)
@ -44,13 +46,18 @@ class ReviewInline(GenericTabularInline):
extra = 0
class CommentInline(GenericTabularInline):
model = Comment
extra = 0
@admin.register(models.Establishment)
class EstablishmentAdmin(admin.ModelAdmin):
"""Establishment admin."""
inlines = [
AwardInline, MetaDataContentInline,
ContactPhoneInline, ContactEmailInline,
ReviewInline]
ReviewInline, CommentInline]
@admin.register(models.EstablishmentSchedule)
@ -58,11 +65,6 @@ class EstablishmentSchedule(admin.ModelAdmin):
"""Establishment schedule"""
@admin.register(models.Comment)
class EstablishmentComment(admin.ModelAdmin):
"""Establishment comments."""
@admin.register(models.Position)
class PositionAdmin(admin.ModelAdmin):
"""Position admin."""

View File

@ -5,7 +5,7 @@ from establishment import models
class EstablishmentFilter(filters.FilterSet):
"""Establishment filterset."""
"""Establishment filter set."""
tag_id = filters.NumberFilter(field_name='tags__metadata__id',)
award_id = filters.NumberFilter(field_name='awards__id',)

View 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',
),
]

View File

@ -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',
),
]

View 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'),
),
]

View 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',
},
),
]

View File

@ -1,11 +1,13 @@
"""Establishment models."""
from functools import reduce
from django.contrib.contenttypes import fields as generic
from django.core.exceptions import ValidationError
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField
from location.models import Address
from utils.models import (ProjectBaseMixin, ImageMixin, TJSONField,
TranslatedFieldsMixin, BaseAttributes)
@ -86,36 +88,61 @@ class EstablishmentQuerySet(models.QuerySet):
'position'),
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):
"""Establishment model."""
STR_FIELD_NAME = 'name'
name = TJSONField(blank=True, null=True, default=None,
verbose_name=_('Name'), help_text='{"en-GB":"some text"}')
name = models.CharField(_('name'), max_length=255, default='')
description = TJSONField(blank=True, null=True, default=None,
verbose_name=_('Description'),
verbose_name=_('description'),
help_text='{"en-GB":"some text"}')
public_mark = models.PositiveIntegerField(blank=True, null=True,
default=None,
verbose_name=_('Public mark'),)
verbose_name=_('public mark'),)
toque_number = models.PositiveIntegerField(blank=True, null=True,
default=None,
verbose_name=_('Toque number'),)
verbose_name=_('toque number'),)
establishment_type = models.ForeignKey(EstablishmentType,
related_name='establishment',
on_delete=models.PROTECT,
verbose_name=_('Type'))
verbose_name=_('type'))
establishment_subtypes = models.ManyToManyField(EstablishmentSubType,
related_name='subtype_establishment',
verbose_name=_('Subtype'))
verbose_name=_('subtype'))
address = models.ForeignKey(Address, blank=True, null=True, default=None,
on_delete=models.PROTECT,
verbose_name=_('Address'))
verbose_name=_('address'))
price_level = models.PositiveIntegerField(blank=True, null=True,
default=None,
verbose_name=_('Price level'))
verbose_name=_('price level'))
website = models.URLField(blank=True, null=True, default=None,
verbose_name=_('Web site URL'))
facebook = models.URLField(blank=True, null=True, default=None,
@ -129,6 +156,7 @@ class Establishment(ProjectBaseMixin, ImageMixin, TranslatedFieldsMixin):
awards = generic.GenericRelation(to='main.Award')
tags = generic.GenericRelation(to='main.MetaDataContent')
reviews = generic.GenericRelation(to='review.Review')
comments = generic.GenericRelation(to='comment.Comment')
objects = EstablishmentQuerySet.as_manager()
@ -138,6 +166,9 @@ class Establishment(ProjectBaseMixin, ImageMixin, TranslatedFieldsMixin):
verbose_name = _('Establishment')
verbose_name_plural = _('Establishments')
def __str__(self):
return f'id:{self.id}-{self.name}'
# todo: recalculate toque_number
def recalculate_toque_number(self):
self.toque_number = 4
@ -314,6 +345,7 @@ class ContactEmail(models.Model):
class Plate(TranslatedFieldsMixin, models.Model):
"""Plate model."""
STR_FIELD_NAME = 'name'
name = TJSONField(
blank=True, null=True, default=None, verbose_name=_('name'),
@ -334,12 +366,12 @@ class Plate(TranslatedFieldsMixin, models.Model):
verbose_name = _('plate')
verbose_name_plural = _('plates')
def __str__(self):
return f'plate_id:{self.id}'
class Menu(TranslatedFieldsMixin, BaseAttributes):
"""Menu model."""
STR_FIELD_NAME = 'category'
category = TJSONField(
blank=True, null=True, default=None, verbose_name=_('category'),
help_text='{"en-GB":"some text"}')
@ -352,36 +384,16 @@ class Menu(TranslatedFieldsMixin, BaseAttributes):
verbose_name_plural = _('menu')
class CommentQuerySet(models.QuerySet):
"""QuerySets for Comment model."""
def by_author(self, author):
"""Return comments by author"""
return self.filter(author=author)
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 SocialNetwork(models.Model):
establishment = models.ForeignKey(
'Establishment', verbose_name=_('establishment'),
related_name='socials', on_delete=models.CASCADE)
title = models.CharField(_('title'), max_length=255)
url = models.URLField(_('URL'))
class Meta:
"""Meta class"""
verbose_name = _('Comment')
verbose_name_plural = _('Comments')
verbose_name = _('social network')
verbose_name_plural = _('social networks')
def __str__(self):
"""String representation"""
return str(self.author)
return self.title

View File

@ -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')

View File

@ -0,0 +1,3 @@
from establishment.serializers.common import *
from establishment.serializers.web import *
from establishment.serializers.back import *

View 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'
]

View 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',
]

View File

View 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'),
]

View File

@ -1,12 +1,19 @@
"""Establishment url patterns."""
from django.urls import include, path
from establishment import views
from django.urls import path
from establishment import views
app_name = 'establishment'
urlpatterns = [
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>/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')
]

View 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)

View File

@ -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()

View File

@ -0,0 +1,3 @@
from establishment.views.common import *
from establishment.views.web import *
from establishment.views.back import *

View 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()

View 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()

View 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')

View File

11
apps/favorites/admin.py Normal file
View 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
View 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')

View 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',
},
),
]

View File

37
apps/favorites/models.py Normal file
View 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}'

View 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
View File

@ -0,0 +1 @@
# Create your tests here.

12
apps/favorites/urls.py Normal file
View 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
View 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"""

View File

@ -10,8 +10,8 @@ class ImageSerializer(serializers.ModelSerializer):
write_only=True)
# RESPONSE
url = serializers.SerializerMethodField()
url = serializers.ImageField(source='image',
read_only=True)
class Meta:
"""Meta class"""
@ -22,6 +22,3 @@ class ImageSerializer(serializers.ModelSerializer):
'url'
)
def get_url(self, obj):
"""Get absolute URL path"""
return obj.get_full_image_url(request=self.context.get('request'))

View File

@ -6,7 +6,7 @@ from . import models, serializers
class ImageUploadView(generics.CreateAPIView):
"""Upload image to gallery"""
permission_classes = (IsAuthenticatedAndTokenIsValid, )
model = models.Image
queryset = models.Image.objects.all()
serializer_class = serializers.ImageSerializer
permission_classes = (IsAuthenticatedAndTokenIsValid, )

View File

@ -46,3 +46,6 @@ class CurrencContentAdmin(admin.ModelAdmin):
"""CurrencContent admin"""
@admin.register(models.Carousel)
class CarouselAdmin(admin.ModelAdmin):
"""Carousel admin."""

View 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',
},
),
]

View File

@ -7,7 +7,9 @@ from django.utils.translation import gettext_lazy as _
from django.contrib.contenttypes.models import ContentType
from location.models import Country
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
#
@ -253,6 +255,10 @@ class MetaData(TranslatedFieldsMixin, models.Model):
return f'id:{self.id}-{label}'
class MetaDataContentQuerySet(ContentTypeQuerySetMixin):
"""QuerySets for MetaDataContent model."""
class MetaDataContent(models.Model):
"""MetaDataContent model."""
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')
metadata = models.ForeignKey(MetaData, on_delete=models.CASCADE)
objects = MetaDataContentQuerySet.as_manager()
class Currency(models.Model):
"""Currency model."""
@ -271,3 +279,56 @@ class Currency(models.Model):
def __str__(self):
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__

View File

@ -1,5 +1,6 @@
"""Main app serializers."""
from rest_framework import serializers
from location.serializers import CountrySerializer
from main import models
@ -82,8 +83,8 @@ class SiteSerializer(serializers.ModelSerializer):
# )
class AwardSerializer(serializers.ModelSerializer):
"""Award serializer."""
class AwardBaseSerializer(serializers.ModelSerializer):
"""Award base serializer."""
title_translated = serializers.CharField(read_only=True, allow_null=True)
@ -92,11 +93,18 @@ class AwardSerializer(serializers.ModelSerializer):
fields = [
'id',
'title_translated',
'award_type',
'vintage_year',
]
class AwardSerializer(AwardBaseSerializer):
"""Award serializer."""
class Meta:
model = models.Award
fields = AwardBaseSerializer.Meta.fields + ['award_type', ]
class MetaDataContentSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(source='metadata.id', read_only=True,)
label_translated = serializers.CharField(
@ -117,4 +125,27 @@ class CurrencySerializer(serializers.ModelSerializer):
fields = [
'id',
'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',
]

View File

@ -10,4 +10,5 @@ urlpatterns = [
path('site-settings/<subdomain>/', views.SiteSettingsView.as_view(), name='site-settings'),
path('awards/', views.AwardView.as_view(), name='awards_list'),
path('awards/<int:pk>/', views.AwardRetrieveView.as_view(), name='awards_retrieve'),
path('carousel/', views.CarouselListView.as_view(), name='carousel-list'),
]

View File

@ -84,3 +84,11 @@ class AwardRetrieveView(generics.RetrieveAPIView):
serializer_class = serializers.AwardSerializer
queryset = models.Award.objects.all()
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

View File

@ -1,8 +1,9 @@
"""News app common app."""
from rest_framework import generics, permissions
from news import filters, models
from news.serializers import common as serializers
from utils.views import JWTGenericViewMixin, JWTListAPIView
from utils.views import JWTGenericViewMixin
class NewsMixin:
@ -18,7 +19,7 @@ class NewsMixin:
.order_by('-is_highlighted', '-created')
class NewsListView(NewsMixin, JWTListAPIView):
class NewsListView(NewsMixin, generics.ListAPIView):
"""News list view."""
filter_class = filters.NewsListFilterSet

View File

@ -19,6 +19,10 @@ class ReviewQuerySet(models.QuerySet):
"""Return reviews by year"""
return self.filter(vintage=year)
def by_status(self, status):
"""Filter by status"""
return self.filter(status=status)
class Review(BaseAttributes, TranslatedFieldsMixin):
"""Review model"""

View File

@ -32,15 +32,6 @@ class UserNotFoundError(AuthErrorMixin, ProjectBaseException):
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):
"""The exception should be thrown when unable to send an email"""
status_code = status.HTTP_400_BAD_REQUEST
@ -120,8 +111,34 @@ class EmailConfirmedError(exceptions.APIException):
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):
"""
The exception should be raised when credentials is not valid for this user
"""
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.')

View File

@ -4,6 +4,7 @@ import re
import string
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.http.request import HttpRequest
from django.utils.timezone import datetime
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):
"""Generate string code."""
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()

View File

@ -32,3 +32,13 @@ def parse_cookies(get_response):
return response
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
View 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))

View File

@ -33,12 +33,11 @@ class GMBlacklistMixin(BlacklistMixin):
"""
@classmethod
def for_user_by_source(cls, user, source: int):
def for_user(cls, user, source: int = None):
"""Create a refresh token."""
token = super().for_user(user)
token['user'] = user.get_user_info()
# Create a record in DB
JWTRefreshToken.objects.add_to_db(user=user, token=token, source=source)
JWTRefreshToken.objects.make(user=user, token=token, source=source)
return token
@ -70,7 +69,7 @@ class GMRefreshToken(GMBlacklistMixin, GMToken, RefreshToken):
# Create a record in DB
user = User.objects.get(id=self.payload.get('user_id'))
JWTAccessToken.objects.add_to_db(user=user,
access_token=access_token,
refresh_token=self)
JWTAccessToken.objects.make(user=user,
access_token=access_token,
refresh_token=self)
return access_token

View File

@ -7,7 +7,7 @@ from rest_framework.response import Response
# JWT
# Login base view mixin
# Login base view mixins
class JWTGenericViewMixin(generics.GenericAPIView):
"""JWT view mixin"""
@ -16,6 +16,13 @@ class JWTGenericViewMixin(generics.GenericAPIView):
REFRESH_TOKEN_HTTP_ONLY = 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'])
def _put_data_in_cookies(self,
@ -26,21 +33,32 @@ class JWTGenericViewMixin(generics.GenericAPIView):
cookies it is list that contain namedtuples
cookies would contain key, value and secure parameters.
"""
COOKIES = list()
COOKIES = []
# Write to cookie access and refresh token with secure flag
if access_token and refresh_token:
_access_token = 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)
_refresh_token = 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)
COOKIES.extend((_access_token, _refresh_token))
if hasattr(self.request, 'locale'):
COOKIES.append(self.COOKIE(key='locale',
value=self.request.locale,
http_only=self.ACCESS_TOKEN_HTTP_ONLY,
secure=self.LOCALE_SECURE,
max_age=settings.COOKIES_MAX_AGE if permanent else None))
if hasattr(self.request, 'country_code'):
COOKIES.append(self.COOKIE(key='country_code',
value=self.request.country_code,
http_only=self.COUNTRY_CODE_HTTP_ONLY,
secure=self.COUNTRY_CODE_SECURE,
max_age=settings.COOKIES_MAX_AGE if permanent else None))
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
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,
secure=self.REFRESH_TOKEN_SECURE,
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)

View File

@ -68,9 +68,14 @@ PROJECT_APPS = [
'configuration.apps.ConfigurationConfig',
'timetable.apps.TimetableConfig',
'review.apps.ReviewConfig',
'comment.apps.CommentConfig',
'favorites.apps.FavoritesConfig',
]
EXTERNAL_APPS = [
'corsheaders',
'django_elasticsearch_dsl',
'django_elasticsearch_dsl_drf',
'django_filters',
'drf_yasg',
'fcm_django',
@ -78,7 +83,6 @@ EXTERNAL_APPS = [
'rest_framework',
'rest_framework.authtoken',
'easy_select2',
'corsheaders',
'oauth2_provider',
'social_django',
'rest_framework_social_oauth2',
@ -86,12 +90,10 @@ EXTERNAL_APPS = [
'rest_framework_simplejwt.token_blacklist',
'solo',
'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 = [
@ -334,16 +336,14 @@ THUMBNAIL_ALIASES = {
# Password reset
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')
# 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
'REFRESH_TOKEN_LIFETIME': timedelta(days=30),
'REFRESH_TOKEN_LIFETIME_SECONDS': 2592000, # 30 days in seconds
@ -374,7 +374,6 @@ PASSWORD_RESET_TIMEOUT_DAYS = 1
# TEMPLATES
CONFIRMATION_PASSWORD_RESET_TEMPLATE = 'account/password_reset_confirm.html'
RESETTING_TOKEN_TEMPLATE = 'account/password_reset_email.html'
CHANGE_EMAIL_TEMPLATE = 'account/change_email.html'
CONFIRM_EMAIL_TEMPLATE = 'authorization/confirm_email.html'
@ -382,6 +381,12 @@ CONFIRM_EMAIL_TEMPLATE = 'authorization/confirm_email.html'
# COOKIES
COOKIES_MAX_AGE = 86400 # 24 hours
SESSION_COOKIE_SAMESITE = None
# CORS
CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_CREDENTIALS = True
# UPLOAD FILES

13
project/settings/stage.py Normal file
View 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'

View File

@ -2,9 +2,8 @@
{% 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:" %}
{% block reset_link %}
http://{{ domain_uri }}{% url 'web:account:change-email-confirm' uidb64=uidb64 token=token %}
{% endblock %}
<a href="https://{{ country_code }}.{{ domain_uri }}/registered/{{ uidb64 }}/{{ token }}/">https://{{ country_code }}.{{ domain_uri }}/registered/{{ uidb64 }}/{{ token }}/</a>
{% trans "Thanks for using our site!" %}

View File

@ -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 %}

View File

@ -2,9 +2,8 @@
{% 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:" %}
{% block reset_link %}
http://{{ domain_uri }}{% url 'web:account:form-password-reset-confirm' uidb64=uidb64 token=token %}
{% endblock %}
<a href="https://{{ country_code }}.{{ domain_uri }}/recovery/{{ uidb64 }}/{{ token }}/">https://{{ country_code }}.{{ domain_uri }}/recovery/{{ uidb64 }}/{{ token }}/</a>
{% trans "Thanks for using our site!" %}

View File

@ -2,9 +2,8 @@
{% 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:" %}
{% block signup_confirm %}
http://{{ domain_uri }}{% url 'auth:signup-confirm' uidb64=uidb64 token=token %}
{% endblock %}
<a href="https://{{ country_code }}.{{ domain_uri }}/registered/{{ uidb64 }}/{{ token }}/">https://{{ country_code }}.{{ domain_uri }}/registered/{{ uidb64 }}/{{ token }}/</a>
{% trans "Thanks for using our site!" %}

View File

@ -8,7 +8,7 @@ urlpatterns = [
# path('account/', include('account.urls.web')),
# path('advertisement/', include('advertisement.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('partner/', include('partner.urls.web')),
]

View File

@ -3,10 +3,11 @@ from django.urls import path, include
app_name = 'mobile'
urlpatterns = [
path('establishments/', include('establishment.urls.mobile')),
# path('account/', include('account.urls.web')),
# path('advertisement/', include('advertisement.urls.web')),
# path('collection/', include('collection.urls.web')),
# path('establishments/', include('establishment.urls.web')),
# path('news/', include('news.urls.web')),
# path('partner/', include('partner.urls.web')),
]
]

View File

@ -28,4 +28,6 @@ urlpatterns = [
path('location/', include('location.urls')),
path('main/', include('main.urls')),
path('translation/', include('translation.urls')),
path('comments/', include('comment.urls.web')),
path('favorites/', include('favorites.urls')),
]