From a450b1e2dfe0bc25bb19b9f3b29b078dfb569d3a Mon Sep 17 00:00:00 2001 From: ZUS666 Date: Wed, 25 Oct 2023 00:50:49 +0500 Subject: [PATCH 01/16] add user fields --- ...005_user_have_orders_user_is_subscribed.py | 23 +++++++++++++++++++ api_spot/users/models.py | 8 +++++++ 2 files changed, 31 insertions(+) create mode 100644 api_spot/users/migrations/0005_user_have_orders_user_is_subscribed.py diff --git a/api_spot/users/migrations/0005_user_have_orders_user_is_subscribed.py b/api_spot/users/migrations/0005_user_have_orders_user_is_subscribed.py new file mode 100644 index 00000000..f2dd11d6 --- /dev/null +++ b/api_spot/users/migrations/0005_user_have_orders_user_is_subscribed.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.5 on 2023-10-24 19:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0004_alter_user_email'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='have_orders', + field=models.BooleanField(default=False, verbose_name='Имеет заказы'), + ), + migrations.AddField( + model_name='user', + name='is_subscribed', + field=models.BooleanField(default=False, verbose_name='Подписан на рассылку'), + ), + ] diff --git a/api_spot/users/models.py b/api_spot/users/models.py index 7de28196..115d81b1 100644 --- a/api_spot/users/models.py +++ b/api_spot/users/models.py @@ -103,6 +103,14 @@ class User(AbstractBaseUser, PermissionsMixin): blank=True, default='', ) + is_subscribed = models.BooleanField( + 'Подписан на рассылку', + default=False, + ) + have_orders = models.BooleanField( + 'Имеет заказы', + default=False, + ) EMAIL_FIELD = 'email' USERNAME_FIELD = 'email' From 2c2054aea393203db4ed1559ab17930006101d67 Mon Sep 17 00:00:00 2001 From: ZUS666 Date: Wed, 25 Oct 2023 17:43:36 +0500 Subject: [PATCH 02/16] promo --- api_spot/promo/__init__.py | 0 api_spot/promo/admin.py | 12 ++ api_spot/promo/apps.py | 6 + api_spot/promo/constants.py | 4 + api_spot/promo/migrations/0001_initial.py | 46 +++++++ .../0002_alter_emailnews_promo_code.py | 19 +++ api_spot/promo/migrations/__init__.py | 0 api_spot/promo/models.py | 68 +++++++++ api_spot/promo/services.py | 73 ++++++++++ api_spot/promo/tasks.py | 38 ++++++ api_spot/promo/tests.py | 4 + api_spot/promo/validators.py | 16 +++ api_spot/promo/views.py | 4 + api_spot/templates/news_email.html | 129 ++++++++++++++++++ api_spot/templates/promocode_email.html | 0 api_spot/users/admin.py | 2 +- 16 files changed, 420 insertions(+), 1 deletion(-) create mode 100644 api_spot/promo/__init__.py create mode 100644 api_spot/promo/admin.py create mode 100644 api_spot/promo/apps.py create mode 100644 api_spot/promo/constants.py create mode 100644 api_spot/promo/migrations/0001_initial.py create mode 100644 api_spot/promo/migrations/0002_alter_emailnews_promo_code.py create mode 100644 api_spot/promo/migrations/__init__.py create mode 100644 api_spot/promo/models.py create mode 100644 api_spot/promo/services.py create mode 100644 api_spot/promo/tasks.py create mode 100644 api_spot/promo/tests.py create mode 100644 api_spot/promo/validators.py create mode 100644 api_spot/promo/views.py create mode 100644 api_spot/templates/news_email.html create mode 100644 api_spot/templates/promocode_email.html diff --git a/api_spot/promo/__init__.py b/api_spot/promo/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api_spot/promo/admin.py b/api_spot/promo/admin.py new file mode 100644 index 00000000..397e41f6 --- /dev/null +++ b/api_spot/promo/admin.py @@ -0,0 +1,12 @@ +from django.contrib import admin +from promo.models import EmailNews, Promocode + + +@admin.register(Promocode) +class PromcodeAdmin(admin.ModelAdmin): + pass + + +@admin.register(EmailNews) +class EmailNewsAdmin(admin.ModelAdmin): + pass diff --git a/api_spot/promo/apps.py b/api_spot/promo/apps.py new file mode 100644 index 00000000..9c8ef8cc --- /dev/null +++ b/api_spot/promo/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PromoConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'promo' diff --git a/api_spot/promo/constants.py b/api_spot/promo/constants.py new file mode 100644 index 00000000..2c9db078 --- /dev/null +++ b/api_spot/promo/constants.py @@ -0,0 +1,4 @@ +MAX_PROMO_DISCOUNT = 50 +MAX_PROMO_DISCOUNT_MESSAGE = f'Скидка не может превышать {MAX_PROMO_DISCOUNT}%' +NEWS_EMAIL_TEMPLATE = 'news_email.html' +PROMOCE_EMAIL_TEMPLATE = 'promocode_email.html' diff --git a/api_spot/promo/migrations/0001_initial.py b/api_spot/promo/migrations/0001_initial.py new file mode 100644 index 00000000..a118425c --- /dev/null +++ b/api_spot/promo/migrations/0001_initial.py @@ -0,0 +1,46 @@ +# Generated by Django 4.2.5 on 2023-10-25 09:19 + +from django.db import migrations, models +import django.db.models.deletion +import promo.validators + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Promocode', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=64, unique=True, verbose_name='Название промокода')), + ('percent_discount', models.PositiveSmallIntegerField(validators=[promo.validators.MaxDiscountValidator(50)], verbose_name='Процент скидки')), + ('expiry_date', models.DateField(validators=[promo.validators.validate_datetime_less_present], verbose_name='Дата истечения')), + ('balance', models.PositiveIntegerField(verbose_name='Количество использований')), + ], + options={ + 'verbose_name': 'Промокод', + 'verbose_name_plural': 'Промокоды', + 'ordering': ('expiry_date',), + }, + ), + migrations.CreateModel( + name='EmailNews', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('subject_message', models.CharField(max_length=128, verbose_name='Тема письма')), + ('text_message', models.TextField(verbose_name='Текст письма')), + ('send_datetime', models.DateTimeField(unique=True, validators=[promo.validators.validate_datetime_less_present], verbose_name='Дата и время отправки')), + ('promo_code', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='promocode', to='promo.promocode')), + ], + options={ + 'verbose_name': 'Email', + 'verbose_name_plural': 'Emails', + 'ordering': ('send_datetime',), + }, + ), + ] diff --git a/api_spot/promo/migrations/0002_alter_emailnews_promo_code.py b/api_spot/promo/migrations/0002_alter_emailnews_promo_code.py new file mode 100644 index 00000000..51c4cb03 --- /dev/null +++ b/api_spot/promo/migrations/0002_alter_emailnews_promo_code.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.5 on 2023-10-25 09:30 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('promo', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='emailnews', + name='promo_code', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='promocode', to='promo.promocode'), + ), + ] diff --git a/api_spot/promo/migrations/__init__.py b/api_spot/promo/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api_spot/promo/models.py b/api_spot/promo/models.py new file mode 100644 index 00000000..be3eff95 --- /dev/null +++ b/api_spot/promo/models.py @@ -0,0 +1,68 @@ +from django.db import models + +from promo.constants import MAX_PROMO_DISCOUNT +from promo.validators import ( + MaxDiscountValidator, validate_datetime_less_present, +) +from promo.services import create_task_after_save_promo_email + + +class Promocode(models.Model): + name = models.CharField( + 'Название промокода', + max_length=64, + unique=True, + ) + percent_discount = models.PositiveSmallIntegerField( + 'Процент скидки', + validators=(MaxDiscountValidator(MAX_PROMO_DISCOUNT),) + ) + expiry_date = models.DateField( + 'Дата истечения', + validators=(validate_datetime_less_present,), + ) + balance = models.PositiveIntegerField( + 'Количество использований', + ) + + def __str__(self): + return self.name + + class Meta: + verbose_name = 'Промокод' + verbose_name_plural = 'Промокоды' + ordering = ('expiry_date',) + + +class EmailNews(models.Model): + subject_message = models.CharField( + 'Тема письма', + max_length=128 + ) + text_message = models.TextField( + 'Текст письма' + ) + send_datetime = models.DateTimeField( + 'Дата и время отправки', + validators=(validate_datetime_less_present,), + unique=True, + ) + promo_code = models.ForeignKey( + Promocode, + related_name='promocode', + on_delete=models.CASCADE, + blank=True, + null=True, + ) + + def __str__(self): + return self.subject_message[:20] + + class Meta: + verbose_name = 'Email' + verbose_name_plural = 'Emails' + ordering = ('send_datetime',) + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + create_task_after_save_promo_email(self) diff --git a/api_spot/promo/services.py b/api_spot/promo/services.py new file mode 100644 index 00000000..8a3847b8 --- /dev/null +++ b/api_spot/promo/services.py @@ -0,0 +1,73 @@ +from django.contrib.auth import get_user_model +from django_celery_beat.models import ClockedSchedule, PeriodicTask + +from promo.constants import NEWS_EMAIL_TEMPLATE, PROMOCE_EMAIL_TEMPLATE + + +User = get_user_model() + + +def get_list_emails(): + """ + Возращает список пользователей подписанных на рассылку. + """ + return [User.objects.filter( + is_subscribed=True).values_list('email', flat=True) + ] + + +def get_data_news(text): + """ + Создание контекста для новостного письма. + """ + context = { + 'text_message': text + } + return context + + +def get_data_promo(promocode): + """ + Создание контекста для письма с промокодом. + """ + context = { + 'promocode': promocode.name, + 'percent_discount': promocode.percent_discount, + 'expiry_data': promocode.expiry_date + } + return context + + +def create_task_after_save_promo_email(obj): + """ + Принимает объект письма + """ + import json + from promo.tasks import create_chunk_task_send_mails + + context = get_data_news(obj.text_message) + template = NEWS_EMAIL_TEMPLATE + if obj.promo_code: + context = {**context, **get_data_promo(obj.promocode)} + template = PROMOCE_EMAIL_TEMPLATE + time = ClockedSchedule.objects.create(clocked_time=obj.send_datetime) + task = PeriodicTask.objects.create( + name=f'отправка письма {obj.subject_message}', + task='promo.tasks.create_chunk_task_send_mails', + clocked=time, + start_time=obj.send_datetime, + one_off=True, + enabled=True, + args=json.dumps([ + obj.subject_message, + template, + context, + ]) + ) + # task.run_tasks() + # tasks = [(self.celery_app.tasks.get(task.task), + # loads(task.args), + # loads(task.kwargs), + # task.queue, + # task.name) + # for task in queryset] \ No newline at end of file diff --git a/api_spot/promo/tasks.py b/api_spot/promo/tasks.py new file mode 100644 index 00000000..875dfb7b --- /dev/null +++ b/api_spot/promo/tasks.py @@ -0,0 +1,38 @@ +# from celery +# from django.conf import settings +from django.core.mail import EmailMultiAlternatives +from django.template.loader import render_to_string + +from api_spot.celery import app +from promo.services import get_list_emails + + +@app.task +def send_mail_news(user_email, subject, template, data, *args, **kwargs): + """ + Формирует и отправляет эл. письмо. + """ + html_body = render_to_string(template, data) + msg = EmailMultiAlternatives( + subject=subject, + to=[user_email] + ) + msg.attach_alternative(html_body, 'text/html') + msg.send() + return f'email {subject} sent to {user_email}' + + +@app.task +def create_chunk_task_send_mails(*args, **kwargs): + from information.models import Question + Question.objects.create(question='asdsad', answer='asdasd') + # list_emails = get_list_emails() + # send_mail_news.chunks( + # { + # 'user_email': email, + # # '' + # } for email in list_emails + # ) + # print(subject) + print(*args) + print(**kwargs) diff --git a/api_spot/promo/tests.py b/api_spot/promo/tests.py new file mode 100644 index 00000000..7c72b39d --- /dev/null +++ b/api_spot/promo/tests.py @@ -0,0 +1,4 @@ +from django.test import TestCase + + +# Create your tests here. diff --git a/api_spot/promo/validators.py b/api_spot/promo/validators.py new file mode 100644 index 00000000..5abbd95a --- /dev/null +++ b/api_spot/promo/validators.py @@ -0,0 +1,16 @@ +import datetime as dt + +from django.utils import timezone +from django.core.exceptions import ValidationError +from django.core.validators import MaxValueValidator + +from promo.constants import MAX_PROMO_DISCOUNT_MESSAGE + + +def validate_datetime_less_present(value): + if value < dt.datetime.now(): + raise ValidationError('Значение должно быть в будущем') + + +class MaxDiscountValidator(MaxValueValidator): + message = MAX_PROMO_DISCOUNT_MESSAGE diff --git a/api_spot/promo/views.py b/api_spot/promo/views.py new file mode 100644 index 00000000..dc1ba72f --- /dev/null +++ b/api_spot/promo/views.py @@ -0,0 +1,4 @@ +from django.shortcuts import render + + +# Create your views here. diff --git a/api_spot/templates/news_email.html b/api_spot/templates/news_email.html new file mode 100644 index 00000000..bb091bd5 --- /dev/null +++ b/api_spot/templates/news_email.html @@ -0,0 +1,129 @@ + + + + + Успешный сброс пароля + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ Красивое изображение +
+

