Merge branch 'develop' into feature/gm-148

# Conflicts:
#	apps/news/models.py
This commit is contained in:
Anatoly 2019-10-04 11:53:31 +03:00
commit 646eeb8972
18 changed files with 193 additions and 8 deletions

View File

@ -4,6 +4,20 @@ from django.db import migrations, models
import uuid import uuid
def fill_uuid(apps, schemaeditor):
Advertisement = apps.get_model('advertisement', 'Advertisement')
for a in Advertisement.objects.all():
a.uuid = uuid.uuid4()
a.save()
def fill_block_level(apps, schemaeditor):
Advertisement = apps.get_model('advertisement', 'Advertisement')
for a in Advertisement.objects.all():
a.block_level = ''
a.save()
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
@ -23,6 +37,12 @@ class Migration(migrations.Migration):
field=models.ManyToManyField(to='translation.Language'), field=models.ManyToManyField(to='translation.Language'),
), ),
migrations.AddField( migrations.AddField(
model_name='advertisement',
name='uuid',
field=models.UUIDField(default=uuid.uuid4, editable=False),
),
migrations.RunPython(fill_uuid, migrations.RunPython.noop),
migrations.AlterField(
model_name='advertisement', model_name='advertisement',
name='uuid', name='uuid',
field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True), field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
@ -32,8 +52,14 @@ class Migration(migrations.Migration):
name='block_level', name='block_level',
), ),
migrations.AddField( migrations.AddField(
model_name='advertisement',
name='block_level',
field=models.CharField(blank=True, null=True, max_length=10, verbose_name='Block level')
),
migrations.RunPython(fill_block_level, migrations.RunPython.noop),
migrations.AlterField(
model_name='advertisement', model_name='advertisement',
name='block_level', name='block_level',
field=models.CharField(max_length=10, verbose_name='Block level') field=models.CharField(max_length=10, verbose_name='Block level')
) ),
] ]

View File

@ -4,7 +4,7 @@ from django.contrib.auth import authenticate
from django.contrib.auth import password_validation as password_validators from django.contrib.auth import password_validation as password_validators
from django.db.models import Q from django.db.models import Q
from rest_framework import serializers from rest_framework import serializers
from rest_framework import validators as rest_validators from rest_framework.generics import get_object_or_404
from account import models as account_models from account import models as account_models
from authorization import tasks from authorization import tasks
@ -76,6 +76,39 @@ class SignupSerializer(serializers.ModelSerializer):
return obj return obj
class ReconfirmSerializer(serializers.ModelSerializer):
class Meta:
model = account_models.User
fields = ('email',)
def validate_email(self, value):
"""Validate email"""
users = list(account_models.User.objects.filter(email=value.lower()).all())
if not users:
raise serializers.ValidationError(detail='User with mentioned email does not exist.')
users = list(filter(lambda user: not user.email_confirmed, users))
if not users:
raise serializers.ValidationError(detail='User with this email is confirmed.')
return value
def create(self, validated_data):
"""Override create method"""
queryset = account_models.User.objects.all()
email = validated_data.get('email').lower()
country_code = self.context.get('request').country_code
obj = get_object_or_404(queryset, email=email)
if settings.USE_CELERY:
tasks.send_confirm_email.delay(
user_id=obj.id,
country_code=country_code)
else:
tasks.send_confirm_email(
user_id=obj.id,
country_code=country_code)
return obj
class LoginByUsernameOrEmailSerializer(SourceSerializerMixin, class LoginByUsernameOrEmailSerializer(SourceSerializerMixin,
serializers.ModelSerializer): serializers.ModelSerializer):
"""Serializer for login user""" """Serializer for login user"""

View File

