Merge remote-tracking branch 'origin/develop' into develop

# Conflicts:
#	apps/establishment/urls/back.py
This commit is contained in:
Dmitriy Kuzmenko 2019-09-19 17:19:40 +03:00
commit 5a36613c97
49 changed files with 776 additions and 108 deletions

View File

@ -35,11 +35,8 @@ class PasswordResetSerializer(serializers.Serializer):
if filters: if filters:
filters.update({'is_active': True}) filters.update({'is_active': True})
user_qs = models.User.objects.filter(**filters) user_qs = models.User.objects.filter(**filters)
if user_qs.exists():
if not user_qs.exists(): user = user_qs.first()
raise utils_exceptions.UserNotFoundError()
user = user_qs.first()
attrs['user'] = user attrs['user'] = user
return attrs return attrs

View File

@ -22,7 +22,7 @@ class PasswordResetView(generics.GenericAPIView):
"""Override create method""" """Override create method"""
serializer = self.get_serializer(data=request.data) serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
if serializer.validated_data.get('user'): if not serializer.validated_data.get('user').is_anonymous:
user = serializer.validated_data.pop('user') user = serializer.validated_data.pop('user')
if settings.USE_CELERY: if settings.USE_CELERY:
tasks.send_reset_password_email.delay(user_id=user.id, tasks.send_reset_password_email.delay(user_id=user.id,

View File

@ -0,0 +1,39 @@
# Generated by Django 2.2.4 on 2019-09-17 13:07
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('translation', '0003_auto_20190901_1032'),
('advertisement', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='advertisement',
name='source',
field=models.PositiveSmallIntegerField(choices=[(0, 'Mobile'), (1, 'Web')], default=0, verbose_name='Source'),
),
migrations.AddField(
model_name='advertisement',
name='target_languages',
field=models.ManyToManyField(to='translation.Language'),
),
migrations.AddField(
model_name='advertisement',
name='uuid',
field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
),
migrations.RemoveField(
model_name='advertisement',
name='block_level',
),
migrations.AddField(
model_name='advertisement',
name='block_level',
field=models.CharField(max_length=10, verbose_name='Block level')
)
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-09-19 13:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('advertisement', '0002_auto_20190917_1307'),
]
operations = [
migrations.AlterField(
model_name='advertisement',
name='source',
field=models.PositiveSmallIntegerField(choices=[(0, 'Mobile'), (1, 'Web'), (2, 'All')], default=0, verbose_name='Source'),
),
]

View File

@ -1,28 +1,22 @@
"""Advertisement app models.""" """Advertisement app models."""
import uuid
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from utils.models import ProjectBaseMixin, ImageMixin from translation.models import Language
from utils.models import ProjectBaseMixin, ImageMixin, PlatformMixin
class Advertisement(ImageMixin, ProjectBaseMixin): class Advertisement(ImageMixin, ProjectBaseMixin, PlatformMixin):
"""Advertisement model.""" """Advertisement model."""
LEVEL_1 = 1 uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
LEVEL_2 = 2
LEVEL_3 = 3
BLOCK_LEVEL_CHOICES = (
(LEVEL_1, _('Ad block level 1')),
(LEVEL_2, _('Ad block level 2')),
(LEVEL_3, _('Ad block level 3'))
)
url = models.URLField(verbose_name=_('Ad URL')) url = models.URLField(verbose_name=_('Ad URL'))
width = models.PositiveIntegerField(verbose_name=_('Block width')) width = models.PositiveIntegerField(verbose_name=_('Block width'))
height = models.PositiveIntegerField(verbose_name=_('Block height')) height = models.PositiveIntegerField(verbose_name=_('Block height'))
block_level = models.PositiveSmallIntegerField(choices=BLOCK_LEVEL_CHOICES, block_level = models.CharField(verbose_name=_('Block level'), max_length=10)
verbose_name=_('Block level')) target_languages = models.ManyToManyField(Language)
class Meta: class Meta:
verbose_name = _('Advertisement') verbose_name = _('Advertisement')

View File

@ -2,6 +2,7 @@
from rest_framework import serializers from rest_framework import serializers
from advertisement import models from advertisement import models
from translation.serializers import LanguageSerializer
class AdvertisementSerializer(serializers.ModelSerializer): class AdvertisementSerializer(serializers.ModelSerializer):
@ -11,9 +12,11 @@ class AdvertisementSerializer(serializers.ModelSerializer):
model = models.Advertisement model = models.Advertisement
fields = ( fields = (
'id', 'id',
'uuid',
'url', 'url',
'image', 'image',
'width', 'width',
'height', 'height',
'block_level', 'block_level',
'source'
) )

View File

@ -3,8 +3,8 @@ from django.urls import path
from advertisement.views import web as views from advertisement.views import web as views
app_name = 'advertisement' app_name = 'advertisements'
urlpatterns = [ urlpatterns = [
path('', views.AdvertisementListView.as_view(), name='list') path('<str:page>/', views.AdvertisementListView.as_view(), name='list')
] ]

View File

@ -10,6 +10,10 @@ class AdvertisementListView(generics.ListAPIView):
"""List view for model Advertisement""" """List view for model Advertisement"""
pagination_class = None pagination_class = None
model = models.Advertisement model = models.Advertisement
permission_classes = (permissions.AllowAny, ) permission_classes = (permissions.AllowAny,)
queryset = models.Advertisement.objects.all()
serializer_class = serializers.AdvertisementSerializer serializer_class = serializers.AdvertisementSerializer
def get_queryset(self):
return models.Advertisement.objects\
.filter(page__page_name__contains=self.kwargs['page'])\
.filter(target_languages__locale=self.request.locale)

View File

@ -0,0 +1,23 @@
# Generated by Django 2.2.4 on 2019-09-19 13:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authorization', '0007_jwtaccesstoken_refresh_token'),
]
operations = [
migrations.AlterField(
model_name='application',
name='source',
field=models.PositiveSmallIntegerField(choices=[(0, 'Mobile'), (1, 'Web'), (2, 'All')], default=0, verbose_name='Source'),
),
migrations.AlterField(
model_name='jwtrefreshtoken',
name='source',
field=models.PositiveSmallIntegerField(choices=[(0, 'Mobile'), (1, 'Web'), (2, 'All')], default=0, verbose_name='Source'),
),
]

