94 lines
3.0 KiB
Python
94 lines
3.0 KiB
Python
|
|
from rest_framework import serializers
|
||
|
|
|
||
|
|
# Cross-app import (photos → tools). String FK in the model avoids DB circular
|
||
|
|
# import; here we need the class directly for the serializer queryset.
|
||
|
|
from apps.tools.models import ShotGroup
|
||
|
|
|
||
|
|
from .models import GroupPhoto, GroupPhotoAnalysis, Photo, PointOfImpact
|
||
|
|
|
||
|
|
|
||
|
|
class PhotoMetaSerializer(serializers.ModelSerializer):
|
||
|
|
"""Photo metadata only — the binary `data` field is never exposed in JSON."""
|
||
|
|
|
||
|
|
class Meta:
|
||
|
|
model = Photo
|
||
|
|
fields = ['id', 'content_type', 'size', 'width', 'height',
|
||
|
|
'uploaded_by', 'uploaded_at', 'description']
|
||
|
|
read_only_fields = ['id', 'size', 'width', 'height', 'uploaded_by', 'uploaded_at']
|
||
|
|
|
||
|
|
|
||
|
|
class GroupPhotoAnalysisSerializer(serializers.ModelSerializer):
|
||
|
|
class Meta:
|
||
|
|
model = GroupPhotoAnalysis
|
||
|
|
fields = [
|
||
|
|
'group_size_mm', 'group_size_moa',
|
||
|
|
'elevation_offset_mm', 'elevation_offset_moa',
|
||
|
|
'windage_offset_mm', 'windage_offset_moa',
|
||
|
|
'mean_radius_mm', 'mean_radius_moa',
|
||
|
|
'notes',
|
||
|
|
]
|
||
|
|
|
||
|
|
def validate(self, attrs):
|
||
|
|
instance = GroupPhotoAnalysis(**attrs)
|
||
|
|
instance.clean()
|
||
|
|
return attrs
|
||
|
|
|
||
|
|
|
||
|
|
class PointOfImpactSerializer(serializers.ModelSerializer):
|
||
|
|
# Write: accept Shot PK; Read: compact inline summary
|
||
|
|
shot_detail = serializers.SerializerMethodField()
|
||
|
|
|
||
|
|
class Meta:
|
||
|
|
model = PointOfImpact
|
||
|
|
fields = [
|
||
|
|
'id', 'order',
|
||
|
|
'shot', 'shot_detail',
|
||
|
|
'x_px', 'y_px',
|
||
|
|
'x_mm', 'y_mm', 'radius_mm',
|
||
|
|
'notes',
|
||
|
|
]
|
||
|
|
|
||
|
|
def get_shot_detail(self, obj):
|
||
|
|
if not obj.shot_id:
|
||
|
|
return None
|
||
|
|
return {
|
||
|
|
'id': obj.shot.pk,
|
||
|
|
'shot_number': obj.shot.shot_number,
|
||
|
|
'velocity_fps': str(obj.shot.velocity_fps),
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
class GroupPhotoSerializer(serializers.ModelSerializer):
|
||
|
|
photo = PhotoMetaSerializer(read_only=True)
|
||
|
|
photo_id = serializers.PrimaryKeyRelatedField(
|
||
|
|
source='photo',
|
||
|
|
queryset=Photo.objects.all(),
|
||
|
|
write_only=True,
|
||
|
|
)
|
||
|
|
# shot_group is optional — photos can exist independently of any group
|
||
|
|
shot_group = serializers.PrimaryKeyRelatedField(
|
||
|
|
queryset=ShotGroup.objects.all(),
|
||
|
|
required=False, allow_null=True,
|
||
|
|
)
|
||
|
|
shot_group_detail = serializers.SerializerMethodField()
|
||
|
|
analysis = GroupPhotoAnalysisSerializer(read_only=True)
|
||
|
|
points_of_impact = PointOfImpactSerializer(many=True, read_only=True)
|
||
|
|
|
||
|
|
class Meta:
|
||
|
|
model = GroupPhoto
|
||
|
|
fields = [
|
||
|
|
'id', 'photo_id', 'photo',
|
||
|
|
'shot_group', 'shot_group_detail', 'caption', 'order',
|
||
|
|
'is_public', 'analysis', 'points_of_impact',
|
||
|
|
]
|
||
|
|
|
||
|
|
def get_shot_group_detail(self, obj):
|
||
|
|
if not obj.shot_group_id:
|
||
|
|
return None
|
||
|
|
sg = obj.shot_group
|
||
|
|
return {
|
||
|
|
'id': sg.pk,
|
||
|
|
'label': sg.label,
|
||
|
|
'distance_m': str(sg.distance_m) if sg.distance_m else None,
|
||
|
|
}
|