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

Merge branch 'master' into...

Merge branch 'master' into 285-manage-permissions-for-persons-users-and-groups-django-groups-in-frontend
parents 17189375 5261b30c
No related branches found
No related tags found
1 merge request!533Resolve "Manage permissions for persons (users) and groups (Django groups) in frontend"
Pipeline #39307 passed
Showing
with 7656 additions and 3186 deletions
......@@ -65,7 +65,6 @@ docs/_build/
aleksis/node_modules/
aleksis/static/
aleksis/whoosh_index/
aleksis/xapian_index/
.coverage
.mypy_cache/
......
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`_
--------
New features
~~~~~~~~~~~~
* Migrate to materialize.css
* Dashboard
* Notifications via SMS (Twilio), Email or on the dashboard
* Admin interface
* Turn into installable, progressive web app
* Devs: Background Tasks with Celery
Minor changes
~~~~~~~~~~~~~
* Customisable save_button template
* Redesign error pages
Bug fixes
~~~~~~~~~
* setup_data no longer forces database connection
`1.0a4`_
--------
New features
~~~~~~~~~~~~
All notable changes to this project will be documented in this file.
The format is based on `Keep a Changelog`_,
and this project adheres to `Semantic Versioning`_.
Unreleased
----------
`2.1`_ - 2021-11-05
-------------------
Added
~~~~~
* Provide an ``ExtensiblePolymorphicModel`` to support the features of extensible models for polymorphic models and vice-versa.
* Implement optional Sentry integration for error and performance tracing.
* Option to limit allowed scopes per application, including mixin to enforce that limit on OAuth resource views
* Support trusted OAuth applications that leave out the authorisation screen.
* Add birthplace to Person model.
Changed
~~~~~~~
* Replace dev.sh helper script with tox environments.
* OAuth Grant Flows are now configured system-wide instead of per app.
* Refactor OAuth2 application management views.
Fixed
~~~~~
* Fix default admin contacts
Credits
~~~~~~~
* We welcome new contributor 🐧 Jonathan Krüger!
* We welcome new contributor 🐭 Lukas Weichelt!
`2.0`_ - 2021-10-29
-------------------
Changed
~~~~~~~
* Refactor views/forms for creating/editing persons.
Fixed
~~~~~
* Fix order of submit buttons in login form and restructure login template
to make 2FA work correctly.
* Fix page title bug on the impersonate page.
* Users were able to edit the linked user if self-editing was activated.
* Users weren't able to edit the allowed fields although they were configured correctly.
* Provide `style.css` and icon files without any authentication to avoid caching issues.
Removed
~~~~~~~
* Remove mass linking of persons to accounts, bevcause the view had performance issues,
but was practically unused.
`2.0rc7`_ - 2021-10-18
----------------------
Fixed
~~~~~
* Configuration mechanisms for OpenID Connect were broken.
* Set a fixed version for django-sass-processor to avoid a bug with searching ``style.css`` in the wrong storage.
* Correct the z-index of the navbar to display the main title again on mobile devices.
Removed
~~~~~~~
* Leftovers from a functionality already dropped in the development process
(custom authentication backends and alternative login views).
`2.0rc6`_ - 2021-10-11
----------------------
Added
~~~~~
* OpenID Connect scope and accompanying claim ``groups``
* Support config files in JSON format
* Allow apps to dynamically generate OAuth scopes
Changed
~~~~~~~
* Do not log or e-mail ALLOWED_HOSTS violations
* Update translations.
* Use initial superuser settings as default contact and from addresses
Fixed
~~~~~
* Show link to imprint in footer
* Fix API for adding OAuth scopes in AppConfigs
* Deleting persons is possible again.
* Removed wrong changelog section
Removed
~~~~~~~
* Dropped data anonymization (django-hattori) support for now
* ``OAUTH2_SCOPES`` setting in apps is not supported anymore. Use ``get_all_scopes`` method
on ``AppConfig`` class instead.
`2.0rc5`_ - 2021-08-25
----------------------
Fixed
~~~~~
* The view for getting the progress of celery tasks didn't respect that there can be anonymous users.
* Updated django to latest 3.2.x
`2.0rc4`_ - 2021-08-01
----------------------
Added
~~~~~
* Allow to configure port for prometheus metrics endpoint.
Fixed
~~~~~
* Correctly deliver server errors to user
* Use text HTTP response for serviceworker.js insteas of binary stream
* Use Django permission instead of rule to prevent performance issues.
`2.0rc3`_ - 2021-07-26
----------------------
Added
~~~~~
* Support PDF generation without available request object (started completely from background).
* Display a loading animation while fetching search results in the sidebar.
Fixed
~~~~~
* Make search suggestions selectable using the arrow keys.
Fixed
~~~~~
* Use correct HTML 5 elements for the search frontend and fix CSS accordingly.
`2.0rc2`_ - 2021-06-24
---------------------
Added
~~~~~
* Allow to install system and build dependencies in docker build
`2.0rc1`_ - 2021-06-23
----------------------
Added
~~~~~
* Add option to disable dashboard auto updating as a user and sitewide.
Changed
~~~~~~~
* Use semantically correct html elements for headings and alerts.
Fixed
~~~~~
* Add missing dependency python-gnupg
* Add missing AWS options to ignore invalid ssl certificates
`2.0b2`_ - 2021-06-15
--------------------
Added
~~~~~~~
* Add option to disable dashboard auto updating as a user and sitewide.
Changed
~~~~~~~
* Add verbose names for all preference sections.
* Add verbose names for all openid connect scopes and show them in grant
view.
* Include public dashboard in navigation
* Update German translations.
Fixed
~~~~~
* Fix broken backup health check
* Make error recovery in about page work
Removed
~~~~~~~
* Drop all leftovers of DataTables.
`2.0b1`_ - 2021-06-01
---------------------
Changed
~~~~~~~
* Rename every occurance of "social account" by "third-party account".
* Use own templates and views for PWA meta and manifest.
* Use term "application" for all authorized OAuth2 applications/tokens.
* Use importlib instead of pkg_resources (no functional changes)
Fixed
~~~~~
* Fix installation documentation (nginx, uWSGI).
* Use a set for data checks registry to prevent double entries.
* Progress page tried to redirect even if the URL is empty.
Removed
~~~~~~~
* Drop django-pwa completely.
`2.0b0`_ - 2021-05-21
---------------------
Added
~~~~~
* Allow defining several search configs for LDAP users and groups
* Use setuptools entrypoints to find apps
* Add django-cachalot as query cache
* Add ``syncable_fields`` property to ``ExtensibleModel`` to discover fields
sync backends can write to
* Add ``aleksis-admin`` script to wrap django-admin with pre-configured settings
* Auto-create persons for users if matching attributes are found
* Add ``django-allauth`` to allow authentication using OAuth, user registration,
password changes and password reset
* Add OAuth2 and OpenID Connect provider support
* Add ``django-uwsgi`` to use uWSGI and Celery in development
* Add loading page for displaying Celery task progress
* Implement generic PDF generation using Chromium
* Support Amazon S3 storage for /media files
* Enable Django REST framework for apps to use at own discretion
* Add method to inject permissions to ExtensibleModels dynamically
* Add helper function which filters queryset by permission and user
* Add generic support for Select 2 with materialize theme
* Add simple message that is shown whenever a page is served from the PWA cache
* Add possibility to upload files using ckeditor
* Show guardians and children on person full page
* Manage object-level permissions in frontend
* Add a generic deletion confirmation view
* Serve Prometheus metrics from app
* Provide system health check endpoint and checks for some components
* Add impersonate button to person view
* Implement a data check system for sanity checks and guided resolution of inconsistencies
* Make the dashboard configurable for users and as default dashboard by admins
* Support dynamic badges in menu items
* Auto-delete old /media files when related model instance is deleted
* Add SortableJS
* Add a widget for links/buttons to other websites
Changed
~~~~~~~
* Make Redis non-optional (see documentation)
* Use Redis as caching and session store to allow horizontal scaling
* Enable PostgreSQL connection pooling
* Use uWSGI to serve /static under development
* Use a token-secured storage as default /media storage
* Rewrite Docker image to serve as generic base image for AlekSIS distributions
* Make Docker image run completely read-only
* Ensure Docker image is compatible with K8s
* Remove legacy file upload functoin; all code is required to use the storage API
* Default search index backend is now Whoosh with Redis storage
* Re-style search result page
* Move notifications to separate page with indicator in menu
* Move to ``BigAutoField`` for all AlekSIS apps
* Require Django 3.2 and Python 3.9
* Person and group lists can now be filtered
* Allow displaying the default widget to anonymous users
Fixed
~~~~~
* Correct behavious of celery-beat in development
* Fix precaching of offline fallback page
* Use correct styling for language selector
* Rewrite notification e-mail template for AlekSIS
* Global search now obeys permissions correctly
* Improve performance of favicon generation
* Dashboard widgets now handle exceptions gracefully
* Roboto font was not available for serving locally
Removed
~~~~~~~
* Dropped support for other search backends than Whoosh
* Drop django-middleware-global-request completely
`2.0a2`_ - 2020-05-04
---------------------
Added
~~~~~
* 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.
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.
Fixed
~~~~~
* DateTimeField Announcement.valid_from received a naive datetime.
* Enable SASS processor in production.
* Fix too short fields.
* Load select2 locally.
`2.0a1`_ - 2020-02-01
---------------------
Added
~~~~~
* Migrate to MaterializeCSS.
* Dashboard.
* Notifications via SMS (Twilio), Email or on the dashboard.
* Admin interface.
* Turn into installable, progressive web app.
* Devs: Background Tasks with Celery.
Changed
~~~~~~~
* Customisable save_button template.
* Redesign error pages.
Fixed
~~~~~
* setup_data no longer forces database connection.
`1.0a4`_ - 2019-11-25
---------------------
Added
~~~~~
* Two-factor authentication with TOTP (Google Authenticator), Yubikey, SMS
and phone call.
* Devs: CRUDMixin provides a crud_event relation that returns all CRUD
events for an object
events for an object.
`1.0a2`_
--------
`1.0a2`_ - 2019-11-11
---------------------
New features
~~~~~~~~~~~~
Added
~~~~~
* Devs: Add ExtensibleModel to allow injection of methods and properties into models.
`1.0a1`_
--------
New features
~~~~~~~~~~~~
* Devs: Add API to get an audit trail for any school-related object
* Devs: Provide template snippet to display an audit trail
* Devs: Provide base template for views that allow browsing back/forth
* Add management command and Cron job for full backups
* Add system status overview page
* Allow enabling and disabling maintenance mode from frontend
* Allow editing the dates of the current school term
* Add logo to school information
* Allow editing school information
* Ensure all actions are reverted if something fails (atomic requests)
`1.0a1`_ - 2019-09-17
---------------------
Bugfixes
~~~~~~~~
Added
~~~~~
* Only show active persons in group and persons views
* Silence KeyError in get_dict template tag
* Devs: Add API to get an audit trail for any school-related object.
* Devs: Provide template snippet to display an audit trail.
* Devs: Provide base template for views that allow browsing back/forth.
* Add management command and Cron job for full backups.
* Add system status overview page.
* Allow enabling and disabling maintenance mode from frontend.
* Allow editing the dates of the current school term.
* Add logo to school information.
* Allow editing school information.
* Ensure all actions are reverted if something fails (atomic requests).
Minor changes
~~~~~~~~~~~~~
Fixed
~~~~~
* Use bootstrap buttons everywhere
* Only show active persons in group and persons views.
* Silence KeyError in get_dict template tag.
* Use bootstrap buttons everywhere.
.. _Keep a Changelog: https://keepachangelog.com/en/1.0.0/
.. _Semantic Versioning: https://semver.org/spec/v2.0.0.html
_`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
.. _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
.. _2.0b0: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0b0
.. _2.0b1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0b1
.. _2.0b2: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0b2
.. _2.0rc1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0rc1
.. _2.0rc2: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0rc2
.. _2.0rc3: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0rc3
.. _2.0rc4: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0rc4
.. _2.0rc5: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0rc5
.. _2.0rc6: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0rc6
.. _2.0rc7: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0rc7
.. _2.0: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0
.. _2.1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.1
......@@ -26,7 +26,7 @@ RUN apt-get -y update && \
eatmydata apt-get -y upgrade && \
eatmydata apt-get install -y --no-install-recommends \
build-essential \
chromium \
chromium \
dumb-init \
gettext \
libpq5 \
......@@ -34,6 +34,7 @@ RUN apt-get -y update && \
libssl-dev \
postgresql-client \
python3-dev \
python3-magic \
python3-pip \
uwsgi \
uwsgi-plugin-python3 \
......@@ -94,7 +95,18 @@ USER 33:33
# Additional steps
ONBUILD ARG APPS
ONBUILD ARG BUILD_DEPS
ONBUILD ARG SYSTEM_DEPS
ONBUILD USER 0:0
ONBUILD RUN set -e; \
if [ -n "$BUILD_DEPS" ]; then \
eatmydata apt-get update; \
eatmydata apt-get install -y $BUILD_DEPS; \
fi; \
if [ -n "$SYSTEM_DEPS" ]; then \
eatmydata apt-get update; \
eatmydata apt-get install -y $SYSTEM_DEPS; \
fi;
ONBUILD RUN set -e; \
if [ -n "$APPS" ]; then \
eatmydata pip install $APPS; \
......@@ -102,7 +114,7 @@ ONBUILD RUN set -e; \
eatmydata aleksis-admin yarn install; \
eatmydata aleksis-admin collectstatic --no-input; \
rm -rf /usr/local/share/.cache; \
eatmydata apt-get remove --purge -y yarnpkg; \
eatmydata apt-get remove --purge -y yarnpkg $BUILD_DEPS; \
eatmydata apt-get autoremove --purge -y; \
apt-get clean -y; \
rm -f /var/lib/apt/lists/*_*; \
......
AlekSIS (School Information System) — Core (Core functionality and app framework)
=================================================================================
AlekSIS standard distribution
-----------------------------
This is the core of the AlekSIS framework and the official distribution
(see below). It bundles functionality for all apps, and utilities for
developers and administrators.
The AlekSIS standard distribution with information about all official apps
can be found on `EduGit`_.
If you are looking for the AlekSIS standard distribution, i.e. the complete
software product ready for installation and usage, please visit the `AlekSIS`_
website or the distribution repository on `EduGit`_.
Features
--------
The AlekSIS-Core currently provides the following features:
The AlekSIS core currently provides the following features:
* For users:
* Authentication via OAuth applications
* Configurable dashboard
* Custom menu entries (e.g. in footer)
* Global preferences
* Global search
* Group types
* Manage announcements
* Manage groups
* Manage persons
* Notifications via SMS email or dashboard
* PWA with offline caching
* Rules and permissions for users, objects and pages
* Two factor authentication via Yubikey, OTP or SMS
* User preferences
* User registration, password changes and password reset
* For admins
* Asynchronous tasks with celery
* Authentication via LDAP
* Automatic backup of database, static and media files
* Generic PDF generation with chromium
* OAuth2 and OpenID Connect provider support
* Serve prometheus metrics
* System health and data checks
* For developers
* `aleksis-admin` script to wrap django-admin with pre-configured settings
* Caching with Redis
* Django REST framework for apps to use at own discretion
* Injection of fields, methods, permissions and properties via custom `ExtensibleModel`
* K8s compatible, read-only Docker image
* Object-level permissions and rules with `django-guardian` and `django-rules`
* Query caching with `django-cachalot`
* Search with `django-haystack` and `Whoosh` backend
* uWSGI and Celery via `django-uwsgi` in development
Licence
-------
::
Copyright © 2017, 2018, 2019, 2020, 2021 Jonathan Weth <wethjo@katharineum.de>
Copyright © 2017, 2018, 2019, 2020, 2021 Jonathan Weth <dev@jonathanweth.de>
Copyright © 2017, 2018, 2019, 2020 Frank Poetzsch-Heffter <p-h@katharineum.de>
Copyright © 2018, 2019, 2020, 2021 Julian Leucker <leuckeju@katharineum.de>
Copyright © 2018, 2019, 2020, 2021 Hangzhi Yu <yuha@katharineum.de>
Copyright © 2019, 2020, 2021 Dominik George <dominik.george@teckids.org>
Copyright © 2019, 2020, 2021 Tom Teichler <tom.teichler@teckids.org>
Copyright © 2019 mirabilos <thorsten.glaser@teckids.org>
Copyright © 2021 Lloyd Meins <meinsll@katharineum.de>
Copyright © 2021 magicfelix <felix@felix-zauberer.de>
Licenced under the EUPL, version 1.2 or later, by Teckids e.V. (Bonn, Germany).
......@@ -52,6 +76,6 @@ 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
.. _European Union Public Licence: https://eupl.eu/
.. _EduGit: https://edugit.org/AlekSIS/official/AlekSIS
import pkg_resources
from importlib import metadata
try:
from .celery import app as celery_app
......@@ -7,7 +7,7 @@ except ModuleNotFoundError:
celery_app = None
try:
__version__ = pkg_resources.get_distribution("AlekSIS-Core").version
__version__ = metadata.distribution("AlekSIS-Core").version
except Exception:
__version__ = "unknown"
......
from hattori.base import BaseAnonymizer, faker
from .models import Person
class PersonAnonymizer(BaseAnonymizer):
model = Person
attributes = [
("first_name", faker.first_name),
("last_name", faker.last_name),
("additional_name", ""),
("short_name", lambda **kwargs: faker.pystr(min_chars=3, max_chars=5, **kwargs)),
("street", faker.street_name),
("housenumber", faker.building_number),
("postal_code", faker.postcode),
("place", faker.city),
("phone_number", ""),
("mobile_number", ""),
("email", faker.email),
(
"date_of_birth",
lambda **kwargs: faker.date_of_birth(minimum_age=8, maximum_age=66, **kwargs),
),
("photo", ""),
]
......@@ -2,8 +2,10 @@ from typing import Any, Optional
import django.apps
from django.apps import apps
from django.conf import settings
from django.http import HttpRequest
from django.utils.module_loading import autodiscover_modules
from django.utils.translation import gettext as _
from dynamic_preferences.registries import preference_models
from health_check.plugins import plugin_dir
......@@ -14,7 +16,7 @@ from .registries import (
site_preferences_registry,
)
from .util.apps import AppConfig
from .util.core_helpers import has_person
from .util.core_helpers import get_or_create_favicon, has_person
from .util.sass_helpers import clean_scss
......@@ -35,12 +37,15 @@ class CoreConfig(AppConfig):
([2019, 2020, 2021], "Dominik George", "dominik.george@teckids.org"),
([2019, 2020, 2021], "Tom Teichler", "tom.teichler@teckids.org"),
([2019], "mirabilos", "thorsten.glaser@teckids.org"),
([2021], "Lloyd Meins", "meinsll@katharineum.de"),
([2021], "magicfelix", "felix@felix-zauberer.de"),
)
def ready(self):
super().ready()
from django.conf import settings # noqa
# Autodiscover various modules defined by AlekSIS
autodiscover_modules("form_extensions", "model_extensions", "checks")
......@@ -71,9 +76,9 @@ class CoreConfig(AppConfig):
"""Get all data checks from all loaded models."""
from aleksis.core.data_checks import DataCheckRegistry
data_checks = []
data_checks = set()
for model in apps.get_models():
data_checks += getattr(model, "data_checks", [])
data_checks.update(getattr(model, "data_checks", []))
DataCheckRegistry.data_checks = data_checks
def preference_updated(
......@@ -85,6 +90,8 @@ class CoreConfig(AppConfig):
new_value: Optional[Any] = None,
**kwargs,
) -> None:
from django.conf import settings # noqa
if section == "theme":
if name in ("primary", "secondary"):
clean_scss()
......@@ -95,11 +102,14 @@ class CoreConfig(AppConfig):
if new_value:
Favicon.on_site.update_or_create(
title=name,
defaults={"isFavicon": name == "favicon", "faviconImage": new_value,},
title=name, defaults={"isFavicon": is_favicon, "faviconImage": new_value},
)
else:
Favicon.on_site.filter(title=name, isFavicon=is_favicon).delete()
if name in settings.DEFAULT_FAVICON_PATHS:
get_or_create_favicon(
name, settings.DEFAULT_FAVICON_PATHS[name], is_favicon=is_favicon
)
def post_migrate(
self,
......@@ -109,6 +119,8 @@ class CoreConfig(AppConfig):
using: str,
**kwargs,
) -> None:
from django.conf import settings # noqa
super().post_migrate(app_config, verbosity, interactive, using, **kwargs)
# Ensure presence of an OTP YubiKey default config
......@@ -116,9 +128,29 @@ class CoreConfig(AppConfig):
name="default", defaults={"use_ssl": True, "param_sl": "", "param_timeout": ""}
)
# Ensure that default Favicon object exists
for name, default in settings.DEFAULT_FAVICON_PATHS.items():
get_or_create_favicon(name, default, is_favicon=name == "favicon")
def user_logged_in(
self, sender: type, request: Optional[HttpRequest], user: "User", **kwargs
) -> None:
if has_person(user):
# Save the associated person to pick up defaults
user.person.save()
@classmethod
def get_all_scopes(cls) -> dict[str, str]:
scopes = {
"read": "Read anything the resource owner can read",
"write": "Write anything the resource owner can write",
}
if settings.OAUTH2_PROVIDER.get("OIDC_ENABLED", False):
scopes |= {
"openid": _("OpenID Connect scope"),
"profile": _("Given name, family name, link to profile and picture if existing."),
"address": _("Full home postal address"),
"email": _("Email address"),
"phone": _("Home and mobile phone"),
}
return scopes
import logging
from django.apps import apps
from django.contrib.contenttypes.models import ContentType
from django.db.models.aggregates import Count
from django.utils.functional import classproperty
......@@ -156,11 +157,19 @@ class DataCheck:
solve_options = {IgnoreSolveOption.name: IgnoreSolveOption}
_current_results = []
@classmethod
def check_data(cls):
"""Find all objects with data issues and register them."""
pass
@classmethod
def run_check_data(cls):
"""Wrap ``check_data`` to ensure that post-processing tasks are run."""
cls.check_data()
cls.delete_old_results()
@classmethod
def solve(cls, check_result: "DataCheckResult", solve_option: str):
"""Execute a solve option for an object detected by this check.
......@@ -188,16 +197,35 @@ class DataCheck:
from aleksis.core.models import DataCheckResult
ct = ContentType.objects.get_for_model(instance)
result = DataCheckResult.objects.get_or_create(
result, __ = DataCheckResult.objects.get_or_create(
check=cls.name, content_type=ct, object_id=instance.id
)
# Track all existing problems (for deleting old results)
cls._current_results.append(result)
return result
@classmethod
def delete_old_results(cls):
"""Delete old data check results for problems which exist no longer."""
DataCheckResult = apps.get_model("core", "DataCheckResult")
pks = [r.pk for r in cls._current_results]
old_results = DataCheckResult.objects.filter(check=cls.name).exclude(pk__in=pks)
if old_results:
logging.info(f"Delete {old_results.count()} old data check results.")
old_results.delete()
# Reset list with existing problems
cls._current_results = []
class DataCheckRegistry:
"""Create central registry for all data checks in AlekSIS."""
data_checks = []
data_checks: set = set()
@classproperty
def data_checks_by_name(cls):
......@@ -213,7 +241,7 @@ def check_data(recorder: ProgressRecorder):
"""Execute all registered data checks and send email if activated."""
for check in recorder.iterate(DataCheckRegistry.data_checks):
logging.info(f"Run check: {check.verbose_name}")
check.check_data()
check.run_check_data()
if get_site_preferences()["general__data_checks_send_emails"]:
send_emails_for_data_checks()
......@@ -256,3 +284,35 @@ def send_emails_for_data_checks():
logging.info("Sent notification email because of unsent data checks")
results.update(sent=True)
class DeactivateDashboardWidgetSolveOption(SolveOption):
name = "deactivate_dashboard_widget"
verbose_name = _("Deactivate DashboardWidget")
@classmethod
def solve(cls, check_result: "DataCheckResult"):
widget = check_result.related_object
widget.active = False
widget.save()
check_result.delete()
class BrokenDashboardWidgetDataCheck(DataCheck):
name = "broken_dashboard_widgets"
verbose_name = _("Ensure that there are no broken DashboardWidgets.")
problem_name = _("The DashboardWidget was reported broken automatically.")
solve_options = {
IgnoreSolveOption.name: IgnoreSolveOption,
DeactivateDashboardWidgetSolveOption.name: DeactivateDashboardWidgetSolveOption,
}
@classmethod
def check_data(cls):
from .models import DashboardWidget
broken_widgets = DashboardWidget.objects.filter(broken=True, active=True)
for widget in broken_widgets:
logging.info("Check DashboardWidget %s", widget)
cls.register_result(widget)
from datetime import datetime, time
from typing import Any, Callable, Dict, List, Sequence, Tuple
from typing import Any, Callable, Dict, Sequence
from django import forms
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.core.exceptions import ValidationError
......@@ -9,9 +10,11 @@ from django.db.models import QuerySet
from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _
from allauth.account.adapter import get_adapter
from allauth.account.forms import SignupForm
from allauth.account.utils import setup_user_email
from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget, Select2Widget
from dynamic_preferences.forms import PreferenceForm
from guardian.core import ObjectPermissionChecker
from guardian.shortcuts import assign_perm
from material import Fieldset, Layout, Row
......@@ -22,6 +25,7 @@ from .models import (
DashboardWidget,
Group,
GroupType,
OAuthApplication,
Person,
SchoolTerm,
)
......@@ -30,58 +34,12 @@ from .registries import (
person_preferences_registry,
site_preferences_registry,
)
from .util.auth_helpers import AppScopes
from .util.core_helpers import get_site_preferences
class PersonAccountForm(forms.ModelForm):
"""Form to assign user accounts to persons in the frontend."""
class Meta:
model = Person
fields = ["last_name", "first_name", "user"]
widgets = {"user": Select2Widget(attrs={"class": "browser-default"})}
new_user = forms.CharField(required=False)
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()
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,
first_name=self.instance.first_name,
last_name=self.instance.last_name,
)
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."""
class PersonForm(ExtensibleForm):
"""Form to edit or add a person object in the frontend."""
layout = Layout(
Fieldset(
......@@ -94,7 +52,11 @@ class EditPersonForm(ExtensibleForm):
Fieldset(_("Address"), Row("street", "housenumber"), Row("postal_code", "place")),
Fieldset(_("Contact data"), "email", Row("phone_number", "mobile_number")),
Fieldset(
_("Advanced personal data"), Row("sex", "date_of_birth"), Row("photo"), "guardians",
_("Advanced personal data"),
Row("date_of_birth", "place_of_birth"),
Row("sex"),
Row("photo"),
"guardians",
),
)
......@@ -115,6 +77,7 @@ class EditPersonForm(ExtensibleForm):
"mobile_number",
"email",
"date_of_birth",
"place_of_birth",
"sex",
"photo",
"guardians",
......@@ -140,25 +103,49 @@ class EditPersonForm(ExtensibleForm):
required=False, label=_("New user"), help_text=_("Create a new account")
)
def __init__(self, request: HttpRequest, *args, **kwargs):
def __init__(self, *args, **kwargs):
request = kwargs.pop("request", None)
super().__init__(*args, **kwargs)
# Disable non-editable fields
person_fields = set([field.name for field in Person.syncable_fields()]).intersection(
set(self.fields)
)
allowed_person_fields = get_site_preferences()["account__editable_fields_person"]
if self.instance:
checker = ObjectPermissionChecker(request.user)
checker.prefetch_perms([self.instance])
if (
request
and self.instance
and not request.user.has_perm("core.change_person", self.instance)
):
# First, disable all fields
for field in self.fields:
self.fields[field].disabled = True
for field in person_fields:
if not checker.has_perm(f"core.change_person_field_{field}", self.instance):
self.fields[field].disabled = True
# Then, activate allowed fields
for field in allowed_person_fields:
self.fields[field].disabled = False
def clean(self) -> None:
# Use code implemented in dedicated form to verify user selection
return PersonAccountForm.clean(self)
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():
# 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,
first_name=self.instance.first_name,
last_name=self.instance.last_name,
)
self.cleaned_data["user"] = new_user_obj
class EditGroupForm(SchoolTermRelatedExtensibleForm):
......@@ -379,8 +366,7 @@ class SchoolTermForm(ExtensibleForm):
class DashboardWidgetOrderForm(ExtensibleForm):
pk = forms.ModelChoiceField(
queryset=DashboardWidget.objects.all(),
widget=forms.HiddenInput(attrs={"class": "pk-input"}),
queryset=None, widget=forms.HiddenInput(attrs={"class": "pk-input"}),
)
order = forms.IntegerField(initial=0, widget=forms.HiddenInput(attrs={"class": "order-input"}))
......@@ -388,6 +374,12 @@ class DashboardWidgetOrderForm(ExtensibleForm):
model = DashboardWidget
fields = []
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Set queryset here to prevent problems with not migrated database due to special queryset
self.fields["pk"].queryset = DashboardWidget.objects.all()
DashboardWidgetOrderFormSet = forms.formset_factory(
form=DashboardWidgetOrderForm, max_num=0, extra=0
......@@ -504,6 +496,81 @@ class AssignPermissionForm(forms.Form):
assign_perm(permission_name, django_group, instance)
class AccountRegisterForm(SignupForm, ExtensibleForm):
"""Form to register new user accounts."""
class Meta:
model = Group
fields = []
layout = Layout(
Fieldset(_("Base data"), Row("first_name", "last_name"),),
Fieldset(
_("Account data"), "username", Row("email", "email2"), Row("password1", "password2"),
),
Fieldset(_("Consents"), Row("privacy_policy"),),
)
def __init__(self, *args, **kwargs):
super(AccountRegisterForm, self).__init__(*args, **kwargs)
self.fields["password1"] = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
privacy_policy = get_site_preferences()["footer__privacy_url"]
if settings.SIGNUP_PASSWORD_ENTER_TWICE:
self.fields["password2"] = forms.CharField(
label=_("Password (again)"), widget=forms.PasswordInput
)
self.fields["first_name"] = forms.CharField(required=True,)
self.fields["last_name"] = forms.CharField(required=True,)
self.fields["privacy_policy"] = forms.BooleanField(
help_text=_(
f"I have read the <a href='{privacy_policy}'>Privacy policy</a>"
" and agree with them."
),
required=True,
)
def clean(self):
super(AccountRegisterForm, self).clean()
dummy_user = get_user_model()
password = self.cleaned_data.get("password1")
if password:
try:
get_adapter().clean_password(password, user=dummy_user)
except forms.ValidationError as e:
self.add_error("password1", e)
if (
settings.SIGNUP_PASSWORD_ENTER_TWICE
and "password1" in self.cleaned_data
and "password2" in self.cleaned_data
):
if self.cleaned_data["password1"] != self.cleaned_data["password2"]:
self.add_error(
"password2", _("You must type the same password each time."),
)
return self.cleaned_data
def save(self, request):
adapter = get_adapter(request)
user = adapter.new_user(request)
adapter.save_user(request, user, self)
Person.objects.create(
first_name=self.cleaned_data["first_name"],
last_name=self.cleaned_data["last_name"],
email=self.cleaned_data["email"],
user=user,
)
self.custom_signup(request, user)
setup_user_email(request, user, [])
return user
class ActionForm(forms.Form):
"""Generic form for executing actions on multiple items of a queryset.
......@@ -555,11 +622,11 @@ class ActionForm(forms.Form):
"""Get all defined actions."""
return self.actions
def _get_actions_dict(self) -> Dict[str, Callable]:
def _get_actions_dict(self) -> dict[str, Callable]:
"""Get all defined actions as dictionary."""
return {value.__name__: value for value in self.get_actions()}
def _get_action_choices(self) -> List[Tuple[str, str]]:
def _get_action_choices(self) -> list[tuple[str, str]]:
"""Get all defined actions as Django choices."""
return [
(value.__name__, getattr(value, "short_description", value.__name__))
......@@ -616,15 +683,15 @@ class ListActionForm(ActionForm):
# Return None in order not to raise an unwanted exception
return None
def _get_dict(self) -> Dict[str, dict]:
def _get_dict(self) -> dict[str, dict]:
"""Get the items sorted by pk attribute."""
return {item["pk"]: item for item in self.items}
def _get_choices(self) -> List[Tuple[str, str]]:
def _get_choices(self) -> list[tuple[str, str]]:
"""Get the items as Django choices."""
return [(item["pk"], item["pk"]) for item in self.items]
def _get_real_items(self, items: Sequence[dict]) -> List[dict]:
def _get_real_items(self, items: Sequence[dict]) -> list[dict]:
"""Get the real dictionaries from a list of pks."""
items_dict = self._get_dict()
real_items = []
......@@ -634,7 +701,7 @@ class ListActionForm(ActionForm):
real_items.append(items_dict[item])
return real_items
def clean_selected_objects(self) -> List[dict]:
def clean_selected_objects(self) -> list[dict]:
data = self.cleaned_data["selected_objects"]
items = self._get_real_items(data)
return items
......@@ -643,3 +710,23 @@ class ListActionForm(ActionForm):
self.items = items
super().__init__(request, *args, **kwargs)
self.fields["selected_objects"].choices = self._get_choices()
class OAuthApplicationForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["allowed_scopes"].widget = forms.SelectMultiple(
choices=list(AppScopes().get_all_scopes().items())
)
class Meta:
model = OAuthApplication
fields = (
"name",
"client_id",
"client_secret",
"client_type",
"allowed_scopes",
"redirect_uris",
"skip_authorization",
)
......@@ -34,10 +34,13 @@ class BaseBackupHealthCheck(BaseHealthCheckBackend):
def check_status(self):
storage = get_storage()
backups = storage.list_backups(content_type=self.content_type)
if not storage.storage.exists(""):
self.add_error(_("The backup folder doesn't exist."))
return
if backups:
last_backup = backups[-1]
last_backup_time = dbbackup_utils.filename_to_date(last_backup)
time_gone_since_backup = last_backup_time - datetime.now()
last_backup = backups[:1]
last_backup_time = dbbackup_utils.filename_to_date(last_backup[0])
time_gone_since_backup = datetime.now() - last_backup_time
# Check if backup is older than configured time
if time_gone_since_backup.seconds > self.configured_seconds:
......
This diff is collapsed.
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-01-11 21:30+0100\n"
"POT-Creation-Date: 2021-10-28 16:18+0200\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,18 +18,18 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
#: static/js/main.js:15
#: aleksis/core/static/js/main.js:15
msgid "Today"
msgstr ""
#: static/js/main.js:16
#: aleksis/core/static/js/main.js:16
msgid "Cancel"
msgstr ""
#: static/js/main.js:17
#: aleksis/core/static/js/main.js:17
msgid "OK"
msgstr ""
#: static/js/main.js:118
#: aleksis/core/static/js/main.js:127
msgid "This page may contain outdated information since there is no internet connection."
msgstr ""
This diff is collapsed.
......@@ -3,32 +3,36 @@
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-01-11 21:30+0100\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"
"Language: \n"
"POT-Creation-Date: 2021-08-28 17:53+0200\n"
"PO-Revision-Date: 2021-10-28 14:37+0000\n"
"Last-Translator: Jonathan Weth <teckids@jonathanweth.de>\n"
"Language-Team: German <https://translate.edugit.org/projects/aleksis/"
"aleksis-core-js/de/>\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.8\n"
#: static/js/main.js:15
#: aleksis/core/static/js/main.js:15
msgid "Today"
msgstr ""
msgstr "Heute"
#: static/js/main.js:16
#: aleksis/core/static/js/main.js:16
msgid "Cancel"
msgstr ""
msgstr "Abbrechen"
#: static/js/main.js:17
#: aleksis/core/static/js/main.js:17
msgid "OK"
msgstr ""
msgstr "OK"
#: static/js/main.js:118
#: aleksis/core/static/js/main.js:127
msgid "This page may contain outdated information since there is no internet connection."
msgstr ""
"Diese Seite enthält vielleicht veraltete Informationen, da es keine "
"Internetverbindung gibt."
This diff is collapsed.
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-01-11 21:30+0100\n"
"POT-Creation-Date: 2021-10-28 16:18+0200\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,18 +18,18 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: static/js/main.js:15
#: aleksis/core/static/js/main.js:15
msgid "Today"
msgstr ""
#: static/js/main.js:16
#: aleksis/core/static/js/main.js:16
msgid "Cancel"
msgstr ""
#: static/js/main.js:17
#: aleksis/core/static/js/main.js:17
msgid "OK"
msgstr ""
#: static/js/main.js:118
#: aleksis/core/static/js/main.js:127
msgid "This page may contain outdated information since there is no internet connection."
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: 2021-01-11 21:30+0100\n"
"POT-Creation-Date: 2021-10-28 16:18+0200\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,18 +17,18 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: static/js/main.js:15
#: aleksis/core/static/js/main.js:15
msgid "Today"
msgstr ""
#: static/js/main.js:16
#: aleksis/core/static/js/main.js:16
msgid "Cancel"
msgstr ""
#: static/js/main.js:17
#: aleksis/core/static/js/main.js:17
msgid "OK"
msgstr ""
#: static/js/main.js:118
#: aleksis/core/static/js/main.js:127
msgid "This page may contain outdated information since there is no internet connection."
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: 2021-01-11 21:30+0100\n"
"POT-Creation-Date: 2021-10-28 16:18+0200\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,18 +17,18 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: static/js/main.js:15
#: aleksis/core/static/js/main.js:15
msgid "Today"
msgstr ""
#: static/js/main.js:16
#: aleksis/core/static/js/main.js:16
msgid "Cancel"
msgstr ""
#: static/js/main.js:17
#: aleksis/core/static/js/main.js:17
msgid "OK"
msgstr ""
#: static/js/main.js:118
#: aleksis/core/static/js/main.js:127
msgid "This page may contain outdated information since there is no internet connection."
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