View File

@ -48,8 +48,8 @@ class CollectionItemSerializer(serializers.ModelSerializer):
fields = [ fields = [
'id', 'id',
'collection', 'collection',
'item_type', 'content_type',
'item_ids' 'object_id',
] ]

View File

@ -61,11 +61,6 @@ class EstablishmentAdmin(admin.ModelAdmin):
ReviewInline, CommentInline] ReviewInline, CommentInline]
@admin.register(models.EstablishmentSchedule)
class EstablishmentSchedule(admin.ModelAdmin):
"""Establishment schedule"""
@admin.register(models.Position) @admin.register(models.Position)
class PositionAdmin(admin.ModelAdmin): class PositionAdmin(admin.ModelAdmin):
"""Position admin.""" """Position admin."""

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-09-13 13:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0018_socialnetwork'),
]
operations = [
migrations.AddField(
model_name='position',
name='priority',
field=models.IntegerField(default=None, null=True, unique=True),
),
]

View File

@ -0,0 +1,16 @@
# Generated by Django 2.2.4 on 2019-09-18 12:07
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('establishment', '0020_merge_20190917_1415'),
]
operations = [
migrations.DeleteModel(
name='EstablishmentSchedule',
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-09-19 09:55
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('establishment', '0020_merge_20190917_1415'),
('establishment', '0019_position_priority'),
]
operations = [
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.4 on 2019-09-18 14:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('timetable', '0001_initial'),
('establishment', '0021_delete_establishmentschedule'),
]
operations = [
migrations.AddField(
model_name='establishment',
name='schedule',
field=models.ManyToManyField(related_name='schedule', to='timetable.Timetable', verbose_name='Establishment schedule'),
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-09-19 11:36
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('establishment', '0021_merge_20190919_0955'),
('establishment', '0022_establishment_schedule'),
]
operations = [
]

View File

@ -262,6 +262,13 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
booking = models.URLField(blank=True, null=True, default=None, booking = models.URLField(blank=True, null=True, default=None,
verbose_name=_('Booking URL')) verbose_name=_('Booking URL'))
is_publish = models.BooleanField(default=False, verbose_name=_('Publish status')) is_publish = models.BooleanField(default=False, verbose_name=_('Publish status'))
schedule = models.ManyToManyField(to='timetable.Timetable',
verbose_name=_('Establishment schedule'),
related_name='schedule')
# holidays_from = models.DateTimeField(verbose_name=_('Holidays from'),
# help_text=_('Holidays closing date from'))
# holidays_to = models.DateTimeField(verbose_name=_('Holidays to'),
# help_text=_('Holidays closing date to'))
awards = generic.GenericRelation(to='main.Award') awards = generic.GenericRelation(to='main.Award')
tags = generic.GenericRelation(to='main.MetaDataContent') tags = generic.GenericRelation(to='main.MetaDataContent')
reviews = generic.GenericRelation(to='review.Review') reviews = generic.GenericRelation(to='review.Review')
@ -335,6 +342,8 @@ class Position(BaseAttributes, TranslatedFieldsMixin):
name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'), name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'),
help_text='{"en":"some text"}') help_text='{"en":"some text"}')
priority = models.IntegerField(unique=True, null=True, default=None)
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -392,23 +401,6 @@ class EstablishmentScheduleQuerySet(models.QuerySet):
"""QuerySet for model EstablishmentSchedule""" """QuerySet for model EstablishmentSchedule"""
class EstablishmentSchedule(BaseAttributes):
"""Establishment schedule model."""
establishment = models.OneToOneField(Establishment,
related_name='schedule',
on_delete=models.CASCADE,
verbose_name=_('Establishment'))
schedule = models.ManyToManyField(to='timetable.Timetable',
verbose_name=_('Establishment schedule'))
objects = EstablishmentScheduleQuerySet.as_manager()
class Meta:
"""Meta class"""
verbose_name = _('Establishment schedule')
verbose_name_plural = _('Establishment schedules')
class ContactPhone(models.Model): class ContactPhone(models.Model):
"""Contact phone model.""" """Contact phone model."""
establishment = models.ForeignKey( establishment = models.ForeignKey(

View File

@ -1,9 +1,12 @@
import json
from rest_framework import serializers from rest_framework import serializers
from establishment import models from establishment import models
from timetable.models import Timetable
from establishment.serializers import ( from establishment.serializers import (
EstablishmentBaseSerializer, PlateSerializer, ContactEmailsSerializer, EstablishmentBaseSerializer, PlateSerializer, ContactEmailsSerializer,
ContactPhonesSerializer, SocialNetworkRelatedSerializers) ContactPhonesSerializer, SocialNetworkRelatedSerializers, EstablishmentDetailSerializer
)
from main.models import Currency from main.models import Currency
@ -83,4 +86,15 @@ class ContactEmailBackSerializers(PlateSerializer):
'id', 'id',
'establishment', 'establishment',
'email' 'email'
]
class EmployeeBackSerializers(serializers.ModelSerializer):
"""Social network serializers."""
class Meta:
model = models.Employee
fields = [
'id',
'user',
'name'
] ]

