Skip to content
Snippets Groups Projects
Commit 79cb242b authored by Jonathan Weth's avatar Jonathan Weth :keyboard:
Browse files

Merge branch 'feature/extensible-models' into 'master'

Let model extensions define new fields

See merge request AlekSIS/AlekSIS!129
parents a8a684a7 9d866ef4
No related branches found
No related tags found
1 merge request!129Let model extensions define new fields
Pipeline #690 failed
......@@ -26,7 +26,7 @@ class Migration(migrations.Migration):
options={
'ordering': ['short_name', 'name'],
},
bases=(models.Model, aleksis.core.mixins.ExtensibleModel),
bases=(aleksis.core.mixins.ExtensibleModel,),
),
migrations.CreateModel(
name='School',
......@@ -98,7 +98,7 @@ class Migration(migrations.Migration):
options={
'ordering': ['last_name', 'first_name'],
},
bases=(models.Model, aleksis.core.mixins.ExtensibleModel),
bases=(aleksis.core.mixins.ExtensibleModel,),
),
migrations.AddField(
model_name='group',
......
# Generated by Django 3.0.2 on 2020-01-18 21:56
import django.contrib.postgres.fields.jsonb
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0007_create_admin_user'),
]
operations = [
migrations.AddField(
model_name='group',
name='extended_data',
field=django.contrib.postgres.fields.jsonb.JSONField(default=dict),
),
migrations.AddField(
model_name='person',
name='extended_data',
field=django.contrib.postgres.fields.jsonb.JSONField(default=dict),
),
]
......@@ -5,10 +5,12 @@ from django.db import models
from django.db.models import QuerySet
from easyaudit.models import CRUDEvent
from jsonstore.fields import JSONField, JSONFieldMixin
class ExtensibleModel(object):
""" Allow injection of code from AlekSIS apps to extend model functionality.
class ExtensibleModel(models.Model):
""" Allow injection of fields and code from AlekSIS apps to extend
model functionality.
After all apps have been loaded, the code in the `model_extensions` module
in every app is executed. All code that shall be injected into a model goes there.
......@@ -19,6 +21,8 @@ class ExtensibleModel(object):
from datetime import date, timedelta
from jsonstore import CharField
from aleksis.core.models import Person
@Person.property
......@@ -29,6 +33,8 @@ class ExtensibleModel(object):
def age(self) -> timedelta:
return self.date_of_birth - date.today()
Person.field(shirt_size=CharField())
For a more advanced example, using features from the ORM, see AlekSIS-App-Chronos
and AlekSIS-App-Alsijil.
......@@ -37,6 +43,8 @@ class ExtensibleModel(object):
- Dominik George <dominik.george@teckids.org>
"""
extended_data = JSONField(default=dict, editable=False)
@classmethod
def _safe_add(cls, obj: Any, name: Optional[str]) -> None:
# Decide the name for the attribute
......@@ -48,12 +56,12 @@ class ExtensibleModel(object):
else:
raise ValueError("%s is not a valid name." % name)
# Verify that property name does not clash with other names in the class
# Verify that attribute name does not clash with other names in the class
if hasattr(cls, prop_name):
raise ValueError("%s already used." % prop_name)
# Add function wrapped in property decorator if we got here
setattr(cls, prop_name, obj)
# Let Django's model magic add the attribute if we got here
cls.add_to_class(name, obj)
@classmethod
def property(cls, func: Callable[[], Any], name: Optional[str] = None) -> None:
......@@ -67,6 +75,32 @@ class ExtensibleModel(object):
cls._safe_add(func, func.__name__)
@classmethod
def field(cls, **kwargs) -> None:
""" Adds the passed jsonstore field. Must be one of the fields in
django-jsonstore.
Accepts exactly one keyword argument, with the name being the desired
model field name and the value the field instance.
"""
# Force kwargs to be exactly one argument
if len(kwargs) != 1:
raise TypeError("field() takes 1 keyword argument but %d were given" % len(kwargs))
name, field = kwargs.popitem()
# Force the field to be one of the jsonstore fields
if JSONFieldMixin not in field.__class__.__mro__:
raise TypeError("Only jsonstore fields can be added to models.")
# Force use of the one JSONField defined in this mixin
field.json_field_name = "extended_data"
cls._safe_add(field, name)
class Meta:
abstract = True
class CRUDMixin(models.Model):
class Meta:
......
......@@ -64,7 +64,7 @@ class SchoolTerm(models.Model):
verbose_name_plural = _("School terms")
class Person(models.Model, ExtensibleModel):
class Person(ExtensibleModel):
""" A model describing any person related to a school, including, but not
limited to, students, teachers and guardians (parents).
"""
......@@ -159,7 +159,7 @@ class Person(models.Model, ExtensibleModel):
return self.full_name
class Group(models.Model, ExtensibleModel):
class Group(ExtensibleModel):
"""Any kind of group of persons in a school, including, but not limited
classes, clubs, and the like.
"""
......
......@@ -62,6 +62,7 @@ Celery = {version="^4.4.0", optional=true, extras=["django", "redis"]}
django-celery-results = {version="^1.1.2", optional=true}
django-celery-beat = {version="^1.5.0", optional=true}
django-celery-email = {version="^3.0.0", optional=true}
django-jsonstore = "^0.4.1"
[tool.poetry.extras]
ldap = ["django-auth-ldap"]
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment