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 (5545)
Showing with 2370 additions and 266 deletions
module.exports = {
root: true,
overrides: [
{
files: ["*.js", "*.vue"],
// parser: "vue-eslint-parser",
//processor: "@graphql-eslint/graphql",
extends: [
"eslint:recommended",
"plugin:vue/strongly-recommended",
"plugin:@intlify/vue-i18n/recommended",
],
rules: {
"no-unused-vars": "warn",
"vue/no-unused-vars": "off",
"vue/multi-word-component-names": "off",
"vue/attribute-hyphenation": "error",
"vue/v-slot-style": "error",
"@intlify/vue-i18n/key-format-style": [
"error",
"snake_case",
{
splitByDots: false,
},
],
// "@intlify/vue-i18n/no-unused-keys": ["warn", {}],
"@intlify/vue-i18n/no-raw-text": [
"error",
{
ignoreNodes: ["v-icon"],
ignorePattern: "^[-–—·#:()\\[\\]&\\.\\s]+$",
},
],
"@intlify/vue-i18n/no-deprecated-tc": "off",
// Fixes for prettier (avoid eslint-config-prettier)
// The following rules can be used in some cases. See the README for more
// information. (These are marked with `0` instead of `"off"` so that a
// script can distinguish them.)
curly: 0,
"lines-around-comment": 0,
"max-len": 0,
"no-confusing-arrow": 0,
"no-mixed-operators": 0,
"no-tabs": 0,
"no-unexpected-multiline": 0,
quotes: 0,
"@typescript-eslint/quotes": 0,
"babel/quotes": 0,
"vue/html-self-closing": 0,
"vue/max-len": 0,
// The rest are rules that you never need to enable when using Prettier.
"array-bracket-newline": "off",
"array-bracket-spacing": "off",
"array-element-newline": "off",
"arrow-parens": "off",
"arrow-spacing": "off",
"block-spacing": "off",
"brace-style": "off",
"comma-dangle": "off",
"comma-spacing": "off",
"comma-style": "off",
"computed-property-spacing": "off",
"dot-location": "off",
"eol-last": "off",
"func-call-spacing": "off",
"function-call-argument-newline": "off",
"function-paren-newline": "off",
"generator-star": "off",
"generator-star-spacing": "off",
"implicit-arrow-linebreak": "off",
indent: "off",
"jsx-quotes": "off",
"key-spacing": "off",
"keyword-spacing": "off",
"linebreak-style": "off",
"multiline-ternary": "off",
"newline-per-chained-call": "off",
"new-parens": "off",
"no-arrow-condition": "off",
"no-comma-dangle": "off",
"no-extra-parens": "off",
"no-extra-semi": "off",
"no-floating-decimal": "off",
"no-mixed-spaces-and-tabs": "off",
"no-multi-spaces": "off",
"no-multiple-empty-lines": "off",
"no-reserved-keys": "off",
"no-space-before-semi": "off",
"no-trailing-spaces": "off",
"no-whitespace-before-property": "off",
"no-wrap-func": "off",
"nonblock-statement-body-position": "off",
"object-curly-newline": "off",
"object-curly-spacing": "off",
"object-property-newline": "off",
"one-var-declaration-per-line": "off",
"operator-linebreak": "off",
"padded-blocks": "off",
"quote-props": "off",
"rest-spread-spacing": "off",
semi: "off",
"semi-spacing": "off",
"semi-style": "off",
"space-after-function-name": "off",
"space-after-keywords": "off",
"space-before-blocks": "off",
"space-before-function-paren": "off",
"space-before-function-parentheses": "off",
"space-before-keywords": "off",
"space-in-brackets": "off",
"space-in-parens": "off",
"space-infix-ops": "off",
"space-return-throw-case": "off",
"space-unary-ops": "off",
"space-unary-word-ops": "off",
"switch-colon-spacing": "off",
"template-curly-spacing": "off",
"template-tag-spacing": "off",
"unicode-bom": "off",
"wrap-iife": "off",
"wrap-regex": "off",
"yield-star-spacing": "off",
"@babel/object-curly-spacing": "off",
"@babel/semi": "off",
"@typescript-eslint/brace-style": "off",
"@typescript-eslint/comma-dangle": "off",
"@typescript-eslint/comma-spacing": "off",
"@typescript-eslint/func-call-spacing": "off",
"@typescript-eslint/indent": "off",
"@typescript-eslint/keyword-spacing": "off",
"@typescript-eslint/member-delimiter-style": "off",
"@typescript-eslint/no-extra-parens": "off",
"@typescript-eslint/no-extra-semi": "off",
"@typescript-eslint/object-curly-spacing": "off",
"@typescript-eslint/semi": "off",
"@typescript-eslint/space-before-blocks": "off",
"@typescript-eslint/space-before-function-paren": "off",
"@typescript-eslint/space-infix-ops": "off",
"@typescript-eslint/type-annotation-spacing": "off",
"babel/object-curly-spacing": "off",
"babel/semi": "off",
"flowtype/boolean-style": "off",
"flowtype/delimiter-dangle": "off",
"flowtype/generic-spacing": "off",
"flowtype/object-type-curly-spacing": "off",
"flowtype/object-type-delimiter": "off",
"flowtype/quotes": "off",
"flowtype/semi": "off",
"flowtype/space-after-type-colon": "off",
"flowtype/space-before-generic-bracket": "off",
"flowtype/space-before-type-colon": "off",
"flowtype/union-intersection-spacing": "off",
"react/jsx-child-element-spacing": "off",
"react/jsx-closing-bracket-location": "off",
"react/jsx-closing-tag-location": "off",
"react/jsx-curly-newline": "off",
"react/jsx-curly-spacing": "off",
"react/jsx-equals-spacing": "off",
"react/jsx-first-prop-new-line": "off",
"react/jsx-indent": "off",
"react/jsx-indent-props": "off",
"react/jsx-max-props-per-line": "off",
"react/jsx-newline": "off",
"react/jsx-one-expression-per-line": "off",
"react/jsx-props-no-multi-spaces": "off",
"react/jsx-tag-spacing": "off",
"react/jsx-wrap-multilines": "off",
"standard/array-bracket-even-spacing": "off",
"standard/computed-property-even-spacing": "off",
"standard/object-curly-even-spacing": "off",
"unicorn/empty-brace-spaces": "off",
"unicorn/no-nested-ternary": "off",
"unicorn/number-literal-case": "off",
"vue/array-bracket-newline": "off",
"vue/array-bracket-spacing": "off",
"vue/arrow-spacing": "off",
"vue/block-spacing": "off",
"vue/block-tag-newline": "off",
"vue/brace-style": "off",
"vue/comma-dangle": "off",
"vue/comma-spacing": "off",
"vue/comma-style": "off",
"vue/dot-location": "off",
"vue/func-call-spacing": "off",
"vue/html-closing-bracket-newline": "off",
"vue/html-closing-bracket-spacing": "off",
"vue/html-end-tags": "off",
"vue/html-indent": "off",
"vue/html-quotes": "off",
"vue/key-spacing": "off",
"vue/keyword-spacing": "off",
"vue/max-attributes-per-line": "off",
"vue/multiline-html-element-content-newline": "off",
"vue/multiline-ternary": "off",
"vue/mustache-interpolation-spacing": "off",
"vue/no-extra-parens": "off",
"vue/no-multi-spaces": "off",
"vue/no-spaces-around-equal-signs-in-attribute": "off",
"vue/object-curly-newline": "off",
"vue/object-curly-spacing": "off",
"vue/object-property-newline": "off",
"vue/operator-linebreak": "off",
"vue/quote-props": "off",
"vue/script-indent": "off",
"vue/singleline-html-element-content-newline": "off",
"vue/space-in-parens": "off",
"vue/space-infix-ops": "off",
"vue/space-unary-ops": "off",
"vue/template-curly-spacing": "off",
},
settings: {
"vue-i18n": {
localeDir: "./aleksis/core/frontend/messages/*.{json}",
messageSyntaxVersion: "^8.0.0",
},
},
env: {
es2021: true,
},
parserOptions: {
ecmaVersion: "latest",
},
},
{
files: ["*.graphql"],
parser: "@graphql-eslint/eslint-plugin",
plugins: ["@graphql-eslint"],
extends: "plugin:@graphql-eslint/operations-recommended",
parserOptions: {
graphQLConfig: {
schema: "./schema.json",
documents: "../aleksis/**/*/frontend/**/*.graphql",
},
},
rules: {
"@graphql-eslint/no-anonymous-operations": "error",
"@graphql-eslint/no-duplicate-fields": "error",
"@graphql-eslint/naming-convention": [
"error",
{
OperationDefinition: {
style: "camelCase",
forbiddenPrefixes: ["Query", "Mutation", "Subscription", "Get"],
forbiddenSuffixes: ["Query", "Mutation", "Subscription"],
},
},
],
},
},
],
};
{
"name": "aleksis-builddeps",
"version": "1.0.0",
"dependencies": {
"@graphql-eslint/eslint-plugin": "^4.3.0",
"@intlify/eslint-plugin-vue-i18n": "^3.0.0",
"eslint": "^8.26.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-vue": "^9.7.0",
"graphql": "^16.10.0",
"prettier": "^3.4.0",
"stylelint": "^16.0.0",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-standard": "^34.0.0"
}
}
<!-- AlekSIS is developed on EduGit. GitHub only serves as
backup mirror and to help people find the project. If
possible, please submit your merge request on EduGit!
EduGit accepts logins with GitHub accounts.
-->
[ ] I have read the above and have no way to contribute on EduGit
[ ] I understand that GitHub's terms of service exclude young and
learning contributors, but still cannot contribute on EduGit
instead.
......@@ -52,6 +52,11 @@ DEADJOE
.idea
.idea/
# VSCode
.vscode/
.history/
*.code-workspace
# Database
db.sqlite3
......@@ -62,19 +67,32 @@ docs/_build/
*.aux
# Generated files
aleksis/node_modules/
aleksis/static/
aleksis/whoosh_index/
/cache
/node_modules
.dev-js/node_modules
/static/
/whoosh_index/
.vite
.dev-js/.yarn
.dev-js/.pnp.cjs
.dev-js/.pnp.loader.mjs
.dev-js/.yarnrc.yml
.dev-js/schema.json
# Lock files
poetry.lock
package-lock.json
yarn.lock
.dev-js/yarn.lock
# Tests
.coverage
.mypy_cache/
.tox/
htmlcov/
# Data
maintenance_mode_state.txt
media/
package-lock.json
# VSCode
.vscode/
.history/
*.code-workspace
aleksis/core/static/style.css
include:
- project: "AlekSIS/official/AlekSIS"
file: /ci/general.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/test/test.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/test/lint.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/test/security.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/build/dist.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/publish/pypi.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/docker/image.yml
- project: "AlekSIS/official/AlekSIS"
file: "/ci/deploy/review.yml"
- project: "AlekSIS/official/AlekSIS"
file: "/ci/deploy/trigger_dist.yml"
- project: "AlekSIS/official/AlekSIS"
file: /ci/deploy/pages.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/general.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/prepare/lock.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/test/lint.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/test/security.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/test/test.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/build/dist.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/publish/pypi.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/docker/image.yml
- project: "AlekSIS/official/AlekSIS"
file: "/ci/deploy/review.yml"
- project: "AlekSIS/official/AlekSIS"
file: "/ci/deploy/trigger_dist.yml"
......@@ -8,7 +8,9 @@ Jonathan Weth <git@jonathanweth.de> Jonathan Weth <joniweth@gmx.de>
Jonathan Weth <git@jonathanweth.de> Jonathan Weth <mail@jonathanweth.de>
Jonathan Weth <git@jonathanweth.de> Jonathan Weth <wethjo@katharineum.de>
Julian Leucker <leuckerj@gmail.com> Julian <leuckerj@gmail.com>
Lloyd Meins <git@lloydmeins.de> Aithus <lloydmeins@gmx.net>
Silas Della Contrada <s.developer@4-dc.de> sdcieo0330 <silasdc0@gmail.com>
Tom Teichler <tom.teichler@teckids.org> Tom Teichler <t.teichler@babiel.com>
mirabilos <thorsten.glaser@teckids.org> mirabilos <mirabilos@evolvis.org>
mirabilos <thorsten.glaser@teckids.org> mirabilos <t.glaser@tarent.de>
root (Skolelinux) <root@tjener.intern> root <root@tjener.intern>
......
# Byte-compiled / optimized / DLL files
*$py.class
*.py[cod]
__pycache__/
# Distribution / packaging
*.egg
*.egg-info/
.Python
.eggs/
.installed.cfg
build/
develop-eggs/
dist/
downloads/
eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
# Installer logs
pip-delete-this-directory.txt
pip-log.txt
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# pyenv
.python-version
# Environments
.env
.venv
ENV/
env/
venv/
# Editors
*~
DEADJOE
\#*#
# IntelliJ
.idea
.idea/
# Database
db.sqlite3
# Sphinx
docs/_build/
# TeX
*.aux
# Generated files
/node_modules/
/static/
/whoosh_index/
poetry.lock
.coverage
.mypy_cache/
.tox/
htmlcov/
maintenance_mode_state.txt
media/
package-lock.json
yarn.lock
# VSCode
.vscode/
.history/
*.code-workspace
/cache
# Add HTML files to avoid problems with unsupported Django templates
*.html
# Do not check/reformat generated files
aleksis/core/util/licenses.json
.vite/
.pnp.cjs
.pnp.loader.mjs
.git/
{
"extends": ["stylelint-config-standard", "stylelint-config-prettier"]
}
......@@ -9,11 +9,1123 @@ and this project adheres to `Semantic Versioning`_.
Unreleased
----------
Notable, breaking changes
~~~~~~~~~~~~~~~~~~~~~~~~~
* The dashboard has been rebuilt within the new Vue frontend. As a result, all apps with widgets must
be updated accordingly. Installing AlekSIS apps that still use legacy dashboard widgets will cause
the dashboard to break.
* The library for the 2FA was replaced so all accounts with configured 2FA will habe to reconfigure
their 2FA methods. There is support for TOTP, recovery codes, and Webautn.
* Django backend administration (django-admin) has been removed.
* [Dev] `ExtensibleModel` now has a `uuid` field, requiring migrations to be created for all
AlekSIS models.
* [Dev] Celery progress tracking was updated and needs changes if ``recorded_task`` is used.
Added
~~~~~
* Availability events feed indicating persons' free/busy status.
* Free/busy feed aggregating all events that explicitly set a person's or a group's availability status.
* [Dev] Introduce a RoomField analog to other similar fields.
* [Dev] Celery progress component for including in own pages (base for celery progress page).
* [Dev] All AlekSIS models now have a UUID
* Introduce availability types linked to availability events.
* Attendees of personal events are now provided via CalDAV.
* With the correct permissions, it's now possible to change/reset passwords for other users.
* vCards of persons now contain related persons.
* [Dev] Add a CalendarFeedField.
* [Dev] There are two new rules for frontend fields: isGreaterAndNotSameThan and isSmallerAndNotSameThan.
* [Dev] Introduce a Todo base model.
* Introduce personal todos.
* [Dev] Add a PositiveFloatField analog to PositiveSmallIntegerField.
* Add button that marks all notifications as read
* A configurable email contact will be notified on account registrations.
Changed
~~~~~~~
* Dashboard has been migrated to a new, interactive one.
* Announcements are shown in calendar.
* All HTTP requests are now encapsulated in a database transaction.
* DialogObjectForm is now slightly wider.
* [Dev] DialogObjectForm now implicitly handles a missing ``isCreate`` prop depending on the presence of ``editItem``.
* [Dev] DeleteDialog supports the activator slot now.
* The account registration and invitation code forms were migrated to the new frontend.
* Following administration views were migrated to new frontend:
* System status
* Data checks
* Allow limiting page numbers in PDF generation.
* vCards contain a link to the AlekSIS person object.
* Views for changing and resetting passwords were migrated to new frontend.
* [Dev] The ObjectForm bottom aligns its rows now.
* [CardDAV] Accept header and vCard version specified in REPORT query are respected.
Fixed
~~~~~
* Buttons in some dialogs were missing space between.
* [Dev] Dialog-Object-Form's internal dialog-mode-handling did not implement opening via activator slot.
* [Dev] MutateMixin could not be used for affected queries without variables.
* [Dev] RecurrenceField did error if an empty date was input.
* [Dev] InlineCRUDList broke with DialogObjectForm implicitly handling undefined ``isCreate``.
* Celery polling did not decrease frequency after all tasks have been finished.
* The person edit form guardians field did not show existing guardians.
* Filtering persons caused an error.
* Impersonation did not work.
* Fix Sentry integration to work with frontend and backend.
* After accessing forbidden page, redirect to page on successful login.
* Fix DAV endpoint for single objects.
* End datetimes passed to the calendar events query were not respected.
* [Dev] Do not exhaust PostgreSQL max_connections in development
* Editing managed objects was possible in inline lists.
* PDF generation took very long due to not properly closed selenium sessions
* [Dev] `get_single_events` didn't expand events by default.
* [CalDAV] calendar-multiget returned more objects than requested.
Removed
~~~~~~~
* [Dev] ``render_progress_page`` has been removed.
`4.0.1`_ - 2025-04-16
---------------------
Fixed
~~~~~
* It wasn't possible to run data checks due to broken color data checks.
* Recurrence information for calendar events weren't removed on deletion.
* Full day events didn't work in calendar.
* Recurring events without until value weren't shown.
* [Dev] AddressInputType missed country field.
* Detail pages, e.g. for groups, did not work anymore.
* The configured theme colors were not used by the frontend.
* It wasn't possible to change icons of OAuth applications in the frontend.
* First fetching of calendar feeds logged an error to console.
`4.0.0`_ - 2025-03-29
-------------------
Notable, breaking changes
~~~~~~~~~~~~~~~~~~~~~~~~~
The special assignment page for groups and child groups has been removed.
For the calendar system, AlekSIS now needs an extension for the PostgreSQL database.
Please check the docs for instructions how to setup the ``pg_rrule`` extension
for PostgreSQL.
AlekSIS now uses Valkey as a drop-in replacement for Redis. Please update your configuration
files accordingly (see docs for further instructions).
To make setting names consistent, the setting ``auth.login.registration.unique_email``
was renamed to ``auth.registration.unique_email``.
The "additional fields" feature has been removed because it had design issues
that practically made it unusable in all scenarios. No migration path away
from the feature is defined. If you have been using additional group fields
and need a replacement, please contact the development team.
The "managed models" feature is mandatory for all models derived from `ExtensibleModel`
and requires creating a migration for all downstream models to add the respective field.
As legacy pages are no longer themed, you should update them to the new frontend as soon as possible.
To prevent collisions with fields, the class variable ``name`` on ``RegistryObject`` has been renamed
to ``_class_name``. Please update any references and subclasses.
Deprecated
~~~~~~~~~~
* The field ``owners`` of group will be removed in the next release and will be replaced by memberships
using the special ownership role.
Added
~~~~~
* Global calendar system
* CalDAV and CardDAV for syncing calendars and Persons read-only.
* Calendar for birthdays of persons
* Management of personal calendar events.
* Holiday model to track information about holidays.
* Following management views were added:
* Rooms
* Holiday
* Global school term select for limiting data to a specific school term.
* Error message when loading in incompatible browser
* Tooltips for every information in the person page
* New menu item "Data management" with Rooms, Announcements, Holidays, and School Terms
* Priority to sort announcements
* Generic Roles for describing relationships. Currently used for Person-to-Group relationships.
* Mascot images in multiple places throughout the application.
* Generic endpoint for retrieving objects as JSON
* Add option to disallow reserved usernames.
* Allow matching social accounts to local accounts by their username.
* Support RP-initiated logout for OIDC applications
* Support native PostgreSQL connection pooling
* Support profiling with Sentry in addition to tracing
* Introduce .well-known urlpatterns for apps
* [Dev] Views can request to span the entire screen width.
* [Dev] Base model for organisational entities (external companies, associations,…)
* [Dev] Support running of data checks before/after migrations.
* [Dev] Notifications based on calendar alarms.
* [Dev] Components for implementing standard CRUD operations in new frontend.
* [Dev] Options for filtering and sorting of GraphQL queries at the server.
* [Dev] Managed models for instances handled by other apps.
* [Dev] Upload slot sytem for out-of-band uploads in GraphQL clients
Changed
~~~~~~~
* Following management views were migrated to new frontend:
* School Terms
* Announcements
* OAuth Applications
* Persons
* Content width on different screen sizes is more consistent.
* Room model icon changed from the default to a specific one.
* Show only short name, if available, in announcement recipients
* Move "Invite person" to persons page
* Show avatars of groups in all places.
* Use new auth rate limiting settings
* Factor out addresses in their own model and allow multiple addresses with different types
(e. g. home, business) for one person
* Setting ``auth.login.registration.unique_email`` was renamed to ``auth.registration.unique_email``
* Adapt permission scheme for announcements to other permissions.
* Use Firefox instead of Chromium for PDF creation and support external webdriver via
`selenium.url` option, e.g. for use in containers.
* Replace all mentions of Redis with Valkey where possible
* [Dev] Rename `RegistryObject`'s class var `name` to `_class_name`.
* [Dev] Use Django 5.
* [Dev] Bump Python version to 3.10
* [Dev] Child groups are exposed in the GraphQL type for groups.
Fixed
~~~~~
* Accessibility issues with new frontend.
* Improve error handling in frontend and show meaningful error messages.
* GraphQL mutations did not return errors in case of exceptions.
* Make email field unique over all persons.
* Opening group details wasn't possible without permissions for all person details.
* Correctly redirect to AlekSIS frontend after login with a third-party account.
* Our own account adapter wasn't used so signup settings weren't applied correctly.
* Setting images for groups did not work
* Update and fix URLs for 3rdparty login.
* The OpenID Connect Discovery endpoint now returns the issuer data directly
under the URI without a trailing `/`.
* Not-logged in users were able to access all PDF files (CVE-2025-25683).
* [Docs] An example config contained invalid values.
* [Container] Database backup failed with postgres versions 15 and 16.
* [Dev] Foreign keys to ExtensiblePolymorphicModel types were using the wrong manager.
* [Dev] Allow activating more frequent polling for Celery task progress.
* [Dev] Integrate model validation mechanisms into GraphQL queries.
Removed
~~~~~~~
* Yubikey support (not WebAuthn) was removed
* Additional fields.
* Legacy pages are no longer themed.
* [Dev] Batching of GraphQL queries.
* [Dev] `_recursive` methods for groups have been removed.
Developers relying on parent groups need to account for recursion themselves.
* [Dev] Extended fields mechanism on top of django-jsonstore.
`3.2.2`_ - 2025-01-18
---------------------
Fixed
~~~~~
* Not-logged in users were able to access all PDF files (CVE-2025-25683).
`3.2.1`_ - 2024-06-27
---------------------
Fixed
~~~~~
* Synchronisation of AlekSIS and Django groups caused permissions issues
* [OIDC] Custom additional claims were not present in userinfo
* [Docs] The docker-compose method was not described detailed enough
* [Docker] Fix build of production images to include only released versions
* Third-party login buttons now directly open external login page.
* Persons could not be edited by non-superusers with global person editing permission.
* Permission checks for dashboard widget creation and person invitations were invalid
* New Persons were not added to selected primary group on creation
* In some browsers, AlekSIS was not categorised as an installable PWA.
* Announcements without any recipient had a line to display recipients
* Missing migrations for update of OAuth library
`3.2.0`_ - 2023-12-25
---------------------
Fixed
~~~~~
* Description field of Person was not editable.
* [Docs] Certain parts of installation docs not visible
* Nav submenu items could not be distinguished from regular ones.
* Group GraphQL queries failed when queried by owner or member.
* Special printouts included a blank white page at the end.
* Icons of active menu entries are filled if possible.
* Collapse icon on the progress drawer was the wrong way around.
* Deleting persons now requires confirmation.
* Editing of OAuth applications led to broken UI.
* Add missing feedback for password changing and resetting.
* Sex of a person was not translated.
Deprecated
~~~~~~~~~~
This release deprecates some features in preparation for the 4.0 release.
* Additional fields.
* Legacy Yubikey support (not WebAuthn).
* [Dev] *_recursive methods for groups. Developers relying on parent groups
need to account for recursion themselves.
* [Dev] Extended fields mechanism on top of django-jsonstore.
`3.1.7`_ - 2025-01-18
---------------------
Fixed
~~~~~
* Not-logged in users were able to access all PDF files (CVE-2025-25683).
`3.1.6`_ - 2024-06-27
---------------------
Fixed
~~~~~
* [Docs] Certain parts of installation docs not visible
* Synchronisation of AlekSIS and Django groups caused permissions issues
* [OIDC] Custom additional claims were not present in userinfo
* [Docs] The docker-compose method was not described detailed enough
* [Docker] Fix build of production images to include only released versions
* Third-party login buttons now directly open external login page.
* Persons could not be edited by non-superusers with global person editing permission.
* Permission checks for dashboard widget creation and person invitations were invalid
* New Persons were not added to selected primary group on creation
* In some browsers, AlekSIS was not categorised as an installable PWA.
* Announcements without any recipient had a line to display recipients
* Missing migrations for update of OAuth library
`3.1.5`_ - 2023-09-02
---------------------
Fixed
~~~~~
* [Docs] A required package was not listed
* Migrations failed in some cases
`3.1.4`_ - 2023-07-20
---------------------
Fixed
~~~~~
* Extensible form was broken due to a missing import.
`3.1.3`_ – 2023-07-18
---------------------
Fixed
~~~~~
* [Docker] The build could silently continue even if frontend bundling failed, resulting
in an incomplete AlekSIS frontend app.
* Rendering of "simple" PDF templates failed when used with S3 storage.
* Log messages on some loggers did not contain log message
`3.1.2`_ - 2023-07-05
---------------------
Changed
~~~~~~~
* uWSGI is now installed together with AlekSIS-Core per default.
Fixed
~~~~~
* Notifications were not properly shown in the frontend.
* [Dev] Log levels were not correctly propagated to all loggers
* [Dev] Log format did not contain all essential information
* When navigating from legacy to legacy page, the latter would reload once for no reason.
* The oauth authorization page was not accessible when the service worker was active.
* [Docker] Clear obsolete bundle parts when adding apps using ONBUILD
* Extensible forms that used a subset of fields did not render properly
`3.1.1`_ - 2023-07-01
---------------------
Fixed
~~~~~
* Progress page didn't work properly.
* About page failed to load for apps with an unknown licence.
* QUeries for persons with partial permissions failed.
* Some pages couldn't be scrolled when a task progress popup was open.
* Notification query failed on admin users without persons.
* Querying for notification caused unnecessary database requests.
* Loading bar didn't disappear on some pages after loading was finished.
* Support newer versions of django-oauth-toolkit.
`3.1`_ - 2023-05-30
-------------------
Changed
~~~~~~~
* The frontend is now able to display headings in the main toolbar.
Fixed
~~~~~
* Default translations from Vuetify were not loaded.
* Browser locale was not the default locale in the entire frontend.
* In some cases, items in the sidenav menu were not shown.
* The search bar in the sidenav menu was shown even though the user had no permission to see it.
* Accept invitation menu item was shown when the invitation feature was disabled.
* Metrics endpoint for Prometheus was at the wrong URL.
* Polling behavior of the whoAmI and permission queries was improved.
* Confirmation e-mail contained a wrong link.
`3.0`_ - 2023-05-11
-------------------
Added
~~~~~
* GraphQL schema for Rooms
* Provide API endpoint for system status.
* [Dev] UpdateIndicator Vue Component to display the status of interactive pages
* [Dev] DeleteDialog Vue Component to unify item deletion in the new frontend
* Use build-in mechanism in Apollo for GraphQL batch querying.
Changed
~~~~~~~
* Show message on successful logout to inform users properly.
* Phone number country now has to be configured in config file insted of frontend.
Fixed
~~~~~
* GraphQL endpoints for groups, persons, and notifications didn't expose all necessary fields.
* Loading indicator in toolbar was not shown at the complete loading progress.
* 404 page was sometimes shown while the page was still loading.
* Setting of page height in the iframe was not working correctly.
* App switched to offline state when the user was logged out/in.
* The `Stop Impersonation` button is not shown due to an oversee when changing the type of the whoAmI query to an object of UserType
* Offline fallback page for legacy pages was misleading sometimes.
* Route changes in the Legacy-Component iframe didn't trigger a scroll to the top
* Query strings did not get passed when navigating legacy pages inside of the SPA.
* Retry button on error 500 page did not trigger a reload of the page.
* When the Celery worker wasn't able to execute all tasks in time, notifications were sent multiple times.
* Changing the maintenance mode state spawned another SPA instance in the iframe
* Phone numbers couldn't be in regional format.
* System status view wasn't accessible through new frontend if a check failed.
* Progress page didn't show error message on failure.
* Dynamic routes were not removed/hidden when the respective object registering it was deleted.
* Django messages were not displayed in Vue frontend.
* Links to data check objects did not work properly.
* Backend cleanup task for Celery wasn't working.
* URLs in invitation email were broken.
* Invitation view didn't work.
* Invitation emails were using wrong styling.
* GraphQL queries and mutations did not log exceptions.
`3.0b3`_ - 2023-03-19
---------------------
Fixed
~~~~~
* Some GraphQL queries could return more data than permitted in related fields.
`3.0b2`_ - 2023-03-09
---------------------
Changed
~~~~~~~
* Change default network policy of the Apollo client to `cache-and-network`.
Fixed
~~~~~
* In case the status code of a response was not in the range between 200 and 299
but still indicates that the response should be delivered, e. g. in the case
of a redirected request, the service worker served the offline fallback page.
* In some cases, the resize listener for the IFrame in the `LegacyBaseTemplate`
did not trigger.
* [Dev] Allow apps to declare URLs in the non-legacy namespace again
`3.0b1`_ - 2023-02-27
---------------------
Added
~~~~~
* Support for two factor authentication via email codes and Webauthn.
`3.0b0`_ - 2023-02-15
---------------------
This release starts a new era of the AlekSIS® framework, by introducing a
dynamic frontend app written in Vue.js which communicates with the backend
through GraphQL. Support for legacy views (Django templates and
Materialize) was removed; while there is backwards compatibility for now,
this is only used by official apps until their views are fully migrated.
AlekSIS and its new frontend require Node.js version 18 or higher to run the
Vite bundler. On Debian, this means that Debian 12 (bookworm) is needed, or
Node.js must be installed from a third-party repository.
Removed
~~~~~~~
* Official support for views rendered server-side in Django is removed. The
`LegacyBaseTemplate` provided for backwards compatibility must not be used
by apps declaring a dependency on AlekSIS >= 3.0.
* Support for deploying AlekSIS in sub-URLs
* Support for production deployments without HTTPS
Deprecated
~~~~~~~~~~
* The `webpack_bundle` management command is replaced by the new `vite`
command. The `webpack_bundle` command will be removed in AlekSIS-Core 4.0.
Added
~~~~~
* Notification drawer in top nav bar
* GraphQL queries for base system and some core data management
* [Dev] New mechanism to register classes over all apps (RegistryObject)
* Model for rooms
Changed
~~~~~~~
* Show languages in local language
* Rewrite of frontend (base template) using Vuetify
* Frontend bundling migrated from Webpack to Vite (cf. installation docs)
* [Dev] The runuwsgi dev server now starts a Vite dev server with HMR in the
background
* OIDC scope "profile" now exposes the avatar instead of the official photo
* Based on Django 4.0
* Use built-in Redis cache backend
* Introduce PBKDF2-SHA1 password hashing
* Persistent database connections are now health-checked as to not fail
requests
* [Dev] The undocumented field `check` on `DataCheckResult` was renamed to `data_check`
* Frontend bundling migrated from Webpack to Vite
* Get dashboard widgets and data checks from apps with new registration mechanism.
* Use write-through cache for sessions to retain on clear_cache
* Better error page with redirect option to login page when user has no permission to access a route.
* Users now can setup as many 2FA devices as they want.
* The 2FA profile overview was completely redesigned.
Fixed
~~~~~
* The system tried to send notifications for done background tasks
in addition to tasks started in the foreground
* 2FA via messages or phone calls didn't work after a faulty dependency
update
* [Dev] Site reference on extensible models can no longer cause name clashes
because of its related name
Removed
~~~~~~~
* iCal feed URLs for birthdays (will be reintroduced later)
* [Dev] Django debug toolbar
* It caused major performance issues and is not useful with the new
frontend anymore
`2.12.3`_ - 2023-03-07
----------------------
Fixed
~~~~~
* The permission check for the dashboard edit page failed when the user had no person assigned.
* OIDC scope "phone" had no claims.
* AlekSIS groups were not synced to Django groups on registration of existing persons
* Invitations for existing short name did not work.
* Invitations for persons without pre-defined e-mail address did not behave correctly
`2.12.2`_ - 2022-12-18
----------------------
Fixed
~~~~~
* Incorporate SPDX license list for app licenses on About page because
spdx-license-list dependency vanished.
`2.12.1`_ - 2022-11-06
----------------------
Fixed
~~~~~
* An invalid backport caused OIDC clients without PKCD to fail.
`2.12`_ - 2022-11-04
--------------------
Added
~~~~~
* Show also group ownerships on person detail page
* [Dev] Provide plain PDF template without header/footer for special layouts.
* [Dev] Introduce support for reformattinga and linting JS, Vue, and CSS files.
Changed
~~~~~~~
* OIDC scope "profile" now exposes the avatar instead of the official photo
* Language selection on Vue pages now runs via GraphQL queries.
* [Dev] Provide function to generate PDF files from fully-rendered templates.
* [Dev] Accept pre-created file object for PDF generation to define
the redirect URL in advance.
Fixed
~~~~~
* The logo in the PDF files was displayed at the wrong position.
* Sometimes the PDF files were not generated correctly
and images were displayed only partially.
* Error message in permission form was misleading.
* Personal invites did not work
* Invite Person view threw an error when personal invites existed
* Detailed information for done Celery tasks weren't saved.
`2.11`_ - 2022-08-27
--------------------
This release sunsets the 2.x series of the AleKSIS core.
Deprecated
~~~~~~~~~~
* All frontends using Django views and Django templates are deprecated and support
for them will be removed in AlekSIS-Core 3.0. All frontend code must be written in
Vue.js and be properly separated from the backend. In the same spirit, all backend
features must expose GraphQL APIs for the frontend to use.
Added
~~~~~
The following features are introduced here mainly to simplify gradual
updates. GraphQL and the Vuetify/Vue.js frontend mechanisms are preview
functionality and app developers should not rely on them before AlekSIS-Core
3.0.
* Introduce GraphQL API and Vue.js frontend implementation
* Introduce webpack bundling for frontend code
`2.10.2`_ - 2022-08-25
----------------------
Fixed
~~~~~
* Celery's logging did not honour Django's logging level
* Automatically clean up expired OAuth tokens after 24 hourse
`2.10.1`_ - 2022-07-24
----------------------
Changed
~~~~~~~
* Make External Link Widget icons clickable
Fixed
~~~~~
* The progress page for background tasks didn't show all status messages.
`2.10`_ - 2022-06-25
--------------------
Added
~~~~~
* Add Ukrainian locale (contributed by Sergiy Gorichenko from Fre(i)e Software GmbH).
* Add third gender to gender choices
* Add DataCheck to validate specific fields of specific models
Changed
~~~~~~~
* Restructure group page and show more information about members.
* django-two-factor-auth >= 1.14.0 is now required due to a
backwards-incompatible breakage in that library
Fixed
~~~~~~~
* Password change view did not redirect to login when accessed unauthenticated.
* Sorting icons were inlined into stylesheet
* iOS devices used the favicon instead of the PWA icon when the PWA was added to the home screen.
Changed
~~~~~~~
* Update icon choices for models to new icon set
`2.9`_ - 2022-05-25
-------------------
Added
~~~~~
* Allow to disable exception mails to admins
* Add possibility to create iCal feeds in all apps and dynamically create user-specific urls.
Fixed
~~~~~
* The menu button used to be displayed twice on smaller screens.
* The icons were loaded from external servers instead from local server.
* Weekdays were not translated if system locales were missing
* Added locales-all to base image and note to docs
* The icons in the account menu were still the old ones.
* Due to a merge error, the once removed account menu in the sidenav appeared again.
* Scheduled notifications were shown on dashboard before time.
* Remove broken notifications menu item in favor of item next to account menu.
* Serve OAuth discovery information under root of domain
* [OAuth2] Resources which are protected with client credentials
allowed access if no scopes were allowed (CVE-2022-29773).
* The site logo could overlap with the menu for logos with an unexpected aspect ratio.
* Some OAuth2 views stopped working with long scope names.
* Resetting password was impossible due to a missing rule
* Language selection was broken when only one language was enabled in
preferences.
Removed
~~~~~~~
* Remove option to limit available languages in preferences.
Changed
~~~~~~~
* [Dev] ActionForm now checks permissions on objects before executing
* [Dev] ActionForm now returns a proper return value from the executed action
* Pin version of javascript dependencies
`2.8.1`_ - 2022-03-13
--------------------
Changed
~~~~~~~
* Official apps can now override any setting
`2.8`_ - 2022-03-11
-------------------
Added
~~~~~
* Add iconify icons
* Use identicons where avatars are missing.
* Display personal photos instead of avatars based on a site preference.
* Add an account menu in the top navbar.
* Create a reusable snippet for avatar content.
* Allow to configure if additional field is required
* Allow to configure description of additional fields
* Allow configuring regex for allowed usernames
* [Dev] Support scheduled notifications.
* Implement StaticContentWidget
* Allow to enable password change independently of password reset
Changed
~~~~~~~
* Added a `Retry` button to the server error page
Fixed
~~~~~
* The user handbook was lacking images and instructions on PWA usage with the Safari browser.
* The ``reset password`` button on the login site used to overflow the card on smaller devices.
Deprecated
~~~~~~~~~~
* Legacy material icon font will be removed in AlekSIS-Core 3.0
`2.7.4`_ - 2022-02-09
---------------------
Changed
~~~~~~~
* Allow disabling query caching with cachalot
* Add invitation key to success message when a person without e-mail address is invited by id
Fixed
~~~~~
* Only exactly one person without e-mail address could be invited
* No person was created and linked to the PersonInvitation object when invite by e-mail is used
* No valid data in the second e-mail field of the signup form when it was disabled
* Invitation options were displayed to superusers even when the feature was disabled
* Inviting newly created persons for registration failed
* Invited person was not displayed correctly in list of sent invitations
* [Docker] Do not clear cache in migration container die to session invalidation issues
* Notification email about user changes was broken
* SQL cache invalidation could fail when hitting OOT database
`2.7.3`_ - 2022-02-03
---------------------
Fixed
~~~~~
* Migration added in 2.7.2 did not work in all scenarios
* [Dev] Field change tracking API for Person was broken in 2.7.2
* [OAuth] Automatic clean-up of expired OAuth tokens could fail
* Allow maskable icons for non-masked use
* Add missing documentation
Known issues
~~~~~~~~~~~~
* Maskable and non-masked icons *purpose) any cannot be separated
`2.7.2`_ - 2022-01-31
---------------------
Changed
~~~~~~~
* [Dev] The (undocumented) setting PDF_CONTEXT_PROCESSORS is now named NON_REQUEST_CONTEXT_PROCESSORS
* [Docker] Cache is now cleared if migrations are applied
* Update German translations.
Fixed
~~~~~
* Celery progress could be inaccurate if recording progress during a transaction
`2.7.1`_ - 2022-01-28
---------------------
Changed
~~~~~~~
* PWA icons can now be marked maskable
* [OAuth] Expired tokens are now cleared in a periodic task
* PDF file jobs are now automatically expired
* Data checks are now scheduled every 15 minutes by default
Fixed
~~~~~
* PDF generation failed with S3 storage due to incompatibility with boto3
* PWA theme colour defaulted to red
* Form for editing group type displayed irrelevant fields
* Permission groups could get outdated if re-assigning a user account to a different person
* User preferences didn't work correctly sometimes due to race conditions.
`2.7`_ - 2022-01-24
-------------------
Added
~~~~~
* Periodic tasks can now have a default schedule, which is automatically created
Fixed
~~~~~
* Signup was forbidden even if it was enabled in settings
* Phone numbers were not properly linked and suboptimally formatted on person page
* Favicon upload failed with S3 storage.
* Some combinations of allowed self-edit fields on persons could cause errors
* Some preferences were required when they shouldn't, and vice versa.
* IO errors on accessing backup directory in health check are now properly reported
* Date picker was not properly initialized if field was already filled.
* The menu item for entering an invitation code received offline was missing
* CleaveJS was not loaded properly when using an external CDN
Changed
-------
* Allow non-superusers with permission to invite persons
`2.6`_ - 2022-01-10
-------------------
Added
~~~~~
* Add option to open entry in new tab for sidebar navigation menu.
* Add preference for configuring the default phone number country code.
* Persons and groups now have two image fields: official photo and public avatar
* Admins recieve an mail for celery tasks with status "FAILURE"
* OpenID Connect RSA keys can now be passed as string in config files
* Views filtering for person names now also search the username of a linked user
* OAuth2 applications now take an icon which is shown in the authorization progress.
* Add support for hiding the main side nav in ``base.html``.
* Provide base template and function for sending emails with a template.
Fixed
~~~~~
* Changing the favicon did not result in all icons being replaced in some cases
* Superusers with a dummy person were able to access the dashboard edit page.
* GroupManager.get_queryset() returned an incomplete QuerySet
* OAuth was broken by a non-semver-adhering django-oauth-toolkit update
* Too long texts in chips didn't result in a larger chip.
* The ``Person`` model had an ``is_active`` flag that was used in unclear ways; it is now removed
* The data check results list view didn't work if a related object had been deleted in the meanwhile.
* Socialaccount login template was not overriden
* Atomic transactions now cause only one Haystack update task to run
* Too long headlines didn't break in another line.
Changed
~~~~~~~
* Configuration files are now deep merged by default
* Improvements for shell_plus module loading
* core.Group model now takes precedence over auth.Group
* Name collisions are resolved by prefixing with the app label
* Apps can extend SHELL_PLUS_APP_PREFIXES and SHELL_PLUS_DONT_LOAD
* [Docker] Base image now contains curl, grep, less, sed, and pspg
* Views raising a 404 error can now customise the message that is displayed on the error page
* OpenID Connect is enabled by default now, without RSA support
* Login and authorization pages for OAuth2/OpenID Connect now indicate that the user is in progress
to authorize an external application.
* Tables can be scrolled horizontally.
* Overhauled person detail page
* Use common base template for all emails.
`2.5`_ – 2022-01-02
-------------------
Added
~~~~~
* Recursive helper methods for group hierarchies
Fixed
~~~~~
* Remove left-over reference to preferences in a form definition that caused
form extensions in downstream apps to break
* Allow non-LDAP users to authenticate if LDAP is used with password handling
* Additional button on progress page for background tasks was shown even if the task failed.
* Register preference for available allowed oauth grants.
`2.4`_ – 2021-12-24
-------------------
Added
~~~~~
* Allow configuration of database options
* User invitations with invite codes and targeted invites for existing
persons
Fixed
~~~~~
* Correctly update theme colours on change again
* Use correct favicon as default AlekSIS favicon
* Show all years in a 200 year range around the current year in date pickers
* Imprint is now called "Imprint" and not "Impress".
* Logo files weren't uploaded to public namespace.
* Limit LDAP network timeouts to not hang indefinitely on login if LDAP
server is unreachable
Changed
~~~~~~~
* Modified the appearance of tables for mobile users to be more user friendly
* [Dev] Remove lock file; locking dependencies is the distribution's
responsibility
Removed
~~~~~~~
* Remove old generated AlekSIS icons
`2.3.1`_ – 2021-12-17
---------------------
Fixed
~~~~~
* Small files could fail to upload to S3 storage due to MemoryFileUploadHandler
* Corrected typos in previous changelog
`2.3`_ – 2021-12-15
-------------------
Added
~~~~~
* [OAuth] Allow apps to fill in their own claim data matching their scopes
Fixed
~~~~~
* View for assigning permissions didn't work with some global permissions.
* PDFs generated in background didn't contain logo or site title.
* Admins were redirected to their user preferences
while they wanted to edit the preferences of another user.
* Some CharFields were using NULL values in database when field is empty
* Optional dependecy `sentry-sdk` was not optional
Changed
~~~~~~~
* Docker base image ships PostgreSQL 14 client binaries for maximum compatibility
* Docker base image contains Sentry client by default (disabled in config by default)
Removed
~~~~~~~
* Remove impersonation page. Use the impersonation button on the person
detail view instead.
`2.2.1`_ – 2021-12-02
--------------------
Fixed
~~~~~
* [Docker] Stop initialisation if migrations fail
* [OAuth] Register `groups` scope and fix claim
* [OAuth] Fix OAuth claims for follow-up requests (e.g. UserInfo)
* [OAuth] Fix grant types checking failing on wrong types under some circumstances
* [OAuth] Re-introduce missing algorithm field in application form
* Remove errornous backup folder check for S3
`2.2`_ - 2021-11-29
-------------------
Added
~~~~~
* Support config files in sub-directories
* Provide views for assigning/managing permissions in frontend
* Support (icon) tabs in the top navbar.
Changed
~~~~~~~
* Update German translations.
Fixed
~~~~~
* Use new MaterializeCSS fork because the old version is no longer maintained.
* Sender wasn't displayed for notifications on dashboard.
* Notifications and activities on dashboard weren't sorted from old to new.
`2.1.1`_ - 2021-11-14
---------------------
Added
~~~~~
* Provide ``SITE_PREFERENCES`` template variable for easier and request-independent access on all site preferences.
Fixed
~~~~~
* Make style.css and favicons cachable.
* Import model extensions from other apps before form extensions.
* Recreate backwards compatiblity for OAuth URLs by using ``oauth/`` again.
* Show correct logo and school title in print template if created in the background.
Removed
~~~~~~~
* Remove fallback code from optional Celery as it's now non-optional.
`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
----------------------
......@@ -379,18 +1491,63 @@ Fixed
.. _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
.. _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
.. _1.0a1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/1.0a1
.. _1.0a2: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/1.0a2
.. _1.0a4: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/1.0a4
.. _2.0a1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.0a1
.. _2.0a2: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.0a2
.. _2.0b0: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.0b0
.. _2.0b1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.0b1
.. _2.0b2: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.0b2
.. _2.0rc1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.0rc1
.. _2.0rc2: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.0rc2
.. _2.0rc3: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.0rc3
.. _2.0rc4: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.0rc4
.. _2.0rc5: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.0rc5
.. _2.0rc6: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.0rc6
.. _2.0rc7: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.0rc7
.. _2.0: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.0
.. _2.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.1
.. _2.1.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.1.1
.. _2.2: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.2
.. _2.2.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.2.1
.. _2.3: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.3
.. _2.3.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.3.1
.. _2.4: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.4
.. _2.5: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.5
.. _2.6: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.6
.. _2.7: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.7
.. _2.7.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.7.1
.. _2.7.2: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.7.2
.. _2.7.3: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.7.3
.. _2.7.4: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.7.4
.. _2.8: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.8
.. _2.8.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.8.1
.. _2.9: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.9
.. _2.10: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.10
.. _2.10.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.10.1
.. _2.10.2: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.10.2
.. _2.11: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.11
.. _2.11.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.11.1
.. _2.12: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.12
.. _2.12.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.12.1
.. _2.12.2: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.12.2
.. _2.12.3: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.12.3
.. _3.0b0: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.0b0
.. _3.0b1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.0b1
.. _3.0b2: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.0b2
.. _3.0b3: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.0b3
.. _3.0: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.0
.. _3.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.1
.. _3.1.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.1.1
.. _3.1.2: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.1.2
.. _3.1.3: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.1.3
.. _3.1.4: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.1.4
.. _3.1.5: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.1.5
.. _3.1.6: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.1.6
.. _3.1.7: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.1.7
.. _3.2.0: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.2.0
.. _3.2.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.2.1
.. _3.2.2: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.2.2
.. _4.0.0: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/4.0.0
.. _4.0.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/4.0.1
FROM debian:bullseye-slim AS core
FROM debian:bookworm-slim AS core
# Build arguments
ARG EXTRAS="ldap,s3"
ARG APP_VERSION=""
ARG EXTRAS="ldap,s3,sentry"
ARG APP_VERSION="==3.0b0"
ARG PIP_EXTRA_INDEX_URL="https://edugit.org/api/v4/projects/461/packages/pypi/simple"
# Configure Python to be nice inside Docker and pip to stfu
ENV PYTHONUNBUFFERED 1
......@@ -10,35 +11,44 @@ ENV PYTHONDONTWRITEBYTECODE 1
ENV PIP_DEFAULT_TIMEOUT 100
ENV PIP_DISABLE_PIP_VERSION_CHECK 1
ENV PIP_NO_CACHE_DIR 1
ENV PIP_EXTRA_INDEX_URL https://edugit.org/api/v4/projects/461/packages/pypi/simple
ENV PIP_EXTRA_INDEX_URL $PIP_EXTRA_INDEX_URL
ENV PIP_USE_DEPRECATED legacy-resolver
ENV DEBIAN_FRONTEND noninteractive
# Configure app settings for build and runtime
ENV ALEKSIS_caching__dir /var/cache/aleksis
ENV ALEKSIS_static__root /usr/share/aleksis/static
ENV ALEKSIS_media__root /var/lib/aleksis/media
ENV ALEKSIS_backup__location /var/lib/aleksis/backups
ENV ALEKSIS_dev__uwsgi__celery false
ENV PSQL_PAGER=pspg
# Install necessary Debian and PyPI packages for build and runtime
RUN apt-get -y update && \
apt-get -y install eatmydata && \
apt-get -y install eatmydata gnupg postgresql-common && \
/usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y && \
eatmydata apt-get -y upgrade && \
eatmydata apt-get install -y --no-install-recommends \
build-essential \
chromium \
curl \
dumb-init \
gettext \
libpq5 \
grep \
less \
libpq-dev \
libssl-dev \
postgresql-client \
locales-all \
postgresql-client-14 \
postgresql-client-15 \
postgresql-client-16 \
pspg \
python3-dev \
python3-magic \
python3-pip \
uwsgi \
uwsgi-plugin-python3 \
yarnpkg
yarnpkg \
git
# Install extra dependencies
RUN case ",$EXTRAS," in \
......@@ -52,23 +62,28 @@ RUN case ",$EXTRAS," in \
# Install core
RUN set -e; \
mkdir -p ${ALEKSIS_static__root} \
mkdir -p ${ALEKSIS_caching__dir} \
${ALEKSIS_static__root} \
${ALEKSIS_media__root} \
${ALEKSIS_backup__location}; \
dpkg-divert --rename --add /usr/lib/$(py3versions -d)/EXTERNALLY-MANAGED; \
eatmydata pip install AlekSIS-Core\[$EXTRAS\]$APP_VERSION
# Define entrypoint, volumes and uWSGI running on port 8000
EXPOSE 8000
VOLUME ${ALEKSIS_media__root} ${ALEKSIS_backup__location}
COPY docker-startup.sh /usr/local/bin/aleksis-docker-startup
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
CMD ["/usr/local/bin/aleksis-docker-startup"]
# Install assets
FROM core as assets
RUN eatmydata aleksis-admin yarn install; \
RUN eatmydata aleksis-admin vite build; \
eatmydata aleksis-admin compile_scss; \
eatmydata aleksis-admin collectstatic --no-input; \
rm -rf /usr/local/share/.cache
# FIXME Introduce deletion after we don't need materializecss anymore for SASS
# also in ONBUILD below
# rm -rf /usr/local/share/.cache ${ALEKSIS_caching__dir}/*
# Clean up build dependencies
FROM assets AS clean
......@@ -76,6 +91,7 @@ RUN set -e; \
eatmydata apt-get remove --purge -y \
build-essential \
gettext \
gnupg \
libpq-dev \
libssl-dev \
libldap2-dev \
......@@ -92,6 +108,7 @@ RUN chown -R www-data:www-data \
${ALEKSIS_media__root} \
${ALEKSIS_backup__location}
USER 33:33
VOLUME ${ALEKSIS_media__root} ${ALEKSIS_backup__location}
# Additional steps
ONBUILD ARG APPS
......@@ -111,8 +128,9 @@ ONBUILD RUN set -e; \
if [ -n "$APPS" ]; then \
eatmydata pip install $APPS; \
fi; \
eatmydata aleksis-admin yarn install; \
eatmydata aleksis-admin collectstatic --no-input; \
eatmydata aleksis-admin vite build; \
eatmydata aleksis-admin compile_scss; \
eatmydata aleksis-admin collectstatic --no-input --clear; \
rm -rf /usr/local/share/.cache; \
eatmydata apt-get remove --purge -y yarnpkg $BUILD_DEPS; \
eatmydata apt-get autoremove --purge -y; \
......
......@@ -121,7 +121,7 @@ been modified and the date of modification.
of the Original Works or Derivative Works, this Distribution or
Communication will be done under the terms of this Licence or of a
later version of this Licence unless the Original Work is expressly
distributed only under this version of the Licencefor example by
distributed only under this version of the Licencefor example by
communicating ‘EUPL v. 1.2 only’. The Licensee (becoming Licensor)
cannot offer or impose any additional terms or conditions on the Work
or Derivative Work that alter or restrict the terms of the Licence.
......@@ -306,7 +306,7 @@ Appendix
* Creative Commons Attribution-ShareAlike v. 3.0 Unported
(CC BY-SA 3.0) for works other than software
* European Union Public Licence (EUPL) v. 1.1, v. 1.2
* Québec Free and Open-Source LicenceReciprocity (LiLiQ-R)
* Québec Free and Open-Source LicenceReciprocity (LiLiQ-R)
or Strong Reciprocity (LiLiQ-R+)
The European Commission may update this Appendix to later versions of
......
......@@ -6,7 +6,7 @@ This is the core of the AlekSIS framework and the official distribution
developers and administrators.
If you are looking for the AlekSIS standard distribution, i.e. the complete
software product ready for installation and usage, please visit the `AlekSIS`_
software product ready for installation and usage, please visit the `AlekSIS®`_
website or the distribution repository on `EduGit`_.
Features
......@@ -16,58 +16,68 @@ 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
* Authentication via local account, LDAP, or social accounts
* Two factor authentication via WebAuthn, OTP, or SMS
* Configurable dashboard with widgets
* User-specific preferences
* Global search
* Group types
* Manage announcements
* Manage groups
* Global calendar system
* CalDAV and CardDAV support
* Manage personal events
* Manage persons
* Notifications via SMS email or dashboard
* Manage groups and group types
* Manage roles per group
* Manage announcements
* Manage holidays
* 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
* User invitations with invite codes and targeted invites
* For admins
* Asynchronous tasks with celery
* Authentication via LDAP
* `aleksis-admin` script to wrap django-admin with pre-configured settings
* Manage school terms
* Custom menu entries (e.g. in footer)
* 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
* Configuration of low-level settings via configuration files
* System-wide preferenes
* Creating dashboard widgets for external links/apps
* For developers
* `aleksis-admin` script to wrap django-admin with pre-configured settings
* Caching with Redis
* Generic PDF generation with firefox
* Caching with Valkey
* Django REST framework for apps to use at own discretion
* Injection of fields, methods, permissions and properties via custom `ExtensibleModel`
* 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
* Object-level permissions and rules with ``django-guardian`` and ``django-rules``
* uWSGI and Celery via ``django-uwsgi`` in development
* Extensible dashbaord widget system
* Extensible calendar system
* Extensible OAuth/OpenID Connect scope and claims system
Licence
-------
::
Copyright © 2017, 2018, 2019, 2020, 2021 Jonathan Weth <dev@jonathanweth.de>
Copyright © 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024 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 © 2018, 2019, 2020, 2021, 2022, 2023, 2024 Hangzhi Yu <yuha@katharineum.de>
Copyright © 2018, 2019, 2020, 2021, 2022, 2023, 2024 Julian Leucker <leuckeju@katharineum.de>
Copyright © 2019, 2020, 2021, 2022, 2023, 2024, 2025 Dominik George <dominik.george@teckids.org>
Copyright © 2019, 2020, 2021, 2022 Tom Teichler <tom.teichler@teckids.org>
Copyright © 2019 mirabilos <thorsten.glaser@teckids.org>
Copyright © 2021, 2022, 2023, 2024 magicfelix <felix@felix-zauberer.de>
Copyright © 2021 Lloyd Meins <meinsll@katharineum.de>
Copyright © 2021 magicfelix <felix@felix-zauberer.de>
Copyright © 2022 Benedict Suska <benedict.suska@teckids.org>
Copyright © 2022, 2023, 2024 Lukas Weichelt <lukas.weichelt@teckids.org>
Copyright © 2023, 2024 Michael Bauer <michael-bauer@posteo.de>
Copyright © 2024 Jonathan Krüger <jonathan.krueger@teckids.org>
Licenced under the EUPL, version 1.2 or later, by Teckids e.V. (Bonn, Germany).
......@@ -76,6 +86,14 @@ 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://aleksis.org
Trademark
---------
AlekSIS® is a registered trademark of the AlekSIS open source project, represented
by Teckids e.V. Please refer to the `trademark policy`_ for hints on using the trademark
AlekSIS®.
.. _AlekSIS®: https://aleksis.org
.. _European Union Public Licence: https://eupl.eu/
.. _EduGit: https://edugit.org/AlekSIS/official/AlekSIS
.. _trademark policy: https://aleksis.org/pages/about
from importlib import metadata
try:
from .celery import app as celery_app
except ModuleNotFoundError:
# Celery is not available
celery_app = None
from .celery import app as celery_app # noqa
try:
__version__ = metadata.distribution("AlekSIS-Core").version
except Exception:
__version__ = "unknown"
default_app_config = "aleksis.core.apps.CoreConfig"
# noqa
from django.contrib import admin
from guardian.admin import GuardedModelAdminMixin
from reversion.admin import VersionAdmin
from .mixins import BaseModelAdmin
from .models import (
Activity,
Announcement,
AnnouncementRecipient,
CustomMenuItem,
DataCheckResult,
Group,
Notification,
Person,
)
admin.site.register(Activity, VersionAdmin)
admin.site.register(Notification, VersionAdmin)
admin.site.register(CustomMenuItem, VersionAdmin)
class AnnouncementRecipientInline(admin.StackedInline):
model = AnnouncementRecipient
class AnnouncementAdmin(BaseModelAdmin, VersionAdmin):
inlines = [
AnnouncementRecipientInline,
]
class GuardedVersionAdmin(GuardedModelAdminMixin, VersionAdmin):
pass
admin.site.register(Announcement, AnnouncementAdmin)
admin.site.register(DataCheckResult)
admin.site.register(Person, GuardedVersionAdmin)
admin.site.register(Group, GuardedVersionAdmin)
from typing import Any, Optional
from typing import TYPE_CHECKING, Any, Optional
import django.apps
from django.apps import apps
from django.conf import settings
from django.contrib import messages
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
from oauthlib.common import Request as OauthlibRequest
from .registries import (
group_preferences_registry,
person_preferences_registry,
site_preferences_registry,
)
from .registries import group_preferences_registry, person_preferences_registry
from .util.apps import AppConfig
from .util.core_helpers import get_or_create_favicon, has_person
from .util.sass_helpers import clean_scss
from .util.core_helpers import (
create_default_celery_schedule,
get_or_create_favicon,
get_site_preferences,
has_person,
)
from .util.types import setup_types
if TYPE_CHECKING:
from django.contrib.auth.models import User
class CoreConfig(AppConfig):
......@@ -30,35 +35,45 @@ class CoreConfig(AppConfig):
}
licence = "EUPL-1.2+"
copyright_info = (
([2017, 2018, 2019, 2020, 2021], "Jonathan Weth", "wethjo@katharineum.de"),
(
[2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024],
"Jonathan Weth",
"dev@jonathanweth.de",
),
([2017, 2018, 2019, 2020], "Frank Poetzsch-Heffter", "p-h@katharineum.de"),
([2018, 2019, 2020, 2021], "Julian Leucker", "leuckeju@katharineum.de"),
([2018, 2019, 2020, 2021], "Hangzhi Yu", "yuha@katharineum.de"),
([2019, 2020, 2021], "Dominik George", "dominik.george@teckids.org"),
([2019, 2020, 2021], "Tom Teichler", "tom.teichler@teckids.org"),
([2018, 2019, 2020, 2021, 2022, 2023, 2024], "Hangzhi Yu", "yuha@katharineum.de"),
([2018, 2019, 2020, 2021, 2022, 2023, 2024], "Julian Leucker", "leuckeju@katharineum.de"),
(
[2019, 2020, 2021, 2022, 2023, 2024, 2025],
"Dominik George",
"dominik.george@teckids.org",
),
([2019, 2020, 2021, 2022], "Tom Teichler", "tom.teichler@teckids.org"),
([2019], "mirabilos", "thorsten.glaser@teckids.org"),
([2021, 2022, 2023, 2024], "magicfelix", "felix@felix-zauberer.de"),
([2021], "Lloyd Meins", "meinsll@katharineum.de"),
([2021], "magicfelix", "felix@felix-zauberer.de"),
([2022], "Benedict Suska", "benedict.suska@teckids.org"),
([2022, 2023, 2024], "Lukas Weichelt", "lukas.weichelt@teckids.org"),
([2023, 2024], "Michael Bauer", "michael-bauer@posteo.de"),
([2024], "Jonathan Krüger", "jonathan.krueger@teckids.org"),
)
def ready(self):
super().ready()
setup_types()
from django.conf import settings # noqa
# Autodiscover various modules defined by AlekSIS
autodiscover_modules("form_extensions", "model_extensions", "checks")
autodiscover_modules("model_extensions", "form_extensions", "checks", "util.dav_handler")
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)
self._load_data_checks()
from .health_checks import (
BackupJobHealthCheck,
DataChecksHealthCheckBackend,
......@@ -71,16 +86,6 @@ class CoreConfig(AppConfig):
plugin_dir.register(MediaBackupAgeHealthCheck)
plugin_dir.register(BackupJobHealthCheck)
@classmethod
def _load_data_checks(cls):
"""Get all data checks from all loaded models."""
from aleksis.core.data_checks import DataCheckRegistry
data_checks = set()
for model in apps.get_models():
data_checks.update(getattr(model, "data_checks", []))
DataCheckRegistry.data_checks = data_checks
def preference_updated(
self,
sender: Any,
......@@ -92,24 +97,43 @@ class CoreConfig(AppConfig):
) -> None:
from django.conf import settings # noqa
if section == "theme":
if name in ("primary", "secondary"):
clean_scss()
elif name in ("favicon", "pwa_icon"):
from favicon.models import Favicon # noqa
if section == "theme" and name in ("favicon", "pwa_icon"):
from favicon.models import Favicon, FaviconImg # noqa
is_favicon = name == "favicon"
if new_value:
# Get file object from preferences instead of using new_value
# to prevent problems with special file storages
file_obj = get_site_preferences()[f"{section}__{name}"]
favicon = Favicon.objects.update_or_create(
title=name,
defaults={"isFavicon": is_favicon, "faviconImage": file_obj},
)[0]
FaviconImg.objects.filter(faviconFK=favicon).delete()
else:
Favicon.objects.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
)
is_favicon = name == "favicon"
def pre_migrate(
self,
app_config: django.apps.AppConfig,
verbosity: int,
interactive: bool,
using: str,
plan: list[tuple],
apps: django.apps.registry.Apps,
**kwargs,
) -> None:
super().pre_migrate(app_config, verbosity, interactive, using, plan, apps)
from .data_checks import check_data_for_migrations
if new_value:
Favicon.on_site.update_or_create(
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
)
# Run data checks to validate data
check_data_for_migrations(with_dependencies=True)
def post_migrate(
self,
......@@ -120,18 +144,20 @@ class CoreConfig(AppConfig):
**kwargs,
) -> None:
from django.conf import settings # noqa
from .data_checks import check_data_for_migrations
super().post_migrate(app_config, verbosity, interactive, using, **kwargs)
# 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": ""}
)
# 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")
# Create default periodic tasks
create_default_celery_schedule()
# Run data checks to validate data
check_data_for_migrations()
def user_logged_in(
self, sender: type, request: Optional[HttpRequest], user: "User", **kwargs
) -> None:
......@@ -139,6 +165,11 @@ class CoreConfig(AppConfig):
# Save the associated person to pick up defaults
user.person.save()
def user_logged_out(
self, sender: type, request: Optional[HttpRequest], user: "User", **kwargs
) -> None:
messages.success(request, _("You have been logged out successfully."))
@classmethod
def get_all_scopes(cls) -> dict[str, str]:
scopes = {
......@@ -149,8 +180,60 @@ class CoreConfig(AppConfig):
scopes |= {
"openid": _("OpenID Connect scope"),
"profile": _("Given name, family name, link to profile and picture if existing."),
"address": _("Full home postal address"),
"addresses": _("Postal addresses"),
"email": _("Email address"),
"phone": _("Home and mobile phone"),
"groups": _("Groups"),
}
return scopes
@classmethod
def get_additional_claims(cls, scopes: list[str], request: OauthlibRequest) -> dict[str, Any]:
django_request = HttpRequest()
django_request.META = request.headers
claims = {
"preferred_username": request.user.username,
}
if "profile" in scopes:
if has_person(request.user):
claims["given_name"] = request.user.person.first_name
claims["family_name"] = request.user.person.last_name
claims["profile"] = django_request.build_absolute_uri(
request.user.person.get_absolute_url()
)
if request.user.person.avatar:
claims["picture"] = django_request.build_absolute_uri(
request.user.person.avatar.url
)
else:
claims["given_name"] = request.user.first_name
claims["family_name"] = request.user.last_name
if "email" in scopes:
if has_person(request.user):
claims["email"] = request.user.person.email
else:
claims["email"] = request.user.email
if "addresses" in scopes and has_person(request.user):
claims["addresses"] = [
{
"street_address": address.street + " " + address.housenumber,
"locality": address.place,
"postal_code": address.postal_code,
}
for address in request.user.person.addresses.all()
]
if "phone" in scopes and has_person(request.user):
claims["mobile_number"] = request.user.person.mobile_number
claims["phone_number"] = request.user.person.phone_number
if "groups" in scopes and has_person(request.user):
claims["groups"] = list(
request.user.person.member_of.values_list("name", flat=True).all()
)
return claims
import logging
import os
from traceback import format_exception
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from aleksis.core.models import TaskUserAssignment
from django.conf import settings
from django.db import transaction
from celery import Celery
from celery.contrib.django.task import DjangoTask
from celery.signals import setup_logging, task_failure
from .util.core_helpers import get_site_preferences
from .util.email import send_email
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "aleksis.core.settings")
app = Celery("aleksis") # noqa
class CeleryTask(DjangoTask):
"""Custom Task object for progress tracking."""
def delay_with_progress(
self, user_assignment: "TaskUserAssignment", *args, **kwargs
) -> "TaskUserAssignment":
"""Start task and track the progress."""
user_assignment.save()
def _inner():
task_result = self.delay(*args, **kwargs)
user_assignment.task_id = task_result.id
user_assignment.save()
transaction.on_commit(_inner)
return user_assignment
app = Celery("aleksis", task_cls=CeleryTask) # noqa
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()
@task_failure.connect
def task_failure_notifier(
sender=None, task_id=None, exception=None, args=None, kwargs=None, traceback=None, **__
):
recipient_list = [e[1] for e in settings.ADMINS]
send_email(
template_name="celery_failure",
from_email=get_site_preferences()["mail__address"],
recipient_list=recipient_list,
context={
"task_name": sender.name,
"task": str(sender),
"task_id": str(task_id),
"exception": str(exception),
"args": args,
"kwargs": kwargs,
"traceback": "".join(format_exception(type(exception), exception, traceback)),
},
)
@setup_logging.connect
def on_setup_logging(*args, **kwargs):
"""Load Django's logging configuration when running inside Celery."""
logging.config.dictConfig(settings.LOGGING)
from collections.abc import Iterable
from typing import Optional
import django.apps
from django.core.checks import Tags, Warning, register
from django.core.checks import Error, Tags, Warning, register # noqa
from .mixins import ExtensibleModel, GlobalPermissionModel, PureDjangoModel
from .schema.base import BaseBatchCreateMutation, BaseBatchDeleteMutation, BaseBatchPatchMutation
from .util.apps import AppConfig
......@@ -71,3 +73,28 @@ def check_app_models_base_class(
)
return results
@register(Tags.security)
def check_all_mutations_with_permissions(
app_configs: Optional[django.apps.registry.Apps] = None, **kwargs
) -> list:
results = []
for base_class in [BaseBatchCreateMutation, BaseBatchPatchMutation, BaseBatchDeleteMutation]:
for subclass in base_class.__subclasses__():
if (
not isinstance(subclass._meta.permissions, Iterable)
or not subclass._meta.permissions
):
results.append(
Error(
f"Mutation {subclass.__name__} doesn't set required permission",
hint=(
"Ensure that the mutation is protected by setting the "
"permissions attribute in the mutation's Meta class."
),
obj=subclass,
id="aleksis.core.E001",
)
)
return results
import logging
from datetime import timedelta
from typing import TYPE_CHECKING
from django.apps import apps
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import connections
from django.db.migrations.recorder import MigrationRecorder
from django.db.models import Model
from django.db.models.aggregates import Count
from django.utils.functional import classproperty
from django.utils.text import slugify
from django.utils.translation import gettext as _
import reversion
from color_contrast import AccessibilityLevel, ModulationMode, check_contrast, modulate
from reversion import set_comment
from templated_email import send_templated_mail
from .mixins import RegistryObject
from .util.celery_progress import ProgressRecorder, recorded_task
from .util.core_helpers import get_site_preferences
from .util.email import send_email
if TYPE_CHECKING:
from aleksis.core.models import DataCheckResult
class SolveOption:
......@@ -28,7 +40,7 @@ class SolveOption:
from django.utils.translation import gettext as _
class DeleteSolveOption(SolveOption):
name = "delete" # has to be unqiue
_class_name = "delete" # has to be unqiue
verbose_name = _("Delete") # should make use of i18n
@classmethod
......@@ -40,7 +52,7 @@ class SolveOption:
the corresponding data check result has to be deleted.
"""
name: str = "default"
_class_name: str = "default"
verbose_name: str = ""
@classmethod
......@@ -51,7 +63,7 @@ class SolveOption:
class IgnoreSolveOption(SolveOption):
"""Mark the object with data issues as solved."""
name = "ignore"
_class_name = "ignore"
verbose_name = _("Ignore problem")
@classmethod
......@@ -61,7 +73,7 @@ class IgnoreSolveOption(SolveOption):
check_result.save()
class DataCheck:
class DataCheck(RegistryObject):
"""Define a data check.
Data checks should be used to search objects of
......@@ -83,12 +95,13 @@ class DataCheck:
from django.utils.translation import gettext as _
class ExampleDataCheck(DataCheck):
name = "example" # has to be unique
_class_name = "example" # has to be unique
verbose_name = _("Ensure that there are no examples.")
problem_name = _("There is an example.") # should both make use of i18n
required_for_migrations = True # Make mandatory for migrations
solve_options = {
IgnoreSolveOption.name: IgnoreSolveOption
IgnoreSolveOption._class_name: IgnoreSolveOption
}
@classmethod
......@@ -119,7 +132,7 @@ class DataCheck:
The dictionary ``solve_options`` should include at least the IgnoreSolveOption,
but preferably also own solve options. The keys in this dictionary
have to be ``<YourOption>SolveOption.name``
have to be ``<YourOption>SolveOption._class_name``
and the values must be the corresponding solve option classes.
The class method ``check_data`` does the actual work. In this method
......@@ -151,11 +164,13 @@ class DataCheck:
the preference ``Send emails if data checks detect problems``.
""" # noqa: D412
name: str = ""
verbose_name: str = ""
problem_name: str = ""
solve_options = {IgnoreSolveOption.name: IgnoreSolveOption}
required_for_migrations: bool = False
migration_dependencies: list[str] = []
solve_options = {IgnoreSolveOption._class_name: IgnoreSolveOption}
_current_results = []
......@@ -187,6 +202,12 @@ class DataCheck:
)
solve_option_obj.solve(check_result)
@classmethod
def get_results(cls):
DataCheckResult = apps.get_model("core", "DataCheckResult")
return DataCheckResult.objects.filter(data_check=cls._class_name)
@classmethod
def register_result(cls, instance) -> "DataCheckResult":
"""Register an object with data issues in the result database.
......@@ -198,7 +219,9 @@ class DataCheck:
ct = ContentType.objects.get_for_model(instance)
result, __ = DataCheckResult.objects.get_or_create(
check=cls.name, content_type=ct, object_id=instance.id
data_check=cls._class_name,
content_type=ct,
object_id=instance.id if not isinstance(instance, int) else instance,
)
# Track all existing problems (for deleting old results)
......@@ -209,37 +232,26 @@ class DataCheck:
@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)
old_results = cls.get_results().exclude(pk__in=pks)
if old_results:
if old_results.exists():
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: set = set()
@classproperty
def data_checks_by_name(cls):
return {check.name: check for check in cls.data_checks}
@classproperty
def data_checks_choices(cls):
return [(check.name, check.verbose_name) for check in cls.data_checks]
return [(check._class_name, check.verbose_name) for check in cls.registered_objects_list]
@recorded_task
@recorded_task(run_every=timedelta(minutes=15))
def check_data(recorder: ProgressRecorder):
"""Execute all registered data checks and send email if activated."""
for check in recorder.iterate(DataCheckRegistry.data_checks):
for check in recorder.iterate(DataCheck.registered_objects_list):
logging.info(f"Run check: {check.verbose_name}")
check.run_check_data()
......@@ -247,6 +259,36 @@ def check_data(recorder: ProgressRecorder):
send_emails_for_data_checks()
def check_data_for_migrations(with_dependencies: bool = False):
"""Run data checks before/after migrations to ensure consistency of data."""
applied_migrations = set(MigrationRecorder(connections["default"]).applied_migrations())
for check in filter(lambda d: d.required_for_migrations, DataCheck.registered_objects_list):
check: DataCheck
if set(check.migration_dependencies).issubset(applied_migrations) or not with_dependencies:
logging.info(f"Run data check: {check.verbose_name}")
check.run_check_data()
# Show results
results = check.get_results().values("id")
for result in results:
result: DataCheckResult
logging.info(f"#{result['id']}")
if results and with_dependencies:
logging.error(
"There are unresolved data checks necessary for migrating. "
"Please resolve them as described in the documentation before migrating."
)
exit(1)
elif results:
logging.error(
"There are unresolved data checks. "
"Please check and resolve them in the web interface."
)
def send_emails_for_data_checks():
"""Notify one or more recipients about new problems with data.
......@@ -257,12 +299,12 @@ def send_emails_for_data_checks():
results = DataCheckResult.objects.filter(solved=False, sent=False)
if results.exists():
results_by_check = results.values("check").annotate(count=Count("check"))
results_by_check = results.values("data_check").annotate(count=Count("data_check"))
results_with_checks = []
for result in results_by_check:
results_with_checks.append(
(DataCheckRegistry.data_checks_by_name[result["check"]], result["count"])
(DataCheck.registered_objects_dict[result["data_check"]], result["count"])
)
recipient_list = [
......@@ -274,9 +316,8 @@ def send_emails_for_data_checks():
for group in get_site_preferences()["general__data_checks_recipient_groups"]:
recipient_list += [p.mail_sender for p in group.announcement_recipients if p.email]
send_templated_mail(
send_email(
template_name="data_checks",
from_email=get_site_preferences()["mail__address"],
recipient_list=recipient_list,
context={"results": results_with_checks},
)
......@@ -287,32 +328,303 @@ def send_emails_for_data_checks():
class DeactivateDashboardWidgetSolveOption(SolveOption):
name = "deactivate_dashboard_widget"
_class_name = "deactivate_dashboard_widget"
verbose_name = _("Deactivate DashboardWidget")
@classmethod
def solve(cls, check_result: "DataCheckResult"):
from .models import DashboardWidget
widget = check_result.related_object
widget.active = False
widget.status = DashboardWidget.Status.OFF.value
widget.save()
check_result.delete()
class BrokenDashboardWidgetDataCheck(DataCheck):
name = "broken_dashboard_widgets"
_class_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,
IgnoreSolveOption._class_name: IgnoreSolveOption,
DeactivateDashboardWidgetSolveOption._class_name: DeactivateDashboardWidgetSolveOption,
}
@classmethod
def check_data(cls):
from .models import DashboardWidget
broken_widgets = DashboardWidget.objects.filter(broken=True, active=True)
broken_widgets = DashboardWidget.objects.filter(status=DashboardWidget.Status.BROKEN.value)
for widget in broken_widgets:
logging.info("Check DashboardWidget %s", widget)
cls.register_result(widget)
def field_validation_data_check_factory(app_name: str, model_name: str, field_name: str) -> type:
from django.apps import apps
class FieldValidationDataCheck(DataCheck):
_class_name = f"field_validation_{slugify(model_name)}_{slugify(field_name)}"
verbose_name = _("Validate field {field} of model {model}.").format(
field=field_name, model=app_name + "." + model_name
)
problem_name = _("The field {} couldn't be validated successfully.").format(field_name)
solve_options = {
IgnoreSolveOption._class_name: IgnoreSolveOption,
}
@classmethod
def check_data(cls):
model: Model = apps.get_model(app_name, model_name)
for obj in model.objects.all():
try:
model._meta.get_field(field_name).validate(getattr(obj, field_name), obj)
except ValidationError:
logging.info(f"Check {model_name} {obj}")
cls.register_result(obj)
FieldValidationDataCheck.__name__ = model_name + "FieldValidationDataCheck"
return FieldValidationDataCheck
class DisallowedUIDDataCheck(DataCheck):
_class_name = "disallowed_uid"
verbose_name = _("Ensure that there are no disallowed usernames.")
problem_name = _("A user with a disallowed username was reported automatically.")
solve_options = {
IgnoreSolveOption._class_name: IgnoreSolveOption,
}
@classmethod
def check_data(cls):
from django.contrib.auth.models import User
disallowed_uids = get_site_preferences()["auth__disallowed_uids"].split(",")
for user in User.objects.filter(username__in=disallowed_uids):
logging.info(f"Check User {user}")
cls.register_result(user)
field_validation_data_check_factory("core", "CustomMenuItem", "icon")
class ChangeEmailAddressSolveOption(SolveOption):
_class_name = "change_email_address"
verbose_name = _("Change email address")
class EmailUniqueDataCheck(DataCheck):
_class_name = "email_unique"
verbose_name = _("Ensure that email addresses are unique among all persons")
problem_name = _("There was a non-unique email address.")
required_for_migrations = True
migration_dependencies = [("core", "0057_drop_otp_yubikey")]
solve_options = {ChangeEmailAddressSolveOption._class_name: ChangeEmailAddressSolveOption}
@classmethod
def check_data(cls):
known_email_addresses = set()
from .models import Person
persons = Person.objects.values("id", "email")
for person in persons:
if person["email"] and person["email"] in known_email_addresses:
cls.register_result(person["id"])
known_email_addresses.add(person["email"])
def accessible_colors_factory(
app_name: str,
model_name: str,
fg_field_name: str = None,
bg_field_name: str = None,
fg_color: str = "#ffffff",
bg_color: str = "#000000",
modulation_mode: ModulationMode = ModulationMode.BOTH,
) -> None:
ColorAccessibilityDataCheck.models.append(
(
app_name,
model_name,
fg_field_name,
bg_field_name,
fg_color,
bg_color,
modulation_mode,
)
)
def _get_colors_from_model_instance(instance, fg_field_name, bg_field_name, fg_color, bg_color):
colors: list[str] = [fg_color, bg_color]
if fg_field_name is not None:
colors[0] = getattr(instance, fg_field_name)
if bg_field_name is not None:
colors[1] = getattr(instance, bg_field_name)
# Transparency is not support for checking contrasts, so simply truncate it
for index, color in enumerate(colors):
if not color.startswith("#"):
continue
if len(color) == 5:
# color is of format "#RGBA"
colors[index] = color[:-1]
elif len(color) == 9:
# color is of format "#RRGGBBAA"
colors[index] = color[:-2]
return colors
class ModulateColorsSolveOption(SolveOption):
_class_name = "modulate_colors"
verbose_name = _("Auto-adjust Colors")
@classmethod
def solve(cls, check_result: "DataCheckResult"):
instance = check_result.related_object
ctype = check_result.content_type
model_info = list(
filter(
lambda m: m[0] == ctype.app_label and m[1] == ctype.model,
ColorAccessibilityDataCheck.models,
)
)
if len(model_info) == 0:
check_result.solved = False
check_result.save()
logging.error(f"Modulate Colors check failed for {check_result}: Model Info not found")
return
elif len(model_info) > 1:
check_result.solved = False
check_result.save()
logging.error(f"Modulate Colors check failed for {check_result}: Duplicate Model Info")
return
[_, _, fg_field_name, bg_field_name, fg_color, bg_color, modulation_mode] = model_info[0]
colors = _get_colors_from_model_instance(
instance, fg_field_name, bg_field_name, fg_color, bg_color
)
fg_new, bg_new, success = modulate(*colors, mode=modulation_mode)
if not success:
check_result.solved = False
check_result.save()
logging.error(
f"Modulate Colors check failed for {check_result}: Modulation not possible"
)
return
if fg_field_name:
setattr(instance, fg_field_name, fg_new)
if bg_field_name:
setattr(instance, bg_field_name, bg_new)
instance.save()
check_result.solved = True
check_result.save()
class ColorAccessibilityDataCheck(DataCheck):
_class_name = "colors_accessibility_datacheck"
verbose_name = _("Validate contrast accessibility of colors of customizable objects.")
problem_name = _("The colors of this object are not accessible.")
solve_options = {
IgnoreSolveOption._class_name: IgnoreSolveOption,
ModulateColorsSolveOption._class_name: ModulateColorsSolveOption,
}
models = []
@classmethod
def check_data(cls):
from django.apps import apps
for [
app_name,
model_name,
fg_field_name,
bg_field_name,
fg_color,
bg_color,
_modulation_mode,
] in cls.models:
model: Model = apps.get_model(app_name, model_name)
for obj in model.objects.all():
colors = _get_colors_from_model_instance(
obj, fg_field_name, bg_field_name, fg_color, bg_color
)
if not check_contrast(*colors, level=AccessibilityLevel.AA):
logging.info(f"Insufficient contrast in {app_name}.{model_name}.{obj}")
cls.register_result(obj)
class ModulateThemeColorsSolveOption(SolveOption):
_class_name = "modulate_theme_colors"
verbose_name = _("Auto-adjust Color")
@classmethod
def solve(cls, check_result: "DataCheckResult"):
instance = check_result.related_object
preference = f"{instance.section}__{instance.name}"
prefs = get_site_preferences()
color = prefs[preference]
fg_new, color_new, success = modulate("#fff", color, mode=ModulationMode.BACKGROUND)
if not success:
check_result.solved = False
check_result.save()
logging.error(f"Modulate {instance.name} theme color failed: Modulation not possible.")
return
prefs[preference] = str(color_new)
check_result.solved = True
check_result.save()
class AccessibleThemeColorsDataCheck(DataCheck):
_class_name = "accessible_themes_colors_datacheck"
verbose_name = _("Validate that theme colors are accessible.")
problem_name = _("The color does not provide enough contrast")
solve_options = {
IgnoreSolveOption._class_name: IgnoreSolveOption,
ModulateThemeColorsSolveOption._class_name: ModulateThemeColorsSolveOption,
}
@classmethod
def check_data(cls):
from dynamic_preferences.models import GlobalPreferenceModel
from .util.core_helpers import get_site_preferences
prefs = get_site_preferences()
primary = prefs["theme__primary"]
secondary = prefs["theme__secondary"]
# White text on primary colored background
if not check_contrast("#fff", primary, level=AccessibilityLevel.AA):
logging.info("Insufficient contrast in primary color")
obj = GlobalPreferenceModel.objects.get(section="theme", name="primary")
cls.register_result(obj)
# White text on secondary colored background
if not check_contrast("#fff", secondary, level=AccessibilityLevel.AA):
logging.info("Insufficient contrast in primary color")
obj = GlobalPreferenceModel.objects.get(section="theme", name="secondary")
cls.register_result(obj)
from functools import wraps
def pwa_cache(view_func):
"""Add headers to a response so that the PWA will recognize it as cacheable."""
@wraps(view_func)
def _wrapped_view_func(request, *args, **kwargs):
# Ensure argument looks like a request.
if not hasattr(request, "META"):
raise TypeError(
"pwa_cache didn't receive an HttpRequest. If you are "
"decorating a classmethod, be sure to use @method_decorator."
)
response = view_func(request, *args, **kwargs)
response.headers["PWA-Is-Cacheable"] = "true"
response.headers["Access-Control-Expose-Headers"] = "PWA-Is-Cacheable"
return response
return _wrapped_view_func
from typing import Sequence
from collections.abc import Sequence
from django.contrib.auth.models import Group as DjangoGroup
from django.contrib.auth.models import Permission, User
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.utils.translation import gettext as _
from django_filters import CharFilter, FilterSet, ModelChoiceFilter, ModelMultipleChoiceFilter
from django_filters import CharFilter, FilterSet, ModelChoiceFilter
from django_select2.forms import ModelSelect2Widget
from guardian.models import GroupObjectPermission, UserObjectPermission
from material import Layout, Row
from aleksis.core.models import Group, GroupType, Person, SchoolTerm
from aleksis.core.models import Person
class MultipleCharFilter(CharFilter):
......@@ -18,10 +23,7 @@ class MultipleCharFilter(CharFilter):
def filter(self, qs, value): # noqa
q = None
for field in self.fields:
if not q:
q = Q(**{field: value})
else:
q = q | Q(**{field: value})
q = Q(**{field: value}) if not q else q | Q(**{field: value})
return qs.filter(q)
def __init__(self, fields: Sequence[str], *args, **kwargs):
......@@ -29,19 +31,6 @@ class MultipleCharFilter(CharFilter):
super().__init__(self, *args, **kwargs)
class GroupFilter(FilterSet):
school_term = ModelChoiceFilter(queryset=SchoolTerm.objects.all())
group_type = ModelChoiceFilter(queryset=GroupType.objects.all())
parent_groups = ModelMultipleChoiceFilter(queryset=Group.objects.all())
search = MultipleCharFilter(["name__icontains", "short_name__icontains"], label=_("Search"))
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form.layout = Layout(Row("search"), Row("school_term", "group_type", "parent_groups"))
self.form.initial = {"school_term": SchoolTerm.current}
class PersonFilter(FilterSet):
name = MultipleCharFilter(
[
......@@ -49,15 +38,17 @@ class PersonFilter(FilterSet):
"additional_name__icontains",
"last_name__icontains",
"short_name__icontains",
"user__username__icontains",
],
label=_("Search by name"),
)
contact = MultipleCharFilter(
[
"street__icontains",
"housenumber__icontains",
"postal_code__icontains",
"place__icontains",
"addresses__street__icontains",
"addresses__housenumber__icontains",
"addresses__postal_code__icontains",
"addresses__place__icontains",
"addresses__country__icontains",
"phone_number__icontains",
"mobile_number__icontains",
"email__icontains",
......@@ -67,8 +58,99 @@ class PersonFilter(FilterSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form.layout = Layout(Row("name", "contact"), Row("is_active", "sex", "primary_group"))
self.form.layout = Layout(Row("name", "contact"), Row("sex", "primary_group"))
class Meta:
model = Person
fields = ["sex", "is_active", "primary_group"]
fields = ["sex", "primary_group"]
class PermissionFilter(FilterSet):
"""Common filter for permissions."""
permission = ModelChoiceFilter(
queryset=Permission.objects.all(),
widget=ModelSelect2Widget(
search_fields=["name__icontains", "codename__icontains"],
attrs={"data-minimum-input-length": 0, "class": "browser-default"},
),
label=_("Permission"),
)
permission__content_type = ModelChoiceFilter(
queryset=ContentType.objects.all(),
widget=ModelSelect2Widget(
search_fields=["app_label__icontains", "model__icontains"],
attrs={"data-minimum-input-length": 0, "class": "browser-default"},
),
label=_("Content type"),
)
class UserPermissionFilter(PermissionFilter):
"""Common filter for user permissions."""
user = ModelChoiceFilter(
queryset=User.objects.all(),
widget=ModelSelect2Widget(
search_fields=["username__icontains", "first_name__icontains", "last_name__icontains"],
attrs={"data-minimum-input-length": 0, "class": "browser-default"},
),
label=_("User"),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form.layout = Layout(Row("user", "permission", "permission__content_type"))
class Meta:
fields = ["user", "permission", "permission__content_type"]
class GroupPermissionFilter(PermissionFilter):
"""Common filter for group permissions."""
group = ModelChoiceFilter(
queryset=DjangoGroup.objects.all(),
widget=ModelSelect2Widget(
search_fields=[
"name__icontains",
],
attrs={"data-minimum-input-length": 0, "class": "browser-default"},
),
label=_("Group"),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form.layout = Layout(Row("group", "permission", "permission__content_type"))
class Meta:
fields = ["group", "permission", "permission__content_type"]
class UserGlobalPermissionFilter(UserPermissionFilter):
"""Filter for global user permissions."""
class Meta(UserPermissionFilter.Meta):
model = User.user_permissions.through
class GroupGlobalPermissionFilter(GroupPermissionFilter):
"""Filter for global group permissions."""
class Meta(GroupPermissionFilter.Meta):
model = DjangoGroup.permissions.through
class UserObjectPermissionFilter(UserPermissionFilter):
"""Filter for object user permissions."""
class Meta(UserPermissionFilter.Meta):
model = UserObjectPermission
class GroupObjectPermissionFilter(GroupPermissionFilter):
"""Filter for object group permissions."""
class Meta(GroupPermissionFilter.Meta):
model = GroupObjectPermission