diff --git a/src/videoag_common/api_object/fields/relationship_fields.py b/src/videoag_common/api_object/fields/relationship_fields.py index 44034525e3c8434b494474336e464cba41b98231..a324c8b8fe33a96a50832b2c8f82286976c29fb9 100644 --- a/src/videoag_common/api_object/fields/relationship_fields.py +++ b/src/videoag_common/api_object/fields/relationship_fields.py @@ -48,21 +48,6 @@ class ApiAbstractRelationshipField(ApiAbstractColumnField[_O], Generic[_O], ABC) # Relationship might have extra join conditions, and then it is always possible to get null values. A constraint # is often not feasible. In this case we just return None instead of throwing an exception return True - - def _serialize_relationship(self, obj, args): - if not self.data_foreign_in_context: - return self._foreign_class.serialize_object_args(obj, args) - id = obj.id - context_name = f"{self._foreign_class.id}_context" - if not hasattr(args, context_name): - raise Exception(f"Missing serialization argument '{context_name}'") - context = getattr(args, context_name) - if not isinstance(context, dict): - raise Exception(f"serialization argument '{context_name}' must be a dict") - if str(id) not in context: - context[str(id)] = None # Indicate that object is being serialized. Prevent loops - context[str(id)] = self._foreign_class.serialize_object_args(obj, args) - return id class ApiMany2OneRelationshipField(ApiAbstractRelationshipField[_O], Generic[_O]): @@ -107,7 +92,7 @@ class ApiMany2OneRelationshipField(ApiAbstractRelationshipField[_O], Generic[_O] return "int" if self.data_foreign_in_context else self._foreign_class.id def _data_db_value_to_json(self, db_value, args) -> JsonTypes: - return self._serialize_relationship(db_value, args) + return self._foreign_class.serialize_object_args(db_value, args, to_context=self.data_foreign_in_context) def _config_db_value_to_json(self, db_value) -> JsonTypes: return db_value.id @@ -160,7 +145,7 @@ class ApiAny2ManyRelationshipField(ApiAbstractRelationshipField[_O], Generic[_O] def _data_db_value_to_json(self, db_value, args) -> JsonTypes: return list(map( - lambda obj: self._serialize_relationship(obj, args), + lambda obj: self._foreign_class.serialize_object_args(obj, args, to_context=self.data_foreign_in_context), db_value )) diff --git a/src/videoag_common/api_object/fields/special_fields.py b/src/videoag_common/api_object/fields/special_fields.py index 3853695ee9cbb10c664b63d1005de3a5278b4c91..b46d517e464af8cc65ac0e122d1686c7bff3bd66 100644 --- a/src/videoag_common/api_object/fields/special_fields.py +++ b/src/videoag_common/api_object/fields/special_fields.py @@ -11,14 +11,19 @@ _O = TypeVar("_O", bound="ApiObject") class ApiDataMethodField(ApiDataField[_O], Generic[_O]): # TODO get type id from function return type - def __init__(self, type_id: str, **kwargs): + def __init__(self, + type_id: str, + data_foreign_in_context: bool = False, + **kwargs): super().__init__(**kwargs) self._data_func: Callable[[_O, Any], JsonTypes] or None = None self._serializer = None - self._type_id: str = type_id + self._type_id = type_id + self._data_type_id = None + self._data_foreign_in_context = data_foreign_in_context def data_get_type_id(self) -> str: - return self._type_id + return self._data_type_id def post_init(self, context: FieldContext): super().post_init(context) @@ -27,7 +32,7 @@ class ApiDataMethodField(ApiDataField[_O], Generic[_O]): self._data_func = context.own_member from ..object import api_get_value_serializer_function - self._serializer = api_get_value_serializer_function(context.all_classes, self._type_id) + self._data_type_id, self._serializer = api_get_value_serializer_function(context.all_classes, self._type_id, self._data_foreign_in_context) def data_get_value(self, object_db: _O, args) -> JsonTypes: kwargs = {} diff --git a/src/videoag_common/api_object/object.py b/src/videoag_common/api_object/object.py index 4dcae98fac3f56c7c6c4ab0cb6906f0993ac630d..f392e21120546c902c61c573415f53bc1a5c14aa 100644 --- a/src/videoag_common/api_object/object.py +++ b/src/videoag_common/api_object/object.py @@ -139,13 +139,18 @@ class ApiObject: def serialize(self, **kwargs): return self.__api_class__.serialize(self, **kwargs) + + def serialize_to_context(self, **kwargs): + return self.__api_class__.serialize_to_context(self, **kwargs) ApiValueTypes = ApiObject or ApiValueObject or str or int or bool or None or list["ApiValueType"] -def api_get_value_serializer_function(all_classes: dict[str, "ApiObjectClass"], type_id: str) \ - -> Callable[[ApiValueTypes, ArgAttributeObject], JsonTypes]: +def api_get_value_serializer_function( + all_classes: dict[str, "ApiObjectClass"], + type_id: str, + foreign_in_context: bool) -> tuple[str, Callable[[ApiValueTypes, ArgAttributeObject], JsonTypes]]: full_type_id = type_id is_array = False if type_id.endswith("[]"): @@ -155,6 +160,9 @@ def api_get_value_serializer_function(all_classes: dict[str, "ApiObjectClass"], _disable_value_object_registration = True if type_id in API_VALUE_OBJECT_CLASSES_BY_ID: + if foreign_in_context: + raise ValueError("foreign_in_context is set but this is not an object with an id") + if type_id in all_classes: raise ValueError(f"Duplicate id '{type}' in API classes and value classes") value_class = API_VALUE_OBJECT_CLASSES_BY_ID[type_id] @@ -168,26 +176,32 @@ def api_get_value_serializer_function(all_classes: dict[str, "ApiObjectClass"], elif type_id in all_classes: object_class = all_classes[type_id] + if foreign_in_context: + type_id = "int" + def serializer(val: ApiValueTypes, args): if val is None: return val if not isinstance(val, object_class.orm_class): raise TypeError(f"Got object of type '{type(val)}' but it was previously declared to be a '{object_class.orm_class}'") - return object_class.serialize_object_args(val, args) + return object_class.serialize_object_args(val, args, to_context=foreign_in_context) elif type_id in ("string", "integer", "boolean"): + if foreign_in_context: + raise ValueError("foreign_in_context is set but this is not an object with an id") + def serializer(val: ApiValueTypes, args): return val else: raise ValueError(f"Unknown type '{full_type_id}' (For ApiValueObject make sure that the __api_id__ attribute is set)") if not is_array: - return serializer + return type_id, serializer def serializer_list(val_list: list[ApiValueTypes], args): if val_list is None: return val_list return list(map(lambda v: serializer(v, args), val_list)) - return serializer_list + return f"{type_id}[]", serializer_list class DeletableApiObject(ApiObject): diff --git a/src/videoag_common/api_object/object_class.py b/src/videoag_common/api_object/object_class.py index ec22348d56ac82b9a8e592ce429d466ea238f4d0..8c66e50ed86756dcd1cec7d0688f31b4264b7ac7 100644 --- a/src/videoag_common/api_object/object_class.py +++ b/src/videoag_common/api_object/object_class.py @@ -297,23 +297,41 @@ class ApiObjectClass: return field_list def serialize(self, obj, **kwargs) -> dict: + return self._serialize_catch_error(obj, to_context=False, **kwargs) + + def serialize_to_context(self, obj, **kwargs) -> int: + return self._serialize_catch_error(obj, to_context=True, **kwargs) + + def _serialize_catch_error(self, obj, to_context: bool, **kwargs) -> dict or int: if not self.enable_data: raise Exception("Serialization not enabled") try: - return self.serialize_object_args(obj, ArgAttributeObject(**kwargs)) + return self.serialize_object_args(obj, ArgAttributeObject(**kwargs), to_context) except AttributeError as e: if not str(e).startswith("'ArgAttributeObject' object has no attribute"): raise e - raise AttributeError(f"Missing a keyword argument for serialize() of object '{self.id}': " + str(e)) from e - - def serialize_object_args(self, obj, args) -> dict: + raise AttributeError(f"Missing a keyword argument for serialization of object '{self.id}': " + str(e)) from e + + def serialize_object_args(self, obj, args, to_context: bool = False) -> dict or int: if not self.enable_data: raise Exception("Serialization not enabled") - res = {} - for variant_id in self._get_current_variant_set(obj): - for field in self._fields_by_variant_by_data_id[variant_id].values(): - field.add_to_data(obj, res, args) - return res + if not to_context: + res = {} + for variant_id in self._get_current_variant_set(obj): + for field in self._fields_by_variant_by_data_id[variant_id].values(): + field.add_to_data(obj, res, args) + return res + + context_name = f"{self.id}_context" + if not hasattr(args, context_name): + raise Exception(f"Missing serialization argument '{context_name}'") + context = getattr(args, context_name) + if not isinstance(context, dict): + raise Exception(f"serialization argument '{context_name}' must be a dict") + if str(self.id) not in context: + context[str(self.id)] = None # Indicate that object is being serialized. Prevent loops + context[str(self.id)] = self.serialize_object_args(obj, args, to_context=False) + return self.id def get_creation_config(self) -> JsonTypes or None: if not self.enable_config: diff --git a/src/videoag_common/objects/course.py b/src/videoag_common/objects/course.py index 8523a2fc6d56ec6aea104f80fdc634e86ba330f4..bacda60b81b2a4ab65f746fc3b9a7b1293ffb33e 100644 --- a/src/videoag_common/objects/course.py +++ b/src/videoag_common/objects/course.py @@ -104,7 +104,6 @@ class Lecture(DeletableApiObject, VisibilityApiObject, ApiViewPermissionsObject, include_in_data=True ) ) - # TODO thumbnail url no_recording: Mapped[bool] = api_mapped( mapped_column(nullable=False, default=False), ApiBooleanField( diff --git a/src/videoag_common/objects/site.py b/src/videoag_common/objects/site.py index af906fc98d93831f064136021d2688563c47b26e..02bb717c107e3deb643f7ba893ff5d8dbcaf89a9 100644 --- a/src/videoag_common/objects/site.py +++ b/src/videoag_common/objects/site.py @@ -191,8 +191,8 @@ class CourseFeatured(Featured): @api_include_in_data( type_id="course", - data_id="course", - data_notes="May be null (deleted/not visible). Does not include lectures" + data_id="course_id", data_foreign_in_context=True, + data_notes="May be null (deleted/not visible). Does not include lectures", ) def _data_course(self, is_mod: bool): return self.course if is_mod else self.public_course