In favorites

This commit is contained in:
evgeniy-st 2019-11-21 18:12:52 +03:00
parent bea485f936
commit 35b6a66c85
18 changed files with 161 additions and 61 deletions

View File

@ -25,7 +25,8 @@ from main.models import Award, Currency
from review.models import Review from review.models import Review
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin, from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
TranslatedFieldsMixin, BaseAttributes, GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes, GalleryModelMixin,
IntermediateGalleryModelMixin, HasTagsMixin) IntermediateGalleryModelMixin, HasTagsMixin,
FavoritesMixin)
# todo: establishment type&subtypes check # todo: establishment type&subtypes check
@ -319,7 +320,8 @@ class EstablishmentQuerySet(models.QuerySet):
return self.exclude(address__city__country__in=countries) return self.exclude(address__city__country__in=countries)
class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin, HasTagsMixin): class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin,
TranslatedFieldsMixin, HasTagsMixin, FavoritesMixin):
"""Establishment model.""" """Establishment model."""
# todo: delete image URL fields after moving on gallery # todo: delete image URL fields after moving on gallery

View File

@ -5,12 +5,11 @@ from django.shortcuts import get_object_or_404
from rest_framework import generics, permissions from rest_framework import generics, permissions
from comment import models as comment_models from comment import models as comment_models
from establishment import filters from comment.serializers import CommentRUDSerializer
from establishment import models, serializers from establishment import filters, models, serializers
from main import methods from main import methods
from utils.pagination import EstablishmentPortionPagination from utils.pagination import EstablishmentPortionPagination
from utils.permissions import IsCountryAdmin from utils.views import FavoritesCreateDestroyMixinView
from comment.serializers import CommentRUDSerializer
class EstablishmentMixinView: class EstablishmentMixinView:
@ -134,21 +133,11 @@ class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView):
return comment_obj return comment_obj
class EstablishmentFavoritesCreateDestroyView(generics.CreateAPIView, generics.DestroyAPIView): class EstablishmentFavoritesCreateDestroyView(FavoritesCreateDestroyMixinView):
"""View for create/destroy establishment from favorites.""" """View for create/destroy establishment from favorites."""
serializer_class = serializers.EstablishmentFavoritesCreateSerializer
lookup_field = 'slug'
def get_object(self): _model = models.Establishment
""" serializer_class = serializers.EstablishmentFavoritesCreateSerializer
Returns the object the view is displaying.
"""
establishment = get_object_or_404(models.Establishment,
slug=self.kwargs['slug'])
favorites = get_object_or_404(establishment.favorites.filter(user=self.request.user))
# May raise a permission denied
self.check_object_permissions(self.request, favorites)
return favorites
class EstablishmentNearestRetrieveView(EstablishmentListView, generics.ListAPIView): class EstablishmentNearestRetrieveView(EstablishmentListView, generics.ListAPIView):

View File

@ -8,7 +8,8 @@ from rest_framework.reverse import reverse
from rating.models import Rating, ViewCount from rating.models import Rating, ViewCount
from utils.models import (BaseAttributes, TJSONField, TranslatedFieldsMixin, HasTagsMixin, from utils.models import (BaseAttributes, TJSONField, TranslatedFieldsMixin, HasTagsMixin,
ProjectBaseMixin, GalleryModelMixin, IntermediateGalleryModelMixin) ProjectBaseMixin, GalleryModelMixin, IntermediateGalleryModelMixin,
FavoritesMixin)
from utils.querysets import TranslationQuerysetMixin from utils.querysets import TranslationQuerysetMixin
from django.conf import settings from django.conf import settings
@ -126,7 +127,8 @@ class NewsQuerySet(TranslationQuerysetMixin):
) )
class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin): class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin,
FavoritesMixin):
"""News model.""" """News model."""
STR_FIELD_NAME = 'title' STR_FIELD_NAME = 'title'

View File

