From 056bb23ac651e07dade83ecb5fd61ccf6e3e0ca5 Mon Sep 17 00:00:00 2001 From: 4rcadia <97033226+Arcadi4@users.noreply.github.com> Date: Sun, 10 May 2026 22:28:12 -0400 Subject: [PATCH 1/9] chore(gitnexus): analyze project --- .gitignore | 1 + AGENTS.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/.gitignore b/.gitignore index 92558663..9749400f 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,4 @@ env/.env.stg # Observability runtime data o11y/grafana/data/ o11y/loki/data/ +.gitnexus diff --git a/AGENTS.md b/AGENTS.md index 6531013f..95ace60b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -54,3 +54,47 @@ Run the smallest relevant checks for the change. If a check cannot be run, menti - Use focused commits and clear commit messages. - Feature work should normally branch from the active development branch. - Production releases should be represented by tags, not by editing this file. + + +# GitNexus — Code Intelligence + +This project is indexed by GitNexus as **alien-commons** (2649 symbols, 4181 relationships, 58 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. + +> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. + +## Always Do + +- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user. +- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows. +- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits. +- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance. +- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`. + +## Never Do + +- NEVER edit a function, class, or method without first running `gitnexus_impact` on it. +- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis. +- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph. +- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope. + +## Resources + +| Resource | Use for | +|----------|---------| +| `gitnexus://repo/alien-commons/context` | Codebase overview, check index freshness | +| `gitnexus://repo/alien-commons/clusters` | All functional areas | +| `gitnexus://repo/alien-commons/processes` | All execution flows | +| `gitnexus://repo/alien-commons/process/{name}` | Step-by-step execution trace | + +## CLI + +| Task | Read this skill file | +|------|---------------------| +| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` | +| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` | +| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` | +| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` | +| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` | +| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` | + + From e26cafeae866c06d883147cad2ce95f50651f5c0 Mon Sep 17 00:00:00 2001 From: 4rcadia <97033226+Arcadi4@users.noreply.github.com> Date: Mon, 11 May 2026 00:49:59 -0400 Subject: [PATCH 2/9] chore(git): update ignore file --- .gitignore | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.gitignore b/.gitignore index 9749400f..9a2e59ff 100644 --- a/.gitignore +++ b/.gitignore @@ -71,4 +71,16 @@ env/.env.stg # Observability runtime data o11y/grafana/data/ o11y/loki/data/ + +# GitNexus .gitnexus + +# Worktrees +.worktree + +# Agents +.pi +.opencode +.sisyphus +.codex +.claude From 8caa2e35f66851b62f7c9ac64a3d4fb535d4aacd Mon Sep 17 00:00:00 2001 From: 4rcadia <97033226+Arcadi4@users.noreply.github.com> Date: Mon, 11 May 2026 01:24:47 -0400 Subject: [PATCH 3/9] feat(notifications): add notifications app scaffold Create notifications Django app with AppConfig, register in INSTALLED_APPS (base and test settings), and wire URL routing to v1/notifications/. --- apps/backend/backend/settings/base.py | 1 + apps/backend/backend/settings/test.py | 1 + apps/backend/backend/urls.py | 1 + apps/backend/notifications/__init__.py | 0 apps/backend/notifications/apps.py | 6 ++++++ apps/backend/notifications/urls.py | 7 +++++++ 6 files changed, 16 insertions(+) create mode 100644 apps/backend/notifications/__init__.py create mode 100644 apps/backend/notifications/apps.py create mode 100644 apps/backend/notifications/urls.py diff --git a/apps/backend/backend/settings/base.py b/apps/backend/backend/settings/base.py index 3c853ac1..9c7a978d 100644 --- a/apps/backend/backend/settings/base.py +++ b/apps/backend/backend/settings/base.py @@ -163,6 +163,7 @@ "comments.apps.CommentsConfig", "reactions.apps.ReactionsConfig", "reports.apps.ReportsConfig", + "notifications.apps.NotificationsConfig", "tasks.apps.TasksConfig", "corsheaders", "rest_framework", diff --git a/apps/backend/backend/settings/test.py b/apps/backend/backend/settings/test.py index f9c43fa6..2401fb95 100644 --- a/apps/backend/backend/settings/test.py +++ b/apps/backend/backend/settings/test.py @@ -29,6 +29,7 @@ "comments.apps.CommentsConfig", "reactions.apps.ReactionsConfig", "reports.apps.ReportsConfig", + "notifications.apps.NotificationsConfig", "tasks.apps.TasksConfig", "corsheaders", "rest_framework", diff --git a/apps/backend/backend/urls.py b/apps/backend/backend/urls.py index 78cf4b2a..6547d85d 100644 --- a/apps/backend/backend/urls.py +++ b/apps/backend/backend/urls.py @@ -14,6 +14,7 @@ path("v1/", include("comments.urls")), path("v1/", include("reactions.urls")), path("v1/", include("reports.urls")), + path("v1/", include("notifications.urls")), ] urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/apps/backend/notifications/__init__.py b/apps/backend/notifications/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/backend/notifications/apps.py b/apps/backend/notifications/apps.py new file mode 100644 index 00000000..3a084766 --- /dev/null +++ b/apps/backend/notifications/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class NotificationsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "notifications" diff --git a/apps/backend/notifications/urls.py b/apps/backend/notifications/urls.py new file mode 100644 index 00000000..7baaf793 --- /dev/null +++ b/apps/backend/notifications/urls.py @@ -0,0 +1,7 @@ +from rest_framework import routers + + +router = routers.SimpleRouter() + +urlpatterns = [] +urlpatterns += router.urls From c183b9656360516244ce42b817078a05ea43b238 Mon Sep 17 00:00:00 2001 From: 4rcadia <97033226+Arcadi4@users.noreply.github.com> Date: Mon, 11 May 2026 01:25:16 -0400 Subject: [PATCH 4/9] chore(notifications): register in lint workflow and docs Add notifications to backend-lint.yml ruff check list, update app registry in README.md and AGENTS.md lint command examples. --- .github/workflows/backend-lint.yml | 1 + apps/backend/AGENTS.md | 2 +- apps/backend/README.md | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backend-lint.yml b/.github/workflows/backend-lint.yml index 3c2aa550..23ff311e 100644 --- a/.github/workflows/backend-lint.yml +++ b/.github/workflows/backend-lint.yml @@ -43,6 +43,7 @@ jobs: backend core logs + notifications reactions reports tasks diff --git a/apps/backend/AGENTS.md b/apps/backend/AGENTS.md index 166416e6..807005f1 100644 --- a/apps/backend/AGENTS.md +++ b/apps/backend/AGENTS.md @@ -34,7 +34,7 @@ This directory contains the Django backend for AlienCommons. Keep backend change - For lint-sensitive changes, prefer: ```bash - uv run ruff check articles backend core logs tasks users manage.py + uv run ruff check articles backend core logs notifications tasks users manage.py ``` - If local dependencies or services make a check impossible, say exactly what was not run and why. diff --git a/apps/backend/README.md b/apps/backend/README.md index 0379bf94..11025c18 100644 --- a/apps/backend/README.md +++ b/apps/backend/README.md @@ -22,6 +22,7 @@ The Django API service for AlienCommons. - `reactions`: like/dislike targets and user reactions - `reports`: content and user moderation reports - `tasks`: scheduled/background task definitions +- `notifications`: user notification preferences and delivery - `core`: shared utilities, responses, permissions, and test helpers ## Local Commands @@ -33,7 +34,7 @@ uv run python manage.py check uv run python manage.py makemigrations uv run python manage.py migrate uv run python manage.py test --settings=backend.settings.test -uv run ruff check articles bookmarks comments reactions reports core users logs tasks backend manage.py +uv run ruff check articles bookmarks comments notifications reactions reports core users logs tasks backend manage.py ``` Or use the root Make targets: From c548c714fb8a3a33edf136cc0f2fc851799b2040 Mon Sep 17 00:00:00 2001 From: 4rcadia <97033226+Arcadi4@users.noreply.github.com> Date: Mon, 11 May 2026 01:33:01 -0400 Subject: [PATCH 5/9] feat(notifications): add notification model foundation --- apps/backend/notifications/admin.py | 23 ++++ .../notifications/migrations/0001_initial.py | 45 +++++++ .../notifications/migrations/__init__.py | 0 apps/backend/notifications/models.py | 123 ++++++++++++++++++ 4 files changed, 191 insertions(+) create mode 100644 apps/backend/notifications/admin.py create mode 100644 apps/backend/notifications/migrations/0001_initial.py create mode 100644 apps/backend/notifications/migrations/__init__.py create mode 100644 apps/backend/notifications/models.py diff --git a/apps/backend/notifications/admin.py b/apps/backend/notifications/admin.py new file mode 100644 index 00000000..4c1d1186 --- /dev/null +++ b/apps/backend/notifications/admin.py @@ -0,0 +1,23 @@ +from django.contrib import admin + +from .models import Notification + + +@admin.register(Notification) +class NotificationAdmin(admin.ModelAdmin): + model = Notification + list_display = ( + "recipient", + "notification_type", + "verb", + "is_read", + "created_at", + ) + list_filter = ("notification_type", "is_read", "created_at") + search_fields = ( + "recipient__username", + "verb", + "dedupe_key", + ) + ordering = ("-created_at",) + readonly_fields = ("id", "created_at", "updated_at") diff --git a/apps/backend/notifications/migrations/0001_initial.py b/apps/backend/notifications/migrations/0001_initial.py new file mode 100644 index 00000000..90404589 --- /dev/null +++ b/apps/backend/notifications/migrations/0001_initial.py @@ -0,0 +1,45 @@ +# Generated by Django 6.0.5 on 2026-05-11 05:30 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Notification', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True, help_text='The created DateTime of the object', verbose_name='created at')), + ('updated_at', models.DateTimeField(auto_now=True, db_index=True, help_text='The updated DateTime of the object', verbose_name='updated at')), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='The UUID of the object', primary_key=True, serialize=False, verbose_name='ID')), + ('actor_object_id', models.UUIDField(help_text='The UUID of the actor object', verbose_name='actor object ID')), + ('target_object_id', models.UUIDField(blank=True, help_text='The UUID of the target object', null=True, verbose_name='target object ID')), + ('action_object_object_id', models.UUIDField(blank=True, help_text='The UUID of the action object', null=True, verbose_name='action object ID')), + ('notification_type', models.CharField(choices=[('mention', 'Mention')], help_text='The type of notification', max_length=50, verbose_name='notification type')), + ('verb', models.CharField(help_text='The action verb describing what happened', max_length=255, verbose_name='verb')), + ('is_read', models.BooleanField(db_index=True, default=False, help_text='Whether the notification has been read', verbose_name='is read')), + ('data', models.JSONField(blank=True, default=dict, help_text='Additional data for the notification', verbose_name='data')), + ('dedupe_key', models.CharField(db_index=True, help_text='Unique key to prevent duplicate notifications', max_length=255, unique=True, verbose_name='dedupe key')), + ('action_object_content_type', models.ForeignKey(blank=True, help_text='The content type of the action object', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notification_action_object', to='contenttypes.contenttype', verbose_name='action object content type')), + ('actor_content_type', models.ForeignKey(help_text='The content type of the actor', on_delete=django.db.models.deletion.CASCADE, related_name='notification_actor', to='contenttypes.contenttype', verbose_name='actor content type')), + ('recipient', models.ForeignKey(help_text='The user who receives the notification', on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL, verbose_name='recipient')), + ('target_content_type', models.ForeignKey(blank=True, help_text='The content type of the target', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notification_target', to='contenttypes.contenttype', verbose_name='target content type')), + ], + options={ + 'verbose_name': 'notification', + 'verbose_name_plural': 'notifications', + 'ordering': ['-created_at'], + 'indexes': [models.Index(fields=['recipient', 'is_read'], name='notificatio_recipie_4e3567_idx'), models.Index(fields=['recipient', 'notification_type'], name='notificatio_recipie_028906_idx'), models.Index(fields=['recipient', 'created_at'], name='notificatio_recipie_f39341_idx')], + }, + ), + ] diff --git a/apps/backend/notifications/migrations/__init__.py b/apps/backend/notifications/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/backend/notifications/models.py b/apps/backend/notifications/models.py new file mode 100644 index 00000000..d0b0ba7a --- /dev/null +++ b/apps/backend/notifications/models.py @@ -0,0 +1,123 @@ +from django.conf import settings +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from core.model_mixins import TimeStampedMixin, UUIDPrimaryKeyMixin + + +class Notification(UUIDPrimaryKeyMixin, + TimeStampedMixin, + models.Model): + class NotificationType(models.TextChoices): + MENTION = "mention", "Mention" + + recipient = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="notifications", + verbose_name=_("recipient"), + help_text=_("The user who receives the notification"), + ) + + actor_content_type = models.ForeignKey( + ContentType, + on_delete=models.CASCADE, + related_name="notification_actor", + verbose_name=_("actor content type"), + help_text=_("The content type of the actor"), + ) + actor_object_id = models.UUIDField( + verbose_name=_("actor object ID"), + help_text=_("The UUID of the actor object"), + ) + actor = GenericForeignKey( + "actor_content_type", + "actor_object_id", + ) + + target_content_type = models.ForeignKey( + ContentType, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name="notification_target", + verbose_name=_("target content type"), + help_text=_("The content type of the target"), + ) + target_object_id = models.UUIDField( + null=True, + blank=True, + verbose_name=_("target object ID"), + help_text=_("The UUID of the target object"), + ) + target = GenericForeignKey( + "target_content_type", + "target_object_id", + ) + + action_object_content_type = models.ForeignKey( + ContentType, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name="notification_action_object", + verbose_name=_("action object content type"), + help_text=_("The content type of the action object"), + ) + action_object_object_id = models.UUIDField( + null=True, + blank=True, + verbose_name=_("action object ID"), + help_text=_("The UUID of the action object"), + ) + action_object = GenericForeignKey( + "action_object_content_type", + "action_object_object_id", + ) + + notification_type = models.CharField( + max_length=50, + choices=NotificationType.choices, + verbose_name=_("notification type"), + help_text=_("The type of notification"), + ) + verb = models.CharField( + max_length=255, + verbose_name=_("verb"), + help_text=_("The action verb describing what happened"), + ) + is_read = models.BooleanField( + default=False, + db_index=True, + verbose_name=_("is read"), + help_text=_("Whether the notification has been read"), + ) + data = models.JSONField( + default=dict, + blank=True, + verbose_name=_("data"), + help_text=_("Additional data for the notification"), + ) + dedupe_key = models.CharField( + max_length=255, + unique=True, + db_index=True, + verbose_name=_("dedupe key"), + help_text=_("Unique key to prevent duplicate notifications"), + ) + + class Meta: + verbose_name = _("notification") + verbose_name_plural = _("notifications") + + ordering = ["-created_at"] + indexes = [ + models.Index(fields=["recipient", "is_read"]), + models.Index(fields=["recipient", "notification_type"]), + models.Index(fields=["recipient", "created_at"]), + ] + + def __str__(self): + return f"{self.notification_type} notification for {self.recipient}" From 289e564e207e9db3aabb65fc0b53521fa1e48286 Mon Sep 17 00:00:00 2001 From: 4rcadia <97033226+Arcadi4@users.noreply.github.com> Date: Mon, 11 May 2026 01:39:01 -0400 Subject: [PATCH 6/9] feat(notifications): add notification services --- apps/backend/notifications/services.py | 106 +++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 apps/backend/notifications/services.py diff --git a/apps/backend/notifications/services.py b/apps/backend/notifications/services.py new file mode 100644 index 00000000..848b008e --- /dev/null +++ b/apps/backend/notifications/services.py @@ -0,0 +1,106 @@ +from django.contrib.contenttypes.models import ContentType +from django.db import transaction + + +from .models import Notification + + +@transaction.atomic +def create_notification( + *, + recipient, + actor, + notification_type, + verb, + dedupe_key, + data=None, + target=None, + action_object=None, +): + actor_content_type = ContentType.objects.get_for_model(actor) + + defaults = { + "recipient": recipient, + "actor_content_type": actor_content_type, + "actor_object_id": actor.id, + "notification_type": notification_type, + "verb": verb, + "data": data or {}, + } + + if target is not None: + target_content_type = ContentType.objects.get_for_model(target) + defaults["target_content_type"] = target_content_type + defaults["target_object_id"] = target.id + + if action_object is not None: + action_object_content_type = ContentType.objects.get_for_model(action_object) + defaults["action_object_content_type"] = action_object_content_type + defaults["action_object_object_id"] = action_object.id + + return Notification.objects.get_or_create( + dedupe_key=dedupe_key, + defaults=defaults, + ) + + +def create_mention_notifications_for_comment(comment, previous_mentions=None): + mention_ids = set(comment.mentions or []) + + if previous_mentions is not None: + previous_ids = set(previous_mentions) + mention_ids = mention_ids - previous_ids + + mention_ids.discard(str(comment.author_id)) + + notifications = [] + for user_id in mention_ids: + from users.models import User + try: + user = User.objects.get(id=user_id) + except User.DoesNotExist: + continue + + dedupe_key = f"comment:{comment.id}:mention:{user_id}" + data = { + "comment_id": str(comment.id), + "author_id": str(comment.author_id), + "author_username": comment.author.username, + } + + notification, created = create_notification( + recipient=user, + actor=comment.author, + action_object=comment, + target=comment.target if hasattr(comment, 'target') else None, + notification_type="mention", + verb="mentioned you in a comment", + dedupe_key=dedupe_key, + data=data, + ) + + if created: + notifications.append(notification) + + return notifications + + +def mark_notification_read(notification): + if not notification.is_read: + notification.is_read = True + notification.save(update_fields=["is_read"]) + return notification + + +def mark_all_notifications_read(user): + return Notification.objects.filter( + recipient=user, + is_read=False, + ).update(is_read=True) + + +def get_unread_count(user): + return Notification.objects.filter( + recipient=user, + is_read=False, + ).count() From 4802e6e257289b455843ad37788326fdfebdfac3 Mon Sep 17 00:00:00 2001 From: 4rcadia <97033226+Arcadi4@users.noreply.github.com> Date: Mon, 11 May 2026 01:44:20 -0400 Subject: [PATCH 7/9] feat(notifications): expose notification read API --- apps/backend/notifications/serializers.py | 40 +++++++++++++++++ apps/backend/notifications/urls.py | 3 ++ apps/backend/notifications/views.py | 52 +++++++++++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 apps/backend/notifications/serializers.py create mode 100644 apps/backend/notifications/views.py diff --git a/apps/backend/notifications/serializers.py b/apps/backend/notifications/serializers.py new file mode 100644 index 00000000..c8929a85 --- /dev/null +++ b/apps/backend/notifications/serializers.py @@ -0,0 +1,40 @@ +from rest_framework import serializers + +from .models import Notification + + +class NotificationReadSerializer(serializers.ModelSerializer): + """Read-only serializer for user notifications. + + Permission enforcement is handled at the ViewSet level via get_queryset(), + which filters by recipient=request.user. This serializer only shapes the + response data and does not perform authorization checks. + """ + + actor_id = serializers.SerializerMethodField() + actor_type = serializers.SerializerMethodField() + + class Meta: + model = Notification + fields = ( + "id", + "notification_type", + "verb", + "is_read", + "data", + "actor_id", + "actor_type", + "created_at", + "updated_at", + ) + read_only_fields = fields + + def get_actor_id(self, obj): + if obj.actor is None: + return None + return str(obj.actor.pk) + + def get_actor_type(self, obj): + if obj.actor_content_type_id is None: + return None + return obj.actor_content_type.model diff --git a/apps/backend/notifications/urls.py b/apps/backend/notifications/urls.py index 7baaf793..fd817bf4 100644 --- a/apps/backend/notifications/urls.py +++ b/apps/backend/notifications/urls.py @@ -1,7 +1,10 @@ from rest_framework import routers +from .views import NotificationViewSet + router = routers.SimpleRouter() +router.register(r"notifications", NotificationViewSet, basename="notification") urlpatterns = [] urlpatterns += router.urls diff --git a/apps/backend/notifications/views.py b/apps/backend/notifications/views.py new file mode 100644 index 00000000..3751998e --- /dev/null +++ b/apps/backend/notifications/views.py @@ -0,0 +1,52 @@ +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated + +from core.views.viewsets import MyReadOnlyModelViewSet +from .models import Notification +from .serializers import NotificationReadSerializer +from .services import ( + get_unread_count, + mark_all_notifications_read, + mark_notification_read, +) + + +class NotificationViewSet(MyReadOnlyModelViewSet): + serializer_class = NotificationReadSerializer + permission_classes = [IsAuthenticated] + filterset_fields = ["is_read", "notification_type"] + + def get_queryset(self): + return Notification.objects.filter(recipient=self.request.user) + + @action(methods=["post"], detail=True) + def mark_read(self, request, pk=None): + notification = self.get_object() + mark_notification_read(notification) + return self.format_success_response( + message="Notification marked as read", + code="marked_read", + data={"id": str(notification.id), "is_read": notification.is_read}, + status_code=status.HTTP_200_OK, + ) + + @action(methods=["post"], detail=False) + def mark_all_read(self, request): + count = mark_all_notifications_read(request.user) + return self.format_success_response( + message="All notifications marked as read", + code="marked_all_read", + data={"count": count}, + status_code=status.HTTP_200_OK, + ) + + @action(methods=["get"], detail=False) + def unread_count(self, request): + count = get_unread_count(request.user) + return self.format_success_response( + message="Unread count retrieved", + code="unread_count", + data={"count": count}, + status_code=status.HTTP_200_OK, + ) From befb35564c20c510bbaadda0cc0b5f65360b1300 Mon Sep 17 00:00:00 2001 From: 4rcadia <97033226+Arcadi4@users.noreply.github.com> Date: Mon, 11 May 2026 02:19:52 -0400 Subject: [PATCH 8/9] feat(notifications): integrate mention notifications into comment workflow --- apps/backend/comments/services.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/backend/comments/services.py b/apps/backend/comments/services.py index 476f68ad..b8b326c2 100644 --- a/apps/backend/comments/services.py +++ b/apps/backend/comments/services.py @@ -7,6 +7,7 @@ get_or_create_comment_target, get_or_create_published_article_target, ) +from notifications.services import create_mention_notifications_for_comment from .models import Comment @@ -38,13 +39,17 @@ def create_comment(*, author, body: str, mentions: list, published_article: Publ mentions=mentions, ) get_or_create_comment_target(comment) + create_mention_notifications_for_comment(comment) return comment +@transaction.atomic def update_comment(*, comment: Comment, body: str, mentions: list): + previous_mentions = set(comment.mentions or []) comment.body = body comment.mentions = mentions comment.save(update_fields=["body", "mentions", "updated_at"]) + create_mention_notifications_for_comment(comment, previous_mentions=previous_mentions) return comment From 52ff6e1aefd1451a819341650a208e0e11810daf Mon Sep 17 00:00:00 2001 From: 4rcadia <97033226+Arcadi4@users.noreply.github.com> Date: Mon, 11 May 2026 03:20:35 -0400 Subject: [PATCH 9/9] fix(notifications): add DjangoFilterBackend for query filtering --- apps/backend/notifications/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/backend/notifications/views.py b/apps/backend/notifications/views.py index 3751998e..a13797f5 100644 --- a/apps/backend/notifications/views.py +++ b/apps/backend/notifications/views.py @@ -1,6 +1,7 @@ from rest_framework import status from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated +from django_filters import rest_framework as filters from core.views.viewsets import MyReadOnlyModelViewSet from .models import Notification @@ -15,6 +16,7 @@ class NotificationViewSet(MyReadOnlyModelViewSet): serializer_class = NotificationReadSerializer permission_classes = [IsAuthenticated] + filter_backends = [filters.DjangoFilterBackend] filterset_fields = ["is_read", "notification_type"] def get_queryset(self):