250 lines
9.8 KiB
Python
250 lines
9.8 KiB
Python
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 (
|
|
ActiveSiteClaimSerializer,
|
|
ActiveSiteSerializer,
|
|
ActiveSiteWriteSerializer,
|
|
SiteRegisterSerializer,
|
|
)
|
|
|
|
|
|
class ActiveSiteViewSet(viewsets.ModelViewSet):
|
|
"""
|
|
List/read: scoped (director all; BUH managed GMs; GM own).
|
|
Create (claim): GM and GM Admin claim a site-registered ActiveSite by code.
|
|
Sync: GM and GM Admin only (pulls farm pusat export into HQ mirror).
|
|
|
|
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 == "register_from_site":
|
|
return SiteRegisterSerializer
|
|
if self.action == "create":
|
|
return ActiveSiteClaimSerializer
|
|
if self.action in {"update", "partial_update"}:
|
|
return ActiveSiteWriteSerializer
|
|
return ActiveSiteSerializer
|
|
|
|
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 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):
|
|
return Response({"detail": "Sync not allowed."}, status=status.HTTP_403_FORBIDDEN)
|
|
active_site = self.get_object()
|
|
if not active_site.is_active:
|
|
return Response(
|
|
{"detail": "Active site is inactive."},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
if not active_site.api_base_url or not active_site.api_key:
|
|
return Response(
|
|
{"detail": "Active site is missing api_base_url or api_key."},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
if not (active_site.source_site_id or "").strip():
|
|
return Response(
|
|
{"detail": "Active site is missing source_site_id."},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
from apps.sync.services.mirror_pull import STATUS_OK, sync_active_site_safe
|
|
|
|
summary = sync_active_site_safe(active_site)
|
|
active_site.refresh_from_db()
|
|
http_status = (
|
|
status.HTTP_200_OK
|
|
if summary.status == STATUS_OK
|
|
else status.HTTP_502_BAD_GATEWAY
|
|
)
|
|
return Response(
|
|
{
|
|
"detail": summary.message or "Sync finished.",
|
|
"summary": summary.to_dict(),
|
|
"active_site": ActiveSiteSerializer(active_site).data,
|
|
},
|
|
status=http_status,
|
|
)
|
|
|
|
@action(detail=False, methods=["post"], url_path="sync-all")
|
|
def sync_all(self, request):
|
|
if not can_sync(request.user):
|
|
return Response({"detail": "Sync not allowed."}, status=status.HTTP_403_FORBIDDEN)
|
|
from apps.sync.services.mirror_pull import STATUS_OK, sync_all_active_sites
|
|
|
|
sites = list(self.get_queryset().filter(is_active=True))
|
|
summaries = sync_all_active_sites(queryset=sites)
|
|
ok = sum(1 for s in summaries if s.status == STATUS_OK)
|
|
return Response(
|
|
{
|
|
"detail": f"Synced {ok}/{len(summaries)} active site(s).",
|
|
"active_site_count": len(summaries),
|
|
"summaries": [s.to_dict() for s in summaries],
|
|
}
|
|
)
|