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:
1 parent
521f36d22d
commit
61f954a3bd
12 files changed
+684
-172
No files matched your search
+158
-9
@@ -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):
|
||||
|
||||
Reference in new issue
Block a user