@ -6,7 +6,7 @@ from rest_framework import generics, permissions
from news import filters, models, serializers from news import filters, models, serializers
from rating.tasks import add_rating from rating.tasks import add_rating
from utils.permissions import IsCountryAdmin, IsContentPageManager from utils.permissions import IsCountryAdmin, IsContentPageManager
from utils.views import CreateDestroyGalleryViewMixin from utils.views import CreateDestroyGalleryViewMixin, FavoritesCreateDestroyMixinView
from utils.serializers import ImageBaseSerializer from utils.serializers import ImageBaseSerializer
@ -150,18 +150,8 @@ class NewsBackOfficeRUDView(NewsBackOfficeMixinView,
return self.retrieve(request, *args, **kwargs) return self.retrieve(request, *args, **kwargs)
class NewsFavoritesCreateDestroyView(generics.CreateAPIView, generics.DestroyAPIView): class NewsFavoritesCreateDestroyView(FavoritesCreateDestroyMixinView):
"""View for create/destroy news from favorites.""" """View for create/destroy news from favorites."""
_model = models.News
serializer_class = serializers.NewsFavoritesCreateSerializer serializer_class = serializers.NewsFavoritesCreateSerializer
lookup_field = 'slug'
def get_object(self):
"""
Returns the object the view is displaying.
"""
news = get_object_or_404(models.News, slug=self.kwargs['slug'])
favorites = get_object_or_404(news.favorites.filter(user=self.request.user))
# May raise a permission denied
self.check_object_permissions(self.request, favorites)
return favorites

View File

@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from utils.models import (BaseAttributes, ProjectBaseMixin, HasTagsMixin, from utils.models import (BaseAttributes, ProjectBaseMixin, HasTagsMixin,
TranslatedFieldsMixin, TJSONField, TranslatedFieldsMixin, TJSONField, FavoritesMixin,
GalleryModelMixin, IntermediateGalleryModelMixin) GalleryModelMixin, IntermediateGalleryModelMixin)
@ -131,7 +131,8 @@ class ProductQuerySet(models.QuerySet):
) )
class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes, HasTagsMixin): class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes,
HasTagsMixin, FavoritesMixin):
"""Product models.""" """Product models."""
EARLIEST_VINTAGE_YEAR = 1700 EARLIEST_VINTAGE_YEAR = 1700

View File

@ -3,9 +3,9 @@ from rest_framework import generics, permissions
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from product.models import Product from product.models import Product
from comment.models import Comment from comment.models import Comment
from product import serializers from product import filters, serializers
from product import filters
from comment.serializers import CommentRUDSerializer from comment.serializers import CommentRUDSerializer
from utils.views import FavoritesCreateDestroyMixinView
class ProductBaseView(generics.GenericAPIView): class ProductBaseView(generics.GenericAPIView):
@ -37,22 +37,11 @@ class ProductDetailView(ProductBaseView, generics.RetrieveAPIView):
serializer_class = serializers.ProductDetailSerializer serializer_class = serializers.ProductDetailSerializer
class CreateFavoriteProductView(generics.CreateAPIView, class CreateFavoriteProductView(FavoritesCreateDestroyMixinView):
generics.DestroyAPIView):
"""View for create/destroy product in favorites.""" """View for create/destroy product in favorites."""
_model = Product
serializer_class = serializers.ProductFavoritesCreateSerializer serializer_class = serializers.ProductFavoritesCreateSerializer
lookup_field = 'slug'
def get_object(self):
"""
Returns the object the view is displaying.
"""
product = get_object_or_404(Product, slug=self.kwargs['slug'])
favorites = get_object_or_404(product.favorites.filter(user=self.request.user))
# May raise a permission denied
self.check_object_permissions(self.request, favorites)
return favorites
class ProductCommentCreateView(generics.CreateAPIView): class ProductCommentCreateView(generics.CreateAPIView):

View File

@ -1,11 +1,12 @@
from search_indexes.documents.establishment import EstablishmentDocument from search_indexes.documents.establishment import EstablishmentDocument
from search_indexes.documents.news import NewsDocument from search_indexes.documents.news import NewsDocument
from search_indexes.documents.product import ProductDocument from search_indexes.documents.product import ProductDocument
from search_indexes.tasks import es_update
# todo: make signal to update documents on related fields # todo: make signal to update documents on related fields
__all__ = [ __all__ = [
'EstablishmentDocument', 'EstablishmentDocument',
'NewsDocument', 'NewsDocument',
'ProductDocument', 'ProductDocument',
'es_update',
] ]

