toolshed/backend/toolshed/serializers.py
2026-08-26 22:18:55 +02:00

327 lines
12 KiB
Python

from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
from rest_framework import serializers
from authentication.models import KnownIdentity, ToolshedUser, FriendRequestIncoming, Group, GroupInviteIncoming, \
GroupMembership
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 GroupMembershipSerializer(serializers.ModelSerializer):
handle = serializers.SerializerMethodField()
class Meta:
model = GroupMembership
fields = ['id', 'group_name', 'group_domain', 'handle', 'created_at']
def get_handle(self, obj):
return f"#{obj.group_name}@{obj.group_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 OwnerScopedPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
"""Resolves/represents by the owner-scoped `id` rather than the model's internal pk, scoped to
the requesting user - StorageLocation.parent points at another StorageLocation, whose publicly
visible identity is now the owner-scoped id (see StorageLocation.create_for_owner), not
internal_id."""
def use_pk_only_optimization(self):
# False: to_representation needs the owner-scoped `id`, not just the internal pk that the
# PKOnlyObject optimization would otherwise limit us to.
return False
def get_queryset(self):
queryset = super().get_queryset()
request = self.context.get('request')
if request is None or type(request.user) != KnownIdentity:
return queryset.none()
identity = request.user
owner_filter = Q(owner_group__in=identity.member_of_groups.all())
if identity.user.exists():
owner_filter |= Q(owner=identity.user.get())
return queryset.filter(owner_filter)
def to_internal_value(self, data):
queryset = self.get_queryset()
try:
if isinstance(data, bool):
raise TypeError
return queryset.get(id=data)
except ObjectDoesNotExist:
self.fail('does_not_exist', pk_value=data)
except (TypeError, ValueError):
self.fail('incorrect_type', data_type=type(data).__name__)
def to_representation(self, value):
return value.id
class StorageLocationSerializer(serializers.ModelSerializer):
owner = OwnerSerializer(read_only=True)
owner_group = GroupOwnerSerializer(read_only=True)
category = serializers.CharField(required=False, allow_null=True, allow_blank=True)
parent = OwnerScopedPrimaryKeyRelatedField(queryset=StorageLocation.objects.all(), required=False,
allow_null=True)
path = serializers.SerializerMethodField()
class Meta:
model = StorageLocation
fields = ['id', 'name', 'description', 'path', 'category', 'owner', 'owner_group', 'parent']
read_only_fields = ['id', 'path']
@staticmethod
def get_path(obj):
if obj.parent:
return StorageLocationSerializer.get_path(obj.parent) + "/" + obj.name
return obj.name
def create(self, validated_data):
return StorageLocation.create_for_owner(**validated_data)
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)
storage_location = OwnerScopedPrimaryKeyRelatedField(queryset=StorageLocation.objects.all(), required=False,
allow_null=True)
class Meta:
model = InventoryItem
fields = ['id', 'name', 'description', 'owner', 'owner_group', 'category', 'availability_policy',
'owned_quantity', 'tags', 'tags_input', 'properties', 'files', 'storage_location']
read_only_fields = ['id']
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.create_for_owner(**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)
# Only the hash is needed to identify a staged file, unlike InventoryItemSerializer.files. See docs/implementation.md#staged-files-are-identified-by-hash-alone.
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))