View File

@ -9,7 +9,7 @@ from location.serializers import AddressSerializer
from main.models import MetaDataContent from main.models import MetaDataContent
from main.serializers import MetaDataContentSerializer, AwardSerializer, CurrencySerializer from main.serializers import MetaDataContentSerializer, AwardSerializer, CurrencySerializer
from review import models as review_models from review import models as review_models
from timetable.models import Timetable from timetable.serialziers import ScheduleRUDSerializer
from utils import exceptions as utils_exceptions from utils import exceptions as utils_exceptions
@ -111,22 +111,6 @@ class EstablishmentSubTypeSerializer(serializers.ModelSerializer):
fields = ('id', 'name_translated') 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): class ReviewSerializer(serializers.ModelSerializer):
"""Serializer for model Review.""" """Serializer for model Review."""
text_translated = serializers.CharField(read_only=True) text_translated = serializers.CharField(read_only=True)
@ -146,12 +130,13 @@ class EstablishmentEmployeeSerializer(serializers.ModelSerializer):
name = serializers.CharField(source='employee.name') name = serializers.CharField(source='employee.name')
position_translated = serializers.CharField(source='position.name_translated') position_translated = serializers.CharField(source='position.name_translated')
awards = AwardSerializer(source='employee.awards', many=True) awards = AwardSerializer(source='employee.awards', many=True)
priority = serializers.IntegerField(source='position.priority')
class Meta: class Meta:
"""Meta class.""" """Meta class."""
model = models.Employee model = models.Employee
fields = ('id', 'name', 'position_translated', 'awards') fields = ('id', 'name', 'position_translated', 'awards', 'priority')
class EstablishmentBaseSerializer(serializers.ModelSerializer): class EstablishmentBaseSerializer(serializers.ModelSerializer):
@ -160,7 +145,7 @@ class EstablishmentBaseSerializer(serializers.ModelSerializer):
subtypes = EstablishmentSubTypeSerializer(many=True) subtypes = EstablishmentSubTypeSerializer(many=True)
address = AddressSerializer() address = AddressSerializer()
tags = MetaDataContentSerializer(many=True) tags = MetaDataContentSerializer(many=True)
preview_image = serializers.ImageField(source='image') preview_image = serializers.SerializerMethodField()
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -203,9 +188,7 @@ class EstablishmentDetailSerializer(EstablishmentListSerializer):
"""Serializer for Establishment model.""" """Serializer for Establishment model."""
description_translated = serializers.CharField(allow_null=True) description_translated = serializers.CharField(allow_null=True)
awards = AwardSerializer(many=True) awards = AwardSerializer(many=True)
schedule = EstablishmentScheduleSerializer(source='schedule.schedule', schedule = ScheduleRUDSerializer(many=True, allow_null=True)
many=True,
allow_null=True)
phones = ContactPhonesSerializer(read_only=True, many=True, ) phones = ContactPhonesSerializer(read_only=True, many=True, )
emails = ContactEmailsSerializer(read_only=True, many=True, ) emails = ContactEmailsSerializer(read_only=True, many=True, )
review = serializers.SerializerMethodField() review = serializers.SerializerMethodField()
@ -355,6 +338,7 @@ class EstablishmentFavoritesCreateSerializer(serializers.ModelSerializer):
class EstablishmentTagListSerializer(serializers.ModelSerializer): class EstablishmentTagListSerializer(serializers.ModelSerializer):
"""List establishment tag serializer.""" """List establishment tag serializer."""
id = serializers.IntegerField(source='metadata.id')
label_translated = serializers.CharField( label_translated = serializers.CharField(
source='metadata.label_translated', read_only=True, allow_null=True) source='metadata.label_translated', read_only=True, allow_null=True)
@ -362,5 +346,6 @@ class EstablishmentTagListSerializer(serializers.ModelSerializer):
"""Meta class.""" """Meta class."""
model = MetaDataContent model = MetaDataContent
fields = [ fields = [
'id',
'label_translated', 'label_translated',
] ]

View File

