Skip to content
Snippets Groups Projects
Commit 929d12b6 authored by Tom Teichler's avatar Tom Teichler :beers:
Browse files

Merge branch 'master' into group-stats

parents 0896cccf d6a0b2bd
No related branches found
No related tags found
1 merge request!229Add statistics about group to group view
Pipeline #2933 passed
Showing
with 2661 additions and 1406 deletions
......@@ -74,3 +74,8 @@ htmlcov/
maintenance_mode_state.txt
media/
package-lock.json
# VSCode
.vscode/
.history/
*.code-workspace
image: registry.edugit.org/teckids/team-sysadmin/docker-images/python-pimped:master
stages:
- test
- build
- deploy
variables:
GIT_SUBMODULE_STRATEGY: recursive
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
FF_NETWORK_PER_BUILD: "true"
cache:
key:
files:
- poetry.lock
- pyproject.toml
paths:
- .cache/pip
- .tox
test:
stage: test
services:
- name: selenium/standalone-firefox
alias: selenium
before_script:
- adduser --disabled-password --gecos "Test User" testuser
- chown -R testuser .
script:
- sudo apt update
- sudo apt install python3-ldap libldap2-dev libssl-dev libsasl2-dev python3.7-dev -y
- sudo -u testuser
env TEST_SELENIUM_HUB=http://selenium:4444/wd/hub
TEST_SELENIUM_BROWSERS=firefox
TEST_HOST=build
tox -e selenium -- --junitxml=.tox/junit.xml
artifacts:
paths:
- .tox/screenshots
reports:
junit: .tox/junit.xml
lint:
stage: test
script:
- tox -e lint,security
allow_failure: true
build_dist:
stage: build
script:
- tox -e build
artifacts:
paths:
- dist/
pages:
stage: deploy
before_script:
- cp -r .tox/screenshots/firefox docs/screenshots
script:
- export LC_ALL=en_GB.utf8
- tox -e docs -- BUILDDIR=../public/docs
artifacts:
paths:
- public/
only:
- master
build_docker:
stage: build
image:
name: gcr.io/kaniko-project/executor:v0.19.0
entrypoint: [""]
script:
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" >/kaniko/.docker/config.json
- /kaniko/executor
--context $CI_PROJECT_DIR
--dockerfile $CI_PROJECT_DIR/Dockerfile
--destination $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
--cache=true
--cleanup
- /kaniko/executor
--context $CI_PROJECT_DIR/docker/nginx
--dockerfile $CI_PROJECT_DIR/docker/nginx/Dockerfile
--destination $CI_REGISTRY_IMAGE/nginx:$CI_COMMIT_REF_NAME
--cache=true
--cleanup
only:
- master
- tags
deploy_demo-master:
stage: deploy
environment:
name: demo/master
url: http://demo-master.aleksis.org
before_script:
- 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- echo "$SSH_KNOWN_HOSTS" >~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
script:
- grep -v "build:" docker-compose.yml | ssh root@demo-master.aleksis.org
env ALEKSIS_IMAGE_TAG=${CI_COMMIT_REF_NAME}
docker-compose
-p aleksis-${CI_ENVIRONMENT_SLUG}
-f /dev/stdin
pull
- grep -v "build:" docker-compose.yml | ssh root@demo-master.aleksis.org
env ALEKSIS_IMAGE_TAG=${CI_COMMIT_REF_NAME}
NGINX_HTTP_PORT=80
ALEKSIS_maintenance__debug=true
docker-compose
-p aleksis-${CI_ENVIRONMENT_SLUG}
-f /dev/stdin
up -d
only:
- master
include:
- local: "/ci/general.yml"
- local: "/ci/test.yml"
- local: "/ci/build_dist.yml"
- local: "/ci/build_docker.yml"
- local: "/ci/deploy.yml"
......@@ -2,10 +2,6 @@
path = apps/official/AlekSIS-App-Chronos
url = https://edugit.org/AlekSIS/Official/AlekSIS-App-Chronos
ignore = untracked
[submodule "apps/official/AlekSIS-App-Exlibris"]
path = apps/official/AlekSIS-App-Exlibris
url = https://edugit.org/AlekSIS/Official/AlekSIS-App-Exlibris
ignore = untracked
[submodule "apps/official/AlekSIS-App-DashboardFeeds"]
path = apps/official/AlekSIS-App-DashboardFeeds
url = https://edugit.org/AlekSIS/Official/AlekSIS-App-DashboardFeeds
......@@ -18,3 +14,7 @@
path = apps/official/AlekSIS-App-Untis
url = https://edugit.org/AlekSIS/official/AlekSIS-App-Untis
ignore = untracked
[submodule "apps/official/AlekSIS-App-Hjelp"]
path = apps/official/AlekSIS-App-Hjelp
url = https://edugit.org/AlekSIS/official/AlekSIS-App-Hjelp
ignore = untracked
Changelog
=========
`2.0a2`_
--------
New features
~~~~~~~~~~~~
* Frontend-ased announcement management
* Auto-create Person on User creation
* Select primary group by pattern if unset
* Shortcut to personal information page
* Support for defining group types
* Add description to Person
* age_at method and age property to Person
* Synchronise AlekSIS groups with Django groups
* Add celery worker, celery-beat worker and celery broker to docker-compose setup
* Global search
* License information page
* Roles and permissions
* User preferences
* Additional fields for people per group
* Support global permission flags by LDAP group
* Persistent announcements
* Custom menu entries (e.g. in footer)
* New logo for AlekSIS
* Two factor authentication with Yubikey, OTP or SMS
* Devs: Add ExtensibleModel to allow apps to add fields, properties
* Devs: Support multiple recipient object for one announcement
Minor changes
~~~~~~~~~~~~~
* Make short_name for group optional
* Generalised live loading of widgets for dashboard
* Devs: Add some CSS helper classes for colours
* Devs: Mandate use of AlekSIS base model
* Devs: Drop import_ref field(s); apps shold now define their own reference fields
Bug fixes
~~~~~~~~~
* DateTimeField Announcement.valid_from received a naive datetime
* Enable SASS processor in production
* Fix too short fields
* Load select2 locally
`2.0a1`_
--------
......@@ -12,8 +56,8 @@ New features
* Dashboard
* Notifications via SMS (Twilio), Email or on the dashboard
* Admin interface
* Turn into installable, progressive web app
* Devs: Background Tasks with Celery
* Turn into installable PWA
Minor changes
~~~~~~~~~~~~~
......@@ -79,3 +123,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
......@@ -48,7 +48,7 @@ Working with the Git repository
The Git repository shall be used as a historic documentation of development
and as change management. It is important that the Git commit history
describes waht was changed, by whom and why.
describes what was changed, by whom and why.
Help and information on Git for beginners are available in the `Git guide`_
......
......@@ -4,31 +4,72 @@ AlekSIS — All-libre extensible kit for school information systems
Warning
-------
**This is a preview version of AlekSIS. Do not use with sensitive data. Especially, do not grant access to students yet.**
**This is an alpha version of AlekSIS, the free school information system.
The AlekSIS team is looking for schools who want to help shape the 2.0
final release and supports interested schools in operating AlekSIS.**
What AlekSIS is
----------------
AlekSIS is a web-based school information system (SIS) which can be used to
`AlekSIS`_ is a web-based school information system (SIS) which can be used to
manage and/or publish organisational subjects of educational institutions.
It was originally developed together with Städt. Leibniz-Gymnasium Remscheid
as a proprietary product. Five years after the school stole the original
code base, as a complete re-implementation as well-designed, free and open
source software, BiscuIT-ng was started. In the meantime, students from the
Katharineum in Lübeck implemented School-Apps with the same goals and tools.
In 2020, BiscuIT-ng and School-Apps were combined into AlekSIS.
Formerly two separate projects (BiscuIT and SchoolApps), developed by
`Teckids e.V.`_ and a team of students at `Katharineum zu Lübeck`_, they
were merged into the AlekSIS project in 2020.
AlekSIS is a platform based on Django, that provides central funstions
and data structures that can be used by apps that are developed and provided
seperately. The core can interact closely with the Debian Edu / Skolelinux
system.
seperately. The AlekSIS team also maintains a set of official apps which
make AlekSIS a fully-featured software solutions for the information
management needs of schools.
By design, the platform can be used by schools to write their own apps for
specific needs they face, also in coding classes. Students are empowered to
create real-world applications that bring direct value to their environment.
AlekSIS is part of the `schul-frei`_ project as a component in sustainable
educational networks.
Core features
--------------
TBA.
* For users:
* Custom menu entries (e.g. in footer)
* Global preferences
* Group types
* Manage announcements
* Manage groups
* Manage persons
* Notifications via SMS email or dashboard
* Rules and permissions for users, objects and pages
* Two factor authentication via Yubikey, OTP or SMS
* User preferences
* For admins
* Asynchronous tasks with celery
* Authentication via LDAP
* Automatic backup of database, static and media files
Official apps
-------------
+--------------------------------------+---------------------------------------------------------------------------------------------+
| App name | Purpose |
+======================================+=============================================================================================+
| `AlekSIS-App-Chronos`_ | The Chronos app provides functionality for digital timetables. |
+--------------------------------------+---------------------------------------------------------------------------------------------+
| `AlekSIS-App-DashboardFeeds`_ | The DashboardFeeds app provides functionality to add RSS or Atom feeds to dashboard |
+--------------------------------------+---------------------------------------------------------------------------------------------+
| `AlekSIS-App-Hjelp`_ | The Hjelp app provides functionality for aiding users. |
+--------------------------------------+---------------------------------------------------------------------------------------------+
| `AlekSIS-App-LDAP`_ | The LDAP app provides functionality to import users and groups from LDAP |
+--------------------------------------+---------------------------------------------------------------------------------------------+
| `AlekSIS-App-Untis`_ | This app provides import and export functions to interact with Untis, a timetable software. |
+--------------------------------------+---------------------------------------------------------------------------------------------+
Licence
-------
......@@ -40,8 +81,8 @@ Licence
Copyright © 2018, 2019, 2020 Julian Leucker <leuckeju@katharineum.de>
Copyright © 2018, 2019, 2020 Hangzhi Yu <yuha@katharineum.de>
Copyright © 2019, 2020 Dominik George <dominik.george@teckids.org>
Copyright © 2019, 2020 mirabilos <thorsten.glaser@teckids.org>
Copyright © 2019, 2020 Tom Teichler <tom.teichler@teckids.org>
Copyright © 2019 mirabilos <thorsten.glaser@teckids.org>
Licenced under the EUPL, version 1.2 or later
......@@ -50,5 +91,13 @@ full licence text or on the `European Union Public Licence`_ website
https://joinup.ec.europa.eu/collection/eupl/guidelines-users-and-developers
(including all other official language versions).
.. _AlekSIS: https://edugit.org/AlekSIS/Official/AlekSIS
.. _AlekSIS: https://aleksis.org/
.. _Teckids e.V.: https://www.teckids.org/
.. _Katharineum zu Lübeck: https://www.katharineum.de/
.. _European Union Public Licence: https://eupl.eu/
.. _schul-frei: https://schul-frei.org/
.. _AlekSIS-App-Chronos: https://edugit.org/AlekSIS/official/AlekSIS-App-Chronos
.. _AlekSIS-App-DashboardFeeds: https://edugit.org/AlekSIS/official/AlekSIS-App-DashboardFeeds
.. _AlekSIS-App-Hjelp: https://edugit.org/AlekSIS/official/AlekSIS-App-Hjelp
.. _AlekSIS-App-LDAP: https://edugit.org/AlekSIS/official/AlekSIS-App-LDAP
.. _AlekSIS-App-Untis: https://edugit.org/AlekSIS/official/AlekSIS-App-Untis
import pkg_resources
from django.utils.translation import gettext_lazy as _
try:
from .celery import app as celery_app
except ModuleNotFoundError:
......
# noqa
from django.contrib import admin
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)
admin.site.register(Group)
admin.site.register(School)
admin.site.register(SchoolTerm)
admin.site.register(Activity)
admin.site.register(Notification)
admin.site.register(CustomMenuItem)
admin.site.register(Person, VersionAdmin)
admin.site.register(Group, VersionAdmin)
admin.site.register(Activity, VersionAdmin)
admin.site.register(Notification, VersionAdmin)
admin.site.register(CustomMenuItem, VersionAdmin)
class AnnouncementRecipientInline(admin.StackedInline):
model = AnnouncementRecipient
class AnnouncementAdmin(admin.ModelAdmin):
class AnnouncementAdmin(BaseModelAdmin, VersionAdmin):
inlines = [
AnnouncementRecipientInline,
]
......
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.utils.module_loading import autodiscover_modules
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
class CoreConfig(AppConfig):
......@@ -17,18 +24,54 @@ class CoreConfig(AppConfig):
"Repository": "https://edugit.org/AlekSIS/official/AlekSIS/",
}
licence = "EUPL-1.2+"
copyright = (
copyright_info = (
([2017, 2018, 2019, 2020], "Jonathan Weth", "wethjo@katharineum.de"),
([2017, 2018, 2019], "Frank Poetzsch-Heffter", "p-h@katharineum.de"),
([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()
# Autodiscover various modules defined by AlekSIS
autodiscover_modules("form_extensions", "model_extensions", "checks")
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
is_favicon = name == "favicon"
if new_value:
Favicon.on_site.update_or_create(
title=name,
defaults={"isFavicon": name == "favicon", "faviconImage": new_value,},
)
else:
Favicon.on_site.filter(title=name, isFavicon=is_favicon).delete()
def post_migrate(
self,
......@@ -42,7 +85,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()
......@@ -11,8 +11,7 @@ from .util.apps import AppConfig
def check_app_configs_base_class(
app_configs: Optional[django.apps.registry.Apps] = None, **kwargs
) -> list:
""" Checks whether all apps derive from AlekSIS's base app config """
"""Check whether all apps derive from AlekSIS's base app config."""
results = []
if app_configs is None:
......@@ -22,8 +21,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",
)
......@@ -36,8 +39,7 @@ def check_app_configs_base_class(
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 """
"""Check whether all app models derive from AlekSIS's base ExtensibleModel."""
results = []
if app_configs is None:
......@@ -48,8 +50,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 typing import Callable
from django.contrib.auth.decorators import login_required, user_passes_test
from .util.core_helpers import has_person
def admin_required(function: Callable = None) -> Callable:
actual_decorator = user_passes_test(lambda u: u.is_active and u.is_superuser)
return actual_decorator(function)
def person_required(function: Callable = None) -> Callable:
""" Requires a logged-in user which is linked to a person. """
actual_decorator = user_passes_test(has_person)
return actual_decorator(login_required(function))
from django_filters import CharFilter, FilterSet
from material import Layout, Row
class GroupFilter(FilterSet):
name = CharFilter(lookup_expr="icontains")
short_name = CharFilter(lookup_expr="icontains")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form.layout = Layout(Row("name", "short_name"))
class PersonFilter(FilterSet):
first_name = CharFilter(lookup_expr="icontains")
last_name = CharFilter(lookup_expr="icontains")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form.layout = Layout(Row("first_name", "last_name"))
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 .mixins import ExtensibleForm
from .models import Group, Person, School, SchoolTerm, Announcement, AnnouncementRecipient
from dynamic_preferences.forms import PreferenceForm
from material import Fieldset, Layout, Row
from .mixins import ExtensibleForm, SchoolTermRelatedExtensibleForm
from .models import AdditionalField, Announcement, Group, GroupType, Person, SchoolTerm
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,22 +30,27 @@ 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
def clean(self) -> None:
User = get_user_model()
user = get_user_model()
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():
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:
new_user_obj = User.objects.create_user(
# 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,
first_name=self.instance.first_name,
......@@ -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,31 +117,18 @@ class EditPersonForm(ExtensibleForm):
)
def clean(self) -> None:
User = get_user_model()
# Use code implemented in dedicated form to verify user selection
return PersonAccountForm.clean(self)
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
class EditGroupForm(SchoolTermRelatedExtensibleForm):
"""Form to edit an existing group in the frontend."""
class EditGroupForm(ExtensibleForm):
layout = Layout(
Fieldset(_("Common data"), "name", "short_name"),
Fieldset(_("School term"), "school_term"),
Fieldset(_("Common data"), "name", "short_name", "group_type"),
Fieldset(_("Persons"), "members", "owners", "parent_groups"),
Fieldset(_("Additional data"), "additional_fields"),
)
class Meta:
......@@ -152,29 +152,13 @@ class EditGroupForm(ExtensibleForm):
"parent_groups": ModelSelect2MultipleWidget(
search_fields=["name__icontains", "short_name__icontains"]
),
"additional_fields": ModelSelect2MultipleWidget(search_fields=["title__icontains",]),
}
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)
......@@ -187,7 +171,7 @@ class AnnouncementForm(ExtensibleForm):
persons = forms.ModelMultipleChoiceField(
Person.objects.all(), label=_("Persons"), required=False
)
groups = forms.ModelMultipleChoiceField(Group.objects.all(), label=_("Groups"), required=False)
groups = forms.ModelMultipleChoiceField(queryset=None, label=_("Groups"), required=False)
layout = Layout(
Fieldset(
......@@ -200,6 +184,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 +203,19 @@ class AnnouncementForm(ExtensibleForm):
"groups": announcement.get_recipients_for_model(Group),
"persons": announcement.get_recipients_for_model(Person),
}
super().__init__(*args, **kwargs)
self.fields["groups"].queryset = Group.objects.for_current_school_term_or_all()
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,38 +225,89 @@ 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
exclude = []
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
class EditAdditionalFieldForm(forms.ModelForm):
"""Form to manage additional fields."""
class Meta:
model = AdditionalField
exclude = []
class EditGroupTypeForm(forms.ModelForm):
"""Form to manage group types."""
class Meta:
model = GroupType
exclude = []
class SchoolTermForm(ExtensibleForm):
"""Form for managing school years."""
layout = Layout("name", Row("date_start", "date_end"))
class Meta:
model = SchoolTerm
exclude = []
This diff is collapsed.
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-03-30 09:18+0000\n"
"POT-Creation-Date: 2020-04-28 13:31+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......
This diff is collapsed.
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-03-30 09:18+0000\n"
"POT-Creation-Date: 2020-05-03 10:36+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......@@ -17,14 +17,14 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: static/js/main.js:21
#: aleksis/core/static/js/main.js:21
msgid "Today"
msgstr ""
#: static/js/main.js:22
#: aleksis/core/static/js/main.js:22
msgid "Cancel"
msgstr ""
#: static/js/main.js:23
#: aleksis/core/static/js/main.js:23
msgid "OK"
msgstr ""
This diff is collapsed.
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-03-30 09:18+0000\n"
"POT-Creation-Date: 2020-05-03 10:36+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......@@ -18,14 +18,14 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: static/js/main.js:21
#: aleksis/core/static/js/main.js:21
msgid "Today"
msgstr ""
#: static/js/main.js:22
#: aleksis/core/static/js/main.js:22
msgid "Cancel"
msgstr ""
#: static/js/main.js:23
#: aleksis/core/static/js/main.js:23
msgid "OK"
msgstr ""
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