Merge branch 'develop' of ssh://gl.id-east.ru:222/gm/gm-backend into develop

This commit is contained in:
Dmitriy Kuzmenko 2020-01-09 17:47:26 +03:00
commit 807f8b4169
25 changed files with 669 additions and 250 deletions

View File

@ -8,6 +8,7 @@ from rest_framework import serializers
from rest_framework import validators as rest_validators
from account import models, tasks
from notification.models import Subscriber
from utils import exceptions as utils_exceptions
from utils import methods as utils_methods
@ -46,6 +47,12 @@ class UserSerializer(serializers.ModelSerializer):
'newsletter',
]
def create(self, validated_data):
user = super(UserSerializer, self).create(validated_data)
validated_data['user'] = user
Subscriber.objects.make_subscriber(**validated_data)
return user
def validate_email(self, value):
"""Validate email value"""
if value == self.instance.email:

View File

@ -848,10 +848,13 @@ class GuideElementManager(models.Manager):
if product_qs.exists() and wine_region_node_qs.exists():
wine_region_node = wine_region_node_qs.first()
root_node = wine_region_node.get_root()
return self.get_or_create(guide_element_type=guide_element_type_qs.first(),
parent=wine_region_node,
guide=root_node.guide,
product=product_qs.first())
product = product_qs.first()
if product.establishment:
return self.get_or_create(guide_element_type=guide_element_type_qs.first(),
parent=wine_region_node,
guide=root_node.guide,
establishment=product.establishment)
return None, False
def get_or_create_color_wine_section_node(self, wine_color_name: str, yard_node_id: int):
@ -869,7 +872,7 @@ class GuideElementManager(models.Manager):
})
if parent_node_qs.exists() and guide_element_type_qs.exists():
root_node = parent_node_qs.first().get_root()
root_node = parent_node_qs.first()
return self.get_or_create(guide_element_type=guide_element_type_qs.first(),
parent=root_node,
wine_color_section=wine_color_section,
@ -884,7 +887,7 @@ class GuideElementManager(models.Manager):
review_qs = Review.objects.filter(id=review_id)
if parent_node_qs.exists() and wine_qs.exists() and review_qs.exists() and guide_element_type_qs.exists():
root_node = parent_node_qs.first().get_root()
root_node = parent_node_qs.first()
return self.get_or_create(guide_element_type=guide_element_type_qs.first(),
parent=root_node,
product=wine_qs.first(),
@ -973,8 +976,3 @@ class GuideElement(ProjectBaseMixin, MPTTModel):
def __str__(self):
"""Overridden dunder method."""
return self.guide_element_type.name if self.guide_element_type else self.id
@property
def advertorial_page(self):
if self.advertorial:
return self.advertorial.right_pages

View File

@ -152,6 +152,8 @@ class GuideElementBaseSerializer(serializers.ModelSerializer):
allow_null=True)
wine_color_section_name = serializers.CharField(source='wine_color_section.name',
allow_null=True)
wine_region_name = serializers.CharField(source='wine_region.name',
allow_null=True)
node_name = serializers.CharField(source='guide_element_type.name')
label_photo = serializers.ImageField(source='label_photo.image', allow_null=True)
city_name = serializers.CharField(source='city.name', allow_null=True)
@ -167,12 +169,12 @@ class GuideElementBaseSerializer(serializers.ModelSerializer):
'node_name',
'establishment_detail',
'review',
'wine_region',
'product_detail',
'priority',
'city_name',
'section_name',
'wine_color_section_name',
'wine_region_name',
'children',
'label_photo',
]
@ -237,19 +239,23 @@ class GuideElementExportSerializer(GuideElementBaseSerializer):
default=None)
metadata = serializers.ListField(source='establishment.metadata',
default=None)
advertorial_page = serializers.IntegerField(default=None)
advertorial = serializers.DictField(source='advertorial.__dict__', default=None)
# PRODUCT
product_name = serializers.CharField(source='product.name',
default=None)
product_review = serializers.DictField(source='product.establishment.last_published_review_data',
product_review = serializers.DictField(source='product.last_published_review_data',
default=None)
product_type = serializers.CharField(source='product.product_type_label',
product_type = serializers.CharField(source='product.product_type.label',
default=None)
product_subtypes = serializers.CharField(source='product.product_subtype_labels',
default=None)
product_city = serializers.CharField(source='product.establishment.address.city.name',
default=None)
product_address = serializers.CharField(source='product.establishment.address.full_address',
default=None)
product_metadata = serializers.ListField(source='product.metadata',
default=None)
class Meta:
model = models.GuideElement
@ -257,16 +263,8 @@ class GuideElementExportSerializer(GuideElementBaseSerializer):
'id',
'guide',
'node_name',
# 'establishment',
# 'review',
# 'wine_region',
# 'product_detail',
# 'priority',
'city_name',
# 'section_name',
# 'wine_color_section_name',
# 'children',
'label_photo_url',
'wine_color_section_name',
'name',
'public_mark',
'toque_number',
@ -278,5 +276,12 @@ class GuideElementExportSerializer(GuideElementBaseSerializer):
'review',
'price_level',
'metadata',
'advertorial_page',
'advertorial',
'product_name',
'product_review',
'product_type',
'product_subtypes',
'product_address',
'product_city',
'product_metadata',
]

View File

@ -148,10 +148,11 @@ def export_guide(guide_id, user_id, file_type='csv'):
guide = Guide.objects.get(id=guide_id)
root = GuideElement.objects.get_root_node(guide)
if root:
nodes = root.get_descendants().select_related('review', 'establishment', 'wine_region',
'product', 'city', 'wine_color_section',
'section', 'label_photo', 'guide',
'city__country', 'establishment__establishment_type')
nodes = root.get_descendants().select_related(
'review', 'establishment', 'wine_region',
'product', 'city', 'wine_color_section',
'section', 'label_photo', 'guide',
'city__country', 'establishment__establishment_type')
serializer = GuideElementExportSerializer(nodes, many=True)
data = serializer.data
SendGuideExport(

View File

@ -186,16 +186,19 @@ class EmployeeBackSerializers(serializers.ModelSerializer):
"""Get last list actual public_mark"""
qs = obj.establishmentemployee_set.actual().order_by('-from_date')\
.values('establishment__public_mark').first()
return qs['establishment__public_mark']
return qs['establishment__public_mark'] if qs else None
def get_positions(self, obj):
"""Get last list actual positions"""
est_id = obj.establishmentemployee_set.actual().\
order_by('-from_date').first().establishment_id
order_by('-from_date').first()
if not est_id:
return None
qs = obj.establishmentemployee_set.actual()\
.filter(establishment_id=est_id)\
.filter(establishment_id=est_id.establishment_id)\
.prefetch_related('position').values('position')
positions = models.Position.objects.filter(id__in=[q['position'] for q in qs])
@ -205,11 +208,14 @@ class EmployeeBackSerializers(serializers.ModelSerializer):
def get_establishment(self, obj):
"""Get last actual establishment"""
est = obj.establishmentemployee_set.actual().order_by('-from_date')\
.first().establishment
.first()
if not est:
return None
return {
"id": est.id,
"slug": est.slug
"id": est.establishment.id,
"slug": est.establishment.slug
}
class Meta:

View File

@ -1,43 +1,60 @@
from datetime import datetime
from smtplib import SMTPException
from celery import shared_task
from django.core.mail import send_mail
from notification.models import Subscriber
from news import models
from django.template.loader import render_to_string, get_template
from django.conf import settings
from smtplib import SMTPException
from django.core.mail import send_mail
from django.core.validators import EMPTY_VALUES
from django.template.loader import get_template, render_to_string
from main.models import SiteSettings
from news import models
from notification.models import Subscribe
@shared_task
def send_email_with_news(news_ids):
subscribers = Subscriber.objects.filter(state=Subscriber.USABLE)
subscribes = Subscribe.objects.all() \
.prefetch_related('subscriber') \
.prefetch_related('subscription_type')
sent_news = models.News.objects.filter(id__in=news_ids)
htmly = get_template(settings.NEWS_EMAIL_TEMPLATE)
year = datetime.now().year
socials = list(SiteSettings.objects.with_country())
socials = dict(zip(map(lambda s: s.country.code, socials), socials))
for s in subscribers:
socials_for_subscriber = socials.get(s.country_code)
socials = list(SiteSettings.objects.with_country().select_related('country'))
socials = dict(zip(map(lambda social: social.country.code, socials), socials))
for subscribe in subscribes.filter(unsubscribe_date=None):
country = subscribe.subscription_type.country
if country is None:
continue
socials_for_subscriber = socials.get(country.code)
subscriber = subscribe.subscriber
try:
for n in sent_news:
context = {"title": n.title.get(s.locale),
"subtitle": n.subtitle.get(s.locale),
"description": n.description.get(s.locale),
"code": s.update_code,
"image_url": n.image_url if n.image_url not in EMPTY_VALUES else None,
"domain_uri": settings.DOMAIN_URI,
"slug": n.slug,
"country_code": s.country_code,
"twitter_page_url": socials_for_subscriber.twitter_page_url if socials_for_subscriber else '#',
"instagram_page_url": socials_for_subscriber.instagram_page_url if socials_for_subscriber else '#',
"facebook_page_url": socials_for_subscriber.facebook_page_url if socials_for_subscriber else '#',
"send_to": s.send_to,
"year": year}
send_mail("G&M News", render_to_string(settings.NEWS_EMAIL_TEMPLATE, context),
settings.EMAIL_HOST_USER, [s.send_to], fail_silently=False,
html_message=htmly.render(context))
for new in sent_news:
context = {
"title": new.title.get(subscriber.locale),
"subtitle": new.subtitle.get(subscriber.locale),
"description": new.description.get(subscriber.locale),
"code": subscriber.update_code,
"image_url": new.image_url if new.image_url not in EMPTY_VALUES else None,
"domain_uri": settings.DOMAIN_URI,
"slug": new.slug,
"country_code": subscriber.country_code,
"twitter_page_url": socials_for_subscriber.twitter_page_url if socials_for_subscriber else '#',
"instagram_page_url": socials_for_subscriber.instagram_page_url if socials_for_subscriber else '#',
"facebook_page_url": socials_for_subscriber.facebook_page_url if socials_for_subscriber else '#',
"send_to": subscriber.send_to,
"year": year
}
send_mail(
"G&M News", render_to_string(settings.NEWS_EMAIL_TEMPLATE, context),
settings.EMAIL_HOST_USER, [subscriber.send_to], fail_silently=False,
html_message=htmly.render(context)
)
except SMTPException:
continue

View File

@ -145,7 +145,7 @@ def add_tags():
)
if created:
text_value = ' '.join(new_tag.value.split('_'))
translation = SiteInterfaceDictionary(page={'en-GB': text_value}, keywords=f'tag.{new_tag.category}.{new_tag.value}')
translation = SiteInterfaceDictionary(text={'en-GB': text_value}, keywords=f'tag.{new_tag.category}.{new_tag.value}')
translation.save()
new_tag.translation = translation
new_tag.save()

View File

@ -2,17 +2,17 @@
from django.urls import path
from news import views
from search_indexes.views import NewsDocumentViewSet
app_name = 'news'
urlpatterns = [
path('', views.NewsBackOfficeLCView.as_view(), name='list-create'),
path('<int:pk>/', views.NewsBackOfficeRUDView.as_view(),
name='retrieve-update-destroy'),
path('<int:pk>/gallery/', views.NewsBackOfficeGalleryListView.as_view(),
name='gallery-list'),
path('<int:pk>/', views.NewsBackOfficeRUDView.as_view(), name='retrieve-update-destroy'),
path('<int:pk>/gallery/', views.NewsBackOfficeGalleryListView.as_view(), name='gallery-list'),
path('<int:pk>/gallery/<int:image_id>/', views.NewsBackOfficeGalleryCreateDestroyView.as_view(),
name='gallery-create-destroy'),
path('<int:pk>/carousels/', views.NewsCarouselCreateDestroyView.as_view(), name='create-destroy-carousels'),
path('<int:pk>/clone/<str:country_code>', views.NewsCloneView.as_view(), name='clone-news-item'),
path('search/', NewsDocumentViewSet.as_view({'get': 'list'}), name='search-news'),
]

View File

@ -0,0 +1,38 @@
# Generated by Django 2.2.7 on 2019-12-27 12:12
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('notification', '0004_auto_20191118_1307'),
]
operations = [
migrations.RemoveField(
model_name='subscriber',
name='subscription_type',
),
migrations.CreateModel(
name='Subscribe',
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')),
('subscriber', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='notification.Subscriber')),
('subscription_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='notification.SubscriptionType')),
],
options={
'verbose_name': 'Subscribe',
'verbose_name_plural': 'Subscribes',
},
),
migrations.AddField(
model_name='subscriber',
name='subscription_types',
field=models.ManyToManyField(through='notification.Subscribe', to='notification.SubscriptionType'),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 2.2.7 on 2019-12-27 12:16
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('notification', '0005_auto_20191227_1212'),
]
operations = [
migrations.AddField(
model_name='subscribe',
name='subscribe_date',
field=models.DateTimeField(blank=True, default=django.utils.timezone.now, null=True, verbose_name='Last subscribe date'),
),
migrations.AddField(
model_name='subscribe',
name='unsubscribe_date',
field=models.DateTimeField(blank=True, default=None, null=True, verbose_name='Last unsubscribe date'),
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 2.2.7 on 2019-12-27 14:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('notification', '0006_auto_20191227_1216'),
]
operations = [
migrations.RemoveField(
model_name='subscriber',
name='state',
),
migrations.AddField(
model_name='subscribe',
name='state',
field=models.PositiveIntegerField(choices=[(0, 'Unusable'), (1, 'Usable')], default=1, verbose_name='State'),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 2.2.7 on 2019-12-30 15:04
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('notification', '0007_auto_20191227_1426'),
]
operations = [
migrations.RemoveField(
model_name='subscribe',
name='state',
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 2.2.7 on 2019-12-31 01:31
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('location', '0033_merge_20191224_0920'),
('notification', '0008_remove_subscribe_state'),
]
operations = [
migrations.AddField(
model_name='subscriptiontype',
name='country',
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='location.Country', verbose_name='Country'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.7 on 2019-12-31 01:35
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('notification', '0009_subscriptiontype_country'),
]
operations = [
migrations.AlterField(
model_name='subscriptiontype',
name='country',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='location.Country', verbose_name='country'),
),
]

View File

@ -1,10 +1,14 @@
"""Notification app models."""
from django.conf import settings
from django.db import models
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from account.models import User
from location.models import Country
from utils.methods import generate_string_code
from utils.models import ProjectBaseMixin, TranslatedFieldsMixin, TJSONField
from utils.models import ProjectBaseMixin, TJSONField, TranslatedFieldsMixin
class SubscriptionType(ProjectBaseMixin, TranslatedFieldsMixin):
@ -12,6 +16,9 @@ class SubscriptionType(ProjectBaseMixin, TranslatedFieldsMixin):
name = TJSONField(blank=True, null=True, default=None,
verbose_name=_('name'),
help_text='{"en-GB":"some text"}')
country = models.ForeignKey(Country, on_delete=models.PROTECT,
blank=True, null=True, default=None,
verbose_name=_('country'))
# todo: associate user & subscriber after users registration
@ -19,7 +26,7 @@ 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, subscription_type=None, *args, **kwargs):
locale=None, subscription_types=None, *args, **kwargs):
"""Make subscriber and update info."""
# search existing object
if not user:
@ -40,14 +47,15 @@ class SubscriberManager(models.Manager):
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.subscription_type = subscription_type
obj.save()
obj.subscription_types.set(subscription_types)
obj.subscribe_set.update(unsubscribe_date=None)
else:
obj = self.model.objects.create(user=user, email=email, ip_address=ip_address,
country_code=country_code, locale=locale,
subscription_type=subscription_type)
country_code=country_code, locale=locale)
obj.subscription_types.set(subscription_types)
obj.subscribe_set.update(unsubscribe_date=None)
return obj
def associate_user(self, user):
@ -62,27 +70,9 @@ class SubscriberManager(models.Manager):
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.ForeignKey(
User,
blank=True,
@ -96,7 +86,6 @@ class Subscriber(ProjectBaseMixin):
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,
@ -107,9 +96,9 @@ class Subscriber(ProjectBaseMixin):
)
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
subscription_type = models.ForeignKey(SubscriptionType, on_delete=models.CASCADE, null=True, default=None)
subscription_types = models.ManyToManyField(SubscriptionType, through='Subscribe')
objects = SubscriberManager.from_queryset(SubscriberQuerySet)()
objects = SubscriberManager()
class Meta:
"""Meta class."""
@ -123,10 +112,9 @@ class Subscriber(ProjectBaseMixin):
self.update_code = generate_string_code()
return super(Subscriber, self).save(*args, **kwargs)
def unsubscribe(self):
def unsubscribe(self, query: dict):
"""Unsubscribe user."""
self.state = self.UNUSABLE
self.save()
self.subscribe_set.update(unsubscribe_date=now())
@property
def send_to(self):
@ -141,3 +129,19 @@ class Subscriber(ProjectBaseMixin):
url = settings.SITE_REDIRECT_URL_UNSUBSCRIBE
query = f'?code={self.update_code}'
return f'{schema}://{site_domain}{url}{query}'
class Subscribe(ProjectBaseMixin):
"""Subscribe model."""
subscribe_date = models.DateTimeField(_('Last subscribe date'), blank=True, null=True, default=now)
unsubscribe_date = models.DateTimeField(_('Last unsubscribe date'), blank=True, null=True, default=None)
subscriber = models.ForeignKey(Subscriber, on_delete=models.CASCADE)
subscription_type = models.ForeignKey(SubscriptionType, on_delete=models.CASCADE)
class Meta:
"""Meta class."""
verbose_name = _('Subscribe')
verbose_name_plural = _('Subscribes')

