Прикрутил отправку уведомлений о новом продукте и статьи

This commit is contained in:
NikDizell 2026-06-20 14:10:46 +03:00
parent 30b9bf1967
commit 99140bf8ca
9 changed files with 181 additions and 4 deletions

View File

@ -4,6 +4,8 @@ from django.db import models
from django_ckeditor_5.widgets import CKEditor5Widget from django_ckeditor_5.widgets import CKEditor5Widget
from django.utils.html import format_html from django.utils.html import format_html
from django.urls import reverse from django.urls import reverse
from programmer.tasks import send_new_article_notification
from django.contrib import messages
@admin.register(Category) @admin.register(Category)
@ -25,7 +27,7 @@ class ArticleAdmin(admin.ModelAdmin):
# filter_horizontal = ('tags',) # для удобного выбора тегов # filter_horizontal = ('tags',) # для удобного выбора тегов
raw_id_fields = ('author',) # удобно при большом количестве пользователей raw_id_fields = ('author',) # удобно при большом количестве пользователей
date_hierarchy = 'time_create' date_hierarchy = 'time_create'
actions = ['make_published', 'make_unpublished'] actions = ['make_published', 'make_unpublished', 'send_notification']
fieldsets = ( fieldsets = (
(None, { (None, {
@ -63,6 +65,20 @@ class ArticleAdmin(admin.ModelAdmin):
queryset.update(is_published=False) queryset.update(is_published=False)
make_unpublished.short_description = "Снять с публикации" 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): def view_on_site(self, obj):
if obj.is_published: if obj.is_published:

View File

@ -4,3 +4,6 @@ from django.apps import AppConfig
class BlogConfig(AppConfig): class BlogConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'blog' name = 'blog'
def ready(self):
import blog.signals

View File

@ -3,10 +3,12 @@ from django.dispatch import receiver
from .models import Article from .models import Article
from programmer.models import Profile from programmer.models import Profile
from django.core.mail import send_mail from django.core.mail import send_mail
from programmer.tasks import send_new_article_notification
@receiver(post_save, sender=Article) @receiver(post_save, sender=Article)
def notify_about_new_article(sender, instance, created, **kwargs): def notify_about_new_article(sender, instance, created, **kwargs):
if created and instance.is_published: if created and instance.is_published:
profiles = Profile.objects.filter(notify_new_articles=True, user__is_active=True) profiles = Profile.objects.filter(notify_new_articles=True, user__is_active=True)
emails = [p.user.email for p in profiles if p.user.email] emails = [p.user.email for p in profiles if p.user.email]
# отправка письма (можно асинхронно) send_new_article_notification.delay(instance.id)

View File

@ -5,6 +5,8 @@ from django_ckeditor_5.widgets import CKEditor5Widget
from .models import ProductCategory, Configuration, Product, Order from .models import ProductCategory, Configuration, Product, Order
from programmer.utils.email_notifications import send_multiple_order_notifications, send_multiple_order_notifications_async from programmer.utils.email_notifications import send_multiple_order_notifications, send_multiple_order_notifications_async
from django.urls import reverse from django.urls import reverse
from programmer.tasks import send_new_product_notification
from django.contrib import messages
@admin.register(ProductCategory) @admin.register(ProductCategory)
@ -35,7 +37,7 @@ class ProductAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('title',)} prepopulated_fields = {'slug': ('title',)}
filter_horizontal = ('configurations',) filter_horizontal = ('configurations',)
date_hierarchy = 'time_create' date_hierarchy = 'time_create'
actions = ['make_published', 'make_unpublished'] actions = ['make_published', 'make_unpublished', 'send_notification']
fieldsets = ( fieldsets = (
(None, { (None, {
@ -72,6 +74,20 @@ class ProductAdmin(admin.ModelAdmin):
queryset.update(is_published=False) queryset.update(is_published=False)
make_unpublished.short_description = "Снять с публикации" 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) @admin.register(Order)
class OrderAdmin(admin.ModelAdmin): class OrderAdmin(admin.ModelAdmin):

View File

@ -4,3 +4,6 @@ from django.apps import AppConfig
class ProductsConfig(AppConfig): class ProductsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'products' name = 'products'
def ready(self):
import products.signals

13
products/signals.py Normal file
View File

@ -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)

View File

@ -1,8 +1,15 @@
from background_task import background from background_task import background
from django.conf import settings 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 import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
User = get_user_model()
@background(schedule=0) @background(schedule=0)
def send_order_notification_background(order_id): def send_order_notification_background(order_id):
@ -54,3 +61,88 @@ if shared_task is not None:
from products.models import Order from products.models import Order
orders = Order.objects.filter(id__in=order_ids) orders = Order.objects.filter(id__in=order_ids)
return send_multiple_order_notifications(orders) 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}")

View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<h2>Здравствуйте!</h2>
<p>На сайте <strong>{{ site_name }}</strong> вышла новая статья:</p>
<h3><a href="{{ site_url }}{{ article.get_absolute_url }}">{{ article.title }}</a></h3>
<p>{{ article.content|truncatewords:50|safe }}</p>
<p>Читать полностью: <a href="{{ site_url }}{{ article.get_absolute_url }}">{{ site_url }}{{ article.get_absolute_url }}</a></p>
<hr>
<p style="color: #666; font-size: 12px;">Вы получили это письмо, потому что подписаны на уведомления о новых статьях.</p>
<p style="color: #666; font-size: 12px;">Отписаться можно в <a href="{{ site_url }}{% url 'profile' %}">личном кабинете</a>.</p>
</body>
</html>

View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<h2>Здравствуйте!</h2>
<p>На сайте <strong>{{ site_name }}</strong> появился новый продукт:</p>
<h3><a href="{{ site_url }}{{ product.get_absolute_url }}">{{ product.title }}</a></h3>
<p>{{ product.short_description|truncatewords:40 }}</p>
<p>Подробнее: <a href="{{ site_url }}{{ product.get_absolute_url }}">{{ site_url }}{{ product.get_absolute_url }}</a></p>
<hr>
<p style="color: #666; font-size: 12px;">Вы получили это письмо, потому что подписаны на уведомления о новых продуктах.</p>
<p style="color: #666; font-size: 12px;">Отписаться можно в <a href="{{ site_url }}{% url 'profile' %}">личном кабинете</a>.</p>
</body>
</html>