Add source_site_id to ActiveSite model and implement unique constraint. Update serializers and views for site registration and claiming. Enhance tests for site registration and claiming functionality.

This commit is contained in:
Alberto-Audrix committed 2026-09-15 09:45:23 +07:00
1 parent 521f36d22d
commit 61f954a3bd
12 files changed
+684 -172

No files matched your search

+158 -9
View File
@@ -1,49 +1,198 @@
from rest_framework import status, viewsets
from rest_framework import permissions, status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from django.db import IntegrityError
from apps.accounts.permissions import CanRegisterActiveSite, IsExecutive, can_sync
from apps.accounts.scoping import filter_active_sites_queryset
from apps.farms.services.active_site_registry import ensure_hq_site_for_active_site
from apps.sync.models import ActiveSite
from apps.sync.serializers import ActiveSiteSerializer, ActiveSiteWriteSerializer
from apps.sync.serializers import (
ActiveSiteClaimSerializer,
ActiveSiteSerializer,
ActiveSiteWriteSerializer,
SiteRegisterSerializer,
)
class ActiveSiteViewSet(viewsets.ModelViewSet):
"""
List/read: scoped (director all; BUH managed GMs; GM own).
Create/update/delete: GM and GM Admin.
Create (claim): GM and GM Admin claim a site-registered ActiveSite by code.
Sync: GM and GM Admin only (stubs until mirror pull is implemented).
Creating an active site also attaches (or creates) an HQ Site so farm data
appears in the executive view list.
Site dashboards create/fill ActiveSite via register-from-site (API key auth).
Claiming attaches managed_by and ensures an HQ Site for the view list.
Destroy (Hapus) unclaims only — does not delete the ActiveSite row.
"""
executive_sync_action = True
http_method_names = ["get", "post", "put", "patch", "delete", "head", "options"]
def get_queryset(self):
qs = ActiveSite.objects.select_related("managed_by").all()
return filter_active_sites_queryset(qs, self.request.user)
def get_permissions(self):
if self.action == "register_from_site":
return [permissions.IsAuthenticated()]
if self.action in {"create", "update", "partial_update", "destroy"}:
return [CanRegisterActiveSite()]
return [IsExecutive()]
def get_serializer_class(self):
if self.action in {"create", "update", "partial_update"}:
if self.action == "register_from_site":
return SiteRegisterSerializer
if self.action == "create":
return ActiveSiteClaimSerializer
if self.action in {"update", "partial_update"}:
return ActiveSiteWriteSerializer
return ActiveSiteSerializer
def perform_create(self, serializer):
active_site = serializer.save(managed_by=self.request.user)
def create(self, request, *args, **kwargs):
"""GM claim-by-code: site must have registered first."""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
code = serializer.validated_data["code"]
active_site = ActiveSite.objects.filter(code=code).first()
if active_site is None:
return Response(
{
"detail": (
"Site belum mendaftar dari dasbor site. "
"Minta staf site mendaftar dulu dengan kode ini."
)
},
status=status.HTTP_404_NOT_FOUND,
)
owner_id = active_site.managed_by_id
if owner_id and owner_id != request.user.pk:
return Response(
{"detail": "Kode sudah diklaim oleh GM lain."},
status=status.HTTP_409_CONFLICT,
)
if owner_id != request.user.pk:
active_site.managed_by = request.user
active_site.save(update_fields=["managed_by", "updated_at"])
ensure_hq_site_for_active_site(active_site)
return Response(
ActiveSiteSerializer(active_site).data,
status=status.HTTP_200_OK,
)
def perform_update(self, serializer):
active_site = serializer.save()
if active_site.is_active:
if active_site.is_active and active_site.managed_by_id:
ensure_hq_site_for_active_site(active_site)
def destroy(self, request, *args, **kwargs):
"""
Hapus from daftar tampilan: unclaim only.
Keeps the ActiveSite registry row (credentials / source_site_id) so the
site dashboard registration is not wiped. Clears managed_by and unlinks
any HQ Site.active_site pointer.
"""
from apps.farms.models import Site
active_site = self.get_object()
Site.objects.filter(active_site=active_site).update(active_site=None)
active_site.managed_by = None
active_site.save(update_fields=["managed_by", "updated_at"])
return Response(status=status.HTTP_204_NO_CONTENT)
@action(detail=False, methods=["post"], url_path="register-from-site")
def register_from_site(self, request):
"""
Machine endpoint: site dashboard creates/updates ActiveSite credentials.
Does not set managed_by (GM claim is separate).
One farm site_id maps to at most one ActiveSite. Re-register with a new
code updates that row; it never inserts a second ActiveSite.
"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
code = data["code"]
source_site_id = data["site_id"]
active_site = ActiveSite.objects.filter(source_site_id=source_site_id).first()
if active_site is None:
by_code = ActiveSite.objects.filter(code=code).first()
if by_code is not None:
if by_code.source_site_id and by_code.source_site_id != source_site_id:
return Response(
{"detail": "Kode sudah dipakai site lain."},
status=status.HTTP_409_CONFLICT,
)
# Legacy row missing source_site_id: attach this site to it.
active_site = by_code
if active_site is None:
try:
active_site = ActiveSite.objects.create(
code=code,
name=data["name"],
source_site_id=source_site_id,
api_base_url=data["api_base_url"],
api_key=data["api_key"],
managed_by=None,
is_active=True,
)
except IntegrityError:
# Concurrent register for same source_site_id or code.
active_site = ActiveSite.objects.filter(
source_site_id=source_site_id
).first() or ActiveSite.objects.filter(code=code).first()
if active_site is None:
return Response(
{"detail": "Gagal mendaftarkan site — coba lagi."},
status=status.HTTP_409_CONFLICT,
)
else:
return Response(
ActiveSiteSerializer(active_site).data,
status=status.HTTP_201_CREATED,
)
if (
code != active_site.code
and ActiveSite.objects.filter(code=code).exclude(pk=active_site.pk).exists()
):
return Response(
{"detail": "Kode sudah dipakai site lain."},
status=status.HTTP_409_CONFLICT,
)
active_site.code = code
active_site.name = data["name"]
active_site.source_site_id = source_site_id
active_site.api_base_url = data["api_base_url"]
active_site.api_key = data["api_key"]
active_site.is_active = True
try:
active_site.save(
update_fields=[
"code",
"name",
"source_site_id",
"api_base_url",
"api_key",
"is_active",
"updated_at",
]
)
except IntegrityError:
return Response(
{"detail": "Site atau kode bentrok dengan pendaftaran lain."},
status=status.HTTP_409_CONFLICT,
)
return Response(ActiveSiteSerializer(active_site).data, status=status.HTTP_200_OK)
@action(detail=True, methods=["post"], url_path="sync")
def sync(self, request, pk=None):
if not can_sync(request.user):