@ -10,6 +10,10 @@ app_name = 'establishment'
urlpatterns = [ urlpatterns = [
path('', views.EstablishmentListCreateView.as_view(), name='list'), path('', views.EstablishmentListCreateView.as_view(), name='list'),
path('<int:pk>/', views.EstablishmentRetrieveView.as_view(), name='detail'), path('<int:pk>/', views.EstablishmentRetrieveView.as_view(), name='detail'),
path('<int:pk>/schedule/<int:schedule_id>/', views.EstablishmentScheduleRUDView.as_view(),
name='schedule-rud'),
path('<int:pk>/schedule/', views.EstablishmentScheduleCreateView.as_view(),
name='schedule-create'),
path('menus/', views.MenuListCreateView.as_view(), name='menu-list'), path('menus/', views.MenuListCreateView.as_view(), name='menu-list'),
path('menus/<int:pk>/', views.MenuRUDView.as_view(), name='menu-rud'), path('menus/<int:pk>/', views.MenuRUDView.as_view(), name='menu-rud'),
path('plates/', views.PlateListCreateView.as_view(), name='plates'), path('plates/', views.PlateListCreateView.as_view(), name='plates'),
@ -20,5 +24,6 @@ urlpatterns = [
path('phones/<int:pk>/', views.PhonesRUDView.as_view(), name='phones-rud'), path('phones/<int:pk>/', views.PhonesRUDView.as_view(), name='phones-rud'),
path('emails/', views.EmailListCreateView.as_view(), name='emails'), path('emails/', views.EmailListCreateView.as_view(), name='emails'),
path('emails/<int:pk>/', views.EmailRUDView.as_view(), name='emails-rud'), path('emails/<int:pk>/', views.EmailRUDView.as_view(), name='emails-rud'),
path('employees/', views.EmployeeListCreateView.as_view(), name='employees'),
path('employees/<int:pk>/', views.EmployeeRUDView.as_view(), name='employees-rud'),
] ]

View File

@ -2,7 +2,8 @@
from rest_framework import generics from rest_framework import generics
from establishment import models, serializers from establishment import models
from establishment import serializers
from establishment.views.common import EstablishmentMixin from establishment.views.common import EstablishmentMixin
@ -73,4 +74,16 @@ class EmailListCreateView(generics.ListCreateAPIView):
class EmailRUDView(generics.RetrieveUpdateDestroyAPIView): class EmailRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Social RUD view.""" """Social RUD view."""
serializer_class = serializers.ContactEmailBackSerializers serializer_class = serializers.ContactEmailBackSerializers
queryset = models.ContactEmail.objects.all() queryset = models.ContactEmail.objects.all()
class EmployeeListCreateView(generics.ListCreateAPIView):
"""Emplyoee list create view."""
serializer_class = serializers.EmployeeBackSerializers
queryset = models.Employee.objects.all()
pagination_class = None
class EmployeeRUDView(generics.RetrieveDestroyAPIView):
"""Social RUD view."""
serializer_class = serializers.EmployeeBackSerializers
queryset = models.Employee.objects.all()

View File

@ -6,9 +6,9 @@ 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 establishment import filters
from establishment import models, serializers from establishment import models, serializers
from main.models import MetaDataContent
from utils.views import JWTGenericViewMixin
from establishment.views import EstablishmentMixin from establishment.views import EstablishmentMixin
from main.models import MetaDataContent
from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer
class EstablishmentListView(EstablishmentMixin, generics.ListAPIView): class EstablishmentListView(EstablishmentMixin, generics.ListAPIView):
@ -34,12 +34,12 @@ class EstablishmentSimilarListView(EstablishmentListView):
.order_by('-total_mark')[:13] .order_by('-total_mark')[:13]
class EstablishmentRetrieveView(EstablishmentMixin, JWTGenericViewMixin, generics.RetrieveAPIView): class EstablishmentRetrieveView(EstablishmentMixin, generics.RetrieveAPIView):
"""Resource for getting a establishment.""" """Resource for getting a establishment."""
serializer_class = serializers.EstablishmentDetailSerializer serializer_class = serializers.EstablishmentDetailSerializer
class EstablishmentTypeListView(JWTGenericViewMixin, generics.ListAPIView): class EstablishmentTypeListView(generics.ListAPIView):
"""Resource for getting a list of establishment types.""" """Resource for getting a list of establishment types."""
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
@ -122,7 +122,7 @@ class EstablishmentFavoritesCreateDestroyView(generics.CreateAPIView, generics.D
return obj return obj
class EstablishmentNearestRetrieveView(EstablishmentMixin, JWTGenericViewMixin, generics.ListAPIView): class EstablishmentNearestRetrieveView(EstablishmentMixin, generics.ListAPIView):
"""Resource for getting list of nearest establishments.""" """Resource for getting list of nearest establishments."""
serializer_class = serializers.EstablishmentListSerializer serializer_class = serializers.EstablishmentListSerializer
filter_class = filters.EstablishmentFilter filter_class = filters.EstablishmentFilter
@ -150,4 +150,42 @@ class EstablishmentTagListView(generics.ListAPIView):
def get_queryset(self): def get_queryset(self):
"""Override get_queryset method""" """Override get_queryset method"""
return MetaDataContent.objects.by_content_type(app_label='establishment', return MetaDataContent.objects.by_content_type(app_label='establishment',
model='establishment') model='establishment')\
.distinct('metadata__label')
class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Establishment schedule RUD view"""
serializer_class = ScheduleRUDSerializer
def get_object(self):
"""
Returns the object the view is displaying.
"""
lookup_url_kwargs = ('pk', 'schedule_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_pk = self.kwargs['pk']
schedule_id = self.kwargs['schedule_id']
establishment = get_object_or_404(klass=models.Establishment.objects.all(),
pk=establishment_pk)
schedule = get_object_or_404(klass=establishment.schedule,
id=schedule_id)
# May raise a permission denied
self.check_object_permissions(self.request, establishment)
self.check_object_permissions(self.request, schedule)
return schedule
class EstablishmentScheduleCreateView(generics.CreateAPIView):
"""Establishment schedule Create view"""
serializer_class = ScheduleCreateSerializer

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-09-12 13:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0013_auto_20190901_1032'),
]
operations = [
migrations.AddField(
model_name='feature',
name='priority',
field=models.IntegerField(default=None, null=True, unique=True),
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-09-18 13:41
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0014_feature_priority'),
('main', '0014_carousel'),
]
operations = [
]

View File

@ -0,0 +1,26 @@
# Generated by Django 2.2.4 on 2019-09-17 13:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('advertisement', '0002_auto_20190917_1307'),
('main', '0014_carousel'),
]
operations = [
migrations.CreateModel(
name='Page',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('page_name', models.CharField(max_length=255, unique=True)),
('advertisements', models.ManyToManyField(to='advertisement.Advertisement')),
],
options={
'verbose_name': 'Page',
'verbose_name_plural': 'Pages',
},
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-09-19 09:54
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0015_page'),
('main', '0015_merge_20190918_1341'),
]
operations = [
]

View File

@ -5,8 +5,11 @@ from django.contrib.postgres.fields import JSONField
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from advertisement.models import Advertisement
from location.models import Country from location.models import Country
from main import methods from main import methods
from review.models import Review
from utils.models import (ProjectBaseMixin, TJSONField, from utils.models import (ProjectBaseMixin, TJSONField,
TranslatedFieldsMixin, ImageMixin) TranslatedFieldsMixin, ImageMixin)
from utils.querysets import ContentTypeQuerySetMixin from utils.querysets import ContentTypeQuerySetMixin
@ -160,6 +163,7 @@ class Feature(ProjectBaseMixin):
"""Feature model.""" """Feature model."""
slug = models.CharField(max_length=255, unique=True) slug = models.CharField(max_length=255, unique=True)
priority = models.IntegerField(unique=True, null=True, default=None)
site_settings = models.ManyToManyField(SiteSettings, through='SiteFeature') site_settings = models.ManyToManyField(SiteSettings, through='SiteFeature')
class Meta: class Meta:
@ -311,6 +315,13 @@ class Carousel(models.Model):
if hasattr(self.content_object, 'awards'): if hasattr(self.content_object, 'awards'):
return self.content_object.awards return self.content_object.awards
@property
def vintage_year(self):
if hasattr(self.content_object, 'reviews'):
review_qs = self.content_object.reviews.by_status(Review.READY)
if review_qs.exists():
return review_qs.last().vintage
@property @property
def toque_number(self): def toque_number(self):
if hasattr(self.content_object, 'toque_number'): if hasattr(self.content_object, 'toque_number'):
@ -323,12 +334,26 @@ class Carousel(models.Model):
@property @property
def image(self): def image(self):
# Check if Generic obj has an image if hasattr(self.content_object.image, 'url'):
if not hasattr(self.content_object.image, 'url'): return self.content_object.image
# Check if Generic obj has a FK to gallery if hasattr(self.content_object.image.image, 'url'):
return self.content_object.image.image.url return self.content_object.image.image
return self.content_object.image.url
@property @property
def model_name(self): def model_name(self):
return self.content_object.__class__.__name__ return self.content_object.__class__.__name__
class Page(models.Model):
"""Page model."""
page_name = models.CharField(max_length=255, unique=True)
advertisements = models.ManyToManyField(Advertisement)
class Meta:
"""Meta class."""
verbose_name = _('Page')
verbose_name_plural = _('Pages')
def __str__(self):
return f'{self.page_name}'

View File

@ -1,6 +1,7 @@
"""Main app serializers.""" """Main app serializers."""
from rest_framework import serializers from rest_framework import serializers
from advertisement.serializers.web import AdvertisementSerializer
from location.serializers import CountrySerializer from location.serializers import CountrySerializer
from main import models from main import models
@ -15,20 +16,23 @@ class FeatureSerializer(serializers.ModelSerializer):
fields = ( fields = (
'id', 'id',
'slug', 'slug',
'priority'
) )
class SiteFeatureSerializer(serializers.ModelSerializer): class SiteFeatureSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(source='feature.id') id = serializers.IntegerField(source='feature.id')
slug = serializers.CharField(source='feature.slug') slug = serializers.CharField(source='feature.slug')
priority = serializers.IntegerField(source='feature.priority')
class Meta: class Meta:
"""Meta class.""" """Meta class."""
model = models.SiteFeature model = models.SiteFeature
fields = ('main', fields = ('main',
'id', 'id',
'slug') 'slug',
'priority'
)
class SiteSettingsSerializer(serializers.ModelSerializer): class SiteSettingsSerializer(serializers.ModelSerializer):
@ -36,7 +40,7 @@ class SiteSettingsSerializer(serializers.ModelSerializer):
published_features = SiteFeatureSerializer(source='published_sitefeatures', published_features = SiteFeatureSerializer(source='published_sitefeatures',
many=True, allow_null=True) many=True, allow_null=True)
#todo: remove this # todo: remove this
country_code = serializers.CharField(source='subdomain', read_only=True) country_code = serializers.CharField(source='subdomain', read_only=True)
class Meta: class Meta:
@ -58,7 +62,6 @@ class SiteSettingsSerializer(serializers.ModelSerializer):
class SiteSerializer(serializers.ModelSerializer): class SiteSerializer(serializers.ModelSerializer):
country = CountrySerializer() country = CountrySerializer()
class Meta: class Meta:
@ -106,7 +109,7 @@ class AwardSerializer(AwardBaseSerializer):
class MetaDataContentSerializer(serializers.ModelSerializer): class MetaDataContentSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(source='metadata.id', read_only=True,) id = serializers.IntegerField(source='metadata.id', read_only=True, )
label_translated = serializers.CharField( label_translated = serializers.CharField(
source='metadata.label_translated', read_only=True, allow_null=True) source='metadata.label_translated', read_only=True, allow_null=True)
@ -120,6 +123,7 @@ class MetaDataContentSerializer(serializers.ModelSerializer):
class CurrencySerializer(serializers.ModelSerializer): class CurrencySerializer(serializers.ModelSerializer):
"""Currency serializer""" """Currency serializer"""
class Meta: class Meta:
model = models.Currency model = models.Currency
fields = [ fields = [
@ -134,8 +138,9 @@ class CarouselListSerializer(serializers.ModelSerializer):
name = serializers.CharField() name = serializers.CharField()
toque_number = serializers.CharField() toque_number = serializers.CharField()
public_mark = serializers.CharField() public_mark = serializers.CharField()
image = serializers.URLField() image = serializers.ImageField()
awards = AwardBaseSerializer(many=True) awards = AwardBaseSerializer(many=True)
vintage_year = serializers.IntegerField()
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -148,4 +153,19 @@ class CarouselListSerializer(serializers.ModelSerializer):
'toque_number', 'toque_number',
'public_mark', 'public_mark',
'image', 'image',
'vintage_year',
]
class PageSerializer(serializers.ModelSerializer):
page_name = serializers.CharField()
advertisements = AdvertisementSerializer(source='advertisements', many=True)
class Meta:
"""Meta class."""
model = models.Carousel
fields = [
'id',
'page_name',
'advertisements'
] ]

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

8
apps/recipe/apps.py Normal file
View File

@ -0,0 +1,8 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class RecipeConfig(AppConfig):
name = 'recipe'
verbose_name = _('RecipeConfig')

View File

@ -0,0 +1,44 @@
# Generated by Django 2.2.4 on 2019-09-19 11:07
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import easy_thumbnails.fields
import utils.methods
import utils.models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Recipe',
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')),
('image', easy_thumbnails.fields.ThumbnailerImageField(blank=True, default=None, null=True, upload_to=utils.methods.image_path, verbose_name='Image')),
('title', utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB": "some text"}', null=True, verbose_name='Title')),
('subtitle', utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB": "some text"}', null=True, verbose_name='Subtitle')),
('description', utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB": "some text"}', null=True, verbose_name='Description')),
('state', models.PositiveSmallIntegerField(choices=[(0, 'Waiting'), (1, 'Hidden'), (2, 'Published'), (3, 'Published exclusive')], default=0, verbose_name='State')),
('author', models.CharField(blank=True, default=None, max_length=255, null=True, verbose_name='Author')),
('published_at', models.DateTimeField(blank=True, default=None, help_text='Published at', null=True, verbose_name='Published at')),
('published_scheduled_at', models.DateTimeField(blank=True, default=None, help_text='Published scheduled at', null=True, verbose_name='Published scheduled at')),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='recipe_records_created', to=settings.AUTH_USER_MODEL, verbose_name='created by')),
('modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='recipe_records_modified', to=settings.AUTH_USER_MODEL, verbose_name='modified by')),
],
options={
'verbose_name': 'Recipe',
'verbose_name_plural': 'Recipes',
},
bases=(utils.models.TranslatedFieldsMixin, models.Model),
),
]

