Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • hansegucker/AlekSIS-Core
  • pinguin/AlekSIS-Core
  • AlekSIS/official/AlekSIS-Core
  • sunweaver/AlekSIS-Core
  • sggua/AlekSIS-Core
  • edward/AlekSIS-Core
  • magicfelix/AlekSIS-Core
7 results
Show changes
Commits on Source (82)
Showing
with 1210 additions and 941 deletions
Changelog
=========
`2.0a2`_
--------
New features
~~~~~~~~~~~~
* Add frontend-ased announcement management
* Auto-create Person on User creation
* Generel LDAP import/export interface
* Select primary group if unset
* Support multiple recipient object for one announcement
* Mich-Seite
* Add support for defining group types
* Add description to Person
* Add age_at method and age property
* Synchronise AlekSIS groups with Django groups
* Add celery worker celery-beat worker and celery broker to docker-compose.yml
* Re-add django-dbbackup
* Global search
* Add license information page
* Roles and permissions
* User preferences
* Additional fields for people per group
* Support global permission flags by LDAP group
* Remove legacy multi-tenant support
* Generalise announcements
* Allow custom menu entries (e.g. in footer)
* New logo for AlekSIS
* Two factor authentication with Yubikey or OTP
Minor changes
~~~~~~~~~~~~~
* Add some CSS helper classes for colours
* Show announcements on dashboard
* Mandate use of AlekSIS base model
* Make short_name for group optional
* Drop import_ref field(s)
* Generalised live loading of widgets for dashboard
* Turn into installable web app
Bug fixes
~~~~~~~~~
* DateTimeField Announcement.valid_from received a naive datetime
* Enable SASS processor in production
* Fix too short fields
* Load select2 externally
`2.0a1`_
--------
......@@ -79,3 +127,4 @@ _`1.0a1`: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/1.0a1
_`1.0a2`: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/1.0a2
_`1.0a4`: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/1.0a4
_`2.0a1`: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0a1
_`2.0a2`: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0a2
import pkg_resources
from django.utils.translation import gettext_lazy as _
try:
from .celery import app as celery_app
except ModuleNotFoundError:
......
......@@ -4,22 +4,17 @@ from reversion.admin import VersionAdmin
from .mixins import BaseModelAdmin
from .models import (
Group,
Person,
School,
SchoolTerm,
Activity,
Notification,
Announcement,
AnnouncementRecipient,
CustomMenuItem,
Group,
Notification,
Person,
)
admin.site.register(Person, VersionAdmin)
admin.site.register(Group, VersionAdmin)
admin.site.register(School, VersionAdmin)
admin.site.register(SchoolTerm, VersionAdmin)
admin.site.register(Activity, VersionAdmin)
admin.site.register(Notification, VersionAdmin)
admin.site.register(CustomMenuItem, VersionAdmin)
......
from typing import Any, List, Optional, Tuple
import django.apps
from django.contrib.auth.signals import user_logged_in
from django.http import HttpRequest
from django.shortcuts import get_user_model
from .signals import clean_scss
from dynamic_preferences.registries import preference_models
from .registries import (
group_preferences_registry,
person_preferences_registry,
site_preferences_registry,
)
from .util.apps import AppConfig
from .util.core_helpers import has_person
from .util.sass_helpers import clean_scss
User = get_user_model()
class CoreConfig(AppConfig):
......@@ -23,12 +33,40 @@ class CoreConfig(AppConfig):
([2018, 2019, 2020], "Julian Leucker", "leuckeju@katharineum.de"),
([2018, 2019, 2020], "Hangzhi Yu", "yuha@katharineum.de"),
([2019, 2020], "Dominik George", "dominik.george@teckids.org"),
([2019, 2020], "mirabilos", "thorsten.glaser@teckids.org"),
([2019, 2020], "Tom Teichler", "tom.teichler@teckids.org"),
([2019], "mirabilos", "thorsten.glaser@teckids.org"),
)
def config_updated(self, *args, **kwargs) -> None:
clean_scss()
def ready(self):
super().ready()
SitePreferenceModel = self.get_model("SitePreferenceModel")
PersonPreferenceModel = self.get_model("PersonPreferenceModel")
GroupPreferenceModel = self.get_model("GroupPreferenceModel")
preference_models.register(SitePreferenceModel, site_preferences_registry)
preference_models.register(PersonPreferenceModel, person_preferences_registry)
preference_models.register(GroupPreferenceModel, group_preferences_registry)
def preference_updated(
self,
sender: Any,
section: Optional[str] = None,
name: Optional[str] = None,
old_value: Optional[Any] = None,
new_value: Optional[Any] = None,
**kwargs,
) -> None:
if section == "theme":
if name in ("primary", "secondary"):
clean_scss()
elif name in ("favicon", "pwa_icon"):
from favicon.models import Favicon # noqa
Favicon.on_site.update_or_create(
title=name,
defaults={"isFavicon": name == "favicon", "faviconImage": new_value,},
)
def post_migrate(
self,
......@@ -42,7 +80,7 @@ class CoreConfig(AppConfig):
) -> None:
super().post_migrate(app_config, verbosity, interactive, using, plan, apps)
# Ensure presence of a OTP YubiKey default config
# Ensure presence of an OTP YubiKey default config
apps.get_model("otp_yubikey", "ValidationService").objects.using(using).update_or_create(
name="default", defaults={"use_ssl": True, "param_sl": "", "param_timeout": ""}
)
......
import os
from celery import Celery
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "aleksis.core.settings")
app = Celery("aleksis") # noqa
app.config_from_object('django.conf:settings', namespace='CELERY')
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()
......@@ -22,8 +22,12 @@ def check_app_configs_base_class(
if not isinstance(app_config, AppConfig):
results.append(
Warning(
"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.",
f"App config {app_config.name} does not derive"
"from aleksis.core.util.apps.AppConfig.",
hint=(
"Ensure the app uses the correct base class for all"
"registry functionality to work."
),
obj=app_config,
id="aleksis.core.W001",
)
......@@ -48,8 +52,13 @@ def check_app_models_base_class(
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.",
f"Model {model._meta.object_name} in app config {app_config.name} does"
"not derive from aleksis.core.mixins.ExtensibleModel.",
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",
)
......
from django_filters import FilterSet, CharFilter
from django_filters import CharFilter, FilterSet
from material import Layout, Row
......
from datetime import time, datetime
from typing import Optional
from datetime import datetime, time
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django_select2.forms import ModelSelect2MultipleWidget, Select2Widget
from material import Layout, Fieldset, Row
from dynamic_preferences.forms import PreferenceForm
from material import Fieldset, Layout, Row
from .mixins import ExtensibleForm
from .models import Group, Person, School, SchoolTerm, Announcement, AnnouncementRecipient
from .models import Announcement, Group, Person
from .registries import (
group_preferences_registry,
person_preferences_registry,
site_preferences_registry,
)
class PersonAccountForm(forms.ModelForm):
""" Form to assign user accounts to persons in the frontend :"""
class Meta:
model = Person
fields = ["last_name", "first_name", "user"]
......@@ -25,6 +30,8 @@ class PersonAccountForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Fields displayed only for informational purposes
self.fields["first_name"].disabled = True
self.fields["last_name"].disabled = True
......@@ -33,13 +40,16 @@ class PersonAccountForm(forms.ModelForm):
if self.cleaned_data.get("new_user", None):
if self.cleaned_data.get("user", None):
# The user selected both an existing user and provided a name to create a new one
self.add_error(
"new_user",
_("You cannot set a new username when also selecting an existing user."),
)
elif User.objects.filter(username=self.cleaned_data["new_user"]).exists():
# The user tried to create a new user with the name of an existing user
self.add_error("new_user", _("This username is already in use."))
else:
# Create new User object and assign to form field for existing user
new_user_obj = User.objects.create_user(
self.cleaned_data["new_user"],
self.instance.email,
......@@ -50,12 +60,15 @@ class PersonAccountForm(forms.ModelForm):
self.cleaned_data["user"] = new_user_obj
# Formset for batch-processing of assignments of users to persons
PersonsAccountsFormSet = forms.modelformset_factory(
Person, form=PersonAccountForm, max_num=0, extra=0
)
class EditPersonForm(ExtensibleForm):
""" Form to edit an existing person object in the frontend """
layout = Layout(
Fieldset(
_("Base data"),
......@@ -104,28 +117,13 @@ class EditPersonForm(ExtensibleForm):
)
def clean(self) -> None:
User = get_user_model()
if self.cleaned_data.get("new_user", None):
if self.cleaned_data.get("user", None):
self.add_error(
"new_user",
_("You cannot set a new username when also selecting an existing user."),
)
elif User.objects.filter(username=self.cleaned_data["new_user"]).exists():
self.add_error("new_user", _("This username is already in use."))
else:
new_user_obj = User.objects.create_user(
self.cleaned_data["new_user"],
self.instance.email,
first_name=self.instance.first_name,
last_name=self.instance.last_name,
)
self.cleaned_data["user"] = new_user_obj
# Use code implemented in dedicated form to verify user selection
return PersonAccountForm.clean(self)
class EditGroupForm(ExtensibleForm):
""" Form to edit an existing group in the frontend """
layout = Layout(
Fieldset(_("Common data"), "name", "short_name"),
Fieldset(_("Persons"), "members", "owners", "parent_groups"),
......@@ -155,26 +153,9 @@ class EditGroupForm(ExtensibleForm):
}
class EditSchoolForm(ExtensibleForm):
layout = Layout(
Fieldset(_("School name"), "name", "name_official"),
Fieldset(_("School logo"), Row("logo", "logo_cropping")),
)
class Meta:
model = School
fields = ["name", "name_official", "logo", "logo_cropping"]
class EditTermForm(ExtensibleForm):
layout = Layout("caption", Row("date_start", "date_end"))
class Meta:
model = SchoolTerm
fields = ["caption", "date_start", "date_end"]
class AnnouncementForm(ExtensibleForm):
""" Form to create or edit an announcement in the frontend """
valid_from = forms.DateTimeField(required=False)
valid_until = forms.DateTimeField(required=False)
......@@ -200,6 +181,7 @@ class AnnouncementForm(ExtensibleForm):
def __init__(self, *args, **kwargs):
if "instance" not in kwargs:
# Default to today and whole day for new announcements
kwargs["initial"] = {
"valid_from_date": datetime.now(),
"valid_from_time": time(0, 0),
......@@ -218,20 +200,17 @@ class AnnouncementForm(ExtensibleForm):
"groups": announcement.get_recipients_for_model(Group),
"persons": announcement.get_recipients_for_model(Person),
}
super().__init__(*args, **kwargs)
def clean(self):
data = super().clean()
# Check date and time
from_date = data["valid_from_date"]
from_time = data["valid_from_time"]
until_date = data["valid_until_date"]
until_time = data["valid_until_time"]
valid_from = datetime.combine(from_date, from_time)
valid_until = datetime.combine(until_date, until_time)
# Combine date and time fields into datetime objects
valid_from = datetime.combine(data["valid_from_date"], data["valid_from_time"])
valid_until = datetime.combine(data["valid_until_date"], data["valid_until_time"])
# Sanity check validity range
if valid_until < datetime.now():
raise ValidationError(
_("You are not allowed to create announcements which are only valid in the past.")
......@@ -241,37 +220,38 @@ class AnnouncementForm(ExtensibleForm):
_("The from date and time must be earlier then the until date and time.")
)
# Inject real time data if all went well
data["valid_from"] = valid_from
data["valid_until"] = valid_until
# Check recipients
# Ensure at least one group or one person is set as recipient
if "groups" not in data and "persons" not in data:
raise ValidationError(_("You need at least one recipient."))
recipients = []
recipients += data.get("groups", [])
recipients += data.get("persons", [])
data["recipients"] = recipients
# Unwrap all recipients into single user objects and generate final list
data["recipients"] = []
data["recipients"] += data.get("groups", [])
data["recipients"] += data.get("persons", [])
return data
def save(self, _=False):
# Save announcement
a = self.instance if self.instance is not None else Announcement()
a.valid_from = self.cleaned_data["valid_from"]
a.valid_until = self.cleaned_data["valid_until"]
a.title = self.cleaned_data["title"]
a.description = self.cleaned_data["description"]
a.save()
# Save announcement, respecting data injected in clean()
if self.instance is None:
self.instance = Announcement()
self.instance.valid_from = self.cleaned_data["valid_from"]
self.instance.valid_until = self.cleaned_data["valid_until"]
self.instance.title = self.cleaned_data["title"]
self.instance.description = self.cleaned_data["description"]
self.instance.save()
# Save recipients
a.recipients.all().delete()
self.instance.recipients.all().delete()
for recipient in self.cleaned_data["recipients"]:
a.recipients.create(recipient=recipient)
a.save()
self.instance.recipients.create(recipient=recipient)
self.instance.save()
return a
return self.instance
class Meta:
model = Announcement
......@@ -279,4 +259,24 @@ class AnnouncementForm(ExtensibleForm):
class ChildGroupsForm(forms.Form):
""" Inline form for group editing to select child groups """
child_groups = forms.ModelMultipleChoiceField(queryset=Group.objects.all())
class SitePreferenceForm(PreferenceForm):
""" Form to edit site preferences """
registry = site_preferences_registry
class PersonPreferenceForm(PreferenceForm):
""" Form to edit preferences valid for one person"""
registry = person_preferences_registry
class GroupPreferenceForm(PreferenceForm):
""" Form to edit preferences valid for members of a group"""
registry = group_preferences_registry
......@@ -38,13 +38,10 @@ MENUS = {
"validators": ["menu_generator.validators.is_authenticated"],
},
{
"name": _("Two factor auth"),
"name": _("2FA"),
"url": "two_factor:profile",
"icon": "phonelink_lock",
"validators": [
"menu_generator.validators.is_authenticated",
lambda request: "two_factor" in settings.INSTALLED_APPS,
],
"validators": ["menu_generator.validators.is_authenticated",],
},
{
"name": _("Me"),
......@@ -55,6 +52,15 @@ MENUS = {
"aleksis.core.util.core_helpers.has_person",
],
},
{
"name": _("Preferences"),
"url": "preferences_person",
"icon": "settings",
"validators": [
"menu_generator.validators.is_authenticated",
"aleksis.core.util.core_helpers.has_person",
],
},
],
},
{
......@@ -70,7 +76,10 @@ MENUS = {
"url": "announcements",
"icon": "announcement",
"validators": [
("aleksis.core.util.predicates.permission_validator", "core.view_announcements"),
(
"aleksis.core.util.predicates.permission_validator",
"core.view_announcements",
),
],
},
{
......@@ -86,7 +95,10 @@ MENUS = {
"url": "system_status",
"icon": "power_settings_new",
"validators": [
("aleksis.core.util.predicates.permission_validator", "core.view_system_status"),
(
"aleksis.core.util.predicates.permission_validator",
"core.view_system_status",
),
],
},
{
......@@ -98,20 +110,21 @@ MENUS = {
],
},
{
"name": _("Manage school"),
"url": "school_management",
"icon": "school",
"name": _("Configuration"),
"url": "preferences_site",
"icon": "settings",
"validators": [
("aleksis.core.util.predicates.permission_validator", "core.manage_school"),
(
"aleksis.core.util.predicates.permission_validator",
"core.change_site_preferences",
),
],
},
{
"name": _("Backend Admin"),
"url": "admin:index",
"icon": "settings",
"validators": [
"menu_generator.validators.is_superuser",
],
"validators": ["menu_generator.validators.is_superuser",],
},
],
},
......@@ -120,7 +133,9 @@ MENUS = {
"url": "#",
"icon": "people",
"root": True,
"validators": [("aleksis.core.util.predicates.permission_validator", "core.view_people_menu")],
"validators": [
("aleksis.core.util.predicates.permission_validator", "core.view_people_menu")
],
"submenu": [
{
"name": _("Persons"),
......@@ -143,8 +158,10 @@ MENUS = {
"url": "persons_accounts",
"icon": "person_add",
"validators": [
"menu_generator.validators.is_authenticated",
"menu_generator.validators.is_superuser",
(
"aleksis.core.util.predicates.permission_validator",
"core.link_persons_accounts",
)
],
},
{
......@@ -152,7 +169,10 @@ MENUS = {
"url": "groups_child_groups",
"icon": "group_add",
"validators": [
("aleksis.core.util.predicates.permission_validator", "core.assign_child_groups_to_groups")
(
"aleksis.core.util.predicates.permission_validator",
"core.assign_child_groups_to_groups",
)
],
},
],
......@@ -163,12 +183,11 @@ MENUS = {
"name": _("Assign child groups to groups"),
"url": "groups_child_groups",
"validators": [
("aleksis.core.util.predicates.permission_validator", "core.assign_child_groups_to_groups")
(
"aleksis.core.util.predicates.permission_validator",
"core.assign_child_groups_to_groups",
)
],
},
],
"SCHOOL_MANAGEMENT_MENU": [
{"name": _("Edit school information"), "url": "edit_school_information", },
{"name": _("Edit school term"), "url": "edit_school_term", },
],
}
# Generated by Django 3.0.2 on 2020-01-03 19:18
import aleksis.core.mixins
from django.conf import settings
import django.contrib.postgres.fields.jsonb
from django.db import migrations, models
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import image_cropping.fields
import phonenumber_field.modelfields
import aleksis.core.mixins
class Migration(migrations.Migration):
initial = True
......
# Generated by Django 3.0.2 on 2020-01-03 19:19
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
......
# Generated by Django 3.0.2 on 2020-01-05 16:50
from django.db import migrations, models
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
......
# Generated by Django 3.0.2 on 2020-01-05 18:32
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
......
# Generated by Django 3.0.2 on 2020-01-29 16:45
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
......
# Generated by Django 3.0.2 on 2020-02-03 22:41
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
......
# Generated by Django 3.0.3 on 2020-02-10 14:22
import datetime
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
......
# 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
import aleksis.core.util.core_helpers
class Migration(migrations.Migration):
......
# Generated by Django 3.0.3 on 2020-03-11 18:43
import aleksis.core.models
import django.contrib.postgres.fields.jsonb
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
import aleksis.core.models
class Migration(migrations.Migration):
......