View File

@ -1,6 +1,8 @@
"""Notification app serializers."""
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from location.serializers import CountrySimpleSerializer
from notification import models
from utils.methods import get_user_ip
from utils.serializers import TranslatedField
@ -10,6 +12,8 @@ class SubscriptionTypeSerializer(serializers.ModelSerializer):
"""Subscription type serializer."""
name_translated = TranslatedField()
country = CountrySimpleSerializer()
class Meta:
"""Meta class."""
@ -18,14 +22,15 @@ class SubscriptionTypeSerializer(serializers.ModelSerializer):
'id',
'index_name',
'name_translated',
'country',
)
class SubscribeSerializer(serializers.ModelSerializer):
"""Subscribe serializer."""
class CreateSubscribeSerializer(serializers.ModelSerializer):
"""Create Subscribe serializer."""
email = serializers.EmailField(required=False, source='send_to')
subscription_type = SubscriptionTypeSerializer(read_only=True)
subscription_types = serializers.PrimaryKeyRelatedField(many=True, queryset=models.SubscriptionType.objects.all())
class Meta:
"""Meta class."""
@ -33,10 +38,9 @@ class SubscribeSerializer(serializers.ModelSerializer):
model = models.Subscriber
fields = (
'email',
'subscription_type',
'state',
'subscription_types',
'link_to_unsubscribe',
)
read_only_fields = ('state',)
def validate(self, attrs):
"""Validate attrs."""
@ -45,6 +49,10 @@ class SubscribeSerializer(serializers.ModelSerializer):
# validate email
email = attrs.get('send_to')
if attrs.get('email'):
email = attrs.get('email')
if user.is_authenticated:
if email is not None and email != user.email:
raise serializers.ValidationError(_('Does not match user email'))
@ -57,18 +65,40 @@ class SubscribeSerializer(serializers.ModelSerializer):
attrs['country_code'] = request.country_code
attrs['locale'] = request.locale
attrs['ip_address'] = get_user_ip(request)
if user.is_authenticated:
attrs['user'] = user
subscription_type_id = self.context.get('request').parser_context.get('kwargs').get("subscription_type_pk")
subscription_type_qs = models.SubscriptionType.objects.filter(id=subscription_type_id)
if not subscription_type_qs.exists():
raise serializers.ValidationError({'detail': _(f'SubscriptionType not found.')})
attrs["subscription_type"] = subscription_type_qs.first()
return attrs
def create(self, validated_data):
"""Create obj."""
subscriber = models.Subscriber.objects.make_subscriber(**validated_data)
return subscriber
return models.Subscriber.objects.make_subscriber(**validated_data)
class SubscribeObjectSerializer(serializers.ModelSerializer):
"""Subscribe serializer."""
class Meta:
"""Meta class."""
model = models.Subscriber
fields = ('subscriber', )
read_only_fields = ('subscribe_date', 'unsubscribe_date',)
class SubscribeSerializer(serializers.ModelSerializer):
"""Subscribe serializer."""
email = serializers.EmailField(required=False, source='send_to')
subscription_types = SubscriptionTypeSerializer(many=True, read_only=True)
class Meta:
"""Meta class."""
model = models.Subscriber
fields = (
'email',
'subscription_types',
'link_to_unsubscribe',
)

