FIX(promotions): include filters.py/docs/urls.py in admin panel restructure

These were dropped from the prior restructuring commit by a failed
multi-pathspec git add (one invalid path aborted the whole call).
Wires up filters.py's Admin*Filter classes, mounts apps.promotions.urls
(the new router package) instead of the deleted urls_admin module, and
brings docs/adminpanel_technical.md in sync with the new layout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Ali Asadi 2026-08-23 17:28:33 +03:30
parent 1d77303f00
commit c359105019
3 changed files with 58 additions and 15 deletions

View file

@ -1,11 +1,36 @@
import django_filters
from .models import Sample
from .handlers import ProcessorTypeChoices
from .models import Promotion, PaymentStateChoices
class SampleFilter(django_filters.FilterSet):
class AdminPromotionFilter(django_filters.FilterSet):
user_uuid = django_filters.UUIDFilter(field_name='user_uuid')
plan = django_filters.UUIDFilter(field_name='plan__uuid')
promotion_type = django_filters.ChoiceFilter(field_name='plan__processor', choices=ProcessorTypeChoices.choices)
state = django_filters.ChoiceFilter(field_name='state', choices=PaymentStateChoices.choices)
event_label = django_filters.CharFilter(field_name='event__label', lookup_expr='exact')
created_after = django_filters.DateTimeFilter(field_name='created_at', lookup_expr='gte')
created_before = django_filters.DateTimeFilter(field_name='created_at', lookup_expr='lte')
class Meta:
model = Sample
fields = {
'data': ['exact'],
}
model = Promotion
fields = ['user_uuid', 'plan', 'promotion_type', 'state', 'event_label', 'created_after', 'created_before']
class AdminReferralFilter(django_filters.FilterSet):
# invited_by == Promotion.user_uuid: in the referral recipient DSL
# ("->event:referral") this is who gets paid, i.e. the referrer.
invited_by = django_filters.UUIDFilter(field_name='user_uuid')
# invited_user lives only in free-form JSON (event.data['user']), so this is a
# CharFilter, not UUIDFilter: JSONField key-transform lookups compare against
# the stored string, not a native UUID the adapter can serialize.
invited_user = django_filters.CharFilter(field_name='event__data__user')
plan = django_filters.UUIDFilter(field_name='plan__uuid')
state = django_filters.ChoiceFilter(field_name='state', choices=PaymentStateChoices.choices)
created_after = django_filters.DateTimeFilter(field_name='created_at', lookup_expr='gte')
created_before = django_filters.DateTimeFilter(field_name='created_at', lookup_expr='lte')
class Meta:
model = Promotion
fields = ['invited_user', 'invited_by', 'plan', 'state', 'created_after', 'created_before']

View file