View File

57
apps/recipe/models.py Normal file
View File

@ -0,0 +1,57 @@
"""Recipe app models."""
from django.db import models
from django.utils.translation import ugettext_lazy as _
from utils.models import (TranslatedFieldsMixin, ImageMixin, BaseAttributes,
TJSONField)
class RecipeQuerySet(models.QuerySet):
"""Extended queryset for Recipe model."""
# todo: what records are considered published?
def published(self):
return self.filter(state__in=[self.model.PUBLISHED,
self.model.PUBLISHED_EXCLUSIVE])
class Recipe(TranslatedFieldsMixin, ImageMixin, BaseAttributes):
"""Recipe model."""
WAITING = 0
HIDDEN = 1
PUBLISHED = 2
PUBLISHED_EXCLUSIVE = 3
STATE_CHOICES = (
(WAITING, _('Waiting')),
(HIDDEN, _('Hidden')),
(PUBLISHED, _('Published')),
(PUBLISHED_EXCLUSIVE, _('Published exclusive')),
)
STR_FIELD_NAME = 'title'
title = TJSONField(blank=True, null=True, default=None, verbose_name=_('Title'),
help_text='{"en-GB": "some text"}')
subtitle = TJSONField(blank=True, null=True, default=None, verbose_name=_('Subtitle'),
help_text='{"en-GB": "some text"}')
description = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'),
help_text='{"en-GB": "some text"}')
state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES,
verbose_name=_('State'))
author = models.CharField(max_length=255, blank=True, null=True, default=None,
verbose_name=_('Author'))
published_at = models.DateTimeField(verbose_name=_('Published at'),
blank=True, default=None, null=True,
help_text=_('Published at'))
published_scheduled_at = models.DateTimeField(verbose_name=_('Published scheduled at'),
blank=True, default=None, null=True,
help_text=_('Published scheduled at'))
objects = RecipeQuerySet.as_manager()
class Meta:
"""Meta class."""
verbose_name = _('Recipe')
verbose_name_plural = _('Recipes')

