From 2a8ffb16dea49f6048e3daa97b810eaeceefa79a Mon Sep 17 00:00:00 2001 From: evgeniy-st Date: Fri, 30 Aug 2019 15:40:53 +0300 Subject: [PATCH 1/9] Notification app & subscription --- apps/notification/__init__.py | 0 apps/notification/admin.py | 3 + apps/notification/apps.py | 7 ++ apps/notification/migrations/0001_initial.py | 37 ++++++ apps/notification/migrations/__init__.py | 0 apps/notification/models.py | 124 +++++++++++++++++++ apps/notification/serializers/__init__.py | 0 apps/notification/serializers/common.py | 47 +++++++ apps/notification/urls/__init__.py | 0 apps/notification/urls/common.py | 12 ++ apps/notification/urls/web.py | 7 ++ apps/notification/views/__init__.py | 0 apps/notification/views/common.py | 78 ++++++++++++ apps/utils/methods.py | 18 +++ project/settings/base.py | 5 +- project/settings/development.py | 1 + project/settings/local.py | 5 - project/urls/web.py | 1 + 18 files changed, 339 insertions(+), 6 deletions(-) create mode 100644 apps/notification/__init__.py create mode 100644 apps/notification/admin.py create mode 100644 apps/notification/apps.py create mode 100644 apps/notification/migrations/0001_initial.py create mode 100644 apps/notification/migrations/__init__.py create mode 100644 apps/notification/models.py create mode 100644 apps/notification/serializers/__init__.py create mode 100644 apps/notification/serializers/common.py create mode 100644 apps/notification/urls/__init__.py create mode 100644 apps/notification/urls/common.py create mode 100644 apps/notification/urls/web.py create mode 100644 apps/notification/views/__init__.py create mode 100644 apps/notification/views/common.py diff --git a/apps/notification/__init__.py b/apps/notification/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/notification/admin.py b/apps/notification/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/apps/notification/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/notification/apps.py b/apps/notification/apps.py new file mode 100644 index 00000000..2035ffa3 --- /dev/null +++ b/apps/notification/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig +from django.utils.translation import ugettext_lazy as _ + + +class NotificationConfig(AppConfig): + name = 'notification' + verbose_name = _('notification') diff --git a/apps/notification/migrations/0001_initial.py b/apps/notification/migrations/0001_initial.py new file mode 100644 index 00000000..6cdcf6f2 --- /dev/null +++ b/apps/notification/migrations/0001_initial.py @@ -0,0 +1,37 @@ +# Generated by Django 2.2.4 on 2019-08-30 11:22 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Subscriber', + 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')), + ('email', models.EmailField(blank=True, default=None, max_length=254, null=True, unique=True, verbose_name='Email')), + ('ip_address', models.GenericIPAddressField(blank=True, default=None, null=True, verbose_name='IP address')), + ('country_code', models.CharField(blank=True, default=None, max_length=10, null=True, verbose_name='Country code')), + ('locale', models.CharField(blank=True, default=None, max_length=10, null=True, verbose_name='Locale identifier')), + ('state', models.PositiveIntegerField(choices=[(0, 'Unusable'), (1, 'Usable')], default=1, verbose_name='State')), + ('update_code', models.CharField(blank=True, db_index=True, default=None, max_length=254, null=True, verbose_name='Token')), + ('user', models.OneToOneField(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='subscriber', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'verbose_name': 'Subscriber', + 'verbose_name_plural': 'Subscribers', + }, + ), + ] diff --git a/apps/notification/migrations/__init__.py b/apps/notification/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/notification/models.py b/apps/notification/models.py new file mode 100644 index 00000000..85176d24 --- /dev/null +++ b/apps/notification/models.py @@ -0,0 +1,124 @@ +"""Notification app models.""" +from django.conf import settings +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from account.models import User +from utils.methods import generate_string_code +from utils.models import ProjectBaseMixin + + +# todo: associate user & subscriber after users registration +class SubscriberManager(models.Manager): + """Extended manager for Subscriber model.""" + + def make_subscriber(self, email=None, user=None, ip_address=None, country_code=None, + locale=None, *args, **kwargs): + """Make subscriber and update info.""" + # search existing object + if not user: + user = User.objects.filter(email=email).first() + if user: + obj = self.model.objects.filter(models.Q(user=user) | models.Q( + email=user.email)).first() + else: + obj = self.model.objects.filter(email=email).first() + + # update or create + if obj: + if user: + obj.user = user + obj.email = None + else: + obj.email = email + obj.ip_address = ip_address + obj.country_code = country_code + obj.locale = locale + obj.state = self.model.USABLE + obj.update_code = generate_string_code() + obj.save() + else: + obj = self.model.objects.create(user=user, email=email, ip_address=ip_address, + country_code=country_code, locale=locale) + return obj + + def associate_user(self, user): + """Associate user.""" + obj = self.model.objects.filter(user=user).first() + if obj is None: + obj = self.model.objects.filter(email=user.email_confirmed, user__isnull=True).first() + if obj: + obj.user = user + obj.email = None + obj.save() + return obj + + +class SubscriberQuerySet(models.QuerySet): + """Extended queryset for Subscriber model.""" + + def by_usable(self, switcher=True): + if switcher: + return self.filter(state=self.model.USABLE) + else: + return self.filter(state=self.model.UNUSABLE) + + +class Subscriber(ProjectBaseMixin): + """Subscriber model.""" + + UNUSABLE = 0 + USABLE = 1 + + STATE_CHOICES = ( + (UNUSABLE, _('Unusable')), + (USABLE, _('Usable')), + ) + + user = models.OneToOneField(User, blank=True, null=True, default=None, + on_delete=models.SET_NULL, related_name='subscriber', + verbose_name=_('User')) + email = models.EmailField(blank=True, null=True, default=None, unique=True, + verbose_name=_('Email')) + ip_address = models.GenericIPAddressField(blank=True, null=True, default=None, + verbose_name=_('IP address')) + country_code = models.CharField(max_length=10, blank=True, null=True, default=None, + verbose_name=_('Country code')) + locale = models.CharField(blank=True, null=True, default=None, + max_length=10, verbose_name=_('Locale identifier')) + state = models.PositiveIntegerField(choices=STATE_CHOICES, default=USABLE, + verbose_name=_('State')) + update_code = models.CharField(max_length=254, blank=True, null=True, default=None, + db_index=True, verbose_name=_('Token')) + + objects = SubscriberManager.from_queryset(SubscriberQuerySet)() + + class Meta: + """Meta class.""" + + verbose_name = _('Subscriber') + verbose_name_plural = _('Subscribers') + + def save(self, *args, **kwargs): + """Overrided save method.""" + if self.update_code is None: + self.update_code = generate_string_code() + return super(Subscriber, self).save(*args, **kwargs) + + def unsubscribe(self): + """Unsubscribe user.""" + self.state = self.UNUSABLE + self.save() + + @property + def send_to(self): + """Actual email.""" + return self.user.email if self.user else self.email + + @property + def link_to_unsubscribe(self): + """Link to unsubscribe.""" + schema = settings.SCHEMA_URI + site_domain = settings.SITE_DOMAIN_URI + url = settings.SITE_REDIRECT_URL_UNSUBSCRIBE + query = f'?code={self.update_code}' + return f'{schema}://{site_domain}{url}{query}' diff --git a/apps/notification/serializers/__init__.py b/apps/notification/serializers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/notification/serializers/common.py b/apps/notification/serializers/common.py new file mode 100644 index 00000000..a4d85d1f --- /dev/null +++ b/apps/notification/serializers/common.py @@ -0,0 +1,47 @@ +"""Notification app serializers.""" +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers +from notification import models +from utils.methods import get_user_ip + + +class SubscribeSerializer(serializers.ModelSerializer): + """Subscribe serializer.""" + + email = serializers.EmailField(required=False, source='send_to') + state_display = serializers.CharField(source='get_state_display', read_only=True) + + class Meta: + """Meta class.""" + + model = models.Subscriber + fields = ('email', 'state', 'state_display') + read_only_fields = ('state', 'state_display') + + def validate(self, attrs): + """Validate attrs.""" + request = self.context.get('request') + user = request.user + + # validate email + email = attrs.get('send_to') + if user.is_authenticated: + if email is not None and email != user.email: + raise serializers.ValidationError(_('Does not match user email')) + else: + if email is None: + raise serializers.ValidationError({'email': _('This field is required.')}) + + # append info + attrs['email'] = email + attrs['country_code'] = request.country_code + attrs['locale'] = request.locale + attrs['ip_address'] = get_user_ip(request) + if user.is_authenticated: + attrs['user'] = user + return attrs + + def create(self, validated_data): + """Create obj.""" + obj = models.Subscriber.objects.make_subscriber(**validated_data) + return obj diff --git a/apps/notification/urls/__init__.py b/apps/notification/urls/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/notification/urls/common.py b/apps/notification/urls/common.py new file mode 100644 index 00000000..df43c805 --- /dev/null +++ b/apps/notification/urls/common.py @@ -0,0 +1,12 @@ +"""Notification app common urlconf.""" +from django.urls import path +from notification.views import common + + +urlpatterns = [ + path('subscribe/', common.SubscribeView.as_view(), name='subscribe'), + path('subscribe-info/', common.SubscribeInfoAuthUserView.as_view(), name='check-code-auth'), + path('subscribe-info//', common.SubscribeInfoView.as_view(), name='check-code'), + path('unsubscribe/', common.UnsubscribeAuthUserView.as_view(), name='unsubscribe-auth'), + path('unsubscribe//', common.UnsubscribeView.as_view(), name='unsubscribe'), +] \ No newline at end of file diff --git a/apps/notification/urls/web.py b/apps/notification/urls/web.py new file mode 100644 index 00000000..4edbd858 --- /dev/null +++ b/apps/notification/urls/web.py @@ -0,0 +1,7 @@ +"""Establishment app web urlconf.""" +from notification.urls.common import urlpatterns as common_urlpatterns + + +urlpatterns = [] + +urlpatterns.extend(common_urlpatterns) diff --git a/apps/notification/views/__init__.py b/apps/notification/views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/notification/views/common.py b/apps/notification/views/common.py new file mode 100644 index 00000000..ee133fa3 --- /dev/null +++ b/apps/notification/views/common.py @@ -0,0 +1,78 @@ +"""Notification app common views.""" +from django.shortcuts import get_object_or_404 +from rest_framework import generics, permissions +from rest_framework.response import Response +from notification import models, throttling +from notification.serializers import common as serializers + + +class SubscribeView(generics.GenericAPIView): + """Subscribe View.""" + + queryset = models.Subscriber.objects.all() + permission_classes = (permissions.AllowAny, ) + serializer_class = serializers.SubscribeSerializer + + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(data=serializer.data) + + +class SubscribeInfoView(generics.RetrieveAPIView): + """Subscribe info view.""" + + lookup_field = 'update_code' + lookup_url_kwarg = 'code' + permission_classes = (permissions.AllowAny, ) + queryset = models.Subscriber.objects.all() + serializer_class = serializers.SubscribeSerializer + + +class SubscribeInfoAuthUserView(generics.RetrieveAPIView): + """Subscribe info auth user view.""" + + permission_classes = (permissions.IsAuthenticated, ) + queryset = models.Subscriber.objects.all() + serializer_class = serializers.SubscribeSerializer + + def get_object(self): + user = self.request.user + queryset = self.filter_queryset(self.get_queryset()) + filter_kwargs = {'user': user} + obj = get_object_or_404(queryset, **filter_kwargs) + self.check_object_permissions(self.request, obj) + return obj + + +class UnsubscribeView(generics.GenericAPIView): + """Unsubscribe view.""" + + lookup_field = 'update_code' + lookup_url_kwarg = 'code' + permission_classes = (permissions.AllowAny, ) + queryset = models.Subscriber.objects.all() + serializer_class = serializers.SubscribeSerializer + + def patch(self, request, *args, **kw): + obj = self.get_object() + obj.unsubscribe() + serializer = self.get_serializer(instance=obj) + return Response(data=serializer.data) + + +class UnsubscribeAuthUserView(generics.GenericAPIView): + """Unsubscribe auth user view.""" + + permission_classes = (permissions.IsAuthenticated, ) + queryset = models.Subscriber.objects.all() + serializer_class = serializers.SubscribeSerializer + + def patch(self, request, *args, **kw): + user = request.user + obj = get_object_or_404(models.Subscriber, user=user) + obj.unsubscribe() + serializer = self.get_serializer(instance=obj) + return Response(data=serializer.data) + diff --git a/apps/utils/methods.py b/apps/utils/methods.py index 9dcafa97..a34fcc07 100644 --- a/apps/utils/methods.py +++ b/apps/utils/methods.py @@ -1,6 +1,7 @@ """Utils app method.""" import random import re +import string from django.conf import settings from django.http.request import HttpRequest @@ -60,3 +61,20 @@ def svg_image_path(instance, filename): instance._meta.model_name, datetime.now().strftime(settings.REST_DATE_FORMAT), filename) + + +def get_user_ip(request): + """Get user ip.""" + meta_dict = request.META + x_forwarded_for = meta_dict.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0] + else: + ip = meta_dict.get('REMOTE_ADDR') + return ip + + +def generate_string_code(size=64, + chars=string.ascii_lowercase + string.ascii_uppercase + string.digits): + """Generate string code.""" + return ''.join([random.SystemRandom().choice(chars) for _ in range(size)]) diff --git a/project/settings/base.py b/project/settings/base.py index 5cd8b5b0..8a8fa6aa 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -61,6 +61,7 @@ PROJECT_APPS = [ 'location.apps.LocationConfig', 'main.apps.MainConfig', 'news.apps.NewsConfig', + 'notification.apps.NotificationConfig', 'partner.apps.PartnerConfig', 'translation.apps.TranslationConfig', 'configuration.apps.ConfigurationConfig', @@ -300,7 +301,6 @@ CELERY_ACCEPT_CONTENT = ['application/json'] CELERY_TASK_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json' CELERY_TIMEZONE = TIME_ZONE -USE_CELERY = False # Django FCM (Firebase push notificatoins) FCM_DJANGO_SETTINGS = { @@ -385,3 +385,6 @@ FILE_UPLOAD_PERMISSIONS = 0o644 SOLO_CACHE_TIMEOUT = 300 + +# REDIRECT URL +SITE_REDIRECT_URL_UNSUBSCRIBE = '/unsubscribe/' diff --git a/project/settings/development.py b/project/settings/development.py index 798eb6b0..7ecc6aa2 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -5,6 +5,7 @@ ALLOWED_HOSTS = ['gm.id-east.ru', '95.213.204.126'] SEND_SMS = False SMS_CODE_SHOW = True +USE_CELERY = False SCHEMA_URI = 'http' DEFAULT_SUBDOMAIN = 'www' diff --git a/project/settings/local.py b/project/settings/local.py index 93e6948b..19b3b51c 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -12,11 +12,6 @@ DEFAULT_SUBDOMAIN = 'www' SITE_DOMAIN_URI = 'testserver.com:8000' DOMAIN_URI = '0.0.0.0:8000' -# OTHER SETTINGS -API_HOST = '0.0.0.0:8000' -API_HOST_URL = 'http://%s' % API_HOST - - # CELERY BROKER_URL = 'amqp://rabbitmq:5672' CELERY_RESULT_BACKEND = BROKER_URL diff --git a/project/urls/web.py b/project/urls/web.py index baf361b4..1a9554d5 100644 --- a/project/urls/web.py +++ b/project/urls/web.py @@ -23,6 +23,7 @@ urlpatterns = [ path('collection/', include('collection.urls.web')), path('establishments/', include('establishment.urls.web')), path('news/', include('news.urls.web')), + path('notifications/', include('notification.urls.web')), path('partner/', include('partner.urls.web')), path('location/', include('location.urls')), path('main/', include('main.urls')), From 9373137a298d80e0b1a8f2470e1c25c488624517 Mon Sep 17 00:00:00 2001 From: evgeniy-st Date: Fri, 30 Aug 2019 15:43:15 +0300 Subject: [PATCH 2/9] remove throttling --- apps/notification/views/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/notification/views/common.py b/apps/notification/views/common.py index ee133fa3..ccb3cd69 100644 --- a/apps/notification/views/common.py +++ b/apps/notification/views/common.py @@ -2,7 +2,7 @@ from django.shortcuts import get_object_or_404 from rest_framework import generics, permissions from rest_framework.response import Response -from notification import models, throttling +from notification import models from notification.serializers import common as serializers From 9e1bf5ba0eb7565317549ae1251cdb0549f825c4 Mon Sep 17 00:00:00 2001 From: evgeniy-st Date: Fri, 30 Aug 2019 16:08:01 +0300 Subject: [PATCH 3/9] update news endpoint --- apps/news/serializers/common.py | 4 +-- apps/news/views/common.py | 43 ++++++++++++--------------------- 2 files changed, 17 insertions(+), 30 deletions(-) diff --git a/apps/news/serializers/common.py b/apps/news/serializers/common.py index 904c6f89..b64a7fc9 100644 --- a/apps/news/serializers/common.py +++ b/apps/news/serializers/common.py @@ -1,5 +1,5 @@ +"""News app common serializers.""" from rest_framework import serializers - from gallery import models as gallery_models from location.models import Address from location.serializers import AddressSerializer @@ -23,7 +23,7 @@ class NewsSerializer(serializers.ModelSerializer): title_translated = serializers.CharField(read_only=True, allow_null=True) subtitle_translated = serializers.CharField(read_only=True, allow_null=True) description_translated = serializers.CharField(read_only=True, allow_null=True) - image_url = serializers.ImageField(source='image.image') + image_url = serializers.ImageField(source='image.image', allow_null=True) class Meta: model = models.News diff --git a/apps/news/views/common.py b/apps/news/views/common.py index 82c77068..2678289b 100644 --- a/apps/news/views/common.py +++ b/apps/news/views/common.py @@ -1,50 +1,37 @@ +"""News app common app.""" from rest_framework import generics, permissions - -from news import filters -from news.models import News, NewsType +from news import filters, models from news.serializers import common as serializers -from utils.views import (JWTGenericViewMixin, - JWTListAPIView) +from utils.views import JWTGenericViewMixin, JWTListAPIView -# Mixins -class NewsViewMixin(JWTGenericViewMixin): - """View mixin for News model""" +class NewsMixin: + """News mixin.""" - def get_queryset(self, *args, **kwargs): - """Override get_queryset method""" - return News.objects.annotate_localized_fields(locale=self.request.locale) - - -class NewsListView(NewsViewMixin, JWTListAPIView): - """News list view.""" permission_classes = (permissions.AllowAny, ) serializer_class = serializers.NewsSerializer - filter_class = filters.NewsListFilterSet def get_queryset(self, *args, **kwargs): """Override get_queryset method""" - return News.objects.published()\ - .by_country_code(code=self.request.country_code)\ - .order_by('-is_highlighted', '-created') + return models.News.objects.published() \ + .by_country_code(code=self.request.country_code) \ + .order_by('-is_highlighted', '-created') -# class NewsCreateView(generics.CreateAPIView): -# """News list view.""" -# queryset = News.objects.all() -# permission_classes = (permissions.IsAuthenticated, ) -# serializer_class = serializers.NewsCreateUpdateSerializer +class NewsListView(NewsMixin, JWTListAPIView): + """News list view.""" + + filter_class = filters.NewsListFilterSet -class NewsDetailView(NewsViewMixin, generics.RetrieveAPIView): +class NewsDetailView(NewsMixin, JWTGenericViewMixin, generics.RetrieveAPIView): """News detail view.""" - permission_classes = (permissions.IsAuthenticatedOrReadOnly, ) - serializer_class = serializers.NewsSerializer class NewsTypeListView(generics.ListAPIView): """NewsType list view.""" + serializer_class = serializers.NewsTypeSerializer permission_classes = (permissions.AllowAny, ) pagination_class = None - queryset = NewsType.objects.all() + queryset = models.NewsType.objects.all() From 805bda3267ba7dc7a14adb64a558c9b1f1b89fc0 Mon Sep 17 00:00:00 2001 From: evgeniy-st Date: Fri, 30 Aug 2019 17:25:07 +0300 Subject: [PATCH 4/9] Extend establishment filterset --- apps/establishment/filters.py | 20 ++++++++++++++++---- apps/establishment/models.py | 22 ++++++++++++++++++---- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/apps/establishment/filters.py b/apps/establishment/filters.py index dc391bf6..46960d70 100644 --- a/apps/establishment/filters.py +++ b/apps/establishment/filters.py @@ -1,16 +1,28 @@ -from django_filters import FilterSet +"""Establishment app filters.""" +from django.core.validators import EMPTY_VALUES from django_filters import rest_framework as filters - from establishment import models -class EstablishmentFilter(FilterSet): +class EstablishmentFilter(filters.FilterSet): + """Establishment filterset.""" + tag_id = filters.NumberFilter(field_name='tags__metadata__id',) award_id = filters.NumberFilter(field_name='awards__id',) + search = filters.CharFilter(method='search_text') class Meta: + """Meta class.""" + model = models.Establishment fields = ( 'tag_id', - 'award_id' + 'award_id', + 'search', ) + + def search_text(self, queryset, name, value): + """Search text.""" + if value not in EMPTY_VALUES: + return queryset.search(value, locale=self.request.locale) + return queryset diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 56cc531c..68b8872e 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -1,4 +1,5 @@ """Establishment models.""" +from functools import reduce from django.core.exceptions import ValidationError from django.db import models from django.utils.translation import gettext_lazy as _ @@ -50,15 +51,26 @@ class EstablishmentSubType(ProjectBaseMixin, TraslatedFieldsMixin): verbose_name = _('Establishment subtype') verbose_name_plural = _('Establishment subtypes') - # def __str__(self): - # """__str__ method.""" - # return self.name - def clean_fields(self, exclude=None): if not self.establishment_type.use_subtypes: raise ValidationError(_('Establishment type is not use subtypes.')) +class EstablishmentQuerySet(models.QuerySet): + """Extended queryset for Establishment model.""" + + def search(self, value, locale=None): + """Search text in JSON fields.""" + if locale is not None: + filters = [ + {f'name__{locale}__icontains': value}, + {f'description__{locale}__icontains': value} + ] + return self.filter(reduce(lambda x, y: x | y, [models.Q(**i) for i in filters])) + else: + return self.none() + + class Establishment(ProjectBaseMixin, ImageMixin, TraslatedFieldsMixin): """Establishment model.""" @@ -89,6 +101,8 @@ class Establishment(ProjectBaseMixin, ImageMixin, TraslatedFieldsMixin): awards = generic.GenericRelation(to='main.Award') tags = generic.GenericRelation(to='main.MetaDataContent') + objects = EstablishmentQuerySet.as_manager() + class Meta: """Meta class.""" From fc4facbf83941108d47088cf2c0eb09e6ca35830 Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Sun, 1 Sep 2019 12:35:09 +0300 Subject: [PATCH 5/9] add contacts for establishments --- apps/establishment/admin.py | 25 +++- .../migrations/0005_auto_20190901_0831.py | 77 +++++++++++ .../migrations/0006_merge_20190901_0846.py | 14 ++ apps/establishment/models.py | 122 ++++++++++++++++-- apps/establishment/serializers.py | 37 ++++++ .../migrations/0003_auto_20190821_1407.py | 2 +- .../migrations/0009_auto_20190901_0831.py | 19 +++ apps/location/models.py | 2 +- apps/main/models.py | 16 ++- apps/news/models.py | 6 +- apps/translation/models.py | 2 +- project/settings/base.py | 1 + requirements/base.txt | 1 + 13 files changed, 307 insertions(+), 17 deletions(-) create mode 100644 apps/establishment/migrations/0005_auto_20190901_0831.py create mode 100644 apps/establishment/migrations/0006_merge_20190901_0846.py create mode 100644 apps/location/migrations/0009_auto_20190901_0831.py diff --git a/apps/establishment/admin.py b/apps/establishment/admin.py index d40950b1..a5209c3d 100644 --- a/apps/establishment/admin.py +++ b/apps/establishment/admin.py @@ -25,10 +25,33 @@ class MetaDataContentInline(GenericTabularInline): extra = 0 +class ContactPhoneInline(admin.TabularInline): + """Contact phone inline admin.""" + model = models.ContactPhone + extra = 0 + + +class ContactEmailInline(admin.TabularInline): + """Contact email inline admin.""" + model = models.ContactEmail + extra = 0 + + +@admin.register(models.Contact) +class ContactAdmin(admin.ModelAdmin): + """Contact admin.""" + inlines = [ContactPhoneInline, ContactEmailInline, ] + + +class ContactsInline(admin.TabularInline): + model = models.Contact + extra = 0 + + @admin.register(models.Establishment) class EstablishmentAdmin(admin.ModelAdmin): """Establishment admin.""" - inlines = [AwardInline, MetaDataContentInline] + inlines = [AwardInline, MetaDataContentInline, ContactsInline, ] @admin.register(models.EstablishmentSchedule) diff --git a/apps/establishment/migrations/0005_auto_20190901_0831.py b/apps/establishment/migrations/0005_auto_20190901_0831.py new file mode 100644 index 00000000..ef99bf4d --- /dev/null +++ b/apps/establishment/migrations/0005_auto_20190901_0831.py @@ -0,0 +1,77 @@ +# Generated by Django 2.2.4 on 2019-09-01 08:31 + +from django.db import migrations, models +import django.db.models.deletion +import phonenumber_field.modelfields +import utils.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('location', '0009_auto_20190901_0831'), + ('establishment', '0004_auto_20190828_1156'), + ] + + operations = [ + migrations.CreateModel( + name='Contact', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('address', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='location.Address')), + ], + options={ + 'verbose_name': 'contact', + 'verbose_name_plural': 'contacts', + }, + ), + migrations.AlterField( + model_name='establishment', + name='description', + field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='Description'), + ), + migrations.AlterField( + model_name='establishment', + name='name', + field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='Name'), + ), + migrations.AlterField( + model_name='establishmentsubtype', + name='name', + field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='Description'), + ), + migrations.AlterField( + model_name='establishmenttype', + name='name', + field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='Description'), + ), + migrations.CreateModel( + name='ContactPhone', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('phone', phonenumber_field.modelfields.PhoneNumberField(max_length=128)), + ('contact', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='phones', to='establishment.Contact')), + ], + options={ + 'verbose_name': 'contact phone', + 'verbose_name_plural': 'contact phones', + }, + ), + migrations.CreateModel( + name='ContactEmail', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('email', models.EmailField(max_length=254)), + ('contact', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='emails', to='establishment.Contact')), + ], + options={ + 'verbose_name': 'contact email', + 'verbose_name_plural': 'contact emails', + }, + ), + migrations.AddField( + model_name='contact', + name='establishment', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contacts', to='establishment.Establishment'), + ), + ] diff --git a/apps/establishment/migrations/0006_merge_20190901_0846.py b/apps/establishment/migrations/0006_merge_20190901_0846.py new file mode 100644 index 00000000..e629f2b9 --- /dev/null +++ b/apps/establishment/migrations/0006_merge_20190901_0846.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.4 on 2019-09-01 08:46 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0005_auto_20190901_0831'), + ('establishment', '0005_establishmentschedule'), + ] + + operations = [ + ] diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 68b8872e..0c73b722 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -1,12 +1,15 @@ """Establishment models.""" -from functools import reduce +from django.contrib.contenttypes import fields as generic from django.core.exceptions import ValidationError from django.db import models from django.utils.translation import gettext_lazy as _ +from phonenumber_field.modelfields import PhoneNumberField + from location.models import Address -from utils.models import (ProjectBaseMixin, ImageMixin, TJSONField, - TraslatedFieldsMixin, BaseAttributes) -from django.contrib.contenttypes import fields as generic +from utils.models import ( + ProjectBaseMixin, ImageMixin, TJSONField, + TraslatedFieldsMixin, BaseAttributes +) # todo: establishment type&subtypes check @@ -14,7 +17,7 @@ class EstablishmentType(ProjectBaseMixin, TraslatedFieldsMixin): """Establishment type model.""" name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'), - help_text='{"en":"some text"}') + help_text='{"en-GB":"some text"}') use_subtypes = models.BooleanField(_('Use subtypes'), default=True) class Meta: @@ -38,7 +41,7 @@ class EstablishmentSubType(ProjectBaseMixin, TraslatedFieldsMixin): """Establishment type model.""" name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'), - help_text='{"en":"some text"}') + help_text='{"en-GB":"some text"}') establishment_type = models.ForeignKey(EstablishmentType, on_delete=models.CASCADE, verbose_name=_('Type')) @@ -75,10 +78,10 @@ class Establishment(ProjectBaseMixin, ImageMixin, TraslatedFieldsMixin): """Establishment model.""" name = TJSONField(blank=True, null=True, default=None, - verbose_name=_('Name'), help_text='{"en":"some text"}') + verbose_name=_('Name'), help_text='{"en-GB":"some text"}') description = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'), - help_text='{"en":"some text"}') + help_text='{"en-GB":"some text"}') public_mark = models.PositiveIntegerField(blank=True, null=True, default=None, verbose_name=_('Public mark'),) @@ -160,3 +163,106 @@ class EstablishmentSchedule(BaseAttributes): """Meta class""" verbose_name = _('Establishment schedule') verbose_name_plural = _('Establishment schedules') + + +class Contact(models.Model): + """Contact model.""" + establishment = models.ForeignKey( + Establishment, related_name='contacts', on_delete=models.CASCADE) + address = models.ForeignKey('location.Address', on_delete=models.CASCADE) + + class Meta: + verbose_name = _('contact') + verbose_name_plural = _('contacts') + + +class ContactPhone(models.Model): + """Contact phone model.""" + contact = models.ForeignKey( + Contact, related_name='phones', on_delete=models.CASCADE) + phone = PhoneNumberField() + + class Meta: + verbose_name = _('contact phone') + verbose_name_plural = _('contact phones') + + def __str__(self): + return f'{self.phone.as_e164}' + + +class ContactEmail(models.Model): + """Contact email model.""" + contact = models.ForeignKey( + Contact, related_name='emails', on_delete=models.CASCADE) + email = models.EmailField() + + class Meta: + verbose_name = _('contact email') + verbose_name_plural = _('contact emails') + + def __str__(self): + return f'{self.email}' + +# +# class Wine(TraslatedFieldsMixin, 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(TraslatedFieldsMixin, models.Model): +# """Plate model.""" +# +# STARTER = 0 +# MAIN = 1 +# COURSE = 2 +# DESSERT = 3 +# +# PLATE_TYPE_CHOICES = ( +# (STARTER, _('starter')), +# (MAIN, _('main')), +# (COURSE, _('course')), +# (DESSERT, _('dessert')), +# ) +# name = models.CharField(_('name'), max_length=255) +# plate_type = models.PositiveSmallIntegerField(_('plate_type'), choices=PLATE_TYPE_CHOICES) +# description = TJSONField( +# blank=True, null=True, default=None, verbose_name=_('description'), +# help_text='{"en-GB":"some text"}') +# price = models.DecimalField( +# _('price'), max_digits=14, decimal_places=2) +# is_signature_plate = models.BooleanField(_('is signature plate')) +# currency = models.ForeignKey( +# 'main.Currency', verbose_name=_('currency'), on_delete=models.CASCADE) +# +# menu = models.ManyToManyField(to='Plate', verbose_name=_(''), through='establishment.Menu') +# +# class Meta: +# verbose_name = _('plate') +# verbose_name_plural = _('plates') +# +# def __str__(self): +# return f'plate_id:{self.id}' +# +# +# class Menu(TraslatedFieldsMixin, BaseAttributes): +# """Menu model.""" +# establishment = models.ForeignKey( +# 'establishment.Establishment', verbose_name=_('establishment'), +# on_delete=models.CASCADE) +# plate = models.ForeignKey(Plate, verbose_name=_('menu'), on_delete=models.CASCADE) +# +# class Meta: +# verbose_name = _('menu') +# verbose_name_plural = _('menu') \ No newline at end of file diff --git a/apps/establishment/serializers.py b/apps/establishment/serializers.py index 49788db3..92c7bb1c 100644 --- a/apps/establishment/serializers.py +++ b/apps/establishment/serializers.py @@ -6,6 +6,39 @@ from main.serializers import MetaDataContentSerializer, AwardSerializer from timetable.models import Timetable +class ContactPhonesSerializer(serializers.ModelSerializer): + """Contact phone serializer""" + class Meta: + model = models.ContactPhone + fields = [ + 'phone' + ] + + +class ContactEmailsSerializer(serializers.ModelSerializer): + """Contact email serializer""" + class Meta: + model = models.ContactEmail + fields = [ + 'email' + ] + + +class ContactSerializer(serializers.ModelSerializer): + """Contact serializer.""" + address = AddressSerializer(read_only=True) + phones = ContactPhonesSerializer(read_only=True, many=True,) + emails = ContactEmailsSerializer(read_only=True, many=True,) + + class Meta: + model = models.Contact + fields = [ + 'address', + 'phones', + 'emails' + ] + + class EstablishmentTypeSerializer(serializers.ModelSerializer): """Serializer for EstablishmentType model.""" @@ -59,6 +92,7 @@ class EstablishmentSerializer(serializers.ModelSerializer): schedule = EstablishmentScheduleSerializer(source='schedule.schedule', many=True, allow_null=True) + contacts = ContactSerializer(read_only=True, many=True, ) class Meta: """Meta class.""" @@ -79,4 +113,7 @@ class EstablishmentSerializer(serializers.ModelSerializer): 'tags', 'awards', 'schedule', + 'contacts' ) + + diff --git a/apps/location/migrations/0003_auto_20190821_1407.py b/apps/location/migrations/0003_auto_20190821_1407.py index 548e2a70..c82ec597 100644 --- a/apps/location/migrations/0003_auto_20190821_1407.py +++ b/apps/location/migrations/0003_auto_20190821_1407.py @@ -30,6 +30,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='country', name='name', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=None, help_text='{"en":"some text"}', null=True, verbose_name='Text'), + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='Text'), ), ] diff --git a/apps/location/migrations/0009_auto_20190901_0831.py b/apps/location/migrations/0009_auto_20190901_0831.py new file mode 100644 index 00000000..c4e21715 --- /dev/null +++ b/apps/location/migrations/0009_auto_20190901_0831.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.4 on 2019-09-01 08:31 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('location', '0008_auto_20190827_1302'), + ] + + operations = [ + migrations.AlterField( + model_name='country', + name='name', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='Name'), + ), + ] diff --git a/apps/location/models.py b/apps/location/models.py index 56bd89f3..613cc013 100644 --- a/apps/location/models.py +++ b/apps/location/models.py @@ -17,7 +17,7 @@ class Country(SVGImageMixin, ProjectBaseMixin): """Country model.""" name = JSONField(null=True, blank=True, default=None, - verbose_name=_('Name'), help_text='{"en":"some text"}') + verbose_name=_('Name'), help_text='{"en-GB":"some text"}') code = models.CharField(max_length=255, unique=True, verbose_name=_('Code')) low_price = models.IntegerField(default=25, verbose_name=_('Low price')) high_price = models.IntegerField(default=50, verbose_name=_('High price')) diff --git a/apps/main/models.py b/apps/main/models.py index cb406d98..b58dc402 100644 --- a/apps/main/models.py +++ b/apps/main/models.py @@ -190,7 +190,7 @@ class Award(TraslatedFieldsMixin, models.Model): award_type = models.ForeignKey('main.AwardType', on_delete=models.CASCADE) title = TJSONField( _('title'), null=True, blank=True, - default=None, help_text='{"en":"some text"}') + default=None, help_text='{"en-GB":"some text"}') vintage_year = models.CharField(_('vintage year'), max_length=255, default='') content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) @@ -228,7 +228,7 @@ class MetaData(TraslatedFieldsMixin, models.Model): """MetaData model.""" label = TJSONField( _('label'), null=True, blank=True, - default=None, help_text='{"en":"some text"}') + default=None, help_text='{"en-GB":"some text"}') category = models.ForeignKey( MetaDataCategory, verbose_name=_('category'), on_delete=models.CASCADE) @@ -250,3 +250,15 @@ class MetaDataContent(models.Model): object_id = models.PositiveIntegerField() content_object = generic.GenericForeignKey('content_type', 'object_id') metadata = models.ForeignKey(MetaData, on_delete=models.CASCADE) + + +class Currency(models.Model): + """Currency model.""" + name = models.CharField(_('name'), max_length=50) + + class Meta: + verbose_name = _('currency') + verbose_name_plural = _('currencies') + + def __str__(self): + return f'{self.name}' diff --git a/apps/news/models.py b/apps/news/models.py index e3eff57e..0abc7ab5 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -43,14 +43,14 @@ class News(BaseAttributes, TraslatedFieldsMixin): title = TJSONField( _('title'), null=True, blank=True, - default=None, help_text='{"en":"some text"}') + default=None, help_text='{"en-GB":"some text"}') subtitle = TJSONField( _('subtitle'), null=True, blank=True, - default=None, help_text='{"en":"some text"}' + default=None, help_text='{"en-GB":"some text"}' ) description = TJSONField( _('description'), null=True, blank=True, - default=None, help_text='{"en":"some text"}' + default=None, help_text='{"en-GB":"some text"}' ) start = models.DateTimeField(_('start')) end = models.DateTimeField(_('end')) diff --git a/apps/translation/models.py b/apps/translation/models.py index f675b315..42530965 100644 --- a/apps/translation/models.py +++ b/apps/translation/models.py @@ -49,7 +49,7 @@ class SiteInterfaceDictionary(ProjectBaseMixin): verbose_name=_('Page')) keywords = models.CharField(max_length=255, verbose_name='Keywords') text = JSONField(_('Text'), null=True, blank=True, - default=None, help_text='{"en":"some text"}') + default=None, help_text='{"en-GB":"some text"}') objects = SiteInterfaceDictionaryManager() diff --git a/project/settings/base.py b/project/settings/base.py index 588011d1..6a4a5836 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -82,6 +82,7 @@ EXTERNAL_APPS = [ 'django_extensions', 'rest_framework_simplejwt.token_blacklist', 'solo', + 'phonenumber_field', ] diff --git a/requirements/base.txt b/requirements/base.txt index 20a87346..3d7987c0 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -17,6 +17,7 @@ djangorestframework-xml celery amqp>=2.4.0 geoip2==2.9.0 +django-phonenumber-field[phonenumbers]==2.1.0 # auth socials djangorestframework-oauth From c1738478abb3471511d15f2952ea4f6570b8d0b3 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Sun, 1 Sep 2019 13:24:37 +0300 Subject: [PATCH 6/9] added new model Review --- apps/establishment/admin.py | 11 +++++-- apps/establishment/models.py | 4 ++- apps/establishment/serializers.py | 15 +++++++++ apps/review/admin.py | 10 ++---- apps/review/migrations/0001_initial.py | 43 ++++++++++++++++++++++++++ 5 files changed, 73 insertions(+), 10 deletions(-) create mode 100644 apps/review/migrations/0001_initial.py diff --git a/apps/establishment/admin.py b/apps/establishment/admin.py index d40950b1..d34141db 100644 --- a/apps/establishment/admin.py +++ b/apps/establishment/admin.py @@ -1,8 +1,10 @@ """Establishment admin conf.""" from django.contrib import admin -from establishment import models from django.contrib.contenttypes.admin import GenericTabularInline + +from establishment import models from main.models import Award, MetaDataContent +from review import models as review_models @admin.register(models.EstablishmentType) @@ -25,10 +27,15 @@ class MetaDataContentInline(GenericTabularInline): extra = 0 +class ReviewInline(GenericTabularInline): + model = review_models.Review + extra = 0 + + @admin.register(models.Establishment) class EstablishmentAdmin(admin.ModelAdmin): """Establishment admin.""" - inlines = [AwardInline, MetaDataContentInline] + inlines = [AwardInline, MetaDataContentInline, ReviewInline] @admin.register(models.EstablishmentSchedule) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 56cc531c..0a2a8831 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -1,11 +1,12 @@ """Establishment models.""" +from django.contrib.contenttypes import fields as generic from django.core.exceptions import ValidationError from django.db import models from django.utils.translation import gettext_lazy as _ + from location.models import Address from utils.models import (ProjectBaseMixin, ImageMixin, TJSONField, TraslatedFieldsMixin, BaseAttributes) -from django.contrib.contenttypes import fields as generic # todo: establishment type&subtypes check @@ -88,6 +89,7 @@ class Establishment(ProjectBaseMixin, ImageMixin, TraslatedFieldsMixin): verbose_name=_('Price level')) awards = generic.GenericRelation(to='main.Award') tags = generic.GenericRelation(to='main.MetaDataContent') + reviews = generic.GenericRelation(to='review.Review') class Meta: """Meta class.""" diff --git a/apps/establishment/serializers.py b/apps/establishment/serializers.py index 49788db3..fef87cb5 100644 --- a/apps/establishment/serializers.py +++ b/apps/establishment/serializers.py @@ -1,8 +1,10 @@ """Establishment serializers.""" from rest_framework import serializers + from establishment import models from location.serializers import AddressSerializer from main.serializers import MetaDataContentSerializer, AwardSerializer +from review import models as review_models from timetable.models import Timetable @@ -46,6 +48,16 @@ class EstablishmentScheduleSerializer(serializers.ModelSerializer): ) +class ReviewSerializer(serializers.ModelSerializer): + """Serializer for model Review.""" + class Meta: + """Meta class.""" + model = review_models.Review + fields = ( + 'text', + ) + + class EstablishmentSerializer(serializers.ModelSerializer): """Serializer for Establishment model.""" @@ -59,6 +71,8 @@ class EstablishmentSerializer(serializers.ModelSerializer): schedule = EstablishmentScheduleSerializer(source='schedule.schedule', many=True, allow_null=True) + reviews = ReviewSerializer(source='reviews.last', + allow_null=True) class Meta: """Meta class.""" @@ -79,4 +93,5 @@ class EstablishmentSerializer(serializers.ModelSerializer): 'tags', 'awards', 'schedule', + 'reviews', ) diff --git a/apps/review/admin.py b/apps/review/admin.py index f3aeca39..cbd0bf94 100644 --- a/apps/review/admin.py +++ b/apps/review/admin.py @@ -1,7 +1,3 @@ -from django.contrib import admin -from review import models - - -@admin.register(models.Review) -class ReviewAdminModel(admin.ModelAdmin): - """Admin model for model Review.""" +# @admin.register(models.Review) +# class ReviewAdminModel(admin.ModelAdmin): +# """Admin model for model Review.""" diff --git a/apps/review/migrations/0001_initial.py b/apps/review/migrations/0001_initial.py new file mode 100644 index 00000000..3deac75e --- /dev/null +++ b/apps/review/migrations/0001_initial.py @@ -0,0 +1,43 @@ +# Generated by Django 2.2.4 on 2019-09-01 09:32 + +import django.core.validators +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('translation', '0002_siteinterfacedictionary'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Review', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')), + ('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('object_id', models.PositiveIntegerField()), + ('text', models.TextField(verbose_name='Text')), + ('status', models.PositiveSmallIntegerField(choices=[(0, 'To investigate'), (1, 'To review'), (2, 'Ready')], default=0)), + ('published_at', models.DateTimeField(blank=True, default=None, help_text='Review published datetime', null=True, verbose_name='Publish datetime')), + ('vintage', models.IntegerField(validators=[django.core.validators.MinValueValidator(1900), django.core.validators.MaxValueValidator(2100)], verbose_name='Year of review')), + ('child', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='review.Review', verbose_name='Child review')), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='review_records_created', to=settings.AUTH_USER_MODEL, verbose_name='created by')), + ('language', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to='translation.Language', verbose_name='Review language')), + ('modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='review_records_modified', to=settings.AUTH_USER_MODEL, verbose_name='modified by')), + ('reviewer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to=settings.AUTH_USER_MODEL, verbose_name='Reviewer')), + ], + options={ + 'verbose_name': 'Review', + 'verbose_name_plural': 'Reviews', + }, + ), + ] From 3f90ef6dab6353fee287a79c0ec526724cca8b49 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Sun, 1 Sep 2019 13:29:17 +0300 Subject: [PATCH 7/9] fix merge --- apps/establishment/models.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 68b8872e..8f315fff 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -1,12 +1,14 @@ """Establishment models.""" from functools import reduce + +from django.contrib.contenttypes import fields as generic from django.core.exceptions import ValidationError from django.db import models from django.utils.translation import gettext_lazy as _ + from location.models import Address from utils.models import (ProjectBaseMixin, ImageMixin, TJSONField, TraslatedFieldsMixin, BaseAttributes) -from django.contrib.contenttypes import fields as generic # todo: establishment type&subtypes check @@ -100,6 +102,7 @@ class Establishment(ProjectBaseMixin, ImageMixin, TraslatedFieldsMixin): verbose_name=_('Price level')) awards = generic.GenericRelation(to='main.Award') tags = generic.GenericRelation(to='main.MetaDataContent') + reviews = generic.GenericRelation(to='review.Review') objects = EstablishmentQuerySet.as_manager() From ef59216feb9592871f54ec8c3322f4c08cefad32 Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Sun, 1 Sep 2019 13:39:01 +0300 Subject: [PATCH 8/9] change contacts for est. --- apps/establishment/admin.py | 15 ++----- .../migrations/0007_auto_20190901_1032.py | 30 ++++++++++++++ .../0008_contactemail_contactphone.py | 39 +++++++++++++++++++ apps/establishment/models.py | 19 ++------- apps/establishment/serializers.py | 21 ++-------- .../migrations/0013_auto_20190901_1032.py | 35 +++++++++++++++++ .../migrations/0009_auto_20190901_1032.py | 29 ++++++++++++++ .../migrations/0003_auto_20190901_1032.py | 19 +++++++++ 8 files changed, 163 insertions(+), 44 deletions(-) create mode 100644 apps/establishment/migrations/0007_auto_20190901_1032.py create mode 100644 apps/establishment/migrations/0008_contactemail_contactphone.py create mode 100644 apps/main/migrations/0013_auto_20190901_1032.py create mode 100644 apps/news/migrations/0009_auto_20190901_1032.py create mode 100644 apps/translation/migrations/0003_auto_20190901_1032.py diff --git a/apps/establishment/admin.py b/apps/establishment/admin.py index a5209c3d..37a2e8cb 100644 --- a/apps/establishment/admin.py +++ b/apps/establishment/admin.py @@ -37,21 +37,12 @@ class ContactEmailInline(admin.TabularInline): extra = 0 -@admin.register(models.Contact) -class ContactAdmin(admin.ModelAdmin): - """Contact admin.""" - inlines = [ContactPhoneInline, ContactEmailInline, ] - - -class ContactsInline(admin.TabularInline): - model = models.Contact - extra = 0 - - @admin.register(models.Establishment) class EstablishmentAdmin(admin.ModelAdmin): """Establishment admin.""" - inlines = [AwardInline, MetaDataContentInline, ContactsInline, ] + inlines = [ + AwardInline, MetaDataContentInline, + ContactPhoneInline, ContactEmailInline] @admin.register(models.EstablishmentSchedule) diff --git a/apps/establishment/migrations/0007_auto_20190901_1032.py b/apps/establishment/migrations/0007_auto_20190901_1032.py new file mode 100644 index 00000000..eb6c6a4a --- /dev/null +++ b/apps/establishment/migrations/0007_auto_20190901_1032.py @@ -0,0 +1,30 @@ +# Generated by Django 2.2.4 on 2019-09-01 10:32 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0006_merge_20190901_0846'), + ] + + operations = [ + migrations.RemoveField( + model_name='contactemail', + name='contact', + ), + migrations.RemoveField( + model_name='contactphone', + name='contact', + ), + migrations.DeleteModel( + name='Contact', + ), + migrations.DeleteModel( + name='ContactEmail', + ), + migrations.DeleteModel( + name='ContactPhone', + ), + ] diff --git a/apps/establishment/migrations/0008_contactemail_contactphone.py b/apps/establishment/migrations/0008_contactemail_contactphone.py new file mode 100644 index 00000000..bda629ed --- /dev/null +++ b/apps/establishment/migrations/0008_contactemail_contactphone.py @@ -0,0 +1,39 @@ +# Generated by Django 2.2.4 on 2019-09-01 10:36 + +from django.db import migrations, models +import django.db.models.deletion +import phonenumber_field.modelfields + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0007_auto_20190901_1032'), + ] + + operations = [ + migrations.CreateModel( + name='ContactPhone', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('phone', phonenumber_field.modelfields.PhoneNumberField(max_length=128)), + ('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='phones', to='establishment.Establishment')), + ], + options={ + 'verbose_name': 'contact phone', + 'verbose_name_plural': 'contact phones', + }, + ), + migrations.CreateModel( + name='ContactEmail', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('email', models.EmailField(max_length=254)), + ('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='emails', to='establishment.Establishment')), + ], + options={ + 'verbose_name': 'contact email', + 'verbose_name_plural': 'contact emails', + }, + ), + ] diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 0c73b722..8f346564 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -165,21 +165,10 @@ class EstablishmentSchedule(BaseAttributes): verbose_name_plural = _('Establishment schedules') -class Contact(models.Model): - """Contact model.""" - establishment = models.ForeignKey( - Establishment, related_name='contacts', on_delete=models.CASCADE) - address = models.ForeignKey('location.Address', on_delete=models.CASCADE) - - class Meta: - verbose_name = _('contact') - verbose_name_plural = _('contacts') - - class ContactPhone(models.Model): """Contact phone model.""" - contact = models.ForeignKey( - Contact, related_name='phones', on_delete=models.CASCADE) + establishment = models.ForeignKey( + Establishment, related_name='phones', on_delete=models.CASCADE) phone = PhoneNumberField() class Meta: @@ -192,8 +181,8 @@ class ContactPhone(models.Model): class ContactEmail(models.Model): """Contact email model.""" - contact = models.ForeignKey( - Contact, related_name='emails', on_delete=models.CASCADE) + establishment = models.ForeignKey( + Establishment, related_name='emails', on_delete=models.CASCADE) email = models.EmailField() class Meta: diff --git a/apps/establishment/serializers.py b/apps/establishment/serializers.py index 92c7bb1c..7e3a8085 100644 --- a/apps/establishment/serializers.py +++ b/apps/establishment/serializers.py @@ -24,21 +24,6 @@ class ContactEmailsSerializer(serializers.ModelSerializer): ] -class ContactSerializer(serializers.ModelSerializer): - """Contact serializer.""" - address = AddressSerializer(read_only=True) - phones = ContactPhonesSerializer(read_only=True, many=True,) - emails = ContactEmailsSerializer(read_only=True, many=True,) - - class Meta: - model = models.Contact - fields = [ - 'address', - 'phones', - 'emails' - ] - - class EstablishmentTypeSerializer(serializers.ModelSerializer): """Serializer for EstablishmentType model.""" @@ -92,7 +77,8 @@ class EstablishmentSerializer(serializers.ModelSerializer): schedule = EstablishmentScheduleSerializer(source='schedule.schedule', many=True, allow_null=True) - contacts = ContactSerializer(read_only=True, many=True, ) + phones = ContactPhonesSerializer(read_only=True, many=True, ) + emails = ContactEmailsSerializer(read_only=True, many=True, ) class Meta: """Meta class.""" @@ -113,7 +99,8 @@ class EstablishmentSerializer(serializers.ModelSerializer): 'tags', 'awards', 'schedule', - 'contacts' + 'phones', + 'emails' ) diff --git a/apps/main/migrations/0013_auto_20190901_1032.py b/apps/main/migrations/0013_auto_20190901_1032.py new file mode 100644 index 00000000..95c86883 --- /dev/null +++ b/apps/main/migrations/0013_auto_20190901_1032.py @@ -0,0 +1,35 @@ +# Generated by Django 2.2.4 on 2019-09-01 10:32 + +from django.db import migrations, models +import utils.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0012_auto_20190829_1155'), + ] + + operations = [ + migrations.CreateModel( + name='Currency', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, verbose_name='name')), + ], + options={ + 'verbose_name': 'currency', + 'verbose_name_plural': 'currencies', + }, + ), + migrations.AlterField( + model_name='award', + name='title', + field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='title'), + ), + migrations.AlterField( + model_name='metadata', + name='label', + field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='label'), + ), + ] diff --git a/apps/news/migrations/0009_auto_20190901_1032.py b/apps/news/migrations/0009_auto_20190901_1032.py new file mode 100644 index 00000000..5a083240 --- /dev/null +++ b/apps/news/migrations/0009_auto_20190901_1032.py @@ -0,0 +1,29 @@ +# Generated by Django 2.2.4 on 2019-09-01 10:32 + +from django.db import migrations +import utils.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('news', '0008_auto_20190828_1522'), + ] + + operations = [ + migrations.AlterField( + model_name='news', + name='description', + field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='description'), + ), + migrations.AlterField( + model_name='news', + name='subtitle', + field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='subtitle'), + ), + migrations.AlterField( + model_name='news', + name='title', + field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='title'), + ), + ] diff --git a/apps/translation/migrations/0003_auto_20190901_1032.py b/apps/translation/migrations/0003_auto_20190901_1032.py new file mode 100644 index 00000000..85bf63eb --- /dev/null +++ b/apps/translation/migrations/0003_auto_20190901_1032.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.4 on 2019-09-01 10:32 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('translation', '0002_siteinterfacedictionary'), + ] + + operations = [ + migrations.AlterField( + model_name='siteinterfacedictionary', + name='text', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='Text'), + ), + ] From 1494bbbf36d7f449c1ca297a54161aec8088fb5d Mon Sep 17 00:00:00 2001 From: Anatoly Date: Sun, 1 Sep 2019 14:09:37 +0300 Subject: [PATCH 9/9] Merge branch 'feature/reviews' into develop # Conflicts: # apps/establishment/admin.py # apps/establishment/models.py # apps/establishment/serializers.py --- apps/establishment/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/establishment/serializers.py b/apps/establishment/serializers.py index ea4af485..4cd5e348 100644 --- a/apps/establishment/serializers.py +++ b/apps/establishment/serializers.py @@ -1,5 +1,6 @@ """Establishment serializers.""" from rest_framework import serializers + from establishment import models from location.serializers import AddressSerializer from main.serializers import MetaDataContentSerializer, AwardSerializer @@ -113,6 +114,6 @@ class EstablishmentSerializer(serializers.ModelSerializer): 'awards', 'schedule', 'phones', - 'emails' + 'emails', 'reviews', )