View File

@ -113,6 +113,7 @@ class EstablishmentDocument(Document):
), ),
}, },
) )
favorites_for_users = fields.ListField(field=fields.IntegerField())
class Django: class Django:

View File

@ -41,6 +41,7 @@ class NewsDocument(Document):
properties=OBJECT_FIELD_PROPERTIES), properties=OBJECT_FIELD_PROPERTIES),
}, },
multi=True) multi=True)
favorites_for_users = fields.ListField(field=fields.IntegerField())
class Django: class Django:
@ -57,7 +58,7 @@ class NewsDocument(Document):
related_models = [models.NewsType] related_models = [models.NewsType]
def get_queryset(self): def get_queryset(self):
return super().get_queryset().published().with_base_related().sort_by_start() return super().get_queryset().published().with_base_related()
def get_instances_from_related(self, related_instance): def get_instances_from_related(self, related_instance):
"""If related_models is set, define how to retrieve the Car instance(s) from the related model. """If related_models is set, define how to retrieve the Car instance(s) from the related model.

View File

@ -148,6 +148,7 @@ class ProductDocument(Document):
name = fields.TextField(attr='display_name', analyzer='english') name = fields.TextField(attr='display_name', analyzer='english')
name_ru = fields.TextField(attr='display_name', analyzer='russian') name_ru = fields.TextField(attr='display_name', analyzer='russian')
name_fr = fields.TextField(attr='display_name', analyzer='french') name_fr = fields.TextField(attr='display_name', analyzer='french')
favorites_for_users = fields.ListField(field=fields.IntegerField())
class Django: class Django:
model = models.Product model = models.Product

View File

@ -167,7 +167,25 @@ class ScheduleDocumentSerializer(serializers.Serializer):
closed_at = serializers.CharField() closed_at = serializers.CharField()
class NewsDocumentSerializer(DocumentSerializer): class InFavoritesMixin(DocumentSerializer):
"""Append in_favorites field."""
in_favorites = serializers.SerializerMethodField()
def get_in_favorites(self, obj):
request = self.context['request']
user = request.user
if user.is_authenticated:
return user.id in obj.favorites_for_users
return False
class Meta:
"""Meta class."""
_abstract = True
class NewsDocumentSerializer(InFavoritesMixin, DocumentSerializer):
"""News document serializer.""" """News document serializer."""
title_translated = serializers.SerializerMethodField(allow_null=True) title_translated = serializers.SerializerMethodField(allow_null=True)
@ -200,7 +218,7 @@ class NewsDocumentSerializer(DocumentSerializer):
return get_translated_value(obj.subtitle) return get_translated_value(obj.subtitle)
class EstablishmentDocumentSerializer(DocumentSerializer): class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer):
"""Establishment document serializer.""" """Establishment document serializer."""
establishment_type = EstablishmentTypeSerializer() establishment_type = EstablishmentTypeSerializer()
@ -236,7 +254,7 @@ class EstablishmentDocumentSerializer(DocumentSerializer):
) )
class ProductDocumentSerializer(DocumentSerializer): class ProductDocumentSerializer(InFavoritesMixin, DocumentSerializer):
"""Product document serializer""" """Product document serializer"""
tags = TagsDocumentSerializer(many=True, source='related_tags') tags = TagsDocumentSerializer(many=True, source='related_tags')

View File

@ -0,0 +1,50 @@
"""SearchIndex tasks."""
import logging
from django.db import models
from celery.schedules import crontab
from celery.task import periodic_task
from django_elasticsearch_dsl.registries import registry
from django_redis import get_redis_connection
from establishment.models import Establishment
from news.models import News
from product.models import Product
logger = logging.getLogger(__name__)
@periodic_task(run_every=crontab(minute=1))
def update_index():
"""Updates ES index."""
try:
cn = get_redis_connection('es_queue')
for model in [Establishment, News, Product]:
model_name = model.__name__.lower()
while True:
ids = cn.spop(model_name, 500)
if not ids:
break
qs = model.objects.filter(id__in=ids)
try:
doc = registry.get_documents([model]).pop()
except KeyError:
pass
else:
doc().update(qs)
except Exception as ex:
logger.error(f'Updating index failed: {ex}')
def es_update(obj):
"""Adds object to set of objects for indexing."""
try:
cn = get_redis_connection('es_queue')
allowed_models = [Establishment, News, Product]
if isinstance(obj, models.QuerySet) and obj.model in allowed_models:
key = obj.model.__name__.lower()
cn.sadd(key, *obj.values_list('id', flat=True))
elif isinstance(obj, models.Model) and obj.__class__ in allowed_models:
key = obj.__class__.__name__.lower()
cn.sadd(key, obj.id)
except Exception as ex:
logger.warning(f'Send obj to ES failed: {ex}')