View File

View File

@ -0,0 +1,30 @@
"""Recipe app common serializers."""
from rest_framework import serializers
from recipe import models
class RecipeListSerializer(serializers.ModelSerializer):
"""Serializer for list of recipes."""
title_translated = serializers.CharField(allow_null=True, read_only=True)
subtitle_translated = serializers.CharField(allow_null=True, read_only=True)
in_favorites = serializers.BooleanField()
class Meta:
"""Meta class."""
model = models.Recipe
fields = ('id', 'title_translated', 'subtitle_translated', 'author',
'published_at', 'in_favorites')
read_only_fields = fields
class RecipeDetailSerializer(RecipeListSerializer):
"""Serializer for more information about the recipe."""
description_translated = serializers.CharField(allow_null=True, read_only=True)
class Meta(RecipeListSerializer.Meta):
"""Meta class."""
fields = RecipeListSerializer.Meta.fields + ('description_translated',)

View File

View File

@ -0,0 +1,10 @@
"""Recipe app common urlpatterns."""
from django.urls import path
from recipe.views import common as views
app_name = 'recipe'
urlpatterns = [
path('', views.RecipeListView.as_view(), name='list'),
path('<int:pk>/', views.RecipeDetailView.as_view(), name='detail'),
]

6
apps/recipe/urls/web.py Normal file
View File

@ -0,0 +1,6 @@
"""Recipe app web urlconf."""
from recipe.urls.common import urlpatterns as common_urlpatterns
urlpatterns = []
urlpatterns.extend(common_urlpatterns)

View File

View File

