.. _semantic-models: Semantic models =============== The standard way in Django to define models is something like this: .. code-block:: python class User(Model): is_active = BooleanField() name = CharField() person_number = CharField() manager = ForeignKey('self', on_delete=CASCADE) roles = ManyToManyField(Role, blank=True) The issue with that is that the semantic meaning of each field is hidden behind the name, and not in the type. The `name` and `person_number` fields have the same type but should be handled differently. Since iommi shortcut registrations are based on the type, you can't customize the parsing or rendering of the `person_number` or `birth_place` fields on the project level via the :ref:`style`. Moreover in this example the type information alone is not enough for other customization. For `ForeignKey`, by default in iommi you'll get a select2 drop-down to select from all items in that table. This is not good UX for a location, and it's probably not good UX for a manager field either as that should most likely exclude non-active users, and/or limited to users with a certain role. For `CharField`, the default is to present a text field, but in the above model we want a Swedish "person number", which has a specific storage format, can accept a variety of input formats that can be unambiguously parsed, and even has a checksum that can be used to validate that the user input is correct. A solution to this is to create an additional specialized type to specify semantic model fields: .. code-block:: python class PersonNumberField(CharField): pass This field can then be registered in iommi: .. code-block:: python register_factory(PersonNumberField, shortcut_name='person_number') Then change the model to use `PersonNumberField`: .. code-block:: python class User(Model): is_active = BooleanField() name = CharField() person_number = PersonNumberField() manager = ForeignKey('self', on_delete=CASCADE) For foreign key, and many-to-many fields it would be cumbersome to make custom classes, so registrations are done slightly differently: .. code-block:: python # foreign key and one-to-one # related, related_multiple register_related_factory(User, shortcut_name='user') # many-to-many and one-to-many (aka reverse foreign key) register_related_multiple_factory(Role, shortcut_name='roles') You will then need to add shortcuts for these in your subclasses of `Column`, `Field`, and `Filter`. .. code-block:: python class Field(iommi.Field): @classmethod @with_defaults( parse=person_number__parse, ) def person_number(cls, **kwargs): return cls.text(**kwargs) @classmethod @with_defaults( choices=lambda **_: User.objects.filter(is_active=True), ) def user(cls, **kwargs): return cls.foreign_key(**kwargs) @classmethod @with_defaults() def roles(cls, **kwargs): return cls.many_to_many(**kwargs) ...and similar for `Field` and `Filter`. You can also add configuration via the `Style` machinery. This is useful for reusable apps. Semantic models requires a little bit more initial setup, but for commonly used field types, it will make new views correct by default and super easy to setup. .. warning:: The `register_related_factory`/`register_related_multiple_factory` registrations are global and keyed on the model type, but the custom shortcut they point at (`user`, `roles`, ...) only exists on *your* subclasses of `Column`/`Field`/`Filter`. Any code that builds parts from the model using the *base* iommi classes will look up the shortcut on a class that doesn't have it and fail with `AttributeError: type object '...' has no attribute ''`. The most common place this bites is the built-in :doc:`admin`, which uses the base `Table`/`Form` (and therefore base `Column`/`Field`/`Filter`) by default. To make the admin (or anything else that auto-generates from your models) use your shortcuts, point it at your subclasses:: from iommi.admin import Admin as BaseAdmin class Admin(BaseAdmin): class Meta: # so the list view's columns/filters/bulk-form find `.user()`, `.roles()`, ... table_class = MyEditTable # so the create/edit/delete forms find `.user()`, `.roles()`, ... form_class = MyForm Note that both `table_class` and `form_class` are needed: `table_class` covers the list view, while `form_class` covers the create/edit/delete views. The same applies anywhere the auto machinery runs against a model with one of these registered relations -- make sure the `Table`/`Form`/`Query` involved use your subclasses, not the base iommi ones.