final all done 2nd last plan before goign live
This commit is contained in:
1
backend/igny8_core/business/notifications/__init__.py
Normal file
1
backend/igny8_core/business/notifications/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Notifications module
|
||||
40
backend/igny8_core/business/notifications/admin.py
Normal file
40
backend/igny8_core/business/notifications/admin.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
Notification Admin Configuration
|
||||
"""
|
||||
|
||||
from django.contrib import admin
|
||||
from unfold.admin import ModelAdmin
|
||||
|
||||
from .models import Notification
|
||||
|
||||
|
||||
@admin.register(Notification)
|
||||
class NotificationAdmin(ModelAdmin):
|
||||
list_display = ['title', 'notification_type', 'severity', 'account', 'user', 'is_read', 'created_at']
|
||||
list_filter = ['notification_type', 'severity', 'is_read', 'created_at']
|
||||
search_fields = ['title', 'message', 'account__name', 'user__email']
|
||||
readonly_fields = ['created_at', 'updated_at', 'read_at']
|
||||
ordering = ['-created_at']
|
||||
|
||||
fieldsets = (
|
||||
('Notification', {
|
||||
'fields': ('account', 'user', 'notification_type', 'severity')
|
||||
}),
|
||||
('Content', {
|
||||
'fields': ('title', 'message', 'site')
|
||||
}),
|
||||
('Action', {
|
||||
'fields': ('action_url', 'action_label')
|
||||
}),
|
||||
('Status', {
|
||||
'fields': ('is_read', 'read_at')
|
||||
}),
|
||||
('Metadata', {
|
||||
'fields': ('metadata',),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
13
backend/igny8_core/business/notifications/apps.py
Normal file
13
backend/igny8_core/business/notifications/apps.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
Notifications App Configuration
|
||||
"""
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class NotificationsConfig(AppConfig):
|
||||
"""Configuration for the notifications app."""
|
||||
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'igny8_core.business.notifications'
|
||||
label = 'notifications'
|
||||
verbose_name = 'Notifications'
|
||||
@@ -0,0 +1,45 @@
|
||||
# Generated by Django 5.2.9 on 2025-12-27 22:02
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('igny8_core_auth', '0018_add_country_remove_intent_seedkeyword'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Notification',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('notification_type', models.CharField(choices=[('ai_cluster_complete', 'Clustering Complete'), ('ai_cluster_failed', 'Clustering Failed'), ('ai_ideas_complete', 'Ideas Generated'), ('ai_ideas_failed', 'Idea Generation Failed'), ('ai_content_complete', 'Content Generated'), ('ai_content_failed', 'Content Generation Failed'), ('ai_images_complete', 'Images Generated'), ('ai_images_failed', 'Image Generation Failed'), ('ai_prompts_complete', 'Image Prompts Created'), ('ai_prompts_failed', 'Image Prompts Failed'), ('content_ready_review', 'Content Ready for Review'), ('content_published', 'Content Published'), ('content_publish_failed', 'Publishing Failed'), ('wordpress_sync_success', 'WordPress Sync Complete'), ('wordpress_sync_failed', 'WordPress Sync Failed'), ('credits_low', 'Credits Running Low'), ('credits_depleted', 'Credits Depleted'), ('site_setup_complete', 'Site Setup Complete'), ('keywords_imported', 'Keywords Imported'), ('system_info', 'System Information')], default='system_info', max_length=50)),
|
||||
('title', models.CharField(max_length=200)),
|
||||
('message', models.TextField()),
|
||||
('severity', models.CharField(choices=[('info', 'Info'), ('success', 'Success'), ('warning', 'Warning'), ('error', 'Error')], default='info', max_length=20)),
|
||||
('object_id', models.PositiveIntegerField(blank=True, null=True)),
|
||||
('action_url', models.CharField(blank=True, max_length=500, null=True)),
|
||||
('action_label', models.CharField(blank=True, max_length=50, null=True)),
|
||||
('is_read', models.BooleanField(default=False)),
|
||||
('read_at', models.DateTimeField(blank=True, null=True)),
|
||||
('metadata', models.JSONField(blank=True, default=dict)),
|
||||
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account')),
|
||||
('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
||||
('site', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to='igny8_core_auth.site')),
|
||||
('user', models.ForeignKey(blank=True, help_text='If null, notification is visible to all account users', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created_at'],
|
||||
'indexes': [models.Index(fields=['account', '-created_at'], name='notificatio_tenant__3b20a7_idx'), models.Index(fields=['account', 'is_read', '-created_at'], name='notificatio_tenant__9a5521_idx'), models.Index(fields=['user', '-created_at'], name='notificatio_user_id_05b4bc_idx')],
|
||||
},
|
||||
),
|
||||
]
|
||||
191
backend/igny8_core/business/notifications/models.py
Normal file
191
backend/igny8_core/business/notifications/models.py
Normal file
@@ -0,0 +1,191 @@
|
||||
"""
|
||||
Notification Models for IGNY8
|
||||
|
||||
This module provides a notification system for tracking AI operations,
|
||||
workflow events, and system alerts.
|
||||
"""
|
||||
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from igny8_core.auth.models import AccountBaseModel
|
||||
|
||||
|
||||
class NotificationType(models.TextChoices):
|
||||
"""Notification type choices"""
|
||||
# AI Operations
|
||||
AI_CLUSTER_COMPLETE = 'ai_cluster_complete', 'Clustering Complete'
|
||||
AI_CLUSTER_FAILED = 'ai_cluster_failed', 'Clustering Failed'
|
||||
AI_IDEAS_COMPLETE = 'ai_ideas_complete', 'Ideas Generated'
|
||||
AI_IDEAS_FAILED = 'ai_ideas_failed', 'Idea Generation Failed'
|
||||
AI_CONTENT_COMPLETE = 'ai_content_complete', 'Content Generated'
|
||||
AI_CONTENT_FAILED = 'ai_content_failed', 'Content Generation Failed'
|
||||
AI_IMAGES_COMPLETE = 'ai_images_complete', 'Images Generated'
|
||||
AI_IMAGES_FAILED = 'ai_images_failed', 'Image Generation Failed'
|
||||
AI_PROMPTS_COMPLETE = 'ai_prompts_complete', 'Image Prompts Created'
|
||||
AI_PROMPTS_FAILED = 'ai_prompts_failed', 'Image Prompts Failed'
|
||||
|
||||
# Workflow
|
||||
CONTENT_READY_REVIEW = 'content_ready_review', 'Content Ready for Review'
|
||||
CONTENT_PUBLISHED = 'content_published', 'Content Published'
|
||||
CONTENT_PUBLISH_FAILED = 'content_publish_failed', 'Publishing Failed'
|
||||
|
||||
# WordPress Sync
|
||||
WORDPRESS_SYNC_SUCCESS = 'wordpress_sync_success', 'WordPress Sync Complete'
|
||||
WORDPRESS_SYNC_FAILED = 'wordpress_sync_failed', 'WordPress Sync Failed'
|
||||
|
||||
# Credits/Billing
|
||||
CREDITS_LOW = 'credits_low', 'Credits Running Low'
|
||||
CREDITS_DEPLETED = 'credits_depleted', 'Credits Depleted'
|
||||
|
||||
# Setup
|
||||
SITE_SETUP_COMPLETE = 'site_setup_complete', 'Site Setup Complete'
|
||||
KEYWORDS_IMPORTED = 'keywords_imported', 'Keywords Imported'
|
||||
|
||||
# System
|
||||
SYSTEM_INFO = 'system_info', 'System Information'
|
||||
|
||||
|
||||
class NotificationSeverity(models.TextChoices):
|
||||
"""Notification severity choices"""
|
||||
INFO = 'info', 'Info'
|
||||
SUCCESS = 'success', 'Success'
|
||||
WARNING = 'warning', 'Warning'
|
||||
ERROR = 'error', 'Error'
|
||||
|
||||
|
||||
class Notification(AccountBaseModel):
|
||||
"""
|
||||
Notification model for tracking events and alerts
|
||||
|
||||
Notifications are account-scoped (via AccountBaseModel) and can optionally target specific users.
|
||||
They support generic relations to link to any related object.
|
||||
"""
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='notifications',
|
||||
help_text='If null, notification is visible to all account users'
|
||||
)
|
||||
|
||||
# Notification content
|
||||
notification_type = models.CharField(
|
||||
max_length=50,
|
||||
choices=NotificationType.choices,
|
||||
default=NotificationType.SYSTEM_INFO
|
||||
)
|
||||
title = models.CharField(max_length=200)
|
||||
message = models.TextField()
|
||||
severity = models.CharField(
|
||||
max_length=20,
|
||||
choices=NotificationSeverity.choices,
|
||||
default=NotificationSeverity.INFO
|
||||
)
|
||||
|
||||
# Related site (optional)
|
||||
site = models.ForeignKey(
|
||||
'igny8_core_auth.Site',
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='notifications'
|
||||
)
|
||||
|
||||
# Generic relation to any object
|
||||
content_type = models.ForeignKey(
|
||||
ContentType,
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
object_id = models.PositiveIntegerField(null=True, blank=True)
|
||||
content_object = GenericForeignKey('content_type', 'object_id')
|
||||
|
||||
# Action
|
||||
action_url = models.CharField(max_length=500, null=True, blank=True)
|
||||
action_label = models.CharField(max_length=50, null=True, blank=True)
|
||||
|
||||
# Status
|
||||
is_read = models.BooleanField(default=False)
|
||||
read_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
# Metadata for counts/details
|
||||
metadata = models.JSONField(default=dict, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['account', '-created_at']),
|
||||
models.Index(fields=['account', 'is_read', '-created_at']),
|
||||
models.Index(fields=['user', '-created_at']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title} ({self.notification_type})"
|
||||
|
||||
def mark_as_read(self):
|
||||
"""Mark notification as read"""
|
||||
if not self.is_read:
|
||||
from django.utils import timezone
|
||||
self.is_read = True
|
||||
self.read_at = timezone.now()
|
||||
self.save(update_fields=['is_read', 'read_at', 'updated_at'])
|
||||
|
||||
@classmethod
|
||||
def create_notification(
|
||||
cls,
|
||||
account,
|
||||
notification_type: str,
|
||||
title: str,
|
||||
message: str,
|
||||
severity: str = NotificationSeverity.INFO,
|
||||
user=None,
|
||||
site=None,
|
||||
content_object=None,
|
||||
action_url: str = None,
|
||||
action_label: str = None,
|
||||
metadata: dict = None
|
||||
):
|
||||
"""
|
||||
Factory method to create notifications
|
||||
|
||||
Args:
|
||||
account: The account this notification belongs to
|
||||
notification_type: Type from NotificationType choices
|
||||
title: Notification title
|
||||
message: Notification message body
|
||||
severity: Severity level from NotificationSeverity choices
|
||||
user: Optional specific user (if None, visible to all account users)
|
||||
site: Optional related site
|
||||
content_object: Optional related object (using GenericForeignKey)
|
||||
action_url: Optional URL for action button
|
||||
action_label: Optional label for action button
|
||||
metadata: Optional dict with additional data (counts, etc.)
|
||||
|
||||
Returns:
|
||||
Created Notification instance
|
||||
"""
|
||||
notification = cls(
|
||||
account=account,
|
||||
user=user,
|
||||
notification_type=notification_type,
|
||||
title=title,
|
||||
message=message,
|
||||
severity=severity,
|
||||
site=site,
|
||||
action_url=action_url,
|
||||
action_label=action_label,
|
||||
metadata=metadata or {}
|
||||
)
|
||||
|
||||
if content_object:
|
||||
notification.content_type = ContentType.objects.get_for_model(content_object)
|
||||
notification.object_id = content_object.pk
|
||||
|
||||
notification.save()
|
||||
return notification
|
||||
90
backend/igny8_core/business/notifications/serializers.py
Normal file
90
backend/igny8_core/business/notifications/serializers.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
Notification Serializers
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import Notification
|
||||
|
||||
|
||||
class NotificationSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for Notification model"""
|
||||
|
||||
site_name = serializers.CharField(source='site.name', read_only=True, default=None)
|
||||
|
||||
class Meta:
|
||||
model = Notification
|
||||
fields = [
|
||||
'id',
|
||||
'notification_type',
|
||||
'title',
|
||||
'message',
|
||||
'severity',
|
||||
'site',
|
||||
'site_name',
|
||||
'action_url',
|
||||
'action_label',
|
||||
'is_read',
|
||||
'read_at',
|
||||
'metadata',
|
||||
'created_at',
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'read_at']
|
||||
|
||||
|
||||
class NotificationListSerializer(serializers.ModelSerializer):
|
||||
"""Lightweight serializer for notification lists"""
|
||||
|
||||
site_name = serializers.CharField(source='site.name', read_only=True, default=None)
|
||||
time_ago = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Notification
|
||||
fields = [
|
||||
'id',
|
||||
'notification_type',
|
||||
'title',
|
||||
'message',
|
||||
'severity',
|
||||
'site_name',
|
||||
'action_url',
|
||||
'action_label',
|
||||
'is_read',
|
||||
'created_at',
|
||||
'time_ago',
|
||||
'metadata',
|
||||
]
|
||||
|
||||
def get_time_ago(self, obj):
|
||||
"""Return human-readable time since notification"""
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
now = timezone.now()
|
||||
diff = now - obj.created_at
|
||||
|
||||
if diff < timedelta(minutes=1):
|
||||
return 'Just now'
|
||||
elif diff < timedelta(hours=1):
|
||||
minutes = int(diff.total_seconds() / 60)
|
||||
return f'{minutes} minute{"s" if minutes != 1 else ""} ago'
|
||||
elif diff < timedelta(days=1):
|
||||
hours = int(diff.total_seconds() / 3600)
|
||||
return f'{hours} hour{"s" if hours != 1 else ""} ago'
|
||||
elif diff < timedelta(days=7):
|
||||
days = diff.days
|
||||
if days == 1:
|
||||
return 'Yesterday'
|
||||
return f'{days} days ago'
|
||||
else:
|
||||
return obj.created_at.strftime('%b %d, %Y')
|
||||
|
||||
|
||||
class MarkReadSerializer(serializers.Serializer):
|
||||
"""Serializer for marking notifications as read"""
|
||||
|
||||
notification_ids = serializers.ListField(
|
||||
child=serializers.IntegerField(),
|
||||
required=False,
|
||||
help_text='List of notification IDs to mark as read. If empty, marks all as read.'
|
||||
)
|
||||
306
backend/igny8_core/business/notifications/services.py
Normal file
306
backend/igny8_core/business/notifications/services.py
Normal file
@@ -0,0 +1,306 @@
|
||||
"""
|
||||
Notification Service
|
||||
|
||||
Provides methods to create notifications for various events in the system.
|
||||
"""
|
||||
|
||||
from .models import Notification, NotificationType, NotificationSeverity
|
||||
|
||||
|
||||
class NotificationService:
|
||||
"""Service for creating notifications"""
|
||||
|
||||
@staticmethod
|
||||
def notify_clustering_complete(account, site=None, cluster_count=0, keyword_count=0, user=None):
|
||||
"""Create notification when keyword clustering completes"""
|
||||
return Notification.create_notification(
|
||||
account=account,
|
||||
notification_type=NotificationType.AI_CLUSTER_COMPLETE,
|
||||
title='Clustering Complete',
|
||||
message=f'Created {cluster_count} clusters from {keyword_count} keywords',
|
||||
severity=NotificationSeverity.SUCCESS,
|
||||
user=user,
|
||||
site=site,
|
||||
action_url='/planner/clusters',
|
||||
action_label='View Clusters',
|
||||
metadata={'cluster_count': cluster_count, 'keyword_count': keyword_count}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def notify_clustering_failed(account, site=None, error=None, user=None):
|
||||
"""Create notification when keyword clustering fails"""
|
||||
return Notification.create_notification(
|
||||
account=account,
|
||||
notification_type=NotificationType.AI_CLUSTER_FAILED,
|
||||
title='Clustering Failed',
|
||||
message=f'Failed to cluster keywords: {error}' if error else 'Failed to cluster keywords',
|
||||
severity=NotificationSeverity.ERROR,
|
||||
user=user,
|
||||
site=site,
|
||||
action_url='/planner/keywords',
|
||||
action_label='View Keywords',
|
||||
metadata={'error': str(error) if error else None}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def notify_ideas_complete(account, site=None, idea_count=0, cluster_count=0, user=None):
|
||||
"""Create notification when idea generation completes"""
|
||||
return Notification.create_notification(
|
||||
account=account,
|
||||
notification_type=NotificationType.AI_IDEAS_COMPLETE,
|
||||
title='Ideas Generated',
|
||||
message=f'Generated {idea_count} content ideas from {cluster_count} clusters',
|
||||
severity=NotificationSeverity.SUCCESS,
|
||||
user=user,
|
||||
site=site,
|
||||
action_url='/planner/ideas',
|
||||
action_label='View Ideas',
|
||||
metadata={'idea_count': idea_count, 'cluster_count': cluster_count}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def notify_ideas_failed(account, site=None, error=None, user=None):
|
||||
"""Create notification when idea generation fails"""
|
||||
return Notification.create_notification(
|
||||
account=account,
|
||||
notification_type=NotificationType.AI_IDEAS_FAILED,
|
||||
title='Idea Generation Failed',
|
||||
message=f'Failed to generate ideas: {error}' if error else 'Failed to generate ideas',
|
||||
severity=NotificationSeverity.ERROR,
|
||||
user=user,
|
||||
site=site,
|
||||
action_url='/planner/clusters',
|
||||
action_label='View Clusters',
|
||||
metadata={'error': str(error) if error else None}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def notify_content_complete(account, site=None, article_count=0, word_count=0, user=None):
|
||||
"""Create notification when content generation completes"""
|
||||
return Notification.create_notification(
|
||||
account=account,
|
||||
notification_type=NotificationType.AI_CONTENT_COMPLETE,
|
||||
title='Content Generated',
|
||||
message=f'Generated {article_count} article{"s" if article_count != 1 else ""} ({word_count:,} words)',
|
||||
severity=NotificationSeverity.SUCCESS,
|
||||
user=user,
|
||||
site=site,
|
||||
action_url='/writer/content',
|
||||
action_label='View Content',
|
||||
metadata={'article_count': article_count, 'word_count': word_count}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def notify_content_failed(account, site=None, error=None, user=None):
|
||||
"""Create notification when content generation fails"""
|
||||
return Notification.create_notification(
|
||||
account=account,
|
||||
notification_type=NotificationType.AI_CONTENT_FAILED,
|
||||
title='Content Generation Failed',
|
||||
message=f'Failed to generate content: {error}' if error else 'Failed to generate content',
|
||||
severity=NotificationSeverity.ERROR,
|
||||
user=user,
|
||||
site=site,
|
||||
action_url='/writer/tasks',
|
||||
action_label='View Tasks',
|
||||
metadata={'error': str(error) if error else None}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def notify_images_complete(account, site=None, image_count=0, user=None):
|
||||
"""Create notification when image generation completes"""
|
||||
return Notification.create_notification(
|
||||
account=account,
|
||||
notification_type=NotificationType.AI_IMAGES_COMPLETE,
|
||||
title='Images Generated',
|
||||
message=f'Generated {image_count} image{"s" if image_count != 1 else ""}',
|
||||
severity=NotificationSeverity.SUCCESS,
|
||||
user=user,
|
||||
site=site,
|
||||
action_url='/writer/images',
|
||||
action_label='View Images',
|
||||
metadata={'image_count': image_count}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def notify_images_failed(account, site=None, error=None, image_count=0, user=None):
|
||||
"""Create notification when image generation fails"""
|
||||
return Notification.create_notification(
|
||||
account=account,
|
||||
notification_type=NotificationType.AI_IMAGES_FAILED,
|
||||
title='Image Generation Failed',
|
||||
message=f'Failed to generate {image_count} image{"s" if image_count != 1 else ""}: {error}' if error else f'Failed to generate images',
|
||||
severity=NotificationSeverity.ERROR,
|
||||
user=user,
|
||||
site=site,
|
||||
action_url='/writer/images',
|
||||
action_label='View Images',
|
||||
metadata={'error': str(error) if error else None, 'image_count': image_count}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def notify_prompts_complete(account, site=None, prompt_count=0, user=None):
|
||||
"""Create notification when image prompt generation completes"""
|
||||
in_article_count = prompt_count - 1 if prompt_count > 1 else 0
|
||||
message = f'{prompt_count} image prompts ready (1 featured + {in_article_count} in-article)' if in_article_count > 0 else '1 image prompt ready'
|
||||
|
||||
return Notification.create_notification(
|
||||
account=account,
|
||||
notification_type=NotificationType.AI_PROMPTS_COMPLETE,
|
||||
title='Image Prompts Created',
|
||||
message=message,
|
||||
severity=NotificationSeverity.SUCCESS,
|
||||
user=user,
|
||||
site=site,
|
||||
action_url='/writer/images',
|
||||
action_label='Generate Images',
|
||||
metadata={'prompt_count': prompt_count, 'in_article_count': in_article_count}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def notify_prompts_failed(account, site=None, error=None, user=None):
|
||||
"""Create notification when image prompt generation fails"""
|
||||
return Notification.create_notification(
|
||||
account=account,
|
||||
notification_type=NotificationType.AI_PROMPTS_FAILED,
|
||||
title='Image Prompts Failed',
|
||||
message=f'Failed to create image prompts: {error}' if error else 'Failed to create image prompts',
|
||||
severity=NotificationSeverity.ERROR,
|
||||
user=user,
|
||||
site=site,
|
||||
action_url='/writer/content',
|
||||
action_label='View Content',
|
||||
metadata={'error': str(error) if error else None}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def notify_content_published(account, site=None, title='', content_object=None, user=None):
|
||||
"""Create notification when content is published"""
|
||||
site_name = site.name if site else 'site'
|
||||
return Notification.create_notification(
|
||||
account=account,
|
||||
notification_type=NotificationType.CONTENT_PUBLISHED,
|
||||
title='Content Published',
|
||||
message=f'"{title}" published to {site_name}',
|
||||
severity=NotificationSeverity.SUCCESS,
|
||||
user=user,
|
||||
site=site,
|
||||
content_object=content_object,
|
||||
action_url='/writer/published',
|
||||
action_label='View Published',
|
||||
metadata={'content_title': title}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def notify_publish_failed(account, site=None, title='', error=None, user=None):
|
||||
"""Create notification when publishing fails"""
|
||||
return Notification.create_notification(
|
||||
account=account,
|
||||
notification_type=NotificationType.CONTENT_PUBLISH_FAILED,
|
||||
title='Publishing Failed',
|
||||
message=f'Failed to publish "{title}": {error}' if error else f'Failed to publish "{title}"',
|
||||
severity=NotificationSeverity.ERROR,
|
||||
user=user,
|
||||
site=site,
|
||||
action_url='/writer/review',
|
||||
action_label='View Review',
|
||||
metadata={'content_title': title, 'error': str(error) if error else None}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def notify_wordpress_sync_success(account, site=None, count=0, user=None):
|
||||
"""Create notification when WordPress sync succeeds"""
|
||||
site_name = site.name if site else 'site'
|
||||
return Notification.create_notification(
|
||||
account=account,
|
||||
notification_type=NotificationType.WORDPRESS_SYNC_SUCCESS,
|
||||
title='WordPress Synced',
|
||||
message=f'Synced {count} item{"s" if count != 1 else ""} with {site_name}',
|
||||
severity=NotificationSeverity.SUCCESS,
|
||||
user=user,
|
||||
site=site,
|
||||
action_url='/writer/published',
|
||||
action_label='View Published',
|
||||
metadata={'sync_count': count}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def notify_wordpress_sync_failed(account, site=None, error=None, user=None):
|
||||
"""Create notification when WordPress sync fails"""
|
||||
site_name = site.name if site else 'site'
|
||||
return Notification.create_notification(
|
||||
account=account,
|
||||
notification_type=NotificationType.WORDPRESS_SYNC_FAILED,
|
||||
title='Sync Failed',
|
||||
message=f'WordPress sync failed for {site_name}: {error}' if error else f'WordPress sync failed for {site_name}',
|
||||
severity=NotificationSeverity.ERROR,
|
||||
user=user,
|
||||
site=site,
|
||||
action_url=f'/sites/{site.id}/integrations' if site else '/sites',
|
||||
action_label='Check Integration',
|
||||
metadata={'error': str(error) if error else None}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def notify_credits_low(account, percentage_used=80, credits_remaining=0, user=None):
|
||||
"""Create notification when credits are running low"""
|
||||
return Notification.create_notification(
|
||||
account=account,
|
||||
notification_type=NotificationType.CREDITS_LOW,
|
||||
title='Credits Running Low',
|
||||
message=f"You've used {percentage_used}% of your credits. {credits_remaining} credits remaining.",
|
||||
severity=NotificationSeverity.WARNING,
|
||||
user=user,
|
||||
action_url='/account/billing',
|
||||
action_label='Upgrade Plan',
|
||||
metadata={'percentage_used': percentage_used, 'credits_remaining': credits_remaining}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def notify_credits_depleted(account, user=None):
|
||||
"""Create notification when credits are depleted"""
|
||||
return Notification.create_notification(
|
||||
account=account,
|
||||
notification_type=NotificationType.CREDITS_DEPLETED,
|
||||
title='Credits Depleted',
|
||||
message='Your credits are exhausted. Upgrade to continue using AI features.',
|
||||
severity=NotificationSeverity.ERROR,
|
||||
user=user,
|
||||
action_url='/account/billing',
|
||||
action_label='Upgrade Now',
|
||||
metadata={}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def notify_site_setup_complete(account, site=None, user=None):
|
||||
"""Create notification when site setup is complete"""
|
||||
site_name = site.name if site else 'Site'
|
||||
return Notification.create_notification(
|
||||
account=account,
|
||||
notification_type=NotificationType.SITE_SETUP_COMPLETE,
|
||||
title='Site Ready',
|
||||
message=f'{site_name} is fully configured and ready!',
|
||||
severity=NotificationSeverity.SUCCESS,
|
||||
user=user,
|
||||
site=site,
|
||||
action_url=f'/sites/{site.id}' if site else '/sites',
|
||||
action_label='View Site',
|
||||
metadata={}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def notify_keywords_imported(account, site=None, count=0, user=None):
|
||||
"""Create notification when keywords are imported"""
|
||||
site_name = site.name if site else 'site'
|
||||
return Notification.create_notification(
|
||||
account=account,
|
||||
notification_type=NotificationType.KEYWORDS_IMPORTED,
|
||||
title='Keywords Imported',
|
||||
message=f'Added {count} keyword{"s" if count != 1 else ""} to {site_name}',
|
||||
severity=NotificationSeverity.INFO,
|
||||
user=user,
|
||||
site=site,
|
||||
action_url='/planner/keywords',
|
||||
action_label='View Keywords',
|
||||
metadata={'keyword_count': count}
|
||||
)
|
||||
15
backend/igny8_core/business/notifications/urls.py
Normal file
15
backend/igny8_core/business/notifications/urls.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
Notification URL Configuration
|
||||
"""
|
||||
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from .views import NotificationViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'notifications', NotificationViewSet, basename='notification')
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
132
backend/igny8_core/business/notifications/views.py
Normal file
132
backend/igny8_core/business/notifications/views.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""
|
||||
Notification Views
|
||||
"""
|
||||
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from django.utils import timezone
|
||||
|
||||
from igny8_core.api.pagination import CustomPageNumberPagination
|
||||
from igny8_core.api.base import AccountModelViewSet
|
||||
|
||||
from .models import Notification
|
||||
from .serializers import NotificationSerializer, NotificationListSerializer, MarkReadSerializer
|
||||
|
||||
|
||||
class NotificationViewSet(AccountModelViewSet):
|
||||
"""
|
||||
ViewSet for managing notifications
|
||||
|
||||
Endpoints:
|
||||
- GET /api/v1/notifications/ - List notifications
|
||||
- GET /api/v1/notifications/{id}/ - Get notification detail
|
||||
- DELETE /api/v1/notifications/{id}/ - Delete notification
|
||||
- POST /api/v1/notifications/{id}/read/ - Mark single notification as read
|
||||
- POST /api/v1/notifications/read-all/ - Mark all notifications as read
|
||||
- GET /api/v1/notifications/unread-count/ - Get unread notification count
|
||||
"""
|
||||
|
||||
serializer_class = NotificationSerializer
|
||||
pagination_class = CustomPageNumberPagination
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
"""Filter notifications for current account and user"""
|
||||
from django.db.models import Q
|
||||
|
||||
user = self.request.user
|
||||
account = getattr(user, 'account', None)
|
||||
|
||||
if not account:
|
||||
return Notification.objects.none()
|
||||
|
||||
# Get notifications for this account that are either:
|
||||
# - For all users (user=None)
|
||||
# - For this specific user
|
||||
queryset = Notification.objects.filter(
|
||||
Q(account=account, user__isnull=True) |
|
||||
Q(account=account, user=user)
|
||||
).select_related('site').order_by('-created_at')
|
||||
|
||||
# Optional filters
|
||||
is_read = self.request.query_params.get('is_read')
|
||||
if is_read is not None:
|
||||
queryset = queryset.filter(is_read=is_read.lower() == 'true')
|
||||
|
||||
notification_type = self.request.query_params.get('type')
|
||||
if notification_type:
|
||||
queryset = queryset.filter(notification_type=notification_type)
|
||||
|
||||
severity = self.request.query_params.get('severity')
|
||||
if severity:
|
||||
queryset = queryset.filter(severity=severity)
|
||||
|
||||
return queryset
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""Use list serializer for list action"""
|
||||
if self.action == 'list':
|
||||
return NotificationListSerializer
|
||||
return NotificationSerializer
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""List notifications with unread count"""
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
# Get unread count
|
||||
unread_count = queryset.filter(is_read=False).count()
|
||||
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
response = self.get_paginated_response(serializer.data)
|
||||
response.data['unread_count'] = unread_count
|
||||
return response
|
||||
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response({
|
||||
'results': serializer.data,
|
||||
'unread_count': unread_count
|
||||
})
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def read(self, request, pk=None):
|
||||
"""Mark a single notification as read"""
|
||||
notification = self.get_object()
|
||||
notification.mark_as_read()
|
||||
serializer = self.get_serializer(notification)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=['post'], url_path='read-all')
|
||||
def read_all(self, request):
|
||||
"""Mark all notifications as read"""
|
||||
serializer = MarkReadSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
notification_ids = serializer.validated_data.get('notification_ids', [])
|
||||
|
||||
queryset = self.get_queryset().filter(is_read=False)
|
||||
|
||||
if notification_ids:
|
||||
queryset = queryset.filter(id__in=notification_ids)
|
||||
|
||||
count = queryset.update(is_read=True, read_at=timezone.now())
|
||||
|
||||
return Response({
|
||||
'status': 'success',
|
||||
'marked_read': count
|
||||
})
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='unread-count')
|
||||
def unread_count(self, request):
|
||||
"""Get count of unread notifications"""
|
||||
count = self.get_queryset().filter(is_read=False).count()
|
||||
return Response({'unread_count': count})
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
"""Delete a notification"""
|
||||
instance = self.get_object()
|
||||
self.perform_destroy(instance)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -66,6 +66,7 @@ INSTALLED_APPS = [
|
||||
'igny8_core.modules.system.apps.SystemConfig',
|
||||
'igny8_core.modules.billing.apps.BillingConfig',
|
||||
'igny8_core.business.automation', # AI Automation Pipeline
|
||||
'igny8_core.business.notifications.apps.NotificationsConfig', # User Notifications
|
||||
'igny8_core.business.optimization.apps.OptimizationConfig',
|
||||
'igny8_core.business.publishing.apps.PublishingConfig',
|
||||
'igny8_core.business.integration.apps.IntegrationConfig',
|
||||
|
||||
@@ -44,6 +44,7 @@ urlpatterns = [
|
||||
path('api/v1/billing/', include('igny8_core.business.billing.urls')), # Billing (credits, invoices, payments, packages)
|
||||
path('api/v1/admin/', include('igny8_core.modules.billing.admin_urls')), # Admin billing
|
||||
path('api/v1/automation/', include('igny8_core.business.automation.urls')), # Automation endpoints
|
||||
path('api/v1/', include('igny8_core.business.notifications.urls')), # Notifications endpoints
|
||||
path('api/v1/linker/', include('igny8_core.modules.linker.urls')), # Linker endpoints
|
||||
path('api/v1/optimizer/', include('igny8_core.modules.optimizer.urls')), # Optimizer endpoints
|
||||
path('api/v1/publisher/', include('igny8_core.modules.publisher.urls')), # Publisher endpoints
|
||||
|
||||
Reference in New Issue
Block a user