@ -0,0 +1,28 @@
"""Recipe app common views."""
from rest_framework import generics, permissions
from recipe import models
from recipe.serializers import common as serializers
class RecipeViewMixin(generics.GenericAPIView):
"""Recipe view mixin."""
pagination_class = None
permission_classes = (permissions.AllowAny,)
def get_queryset(self):
user = self.request.user
qs = models.Recipe.objects.published().annotate_in_favorites(user)
return qs
class RecipeListView(RecipeViewMixin, generics.ListAPIView):
"""Resource for obtaining a list of recipes."""
serializer_class = serializers.RecipeListSerializer
class RecipeDetailView(RecipeViewMixin, generics.RetrieveAPIView):
"""Resource for detailed recipe information."""
serializer_class = serializers.RecipeDetailSerializer

View File

@ -16,10 +16,28 @@ class EstablishmentDocument(Document):
description = fields.ObjectField(attr='description_indexing', description = fields.ObjectField(attr='description_indexing',
properties=OBJECT_FIELD_PROPERTIES) properties=OBJECT_FIELD_PROPERTIES)
establishment_type = fields.ObjectField(
properties={
'id': fields.IntegerField(),
'name': fields.ObjectField(attr='name_indexing',
properties=OBJECT_FIELD_PROPERTIES)
})
establishment_subtypes = fields.ObjectField(
properties={
'id': fields.IntegerField(),
'name': fields.ObjectField(attr='name_indexing',
properties=OBJECT_FIELD_PROPERTIES)
},
multi=True)
tags = fields.ObjectField( tags = fields.ObjectField(
properties={ properties={
'id': fields.IntegerField(attr='id'), 'id': fields.IntegerField(attr='metadata.id'),
'label': fields.ObjectField(attr='label') 'label': fields.ObjectField(attr='metadata.label_indexing',
properties=OBJECT_FIELD_PROPERTIES),
'category': fields.ObjectField(attr='metadata.category',
properties={
'id': fields.IntegerField(),
})
}, },
multi=True) multi=True)
address = fields.ObjectField( address = fields.ObjectField(
@ -50,6 +68,12 @@ class EstablishmentDocument(Document):
), ),
} }
) )
collections = fields.ObjectField(
properties={
'id': fields.IntegerField(attr='collection.id'),
'collection_type': fields.IntegerField(attr='collection.collection_type'),
},
multi=True)
class Django: class Django:
@ -61,5 +85,5 @@ class EstablishmentDocument(Document):
'price_level', 'price_level',
) )
def prepare_tags(self, instance): def get_queryset(self):
return instance.tags_indexing return super().get_queryset().published()

View File

@ -60,6 +60,9 @@ class EstablishmentDocumentSerializer(DocumentSerializer):
'description_translated', 'description_translated',
'tags', 'tags',
'address', 'address',
'collections',
'establishment_type',
'establishment_subtypes',
) )
@staticmethod @staticmethod

View File

@ -1,7 +1,8 @@
"""Search indexes app views.""" """Search indexes app views."""
from rest_framework import permissions from rest_framework import permissions
from django_elasticsearch_dsl_drf import constants from django_elasticsearch_dsl_drf import constants
from django_elasticsearch_dsl_drf.filter_backends import FilteringFilterBackend from django_elasticsearch_dsl_drf.filter_backends import (FilteringFilterBackend,
GeoSpatialFilteringFilterBackend)
from django_elasticsearch_dsl_drf.viewsets import BaseDocumentViewSet from django_elasticsearch_dsl_drf.viewsets import BaseDocumentViewSet
from django_elasticsearch_dsl_drf.pagination import PageNumberPagination from django_elasticsearch_dsl_drf.pagination import PageNumberPagination
from search_indexes import serializers, filters from search_indexes import serializers, filters
@ -47,6 +48,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
filter_backends = [ filter_backends = [
FilteringFilterBackend, FilteringFilterBackend,
filters.CustomSearchFilterBackend, filters.CustomSearchFilterBackend,
GeoSpatialFilteringFilterBackend,
] ]
search_fields = ( search_fields = (
@ -85,5 +87,29 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
}, },
'country': { 'country': {
'field': 'address.city.country.code' 'field': 'address.city.country.code'
},
'tags_id': {
'field': 'tags.id',
},
'tags_category_id': {
'field': 'tags.category.id',
},
'collection_type': {
'field': 'collections.collection_type'
},
'establishment_type': {
'field': 'establishment_type.id'
},
'establishment_subtypes': {
'field': 'establishment_subtypes.id'
},
}
geo_spatial_filter_fields = {
'location': {
'field': 'address.location',
'lookups': [
constants.LOOKUP_FILTER_GEO_BOUNDING_BOX,
]
} }
} }

View File

@ -0,0 +1,43 @@
# Generated by Django 2.2.4 on 2019-09-19 11:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('timetable', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='timetable',
name='closed_at',
field=models.TimeField(null=True, verbose_name='Closed time'),
),
migrations.AddField(
model_name='timetable',
name='opening_at',
field=models.TimeField(null=True, verbose_name='Opening time'),
),
migrations.AlterField(
model_name='timetable',
name='dinner_end',
field=models.TimeField(null=True, verbose_name='Dinner end time'),
),
migrations.AlterField(
model_name='timetable',
name='dinner_start',
field=models.TimeField(null=True, verbose_name='Dinner start time'),
),
migrations.AlterField(
model_name='timetable',
name='lunch_end',
field=models.TimeField(null=True, verbose_name='Lunch end time'),
),
migrations.AlterField(
model_name='timetable',
name='lunch_start',
field=models.TimeField(null=True, verbose_name='Lunch start time'),
),
]

View File

