Прикрутил отправку уведомлений о новом продукте и статьи
This commit is contained in:
parent
30b9bf1967
commit
99140bf8ca
@ -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:
|
||||||
|
|||||||
@ -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
|
||||||
@ -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)
|
||||||
@ -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):
|
||||||
|
|||||||
@ -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
13
products/signals.py
Normal 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)
|
||||||
@ -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}")
|
||||||
|
|||||||
16
programmer/templates/emails/new_article_notification.html
Normal file
16
programmer/templates/emails/new_article_notification.html
Normal 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>
|
||||||
16
programmer/templates/emails/new_product_notification.html
Normal file
16
programmer/templates/emails/new_product_notification.html
Normal 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>
|
||||||
Loading…
x
Reference in New Issue
Block a user