Merge branch 'develop' into feature/similar-wines

This commit is contained in:
Anatoly 2019-12-10 12:46:44 +03:00
commit 439fb17778
59 changed files with 698 additions and 128 deletions

View File

@ -1,3 +1,3 @@
FROM mdillon/postgis:latest
FROM mdillon/postgis:10
RUN localedef -i ru_RU -c -f UTF-8 -A /usr/share/locale/locale.alias ru_RU.UTF-8
ENV LANG ru_RU.utf8

View File

@ -0,0 +1,20 @@
# Generated by Django 2.2.7 on 2019-12-06 06:55
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('establishment', '0067_auto_20191122_1244'),
('account', '0023_auto_20191204_0916'),
]
operations = [
migrations.AddField(
model_name='role',
name='establishment_subtype',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='establishment.EstablishmentSubType', verbose_name='Establishment subtype'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 2.2.7 on 2019-12-10 06:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0024_role_establishment_subtype'),
]
operations = [
migrations.AddField(
model_name='user',
name='city',
field=models.TextField(blank=True, default=None, null=True, verbose_name='User last visited from city'),
),
migrations.AddField(
model_name='user',
name='locale',
field=models.CharField(blank=True, default=None, max_length=10, null=True, verbose_name='User last used locale'),
),
]

View File

@ -1,5 +1,6 @@
"""Account models"""
from datetime import datetime
from tabnanny import verbose
from django.conf import settings
from django.contrib.auth.models import AbstractUser, UserManager as BaseUserManager
@ -15,7 +16,7 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework.authtoken.models import Token
from authorization.models import Application
from establishment.models import Establishment
from establishment.models import Establishment, EstablishmentSubType
from location.models import Country
from main.models import SiteSettings
from utils.models import GMTokenGenerator
@ -33,7 +34,7 @@ class Role(ProjectBaseMixin):
REVIEWER_MANGER = 6
RESTAURANT_REVIEWER = 7
SALES_MAN = 8
WINERY_REVIEWER = 9
WINERY_REVIEWER = 9 # Establishments subtype "winery"
SELLER = 10
ROLE_CHOICES = (
@ -54,6 +55,9 @@ class Role(ProjectBaseMixin):
null=True, blank=True, on_delete=models.SET_NULL)
site = models.ForeignKey(SiteSettings, verbose_name=_('Site settings'),
null=True, blank=True, on_delete=models.SET_NULL)
establishment_subtype = models.ForeignKey(EstablishmentSubType,
verbose_name=_('Establishment subtype'),
null=True, blank=True, on_delete=models.SET_NULL)
class UserManager(BaseUserManager):
@ -103,6 +107,10 @@ class User(AbstractUser):
email_confirmed = models.BooleanField(_('email status'), default=False)
newsletter = models.NullBooleanField(default=True)
old_id = models.IntegerField(null=True, blank=True, default=None)
locale = models.CharField(max_length=10, blank=True, default=None, null=True,
verbose_name=_('User last used locale'))
city = models.TextField(default=None, blank=True, null=True,
verbose_name=_('User last visited from city'))
EMAIL_FIELD = 'email'
USERNAME_FIELD = 'username'

View File

@ -16,11 +16,31 @@ class RoleSerializer(serializers.ModelSerializer):
class BackUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = '__all__'
fields = (
'id',
'last_login',
'is_superuser',
'username',
'last_name',
'first_name',
'is_active',
'date_joined',
'image_url',
'cropped_image_url',
'email',
'email_confirmed',
'unconfirmed_email',
'email_confirmed',
'newsletter',
'roles',
'password',
'city',
'locale',
)
extra_kwargs = {
'password': {'write_only': True}
'password': {'write_only': True},
}
read_only_fields = ('old_password', 'last_login', 'date_joined')
read_only_fields = ('old_password', 'last_login', 'date_joined', 'city', 'locale')
def create(self, validated_data):
user = super().create(validated_data)

View File

@ -8,6 +8,6 @@ app_name = 'account'
urlpatterns = [
path('role/', views.RoleLstView.as_view(), name='role-list-create'),
path('user-role/', views.UserRoleLstView.as_view(), name='user-role-list-create'),
path('user/', views.UserLstView.as_view(), name='user-list-create'),
path('user/', views.UserLstView.as_view(), name='user-create-list'),
path('user/<int:id>/', views.UserRUDView.as_view(), name='user-rud'),
]

View File

@ -1,5 +1,6 @@
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics, permissions
from rest_framework.filters import OrderingFilter
from account import models
from account.models import User
@ -18,10 +19,10 @@ class UserRoleLstView(generics.ListCreateAPIView):
class UserLstView(generics.ListCreateAPIView):
"""User list create view."""
queryset = User.objects.all()
queryset = User.objects.prefetch_related('roles')
serializer_class = serializers.BackUserSerializer
permission_classes = (permissions.IsAdminUser,)
filter_backends = (DjangoFilterBackend,)
filter_backends = (DjangoFilterBackend, OrderingFilter)
filterset_fields = (
'email_confirmed',
'is_staff',
@ -29,6 +30,14 @@ class UserLstView(generics.ListCreateAPIView):
'is_superuser',
'roles',
)
ordering_fields = (
'email_confirmed',
'is_staff',
'is_active',
'is_superuser',
'roles',
'last_login'
)
class UserRUDView(generics.RetrieveUpdateDestroyAPIView):

View File

@ -22,6 +22,10 @@ class AdvertisementQuerySet(models.QuerySet):
"""Filter Advertisement by page type."""
return self.filter(page_type__name=page_type)
def by_country(self, code: str):
"""Filter Advertisement by country code."""
return self.filter(sites__country__code=code)
def by_locale(self, locale):
"""Filter by locale."""
return self.filter(target_languages__locale=locale)

View File

@ -11,14 +11,11 @@ from main.models import SiteSettings
class AdvertisementBaseSerializer(serializers.ModelSerializer):
"""Base serializer for model Advertisement."""
languages = LanguageSerializer(many=True, read_only=True,
source='target_languages')
target_languages = serializers.PrimaryKeyRelatedField(
queryset=Language.objects.all(),
many=True,
write_only=True
)
sites = SiteShortSerializer(many=True, read_only=True)
target_sites = serializers.PrimaryKeyRelatedField(
queryset=SiteSettings.objects.all(),
many=True,
@ -33,9 +30,7 @@ class AdvertisementBaseSerializer(serializers.ModelSerializer):
'uuid',
'url',
'block_level',
'languages',
'target_languages',
'sites',
'target_sites',
'start',
'end',

View File

@ -28,5 +28,8 @@ class AdvertisementPageTypeListView(AdvertisementBaseView, generics.ListAPIView)
product_type = self.kwargs.get('page_type')
qs = super(AdvertisementPageTypeListView, self).get_queryset()
if product_type:
return qs.by_page_type(product_type)
return qs.by_page_type(product_type) \
.by_country(self.request.country_code) \
.by_locale(self.request.locale) \
.distinct('id')
return qs.none()

View File

@ -7,3 +7,4 @@ class AdvertisementPageTypeWebListView(AdvertisementPageTypeListView):
"""Advertisement mobile list view."""
serializer_class = AdvertisementPageTypeWebListSerializer

View File

View File

@ -0,0 +1,8 @@
from booking.urls import common as common_views
app = 'booking'
urlpatterns_api = []
urlpatterns = urlpatterns_api + \
common_views.urlpatterns

8
apps/booking/urls/web.py Normal file
View File

@ -0,0 +1,8 @@
from booking.urls import common as common_views
app = 'booking'
urlpatterns_api = []
urlpatterns = urlpatterns_api + \
common_views.urlpatterns

View File

@ -87,6 +87,42 @@ class Collection(ProjectBaseMixin, CollectionDateMixin,
verbose_name = _('collection')
verbose_name_plural = _('collections')
@property
def _related_objects(self) -> list:
"""Return list of related objects."""
related_objects = []
# get related objects
for related_object in self._meta.related_objects:
related_objects.append(related_object)
return related_objects
@property
def count_related_objects(self) -> int:
"""Return count of related objects."""
counter = 0
# count of related objects
for related_object in [related_object.name for related_object in self._related_objects]:
counter += getattr(self, f'{related_object}').count()
return counter
@property
def related_object_names(self) -> list:
"""Return related object names."""
raw_object_names = []
for related_object in [related_object.name for related_object in self._related_objects]:
instances = getattr(self, f'{related_object}')
if instances.exists():
for instance in instances.all():
raw_object_names.append(instance.slug if hasattr(instance, 'slug') else None)
# parse slugs
object_names = []
re_pattern = r'[\w]+'
for raw_name in raw_object_names:
result = re.findall(re_pattern, raw_name)
if result: object_names.append(' '.join(result).capitalize())
return set(object_names)
class GuideTypeQuerySet(models.QuerySet):
"""QuerySet for model GuideType."""

View File

@ -19,6 +19,8 @@ class CollectionBackOfficeSerializer(CollectionBaseSerializer):
collection_type_display = serializers.CharField(
source='get_collection_type_display', read_only=True)
country = CountrySimpleSerializer(read_only=True)
count_related_objects = serializers.IntegerField(read_only=True)
related_object_names = serializers.ListField(read_only=True)
class Meta:
model = models.Collection
@ -36,6 +38,8 @@ class CollectionBackOfficeSerializer(CollectionBaseSerializer):
'slug',
'start',
'end',
'count_related_objects',
'related_object_names',
]

View File

@ -498,9 +498,15 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin,
def visible_tags(self):
return super().visible_tags \
.exclude(category__index_name__in=['guide', 'collection', 'purchased_item',
'business_tag', 'business_tags_de', 'tag'])
'business_tag', 'business_tags_de']) \
.exclude(value__in=['rss', 'rss_selection'])
# todo: recalculate toque_number
@property
def visible_tags_detail(self):
"""Removes some tags from detail Establishment representation"""
return self.visible_tags.exclude(category__index_name__in=['tag'])
def recalculate_toque_number(self):
toque_number = 0
if self.address and self.public_mark:
@ -865,25 +871,6 @@ class ContactEmail(models.Model):
return f'{self.email}'
#
# class Wine(TranslatedFieldsMixin, models.Model):
# """Wine model."""
# establishment = models.ForeignKey(
# 'establishment.Establishment', verbose_name=_('establishment'),
# on_delete=models.CASCADE)
# bottles = models.IntegerField(_('bottles'))
# price_min = models.DecimalField(
# _('price min'), max_digits=14, decimal_places=2)
# price_max = models.DecimalField(
# _('price max'), max_digits=14, decimal_places=2)
# by_glass = models.BooleanField(_('by glass'))
# price_glass_min = models.DecimalField(
# _('price min'), max_digits=14, decimal_places=2)
# price_glass_max = models.DecimalField(
# _('price max'), max_digits=14, decimal_places=2)
#
class Plate(TranslatedFieldsMixin, models.Model):
"""Plate model."""
STR_FIELD_NAME = 'name'

