adjust citysite table name to activesites

This commit is contained in:
Alberto-Audrix committed 2026-09-11 15:51:29 +07:00
1 parent fb0f996776
commit 64b37b6b6d
27 files changed
+382 -221

No files matched your search

+10 -10
View File
@@ -2,7 +2,7 @@
Settled matrix (v1):
- buh, director (+ their admins until specified otherwise): view only
- gm + gm_admin: view + register city sites (URL) + sync from cities
- gm + gm_admin: view + register active sites (URL) + sync from sites
- gm only (not gm_admin): approve cycle close
"""
@@ -22,7 +22,7 @@ VIEW_ONLY_STATUSES = frozenset(
}
)
# Can register city dashboard URLs into the HQ mirror registry.
# Can register site dashboard URLs into the HQ mirror registry.
SITE_REGISTRY_STATUSES = frozenset(
{
User.STATUS_GM,
@@ -30,7 +30,7 @@ SITE_REGISTRY_STATUSES = frozenset(
}
)
# Can trigger city → HQ mirror sync.
# Can trigger site → HQ mirror sync.
SYNC_STATUSES = frozenset(
{
User.STATUS_GM,
@@ -73,8 +73,8 @@ def can_sync(user) -> bool:
)
def can_register_city_site(user) -> bool:
"""GM / GM Admin may add city dashboard URLs to the HQ registry."""
def can_register_active_site(user) -> bool:
"""GM / GM Admin may add site dashboard URLs to the HQ registry."""
return bool(
user
and getattr(user, "is_authenticated", False)
@@ -105,9 +105,9 @@ def can_manage_users(user) -> bool:
return _status(user) in USER_ADMIN_STATUSES
def can_manage_city_registry(user) -> bool:
"""Alias used by city-site write endpoints — same as register permission."""
return can_register_city_site(user) or bool(getattr(user, "is_superuser", False))
def can_manage_active_site_registry(user) -> bool:
"""Alias used by active-site write endpoints — same as register permission."""
return can_register_active_site(user) or bool(getattr(user, "is_superuser", False))
class IsExecutive(permissions.BasePermission):
@@ -120,9 +120,9 @@ class IsExecutiveAdmin(permissions.BasePermission):
return can_manage_users(request.user)
class CanRegisterCitySite(permissions.BasePermission):
class CanRegisterActiveSite(permissions.BasePermission):
def has_permission(self, request, view):
return can_register_city_site(request.user) or bool(
return can_register_active_site(request.user) or bool(
getattr(request.user, "is_superuser", False)
)
+23 -21
View File
@@ -1,11 +1,11 @@
"""Visibility scoping for HQ executives.
- director / director_admin: all sites / city sites / GMs
- director / director_admin: all sites / active sites / GMs
- buh / bu_admin: only GMs (and gm_admins) they manage, plus those GMs' sites
- gm / gm_admin: only their own sites / city sites they registered
- gm / gm_admin: only their own sites / active sites they registered
Farm Site rows are only visible to non-directors when linked to an active
CitySite in the viewer's Pengaturan registry.
ActiveSite in the viewer's Pengaturan registry.
"""
from __future__ import annotations
@@ -61,7 +61,7 @@ def managed_gm_ids(buh: User) -> list[int]:
def visible_owner_ids(user: User) -> list[int] | None:
"""
Site.user / CitySite.registered_by ids this viewer may see.
Site.user / ActiveSite.registered_by ids this viewer may see.
Returns None when there is no restriction (director / superuser).
"""
@@ -74,13 +74,13 @@ def visible_owner_ids(user: User) -> list[int] | None:
return []
def visible_active_city_site_ids(user: User) -> list[int] | None:
def visible_active_site_ids(user: User) -> list[int] | None:
"""
Active CitySite pks in this viewer's Pengaturan registry.
Enabled ActiveSite pks in this viewer's Pengaturan registry.
Returns None for directors (no restriction). Empty list means no farm sites.
"""
from apps.sync.models import CitySite
from apps.sync.models import ActiveSite
owners = visible_owner_ids(user)
if owners is None:
@@ -88,7 +88,7 @@ def visible_active_city_site_ids(user: User) -> list[int] | None:
if not owners:
return []
return list(
CitySite.objects.filter(
ActiveSite.objects.filter(
registered_by_id__in=owners,
is_active=True,
).values_list("pk", flat=True)
@@ -120,13 +120,13 @@ def filter_sites_queryset(qs: QuerySet, viewer: User) -> QuerySet:
return qs
if not owners:
return qs.none()
city_ids = visible_active_city_site_ids(viewer)
if not city_ids:
active_ids = visible_active_site_ids(viewer)
if not active_ids:
return qs.none()
return qs.filter(user_id__in=owners, city_site_id__in=city_ids)
return qs.filter(user_id__in=owners, active_site_id__in=active_ids)
def filter_city_sites_queryset(qs: QuerySet, viewer: User) -> QuerySet:
def filter_active_sites_queryset(qs: QuerySet, viewer: User) -> QuerySet:
owners = visible_owner_ids(viewer)
if owners is None:
return qs
@@ -141,10 +141,12 @@ def filter_kandangs_queryset(qs: QuerySet, viewer: User) -> QuerySet:
return qs
if not owners:
return qs.none()
city_ids = visible_active_city_site_ids(viewer)
if not city_ids:
active_ids = visible_active_site_ids(viewer)
if not active_ids:
return qs.none()
return qs.filter(site__user_id__in=owners, site__city_site_id__in=city_ids)
return qs.filter(
site__user_id__in=owners, site__active_site_id__in=active_ids
)
def filter_flocks_queryset(qs: QuerySet, viewer: User) -> QuerySet:
@@ -153,12 +155,12 @@ def filter_flocks_queryset(qs: QuerySet, viewer: User) -> QuerySet:
return qs
if not owners:
return qs.none()
city_ids = visible_active_city_site_ids(viewer)
if not city_ids:
active_ids = visible_active_site_ids(viewer)
if not active_ids:
return qs.none()
return qs.filter(
kandang__site__user_id__in=owners,
kandang__site__city_site_id__in=city_ids,
kandang__site__active_site_id__in=active_ids,
)
@@ -168,10 +170,10 @@ def filter_cycles_queryset(qs: QuerySet, viewer: User) -> QuerySet:
return qs
if not owners:
return qs.none()
city_ids = visible_active_city_site_ids(viewer)
if not city_ids:
active_ids = visible_active_site_ids(viewer)
if not active_ids:
return qs.none()
return qs.filter(
kandang__site__user_id__in=owners,
kandang__site__city_site_id__in=city_ids,
kandang__site__active_site_id__in=active_ids,
)
+15 -15
View File
@@ -3,7 +3,7 @@ from rest_framework.test import APIClient
from apps.accounts.models import User
from apps.farms.models import Kandang, Site
from apps.sync.models import CitySite
from apps.sync.models import ActiveSite
class VisibilityScopeTests(TestCase):
@@ -27,23 +27,23 @@ class VisibilityScopeTests(TestCase):
status=User.STATUS_GM,
managed_by=self.buh_other,
)
self.city_a = CitySite.objects.create(
self.active_a = ActiveSite.objects.create(
code="a",
name="City A",
name="Site A",
api_base_url="https://a.example/api/v1",
registered_by=self.gm,
)
self.city_b = CitySite.objects.create(
self.active_b = ActiveSite.objects.create(
code="b",
name="City B",
name="Site B",
api_base_url="https://b.example/api/v1",
registered_by=self.gm_other,
)
self.site = Site.objects.create(
site_name="Site A", user=self.gm, city_site=self.city_a
site_name="Site A", user=self.gm, active_site=self.active_a
)
self.site_other = Site.objects.create(
site_name="Site B", user=self.gm_other, city_site=self.city_b
site_name="Site B", user=self.gm_other, active_site=self.active_b
)
self.orphan = Site.objects.create(site_name="Orphan Seed", user=self.gm)
Kandang.objects.create(kandang_name="K1", site=self.site)
@@ -70,9 +70,9 @@ class VisibilityScopeTests(TestCase):
names = {row["display_name"] for row in response.data["results"]}
self.assertEqual(names, {"gm1"})
def test_gm_sees_own_city_sites_only(self):
def test_gm_sees_own_active_sites_only(self):
self.client.force_authenticate(self.gm)
response = self.client.get("/api/v1/city-sites/")
response = self.client.get("/api/v1/active-sites/")
self.assertEqual(response.status_code, 200)
codes = {row["code"] for row in response.data["results"]}
self.assertEqual(codes, {"a"})
@@ -85,16 +85,16 @@ class VisibilityScopeTests(TestCase):
self.assertEqual(names, {"Site A"})
def test_gm_with_empty_registry_sees_no_sites(self):
self.city_a.delete()
self.active_a.delete()
self.client.force_authenticate(self.gm)
response = self.client.get("/api/v1/sites/")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["results"], [])
def test_registering_city_site_attaches_matching_seed_site(self):
def test_registering_active_site_attaches_matching_seed_site(self):
self.client.force_authenticate(self.gm)
response = self.client.post(
"/api/v1/city-sites/",
"/api/v1/active-sites/",
{
"code": "orphan-seed",
"name": "Orphan Seed",
@@ -105,14 +105,14 @@ class VisibilityScopeTests(TestCase):
)
self.assertEqual(response.status_code, 201)
self.orphan.refresh_from_db()
self.assertEqual(self.orphan.city_site_id, response.data["id"])
self.assertEqual(self.orphan.active_site_id, response.data["id"])
listed = self.client.get("/api/v1/sites/")
names = {row["site_name"] for row in listed.data["results"]}
self.assertIn("Orphan Seed", names)
def test_director_sees_all_city_sites(self):
def test_director_sees_all_active_sites(self):
self.client.force_authenticate(self.director)
response = self.client.get("/api/v1/city-sites/")
response = self.client.get("/api/v1/active-sites/")
self.assertEqual(response.status_code, 200)
codes = {row["code"] for row in response.data["results"]}
self.assertEqual(codes, {"a", "b"})