Add hardware-linked USB intake and inspection workflow
This commit is contained in:
98
inventory/api_station.py
Normal file
98
inventory/api_station.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""USB station: guarded intake updates and idempotent inspection snapshots."""
|
||||
import hashlib
|
||||
import json
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import serializers
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from .api import SecureAPIResponseMixin, StrictSerializer, DeviceSerializer, apply_changes
|
||||
from .api_identity import canonical_hwid
|
||||
from .models import Device, DeviceIdentity, ChoiceValue, InspectionSubmission, InspectionRecord, AuditLog
|
||||
from .services import add_audit
|
||||
from .workflow import advance_device
|
||||
|
||||
class IdentityRequest(StrictSerializer):
|
||||
hwid = serializers.CharField(max_length=100)
|
||||
expected_revision = serializers.IntegerField(min_value=1)
|
||||
def validate_hwid(self, value): return canonical_hwid(value)
|
||||
|
||||
class SpecsSerializer(StrictSerializer):
|
||||
model = serializers.CharField(max_length=200, required=False)
|
||||
cpu = serializers.CharField(max_length=200, required=False)
|
||||
ram_gb = serializers.IntegerField(min_value=1, max_value=2048, required=False)
|
||||
ssd_gb = serializers.IntegerField(min_value=1, required=False)
|
||||
hdd_gb = serializers.IntegerField(min_value=1, required=False)
|
||||
|
||||
class IntakeRequest(IdentityRequest):
|
||||
specifications = SpecsSerializer()
|
||||
|
||||
class ResultSerializer(StrictSerializer):
|
||||
outcome = serializers.ChoiceField(choices=['NOT_TESTED','NOT_APPLICABLE','PASS','HARDWARE_FAILURE','UNAVAILABLE','SETUP_PROBLEM','NEEDS_REVIEW'])
|
||||
notes = serializers.CharField(max_length=10000, allow_blank=True, required=False, default='')
|
||||
evidence = serializers.CharField(max_length=100000, allow_blank=True, required=False, default='')
|
||||
history = serializers.ListField(child=serializers.JSONField(), max_length=500, required=False, default=list)
|
||||
|
||||
class InspectionRequest(IdentityRequest):
|
||||
request_id = serializers.UUIDField()
|
||||
phase = serializers.ChoiceField(choices=['STARTED','IN_PROGRESS','COMPLETE'])
|
||||
technician = serializers.CharField(max_length=200, allow_blank=True)
|
||||
notes = serializers.CharField(max_length=10000, allow_blank=True)
|
||||
results = serializers.DictField(child=ResultSerializer())
|
||||
def validate_results(self, value):
|
||||
if len(value)>50 or len(json.dumps(value))>500000:
|
||||
raise serializers.ValidationError('Too many inspection results.')
|
||||
if any(len(k)>80 for k in value): raise serializers.ValidationError('Invalid test name.')
|
||||
return value
|
||||
def validate(self, attrs):
|
||||
if attrs['phase']=='COMPLETE' and (not attrs['technician'].strip() or not attrs['results'] or any(r['outcome'] in ['NOT_TESTED','NEEDS_REVIEW'] for r in attrs['results'].values())):
|
||||
raise serializers.ValidationError('Complete all tests and enter the technician name first.')
|
||||
return attrs
|
||||
|
||||
def locked_device(number, hwid):
|
||||
d=get_object_or_404(Device.objects.select_for_update(), number=number, archived_at__isnull=True)
|
||||
if not DeviceIdentity.objects.filter(device=d,hwid=hwid).exists():
|
||||
raise serializers.ValidationError('Hardware identity does not match this device.')
|
||||
return d
|
||||
|
||||
class IntakeSyncView(SecureAPIResponseMixin, APIView):
|
||||
@transaction.atomic
|
||||
def post(self, request, number):
|
||||
s=IntakeRequest(data=request.data);s.is_valid(raise_exception=True);v=s.validated_data
|
||||
d=locked_device(number,v['hwid'])
|
||||
if d.stage != Device.Stage.INTAKE or d.revision != v['expected_revision']:
|
||||
return Response({'detail':'Device state changed. Refresh before intake sync.'},status=409)
|
||||
changes={k:x for k,x in v['specifications'].items() if getattr(d,k)!=x}
|
||||
if changes:
|
||||
d=apply_changes(d,changes,user=request.user,api_client=request.auth.name,expected_revision=d.revision)
|
||||
return Response(DeviceSerializer(d).data)
|
||||
|
||||
class InspectionSyncView(SecureAPIResponseMixin, APIView):
|
||||
@transaction.atomic
|
||||
def post(self, request, number):
|
||||
s=InspectionRequest(data=request.data);s.is_valid(raise_exception=True);v=s.validated_data
|
||||
d=locked_device(number,v['hwid'])
|
||||
payload={k:x for k,x in request.data.items() if k not in ['expected_revision','request_id']}
|
||||
digest=hashlib.sha256(json.dumps(payload,sort_keys=True,separators=(',',':')).encode()).hexdigest()
|
||||
previous=InspectionSubmission.objects.filter(request_id=v['request_id']).first()
|
||||
if previous:
|
||||
if previous.device_id != d.pk or previous.payload_hash != digest:
|
||||
return Response({'detail':'Request ID already used for different results.'},status=409)
|
||||
return Response(DeviceSerializer(d).data)
|
||||
if d.revision != v['expected_revision'] or d.stage not in [Device.Stage.INTAKE,Device.Stage.INSPECTION,Device.Stage.REPAIR]:
|
||||
return Response({'detail':'Device state changed. Refresh before inspection sync.'},status=409)
|
||||
if d.stage != Device.Stage.INSPECTION:
|
||||
d=advance_device(d.pk,Device.Stage.INSPECTION,user=request.user,note='USB inspection',expected_revision=d.revision)
|
||||
state=ChoiceValue.objects.get(kind='status',label='Inspektion',active=True)
|
||||
if d.status_id != state.pk:
|
||||
old=d.status;d.status=state;d.save(update_fields=['status'])
|
||||
add_audit(request.user,d,AuditLog.Action.UPDATE,'Status',old,state,api_client=request.auth.name)
|
||||
else:
|
||||
d.save(update_fields=['updated_at'])
|
||||
InspectionSubmission.objects.create(request_id=v['request_id'],device=d,payload_hash=digest,payload=payload,recorded_by=request.user)
|
||||
lines=[f"USB {v['phase']} · {v['technician']}",v['notes']]
|
||||
lines += [f"{key}: {result['outcome']} — {result['notes']}" for key,result in v['results'].items()]
|
||||
summary='\n'.join(lines)
|
||||
InspectionRecord.objects.create(device=d,defects=summary,recorded_by=request.user)
|
||||
add_audit(request.user,d,AuditLog.Action.UPDATE,'USB inspection','',summary,api_client=request.auth.name)
|
||||
return Response(DeviceSerializer(d).data,status=201)
|
||||
Reference in New Issue
Block a user