View File

@ -232,9 +232,13 @@ class EstablishmentBackOfficeGallerySerializer(serializers.ModelSerializer):
def validate(self, attrs):
"""Override validate method."""
establishment_pk = self.get_request_kwargs().get('pk')
establishment_slug = self.get_request_kwargs().get('slug')
search_kwargs = {'pk': establishment_pk} if establishment_pk else {'slug': establishment_slug}
image_id = self.get_request_kwargs().get('image_id')
establishment_qs = models.Establishment.objects.filter(pk=establishment_pk)
establishment_qs = models.Establishment.objects.filter(**search_kwargs)
image_qs = Image.objects.filter(id=image_id)
if not establishment_qs.exists():

View File

@ -17,7 +17,7 @@ from utils.serializers import ImageBaseSerializer, CarouselCreateSerializer
from utils.serializers import (ProjectModelSerializer, TranslatedField,
FavoritesCreateSerializer)
from location.serializers import EstablishmentWineRegionBaseSerializer, \
EstablishmentWineOriginBaseSerializer
EstablishmentWineOriginBaseSerializer
class ContactPhonesSerializer(serializers.ModelSerializer):
@ -239,6 +239,30 @@ class EstablishmentShortSerializer(serializers.ModelSerializer):
]
class _EstablishmentAddressShortSerializer(serializers.ModelSerializer):
"""Short serializer for establishment."""
city = CitySerializer(source='address.city', allow_null=True)
establishment_type = EstablishmentTypeGeoSerializer()
establishment_subtypes = EstablishmentSubTypeBaseSerializer(many=True)
currency = CurrencySerializer(read_only=True)
address = AddressBaseSerializer(read_only=True)
class Meta:
"""Meta class."""
model = models.Establishment
fields = [
'id',
'name',
'index_name',
'slug',
'city',
'establishment_type',
'establishment_subtypes',
'currency',
'address',
]
class EstablishmentProductShortSerializer(serializers.ModelSerializer):
"""SHORT Serializer for displaying info about an establishment on product page."""
establishment_type = EstablishmentTypeGeoSerializer()
@ -369,7 +393,7 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer):
employees = EstablishmentEmployeeSerializer(source='actual_establishment_employees',
many=True)
address = AddressDetailSerializer(read_only=True)
tags = TagBaseSerializer(read_only=True, many=True, source='visible_tags_detail')
menu = MenuSerializers(source='menu_set', many=True, read_only=True)
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)
@ -426,14 +450,18 @@ class EstablishmentSimilarSerializer(EstablishmentBaseSerializer):
address = AddressDetailSerializer(read_only=True)
schedule = ScheduleRUDSerializer(many=True, allow_null=True)
establishment_type = EstablishmentTypeGeoSerializer()
artisan_category = TagBaseSerializer(many=True, allow_null=True)
type = EstablishmentTypeGeoSerializer(source='establishment_type')
artisan_category = TagBaseSerializer(many=True, allow_null=True, read_only=True)
restaurant_category = TagBaseSerializer(many=True, allow_null=True, read_only=True)
restaurant_cuisine = TagBaseSerializer(many=True, allow_null=True, read_only=True)
class Meta(EstablishmentBaseSerializer.Meta):
fields = EstablishmentBaseSerializer.Meta.fields + [
'schedule',
'establishment_type',
'type',
'artisan_category',
'restaurant_category',
'restaurant_cuisine',
]
@ -522,7 +550,9 @@ class EstablishmentCarouselCreateSerializer(CarouselCreateSerializer):
"""Serializer to carousel object w/ model News."""
def validate(self, attrs):
establishment = models.Establishment.objects.filter(pk=self.pk).first()
search_kwargs = {'pk': self.pk} if self.pk else {'slug': self.slug}
establishment = models.Establishment.objects.filter(**search_kwargs).first()
if not establishment:
raise serializers.ValidationError({'detail': _('Object not found.')})

