diff --git a/blog/admin.py b/blog/admin.py index b29508c..8c5ea0e 100644 --- a/blog/admin.py +++ b/blog/admin.py @@ -4,6 +4,8 @@ from django.db import models from django_ckeditor_5.widgets import CKEditor5Widget from django.utils.html import format_html from django.urls import reverse +from programmer.tasks import send_new_article_notification +from django.contrib import messages @admin.register(Category) @@ -25,7 +27,7 @@ class ArticleAdmin(admin.ModelAdmin): # filter_horizontal = ('tags',) # для удобного выбора тегов raw_id_fields = ('author',) # удобно при большом количестве пользователей date_hierarchy = 'time_create' - actions = ['make_published', 'make_unpublished'] + actions = ['make_published', 'make_unpublished', 'send_notification'] fieldsets = ( (None, { @@ -63,6 +65,20 @@ class ArticleAdmin(admin.ModelAdmin): queryset.update(is_published=False) make_unpublished.short_description = "Снять с публикации" + def send_notification(self, request, queryset): + """Отправить уведомления подписчикам для выбранных статей.""" + published = queryset.filter(is_published=True) + count = published.count() + if count == 0: + self.message_user(request, "Нет опубликованных статей для отправки уведомлений.", level=messages.WARNING) + return + + for article in published: + send_new_article_notification.delay(article.id) + + self.message_user(request, f"Уведомления отправлены для {count} статей.") + send_notification.short_description = "Отправить уведомление подписчикам о новых статьях" + # Добавляем кнопку предпросмотра для черновиков def view_on_site(self, obj): if obj.is_published: diff --git a/blog/apps.py b/blog/apps.py index 94788a5..bc4a1d1 100644 --- a/blog/apps.py +++ b/blog/apps.py @@ -4,3 +4,6 @@ from django.apps import AppConfig class BlogConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'blog' + + def ready(self): + import blog.signals \ No newline at end of file diff --git a/blog/signals.py b/blog/signals.py index 1e358be..b1849e2 100644 --- a/blog/signals.py +++ b/blog/signals.py @@ -3,10 +3,12 @@ from django.dispatch import receiver from .models import Article from programmer.models import Profile from django.core.mail import send_mail +from programmer.tasks import send_new_article_notification + @receiver(post_save, sender=Article) def notify_about_new_article(sender, instance, created, **kwargs): if created and instance.is_published: profiles = Profile.objects.filter(notify_new_articles=True, user__is_active=True) emails = [p.user.email for p in profiles if p.user.email] - # отправка письма (можно асинхронно) \ No newline at end of file + send_new_article_notification.delay(instance.id) \ No newline at end of file diff --git a/products/admin.py b/products/admin.py index 065db27..e3ebe71 100644 --- a/products/admin.py +++ b/products/admin.py @@ -5,6 +5,8 @@ from django_ckeditor_5.widgets import CKEditor5Widget from .models import ProductCategory, Configuration, Product, Order from programmer.utils.email_notifications import send_multiple_order_notifications, send_multiple_order_notifications_async from django.urls import reverse +from programmer.tasks import send_new_product_notification +from django.contrib import messages @admin.register(ProductCategory) @@ -35,7 +37,7 @@ class ProductAdmin(admin.ModelAdmin): prepopulated_fields = {'slug': ('title',)} filter_horizontal = ('configurations',) date_hierarchy = 'time_create' - actions = ['make_published', 'make_unpublished'] + actions = ['make_published', 'make_unpublished', 'send_notification'] fieldsets = ( (None, { @@ -72,6 +74,20 @@ class ProductAdmin(admin.ModelAdmin): queryset.update(is_published=False) make_unpublished.short_description = "Снять с публикации" + def send_notification(self, request, queryset): + """Отправить уведомления подписчикам для выбранных продуктов.""" + published = queryset.filter(is_published=True) + count = published.count() + if count == 0: + self.message_user(request, "Нет опубликованных продуктов для отправки уведомлений.", level=messages.WARNING) + return + + for product in published: + send_new_product_notification.delay(product.id) + + self.message_user(request, f"Уведомления отправлены для {count} продуктов.") + send_notification.short_description = "Отправить уведомление подписчикам о новых продуктах" + @admin.register(Order) class OrderAdmin(admin.ModelAdmin): diff --git a/products/apps.py b/products/apps.py index 145a2ac..f57447d 100644 --- a/products/apps.py +++ b/products/apps.py @@ -4,3 +4,6 @@ from django.apps import AppConfig class ProductsConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'products' + + def ready(self): + import products.signals \ No newline at end of file diff --git a/products/signals.py b/products/signals.py new file mode 100644 index 0000000..013278c --- /dev/null +++ b/products/signals.py @@ -0,0 +1,13 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver +from .models import Product +from programmer.tasks import send_new_product_notification + + +@receiver(post_save, sender=Product) +def notify_about_new_product(sender, instance, created, **kwargs): + """ + Отправляет уведомления подписчикам, когда создаётся и публикуется новый продукт. + """ + if created and instance.is_published: + send_new_product_notification.delay(instance.id) \ No newline at end of file diff --git a/programmer/tasks.py b/programmer/tasks.py index e41c6cc..b9e5cc9 100644 --- a/programmer/tasks.py +++ b/programmer/tasks.py @@ -1,8 +1,15 @@ from background_task import background from django.conf import settings +from celery import shared_task +from django.core.mail import send_mail +from django.template.loader import render_to_string +from django.utils.html import strip_tags +from django.contrib.auth import get_user_model import logging + logger = logging.getLogger(__name__) +User = get_user_model() @background(schedule=0) def send_order_notification_background(order_id): @@ -53,4 +60,89 @@ if shared_task is not None: from .utils.email_notifications import send_multiple_order_notifications from products.models import Order orders = Order.objects.filter(id__in=order_ids) - return send_multiple_order_notifications(orders) \ No newline at end of file + return send_multiple_order_notifications(orders) + + +@shared_task +def send_new_article_notification(article_id): + """Отправка уведомлений подписчикам о новой статье.""" + from blog.models import Article + from programmer.models import Profile + + try: + article = Article.objects.select_related('category', 'author').get(id=article_id, is_published=True) + except Article.DoesNotExist: + logger.error(f"Article #{article_id} not found or not published") + return + + # Получаем email-адреса пользователей, подписанных на новые статьи + profiles = Profile.objects.filter(notify_new_articles=True, user__is_active=True) + emails = [p.user.email for p in profiles if p.user.email] + + if not emails: + logger.info(f"No subscribers for new articles, skipping notification for article #{article_id}") + return + + subject = f'📰 Новая статья: {article.title}' + context = { + 'article': article, + 'site_url': settings.ALLOWED_HOSTS[0] if settings.ALLOWED_HOSTS else 'localhost', + 'site_name': 'С.Н.А. Технологии', + } + html_message = render_to_string('emails/new_article_notification.html', context) + plain_message = strip_tags(html_message) + + try: + send_mail( + subject=subject, + message=plain_message, + from_email=settings.DEFAULT_FROM_EMAIL, + recipient_list=emails, + html_message=html_message, + fail_silently=False, + ) + logger.info(f"New article notification sent to {len(emails)} subscribers for article #{article_id}") + except Exception as e: + logger.error(f"Failed to send new article notification for article #{article_id}: {e}") + + +@shared_task +def send_new_product_notification(product_id): + """Отправка уведомлений подписчикам о новом продукте.""" + from products.models import Product + from programmer.models import Profile + + try: + product = Product.objects.select_related('category').get(id=product_id, is_published=True) + except Product.DoesNotExist: + logger.error(f"Product #{product_id} not found or not published") + return + + profiles = Profile.objects.filter(notify_new_products=True, user__is_active=True) + emails = [p.user.email for p in profiles if p.user.email] + + if not emails: + logger.info(f"No subscribers for new products, skipping notification for product #{product_id}") + return + + subject = f'🛒 Новый продукт: {product.title}' + context = { + 'product': product, + 'site_url': settings.ALLOWED_HOSTS[0] if settings.ALLOWED_HOSTS else 'localhost', + 'site_name': 'С.Н.А. Технологии', + } + html_message = render_to_string('emails/new_product_notification.html', context) + plain_message = strip_tags(html_message) + + try: + send_mail( + subject=subject, + message=plain_message, + from_email=settings.DEFAULT_FROM_EMAIL, + recipient_list=emails, + html_message=html_message, + fail_silently=False, + ) + logger.info(f"New product notification sent to {len(emails)} subscribers for product #{product_id}") + except Exception as e: + logger.error(f"Failed to send new product notification for product #{product_id}: {e}") diff --git a/programmer/templates/emails/new_article_notification.html b/programmer/templates/emails/new_article_notification.html new file mode 100644 index 0000000..950fab3 --- /dev/null +++ b/programmer/templates/emails/new_article_notification.html @@ -0,0 +1,16 @@ + + +
+ + + +На сайте {{ site_name }} вышла новая статья:
+{{ article.content|truncatewords:50|safe }}
+Читать полностью: {{ site_url }}{{ article.get_absolute_url }}
+Вы получили это письмо, потому что подписаны на уведомления о новых статьях.
+Отписаться можно в личном кабинете.
+ + \ No newline at end of file diff --git a/programmer/templates/emails/new_product_notification.html b/programmer/templates/emails/new_product_notification.html new file mode 100644 index 0000000..3b7d026 --- /dev/null +++ b/programmer/templates/emails/new_product_notification.html @@ -0,0 +1,16 @@ + + + + + + +На сайте {{ site_name }} появился новый продукт:
+{{ product.short_description|truncatewords:40 }}
+Подробнее: {{ site_url }}{{ product.get_absolute_url }}
+Вы получили это письмо, потому что подписаны на уведомления о новых продуктах.
+Отписаться можно в личном кабинете.
+ + \ No newline at end of file