Новостная рассылка

+
+

+ {{ text_message }} +

+
+

+ Связаться с нами +

+
+

Информационное взаимодействие

+

+ press@itcoworking.com +

+ +
+
+

Предложения и остальные вопросы

+

+ info@itcoworking.com +

+
+
+
+
+ + diff --git a/api_spot/templates/promocode_email.html b/api_spot/templates/promocode_email.html new file mode 100644 index 00000000..e69de29b diff --git a/api_spot/users/admin.py b/api_spot/users/admin.py index 25bf2a68..6e1b728c 100644 --- a/api_spot/users/admin.py +++ b/api_spot/users/admin.py @@ -13,7 +13,7 @@ class UserAdmin(DjangoUserAdmin): (None, {'fields': ('email', 'password')}), (_('Personal info'), {'fields': ('first_name', 'last_name', 'phone', - 'birth_date', 'occupation')}), + 'birth_date', 'occupation', 'is_subscribed')}), ( _('Permissions'), { From 6384d78ac8604cc6fac6ac609010a1199777bcc1 Mon Sep 17 00:00:00 2001 From: ZUS666 Date: Fri, 27 Oct 2023 03:51:07 +0500 Subject: [PATCH 03/16] should work --- api_spot/api_spot/celery.py | 4 + api_spot/api_spot/settings.py | 1 + api_spot/promo/admin.py | 17 ++- api_spot/promo/migrations/0001_initial.py | 9 +- .../0002_alter_emailnews_promo_code.py | 19 --- api_spot/promo/models.py | 19 ++- api_spot/promo/services.py | 54 ++------ api_spot/promo/tasks.py | 49 ++++--- api_spot/templates/promocode_email.html | 129 ++++++++++++++++++ 9 files changed, 205 insertions(+), 96 deletions(-) delete mode 100644 api_spot/promo/migrations/0002_alter_emailnews_promo_code.py diff --git a/api_spot/api_spot/celery.py b/api_spot/api_spot/celery.py index f67ba4b7..0b692be8 100644 --- a/api_spot/api_spot/celery.py +++ b/api_spot/api_spot/celery.py @@ -16,4 +16,8 @@ 'task': 'spots.tasks.repeat_orders_finish', 'schedule': crontab(hour='*/1', minute=1), }, + 'every_day_check_email': { + 'task': 'promo.tasks.every_day_check_today_email_task', + 'schedule': crontab(hour=15, minute=30), + }, } diff --git a/api_spot/api_spot/settings.py b/api_spot/api_spot/settings.py index 6b346b24..b19979d3 100644 --- a/api_spot/api_spot/settings.py +++ b/api_spot/api_spot/settings.py @@ -38,6 +38,7 @@ 'spots', 'information', 'api', + 'promo', 'django_cleanup.apps.CleanupConfig', ] diff --git a/api_spot/promo/admin.py b/api_spot/promo/admin.py index 397e41f6..9e8a0e3c 100644 --- a/api_spot/promo/admin.py +++ b/api_spot/promo/admin.py @@ -1,12 +1,25 @@ from django.contrib import admin + from promo.models import EmailNews, Promocode @admin.register(Promocode) class PromcodeAdmin(admin.ModelAdmin): - pass + list_display = ( + 'name', + 'percent_discount', + 'expiry_date', + 'balance', + ) @admin.register(EmailNews) class EmailNewsAdmin(admin.ModelAdmin): - pass + list_display = ( + 'subject_message', + 'send_date', + 'promocode', + 'is_sent', + 'text_message', + ) + exclude = ('is_sent',) diff --git a/api_spot/promo/migrations/0001_initial.py b/api_spot/promo/migrations/0001_initial.py index a118425c..ff6d956c 100644 --- a/api_spot/promo/migrations/0001_initial.py +++ b/api_spot/promo/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-10-25 09:19 +# Generated by Django 4.2.5 on 2023-10-26 22:45 from django.db import migrations, models import django.db.models.deletion @@ -34,13 +34,14 @@ class Migration(migrations.Migration): ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('subject_message', models.CharField(max_length=128, verbose_name='Тема письма')), ('text_message', models.TextField(verbose_name='Текст письма')), - ('send_datetime', models.DateTimeField(unique=True, validators=[promo.validators.validate_datetime_less_present], verbose_name='Дата и время отправки')), - ('promo_code', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='promocode', to='promo.promocode')), + ('send_date', models.DateField(unique=True, validators=[promo.validators.validate_datetime_less_present], verbose_name='Дата отправки')), + ('is_sent', models.BooleanField(default=False, verbose_name='Отправлен')), + ('promocode', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='promocodes', to='promo.promocode')), ], options={ 'verbose_name': 'Email', 'verbose_name_plural': 'Emails', - 'ordering': ('send_datetime',), + 'ordering': ('send_date',), }, ), ] diff --git a/api_spot/promo/migrations/0002_alter_emailnews_promo_code.py b/api_spot/promo/migrations/0002_alter_emailnews_promo_code.py deleted file mode 100644 index 51c4cb03..00000000 --- a/api_spot/promo/migrations/0002_alter_emailnews_promo_code.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 4.2.5 on 2023-10-25 09:30 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('promo', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='emailnews', - name='promo_code', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='promocode', to='promo.promocode'), - ), - ] diff --git a/api_spot/promo/models.py b/api_spot/promo/models.py index be3eff95..ed9150d6 100644 --- a/api_spot/promo/models.py +++ b/api_spot/promo/models.py @@ -4,7 +4,6 @@ from promo.validators import ( MaxDiscountValidator, validate_datetime_less_present, ) -from promo.services import create_task_after_save_promo_email class Promocode(models.Model): @@ -42,18 +41,22 @@ class EmailNews(models.Model): text_message = models.TextField( 'Текст письма' ) - send_datetime = models.DateTimeField( - 'Дата и время отправки', + send_date = models.DateField( + 'Дата отправки', validators=(validate_datetime_less_present,), unique=True, ) - promo_code = models.ForeignKey( + promocode = models.ForeignKey( Promocode, - related_name='promocode', + related_name='promocodes', on_delete=models.CASCADE, blank=True, null=True, ) + is_sent = models.BooleanField( + 'Отправлен', + default=False, + ) def __str__(self): return self.subject_message[:20] @@ -61,8 +64,4 @@ def __str__(self): class Meta: verbose_name = 'Email' verbose_name_plural = 'Emails' - ordering = ('send_datetime',) - - def save(self, *args, **kwargs): - super().save(*args, **kwargs) - create_task_after_save_promo_email(self) + ordering = ('send_date',) diff --git a/api_spot/promo/services.py b/api_spot/promo/services.py index 8a3847b8..5507b9b6 100644 --- a/api_spot/promo/services.py +++ b/api_spot/promo/services.py @@ -1,7 +1,6 @@ from django.contrib.auth import get_user_model -from django_celery_beat.models import ClockedSchedule, PeriodicTask -from promo.constants import NEWS_EMAIL_TEMPLATE, PROMOCE_EMAIL_TEMPLATE +from promo.models import Promocode User = get_user_model() @@ -11,63 +10,28 @@ def get_list_emails(): """ Возращает список пользователей подписанных на рассылку. """ - return [User.objects.filter( - is_subscribed=True).values_list('email', flat=True) - ] + return list(User.objects.filter( + is_subscribed=True).values_list('email', flat=True)) -def get_data_news(text): +def get_data_news(text_message): """ Создание контекста для новостного письма. """ context = { - 'text_message': text + 'text_message': text_message } return context -def get_data_promo(promocode): +def get_data_promo(promocode_id): """ Создание контекста для письма с промокодом. """ + promocode = Promocode.objects.get(id=promocode_id) context = { - 'promocode': promocode.name, + 'promocode_name': promocode.name, 'percent_discount': promocode.percent_discount, - 'expiry_data': promocode.expiry_date + 'expiry_date': promocode.expiry_date } return context - - -def create_task_after_save_promo_email(obj): - """ - Принимает объект письма - """ - import json - from promo.tasks import create_chunk_task_send_mails - - context = get_data_news(obj.text_message) - template = NEWS_EMAIL_TEMPLATE - if obj.promo_code: - context = {**context, **get_data_promo(obj.promocode)} - template = PROMOCE_EMAIL_TEMPLATE - time = ClockedSchedule.objects.create(clocked_time=obj.send_datetime) - task = PeriodicTask.objects.create( - name=f'отправка письма {obj.subject_message}', - task='promo.tasks.create_chunk_task_send_mails', - clocked=time, - start_time=obj.send_datetime, - one_off=True, - enabled=True, - args=json.dumps([ - obj.subject_message, - template, - context, - ]) - ) - # task.run_tasks() - # tasks = [(self.celery_app.tasks.get(task.task), - # loads(task.args), - # loads(task.kwargs), - # task.queue, - # task.name) - # for task in queryset] \ No newline at end of file diff --git a/api_spot/promo/tasks.py b/api_spot/promo/tasks.py index 875dfb7b..056a5f49 100644 --- a/api_spot/promo/tasks.py +++ b/api_spot/promo/tasks.py @@ -1,18 +1,22 @@ # from celery # from django.conf import settings from django.core.mail import EmailMultiAlternatives +from django.forms.models import model_to_dict from django.template.loader import render_to_string +from django.utils import timezone from api_spot.celery import app -from promo.services import get_list_emails +from promo.constants import NEWS_EMAIL_TEMPLATE, PROMOCE_EMAIL_TEMPLATE +from promo.models import EmailNews +from promo.services import get_data_news, get_data_promo, get_list_emails @app.task -def send_mail_news(user_email, subject, template, data, *args, **kwargs): +def send_mail_news(user_email, subject, template, context): """ Формирует и отправляет эл. письмо. """ - html_body = render_to_string(template, data) + html_body = render_to_string(template, context) msg = EmailMultiAlternatives( subject=subject, to=[user_email] @@ -23,16 +27,29 @@ def send_mail_news(user_email, subject, template, data, *args, **kwargs): @app.task -def create_chunk_task_send_mails(*args, **kwargs): - from information.models import Question - Question.objects.create(question='asdsad', answer='asdasd') - # list_emails = get_list_emails() - # send_mail_news.chunks( - # { - # 'user_email': email, - # # '' - # } for email in list_emails - # ) - # print(subject) - print(*args) - print(**kwargs) +def create_chunk_task_send_mail(email): + subject_message = email.get('subject_message') + text_message = email.get('text_message') + promocode_id = email.get('promocode') + context = get_data_news(text_message) + template = NEWS_EMAIL_TEMPLATE + if promocode_id: + context = {**context, **get_data_promo(promocode_id)} + template = PROMOCE_EMAIL_TEMPLATE + emails = get_list_emails() + chunk = [(email, subject_message, template, context) for email in emails] + send_mail_news.chunks((chunk), 5).apply_async() + + +@app.task +def every_day_check_today_email_task(): + email = EmailNews.objects.filter( + send_date=timezone.now() + ).first() + dict_email = model_to_dict(email) + if email: + create_chunk_task_send_mail(dict_email) + email.is_sent = True + email.save() + return f'{email.subject_message} sent' + return 'There are no emails to send today' diff --git a/api_spot/templates/promocode_email.html b/api_spot/templates/promocode_email.html index e69de29b..ced19aaf 100644 --- a/api_spot/templates/promocode_email.html +++ b/api_spot/templates/promocode_email.html @@ -0,0 +1,129 @@ + + + + + Успешный сброс пароля + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ Красивое изображение +
+