@ -29,6 +29,7 @@ urlpatterns_oauth2 = [
urlpatterns_jwt = [ urlpatterns_jwt = [
path('signup/', views.SignUpView.as_view(), name='signup'), path('signup/', views.SignUpView.as_view(), name='signup'),
path('signup/reconfirm/', views.ReconfirmView.as_view(), name='signup-reconfirm'),
path('signup/confirm/<uidb64>/<token>/', views.ConfirmationEmailView.as_view(), path('signup/confirm/<uidb64>/<token>/', views.ConfirmationEmailView.as_view(),
name='signup-confirm'), name='signup-confirm'),
path('login/', views.LoginByUsernameOrEmailView.as_view(), name='login'), path('login/', views.LoginByUsernameOrEmailView.as_view(), name='login'),

View File

@ -147,6 +147,17 @@ class SignUpView(generics.GenericAPIView):
return Response(status=status.HTTP_201_CREATED) return Response(status=status.HTTP_201_CREATED)
class ReconfirmView(generics.CreateAPIView):
""" Resends confirmation email whether user's still not confirmed """
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.ReconfirmSerializer
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
return Response(status=status.HTTP_201_CREATED)
class ConfirmationEmailView(JWTGenericViewMixin): class ConfirmationEmailView(JWTGenericViewMixin):
"""View for confirmation email""" """View for confirmation email"""

View File

@ -349,7 +349,9 @@ class Carousel(models.Model):
@property @property
def model_name(self): def model_name(self):
return self.content_object.__class__.__name__ if hasattr(self.content_object, 'establishment_type'):
return self.content_object.establishment_type.name_translated
class Page(models.Model): class Page(models.Model):

View File

@ -1,11 +1,13 @@
"""News app models.""" """News app models."""
from django.contrib.contenttypes import fields as generic
from django.db import models from django.db import models
from django.contrib.contenttypes import fields as generic
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
from utils.models import BaseAttributes, TJSONField, TranslatedFieldsMixin from utils.models import BaseAttributes, TJSONField, TranslatedFieldsMixin
from django.contrib.contenttypes.models import ContentType
from rating.models import Rating
from random import sample as random_sample
class NewsType(models.Model): class NewsType(models.Model):
@ -27,6 +29,9 @@ class NewsType(models.Model):
class NewsQuerySet(models.QuerySet): class NewsQuerySet(models.QuerySet):
"""QuerySet for model News""" """QuerySet for model News"""
def rating_value(self):
return self.annotate(rating=models.Count('ratings__ip', distinct=True))
def with_base_related(self): def with_base_related(self):
"""Return qs with related objects.""" """Return qs with related objects."""
return self.select_related('news_type', 'country').prefetch_related('tags') return self.select_related('news_type', 'country').prefetch_related('tags')
@ -129,6 +134,7 @@ class News(BaseAttributes, TranslatedFieldsMixin):
tags = generic.GenericRelation(to='main.MetaDataContent') tags = generic.GenericRelation(to='main.MetaDataContent')
gallery = models.ManyToManyField('gallery.Image', through='news.NewsGallery') gallery = models.ManyToManyField('gallery.Image', through='news.NewsGallery')
ratings = generic.GenericRelation(Rating)
objects = NewsQuerySet.as_manager() objects = NewsQuerySet.as_manager()
@ -149,6 +155,14 @@ class News(BaseAttributes, TranslatedFieldsMixin):
def web_url(self): def web_url(self):
return reverse('web:news:rud', kwargs={'slug': self.slug}) return reverse('web:news:rud', kwargs={'slug': self.slug})
@property
def should_read(self):
return self.__class__.objects.should_read(self)[:3]
@property
def same_theme(self):
return self.__class__.objects.same_theme(self)[:3]
class NewsGalleryQuerySet(models.QuerySet): class NewsGalleryQuerySet(models.QuerySet):
"""QuerySet for model News""" """QuerySet for model News"""

View File

@ -7,7 +7,7 @@ from rest_framework.response import Response
from gallery.tasks import delete_image from gallery.tasks import delete_image
from news import filters, models, serializers from news import filters, models, serializers
from rating.tasks import add_rating
class NewsMixinView: class NewsMixinView:
"""News mixin.""" """News mixin."""
@ -40,7 +40,6 @@ class NewsDetailView(NewsMixinView, generics.RetrieveAPIView):
"""Override get_queryset method.""" """Override get_queryset method."""
return super().get_queryset().with_extended_related() return super().get_queryset().with_extended_related()
class NewsTypeListView(generics.ListAPIView): class NewsTypeListView(generics.ListAPIView):
"""NewsType list view.""" """NewsType list view."""
@ -140,3 +139,7 @@ class NewsBackOfficeRUDView(NewsBackOfficeMixinView,
serializer_class = serializers.NewsBackOfficeDetailSerializer serializer_class = serializers.NewsBackOfficeDetailSerializer
def get(self, request, pk, *args, **kwargs):
add_rating(remote_addr=request.META.get('REMOTE_ADDR'),
pk=pk, model='news', app_label='news')
return self.retrieve(request, *args, **kwargs)

0
apps/rating/__init__.py Normal file
View File

11
apps/rating/admin.py Normal file
View File

@ -0,0 +1,11 @@
from django.contrib import admin
from rating import models
from rating import tasks
@admin.register(models.Rating)
class RatingAdmin(admin.ModelAdmin):
"""Rating type admin conf."""
list_display = ['name', 'ip']
list_display_links = ['name']

5
apps/rating/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class RatingConfig(AppConfig):
name = 'rating'

View File

@ -0,0 +1,28 @@
# Generated by Django 2.2.4 on 2019-10-02 11:32
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.CreateModel(
name='Rating',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.PositiveIntegerField()),
('ip', models.GenericIPAddressField(verbose_name='ip')),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
],
options={
'verbose_name': 'rating',
},
),
]

View File

19
apps/rating/models.py Normal file
View File

@ -0,0 +1,19 @@
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils.translation import gettext_lazy as _
class Rating(models.Model):
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
ip = models.GenericIPAddressField(verbose_name=_('ip'))
@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

22
apps/rating/tasks.py Normal file
View File

@ -0,0 +1,22 @@
from datetime import timedelta
from celery import task
from rating.models import Rating
from django.contrib.contenttypes.models import ContentType
def add_rating(remote_addr, pk, model, app_label):
add.apply_async(
(remote_addr, pk, model, app_label), countdown=60 * 60
)
@task
def add(remote_addr, pk, model, app_label):
rating = Rating()
rating.ip = remote_addr
rating.object_id = pk
rating.content_type = ContentType.objects.get(app_label=app_label, model=model)
rating.save()

3
apps/rating/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
apps/rating/views.py Normal file
View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@ -60,7 +60,10 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
'description', 'description',
) )
filter_fields = { filter_fields = {
'tag': 'tags.id', 'tag': {
'field': 'tags.id',
'lookups': [constants.LOOKUP_QUERY_IN]
},
'toque_number': { 'toque_number': {
'field': 'toque_number', 'field': 'toque_number',
'lookups': [ 'lookups': [

View File

@ -71,6 +71,7 @@ PROJECT_APPS = [
'review.apps.ReviewConfig', 'review.apps.ReviewConfig',
'comment.apps.CommentConfig', 'comment.apps.CommentConfig',
'favorites.apps.FavoritesConfig', 'favorites.apps.FavoritesConfig',
'rating.apps.RatingConfig',
] ]
EXTERNAL_APPS = [ EXTERNAL_APPS = [