Files
ShooterHub/apps/gears/serializers.py

392 lines
14 KiB
Python
Raw Normal View History

from rest_framework import serializers
from apps.calibers.models import Caliber
from .models import (
Ammo,
Bipod,
Brass,
Bullet,
Firearm,
Gear,
GearStatus,
Magazine,
Powder,
Primer,
ReloadedAmmoBatch,
ReloadRecipe,
Rig,
RigItem,
Scope,
Suppressor,
UserGear,
get_concrete_gear,
)
# ── Caliber helper serializer ─────────────────────────────────────────────────
class CaliberMinSerializer(serializers.ModelSerializer):
class Meta:
model = Caliber
fields = ['id', 'name', 'short_name']
# ── Gear catalog serializers ──────────────────────────────────────────────────
class GearBaseSerializer(serializers.ModelSerializer):
"""Common read-only fields for every gear type (used in list views)."""
class Meta:
model = Gear
fields = [
'id', 'brand', 'model_name', 'description',
'gear_type', 'status', 'created_at',
]
read_only_fields = ['gear_type', 'status', 'created_at']
class FirearmSerializer(serializers.ModelSerializer):
caliber = serializers.PrimaryKeyRelatedField(
queryset=Caliber.objects.filter(status='VERIFIED'),
required=False,
allow_null=True,
)
caliber_detail = serializers.SerializerMethodField()
def get_caliber_detail(self, obj):
if obj.caliber_id:
return CaliberMinSerializer(obj.caliber).data
return None
class Meta:
model = Firearm
fields = [
'id', 'brand', 'model_name', 'description',
'gear_type', 'status',
'firearm_type', 'caliber', 'caliber_detail',
'barrel_length_mm', 'magazine_capacity',
'submitted_by', 'reviewed_by', 'reviewed_at', 'created_at',
]
read_only_fields = ['gear_type', 'status', 'submitted_by', 'reviewed_by', 'reviewed_at', 'created_at', 'caliber_detail']
class ScopeSerializer(serializers.ModelSerializer):
class Meta:
model = Scope
fields = [
'id', 'brand', 'model_name', 'description',
'gear_type', 'status',
'magnification_min', 'magnification_max',
'objective_diameter_mm', 'tube_diameter_mm', 'reticle_type',
'adjustment_unit', 'focal_plane',
'submitted_by', 'reviewed_by', 'reviewed_at', 'created_at',
]
read_only_fields = ['gear_type', 'status', 'submitted_by', 'reviewed_by', 'reviewed_at', 'created_at']
class SuppressorSerializer(serializers.ModelSerializer):
max_caliber = serializers.PrimaryKeyRelatedField(
queryset=Caliber.objects.filter(status='VERIFIED'),
required=False,
allow_null=True,
)
max_caliber_detail = serializers.SerializerMethodField()
def get_max_caliber_detail(self, obj):
if obj.max_caliber_id:
return CaliberMinSerializer(obj.max_caliber).data
return None
class Meta:
model = Suppressor
fields = [
'id', 'brand', 'model_name', 'description',
'gear_type', 'status',
'max_caliber', 'max_caliber_detail', 'thread_pitch', 'length_mm', 'weight_g',
'submitted_by', 'reviewed_by', 'reviewed_at', 'created_at',
]
read_only_fields = ['gear_type', 'status', 'submitted_by', 'reviewed_by', 'reviewed_at', 'created_at', 'max_caliber_detail']
class BipodSerializer(serializers.ModelSerializer):
class Meta:
model = Bipod
fields = [
'id', 'brand', 'model_name', 'description',
'gear_type', 'status',
'min_height_mm', 'max_height_mm', 'attachment_type',
'submitted_by', 'reviewed_by', 'reviewed_at', 'created_at',
]
read_only_fields = ['gear_type', 'status', 'submitted_by', 'reviewed_by', 'reviewed_at', 'created_at']
class MagazineSerializer(serializers.ModelSerializer):
caliber = serializers.PrimaryKeyRelatedField(
queryset=Caliber.objects.filter(status='VERIFIED'),
required=False,
allow_null=True,
)
caliber_detail = serializers.SerializerMethodField()
def get_caliber_detail(self, obj):
if obj.caliber_id:
return CaliberMinSerializer(obj.caliber).data
return None
class Meta:
model = Magazine
fields = [
'id', 'brand', 'model_name', 'description',
'gear_type', 'status',
'caliber', 'caliber_detail', 'capacity',
'submitted_by', 'reviewed_by', 'reviewed_at', 'created_at',
]
read_only_fields = ['gear_type', 'status', 'submitted_by', 'reviewed_by', 'reviewed_at', 'created_at', 'caliber_detail']
# Maps gear_type discriminator → serializer class
_GEAR_SERIALIZER_MAP = {
'FIREARM': FirearmSerializer,
'SCOPE': ScopeSerializer,
'SUPPRESSOR': SuppressorSerializer,
'BIPOD': BipodSerializer,
'MAGAZINE': MagazineSerializer,
}
class PolymorphicGearSerializer(serializers.BaseSerializer):
"""
Read-only serializer that dispatches to the correct typed serializer
based on gear_type. Used when embedding gear details in nested responses.
"""
def to_representation(self, instance):
concrete = get_concrete_gear(instance)
serializer_cls = _GEAR_SERIALIZER_MAP.get(instance.gear_type, GearBaseSerializer)
return serializer_cls(concrete, context=self.context).data
# ── User inventory serializers ────────────────────────────────────────────────
class UserGearSerializer(serializers.ModelSerializer):
# Write: accept a gear FK (VERIFIED or user's own PENDING)
gear = serializers.PrimaryKeyRelatedField(
queryset=Gear.objects.none(), # narrowed in __init__
write_only=True,
)
# Read: return full typed gear details
gear_detail = PolymorphicGearSerializer(source='gear', read_only=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
from django.db.models import Q
request = self.context.get('request')
if request and request.user.is_authenticated:
self.fields['gear'].queryset = Gear.objects.filter(
Q(status=GearStatus.VERIFIED) |
Q(status=GearStatus.PENDING, submitted_by=request.user)
)
else:
self.fields['gear'].queryset = Gear.objects.filter(status=GearStatus.VERIFIED)
class Meta:
model = UserGear
fields = [
'id',
'gear', # write
'gear_detail', # read
'nickname', 'serial_number', 'purchase_date', 'notes',
'added_at',
]
read_only_fields = ['added_at']
# ── Rig serializers ───────────────────────────────────────────────────────────
class RigItemReadSerializer(serializers.ModelSerializer):
user_gear = UserGearSerializer(read_only=True)
class Meta:
model = RigItem
fields = ['id', 'user_gear', 'role']
class RigItemCreateSerializer(serializers.ModelSerializer):
"""Used when adding an item to a rig (POST /rigs/{id}/items/)."""
user_gear = serializers.PrimaryKeyRelatedField(
queryset=UserGear.objects.none() # narrowed to request.user in __init__
)
class Meta:
model = RigItem
fields = ['id', 'user_gear', 'role']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
request = self.context.get('request')
if request and request.user.is_authenticated:
self.fields['user_gear'].queryset = UserGear.objects.filter(
user=request.user
)
def validate(self, attrs):
rig = self.context['rig']
# Build a temporary instance for clean() validation
instance = RigItem(rig=rig, **attrs)
instance.clean()
return attrs
def create(self, validated_data):
rig = self.context['rig']
return RigItem.objects.create(rig=rig, **validated_data)
class RigSerializer(serializers.ModelSerializer):
rig_items = RigItemReadSerializer(many=True, read_only=True)
primary_caliber = serializers.SerializerMethodField()
def get_primary_caliber(self, obj):
for item in obj.rig_items.all():
if item.role == 'PRIMARY':
gear = item.user_gear.gear
# Firearm caliber lives on the MTI child table
try:
firearm = gear.firearm
if firearm.caliber_id:
return {'id': firearm.caliber_id, 'name': firearm.caliber.name}
return None
except Exception:
return None
return None
class Meta:
model = Rig
fields = ['id', 'name', 'description', 'is_public', 'primary_caliber', 'rig_items', 'created_at', 'updated_at']
read_only_fields = ['created_at', 'updated_at']
# ── Ammo catalog serializer ───────────────────────────────────────────────────
class AmmoSerializer(serializers.ModelSerializer):
caliber = serializers.PrimaryKeyRelatedField(
queryset=Caliber.objects.filter(status='VERIFIED'),
required=False,
allow_null=True,
)
caliber_detail = serializers.SerializerMethodField()
def get_caliber_detail(self, obj):
if obj.caliber_id:
return CaliberMinSerializer(obj.caliber).data
return None
class Meta:
model = Ammo
fields = [
'id', 'brand', 'name', 'caliber', 'caliber_detail',
'bullet_weight_gr', 'bullet_type',
'primer_size', 'case_material',
'muzzle_velocity_fps', 'muzzle_energy_ftlb',
'box_count', 'notes', 'status',
'submitted_by', 'reviewed_by', 'reviewed_at', 'created_at',
]
read_only_fields = ['status', 'submitted_by', 'reviewed_by', 'reviewed_at', 'created_at', 'caliber_detail']
# ── Reloading component serializers ──────────────────────────────────────────
class PrimerSerializer(serializers.ModelSerializer):
class Meta:
model = Primer
fields = ['id', 'brand', 'name', 'size', 'notes', 'status', 'submitted_by']
read_only_fields = ['status', 'submitted_by']
class BrassSerializer(serializers.ModelSerializer):
caliber = serializers.PrimaryKeyRelatedField(
queryset=Caliber.objects.filter(status='VERIFIED'),
required=False,
allow_null=True,
)
caliber_detail = serializers.SerializerMethodField()
def get_caliber_detail(self, obj):
if obj.caliber_id:
return CaliberMinSerializer(obj.caliber).data
return None
class Meta:
model = Brass
fields = ['id', 'brand', 'caliber', 'caliber_detail', 'primer_pocket', 'trim_length_mm', 'notes', 'status', 'submitted_by']
read_only_fields = ['status', 'submitted_by', 'caliber_detail']
class BulletSerializer(serializers.ModelSerializer):
class Meta:
model = Bullet
fields = [
'id', 'brand', 'model_name', 'weight_gr', 'bullet_type',
'diameter_mm', 'length_mm', 'bc_g1', 'bc_g7',
'status', 'submitted_by',
]
read_only_fields = ['status', 'submitted_by']
class PowderSerializer(serializers.ModelSerializer):
class Meta:
model = Powder
fields = ['id', 'brand', 'name', 'powder_type', 'burn_rate_index', 'notes', 'status', 'submitted_by']
read_only_fields = ['status', 'submitted_by']
# ── Reload development serializers ────────────────────────────────────────────
class ReloadedAmmoBatchSerializer(serializers.ModelSerializer):
powder_detail = PowderSerializer(source='powder', read_only=True)
class Meta:
model = ReloadedAmmoBatch
fields = [
'id', 'recipe',
'powder', # write (PK)
'powder_detail', # read (nested)
'powder_charge_gr', 'quantity',
'oal_mm', 'coal_mm', 'crimp',
'case_prep_notes', 'notes', 'loaded_at',
'created_at', 'updated_at',
]
read_only_fields = ['created_at', 'updated_at']
class ReloadRecipeSerializer(serializers.ModelSerializer):
caliber = serializers.PrimaryKeyRelatedField(
queryset=Caliber.objects.filter(status='VERIFIED'),
required=False,
allow_null=True,
)
caliber_detail = serializers.SerializerMethodField()
primer_detail = PrimerSerializer(source='primer', read_only=True)
brass_detail = BrassSerializer(source='brass', read_only=True)
bullet_detail = BulletSerializer(source='bullet', read_only=True)
batches = ReloadedAmmoBatchSerializer(many=True, read_only=True)
def get_caliber_detail(self, obj):
if obj.caliber_id:
return CaliberMinSerializer(obj.caliber).data
return None
class Meta:
model = ReloadRecipe
fields = [
'id', 'name', 'caliber', 'caliber_detail',
'primer', # write
'primer_detail', # read
'brass',
'brass_detail',
'bullet',
'bullet_detail',
'notes', 'is_public', 'batches',
'created_at', 'updated_at',
]
read_only_fields = ['created_at', 'updated_at', 'caliber_detail']