diff --git a/backend/README.md b/backend/README.md index c665820..1bccd81 100644 --- a/backend/README.md +++ b/backend/README.md @@ -17,8 +17,8 @@ Main-office **mirror API** for the executive frontend. Domain shapes match site `bootstrap_admin` / seed `admin` uses status `superuser`. This app does not use `is_staff`. GM↔BUH link: `User.managed_by` (confirmed). **Only `gm`** may request a **`buh`** (not `bu_admin` / `gm_admin`); **only that `buh`** may approve. -Active site registry ownership: `ActiveSite.registered_by`. -GM farm visibility follows the Pengaturan active-site list: only HQ `Site` rows linked to an enabled registered `ActiveSite` are shown (empty list ⇒ no sites). +Active site registry ownership: `ActiveSite.managed_by`. +GM farm visibility follows the Pengaturan active-site list: only HQ `Site` rows linked to an enabled `ActiveSite` they manage are shown (empty list ⇒ no sites). BUH farm visibility is the union of those lists for **confirmed** managed GMs (read-only Pengaturan rollup; optional GM filter in the header). `POST /api/v1/cycles/{id}/approve-close/` — GM only. diff --git a/backend/apps/accounts/scoping.py b/backend/apps/accounts/scoping.py index c8eb819..6a0721c 100644 --- a/backend/apps/accounts/scoping.py +++ b/backend/apps/accounts/scoping.py @@ -2,9 +2,9 @@ - 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 / active sites they registered +- gm / gm_admin: only their own sites / active sites they manage -Farm Site rows are only visible to non-directors when linked to an active +Farm Site rows are only visible to non-directors when linked to an enabled ActiveSite in the viewer's Pengaturan registry. """ @@ -61,7 +61,7 @@ def managed_gm_ids(buh: User) -> list[int]: def visible_owner_ids(user: User) -> list[int] | None: """ - Site.user / ActiveSite.registered_by ids this viewer may see. + Site.user / ActiveSite.managed_by ids this viewer may see. Returns None when there is no restriction (director / superuser). """ @@ -89,7 +89,7 @@ def visible_active_site_ids(user: User) -> list[int] | None: return [] return list( ActiveSite.objects.filter( - registered_by_id__in=owners, + managed_by_id__in=owners, is_active=True, ).values_list("pk", flat=True) ) @@ -132,7 +132,7 @@ def filter_active_sites_queryset(qs: QuerySet, viewer: User) -> QuerySet: return qs if not owners: return qs.none() - return qs.filter(registered_by_id__in=owners) + return qs.filter(managed_by_id__in=owners) def filter_kandangs_queryset(qs: QuerySet, viewer: User) -> QuerySet: diff --git a/backend/apps/accounts/tests_scoping.py b/backend/apps/accounts/tests_scoping.py index a38ba3c..ad1c1eb 100644 --- a/backend/apps/accounts/tests_scoping.py +++ b/backend/apps/accounts/tests_scoping.py @@ -31,13 +31,13 @@ class VisibilityScopeTests(TestCase): code="a", name="Site A", api_base_url="https://a.example/api/v1", - registered_by=self.gm, + managed_by=self.gm, ) self.active_b = ActiveSite.objects.create( code="b", name="Site B", api_base_url="https://b.example/api/v1", - registered_by=self.gm_other, + managed_by=self.gm_other, ) self.site = Site.objects.create( site_name="Site A", user=self.gm, active_site=self.active_a @@ -98,7 +98,6 @@ class VisibilityScopeTests(TestCase): { "code": "orphan-seed", "name": "Orphan Seed", - "api_base_url": "https://orphan.example/api/v1", "is_active": True, }, format="json", diff --git a/backend/apps/farms/services/active_site_registry.py b/backend/apps/farms/services/active_site_registry.py index ab29fa4..26afc44 100644 --- a/backend/apps/farms/services/active_site_registry.py +++ b/backend/apps/farms/services/active_site_registry.py @@ -18,9 +18,9 @@ def ensure_hq_site_for_active_site(active_site: ActiveSite) -> Site: if existing: return existing - owner = active_site.registered_by + owner = active_site.managed_by if owner is None: - raise ValueError("ActiveSite.registered_by is required to create an HQ site.") + raise ValueError("ActiveSite.managed_by is required to create an HQ site.") site_name = (active_site.name or active_site.code or "Site")[:30] orphan = ( diff --git a/backend/apps/farms/tests.py b/backend/apps/farms/tests.py index b3a365c..1c8dac2 100644 --- a/backend/apps/farms/tests.py +++ b/backend/apps/farms/tests.py @@ -11,12 +11,12 @@ from apps.sync.models import ActiveSite def _gm_visible_site(*, owner: User, site_name: str, code: str) -> Site: - """GM scope only sees sites linked to an ActiveSite they registered.""" + """GM scope only sees sites linked to an ActiveSite they manage.""" active_site = ActiveSite.objects.create( code=code, name=site_name, api_base_url=f"https://{code}.example/api/v1", - registered_by=owner, + managed_by=owner, ) return Site.objects.create( site_name=site_name, user=owner, active_site=active_site diff --git a/backend/apps/farms/views.py b/backend/apps/farms/views.py index 51cf653..8b4f381 100644 --- a/backend/apps/farms/views.py +++ b/backend/apps/farms/views.py @@ -48,7 +48,7 @@ def _upsert_mirror_cycle_from_ingest(active_site: ActiveSite, payload: dict) -> site_name = str(payload.get("site_name") or active_site.name)[:30] kandang_name = str(payload.get("kandang_name") or "Kandang")[:30] - owner = active_site.registered_by + owner = active_site.managed_by if owner is None: owner = User.objects.filter(status=User.STATUS_GM).first() if owner is None: diff --git a/backend/apps/jobs/management/commands/seed_demo.py b/backend/apps/jobs/management/commands/seed_demo.py index 5b840d4..2bfb53c 100644 --- a/backend/apps/jobs/management/commands/seed_demo.py +++ b/backend/apps/jobs/management/commands/seed_demo.py @@ -108,8 +108,8 @@ class Command(BaseCommand): if not staff: return moved_sites = Site.objects.filter(user=staff).update(user=gm) - moved_active = ActiveSite.objects.filter(registered_by=staff).update( - registered_by=gm + moved_active = ActiveSite.objects.filter(managed_by=staff).update( + managed_by=gm ) ApiKey.objects.filter(user=staff).delete() staff.delete() diff --git a/backend/apps/sync/migrations/0004_activesite_api_base_url_optional.py b/backend/apps/sync/migrations/0004_activesite_api_base_url_optional.py new file mode 100644 index 0000000..cdf9483 --- /dev/null +++ b/backend/apps/sync/migrations/0004_activesite_api_base_url_optional.py @@ -0,0 +1,36 @@ +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("sync", "0003_rename_citysite_to_activesite"), + ] + + operations = [ + migrations.AlterField( + model_name="activesite", + name="api_base_url", + field=models.URLField( + blank=True, + default="", + help_text="Optional site API root including /api/v1 (for outbound sync/push)", + max_length=255, + ), + ), + migrations.AlterField( + model_name="activesite", + name="registered_by", + field=models.ForeignKey( + blank=True, + help_text="GM / GM Admin who registered this site by name and code", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="registered_active_sites", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/backend/apps/sync/migrations/0005_rename_registered_by_to_managed_by.py b/backend/apps/sync/migrations/0005_rename_registered_by_to_managed_by.py new file mode 100644 index 0000000..c11096a --- /dev/null +++ b/backend/apps/sync/migrations/0005_rename_registered_by_to_managed_by.py @@ -0,0 +1,33 @@ +# Generated manually: rename ActiveSite.registered_by → managed_by + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("sync", "0004_activesite_api_base_url_optional"), + ] + + operations = [ + migrations.RenameField( + model_name="activesite", + old_name="registered_by", + new_name="managed_by", + ), + migrations.AlterField( + model_name="activesite", + name="managed_by", + field=models.ForeignKey( + blank=True, + help_text="GM / GM Admin who manages this active site", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="managed_active_sites", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/backend/apps/sync/models.py b/backend/apps/sync/models.py index c4e8f0f..04f24d9 100644 --- a/backend/apps/sync/models.py +++ b/backend/apps/sync/models.py @@ -17,20 +17,22 @@ class ActiveSite(models.Model): name = models.CharField(max_length=120) api_base_url = models.URLField( max_length=255, - help_text="Site API root including /api/v1, e.g. https://site.example/api/v1", + blank=True, + default="", + help_text="Optional site API root including /api/v1 (for outbound sync/push)", ) api_key = models.CharField( max_length=255, blank=True, help_text="X-API-Key for pulling from the site API (keep server-side only)", ) - registered_by = models.ForeignKey( + managed_by = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, - related_name="registered_active_sites", - help_text="GM / GM Admin who added this site dashboard URL", + related_name="managed_active_sites", + help_text="GM / GM Admin who manages this active site", ) is_active = models.BooleanField(default=True) last_synced_at = models.DateTimeField(null=True, blank=True) diff --git a/backend/apps/sync/serializers.py b/backend/apps/sync/serializers.py index 7b3cea5..bd4012f 100644 --- a/backend/apps/sync/serializers.py +++ b/backend/apps/sync/serializers.py @@ -7,8 +7,8 @@ class ActiveSiteSerializer(serializers.ModelSerializer): id = serializers.IntegerField(source="pk", read_only=True) # Never echo the outbound site API key back to the browser by default. has_api_key = serializers.SerializerMethodField() - registered_by_name = serializers.CharField( - source="registered_by.display_name", read_only=True, allow_null=True + managed_by_name = serializers.CharField( + source="managed_by.display_name", read_only=True, allow_null=True ) class Meta: @@ -19,8 +19,8 @@ class ActiveSiteSerializer(serializers.ModelSerializer): "name", "api_base_url", "has_api_key", - "registered_by", - "registered_by_name", + "managed_by", + "managed_by_name", "is_active", "last_synced_at", "last_sync_status", @@ -31,8 +31,8 @@ class ActiveSiteSerializer(serializers.ModelSerializer): read_only_fields = [ "id", "has_api_key", - "registered_by", - "registered_by_name", + "managed_by", + "managed_by_name", "last_synced_at", "last_sync_status", "last_sync_message", diff --git a/backend/apps/sync/views.py b/backend/apps/sync/views.py index f3a2519..4abb820 100644 --- a/backend/apps/sync/views.py +++ b/backend/apps/sync/views.py @@ -22,7 +22,7 @@ class ActiveSiteViewSet(viewsets.ModelViewSet): executive_sync_action = True def get_queryset(self): - qs = ActiveSite.objects.select_related("registered_by").all() + qs = ActiveSite.objects.select_related("managed_by").all() return filter_active_sites_queryset(qs, self.request.user) def get_permissions(self): @@ -36,7 +36,7 @@ class ActiveSiteViewSet(viewsets.ModelViewSet): return ActiveSiteSerializer def perform_create(self, serializer): - active_site = serializer.save(registered_by=self.request.user) + active_site = serializer.save(managed_by=self.request.user) ensure_hq_site_for_active_site(active_site) def perform_update(self, serializer): diff --git a/components/SettingsPage.tsx b/components/SettingsPage.tsx index f609c0a..94092d0 100644 --- a/components/SettingsPage.tsx +++ b/components/SettingsPage.tsx @@ -68,10 +68,10 @@ const BuhViewList: React.FC = () => { const sitesByGm = useMemo(() => { const map = new Map(); for (const site of activeSites) { - if (site.registered_by == null) continue; - const rows = map.get(site.registered_by) ?? []; + if (site.managed_by == null) continue; + const rows = map.get(site.managed_by) ?? []; rows.push(site); - map.set(site.registered_by, rows); + map.set(site.managed_by, rows); } return map; }, [activeSites]); @@ -224,8 +224,6 @@ const GmViewList: React.FC = () => { const [name, setName] = useState(''); const [code, setCode] = useState(''); const [codeTouched, setCodeTouched] = useState(false); - const [apiBaseUrl, setApiBaseUrl] = useState(''); - const [apiKey, setApiKey] = useState(''); const flash = (msg: string) => { setNotice(msg); @@ -263,8 +261,6 @@ const GmViewList: React.FC = () => { setName(''); setCode(''); setCodeTouched(false); - setApiBaseUrl(''); - setApiKey(''); }; const wrap = async (fn: () => Promise) => { @@ -314,7 +310,7 @@ const GmViewList: React.FC = () => {
{error && (
@@ -400,7 +396,8 @@ const GmViewList: React.FC = () => {

Tambah site

- Daftarkan URL API dasbor kota agar data site muncul di daftar tampilan Anda. + Masukkan nama dan kode yang sama dengan yang diisi manajer di dasbor site, agar site + terhubung ke daftar tampilan Anda.

{ const created = await api.activeSites.create({ code: resolvedCode, name: name.trim(), - api_base_url: apiBaseUrl.trim(), - ...(apiKey.trim() ? { api_key: apiKey.trim() } : {}), is_active: true, }); resetForm(); @@ -448,28 +443,6 @@ const GmViewList: React.FC = () => { required /> - -