Промокод

+
+

+ {{ text_message }} Промокод: {{ promocode_name }} на {{ percent_discount }}% до {{ expiry_date }} +

+
+

+ Связаться с нами +

+
+

Информационное взаимодействие

+

+ press@itcoworking.com +

+ +
+
+

Предложения и остальные вопросы

+

+ info@itcoworking.com +

+
+
+
+
+ + From f3f474e884afa2343d97bfd86fa3fb259b9910a2 Mon Sep 17 00:00:00 2001 From: ZUS666 Date: Fri, 27 Oct 2023 03:55:55 +0500 Subject: [PATCH 04/16] fix --- api_spot/promo/tasks.py | 2 -- api_spot/promo/validators.py | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/api_spot/promo/tasks.py b/api_spot/promo/tasks.py index 056a5f49..0848eec9 100644 --- a/api_spot/promo/tasks.py +++ b/api_spot/promo/tasks.py @@ -1,5 +1,3 @@ -# from celery -# from django.conf import settings from django.core.mail import EmailMultiAlternatives from django.forms.models import model_to_dict from django.template.loader import render_to_string diff --git a/api_spot/promo/validators.py b/api_spot/promo/validators.py index 5abbd95a..5c0c2f34 100644 --- a/api_spot/promo/validators.py +++ b/api_spot/promo/validators.py @@ -1,6 +1,5 @@ import datetime as dt -from django.utils import timezone from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator @@ -8,7 +7,7 @@ def validate_datetime_less_present(value): - if value < dt.datetime.now(): + if value < dt.date.today(): raise ValidationError('Значение должно быть в будущем') From 0fc143d6a217d2b7aa3f5a0cd00af5f3f22d2e4f Mon Sep 17 00:00:00 2001 From: ZUS666 Date: Fri, 27 Oct 2023 03:58:42 +0500 Subject: [PATCH 05/16] delete empty files --- api_spot/promo/tests.py | 4 ---- api_spot/promo/views.py | 4 ---- 2 files changed, 8 deletions(-) delete mode 100644 api_spot/promo/tests.py delete mode 100644 api_spot/promo/views.py diff --git a/api_spot/promo/tests.py b/api_spot/promo/tests.py deleted file mode 100644 index 7c72b39d..00000000 --- a/api_spot/promo/tests.py +++ /dev/null @@ -1,4 +0,0 @@ -from django.test import TestCase - - -# Create your tests here. diff --git a/api_spot/promo/views.py b/api_spot/promo/views.py deleted file mode 100644 index dc1ba72f..00000000 --- a/api_spot/promo/views.py +++ /dev/null @@ -1,4 +0,0 @@ -from django.shortcuts import render - - -# Create your views here. From 1178bc25974b0dd449022b20e3b0dd8fb3766359 Mon Sep 17 00:00:00 2001 From: ZUS666 Date: Fri, 27 Oct 2023 04:10:09 +0500 Subject: [PATCH 06/16] add exclude is_sent --- api_spot/promo/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api_spot/promo/tasks.py b/api_spot/promo/tasks.py index 0848eec9..17853fc3 100644 --- a/api_spot/promo/tasks.py +++ b/api_spot/promo/tasks.py @@ -43,7 +43,7 @@ def create_chunk_task_send_mail(email): def every_day_check_today_email_task(): email = EmailNews.objects.filter( send_date=timezone.now() - ).first() + ).exclude(is_sent=True).first() dict_email = model_to_dict(email) if email: create_chunk_task_send_mail(dict_email) From 6c4ccbf9b136ea482ead62d062508b99236ce8cc Mon Sep 17 00:00:00 2001 From: Arseny Date: Fri, 27 Oct 2023 09:52:17 +0300 Subject: [PATCH 07/16] fix --- api_spot/spots/admin.py | 13 ++++++++++++- api_spot/spots/constants.py | 12 ++++++------ .../0013_alter_location_days_open.py | 18 ++++++++++++++++++ api_spot/templates/order_confirmation.html | 3 +-- 4 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 api_spot/spots/migrations/0013_alter_location_days_open.py diff --git a/api_spot/spots/admin.py b/api_spot/spots/admin.py index 2fbf7df3..32b7a10c 100644 --- a/api_spot/spots/admin.py +++ b/api_spot/spots/admin.py @@ -18,8 +18,19 @@ def preview(self, obj): ) +@admin.register(PlanPhoto) +class PlanPhotoAdmin(admin.ModelAdmin): + list_display = ('id', 'location', 'preview') + readonly_fields = ('preview', ) + + def preview(self, obj): + return mark_safe( + f'' + ) + + @admin.register(ExtraPhoto) -class ImageAdmin(admin.ModelAdmin): +class ExtraPhotoAdmin(admin.ModelAdmin): list_display = ('id', 'location', 'preview') def preview(self, obj): diff --git a/api_spot/spots/constants.py b/api_spot/spots/constants.py index 47736038..e62a9026 100644 --- a/api_spot/spots/constants.py +++ b/api_spot/spots/constants.py @@ -62,14 +62,14 @@ NAME_CACHE_WORKSPACE = 'workspace' NAME_CACHE_MEETING_ROOM = 'meeting_room' DAYS_CHOICES: tuple[str, str] = ( - ('пн-вс', 'пн-вс'), - ('пн-сб', 'пн-сб'), - ('пн-пт', 'пн-пт'), + ('Пн-Вс', 'Пн-Вс'), + ('Пн-Сб', 'Пн-Сб'), + ('Пн-Пт', 'Пн-Пт'), ) DAYS_DICT = { - 'вс': 6, - 'сб': 5, - 'пт': 4, + 'Вс': 6, + 'Сб': 5, + 'Пт': 4, } # Spot WORK_SPACE = 'Рабочее место' diff --git a/api_spot/spots/migrations/0013_alter_location_days_open.py b/api_spot/spots/migrations/0013_alter_location_days_open.py new file mode 100644 index 00000000..baed74e8 --- /dev/null +++ b/api_spot/spots/migrations/0013_alter_location_days_open.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.5 on 2023-10-27 06:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('spots', '0012_alter_review_options'), + ] + + operations = [ + migrations.AlterField( + model_name='location', + name='days_open', + field=models.CharField(choices=[('Пн-Вс', 'Пн-Вс'), ('Пн-Сб', 'Пн-Сб'), ('Пн-Пт', 'Пн-Пт')], default=('Пн-Вс', 'Пн-Вс'), max_length=32, verbose_name='Дни недели через -'), + ), + ] diff --git a/api_spot/templates/order_confirmation.html b/api_spot/templates/order_confirmation.html index fd6fe88f..daf47754 100644 --- a/api_spot/templates/order_confirmation.html +++ b/api_spot/templates/order_confirmation.html @@ -43,8 +43,7 @@

