toolshed/backend/toolshed/serializers.py
2026-08-22 23:29:02 +02:00

271 lines
9.7 KiB
Python

from rest_framework import serializers
from authentication.models import KnownIdentity, ToolshedUser, FriendRequestIncoming, Group, GroupInviteIncoming
from authentication.serializers import OwnerSerializer, GroupOwnerSerializer
from files.models import File
from files.serializers import FileSerializer
from toolshed.models import Category, Property, ItemProperty, InventoryItem, Tag, StorageLocation, WorkflowInstance
def parse_handle(handle):
"""Parse a fully qualified handle like 'git:base#property:length' into (origin, entity_type, name)"""
if '#' not in handle:
# Fallback to old format (just name)
return None, None, handle
origin, rest = handle.split('#', 1)
if ':' not in rest:
return origin, None, rest
entity_type, name = rest.split(':', 1)
return origin, entity_type, name
def resolve_category_handle(handle):
"""Resolve a fully qualified handle to a Category object"""
origin, entity_type, name = parse_handle(handle)
if origin and entity_type == 'category':
return Category.objects.get(origin=origin, name=name)
# Fallback to name-only lookup
return Category.objects.get(name=handle.split('/')[-1])
def resolve_property_handle(handle):
"""Resolve a fully qualified handle to a Property object"""
origin, entity_type, name = parse_handle(handle)
if origin and entity_type == 'property':
return Property.objects.get(origin=origin, name=name)
# Fallback to name-only lookup
return Property.objects.get(name=handle)
def resolve_tag_handle(handle):
"""Resolve a fully qualified handle to a Tag object"""
origin, entity_type, name = parse_handle(handle)
if origin and entity_type == 'tag':
return Tag.objects.get(origin=origin, name=name)
# Fallback to name-only lookup
return Tag.objects.get(name=handle)
class FriendSerializer(serializers.ModelSerializer):
username = serializers.SerializerMethodField()
class Meta:
model = KnownIdentity
fields = ['id', 'username', 'public_key']
def get_username(self, obj):
return obj.username + '@' + obj.domain
class FriendRequestSerializer(serializers.ModelSerializer):
befriender = serializers.SerializerMethodField()
class Meta:
model = FriendRequestIncoming
fields = ['befriender', 'befriender_public_key', 'secret', 'id']
def get_befriender(self, obj):
return obj.befriender_username + '@' + obj.befriender_domain
class GroupMemberSerializer(serializers.ModelSerializer):
username = serializers.SerializerMethodField()
class Meta:
model = KnownIdentity
fields = ['id', 'username', 'public_key']
def get_username(self, obj):
return obj.username + '@' + obj.domain
class GroupSerializer(serializers.ModelSerializer):
handle = serializers.SerializerMethodField()
members = GroupMemberSerializer(many=True, read_only=True)
class Meta:
model = Group
fields = ['id', 'name', 'domain', 'handle', 'members']
read_only_fields = ['domain', 'handle', 'members']
def get_handle(self, obj):
return str(obj)
class GroupIdMapSerializer(serializers.ModelSerializer):
handle = serializers.SerializerMethodField()
class Meta:
model = Group
fields = ['id', 'name', 'domain', 'handle']
def get_handle(self, obj):
return str(obj)
class GroupInviteIncomingSerializer(serializers.ModelSerializer):
group = serializers.SerializerMethodField()
inviter = serializers.SerializerMethodField()
class Meta:
model = GroupInviteIncoming
fields = ['id', 'group', 'inviter', 'secret']
def get_group(self, obj):
return f"#{obj.group_name}@{obj.group_domain}"
def get_inviter(self, obj):
return obj.inviter_username + '@' + obj.inviter_domain
class PropertySerializer(serializers.ModelSerializer):
category = serializers.SerializerMethodField()
handle = serializers.SerializerMethodField()
def get_category(self, obj):
return resolve_category_handle(obj.category.get_handle()) if obj.category else None
def get_handle(self, obj):
return obj.get_handle()
class Meta:
model = Property
fields = ['name', 'description', 'category', 'unit_symbol', 'unit_name', 'unit_name_plural', 'base2_prefix', 'handle']
class CategorySerializer(serializers.ModelSerializer):
handle = serializers.SerializerMethodField()
class Meta:
model = Category
fields = ['name', 'handle']
def get_handle(self, obj):
return obj.get_handle()
def to_representation(self, instance):
return instance.name
def to_internal_value(self, data):
return resolve_category_handle(data.split("/")[-1])
class StorageLocationSerializer(serializers.ModelSerializer):
owner = OwnerSerializer(read_only=True)
category = serializers.CharField(required=False, allow_null=True, allow_blank=True)
path = serializers.SerializerMethodField()
class Meta:
model = StorageLocation
fields = ['id', 'name', 'description', 'path', 'category', 'owner', 'parent']
read_only_fields = ['path']
@staticmethod
def get_path(obj):
if obj.parent:
return StorageLocationSerializer.get_path(obj.parent) + "/" + obj.name
return obj.name
class ItemPropertySerializer(serializers.ModelSerializer):
property = PropertySerializer(read_only=True)
handle = serializers.SerializerMethodField()
class Meta:
model = ItemProperty
fields = ['property', 'value', 'handle']
def get_handle(self, obj):
return obj.property.get_handle()
def to_representation(self, instance):
return {'value': instance.value, 'name': instance.property.name}
def to_internal_value(self, data):
prop = resolve_property_handle(data.get('name') or data.get('handle'))
value = data['value']
return {'property': prop, 'value': value}
class InventoryItemSerializer(serializers.ModelSerializer):
owner = OwnerSerializer(read_only=True)
owner_group = GroupOwnerSerializer(read_only=True)
tags = serializers.SerializerMethodField()
tags_input = serializers.ListField(child=serializers.CharField(), write_only=True, required=False)
properties = ItemPropertySerializer(many=True, required=False, source='itemproperty_set')
category = CategorySerializer(required=False, allow_null=True)
files = FileSerializer(many=True, read_only=True)
class Meta:
model = InventoryItem
fields = ['id', 'name', 'description', 'owner', 'owner_group', 'category', 'availability_policy',
'owned_quantity', 'tags', 'tags_input', 'properties', 'files', 'storage_location']
def get_tags(self, obj):
return [tag.name for tag in obj.tags.all()]
def to_internal_value(self, data):
files = data.pop('files', [])
tags_input = data.pop('tags_input', data.pop('tags', []))
ret = super().to_internal_value(data)
ret['tags'] = [resolve_tag_handle(tag) for tag in tags_input]
ret['files'] = files
return ret
def create(self, validated_data):
tags = validated_data.pop('tags', [])
props = validated_data.pop('itemproperty_set', [])
files = validated_data.pop('files', [])
item = InventoryItem.objects.create(**validated_data)
for tag in tags:
item.tags.add(tag, through_defaults={})
for prop in props:
ItemProperty.objects.create(inventory_item=item, property=prop['property'], value=prop['value'])
for file in files:
if type(file) == dict:
file_serializer = FileSerializer(data=file)
if file_serializer.is_valid():
file_serializer.save()
item.files.add(file_serializer.instance)
else:
raise serializers.ValidationError(file_serializer.errors)
elif type(file) == int:
if File.objects.filter(id=file).exists():
item.files.add(File.objects.get(id=file))
else:
raise serializers.ValidationError("File with id {} does not exist".format(file))
item.save()
return item
def update(self, instance, validated_data):
tags = validated_data.pop('tags', [])
props = validated_data.pop('itemproperty_set', [])
item = super().update(instance, validated_data)
item.tags.clear()
item.properties.clear()
if 'category' not in validated_data:
item.category = None
for tag in tags:
item.tags.add(tag)
for prop in props:
ItemProperty.objects.create(inventory_item=item, property=prop['property'], value=prop['value'])
item.save()
return item
class WorkflowInstanceSerializer(serializers.ModelSerializer):
owner = serializers.StringRelatedField(read_only=True)
# Hash is enough to identify a staged file (the client computes the same SHA-256 the backend
# does, and can fetch bytes from a hash-derived storage path) - for anything staged by *this*
# session there's nothing more to say, and for a file staged elsewhere (another device/tab),
# hash is what lets this session recognize and fetch it. Unlike InventoryItemSerializer.files,
# no fuller FileSerializer representation is needed here.
staged_files = serializers.SerializerMethodField()
class Meta:
model = WorkflowInstance
fields = ['id', 'slug', 'state', 'payload', 'owner', 'staged_files', 'created_at', 'updated_at']
read_only_fields = ['owner', 'staged_files', 'created_at', 'updated_at']
def get_staged_files(self, obj):
return list(obj.staged_files.values_list('hash', flat=True))