View File

@ -4,7 +4,8 @@ from account.models import User
from rest_framework import status
from http.cookies import SimpleCookie
from main.models import Currency
from establishment.models import Establishment, EstablishmentType, Menu, SocialChoice, SocialNetwork
from establishment.models import Establishment, EstablishmentType, EstablishmentSubType,\
Menu, SocialChoice, SocialNetwork
# Create your tests here.
from translation.models import Language
from account.models import Role, UserRole
@ -87,7 +88,7 @@ class BaseTestCase(APITestCase):
)
class EstablishmentBTests(BaseTestCase):
class EstablishmentBackTests(BaseTestCase):
def test_establishment_CRUD(self):
params = {'page': 1, 'page_size': 1, }
response = self.client.get('/api/back/establishments/', params, format='json')
@ -104,18 +105,18 @@ class EstablishmentBTests(BaseTestCase):
response = self.client.post('/api/back/establishments/', data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.get(f'/api/back/establishments/{self.establishment.id}/', format='json')
response = self.client.get(f'/api/back/establishments/slug/{self.establishment.slug}/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
update_data = {
'name': 'Test new establishment'
}
response = self.client.patch(f'/api/back/establishments/{self.establishment.id}/',
response = self.client.patch(f'/api/back/establishments/slug/{self.establishment.slug}/',
data=update_data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = self.client.delete(f'/api/back/establishments/{self.establishment.id}/',
response = self.client.delete(f'/api/back/establishments/slug/{self.establishment.slug}/',
format='json')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
@ -372,22 +373,22 @@ class EstablishmentShedulerTests(ChildTestCase):
'weekday': 1
}
response = self.client.post(f'/api/back/establishments/{self.establishment.id}/schedule/', data=data)
response = self.client.post(f'/api/back/establishments/slug/{self.establishment.slug}/schedule/', data=data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
schedule = response.data
response = self.client.get(f'/api/back/establishments/{self.establishment.id}/schedule/{schedule["id"]}/')
response = self.client.get(f'/api/back/establishments/slug/{self.establishment.slug}/schedule/{schedule["id"]}/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
update_data = {
'weekday': 2
}
response = self.client.patch(f'/api/back/establishments/{self.establishment.id}/schedule/{schedule["id"]}/',
response = self.client.patch(f'/api/back/establishments/slug/{self.establishment.slug}/schedule/{schedule["id"]}/',
data=update_data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = self.client.delete(f'/api/back/establishments/{self.establishment.id}/schedule/{schedule["id"]}/')
response = self.client.delete(f'/api/back/establishments/slug/{self.establishment.slug}/schedule/{schedule["id"]}/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
@ -484,8 +485,8 @@ class EstablishmentCarouselTests(ChildTestCase):
"object_id": self.establishment.id
}
response = self.client.post(f'/api/back/establishments/{self.establishment.id}/carousels/', data=data)
response = self.client.post(f'/api/back/establishments/slug/{self.establishment.slug}/carousels/', data=data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.delete(f'/api/back/establishments/{self.establishment.id}/carousels/')
response = self.client.delete(f'/api/back/establishments/slug/{self.establishment.slug}/carousels/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

View File

@ -8,25 +8,25 @@ app_name = 'establishment'
urlpatterns = [
path('', views.EstablishmentListCreateView.as_view(), name='list'),
path('<int:pk>/', views.EstablishmentRUDView.as_view(), name='detail'),
path('<int:pk>/carousels/', views.EstablishmentCarouselCreateDestroyView.as_view(),
path('slug/<slug:slug>/', views.EstablishmentRUDView.as_view(), name='detail'),
path('slug/<slug:slug>/carousels/', views.EstablishmentCarouselCreateDestroyView.as_view(),
name='create-destroy-carousels'),
path('<int:pk>/schedule/<int:schedule_id>/', views.EstablishmentScheduleRUDView.as_view(),
path('slug/<slug:slug>/schedule/<int:schedule_id>/', views.EstablishmentScheduleRUDView.as_view(),
name='schedule-rud'),
path('<int:pk>/schedule/', views.EstablishmentScheduleCreateView.as_view(),
path('slug/<slug:slug>/schedule/', views.EstablishmentScheduleCreateView.as_view(),
name='schedule-create'),
path('<int:pk>/gallery/', views.EstablishmentGalleryListView.as_view(),
path('slug/<slug:slug>/gallery/', views.EstablishmentGalleryListView.as_view(),
name='gallery-list'),
path('<int:pk>/gallery/<int:image_id>/',
path('slug/<slug:slug>/gallery/<int:image_id>/',
views.EstablishmentGalleryCreateDestroyView.as_view(),
name='gallery-create-destroy'),
path('<int:pk>/companies/', views.EstablishmentCompanyListCreateView.as_view(),
path('slug/<slug:slug>/companies/', views.EstablishmentCompanyListCreateView.as_view(),
name='company-list-create'),
path('<int:pk>/companies/<int:company_pk>/', views.EstablishmentCompanyRUDView.as_view(),
path('slug/<slug:slug>/companies/<int:company_pk>/', views.EstablishmentCompanyRUDView.as_view(),
name='company-rud'),
path('<int:pk>/notes/', views.EstablishmentNoteListCreateView.as_view(),
path('slug/<slug:slug>/notes/', views.EstablishmentNoteListCreateView.as_view(),
name='note-list-create'),
path('<int:pk>/notes/<int:note_pk>/', views.EstablishmentNoteRUDView.as_view(),
path('slug/<slug:slug>/notes/<int:note_pk>/', views.EstablishmentNoteRUDView.as_view(),
name='note-rud'),
path('menus/', views.MenuListCreateView.as_view(), name='menu-list'),
path('menus/<int:pk>/', views.MenuRUDView.as_view(), name='menu-rud'),

View File

@ -3,10 +3,9 @@ from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404
from rest_framework import generics, permissions, status
from utils.permissions import IsCountryAdmin, IsEstablishmentManager
from establishment import filters, models, serializers
from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer
from utils.permissions import IsCountryAdmin, IsEstablishmentManager
from utils.permissions import IsCountryAdmin, IsEstablishmentManager, IsWineryReviewer
from utils.views import CreateDestroyGalleryViewMixin
from timetable.models import Timetable
from rest_framework import status
@ -25,31 +24,34 @@ class EstablishmentListCreateView(EstablishmentMixinViews, generics.ListCreateAP
"""Establishment list/create view."""
filter_class = filters.EstablishmentFilter
permission_classes = [IsCountryAdmin | IsEstablishmentManager]
permission_classes = [IsWineryReviewer | IsCountryAdmin | IsEstablishmentManager]
queryset = models.Establishment.objects.all()
serializer_class = serializers.EstablishmentListCreateSerializer
class EstablishmentRUDView(generics.RetrieveUpdateDestroyAPIView):
lookup_field = 'slug'
queryset = models.Establishment.objects.all()
serializer_class = serializers.EstablishmentRUDSerializer
permission_classes = [IsCountryAdmin | IsEstablishmentManager]
permission_classes = [IsWineryReviewer | IsCountryAdmin | IsEstablishmentManager]
class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Establishment schedule RUD view"""
lookup_field = 'slug'
serializer_class = ScheduleRUDSerializer
permission_classes = [IsEstablishmentManager]
permission_classes = [IsWineryReviewer |IsEstablishmentManager]
def get_object(self):
"""
Returns the object the view is displaying.
"""
establishment_pk = self.kwargs['pk']
establishment_slug = self.kwargs['slug']
schedule_id = self.kwargs['schedule_id']
establishment = get_object_or_404(klass=models.Establishment.objects.all(),
pk=establishment_pk)
slug=establishment_slug)
schedule = get_object_or_404(klass=establishment.schedule,
id=schedule_id)
@ -62,23 +64,24 @@ class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView):
class EstablishmentScheduleCreateView(generics.CreateAPIView):
"""Establishment schedule Create view"""
lookup_field = 'slug'
serializer_class = ScheduleCreateSerializer
queryset = Timetable.objects.all()
permission_classes = [IsEstablishmentManager]
permission_classes = [IsWineryReviewer | IsEstablishmentManager]
class MenuListCreateView(generics.ListCreateAPIView):
"""Menu list create view."""
serializer_class = serializers.MenuSerializers
queryset = models.Menu.objects.all()
permission_classes = [IsEstablishmentManager]
permission_classes = [IsWineryReviewer | IsEstablishmentManager]
class MenuRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Menu RUD view."""
serializer_class = serializers.MenuRUDSerializers
queryset = models.Menu.objects.all()
permission_classes = [IsEstablishmentManager]
permission_classes = [IsWineryReviewer | IsEstablishmentManager]
class SocialChoiceListCreateView(generics.ListCreateAPIView):
@ -116,14 +119,14 @@ class PlateListCreateView(generics.ListCreateAPIView):
serializer_class = serializers.PlatesSerializers
queryset = models.Plate.objects.all()
pagination_class = None
permission_classes = [IsEstablishmentManager]
permission_classes = [IsWineryReviewer | IsEstablishmentManager]
class PlateRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Plate RUD view."""
serializer_class = serializers.PlatesSerializers
queryset = models.Plate.objects.all()
permission_classes = [IsEstablishmentManager]
permission_classes = [IsWineryReviewer | IsEstablishmentManager]
class PhonesListCreateView(generics.ListCreateAPIView):
@ -131,14 +134,14 @@ class PhonesListCreateView(generics.ListCreateAPIView):
serializer_class = serializers.ContactPhoneBackSerializers
queryset = models.ContactPhone.objects.all()
pagination_class = None
permission_classes = [IsEstablishmentManager]
permission_classes = [IsWineryReviewer | IsEstablishmentManager]
class PhonesRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Phones RUD view."""
serializer_class = serializers.ContactPhoneBackSerializers
queryset = models.ContactPhone.objects.all()
permission_classes = [IsEstablishmentManager]
permission_classes = [IsWineryReviewer | IsEstablishmentManager]
class EmailListCreateView(generics.ListCreateAPIView):
@ -146,14 +149,14 @@ class EmailListCreateView(generics.ListCreateAPIView):
serializer_class = serializers.ContactEmailBackSerializers
queryset = models.ContactEmail.objects.all()
pagination_class = None
permission_classes = [IsEstablishmentManager]
permission_classes = [IsWineryReviewer | IsEstablishmentManager]
class EmailRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Email RUD view."""
serializer_class = serializers.ContactEmailBackSerializers
queryset = models.ContactEmail.objects.all()
permission_classes = [IsEstablishmentManager]
permission_classes = [IsWineryReviewer | IsEstablishmentManager]
class EmployeeListCreateView(generics.ListCreateAPIView):
@ -210,6 +213,7 @@ class EstablishmentSubtypeRUDView(generics.RetrieveUpdateDestroyAPIView):
class EstablishmentGalleryCreateDestroyView(EstablishmentMixinViews,
CreateDestroyGalleryViewMixin):
"""Resource for a create|destroy gallery for establishment for back-office users."""
lookup_field = 'slug'
serializer_class = serializers.EstablishmentBackOfficeGallerySerializer
def get_object(self):
@ -218,7 +222,7 @@ class EstablishmentGalleryCreateDestroyView(EstablishmentMixinViews,
"""
establishment_qs = self.filter_queryset(self.get_queryset())
establishment = get_object_or_404(establishment_qs, pk=self.kwargs.get('pk'))
establishment = get_object_or_404(establishment_qs, slug=self.kwargs.get('slug'))
gallery = get_object_or_404(establishment.establishment_gallery,
image_id=self.kwargs.get('image_id'))
@ -231,12 +235,13 @@ class EstablishmentGalleryCreateDestroyView(EstablishmentMixinViews,
class EstablishmentGalleryListView(EstablishmentMixinViews,
generics.ListAPIView):
"""Resource for returning gallery for establishment for back-office users."""
lookup_field = 'slug'
serializer_class = serializers.ImageBaseSerializer
def get_object(self):
"""Override get_object method."""
qs = super(EstablishmentGalleryListView, self).get_queryset()
establishment = get_object_or_404(qs, pk=self.kwargs.get('pk'))
establishment = get_object_or_404(qs, slug=self.kwargs.get('slug'))
# May raise a permission denied
self.check_object_permissions(self.request, establishment)
@ -252,6 +257,7 @@ class EstablishmentCompanyListCreateView(EstablishmentMixinViews,
generics.ListCreateAPIView):
"""List|Create establishment company view."""
lookup_field = 'slug'
serializer_class = serializers.EstablishmentCompanyListCreateSerializer
def get_object(self):
@ -259,7 +265,7 @@ class EstablishmentCompanyListCreateView(EstablishmentMixinViews,
establishment_qs = models.Establishment.objects.all()
filtered_ad_qs = self.filter_queryset(establishment_qs)
establishment = get_object_or_404(filtered_ad_qs, pk=self.kwargs.get('pk'))
establishment = get_object_or_404(filtered_ad_qs, slug=self.kwargs.get('slug'))
# May raise a permission denied
self.check_object_permissions(self.request, establishment)
@ -275,6 +281,7 @@ class EstablishmentCompanyRUDView(EstablishmentMixinViews,
generics.RetrieveUpdateDestroyAPIView):
"""Create|Retrieve|Update|Destroy establishment company view."""
lookup_field = 'slug'
serializer_class = serializers.CompanyBaseSerializer
def get_object(self):
@ -282,7 +289,7 @@ class EstablishmentCompanyRUDView(EstablishmentMixinViews,
establishment_qs = models.Establishment.objects.all()
filtered_ad_qs = self.filter_queryset(establishment_qs)
establishment = get_object_or_404(filtered_ad_qs, pk=self.kwargs.get('pk'))
establishment = get_object_or_404(filtered_ad_qs, slug=self.kwargs.get('slug'))
company = get_object_or_404(establishment.companies.all(), pk=self.kwargs.get('company_pk'))
# May raise a permission denied
@ -295,6 +302,7 @@ class EstablishmentNoteListCreateView(EstablishmentMixinViews,
generics.ListCreateAPIView):
"""Retrieve|Update|Destroy establishment note view."""
lookup_field = 'slug'
serializer_class = serializers.EstablishmentNoteListCreateSerializer
def get_object(self):
@ -302,7 +310,7 @@ class EstablishmentNoteListCreateView(EstablishmentMixinViews,
establishment_qs = models.Establishment.objects.all()
filtered_establishment_qs = self.filter_queryset(establishment_qs)
establishment = get_object_or_404(filtered_establishment_qs, pk=self.kwargs.get('pk'))
establishment = get_object_or_404(filtered_establishment_qs, slug=self.kwargs.get('slug'))
# May raise a permission denied
self.check_object_permissions(self.request, establishment)
@ -318,6 +326,7 @@ class EstablishmentNoteRUDView(EstablishmentMixinViews,
generics.RetrieveUpdateDestroyAPIView):
"""Create|Retrieve|Update|Destroy establishment note view."""
lookup_field = 'slug'
serializer_class = serializers.EstablishmentNoteBaseSerializer
def get_object(self):
@ -325,7 +334,7 @@ class EstablishmentNoteRUDView(EstablishmentMixinViews,
establishment_qs = models.Establishment.objects.all()
filtered_establishment_qs = self.filter_queryset(establishment_qs)
establishment = get_object_or_404(filtered_establishment_qs, pk=self.kwargs.get('pk'))
establishment = get_object_or_404(filtered_establishment_qs, slug=self.kwargs.get('slug'))
note = get_object_or_404(establishment.notes.all(), pk=self.kwargs['note_pk'])
# May raise a permission denied

View File

@ -126,10 +126,7 @@ class EstablishmentCommentListView(generics.ListAPIView):
"""Override get_queryset method"""
establishment = get_object_or_404(models.Establishment, slug=self.kwargs['slug'])
return comment_models.Comment.objects.by_content_type(app_label='establishment',
model='establishment') \
.by_object_id(object_id=establishment.pk) \
.order_by('-created')
return establishment.comments.order_by('-created')
class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView):
@ -164,6 +161,7 @@ class EstablishmentFavoritesCreateDestroyView(FavoritesCreateDestroyMixinView):
class EstablishmentCarouselCreateDestroyView(CarouselCreateDestroyMixinView):
"""View for create/destroy establishment from carousel."""
lookup_field = 'slug'
_model = models.Establishment
serializer_class = serializers.EstablishmentCarouselCreateSerializer

View File

@ -30,6 +30,8 @@ class FavoritesEstablishmentListView(generics.ListAPIView):
"""Override get_queryset method"""
return Establishment.objects.filter(favorites__user=self.request.user) \
.order_by('-favorites').with_base_related() \
.with_certain_tag_category_related('category', 'restaurant_category') \
.with_certain_tag_category_related('cuisine', 'restaurant_cuisine') \
.with_certain_tag_category_related('shop_category', 'artisan_category')

24
apps/location/filters.py Normal file
View File

@ -0,0 +1,24 @@
from django.core.validators import EMPTY_VALUES
from django_filters import rest_framework as filters
from location import models
class CityBackFilter(filters.FilterSet):
"""Employee filter set."""
search = filters.CharFilter(method='search_by_name')
class Meta:
"""Meta class."""
model = models.City
fields = (
'search',
)
def search_by_name(self, queryset, name, value):
"""Search by name or last name."""
if value not in EMPTY_VALUES:
return queryset.search_by_name(value)
return queryset

View File

@ -5,6 +5,9 @@ from django.db.models.signals import post_save
from django.db.transaction import on_commit
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from functools import reduce
from typing import List
from translation.models import Language
from utils.models import (ProjectBaseMixin, SVGImageMixin, TJSONField,
@ -92,6 +95,18 @@ class Region(models.Model):
class CityQuerySet(models.QuerySet):
"""Extended queryset for City model."""
def _generic_search(self, value, filter_fields_names: List[str]):
"""Generic method for searching value in specified fields"""
filters = [
{f'{field}__icontains': value}
for field in filter_fields_names
]
return self.filter(reduce(lambda x, y: x | y, [models.Q(**i) for i in filters]))
def search_by_name(self, value):
"""Search by name or last_name."""
return self._generic_search(value, ['name', 'code', 'postal_code'])
def by_country_code(self, code):
"""Return establishments by country code"""
return self.filter(country__code=code)

View File

@ -9,6 +9,8 @@ from rest_framework.permissions import IsAuthenticatedOrReadOnly
from django.shortcuts import get_object_or_404
from utils.serializers import ImageBaseSerializer
from location import filters
# Address
@ -31,6 +33,8 @@ class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView):
"""Create view for model City."""
serializer_class = serializers.CitySerializer
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
queryset = models.City.objects.all()
filter_class = filters.CityBackFilter
class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView):

View File

@ -0,0 +1,32 @@
from django.core.management.base import BaseCommand
from tqdm import tqdm
from main.models import SiteSettings, Footer
from transfer.models import Footers
class Command(BaseCommand):
help = '''Add footers from legacy DB.'''
def handle(self, *args, **kwargs):
objects = []
deleted = 0
footers_list = Footers.objects.all()
for old_footer in tqdm(footers_list, desc='Add footers'):
site = SiteSettings.objects.filter(old_id=old_footer.site_id).first()
if site:
if site.footers.exists():
site.footers.all().delete()
deleted += 1
footer = Footer(
site=site,
about_us=old_footer.about_us,
copyright=old_footer.copyright,
created=old_footer.created_at,
modified=old_footer.updated_at
)
objects.append(footer)
Footer.objects.bulk_create(objects)
self.stdout.write(
self.style.WARNING(f'Created {len(objects)}/Deleted {deleted} footer objects.'))

View File

@ -0,0 +1,37 @@
from django.core.management.base import BaseCommand
from tqdm import tqdm
from establishment.models import Establishment
from main.models import Carousel
from transfer.models import HomePages
from location.models import Country
from django.db.models import F
class Command(BaseCommand):
help = '''Add establishment form HomePage to Carousel!'''
@staticmethod
def get_country(country_code):
return Country.objects.filter(code__iexact=country_code).first()
def handle(self, *args, **kwargs):
objects = []
deleted = 0
hp_list = HomePages.objects.annotate(
country=F('site__country_code_2'),
).all()
for hm in tqdm(hp_list, desc='Add home_page.establishments to carousel'):
est = Establishment.objects.filter(old_id=hm.selection_of_week).first()
if est:
if est.carousels.exists():
est.carousels.all().delete()
deleted += 1
carousel = Carousel(
content_object=est,
country=self.get_country(hm.country)
)
objects.append(carousel)
Carousel.objects.bulk_create(objects)
self.stdout.write(
self.style.WARNING(f'Created {len(objects)}/Deleted {deleted} carousel objects.'))

View File

@ -0,0 +1,29 @@
# Generated by Django 2.2.7 on 2019-12-09 13:21
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('main', '0039_sitefeature_old_id'),
]
operations = [
migrations.CreateModel(
name='Footer',
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')),
('about_us', models.TextField(verbose_name='about_us')),
('copyright', models.TextField(verbose_name='copyright')),
('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='footers', to='main.SiteSettings', verbose_name='footer')),
],
options={
'abstract': False,
},
),
]

View File

@ -351,3 +351,12 @@ class PageType(ProjectBaseMixin):
def __str__(self):
"""Overridden dunder method."""
return self.name
class Footer(ProjectBaseMixin):
site = models.ForeignKey(
'main.SiteSettings', related_name='footers', verbose_name=_('footer'),
on_delete=models.PROTECT
)
about_us = models.TextField(_('about_us'))
copyright = models.TextField(_('copyright'))

View File

@ -22,6 +22,7 @@ class FeatureSerializer(serializers.ModelSerializer):
'site_settings',
)
class CurrencySerializer(ProjectModelSerializer):
"""Currency serializer."""
@ -36,6 +37,33 @@ class CurrencySerializer(ProjectModelSerializer):
]
class FooterSerializer(serializers.ModelSerializer):
"""Footer serializer."""
class Meta:
model = models.Footer
fields = [
'id',
'about_us',
'copyright',
'created',
'modified',
]
class FooterBackSerializer(FooterSerializer):
site_id = serializers.PrimaryKeyRelatedField(
queryset=models.SiteSettings.objects.all(),
source='site'
)
class Meta:
model = models.Footer
fields = FooterSerializer.Meta.fields + [
'site_id'
]
class SiteFeatureSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(source='feature.id')
slug = serializers.CharField(source='feature.slug')
@ -68,6 +96,7 @@ class SiteSettingsSerializer(serializers.ModelSerializer):
country_name = serializers.CharField(source='country.name_translated', read_only=True)
time_format = serializers.CharField(source='country.time_format', read_only=True)
footers = FooterSerializer(many=True, read_only=True)
class Meta:
"""Meta class."""
@ -87,6 +116,7 @@ class SiteSettingsSerializer(serializers.ModelSerializer):
'published_features',
'currency',
'country_name',
'footers',
]

View File

@ -18,6 +18,8 @@ urlpatterns = [
name='site-feature-list-create'),
path('site-feature/<int:id>/', views.SiteFeatureRUDBackView.as_view(),
name='site-feature-rud'),
path('footer/', views.FooterBackView.as_view(), name='footer-list-create'),
path('footer/<int:pk>/', views.FooterRUDBackView.as_view(), name='footer-rud'),
]

View File

@ -4,7 +4,7 @@ from rest_framework import generics, permissions
from main import serializers
from main.filters import AwardFilter
from main.models import Award
from main.models import Award, Footer
from main.views import SiteSettingsView, SiteListView
@ -67,3 +67,17 @@ class SiteSettingsBackOfficeView(SiteSettingsView):
class SiteListBackOfficeView(SiteListView):
"""Site settings View."""
serializer_class = serializers.SiteSerializer
class FooterBackView(generics.ListCreateAPIView):
"""Footer back list/create view."""
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
serializer_class = serializers.FooterBackSerializer
queryset = Footer.objects.all()
class FooterRUDBackView(generics.RetrieveUpdateDestroyAPIView):
"""Footer back RUD view."""
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
serializer_class = serializers.FooterBackSerializer
queryset = Footer.objects.all()

View File

@ -0,0 +1,17 @@
from django.core.management.base import BaseCommand
from news.models import News
import re
class Command(BaseCommand):
help = 'Removes empty img html tags from news description'
relative_img_regex = re.compile(r'\<img.+src=(?!https?:\/\/)([^\/].+?)[\"|\']>', re.I)
def handle(self, *args, **kwargs):
for news in News.objects.all():
if isinstance(news.description, dict):
news.description = {locale: self.relative_img_regex.sub('', rich_text)
for locale, rich_text in news.description.items()}
self.stdout.write(self.style.WARNING(f'Replaced {news} empty img html tags...\n'))
news.save()

View File

@ -8,7 +8,7 @@ from partner.serializers import common as serializers
# Mixins
class PartnerViewMixin(generics.GenericAPIView):
"""View mixin for Partner views"""
queryset = models.Partner.objects.all()
queryset = models.Partner.objects.distinct("name")
# Views

View File

@ -9,6 +9,7 @@ class ProductFilterSet(filters.FilterSet):
"""Product filter set."""
establishment_id = filters.NumberFilter()
current_product = filters.CharFilter(method='without_current_product')
product_type = filters.CharFilter(method='by_product_type')
product_subtype = filters.CharFilter(method='by_product_subtype')
@ -21,6 +22,11 @@ class ProductFilterSet(filters.FilterSet):
'product_subtype',
]
def without_current_product(self, queryset, name, value):
if value not in EMPTY_VALUES:
return queryset.without_current_product(value)
return queryset
def by_product_type(self, queryset, name, value):
if value not in EMPTY_VALUES:
return queryset.by_product_type(value)

View File

@ -7,7 +7,7 @@ from tqdm import tqdm
class Command(BaseCommand):
help = '''Add add product tags networks from old db to new db.
help = '''Add product tags networks from old db to new db.
Run after add_product!!!'''
def category_sql(self):
@ -101,14 +101,12 @@ class Command(BaseCommand):
p.tags.clear()
print('End clear tags product')
def remove_tags(self):
print('Begin delete many tags')
Tag.objects.\
filter(news__isnull=True, establishments__isnull=True).delete()
print('End delete many tags')
def product_sql(self):
with connections['legacy'].cursor() as cursor:
cursor.execute('''

View File

@ -0,0 +1,20 @@
from django.core.management.base import BaseCommand
from tag.models import Tag, TagCategory
from tqdm import tqdm
class Command(BaseCommand):
help = '''Check product serial number from old db to new db.
Run after add_product_tag!!!'''
def check_serial_number(self):
category = TagCategory.objects.get(index_name='serial_number')
tags = Tag.objects.filter(category=category, products__isnull=False)
for tag in tqdm(tags, desc='Update serial number for product'):
tag.products.all().update(serial_number=tag.value)
tag.products.clear()
self.stdout.write(self.style.WARNING(f'Check serial number product end.'))
def handle(self, *args, **kwargs):
self.check_serial_number()

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-11-21 09:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0018_purchasedproduct'),
]
operations = [
migrations.AddField(
model_name='product',
name='serial_number',
field=models.CharField(default=None, max_length=255, null=True, verbose_name='Serial number'),
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.7 on 2019-12-09 09:11
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('product', '0019_auto_20191204_1420'),
('product', '0019_product_serial_number'),
]
operations = [
]

View File

@ -102,6 +102,11 @@ class ProductQuerySet(models.QuerySet):
def wines(self):
return self.filter(type__index_name__icontains=ProductType.WINE)
def without_current_product(self, current_product: str):
"""Exclude by current product."""
kwargs = {'pk': int(current_product)} if current_product.isdigit() else {'slug': current_product}
return self.exclude(**kwargs)
def by_product_type(self, product_type: str):
"""Filter by type."""
return self.filter(product_type__index_name__icontains=product_type)
@ -213,6 +218,10 @@ class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes,
comments = generic.GenericRelation(to='comment.Comment')
awards = generic.GenericRelation(to='main.Award', related_query_name='product')
serial_number = models.CharField(max_length=255,
default=None, null=True,
verbose_name=_('Serial number'))
objects = ProductManager.from_queryset(ProductQuerySet)()
class Meta:

View File

@ -4,15 +4,16 @@ from rest_framework import serializers
from comment.models import Comment
from comment.serializers import CommentSerializer
from establishment.serializers import EstablishmentShortSerializer, EstablishmentProductSerializer, EstablishmentProductShortSerializer
from gallery.models import Image
from establishment.serializers import EstablishmentProductShortSerializer
from establishment.serializers.common import _EstablishmentAddressShortSerializer
from location.serializers import WineOriginRegionBaseSerializer,\
WineOriginBaseSerializer, EstablishmentWineOriginBaseSerializer
from main.serializers import AwardSerializer
from product import models
from review.serializers import ReviewShortSerializer
from tag.serializers import TagBaseSerializer, TagCategoryProductSerializer
from utils import exceptions as utils_exceptions
from utils.serializers import TranslatedField, FavoritesCreateSerializer, ImageBaseSerializer
from main.serializers import AwardSerializer
from location.serializers import WineOriginRegionBaseSerializer, WineOriginBaseSerializer
from tag.serializers import TagBaseSerializer, TagCategoryProductSerializer
class ProductTagSerializer(TagBaseSerializer):
@ -95,6 +96,7 @@ class ProductBaseSerializer(serializers.ModelSerializer):
preview_image_url = serializers.URLField(allow_null=True,
read_only=True)
in_favorites = serializers.BooleanField(allow_null=True)
wine_origins = EstablishmentWineOriginBaseSerializer(many=True, read_only=True)
class Meta:
"""Meta class."""
@ -113,13 +115,14 @@ class ProductBaseSerializer(serializers.ModelSerializer):
'wine_regions',
'wine_colors',
'in_favorites',
'wine_origins',
]
class ProductDetailSerializer(ProductBaseSerializer):
"""Product detail serializer."""
description_translated = TranslatedField()
establishment_detail = EstablishmentShortSerializer(source='establishment', read_only=True)
establishment_detail = _EstablishmentAddressShortSerializer(source='establishment', read_only=True)
review = ReviewShortSerializer(source='last_published_review', read_only=True)
awards = AwardSerializer(many=True, read_only=True)
classifications = ProductClassificationBaseSerializer(many=True, read_only=True)

View File

@ -60,10 +60,7 @@ class ProductCommentListView(generics.ListAPIView):
def get_queryset(self):
"""Override get_queryset method"""
product = get_object_or_404(Product, slug=self.kwargs['slug'])
return Comment.objects.by_content_type(app_label='product',
model='product') \
.by_object_id(object_id=product.pk) \
.order_by('-created')
return product.comments.order_by('-created')
class ProductCommentRUDView(generics.RetrieveUpdateDestroyAPIView):

View File

@ -2,6 +2,7 @@
from django.conf import settings
from django_elasticsearch_dsl import Document, Index, fields
from tag import models
from news.models import News
TagCategoryIndex = Index(settings.ELASTICSEARCH_INDEX_NAMES.get(__name__, 'tag_category'))
TagCategoryIndex.settings(number_of_shards=2, number_of_replicas=2)
@ -26,8 +27,16 @@ class TagCategoryDocument(Document):
'public',
'value_type'
)
related_models = [models.Tag]
related_models = [models.Tag, News]
def get_queryset(self):
return super().get_queryset().with_base_related()
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.
The related_models option should be used with caution because it can lead in the index
to the updating of a lot of items.
"""
if isinstance(related_instance, News):
return related_instance.tags

View File

@ -243,8 +243,8 @@ class WineOriginSerializer(serializers.Serializer):
class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer):
"""Establishment document serializer."""
establishment_type = EstablishmentTypeSerializer()
establishment_subtypes = EstablishmentTypeSerializer(many=True)
type = EstablishmentTypeSerializer(source='establishment_type')
subtypes = EstablishmentTypeSerializer(many=True, source='establishment_subtypes')
address = AddressDocumentSerializer(allow_null=True)
tags = TagsDocumentSerializer(many=True, source='visible_tags')
restaurant_category = TagsDocumentSerializer(many=True, allow_null=True)
@ -280,8 +280,8 @@ class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer):
'wine_origins',
# 'works_now',
# 'collections',
# 'establishment_type',
# 'establishment_subtypes',
'type',
'subtypes',
)

View File

@ -45,8 +45,14 @@ class ScheduleRUDSerializer(serializers.ModelSerializer):
.parser_context.get('view')\
.kwargs.get('pk')
establishment_slug = self.context.get('request')\
.parser_context.get('view')\
.kwargs.get('slug')
search_kwargs = {'pk': establishment_pk} if establishment_pk else {'slug': establishment_slug}
# Check if establishment exists.
establishment_qs = Establishment.objects.filter(pk=establishment_pk)
establishment_qs = Establishment.objects.filter(**search_kwargs)
if not establishment_qs.exists():
raise serializers.ValidationError({'detail': _('Establishment not found.')})
attrs['establishment'] = establishment_qs.first()

View File

@ -1000,7 +1000,7 @@ class ProductNotes(MigrateMixin):
db_table = 'product_notes'
class HomePages(models.Model):
class HomePages(MigrateMixin):
using = 'legacy'
site = models.ForeignKey(Sites, models.DO_NOTHING, blank=True, null=True)
@ -1208,3 +1208,17 @@ class NewsletterSubscriber(MigrateMixin):
class Meta:
managed = False
db_table = 'newsletter_subscriptions'
class Footers(MigrateMixin):
using = 'legacy'
about_us = models.TextField(blank=True, null=True)
copyright = models.TextField(blank=True, null=True)
site = models.ForeignKey('Sites', models.DO_NOTHING, blank=True, null=True)
created_at = models.DateTimeField()
updated_at = models.DateTimeField()
class Meta:
managed = False
db_table = 'footers'

View File

@ -68,7 +68,7 @@ class GuideSerializer(TransferSerializerMixin):
class GuideFilterSerializer(TransferSerializerMixin):
id = serializers.IntegerField()
year = serializers.CharField(allow_null=True)
establishment_type = serializers.CharField(allow_null=True)
type = serializers.CharField(allow_null=True, source='establishment_type')
countries = serializers.CharField(allow_null=True)
regions = serializers.CharField(allow_null=True)
subregions = serializers.CharField(allow_null=True)
@ -86,7 +86,7 @@ class GuideFilterSerializer(TransferSerializerMixin):
fields = (
'id',
'year',
'establishment_type',
'type',
'countries',
'regions',
'subregions',

View File

@ -1,7 +1,9 @@
"""Custom middleware."""
from django.utils import translation
"""Custom middlewares."""
from django.utils import translation, timezone
from account.models import User
from configuration.models import TranslationSettings
from main.methods import determine_user_city
from translation.models import Language
@ -12,6 +14,18 @@ def get_locale(cookie_dict):
def get_country_code(cookie_dict):
return cookie_dict.get('country_code')
def user_last_visit(get_response):
"""Updates user last visit w/ current"""
def middleware(request):
response = get_response(request)
if request.user.is_authenticated:
User.objects.filter(pk=request.user.pk).update(**{
'last_login': timezone.now(),
'locale': request.locale,
'city': determine_user_city(request),
})
return response
return middleware
def parse_cookies(get_response):
"""Parse cookies."""

View File

@ -7,7 +7,8 @@ from rest_framework_simplejwt.tokens import AccessToken
from account.models import UserRole, Role
from authorization.models import JWTRefreshToken
from utils.tokens import GMRefreshToken
from establishment.models import EstablishmentSubType
from location.models import Address
class IsAuthenticatedAndTokenIsValid(permissions.BasePermission):
"""
@ -56,8 +57,9 @@ class IsGuest(permissions.IsAuthenticatedOrReadOnly):
"""
Object-level permission to only allow owners of an object to edit it.
"""
SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')
def has_permission(self, request, view):
rules = [
request.user.is_superuser,
request.method in permissions.SAFE_METHODS
@ -306,7 +308,6 @@ class IsEstablishmentManager(IsStandardUser):
rules = [
# special!
super().has_permission(request, view)
# super().has_object_permission(request, view, obj)
]
role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER) \
@ -319,7 +320,6 @@ class IsEstablishmentManager(IsStandardUser):
).exists(),
# special!
super().has_permission(request, view)
# super().has_object_permission(request, view, obj)
]
return any(rules)
@ -368,7 +368,7 @@ class IsRestaurantReviewer(IsStandardUser):
# and request.user.email_confirmed,
if hasattr(request.data, 'user') and hasattr(request.data, 'object_id'):
role = Role.objects.filter(role=Role.RESTAURANT_REVIEWER) \
.first() # 'Comments moderator'
.first()
rules = [
UserRole.objects.filter(user=request.user, role=role,
@ -394,3 +394,58 @@ class IsRestaurantReviewer(IsStandardUser):
]
return any(rules)
class IsWineryReviewer(IsStandardUser):
def has_permission(self, request, view):
rules = [
super().has_permission(request, view)
]
if 'type_id' in request.data and 'address_id' in request.data and request.user:
countries = Address.objects.filter(id=request.data['address_id'])
est = EstablishmentSubType.objects.filter(establishment_type_id=request.data['type_id'])
if est.exists():
role = Role.objects.filter(establishment_subtype_id__in=[type.id for type in est],
role=Role.WINERY_REVIEWER,
country_id__in=[country.id for country in countries]) \
.first()
rules.append(
UserRole.objects.filter(user=request.user, role=role).exists()
)
return any(rules)
def has_object_permission(self, request, view, obj):
rules = [
super().has_object_permission(request, view, obj)
]
if hasattr(obj, 'type_id') or hasattr(obj, 'establishment_type_id'):
type_id: int
if hasattr(obj, 'type_id'):
type_id = obj.type_id
else:
type_id = obj.establishment_type_id
est = EstablishmentSubType.objects.filter(establishment_type_id=type_id)
role = Role.objects.filter(role=Role.WINERY_REVIEWER,
establishment_subtype_id__in=[id for type.id in est],
country_id=obj.country_id).first()
object_id: int
if hasattr(obj, 'object_id'):
object_id = obj.object_id
else:
object_id = obj.establishment_id
rules = [
UserRole.objects.filter(user=request.user, role=role,
establishment_id=object_id
).exists(),
super().has_object_permission(request, view, obj)
]
return any(rules)

View File

@ -118,6 +118,10 @@ class CarouselCreateSerializer(serializers.ModelSerializer):
def pk(self):
return self.request.parser_context.get('kwargs').get('pk')
@property
def slug(self):
return self.request.parser_context.get('kwargs').get('slug')
class RecursiveFieldSerializer(serializers.Serializer):
def to_representation(self, value):

View File

@ -158,7 +158,11 @@ class CarouselCreateDestroyMixinView(BaseCreateDestroyMixinView):
lookup_field = 'id'
def get_base_object(self):
return get_object_or_404(self._model, id=self.kwargs['pk'])
establishment_pk = self.kwargs.get('pk')
establishment_slug = self.kwargs.get('slug')
search_kwargs = {'id': establishment_pk} if establishment_pk else {'slug': establishment_slug}
return get_object_or_404(self._model, **search_kwargs)
def get_object(self):
"""

View File

@ -13,6 +13,9 @@ services:
MYSQL_ROOT_PASSWORD: rootPassword
volumes:
- gm-mysql_db:/var/lib/mysql
- .:/code
# PostgreSQL database
@ -29,6 +32,7 @@ services:
- "5436:5432"
volumes:
- gm-db:/var/lib/postgresql/data/
- .:/code
elasticsearch:

View File

@ -2,14 +2,21 @@
./manage.py transfer -a
./manage.py transfer -d
./manage.py transfer -e
./manage.py transfer -n
./manage.py rm_empty_images # команда для удаления картинок с относительным урлом из news.description
./manage.py upd_transportation
./manage.py transfer --fill_city_gallery
./manage.py transfer -l
./manage.py transfer --product
# Утеряна четкая связь между последовательностью миграций для импорта тегов продуктов,
# что может привести к удалению уже импортированных тегов командой выше.
./manage.py transfer --souvenir
./manage.py transfer --establishment_note
./manage.py transfer --product_note
./manage.py transfer --check_serial_number
./manage.py transfer --wine_characteristics
./manage.py transfer --inquiries
./manage.py transfer --assemblage
./manage.py transfer --purchased_plaques
./manage.py transfer --purchased_plaques
./manage.py rm_empty_images
./manage.py add_artisan_subtype # добавляет подтипы для заведений артизанов

View File

@ -118,6 +118,7 @@ MIDDLEWARE = [
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'utils.middleware.parse_cookies',
'utils.middleware.user_last_visit',
]
ROOT_URLCONF = 'project.urls'
@ -414,10 +415,10 @@ SORL_THUMBNAIL_ALIASES = {
SIMPLE_JWT = {
# 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
'ACCESS_TOKEN_LIFETIME': timedelta(days=182),
'ACCESS_TOKEN_LIFETIME_SECONDS': 15770000, # 6 months
'REFRESH_TOKEN_LIFETIME': timedelta(days=182),
'REFRESH_TOKEN_LIFETIME_SECONDS': 15770000, # 6 months
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
@ -453,7 +454,7 @@ NOTIFICATION_PASSWORD_TEMPLATE = 'account/password_change_email.html'
# COOKIES
COOKIES_MAX_AGE = 2628000 # 30 days
COOKIES_MAX_AGE = 15730000 # 6 months
SESSION_COOKIE_SAMESITE = None
@ -524,3 +525,6 @@ INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next']
THUMBNAIL_ENGINE = 'utils.thumbnail_engine.GMEngine'
COOKIE_DOMAIN = None
ELASTICSEARCH_DSL = {}
ELASTICSEARCH_INDEX_NAMES = {}

View File

@ -3,6 +3,7 @@ from django.urls import path, include
app_name = 'mobile'
urlpatterns = [
path('booking/', include('booking.urls.web')),
path('establishments/', include('establishment.urls.mobile')),
path('location/', include('location.urls.mobile')),
path('main/', include('main.urls.mobile')),

View File

@ -19,7 +19,7 @@ app_name = 'web'
urlpatterns = [
path('account/', include('account.urls.web')),
path('booking/', include('booking.urls')),
path('booking/', include('booking.urls.web')),
path('re_blocks/', include('advertisement.urls.web')),
path('collections/', include('collection.urls.web')),
path('establishments/', include('establishment.urls.web')),