Здравствуйте, {{ first_name }} {{ last_name }}!

- Это письмо подтверждает ваше бронирование места в нашем коворкинге {{ location_name - }}. Не забудьте захватить документ удостоверяющий личность. + Это письмо подтверждает ваше бронирование места в нашем коворкинге {{ location_name }}. Не забудьте захватить документ удостоверяющий личность.

Date: Fri, 27 Oct 2023 12:20:53 +0500 Subject: [PATCH 10/16] subscribe --- api_spot/api/exceptions.py | 14 +++++++++++++ api_spot/api/serializers/users.py | 3 ++- api_spot/api/services/subscribe.py | 6 ++++++ api_spot/api/urls.py | 5 ++++- api_spot/api/views/__init__.py | 2 ++ api_spot/api/views/subscribe.py | 33 ++++++++++++++++++++++++++++++ 6 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 api_spot/api/services/subscribe.py create mode 100644 api_spot/api/views/subscribe.py diff --git a/api_spot/api/exceptions.py b/api_spot/api/exceptions.py index 60f56401..c99642ff 100644 --- a/api_spot/api/exceptions.py +++ b/api_spot/api/exceptions.py @@ -34,3 +34,17 @@ class AddSpotsError(exceptions.ValidationError): default_detail = { 'error': 'Название уже есть в локации' } + + +class SubscribedUserError(exceptions.ValidationError): + default_code = 'subscribe exists' + default_detail = { + 'error': 'Вы уже подписаны.' + } + + +class NotSubscribedUserError(exceptions.ValidationError): + default_code = 'subscribe does not exists' + default_detail = { + 'error': 'Вы не подписаны.' + } diff --git a/api_spot/api/serializers/users.py b/api_spot/api/serializers/users.py index 98d5aa16..68e0b3f4 100644 --- a/api_spot/api/serializers/users.py +++ b/api_spot/api/serializers/users.py @@ -66,8 +66,9 @@ class Meta: 'phone', 'birth_date', 'occupation', + 'is_subscribed' ) - read_only_fields = ('email',) + read_only_fields = ('email', 'is_subscribed') class EmailSerializer(serializers.Serializer): diff --git a/api_spot/api/services/subscribe.py b/api_spot/api/services/subscribe.py new file mode 100644 index 00000000..57262774 --- /dev/null +++ b/api_spot/api/services/subscribe.py @@ -0,0 +1,6 @@ +def subscribe_service(self, error, bool): + user = self.request.user + if user.is_subscribed is bool: + return error + user.is_subscribed = bool + user.save(update_fields='is_subscribed') diff --git a/api_spot/api/urls.py b/api_spot/api/urls.py index 62301eb4..cb92d1d8 100644 --- a/api_spot/api/urls.py +++ b/api_spot/api/urls.py @@ -6,7 +6,7 @@ LocationMapListAPIView, LocationShortListAPIView, LocationViewSet, OrderGetViewSet, OrderViewSet, PayView, PlanPhotoAPIView, QuestionViewSet, ReviewCreateViewSet, ReviewGetViewSet, RuleViewSet, SpotViewSet, - UserViewSet, + SubscireAPIView, UserViewSet, ) @@ -77,6 +77,9 @@ ), path('short_locations/', LocationShortListAPIView.as_view()), path('map_locations/', LocationMapListAPIView.as_view()), + path('subscribe', SubscireAPIView.as_view( + {'post': 'post', 'delete': 'delete'} + )), re_path( r'locations/(?P\d+)/plan_photo/', PlanPhotoAPIView.as_view() diff --git a/api_spot/api/views/__init__.py b/api_spot/api/views/__init__.py index 8289cdde..fe8b443d 100644 --- a/api_spot/api/views/__init__.py +++ b/api_spot/api/views/__init__.py @@ -12,6 +12,7 @@ from .review import ReviewCreateViewSet, ReviewGetViewSet from .rule import RuleViewSet from .spot import SpotViewSet +from .subscribe import SubscireAPIView from .users import UserViewSet @@ -31,4 +32,5 @@ ReviewGetViewSet RuleViewSet SpotViewSet +SubscireAPIView UserViewSet diff --git a/api_spot/api/views/subscribe.py b/api_spot/api/views/subscribe.py new file mode 100644 index 00000000..6bca15e5 --- /dev/null +++ b/api_spot/api/views/subscribe.py @@ -0,0 +1,33 @@ +from django.contrib.auth import get_user_model +from drf_spectacular.utils import extend_schema +from rest_framework import status +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from api.exceptions import NotSubscribedUserError, SubscribedUserError +from api.services.subscribe import subscribe_service + +User = get_user_model() + + +@extend_schema( + tags=('subscribe',) +) +class SubscireAPIView(APIView): + """ + Представление подписки и отписки на новостную рассылку. + """ + permission_classes = (IsAuthenticated,) + + def post(self): + subscribe_service(self, SubscribedUserError, True) + return Response( + {'message': 'Вы успешно подписались'}, status=status.HTTP_200_OK + ) + + def delete(self): + subscribe_service(self, NotSubscribedUserError, False) + return Response( + {'message': 'Вы успешно отписались'}, status=status.HTTP_200_OK + ) From 66aef7cf783175404c10b46ceaf58d1805cd6ded Mon Sep 17 00:00:00 2001 From: Arseny Date: Fri, 27 Oct 2023 10:28:55 +0300 Subject: [PATCH 11/16] fix/image --- .../migrations/0006_alter_avatar_image.py | 19 +++++++++++++++++++ api_spot/users/models.py | 3 ++- api_spot/users/service.py | 9 +++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 api_spot/users/migrations/0006_alter_avatar_image.py create mode 100644 api_spot/users/service.py diff --git a/api_spot/users/migrations/0006_alter_avatar_image.py b/api_spot/users/migrations/0006_alter_avatar_image.py new file mode 100644 index 00000000..0125169f --- /dev/null +++ b/api_spot/users/migrations/0006_alter_avatar_image.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.5 on 2023-10-27 07:26 + +from django.db import migrations, models +import users.service + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0005_avatar'), + ] + + operations = [ + migrations.AlterField( + model_name='avatar', + name='image', + field=models.ImageField(blank=True, null=True, upload_to=users.service.get_avatar_path, verbose_name='Аватар'), + ), + ] diff --git a/api_spot/users/models.py b/api_spot/users/models.py index 1c3761ab..5cbdb018 100644 --- a/api_spot/users/models.py +++ b/api_spot/users/models.py @@ -5,6 +5,7 @@ from django.utils import timezone from phonenumber_field import modelfields +from users.service import get_avatar_path from users.validators import NamesValidator, validate_birth_day @@ -132,7 +133,7 @@ class Avatar(models.Model): 'Аватар', blank=True, null=True, - upload_to='images/users/', + upload_to=get_avatar_path, ) class Meta: diff --git a/api_spot/users/service.py b/api_spot/users/service.py new file mode 100644 index 00000000..3919eaf2 --- /dev/null +++ b/api_spot/users/service.py @@ -0,0 +1,9 @@ +import uuid +import os + + +def get_avatar_path(instance, filename): + """Загрузка с уникальным именем.""" + ext = filename.split('.')[-1] + filename = "%s.%s" % (uuid.uuid4(), ext) + return os.path.join('images/users', filename) From c4f6f33874196c9ffaabb44bde25bd0dc0e78c8a Mon Sep 17 00:00:00 2001 From: ZUS666 Date: Fri, 27 Oct 2023 14:23:56 +0500 Subject: [PATCH 12/16] fix unused import --- api_spot/api/views/subscribe.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/api_spot/api/views/subscribe.py b/api_spot/api/views/subscribe.py index 6bca15e5..223902b5 100644 --- a/api_spot/api/views/subscribe.py +++ b/api_spot/api/views/subscribe.py @@ -1,4 +1,3 @@ -from django.contrib.auth import get_user_model from drf_spectacular.utils import extend_schema from rest_framework import status from rest_framework.permissions import IsAuthenticated @@ -8,8 +7,6 @@ from api.exceptions import NotSubscribedUserError, SubscribedUserError from api.services.subscribe import subscribe_service -User = get_user_model() - @extend_schema( tags=('subscribe',) From 168e03bb561c8582a29cb9bd07c5a52f7302b296 Mon Sep 17 00:00:00 2001 From: ZUS666 Date: Fri, 27 Oct 2023 14:40:46 +0500 Subject: [PATCH 13/16] migrations --- api_spot/api/urls.py | 4 +--- ...scribed.py => 0006_user_have_orders_user_is_subscribed.py} | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) rename api_spot/users/migrations/{0005_user_have_orders_user_is_subscribed.py => 0006_user_have_orders_user_is_subscribed.py} (85%) diff --git a/api_spot/api/urls.py b/api_spot/api/urls.py index 7f6f32df..a9443de5 100644 --- a/api_spot/api/urls.py +++ b/api_spot/api/urls.py @@ -77,9 +77,7 @@ ), path('short_locations/', LocationShortListAPIView.as_view()), path('map_locations/', LocationMapListAPIView.as_view()), - path('subscribe', SubscireAPIView.as_view( - {'post': 'post', 'delete': 'delete'} - )), + path('subscribe/', SubscireAPIView.as_view(), name='subscribe'), re_path( r'locations/(?P\d+)/plan_photo/', PlanPhotoAPIView.as_view() diff --git a/api_spot/users/migrations/0005_user_have_orders_user_is_subscribed.py b/api_spot/users/migrations/0006_user_have_orders_user_is_subscribed.py similarity index 85% rename from api_spot/users/migrations/0005_user_have_orders_user_is_subscribed.py rename to api_spot/users/migrations/0006_user_have_orders_user_is_subscribed.py index f2dd11d6..5abee837 100644 --- a/api_spot/users/migrations/0005_user_have_orders_user_is_subscribed.py +++ b/api_spot/users/migrations/0006_user_have_orders_user_is_subscribed.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-10-24 19:49 +# Generated by Django 4.2.5 on 2023-10-27 09:39 from django.db import migrations, models @@ -6,7 +6,7 @@ class Migration(migrations.Migration): dependencies = [ - ('users', '0004_alter_user_email'), + ('users', '0005_avatar'), ] operations = [ From 94d435005cb5cb9594e1d874f429855951a4c2ce Mon Sep 17 00:00:00 2001 From: Arseny Date: Fri, 27 Oct 2023 13:09:02 +0300 Subject: [PATCH 14/16] fix/nginx --- infra/nginx/default.conf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/infra/nginx/default.conf b/infra/nginx/default.conf index 74252f2e..38b9c2c4 100644 --- a/infra/nginx/default.conf +++ b/infra/nginx/default.conf @@ -1,7 +1,7 @@ server { listen 80; server_name spotit.acceleratorpracticum.ru; - client_max_body_size 10m; + client_max_body_size 3m; server_tokens off; location /.well-known/acme-challenge/ { @@ -17,6 +17,7 @@ server { server { listen 443 ssl; server_name spotit.acceleratorpracticum.ru; + client_max_body_size 3m; server_tokens off; ssl_certificate /etc/letsencrypt/live/spotit.acceleratorpracticum.ru/fullchain.pem; From 0c75032a89949cfcae44e5a5f89b7055a3b4e349 Mon Sep 17 00:00:00 2001 From: ZUS666 Date: Fri, 27 Oct 2023 15:12:29 +0500 Subject: [PATCH 15/16] fix mistakes --- api_spot/api/services/subscribe.py | 4 ++-- api_spot/api/urls.py | 2 +- api_spot/api/views/subscribe.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api_spot/api/services/subscribe.py b/api_spot/api/services/subscribe.py index 57262774..26a7aea1 100644 --- a/api_spot/api/services/subscribe.py +++ b/api_spot/api/services/subscribe.py @@ -1,6 +1,6 @@ def subscribe_service(self, error, bool): user = self.request.user if user.is_subscribed is bool: - return error + raise error user.is_subscribed = bool - user.save(update_fields='is_subscribed') + user.save(update_fields=['is_subscribed']) diff --git a/api_spot/api/urls.py b/api_spot/api/urls.py index a9443de5..89f259c1 100644 --- a/api_spot/api/urls.py +++ b/api_spot/api/urls.py @@ -77,7 +77,7 @@ ), path('short_locations/', LocationShortListAPIView.as_view()), path('map_locations/', LocationMapListAPIView.as_view()), - path('subscribe/', SubscireAPIView.as_view(), name='subscribe'), + path('subscribe/', SubscireAPIView.as_view(),), re_path( r'locations/(?P\d+)/plan_photo/', PlanPhotoAPIView.as_view() diff --git a/api_spot/api/views/subscribe.py b/api_spot/api/views/subscribe.py index 223902b5..fd6ab5e1 100644 --- a/api_spot/api/views/subscribe.py +++ b/api_spot/api/views/subscribe.py @@ -17,13 +17,13 @@ class SubscireAPIView(APIView): """ permission_classes = (IsAuthenticated,) - def post(self): + def post(self, request): subscribe_service(self, SubscribedUserError, True) return Response( {'message': 'Вы успешно подписались'}, status=status.HTTP_200_OK ) - def delete(self): + def delete(self, request): subscribe_service(self, NotSubscribedUserError, False) return Response( {'message': 'Вы успешно отписались'}, status=status.HTTP_200_OK From 7f134427d1a7053240b54b877beb0d11ef19a3f3 Mon Sep 17 00:00:00 2001 From: ZUS666 Date: Fri, 27 Oct 2023 15:47:38 +0500 Subject: [PATCH 16/16] fix --- ...scribed.py => 0007_user_have_orders_user_is_subscribed.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename api_spot/users/migrations/{0006_user_have_orders_user_is_subscribed.py => 0007_user_have_orders_user_is_subscribed.py} (85%) diff --git a/api_spot/users/migrations/0006_user_have_orders_user_is_subscribed.py b/api_spot/users/migrations/0007_user_have_orders_user_is_subscribed.py similarity index 85% rename from api_spot/users/migrations/0006_user_have_orders_user_is_subscribed.py rename to api_spot/users/migrations/0007_user_have_orders_user_is_subscribed.py index 5abee837..bf13a082 100644 --- a/api_spot/users/migrations/0006_user_have_orders_user_is_subscribed.py +++ b/api_spot/users/migrations/0007_user_have_orders_user_is_subscribed.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-10-27 09:39 +# Generated by Django 4.2.5 on 2023-10-27 10:47 from django.db import migrations, models @@ -6,7 +6,7 @@ class Migration(migrations.Migration): dependencies = [ - ('users', '0005_avatar'), + ('users', '0006_alter_avatar_image'), ] operations = [