@ -1,5 +1,6 @@
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from utils.models import ProjectBaseMixin from utils.models import ProjectBaseMixin
@ -23,10 +24,13 @@ class Timetable(ProjectBaseMixin):
(SUNDAY, _('Sunday'))) (SUNDAY, _('Sunday')))
weekday = models.PositiveSmallIntegerField(choices=WEEKDAYS_CHOICES, verbose_name=_('Week day')) weekday = models.PositiveSmallIntegerField(choices=WEEKDAYS_CHOICES, verbose_name=_('Week day'))
lunch_start = models.TimeField(verbose_name=_('Lunch start time'))
lunch_end = models.TimeField(verbose_name=_('Lunch end time')) lunch_start = models.TimeField(verbose_name=_('Lunch start time'), null=True)
dinner_start = models.TimeField(verbose_name=_('Dinner start time')) lunch_end = models.TimeField(verbose_name=_('Lunch end time'), null=True)
dinner_end = models.TimeField(verbose_name=_('Dinner end time')) dinner_start = models.TimeField(verbose_name=_('Dinner start time'), null=True)
dinner_end = models.TimeField(verbose_name=_('Dinner end time'), null=True)
opening_at = models.TimeField(verbose_name=_('Opening time'), null=True)
closed_at = models.TimeField(verbose_name=_('Closed time'), null=True)
class Meta: class Meta:
"""Meta class.""" """Meta class."""

View File

@ -1,16 +1,79 @@
"""Serializer for app timetable""" """Serializer for app timetable"""
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from timetable import models
from establishment.models import Establishment
from timetable.models import Timetable
class TimetableSerializer(serializers.ModelSerializer): class ScheduleRUDSerializer(serializers.ModelSerializer):
"""Serializer for model Timetable""" """Serializer for Establishment model."""
weekday_display = serializers.CharField(source='get_weekday_display',
read_only=True)
lunch_start = serializers.TimeField(required=False)
lunch_end = serializers.TimeField(required=False)
dinner_start = serializers.TimeField(required=False)
dinner_end = serializers.TimeField(required=False)
opening_at = serializers.TimeField(required=False)
closed_at = serializers.TimeField(required=False)
NULLABLE_FIELDS = ['lunch_start', 'lunch_end', 'dinner_start',
'dinner_end', 'opening_at', 'closed_at']
class Meta: class Meta:
"""Meta class.""" """Meta class."""
model = models.Timetable model = Timetable
fields = ( fields = [
'id',
'weekday_display',
'lunch_start',
'lunch_end',
'dinner_start',
'dinner_end',
'opening_at',
'closed_at',
]
def validate(self, attrs):
"""Override validate method"""
establishment_pk = self.context.get('request')\
.parser_context.get('view')\
.kwargs.get('pk')
# Check if establishment exists.
establishment_qs = Establishment.objects.filter(pk=establishment_pk)
if not establishment_qs.exists():
raise serializers.ValidationError({'detail': _('Establishment not found.')})
attrs['establishment'] = establishment_qs.first()
# If fields not in request data then put it in attrs with None value.
if not self.partial:
for field in self.NULLABLE_FIELDS:
if field not in attrs:
attrs.setdefault(field, None)
return attrs
class ScheduleCreateSerializer(ScheduleRUDSerializer):
"""Serializer for Establishment model."""
weekday = serializers.IntegerField(write_only=True)
class Meta:
"""Meta class."""
model = Timetable
fields = ScheduleRUDSerializer.Meta.fields + [
'weekday', 'weekday',
'start', ]
'end',
) def create(self, validated_data):
"""Override create method"""
establishment = validated_data.pop('establishment')
weekday = validated_data.get('weekday')
instance = super().create(validated_data)
schedule_qs = establishment.schedule.filter(weekday=weekday)
if schedule_qs.exists():
schedule_qs.delete()
establishment.schedule.add(instance)
return instance

View File

@ -205,10 +205,12 @@ class PlatformMixin(models.Model):
MOBILE = 0 MOBILE = 0
WEB = 1 WEB = 1
ALL = 2
SOURCES = ( SOURCES = (
(MOBILE, _('Mobile')), (MOBILE, _('Mobile')),
(WEB, _('Web')), (WEB, _('Web')),
(ALL, _('All'))
) )
source = models.PositiveSmallIntegerField(choices=SOURCES, default=MOBILE, source = models.PositiveSmallIntegerField(choices=SOURCES, default=MOBILE,
verbose_name=_('Source')) verbose_name=_('Source'))

View File

@ -63,6 +63,7 @@ PROJECT_APPS = [
'news.apps.NewsConfig', 'news.apps.NewsConfig',
'notification.apps.NotificationConfig', 'notification.apps.NotificationConfig',
'partner.apps.PartnerConfig', 'partner.apps.PartnerConfig',
'recipe.apps.RecipeConfig',
'search_indexes.apps.SearchIndexesConfig', 'search_indexes.apps.SearchIndexesConfig',
'translation.apps.TranslationConfig', 'translation.apps.TranslationConfig',
'configuration.apps.ConfigurationConfig', 'configuration.apps.ConfigurationConfig',

View File

@ -27,6 +27,7 @@ urlpatterns = [
path('partner/', include('partner.urls.web')), path('partner/', include('partner.urls.web')),
path('location/', include('location.urls.web')), path('location/', include('location.urls.web')),
path('main/', include('main.urls')), path('main/', include('main.urls')),
path('recipes/', include('recipe.urls.web')),
path('translation/', include('translation.urls')), path('translation/', include('translation.urls')),
path('comments/', include('comment.urls.web')), path('comments/', include('comment.urls.web')),
path('favorites/', include('favorites.urls')), path('favorites/', include('favorites.urls')),