View File

@ -1,11 +1,15 @@
from datetime import datetime, timedelta
from http.cookies import SimpleCookie
from django.test import TestCase
from rest_framework.test import APITestCase
from rest_framework import status
from rest_framework.test import APITestCase
from account.models import User
from notification.models import Subscriber
from account.models import Role, User, UserRole
from location.models import Country
from main.models import SiteSettings
from news.models import News, NewsType
from notification.models import Subscriber, SubscriptionType
from translation.models import Language
class BaseTestCase(APITestCase):
@ -17,23 +21,22 @@ class BaseTestCase(APITestCase):
self.user = User.objects.create_user(username=self.username, email=self.email, password=self.password)
# get tokens
tokens = User.create_jwt_tokens(self.user)
self.client.cookies = SimpleCookie({'access_token': tokens.get('access_token'),
'refresh_token': tokens.get('refresh_token')})
self.client.cookies = SimpleCookie({
'access_token': tokens.get('access_token'),
'refresh_token': tokens.get('refresh_token')
})
class NotificationAnonSubscribeTestCase(APITestCase):
def test_subscribe(self):
test_data = {
"email": "test@email.com",
"state": 1
"email": "test@email.com"
}
response = self.client.post("/api/web/notifications/subscribe/", data=test_data, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.json()["email"], test_data["email"])
self.assertEqual(response.json()["state"], test_data["state"])
class NotificationSubscribeTestCase(BaseTestCase):
@ -42,21 +45,17 @@ class NotificationSubscribeTestCase(BaseTestCase):
super().setUp()
self.test_data = {
"email": self.email,
"state": 1
"email": self.email
}
def test_subscribe(self):
response = self.client.post("/api/web/notifications/subscribe/", data=self.test_data, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.json()["email"], self.email)
self.assertEqual(response.json()["state"], self.test_data["state"])
def test_subscribe_info_auth_user(self):
Subscriber.objects.create(user=self.user, email=self.email, state=1)
Subscriber.objects.create(user=self.user, email=self.email)
response = self.client.get("/api/web/notifications/subscribe-info/", data=self.test_data, format="json")
@ -66,13 +65,12 @@ class NotificationSubscribeTestCase(BaseTestCase):
class NotificationSubscribeInfoTestCase(APITestCase):
def test_subscribe_info(self):
self.username = 'sedragurda'
self.password = 'sedragurdaredips19'
self.email = 'sedragurda@desoz.com'
self.user = User.objects.create_user(username=self.username, email=self.email, password=self.password)
test_subscriber = Subscriber.objects.create(user=self.user, email=self.email, state=1)
test_subscriber = Subscriber.objects.create(user=self.user, email=self.email)
response = self.client.get(f"/api/web/notifications/subscribe-info/{test_subscriber.update_code}/")
@ -82,12 +80,10 @@ class NotificationSubscribeInfoTestCase(APITestCase):
class NotificationUnsubscribeAuthUserTestCase(BaseTestCase):
def test_unsubscribe_auth_user(self):
Subscriber.objects.create(user=self.user, email=self.email, state=1)
Subscriber.objects.create(user=self.user, email=self.email)
self.test_data = {
"email": self.email,
"state": 1
"email": self.email
}
response = self.client.patch("/api/web/notifications/unsubscribe/", data=self.test_data, format="json")
@ -104,13 +100,102 @@ class NotificationUnsubscribeTestCase(APITestCase):
self.user = User.objects.create_user(username=self.username, email=self.email, password=self.password)
self.test_data = {
"email": self.email,
"state": 1
"email": self.email
}
test_subscriber = Subscriber.objects.create(user=self.user, email=self.email, state=1)
test_subscriber = Subscriber.objects.create(user=self.user, email=self.email)
response = self.client.patch(f"/api/web/notifications/unsubscribe/{test_subscriber.update_code}/",
data=self.test_data, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
class NotificationManySubscribeTestCase(APITestCase):
def test_subscribe(self):
self.username = 'sedragurda'
self.password = 'sedragurdaredips19'
self.email = 'sedragurda@desoz.com'
self.user = User.objects.create_user(
username=self.username, email=self.email, password=self.password)
# get tokens
tokens = User.create_jwt_tokens(self.user)
self.client.cookies = SimpleCookie(
{
'access_token': tokens.get('access_token'),
'refresh_token': tokens.get('refresh_token')
})
self.test_news_type = NewsType.objects.create(name="Test news type")
self.lang, created = Language.objects.get_or_create(
title='Russia',
locale='ru-RU'
)
self.country_ru, created = Country.objects.get_or_create(
name={"en-GB": "Russian"}
)
self.site_ru, created = SiteSettings.objects.get_or_create(
subdomain='ru'
)
role = Role.objects.create(
role=Role.CONTENT_PAGE_MANAGER,
site_id=self.site_ru.id
)
role.save()
user_role = UserRole.objects.create(
user=self.user,
role=role
)
user_role.save()
self.test_news = News.objects.create(
created_by=self.user, modified_by=self.user,
title={"ru-RU": "Test news"},
news_type=self.test_news_type,
description={"ru-RU": "Description test news"},
end=datetime.now() + timedelta(hours=2),
state=News.PUBLISHED,
slugs={'en-GB': 'test-news-slug'},
country=self.country_ru,
site=self.site_ru
)
self.test_subscribe_type = SubscriptionType.objects.create(
index_name='test_index_name',
name={"ru-RU": "Test subscription type"}
)
test_data = {
'email': self.email,
'subscription_types_pk': [
self.test_subscribe_type.id
]
}
response = self.client.post("/api/web/notifications/subscribe/", data=test_data, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.json()["email"], test_data["email"])
def test_unsubscribe(self):
self.username = 'sedragurda'
self.password = 'sedragurdaredips19'
self.email = 'sedragurda@desoz.com'
self.user = User.objects.create_user(
username=self.username, email=self.email, password=self.password)
test_data = {
"email": self.email
}
test_subscriber = Subscriber.objects.create(user=self.user, email=self.email)
response = self.client.patch(f"/api/web/notifications/unsubscribe/{test_subscriber.update_code}/",
data=test_data, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@ -0,0 +1 @@
urlpatterns = []

View File

@ -5,7 +5,7 @@ from notification.views import common
app_name = "notification"
urlpatterns = [
path('subscribe/<int:subscription_type_pk>', common.SubscribeView.as_view(), name='subscribe'),
path('subscribe/', common.CreateSubscribeView.as_view(), name='create-subscribe'),
path('subscribe-info/', common.SubscribeInfoAuthUserView.as_view(), name='check-code-auth'),
path('subscribe-info/<code>/', common.SubscribeInfoView.as_view(), name='check-code'),
path('unsubscribe/', common.UnsubscribeAuthUserView.as_view(), name='unsubscribe-auth'),

View File

@ -2,22 +2,17 @@
from django.shortcuts import get_object_or_404
from rest_framework import generics, permissions
from rest_framework.response import Response
from notification import models
from notification.serializers import common as serializers
class SubscribeView(generics.GenericAPIView):
"""Subscribe View."""
class CreateSubscribeView(generics.CreateAPIView):
"""Create 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)
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.CreateSubscribeSerializer
class SubscribeInfoView(generics.RetrieveAPIView):
@ -25,7 +20,7 @@ class SubscribeInfoView(generics.RetrieveAPIView):
lookup_field = 'update_code'
lookup_url_kwarg = 'code'
permission_classes = (permissions.AllowAny, )
permission_classes = (permissions.AllowAny,)
queryset = models.Subscriber.objects.all()
serializer_class = serializers.SubscribeSerializer
@ -33,7 +28,7 @@ class SubscribeInfoView(generics.RetrieveAPIView):
class SubscribeInfoAuthUserView(generics.ListAPIView):
"""Subscribe info auth user view."""
permission_classes = (permissions.IsAuthenticated, )
permission_classes = (permissions.IsAuthenticated,)
serializer_class = serializers.SubscribeSerializer
def get_queryset(self):
@ -42,18 +37,18 @@ class SubscribeInfoAuthUserView(generics.ListAPIView):
return queryset.filter(user=user)
class UnsubscribeView(generics.GenericAPIView):
class UnsubscribeView(generics.UpdateAPIView):
"""Unsubscribe view."""
lookup_field = 'update_code'
lookup_url_kwarg = 'code'
permission_classes = (permissions.AllowAny, )
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()
obj.unsubscribe(request.query_params)
serializer = self.get_serializer(instance=obj)
return Response(data=serializer.data)
@ -61,14 +56,14 @@ class UnsubscribeView(generics.GenericAPIView):
class UnsubscribeAuthUserView(generics.GenericAPIView):
"""Unsubscribe auth user view."""
permission_classes = (permissions.IsAuthenticated, )
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()
obj.unsubscribe(request.query_params)
serializer = self.get_serializer(instance=obj)
return Response(data=serializer.data)
@ -78,4 +73,3 @@ class SubscriptionTypesView(generics.ListAPIView):
permission_classes = (permissions.AllowAny,)
queryset = models.SubscriptionType.objects.all()
serializer_class = serializers.SubscriptionTypeSerializer

View File

@ -58,6 +58,10 @@ class ProductType(TypeDefaultImageMixin, TranslatedFieldsMixin, ProjectBaseMixin
verbose_name = _('Product type')
verbose_name_plural = _('Product types')
@property
def label(self):
return transform_into_readable_str(self.index_name)
class ProductSubType(TypeDefaultImageMixin, TranslatedFieldsMixin, ProjectBaseMixin):
"""ProductSubtype model."""
@ -410,6 +414,27 @@ class Product(GalleryMixin, TranslatedFieldsMixin, BaseAttributes,
if self.wine_region:
return self.wine_region.name
@property
def metadata(self):
if self.product_type:
metadata = []
tag_categories = (
self.product_type.tag_categories.exclude(index_name__in=[
'business_tag', 'purchased_item', 'accepted_payments_de',
'accepted_payments_hr', 'drinks', 'bottles_per_year',
'serial_number', 'surface', 'cooperative', 'tag',
'outside_sits', 'private_room']).values_list('index_name', flat=True)
)
for category in tag_categories:
tags = self.tags.filter(category__index_name=category).values_list('value', flat=True)
if tags.exists():
category_tags = {category: []}
for tag in tags:
category_tags[category].append(tag)
metadata.append(category_tags)
return metadata
class OnlineProductManager(ProductManager):
"""Extended manger for OnlineProduct model."""

View File

@ -10,10 +10,12 @@ class TagsBaseFilterSet(filters.FilterSet):
# Object type choices
NEWS = 'news'
ESTABLISHMENT = 'establishment'
RECIPES = 'recipe'
TYPE_CHOICES = (
(NEWS, 'News'),
(ESTABLISHMENT, 'Establishment'),
(RECIPES, 'Recipe'),
)
type = filters.MultipleChoiceFilter(choices=TYPE_CHOICES,
@ -91,5 +93,7 @@ class TagsFilterSet(TagsBaseFilterSet):
if self.ESTABLISHMENT in value:
queryset = queryset.for_establishments().filter(category__value_type='list').filter(value__in=settings.ESTABLISHMENT_CHOSEN_TAGS).distinct(
'value')
if self.RECIPES in value:
queryset = queryset.for_news().filter(value__in=settings.RECIPES_CHOSEN_TAGS).distinct('value')
return queryset

View File

@ -3,16 +3,14 @@ import csv
import logging
import os
import tempfile
import xml.etree.ElementTree as ET
from smtplib import SMTPException
import docx
import xlsxwriter
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from docx.blkcntnr import BlockItemContainer
from timetable.models import Timetable
from docx.shared import RGBColor, Pt
import xml.etree.ElementTree as ET
from utils.methods import section_name_into_index_name
@ -83,13 +81,12 @@ class DocTemplate:
def template(self, data: list):
for instance in data:
instance = dict(instance)
element_id = instance.get('id')
index_name = section_name_into_index_name(instance.get('section_name'))
for obj in data:
obj = dict(obj)
index_name = section_name_into_index_name(obj.get('section_name'))
# ESTABLISHMENT HEADING (LEVEL 1)
self.add_heading(name=instance['name'],
self.add_heading(name=obj['name'],
font_style={'size': Pt(18), 'name': 'Palatino', 'bold': False},
level=1)
# ESTABLISHMENT TYPE PARAGRAPH
@ -102,13 +99,13 @@ class DocTemplate:
font_style={'size': Pt(13), 'name': 'Arial', 'bold': True},
level=2)
# CITY NAME HEADING (LEVEL 3)
self.add_heading(name=instance['city_name'],
self.add_heading(name=obj['city_name'],
font_style={'size': Pt(12), 'name': 'Arial', 'bold': True, 'italic': True},
color_rgb=(102, 102, 102))
self.add_empty_line()
# REVIEW HEADING (LEVEL 2)
review = instance.get('review')
review = obj.get('review')
if review:
self.add_heading(name='Review',
font_style={'size': Pt(13), 'name': 'Arial', 'bold': True},
@ -125,7 +122,7 @@ class DocTemplate:
self.add_empty_line()
# PHONE HEADING (LEVEL 2)
phones = instance.get('phones')
phones = obj.get('phones')
if phones:
self.add_heading(name='Phones',
font_style={'size': Pt(13), 'name': 'Arial', 'bold': True},
@ -135,18 +132,18 @@ class DocTemplate:
self.add_empty_line()
# ADDRESS HEADING (LEVEL 2)
address = instance.get('address')
address = obj.get('address')
if address:
self.add_heading(name='Address',
font_style={'size': Pt(13), 'name': 'Arial', 'bold': True},
level=2)
# ADDRESS DATA PARAGRAPH
self.add_paragraph(name=instance.get('address'),
self.add_paragraph(name=obj.get('address'),
font_style={'size': Pt(10), 'name': 'Arial'})
self.add_empty_line()
# TIMETABLE HEADING (LEVEL 2)
schedule = instance.get('schedule')
schedule = obj.get('schedule')
if schedule:
self.add_heading(name='Schedule',
font_style={'size': Pt(13), 'name': 'Arial', 'bold': True},
@ -160,7 +157,7 @@ class DocTemplate:
self.add_empty_line()
# PUBLIC MARK HEADING (LEVEL 2)
public_mark = instance.get('public_mark')
public_mark = obj.get('public_mark')
if public_mark:
self.add_heading(name='Mark',
font_style={'size': Pt(13), 'name': 'Arial', 'bold': True},
@ -171,7 +168,7 @@ class DocTemplate:
self.add_empty_line()
# TOQUE HEADING (LEVEL 2)
toque = instance.get('toque_number')
toque = obj.get('toque_number')
if toque:
self.add_heading(name='Toque',
font_style={'size': Pt(13), 'name': 'Arial', 'bold': True},
@ -182,7 +179,7 @@ class DocTemplate:
self.add_empty_line()
# TOQUE HEADING (LEVEL 2)
price_level = instance.get('price_level')
price_level = obj.get('price_level')
if price_level:
self.add_heading(name='Price level',
font_style={'size': Pt(13), 'name': 'Arial', 'bold': True},
@ -193,7 +190,7 @@ class DocTemplate:
self.add_empty_line()
# SERVICES HEADING (LEVEL 2)
services = instance.get('services')
services = obj.get('services')
if services:
self.add_heading(name='Services',
font_style={'size': Pt(13), 'name': 'Arial', 'bold': True},
@ -203,7 +200,7 @@ class DocTemplate:
self.add_empty_line()
# METADATA HEADING (LEVEL 2)
metadata = instance.get('metadata')
metadata = obj.get('metadata')
if metadata:
for obj in metadata:
for section, tags in obj.items():
@ -382,60 +379,28 @@ class SendGuideExport(SendExportBase):
name = self.guide.slug
return f'export_{name}.{self.file_type}'
def make_doc_file(self):
document = DocTemplate()
document.template(self.get_doc_data())
document.document.save(self.file_path)
def get_headers(self):
headers = list(self.get_data()[0].keys())
headers.pop(headers.index('node_name'))
self.success = True
return headers
def get_data(self):
return self.data
excluded_data = ['guide', ]
if self.guide.guide_type in [self.guide.ARTISAN, self.guide.RESTAURANT]:
excluded_data.extend([
'product_name',
'product_review',
'product_type',
'product_subtypes',
'product_address',
'product_city',
'product_metadata',
'wine_color_section_name',
def get_doc_data(self):
init_data = self.get_data()
objects = []
city_name = None
section_name = None
advertorial_page = None
for instance in init_data:
row_advertorial_page = instance.get('advertorial_page')
if row_advertorial_page:
advertorial_page = row_advertorial_page
else:
instance['advertorial_page'] = advertorial_page
row_city = instance.get('city_name')
if row_city:
city_name = row_city
else:
instance['city_name'] = city_name
row_section = instance.get('node_name')
if row_section.endswith('SectionNode'):
section_name = row_section
else:
instance['section_name'] = section_name
if instance.pop('node_name', None) == 'EstablishmentNode':
objects.append(instance.items())
return objects
def send(self):
self.get_file_method()
print(f'ok: {self.file_path}')
self.send_email()
def get_headers(self):
"""Get headers for model Establishment."""
exclude_headers = ['node_name', ]
headers = list(self.data[0].keys())
if self.guide.RESTAURANT or self.guide.ARTISAN:
exclude_headers.append('product_name', )
if self.guide.WINE:
exclude_headers.extend([
'name',
])
elif self.guide.guide_type == self.guide.WINE:
excluded_data.extend([
'public_mark',
'toque_number',
'schedule',
@ -446,13 +411,63 @@ class SendGuideExport(SendExportBase):
'review',
'price_level',
'metadata',
'public_mark',
'toque_number',
'schedule',
'phones',
'establishment_type',
'establishment_subtypes',
'city_name',
])
for name in set(exclude_headers):
headers.pop(headers.index(name))
else:
self.success = True
return headers
for obj in self.data:
for column in excluded_data:
obj.pop(column) if column in obj.keys() else None
return self.data
def get_doc_data(self):
init_data = self.get_data()
objects = []
city_name = None
section_name = None
ad_number_of_pages = None
ad_right_pages = None
for row in init_data:
row_advertorial = row.pop('advertorial')
if row_advertorial:
ad_number_of_pages = row_advertorial.get('number_of_pages')
ad_right_pages = row_advertorial.get('right_pages')
else:
row['ad_number_of_pages'] = ad_number_of_pages
row['ad_right_pages'] = ad_right_pages
row_city = row.get('city_name')
if row_city:
city_name = row_city
else:
row['city_name'] = city_name
row_section = row.get('node_name')
if row_section.endswith('SectionNode'):
section_name = row_section
else:
row['section_name'] = section_name
if row.pop('node_name', None) == 'EstablishmentNode':
objects.append(row.items())
return objects
def send(self):
self.get_file_method()
print(f'ok: {self.file_path}')
self.send_email()
def make_doc_file(self):
document = DocTemplate()
document.template(self.get_doc_data())
document.document.save(self.file_path)
self.success = True
def make_csv_file(self):
file_header = self.get_headers()
@ -464,6 +479,9 @@ class SendGuideExport(SendExportBase):
# Write headers to CSV file
file_writer.writerow(file_header)
city = None
ad_number_of_pages = None
ad_right_pages = None
for row in self.get_data():
row_city = row.get('city_name')
if row_city:
@ -471,29 +489,110 @@ class SendGuideExport(SendExportBase):
else:
row['city_name'] = city
row_advertorial = row.get('advertorial')
if row_advertorial:
ad_number_of_pages = row_advertorial.get('number_of_pages')
ad_right_pages = row_advertorial.get('right_pages')
else:
row['ad_number_of_pages'] = ad_number_of_pages
row['ad_right_pages'] = ad_right_pages
if row.pop("node_name") == "EstablishmentNode":
file_writer.writerow(row.values())
def make_xml_file(self):
# create the file structure
data = ET.Element('data')
items = ET.SubElement(data, 'items')
city = None
for row in self.get_data():
row_city = row.get('city_name')
if row_city:
city = row_city
else:
row['city_name'] = city
if self.guide.guide_type == self.guide.WINE:
# products
city = None
wine_color = None
establishment_name = None
establishment_address = None
ad_number_of_pages = None
ad_right_pages = None
if row.pop("node_name") == "EstablishmentNode":
for key, value in row.items():
item1 = ET.SubElement(items, 'item')
item1.set('name', key)
item1.text = str(value)
# create the file structure
data = ET.Element('data')
products = ET.SubElement(data, 'products')
# create a new XML file with the results
tree = ET.ElementTree(data)
with open(self.file_path, 'bw') as f:
tree.write(f)
self.success = True
for row in self.get_data():
row_advertorial = row.pop('advertorial')
if row_advertorial:
ad_number_of_pages = row_advertorial.get('number_of_pages')
ad_right_pages = row_advertorial.get('right_pages')
else:
row['ad_number_of_pages'] = ad_number_of_pages
row['ad_right_pages'] = ad_right_pages
row_establishment_address = row.get('address')
if row_establishment_address:
establishment_address = row_establishment_address
else:
row['establishment_name'] = establishment_address
row_establishment = row.pop('name')
if row_establishment:
establishment_name = row_establishment
else:
row['establishment_name'] = establishment_name
row_city = row.get('product_city')
if row_city:
city = row_city
else:
row['city_name'] = city
row_wine_color = row.pop('wine_color_section_name')
if row_wine_color:
wine_color = row_wine_color
else:
row['wine_color'] = wine_color
if row.pop("node_name") == "WineNode":
product = ET.SubElement(products, 'product')
for key, value in row.items():
prop = ET.SubElement(product, 'prop')
prop.set('name', key)
prop.text = str(value)
# create a new XML file with the results
tree = ET.ElementTree(data)
with open(self.file_path, 'bw') as f:
tree.write(f)
self.success = True
elif self.guide.guide_type in [self.guide.ARTISAN, self.guide.RESTAURANT]:
# establishment
# create the file structure
data = ET.Element('data')
items = ET.SubElement(data, 'cities')
city = None
ad_number_of_pages = None
ad_right_pages = None
for row in self.get_data():
row_advertorial = row.pop('advertorial')
if row_advertorial:
ad_number_of_pages = row_advertorial.get('number_of_pages')
ad_right_pages = row_advertorial.get('right_pages')
else:
row['ad_number_of_pages'] = ad_number_of_pages
row['ad_right_pages'] = ad_right_pages
row_city = row.get('city_name')
if row_city:
city = row_city
else:
row['city_name'] = city
if row.pop("node_name") == "EstablishmentNode":
item = ET.SubElement(items, 'city')
for key, value in row.items():
prop = ET.SubElement(item, 'prop')
prop.set('name', key)
prop.text = str(value)
# create a new XML file with the results
tree = ET.ElementTree(data)
with open(self.file_path, 'bw') as f:
tree.write(f)
self.success = True

View File

@ -30,7 +30,6 @@ MEDIA_ROOT = os.path.join(PUBLIC_ROOT, MEDIA_LOCATION)
THUMBNAIL_DEBUG = True
# DATABASES
DATABASES = {
'default': {
@ -119,3 +118,6 @@ EMAIL_HOST = 'smtp.gmail.com'
EMAIL_HOST_USER = 'anatolyfeteleu@gmail.com'
EMAIL_HOST_PASSWORD = 'nggrlnbehzksgmbt'
EMAIL_PORT = 587
# ADD TRANSFER TO INSTALLED APPS
INSTALLED_APPS.append('transfer.apps.TransferConfig')

View File

@ -10,6 +10,7 @@ urlpatterns = [
path('gallery/', include(('gallery.urls', 'gallery'), namespace='gallery')),
path('location/', include('location.urls.back')),
path('news/', include('news.urls.back')),
path('notifications/', include(('notification.urls.back', 'notification'), namespace='notification')),
path('review/', include('review.urls.back')),
path('tags/', include(('tag.urls.back', 'tag'), namespace='tag')),
path('products/', include(('product.urls.back', 'product'), namespace='product')),