@ -1,21 +1,38 @@
# Admin panel — technical reference
Two read-only endpoints added for the admin panel: **Promotions** (every promotion received, per user) and **Referral System** (referral activity). Both live in `apps/promotions`, following this repo's existing `_user` / `_application` file-suffix convention with a new `_admin` suffix:
Two read-only endpoints added for the admin panel: **Promotions** (every promotion received, per user) and **Referral System** (referral activity). They live under `apps/promotions`, structured to match the admin/users folder split used by the sibling `advertising` service (`apps/crm`, `apps/escrow`, `apps/stores` there each split `views/`, `serializers/`, and `urls/` into `admin/`/`users/` subpackages, sharing one `DefaultRouter`). Applied here for the admin slice only — the pre-existing `views_user.py` / `views_application.py` / `urls_user.py` / `urls_application.py` (user-token and application-token APIs) were intentionally left untouched, since restructuring those would rename URL namespaces already relied on by `tests.py` and any external caller.
| File | Purpose |
| Path | Purpose |
|---|---|
| `apps/promotions/views_admin.py` | `AdminPromotionListApiView`, `AdminPromotionDetailApiView`, `AdminReferralListApiView`, `AdminReferralDetailApiView` |
| `apps/promotions/urls_admin.py` | URL routes, `app_name = 'promotions-admin'` |
| `apps/promotions/filters_admin.py` | `AdminPromotionFilter`, `AdminReferralFilter` (django-filter `FilterSet`s) |
| `apps/promotions/serializers.py` | `AdminPromotionSerializer`, `AdminReferralSerializer`, `AdminPlanSummarySerializer` (added alongside the existing serializers, this repo does not split serializers by audience) |
| `apps/promotions/views/admin/promotion.py` | `AdminPromotionViewSet` |
| `apps/promotions/views/admin/referral.py` | `AdminReferralViewSet` |
| `apps/promotions/views/admin/__init__.py`, `apps/promotions/views/__init__.py` | re-export the two viewsets |
| `apps/promotions/urls/router.py` | shared `DefaultRouter` for the admin API |
| `apps/promotions/urls/admin_urls.py` | registers both viewsets on the router |
| `apps/promotions/urls/__init__.py` | combines `router.urls`, sets `app_name = 'promotions-admin'` |
| `apps/promotions/serializers/admin/promotion.py` | `AdminPlanSummarySerializer`, `AdminPromotionSerializer` |
| `apps/promotions/serializers/admin/referral.py` | `AdminReferralSerializer` |
| `apps/promotions/serializers/admin/__init__.py`, `apps/promotions/serializers/__init__.py` | re-export, alongside the pre-existing (now `serializers/common.py`) serializers |
| `apps/promotions/filters.py` | `AdminPromotionFilter`, `AdminReferralFilter` — stays a single flat file, matching how `advertising`'s `filters.py` (e.g. `apps/crm/filters.py`) is *not* split into subfolders even though views/serializers/urls are |
Mounted in `main/urls.py`:
```python
path('api/v2/promotions/admin/', include('apps.promotions.urls_admin', namespace='promotions-admin')),
path('api/v2/promotions/admin/', include('apps.promotions.urls', namespace='promotions-admin')),
```
No models or migrations were changed. Both endpoints are pure `ListAPIView`/`RetrieveAPIView` reads over the existing `Promotion` table.
No models or migrations were changed. Both resources are `GenericViewSet` + `ListModelMixin`/`RetrieveModelMixin` (matching `AdminTicketViewSet` in `advertising`'s `apps/crm/views/admin/ticket.py`) registered on one router, not separate `ListAPIView`/`RetrieveAPIView` classes.
### URL names
Router-generated, under the `promotions-admin` namespace:
| Name | Path |
|---|---|
| `promotions-admin:admin-promotions-list` | `GET /api/v2/promotions/admin/promotions/` |
| `promotions-admin:admin-promotions-detail` | `GET /api/v2/promotions/admin/promotions/<uuid>/` |
| `promotions-admin:admin-referrals-list` | `GET /api/v2/promotions/admin/referrals/` |
| `promotions-admin:admin-referrals-detail` | `GET /api/v2/promotions/admin/referrals/<uuid>/` |
---
@ -207,3 +224,4 @@ GET /api/v2/promotions/admin/referrals/?invited_by=1bb3b561-2823-4a3e-be12-edc5a
- **Detail routes are of limited independent use.** `AdminPromotionDetailApiView` / `AdminReferralDetailApiView` return the same shape as one row of the list endpoint; included for REST completeness (e.g. deep-linking from a table row in the admin UI) but the list endpoint with `plan`/`state` filters covers most real usage.
- **New scopes (`admin.promotions:retrieve`, `admin.referrals:retrieve`) need to be registered on the central accounts OAuth2 provider** before any real token can carry them — that's outside this checkout.
- **No automated tests were added** for these two endpoints (matching the request's scope: verified via `manage.py check`, URL resolution, and an `APIRequestFactory` smoke test against an in-memory SQLite DB with the app's Postgres/JSONField-`GinIndex` usage stubbed around, run manually — see `apps/promotions/tests.py` for the existing `APITestCase` pattern if formal tests are wanted later).
- **The admin/users folder split was applied to the admin slice only.** `views_user.py`, `views_application.py`, `urls_user.py`, `urls_application.py`, and the non-admin serializers (now `serializers/common.py`) were left in place rather than also converted to `views/users/`, `views/application/`, etc., because that would rename the `promotions` / `promotions-application` URL namespaces already used by `tests.py`'s `reverse()` calls (and possibly external callers) — a breaking change outside what was asked for here.

View file

@ -30,7 +30,7 @@ urlpatterns = [
path('oauth2/', include('oauth2_provider.urls', namespace='oauth2_provider')),
path('promotions/', include('apps.promotions.urls_user', namespace='promotions')),
path('api/v2/promotions/application/<user_uuid>/', include('apps.promotions.urls_application', namespace='promotions-application')),
path('api/v2/promotions/admin/', include('apps.promotions.urls_admin', namespace='promotions-admin')),
path('api/v2/promotions/admin/', include('apps.promotions.urls', namespace='promotions-admin')),
]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)