All checks were successful
Build and Deploy LTY / build-and-deploy (push) Successful in 1h5m35s
- Update card models, serializers, views and URLs - Update dances, songs, users admin pages and API modules - Add card migrations (merge furniture into decoration) - Update middleware and settings Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
467 lines
20 KiB
Python
467 lines
20 KiB
Python
from rest_framework import serializers
|
||
from .models import (
|
||
CardTemplate, Card, CardBatch, CardUsageLog,
|
||
ClothingAttributes, PropAttributes, SongAttributes,
|
||
DanceAttributes, DecorationAttributes
|
||
)
|
||
|
||
|
||
class ClothingAttributesSerializer(serializers.ModelSerializer):
|
||
class Meta:
|
||
model = ClothingAttributes
|
||
fields = [
|
||
'style', 'size', 'color', 'season',
|
||
'material', 'fit_type', 'care_instructions'
|
||
]
|
||
|
||
|
||
class PropAttributesSerializer(serializers.ModelSerializer):
|
||
class Meta:
|
||
model = PropAttributes
|
||
fields = [
|
||
'prop_type', 'material', 'size', 'weight',
|
||
'durability', 'usage_instructions'
|
||
]
|
||
|
||
|
||
class SongAttributesSerializer(serializers.ModelSerializer):
|
||
class Meta:
|
||
model = SongAttributes
|
||
fields = [
|
||
'genre', 'duration', 'bpm', 'composer',
|
||
'lyricist', 'arrangement', 'audio_file', 'lyrics'
|
||
]
|
||
|
||
|
||
class DanceAttributesSerializer(serializers.ModelSerializer):
|
||
class Meta:
|
||
model = DanceAttributes
|
||
fields = [
|
||
'style', 'difficulty', 'duration', 'choreographer',
|
||
'required_space', 'calories_burn', 'tutorial_video'
|
||
]
|
||
|
||
|
||
class DecorationAttributesSerializer(serializers.ModelSerializer):
|
||
class Meta:
|
||
model = DecorationAttributes
|
||
fields = [
|
||
'decoration_type', 'style', 'material', 'size',
|
||
'placement', 'indoor_outdoor', 'installation_required', 'care_instructions'
|
||
]
|
||
|
||
|
||
class CardTemplateSerializer(serializers.ModelSerializer):
|
||
category_display = serializers.SerializerMethodField()
|
||
rarity_display = serializers.SerializerMethodField()
|
||
card_type_display = serializers.SerializerMethodField()
|
||
status_display = serializers.SerializerMethodField()
|
||
image_url = serializers.URLField(source='image', required=False, allow_blank=True)
|
||
|
||
# 新增:所有类别专有属性的write_only字段
|
||
clothing_attributes = ClothingAttributesSerializer(write_only=True, required=False)
|
||
prop_attributes = PropAttributesSerializer(write_only=True, required=False)
|
||
song_attributes = SongAttributesSerializer(write_only=True, required=False)
|
||
dance_attributes = DanceAttributesSerializer(write_only=True, required=False)
|
||
decoration_attributes = DecorationAttributesSerializer(write_only=True, required=False)
|
||
|
||
class Meta:
|
||
model = CardTemplate
|
||
fields = [
|
||
'id', 'name', 'category', 'category_display', 'description',
|
||
'card_type', 'card_type_display', 'rarity', 'rarity_display',
|
||
'image', 'image_url', 'model_url', 'model_version',
|
||
'status', 'status_display', 'published_at', 'price',
|
||
'created_at', 'updated_at',
|
||
# 新增
|
||
'clothing_attributes', 'prop_attributes', 'song_attributes', 'dance_attributes', 'decoration_attributes',
|
||
]
|
||
read_only_fields = ['created_at', 'updated_at', 'published_at']
|
||
|
||
def create(self, validated_data):
|
||
# 把所有 *_attributes 字段pop掉,只留主表字段
|
||
# 属性的创建由 View 层的 create 方法负责
|
||
validated_data.pop('clothing_attributes', None)
|
||
validated_data.pop('prop_attributes', None)
|
||
validated_data.pop('song_attributes', None)
|
||
validated_data.pop('dance_attributes', None)
|
||
validated_data.pop('decoration_attributes', None)
|
||
return super().create(validated_data)
|
||
|
||
def get_category_display(self, obj):
|
||
return dict(CardTemplate.CATEGORY_CHOICES).get(obj.category)
|
||
|
||
def get_rarity_display(self, obj):
|
||
return obj.get_rarity_display()
|
||
|
||
def get_card_type_display(self, obj):
|
||
return obj.get_card_type_display()
|
||
|
||
def get_status_display(self, obj):
|
||
return obj.get_status_display()
|
||
|
||
def to_representation(self, instance):
|
||
representation = super().to_representation(instance)
|
||
# 添加类别专有属性
|
||
try:
|
||
if instance.category == 'clothing' and hasattr(instance, 'clothing_attrs'):
|
||
representation['attributes'] = ClothingAttributesSerializer(instance.clothing_attrs).data
|
||
elif instance.category == 'prop' and hasattr(instance, 'prop_attrs'):
|
||
representation['attributes'] = PropAttributesSerializer(instance.prop_attrs).data
|
||
elif instance.category == 'song' and hasattr(instance, 'song_attrs'):
|
||
representation['attributes'] = SongAttributesSerializer(instance.song_attrs).data
|
||
elif instance.category == 'dance' and hasattr(instance, 'dance_attrs'):
|
||
representation['attributes'] = DanceAttributesSerializer(instance.dance_attrs).data
|
||
elif instance.category == 'decoration' and hasattr(instance, 'decoration_attrs'):
|
||
representation['attributes'] = DecorationAttributesSerializer(instance.decoration_attrs).data
|
||
except Exception:
|
||
representation['attributes'] = None
|
||
return representation
|
||
|
||
def update(self, instance, validated_data):
|
||
# 取出专属属性数据
|
||
clothing_data = validated_data.pop('clothing_attributes', None)
|
||
prop_data = validated_data.pop('prop_attributes', None)
|
||
song_data = validated_data.pop('song_attributes', None)
|
||
dance_data = validated_data.pop('dance_attributes', None)
|
||
decoration_data = validated_data.pop('decoration_attributes', None)
|
||
|
||
# 更新主表
|
||
instance = super().update(instance, validated_data)
|
||
|
||
# 更新专属属性
|
||
if clothing_data is not None and hasattr(instance, 'clothing_attrs'):
|
||
for k, v in clothing_data.items():
|
||
setattr(instance.clothing_attrs, k, v)
|
||
instance.clothing_attrs.save()
|
||
if prop_data is not None and hasattr(instance, 'prop_attrs'):
|
||
for k, v in prop_data.items():
|
||
setattr(instance.prop_attrs, k, v)
|
||
instance.prop_attrs.save()
|
||
if song_data is not None and hasattr(instance, 'song_attrs'):
|
||
for k, v in song_data.items():
|
||
setattr(instance.song_attrs, k, v)
|
||
instance.song_attrs.save()
|
||
if dance_data is not None and hasattr(instance, 'dance_attrs'):
|
||
for k, v in dance_data.items():
|
||
setattr(instance.dance_attrs, k, v)
|
||
instance.dance_attrs.save()
|
||
if decoration_data is not None and hasattr(instance, 'decoration_attrs'):
|
||
for k, v in decoration_data.items():
|
||
setattr(instance.decoration_attrs, k, v)
|
||
instance.decoration_attrs.save()
|
||
|
||
return instance
|
||
|
||
|
||
class CardTemplateDetailSerializer(CardTemplateSerializer):
|
||
"""Detailed template serializer with related batches and cards count"""
|
||
batches_count = serializers.SerializerMethodField()
|
||
active_cards_count = serializers.SerializerMethodField()
|
||
|
||
class Meta(CardTemplateSerializer.Meta):
|
||
fields = CardTemplateSerializer.Meta.fields + ['batches_count', 'active_cards_count']
|
||
|
||
def get_batches_count(self, obj):
|
||
return obj.batches.count()
|
||
|
||
def get_active_cards_count(self, obj):
|
||
# 只计算激活状态的卡片数量
|
||
return obj.cards.filter(status='active').count()
|
||
|
||
def to_representation(self, instance):
|
||
representation = super().to_representation(instance)
|
||
|
||
# 添加类别专有属性
|
||
try:
|
||
if instance.category == 'clothing' and hasattr(instance, 'clothing_attrs'):
|
||
representation['attributes'] = ClothingAttributesSerializer(instance.clothing_attrs).data
|
||
elif instance.category == 'prop' and hasattr(instance, 'prop_attrs'):
|
||
representation['attributes'] = PropAttributesSerializer(instance.prop_attrs).data
|
||
elif instance.category == 'song' and hasattr(instance, 'song_attrs'):
|
||
representation['attributes'] = SongAttributesSerializer(instance.song_attrs).data
|
||
elif instance.category == 'dance' and hasattr(instance, 'dance_attrs'):
|
||
representation['attributes'] = DanceAttributesSerializer(instance.dance_attrs).data
|
||
elif instance.category == 'decoration' and hasattr(instance, 'decoration_attrs'):
|
||
representation['attributes'] = DecorationAttributesSerializer(instance.decoration_attrs).data
|
||
except Exception as e:
|
||
# 处理相关属性不存在的情况
|
||
representation['attributes'] = None
|
||
|
||
return representation
|
||
|
||
|
||
class CardSerializer(serializers.ModelSerializer):
|
||
category_display = serializers.SerializerMethodField()
|
||
template_name = serializers.SerializerMethodField()
|
||
status_display = serializers.SerializerMethodField()
|
||
image_url = serializers.SerializerMethodField()
|
||
|
||
class Meta:
|
||
model = Card
|
||
fields = [
|
||
'id', 'unique_id', 'template', 'template_name', 'name',
|
||
'category', 'category_display', 'description', 'batch',
|
||
'image', 'image_url', 'price', 'status', 'status_display',
|
||
'user', 'used_at', 'manufactured', 'manufactured_at',
|
||
'created_at', 'updated_at'
|
||
]
|
||
read_only_fields = [
|
||
'created_at', 'updated_at', 'used_at',
|
||
'manufactured_at', 'image_url'
|
||
]
|
||
|
||
def get_category_display(self, obj):
|
||
return dict(CardTemplate.CATEGORY_CHOICES).get(obj.category)
|
||
|
||
def get_template_name(self, obj):
|
||
return obj.template.name if obj.template else None
|
||
|
||
def get_status_display(self, obj):
|
||
return obj.get_status_display()
|
||
|
||
def get_image_url(self, obj):
|
||
if obj.image:
|
||
return obj.image.url
|
||
# 如果卡片没有图片,使用模板的图片(模板image现在是URLField字符串)
|
||
elif obj.template and obj.template.image:
|
||
return obj.template.image
|
||
return None
|
||
|
||
|
||
class CardDetailSerializer(CardSerializer):
|
||
"""Detailed card serializer with usage logs"""
|
||
usage_logs = serializers.SerializerMethodField()
|
||
template_details = serializers.SerializerMethodField()
|
||
|
||
class Meta(CardSerializer.Meta):
|
||
fields = CardSerializer.Meta.fields + ['usage_logs', 'template_details']
|
||
|
||
def get_usage_logs(self, obj):
|
||
# Only return the 10 most recent logs
|
||
logs = obj.usage_logs.all().order_by('-created_at')[:10]
|
||
return CardUsageLogSerializer(logs, many=True).data
|
||
|
||
def get_template_details(self, obj):
|
||
if obj.template:
|
||
return {
|
||
'rarity': obj.template.rarity,
|
||
'rarity_display': obj.template.get_rarity_display(),
|
||
'card_type': obj.template.card_type,
|
||
'card_type_display': obj.template.get_card_type_display(),
|
||
'model_url': obj.template.model_url,
|
||
'model_version': obj.template.model_version,
|
||
}
|
||
return None
|
||
|
||
|
||
class CardBatchSerializer(serializers.ModelSerializer):
|
||
category_display = serializers.SerializerMethodField()
|
||
template_name = serializers.SerializerMethodField()
|
||
status_display = serializers.SerializerMethodField()
|
||
excel_file_url = serializers.SerializerMethodField()
|
||
|
||
class Meta:
|
||
model = CardBatch
|
||
fields = [
|
||
'id', 'batch_number', 'template', 'template_name',
|
||
'category', 'category_display', 'quantity', 'description',
|
||
'status', 'status_display', 'exported', 'exported_at',
|
||
'excel_file', 'excel_file_url', 'sent_to_production',
|
||
'production_date', 'published', 'published_at',
|
||
'created_at', 'updated_at',
|
||
'start_id', 'end_id'
|
||
]
|
||
read_only_fields = [
|
||
'created_at', 'updated_at', 'exported_at', 'excel_file',
|
||
'excel_file_url', 'production_date', 'published_at', 'quantity',
|
||
'start_id', 'end_id'
|
||
]
|
||
|
||
def get_category_display(self, obj):
|
||
return dict(CardTemplate.CATEGORY_CHOICES).get(obj.category)
|
||
|
||
def get_template_name(self, obj):
|
||
return obj.template.name if obj.template else None
|
||
|
||
def get_status_display(self, obj):
|
||
return obj.get_status_display()
|
||
|
||
def get_excel_file_url(self, obj):
|
||
if obj.excel_file:
|
||
return obj.excel_file.url
|
||
return None
|
||
|
||
|
||
class CardUsageLogSerializer(serializers.ModelSerializer):
|
||
action_display = serializers.SerializerMethodField()
|
||
user_display = serializers.SerializerMethodField()
|
||
|
||
class Meta:
|
||
model = CardUsageLog
|
||
fields = [
|
||
'id', 'card', 'user', 'user_display', 'action', 'action_display',
|
||
'details', 'old_status', 'new_status', 'created_at', 'ip_address'
|
||
]
|
||
read_only_fields = fields
|
||
|
||
def get_action_display(self, obj):
|
||
return obj.get_action_display() if hasattr(obj, 'get_action_display') else obj.action
|
||
|
||
def get_user_display(self, obj):
|
||
if obj.user:
|
||
return obj.user.username
|
||
return None
|
||
|
||
|
||
class CardScanSerializer(serializers.Serializer):
|
||
"""Serializer for scanning a card with NFC"""
|
||
unique_id = serializers.CharField(max_length=100)
|
||
|
||
def validate(self, data):
|
||
"""
|
||
Check that at least one identifier is provided.
|
||
"""
|
||
if not any([data.get('unique_id')]):
|
||
raise serializers.ValidationError("unique_id must be provided.")
|
||
return data
|
||
|
||
|
||
class CardUseSerializer(serializers.Serializer):
|
||
"""Serializer for using a card"""
|
||
unique_id = serializers.CharField(max_length=100)
|
||
|
||
|
||
class CardBatchGenerateSerializer(serializers.Serializer):
|
||
"""Serializer for generating card batches"""
|
||
template = serializers.PrimaryKeyRelatedField(queryset=CardTemplate.objects.all())
|
||
quantity = serializers.IntegerField(min_value=1, max_value=10000)
|
||
description = serializers.CharField(required=False, allow_blank=True)
|
||
|
||
|
||
class CardTemplatePublishSerializer(serializers.Serializer):
|
||
"""Serializer for publishing a card template"""
|
||
template_id = serializers.IntegerField()
|
||
|
||
|
||
class CardBatchPublishSerializer(serializers.Serializer):
|
||
"""Serializer for publishing a card batch"""
|
||
batch_id = serializers.IntegerField()
|
||
|
||
|
||
class CardBatchManufactureSerializer(serializers.Serializer):
|
||
"""Serializer for marking a card batch as manufactured"""
|
||
batch_id = serializers.IntegerField()
|
||
|
||
|
||
# 类别特定的属性序列化器
|
||
class CategoryTemplateSerializer(CardTemplateDetailSerializer):
|
||
"""Base class for category-specific template serializers"""
|
||
|
||
def to_representation(self, instance):
|
||
representation = super().to_representation(instance)
|
||
|
||
# Add category-specific attributes if they exist
|
||
try:
|
||
if instance.category == 'clothing' and hasattr(instance, 'clothing_attrs'):
|
||
representation['attributes'] = ClothingAttributesSerializer(instance.clothing_attrs).data
|
||
elif instance.category == 'prop' and hasattr(instance, 'prop_attrs'):
|
||
representation['attributes'] = PropAttributesSerializer(instance.prop_attrs).data
|
||
elif instance.category == 'song' and hasattr(instance, 'song_attrs'):
|
||
representation['attributes'] = SongAttributesSerializer(instance.song_attrs).data
|
||
elif instance.category == 'dance' and hasattr(instance, 'dance_attrs'):
|
||
representation['attributes'] = DanceAttributesSerializer(instance.dance_attrs).data
|
||
elif instance.category == 'decoration' and hasattr(instance, 'decoration_attrs'):
|
||
representation['attributes'] = DecorationAttributesSerializer(instance.decoration_attrs).data
|
||
except Exception as e:
|
||
# Handle the case where related attributes don't exist
|
||
representation['attributes'] = None
|
||
|
||
return representation
|
||
|
||
|
||
class MobileProductSerializer(serializers.ModelSerializer):
|
||
"""手机端通用产品序列化器 - 适用于所有分类"""
|
||
category_display = serializers.SerializerMethodField()
|
||
card_type_display = serializers.SerializerMethodField()
|
||
rarity_display = serializers.SerializerMethodField()
|
||
status_display = serializers.SerializerMethodField()
|
||
image_url = serializers.URLField(source='image', read_only=True)
|
||
active_cards_count = serializers.SerializerMethodField()
|
||
attributes = serializers.SerializerMethodField()
|
||
|
||
class Meta:
|
||
model = CardTemplate
|
||
fields = [
|
||
'id', 'name', 'description',
|
||
'category', 'category_display',
|
||
'card_type', 'card_type_display',
|
||
'rarity', 'rarity_display',
|
||
'image_url',
|
||
'status', 'status_display',
|
||
'published_at',
|
||
'active_cards_count',
|
||
'attributes',
|
||
]
|
||
|
||
def get_category_display(self, obj):
|
||
return obj.get_category_display()
|
||
|
||
def get_card_type_display(self, obj):
|
||
return obj.get_card_type_display()
|
||
|
||
def get_rarity_display(self, obj):
|
||
return obj.get_rarity_display()
|
||
|
||
def get_status_display(self, obj):
|
||
return obj.get_status_display()
|
||
|
||
def get_active_cards_count(self, obj):
|
||
return obj.cards.filter(status='active').count()
|
||
|
||
def get_attributes(self, obj):
|
||
try:
|
||
attr_map = {
|
||
'clothing': ('clothing_attrs', ClothingAttributesSerializer),
|
||
'prop': ('prop_attrs', PropAttributesSerializer),
|
||
'song': ('song_attrs', SongAttributesSerializer),
|
||
'dance': ('dance_attrs', DanceAttributesSerializer),
|
||
'decoration': ('decoration_attrs', DecorationAttributesSerializer),
|
||
}
|
||
if obj.category in attr_map:
|
||
attr_name, serializer_cls = attr_map[obj.category]
|
||
if hasattr(obj, attr_name):
|
||
return serializer_cls(getattr(obj, attr_name)).data
|
||
except Exception:
|
||
pass
|
||
return None
|
||
|
||
|
||
|
||
# 带有专有属性的卡牌详情序列化器
|
||
class CategoryCardDetailSerializer(CardDetailSerializer):
|
||
"""Card detail serializer with category-specific attributes"""
|
||
|
||
def to_representation(self, instance):
|
||
representation = super().to_representation(instance)
|
||
|
||
# If card has a template, get its specific attributes
|
||
if instance.template:
|
||
template = instance.template
|
||
try:
|
||
# Add category-specific attributes if they exist
|
||
if template.category == 'clothing' and hasattr(template, 'clothing_attrs'):
|
||
representation['attributes'] = ClothingAttributesSerializer(template.clothing_attrs).data
|
||
elif template.category == 'prop' and hasattr(template, 'prop_attrs'):
|
||
representation['attributes'] = PropAttributesSerializer(template.prop_attrs).data
|
||
elif template.category == 'song' and hasattr(template, 'song_attrs'):
|
||
representation['attributes'] = SongAttributesSerializer(template.song_attrs).data
|
||
elif template.category == 'dance' and hasattr(template, 'dance_attrs'):
|
||
representation['attributes'] = DanceAttributesSerializer(template.dance_attrs).data
|
||
elif template.category == 'decoration' and hasattr(template, 'decoration_attrs'):
|
||
representation['attributes'] = DecorationAttributesSerializer(template.decoration_attrs).data
|
||
except Exception as e:
|
||
# Handle the case where related attributes don't exist
|
||
representation['attributes'] = None
|
||
|
||
return representation |