View File

@ -5,6 +5,7 @@ from os.path import exists
from django.conf import settings from django.conf import settings
from django.contrib.auth.tokens import PasswordResetTokenGenerator from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.contrib.gis.db import models from django.contrib.gis.db import models
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import JSONField from django.contrib.postgres.fields import JSONField
from django.contrib.postgres.fields.jsonb import KeyTextTransform from django.contrib.postgres.fields.jsonb import KeyTextTransform
from django.utils import timezone from django.utils import timezone
@ -435,4 +436,11 @@ class HasTagsMixin(models.Model):
abstract = True abstract = True
class FavoritesMixin:
"""Append favorites_for_user property."""
@property
def favorites_for_users(self):
return self.favorites.aggregate(arr=ArrayAgg('user_id')).get('arr')
timezone.datetime.now().date().isoformat() timezone.datetime.now().date().isoformat()

View File

@ -2,11 +2,12 @@ from collections import namedtuple
from django.conf import settings from django.conf import settings
from django.db.transaction import on_commit from django.db.transaction import on_commit
from rest_framework import generics from django.shortcuts import get_object_or_404
from rest_framework import status from rest_framework import generics, status
from rest_framework.response import Response from rest_framework.response import Response
from gallery.tasks import delete_image from gallery.tasks import delete_image
from search_indexes.documents import es_update
# JWT # JWT
@ -121,3 +122,37 @@ class CreateDestroyGalleryViewMixin(generics.CreateAPIView,
# Delete an instances of Gallery model # Delete an instances of Gallery model
gallery_obj.delete() gallery_obj.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
class FavoritesCreateDestroyMixinView(generics.CreateAPIView,
generics.DestroyAPIView):
"""Favorites Create Destroy mixin."""
_model = None
serializer_class = None
lookup_field = 'slug'
def get_base_object(self):
return get_object_or_404(self._model, slug=self.kwargs['slug'])
def get_object(self):
"""
Returns the object the view is displaying.
"""
obj = self.get_base_object()
favorites = get_object_or_404(obj.favorites.filter(user=self.request.user))
# May raise a permission denied
self.check_object_permissions(self.request, favorites)
return favorites
def es_update_base_object(self):
es_update(self.get_base_object())
def perform_create(self, serializer):
serializer.save()
self.es_update_base_object()
def perform_destroy(self, instance):
instance.delete()
self.es_update_base_object()

View File

@ -30,7 +30,7 @@ services:
# Redis # Redis
redis: redis:
image: redis:2.8.23 image: redis:latest
# Celery # Celery
worker: worker:

View File

@ -250,6 +250,17 @@ AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
) )
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
},
'es_queue': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://redis:6379/2'
}
}
# Override default OAuth2 namespace # Override default OAuth2 namespace
DRFSO2_URL_NAMESPACE = 'auth' DRFSO2_URL_NAMESPACE = 'auth'
SOCIAL_AUTH_URL_NAMESPACE = 'auth' SOCIAL_AUTH_URL_NAMESPACE = 'auth'

View File

@ -54,5 +54,6 @@ PyYAML==5.1.2
# temp solution # temp solution
redis==3.2.0 redis==3.2.0
django_redis==4.10.0 # used byes indexing cache
kombu==4.6.6 kombu==4.6.6
celery==4.3.0 celery==4.3.0

View File

@ -1,4 +1,4 @@
#!/bin/sh #!/bin/sh
sleep 5 sleep 5
celery -A project worker -B -l info celery -A project beat -l info