Skip to content

Commit c166b67

Browse files
committed
Finish refactor to separate apps, remove shims and compatability layers
1 parent fe568ce commit c166b67

34 files changed

Lines changed: 3346 additions & 3990 deletions

.github/copilot-instructions.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ You are working in Newsletter Maker, a Django + DRF + Celery + Qdrant backend wi
66

77
- Backend runtime code is split across `core/`, `projects/`, `content/`, `entities/`, `ingestion/`, `newsletters/`, `pipeline/`, `trends/`, and `users/`.
88
- Django project settings and top-level URLs live in `newsletter_maker/`.
9-
- Backend tests live in app-local `tests/` packages first (`users/tests/`, `projects/tests/`, `ingestion/tests/`, `newsletters/tests/`, `pipeline/tests/`), with `core/tests/` reserved for the remaining cross-cutting and mixed compatibility coverage.
9+
- Backend tests live in app-local `tests/` packages first (`users/tests/`, `projects/tests/`, `ingestion/tests/`, `newsletters/tests/`, `pipeline/tests/`), with `core/tests/` reserved for the remaining cross-cutting coverage.
1010
- The repo-root `tests/` package is for integration coverage only. New unit and app-scoped tests should live in the owning app's `tests/` package.
1111
- Frontend application code lives in `frontend/src/app/`, shared UI in `frontend/src/components/`, and shared API/types/helpers in `frontend/src/lib/`.
1212
- Operational and architecture docs live in `docs/`.
@@ -21,8 +21,8 @@ You are working in Newsletter Maker, a Django + DRF + Celery + Qdrant backend wi
2121
## Backend Conventions
2222

2323
- Project scoping is a core invariant. Most API resources are nested under `/api/v1/projects/{project_id}/...`.
24-
- Treat `core/` as the home for genuine cross-cutting concerns and compatibility imports only. New app-owned runtime logic should live with its owning app rather than expanding `core/`.
25-
- Reuse the established DRF patterns in `core/api.py`, `core/api_urls.py`, and `core/serializers.py`:
24+
- Treat `core/` as the home for genuine cross-cutting concerns only. New app-owned runtime logic should live with its owning app rather than expanding `core/`.
25+
- Reuse the established DRF patterns in `core/api.py`, `core/api_urls.py`, and `core/serializer_mixins.py`:
2626
- `ProjectOwnedQuerysetMixin` for nested viewsets
2727
- serializer context containing `project`
2828
- explicit validation for cross-project foreign keys

content/admin.py

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,73 @@
66
from unfold.admin import ModelAdmin
77

88
from content.models import Content, UserFeedback
9-
from core.admin import (
10-
DuplicateStateFilter,
11-
HighValueFilter,
12-
_score_color,
13-
_score_to_percent,
14-
)
9+
10+
11+
def _score_to_percent(value):
12+
"""Normalize score-like values for display as percentages."""
13+
14+
if value is None:
15+
return None
16+
numeric_value = float(value)
17+
if -1.0 <= numeric_value <= 1.0:
18+
return numeric_value * 100
19+
return numeric_value
20+
21+
22+
def _score_color(value) -> str:
23+
"""Return the admin display color for a score-like value."""
24+
25+
percent_value = _score_to_percent(value)
26+
if percent_value is None:
27+
return "inherit"
28+
if percent_value >= 75:
29+
return "green"
30+
if percent_value >= 40:
31+
return "orange"
32+
return "red"
33+
34+
35+
class HighValueFilter(admin.SimpleListFilter):
36+
"""Filter content down to high-value reference items."""
37+
38+
title = "Content Value"
39+
parameter_name = "value_tier"
40+
41+
def lookups(self, request, model_admin):
42+
"""Return the custom filter options displayed in the admin sidebar."""
43+
44+
return (("high_value", "🔥 High Value (Score > 80 & Reference)"),)
45+
46+
def queryset(self, request, queryset):
47+
"""Apply the high-value filter when it is selected."""
48+
49+
if self.value() == "high_value":
50+
return queryset.filter(relevance_score__gt=80, is_reference=True)
51+
return queryset
52+
53+
54+
class DuplicateStateFilter(admin.SimpleListFilter):
55+
"""Filter content by duplicate retention and suppression state."""
56+
57+
title = "Duplicate State"
58+
parameter_name = "duplicate_state"
59+
60+
def lookups(self, request, model_admin):
61+
"""Return duplicate-state options displayed in the admin sidebar."""
62+
63+
return (
64+
("canonical_with_duplicates", "Canonical rows with duplicate signals"),
65+
("suppressed_duplicates", "Suppressed duplicate rows"),
66+
)
67+
68+
def queryset(self, request, queryset):
69+
"""Apply the selected duplicate-state filter."""
70+
71+
if self.value() == "canonical_with_duplicates":
72+
return queryset.filter(duplicate_signal_count__gt=0)
73+
if self.value() == "suppressed_duplicates":
74+
return queryset.filter(duplicate_of__isnull=False)
75+
return queryset
1576

1677

1778
@admin.register(Content)

content/apps.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@ class ContentConfig(AppConfig):
88

99
default_auto_field = "django.db.models.BigAutoField"
1010
name = "content"
11+
12+
def ready(self) -> None:
13+
import content.signals # noqa: F401
Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,15 @@
1-
"""Signal handlers for cross-cutting core behaviors."""
1+
"""Signal handlers for content-owned behaviors."""
22

33
from __future__ import annotations
44

5-
from typing import Any
6-
75
from django.db.models.signals import post_save
86
from django.dispatch import receiver
97

108
from content.models import UserFeedback
11-
from newsletters.signals import handle_anymail_inbound as _handle_anymail_inbound
129
from projects.models import ProjectConfig
1310
from trends.tasks import queue_topic_centroid_recompute
1411

1512

16-
def handle_anymail_inbound(
17-
sender: Any,
18-
event: Any,
19-
esp_name: str,
20-
**kwargs: Any,
21-
) -> None:
22-
"""Preserve the legacy core.signals import path for inbound handling."""
23-
24-
_handle_anymail_inbound(
25-
sender=sender,
26-
event=event,
27-
esp_name=esp_name,
28-
**kwargs,
29-
)
30-
31-
3213
@receiver(post_save, sender=UserFeedback)
3314
def queue_topic_centroid_on_feedback_save(sender, instance, created, **kwargs):
3415
"""Queue centroid recomputation when feedback changes and config allows it."""
@@ -38,4 +19,4 @@ def queue_topic_centroid_on_feedback_save(sender, instance, created, **kwargs):
3819

3920
config, _ = ProjectConfig.objects.get_or_create(project=instance.project)
4021
if config.recompute_topic_centroid_on_feedback_save:
41-
queue_topic_centroid_recompute(instance.project_id)
22+
queue_topic_centroid_recompute(instance.project_id)

0 commit comments

Comments
 (0)