Skip to content
Snippets Groups Projects
Verified Commit 287393ce authored by Jonathan Weth's avatar Jonathan Weth :keyboard:
Browse files

Merge branch 'master' into feature/multiple-recipient-objects-announcement

# Conflicts:
#	aleksis/core/models.py
parents 7d1cf2d1 249022a3
No related branches found
No related tags found
1 merge request!166Allow multiple recipient objects for one announcement
......@@ -3,13 +3,14 @@ from typing import Optional
import django.apps
from django.core.checks import Tags, Warning, register
from .mixins import ExtensibleModel, PureDjangoModel
from .util.apps import AppConfig
@register(Tags.compatibility)
def check_app_configs_base_class(
app_configs: Optional[django.apps.registry.Apps] = None, **kwargs
) -> None:
) -> list:
""" Checks whether all apps derive from AlekSIS's base app config """
results = []
......@@ -17,11 +18,11 @@ def check_app_configs_base_class(
if app_configs is None:
app_configs = django.apps.apps.get_app_configs()
for app_config in app_configs:
if app_config.name.startswith("aleksis.apps.") and not isinstance(app_config, AppConfig):
for app_config in filter(lambda c: c.name.startswith("aleksis."), app_configs):
if not isinstance(app_config, AppConfig):
results.append(
Warning(
"App config %s does not derive from aleksis.core.util.apps.AppConfig.",
"App config %s does not derive from aleksis.core.util.apps.AppConfig." % app_config.name,
hint="Ensure the app uses the correct base class for all registry functionality to work.",
obj=app_config,
id="aleksis.core.W001",
......@@ -29,3 +30,29 @@ def check_app_configs_base_class(
)
return results
@register(Tags.compatibility)
def check_app_models_base_class(
app_configs: Optional[django.apps.registry.Apps] = None, **kwargs
) -> list:
""" Checks whether all app models derive from AlekSIS's base ExtensibleModel """
results = []
if app_configs is None:
app_configs = django.apps.apps.get_app_configs()
for app_config in filter(lambda c: c.name.startswith("aleksis."), app_configs):
for model in app_config.get_models():
if ExtensibleModel not in model.__mro__ and PureDjangoModel not in model.__mro__:
results.append(
Warning(
"Model %s in app config %s does not derive from aleksis.core.mixins.ExtensibleModel." % (model._meta.object_name, app_config.name),
hint="Ensure all models in AlekSIS use ExtensibleModel as base. If your deviation is intentional, you can add the PureDjangoModel mixin instead to silence this warning.",
obj=model,
id="aleksis.core.W002",
)
)
return results
......@@ -9,6 +9,12 @@ MENUS = {
"icon": "lock_open",
"validators": ["menu_generator.validators.is_anonymous"],
},
{
"name": _("Dashboard"),
"url": "index",
"icon": "home",
"validators": ["menu_generator.validators.is_authenticated"],
},
{
"name": _("Account"),
"url": "#",
......
# Generated by Django 3.0.3 on 2020-02-20 12:24
import aleksis.core.util.core_helpers
import django.contrib.postgres.fields.jsonb
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0012_announcement'),
]
operations = [
migrations.RemoveField(
model_name='activity',
name='created_at',
),
migrations.RemoveField(
model_name='notification',
name='created_at',
),
migrations.AddField(
model_name='activity',
name='extended_data',
field=django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False),
),
migrations.AddField(
model_name='announcement',
name='extended_data',
field=django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False),
),
migrations.AddField(
model_name='notification',
name='extended_data',
field=django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False),
),
]
from datetime import datetime
from typing import Any, Callable, Optional
from django.contrib.contenttypes.models import ContentType
......@@ -8,9 +9,34 @@ from easyaudit.models import CRUDEvent
from jsonstore.fields import JSONField, JSONFieldMixin
class ExtensibleModel(models.Model):
""" Allow injection of fields and code from AlekSIS apps to extend
model functionality.
class CRUDMixin(models.Model):
class Meta:
abstract = True
@property
def crud_events(self) -> QuerySet:
"""Get all CRUD events connected to this object from easyaudit."""
content_type = ContentType.objects.get_for_model(self)
return CRUDEvent.objects.filter(
object_id=self.pk, content_type=content_type
).select_related("user")
class ExtensibleModel(CRUDMixin):
""" Base model for all objects in AlekSIS apps
This base model ensures all objects in AlekSIS apps fulfill the
following properties:
* crud_events property to retrieve easyaudit's CRUD event log
* created_at and updated_at properties based n CRUD events
* Allow injection of fields and code from AlekSIS apps to extend
model functionality.
Injection of fields and code
============================
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.
......@@ -43,8 +69,48 @@ class ExtensibleModel(models.Model):
- Dominik George <dominik.george@teckids.org>
"""
@property
def crud_event_create(self) -> Optional[CRUDEvent]:
""" Return create event of this object """
return self.crud_events.filter(event_type=CRUDEvent.CREATE).latest("datetime")
@property
def crud_event_update(self) -> Optional[CRUDEvent]:
""" Return last event of this object """
return self.crud_events.latest("datetime")
@property
def created_at(self) -> Optional[datetime]:
""" Determine creation timestamp from CRUD log """
if self.crud_event_create:
return self.crud_event_create.datetime
@property
def updated_at(self) -> Optional[datetime]:
""" Determine last timestamp from CRUD log """
if self.crud_event_update:
return self.crud_event_update.datetime
extended_data = JSONField(default=dict, editable=False)
@property
def created_by(self) -> Optional[models.Model]:
""" Determine user who created this object from CRUD log """
if self.crud_event_create:
return self.crud_event_create.user
@property
def updated_by(self) -> Optional[models.Model]:
""" Determine user who last updated this object from CRUD log """
if self.crud_event_update:
return self.crud_event_update.user
extended_data = JSONField(default=dict, editable=False)
@classmethod
def _safe_add(cls, obj: Any, name: Optional[str]) -> None:
# Decide the name for the attribute
......@@ -101,17 +167,6 @@ class ExtensibleModel(models.Model):
class Meta:
abstract = True
class CRUDMixin(models.Model):
class Meta:
abstract = True
@property
def crud_events(self) -> QuerySet:
"""Get all CRUD events connected to this object from easyaudit."""
content_type = ContentType.objects.get_for_model(self)
return CRUDEvent.objects.filter(
object_id=self.pk, content_type=content_type
).select_related("user")
class PureDjangoModel(object):
""" No-op mixin to mark a model as deliberately not using ExtensibleModel """
pass
from datetime import date, datetime, timedelta
from datetime import date, datetime
from typing import Optional, Iterable, Union, Sequence, List
from django.contrib.auth import get_user_model
......@@ -14,7 +14,8 @@ from image_cropping import ImageCropField, ImageRatioField
from phonenumber_field.modelfields import PhoneNumberField
from polymorphic.models import PolymorphicModel
from .mixins import ExtensibleModel
from .mixins import ExtensibleModel, PureDjangoModel
from .util.core_helpers import now_tomorrow
from .util.notifications import send_notification
from constance import config
......@@ -236,7 +237,7 @@ class Group(ExtensibleModel):
return "%s (%s)" % (self.name, self.short_name)
class Activity(models.Model):
class Activity(ExtensibleModel):
user = models.ForeignKey("Person", on_delete=models.CASCADE, related_name="activities")
title = models.CharField(max_length=150, verbose_name=_("Title"))
......@@ -244,8 +245,6 @@ class Activity(models.Model):
app = models.CharField(max_length=100, verbose_name=_("Application"))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
def __str__(self):
return self.title
......@@ -254,7 +253,7 @@ class Activity(models.Model):
verbose_name_plural = _("Activities")
class Notification(models.Model):
class Notification(ExtensibleModel):
sender = models.CharField(max_length=100, verbose_name=_("Sender"))
recipient = models.ForeignKey("Person", on_delete=models.CASCADE, related_name="notifications")
......@@ -265,8 +264,6 @@ class Notification(models.Model):
read = models.BooleanField(default=False, verbose_name=_("Read"))
sent = models.BooleanField(default=False, verbose_name=_("Sent"))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
def __str__(self):
return self.title
......@@ -280,11 +277,7 @@ class Notification(models.Model):
verbose_name_plural = _("Notifications")
def now_plus_one_day():
return timezone.datetime.now() + timedelta(days=1)
class Announcement(models.Model):
class Announcement(ExtensibleModel):
title = models.CharField(max_length=150, verbose_name=_("Title"))
description = models.TextField(max_length=500, verbose_name=_("Description"), blank=True)
link = models.URLField(blank=True, verbose_name=_("Link"))
......@@ -294,7 +287,7 @@ class Announcement(models.Model):
)
valid_until = models.DateTimeField(
verbose_name=_("Date and time until when to show"),
default=now_plus_one_day,
default=now_tomorrow,
)
@classmethod
......@@ -373,7 +366,7 @@ class AnnouncementRecipient(models.Model):
verbose_name_plural = _("Announcement recipients")
class DashboardWidget(PolymorphicModel):
class DashboardWidget(PolymorphicModel, PureDjangoModel):
""" Base class for dashboard widgets on the index page
To implement a widget, add a model that subclasses DashboardWidget, sets the template
......
from datetime import datetime, timedelta
import os
import pkgutil
from importlib import import_module
......@@ -7,6 +8,7 @@ from uuid import uuid4
from django.conf import settings
from django.db.models import Model
from django.http import HttpRequest
from django.utils import timezone
from django.utils.functional import lazy
......@@ -148,3 +150,8 @@ def school_information_processor(request: HttpRequest) -> dict:
return {
"SCHOOL": School.get_default,
}
def now_tomorrow() -> datetime:
""" Return current time tomorrow """
return timezone.datetime.now() + timedelta(days=1)
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