tetra-framework / tetra

Tetra - A full stack component framework for Django using Alpine.js
https://www.tetraframework.com
MIT License
538 stars 14 forks source link

Serialization/encoding data does not work with models #64

Open nerdoc opened 1 month ago

nerdoc commented 1 month ago

Maybe I am misunderstanding something. There are 2 classes in utils.py: TetraJSONEncoder and TetraJSONDecoder. In theory, the purpose is clear, and I already extended both to handle models:

        ...
        elif isinstance(o, models.Model):
            return {
                "__type": f"model.{o._meta.app_label}.{o.__class__.__name__}",
                "value": o.pk,

But no matter how hard I debug tetra, I NEVER get the TetraJSONEncoder.default() method to get called. It is never, ever called. In https://github.com/tetra-framework/tetra/blob/3864e452fb8fb3a84fc662a686cd434b32fa5f07/tetra/utils.py#L81 TetraJSONEncoder is used, but internally, only .encode() is called, and never .default().

Is there anything I am missing?

@samwillis have you got a clue here? Maybe I did not fully understand the serialization workflow of Tetra.

nerdoc commented 1 month ago

I am adding unittests for the TetraJSONEncoder/Decoders.

@samwillis, you wrote TetraJSONEncoder that is a subclass of json.JSONEncoder, but your docstring says it is based on DjangoJSONEncoder. You were copying some of the code there, and extending the return value with a __type hint within a dict, so you could decode it later easier. DjangoJSONEncoder always returns a str, TetraJSONEncoder a str or a dict[str, Any] - or everything the json.JSONEncoder supports (simple python objects).

Generally speaking, I tried to create a mermaid diagram - mainly to understand things better.

graph TD;

subgraph python[Plain Python objects]
int
str
wrongModel[Model]
end

subgraph special[Special objects]
Model
datetime
end

JSONDecoder --> wrongModel
wrongModel --> TypeError
int  --> JSONEncoder
str --> JSONEncoder
JSONEncoder --> JSON-object
Model --> TetraJSONEncoder
datetime --> TetraJSONEncoder
TetraJSONEncoder --> JSON-object

subgraph serialized[JSON object]
JSON-object
end

JSON-object --> JSONDecoder
JSON-object --> TetraJSONDecoder
JSONDecoder --> str 
JSONDecoder --> int 
TetraJSONDecoder --> Model
TetraJSONDecoder --> datetime

I already added model support to it:

        elif isinstance(obj, models.Model):
            return {
                "__type": f"model.{obj._meta.app_label}.{obj.__class__.__name__}",
                "value": obj.pk,
            }

After a few tests, I would want to merge two others that seem to be added in the meantime to Django:

        elif isinstance(obj, datetime.timedelta):
            return duration_iso_string(obj)
        elif isinstance(obj, (decimal.Decimal, uuid.UUID, Promise)):
            return str(obj)

The tests that I created showed that when I encode and decode a simple model, it works flawlessly.

But I have problems in a form that has a ForeignKey field despite the Js/Alpine data structure holding the correct index key of the model, it is not retrieved, and I can't see that the TetraJSONDecoder is ever called, when e.g. calling the "submit" method of the component.

I'm stuck here.

nerdoc commented 1 month ago

And on: The TetraJSONDecoder decodes the object when e.g. calling a method from the frontend/javascript: The TetraJSONDecoder.object_hook it receives is a dict like this:

{'key': None, 'account_type': 1, 'country': 'AT', 'federal_state': '1', 'title': None, 'company_name': None, 
'health_service_type': 'doc', 'medical_speciality': 'gp', 'phone_number': None, 'comment': None, 
'identity_proof': None, 'profession_proof': None, 'first_name': None, 'last_name': None, 'email': None, 
'terms_conditions': True}

this is a dict, and no special one with a __type - just normal dict, which AFAIK will get fed into the component's data directly.

Maybe then my way of encoding the data is completely wrong, see above in https://github.com/tetra-framework/tetra/issues/64#issue-2329613568.

Because the TetraJSONEncoder.default() never gets a model - but always a dict. And for simple str, int, dict objects, default() is never called.