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 (69)
Showing
with 2928 additions and 2050 deletions
...@@ -6,6 +6,40 @@ All notable changes to this project will be documented in this file. ...@@ -6,6 +6,40 @@ All notable changes to this project will be documented in this file.
The format is based on `Keep a Changelog`_, The format is based on `Keep a Changelog`_,
and this project adheres to `Semantic Versioning`_. and this project adheres to `Semantic Versioning`_.
Unreleased
----------
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
`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 `2.1.1`_ - 2021-11-14
--------------------- ---------------------
...@@ -464,3 +498,4 @@ Fixed ...@@ -464,3 +498,4 @@ Fixed
.. _2.0: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0 .. _2.0: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0
.. _2.1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.1 .. _2.1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.1
.. _2.1.1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.1.1 .. _2.1.1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.1.1
.. _2.2: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.2
...@@ -76,6 +76,14 @@ full licence text or on the `European Union Public Licence`_ website ...@@ -76,6 +76,14 @@ full licence text or on the `European Union Public Licence`_ website
https://joinup.ec.europa.eu/collection/eupl/guidelines-users-and-developers https://joinup.ec.europa.eu/collection/eupl/guidelines-users-and-developers
(including all other official language versions). (including all other official language versions).
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 .. _AlekSIS: https://aleksis.org
.. _European Union Public Licence: https://eupl.eu/ .. _European Union Public Licence: https://eupl.eu/
.. _EduGit: https://edugit.org/AlekSIS/official/AlekSIS .. _EduGit: https://edugit.org/AlekSIS/official/AlekSIS
.. _trademark policy: https://aleksis.org/pages/about
...@@ -153,5 +153,6 @@ class CoreConfig(AppConfig): ...@@ -153,5 +153,6 @@ class CoreConfig(AppConfig):
"address": _("Full home postal address"), "address": _("Full home postal address"),
"email": _("Email address"), "email": _("Email address"),
"phone": _("Home and mobile phone"), "phone": _("Home and mobile phone"),
"groups": _("Groups"),
} }
return scopes return scopes
from typing import Sequence from typing 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.db.models import Q
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django_filters import CharFilter, FilterSet, ModelChoiceFilter, ModelMultipleChoiceFilter from django_filters import CharFilter, FilterSet, ModelChoiceFilter, ModelMultipleChoiceFilter
from django_select2.forms import ModelSelect2Widget
from guardian.models import GroupObjectPermission, UserObjectPermission
from material import Layout, Row from material import Layout, Row
from aleksis.core.models import Group, GroupType, Person, SchoolTerm from aleksis.core.models import Group, GroupType, Person, SchoolTerm
...@@ -72,3 +77,94 @@ class PersonFilter(FilterSet): ...@@ -72,3 +77,94 @@ class PersonFilter(FilterSet):
class Meta: class Meta:
model = Person model = Person
fields = ["sex", "is_active", "primary_group"] fields = ["sex", "is_active", "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
from datetime import datetime, time from datetime import datetime, time
from typing import Callable, Sequence from typing import Any, Callable, Dict, Sequence
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db.models import QuerySet from django.db.models import QuerySet
from django.http import HttpRequest from django.http import HttpRequest
...@@ -10,9 +12,10 @@ from django.utils.translation import gettext_lazy as _ ...@@ -10,9 +12,10 @@ from django.utils.translation import gettext_lazy as _
from allauth.account.adapter import get_adapter from allauth.account.adapter import get_adapter
from allauth.account.forms import SignupForm from allauth.account.forms import SignupForm
from allauth.account.utils import get_user_model, setup_user_email from allauth.account.utils import setup_user_email
from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget, Select2Widget from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget, Select2Widget
from dynamic_preferences.forms import PreferenceForm from dynamic_preferences.forms import PreferenceForm
from guardian.shortcuts import assign_perm
from material import Fieldset, Layout, Row from material import Fieldset, Layout, Row
from .mixins import ExtensibleForm, SchoolTermRelatedExtensibleForm from .mixins import ExtensibleForm, SchoolTermRelatedExtensibleForm
...@@ -389,6 +392,116 @@ DashboardWidgetOrderFormSet = forms.formset_factory( ...@@ -389,6 +392,116 @@ DashboardWidgetOrderFormSet = forms.formset_factory(
) )
class SelectPermissionForm(forms.Form):
"""Select a permission to assign."""
selected_permission = forms.ModelChoiceField(
queryset=Permission.objects.all(),
widget=ModelSelect2Widget(
search_fields=["name__icontains", "codename__icontains"],
attrs={"data-minimum-input-length": 0, "class": "browser-default"},
),
)
class AssignPermissionForm(forms.Form):
"""Assign a permission to user/groups for all/some objects."""
layout = Layout(
Fieldset(_("Who should get the permission?"), "groups", "persons"),
Fieldset(_("On what?"), "objects", "all_objects"),
)
groups = forms.ModelMultipleChoiceField(
queryset=Group.objects.all(),
widget=ModelSelect2MultipleWidget(
search_fields=["name__icontains", "short_name__icontains"],
attrs={"data-minimum-input-length": 0, "class": "browser-default"},
),
required=False,
)
persons = forms.ModelMultipleChoiceField(
queryset=Person.objects.all(),
widget=ModelSelect2MultipleWidget(
search_fields=[
"first_name__icontains",
"last_name__icontains",
"short_name__icontains",
],
attrs={"data-minimum-input-length": 0, "class": "browser-default"},
),
required=False,
)
objects = forms.ModelMultipleChoiceField(
queryset=None,
required=False,
label=_("Select objects which the permission should be granted for:"),
)
all_objects = forms.BooleanField(
required=False, label=_("Grant the permission for all objects")
)
def clean(self) -> Dict[str, Any]:
"""Clean form to ensure that at least one target and one type is selected."""
cleaned_data = super().clean()
if not cleaned_data.get("persons") and not cleaned_data.get("groups"):
raise ValidationError(
_("You must select at least one group or person which should get the permission.")
)
if not cleaned_data.get("objects") and not cleaned_data.get("all_objects"):
raise ValidationError(
_("You must grant the permission to all objects and/" "or to some objects.")
)
return cleaned_data
def __init__(self, *args, permission: Permission, **kwargs):
self.permission = permission
super().__init__(*args, **kwargs)
model_class = self.permission.content_type.model_class()
queryset = model_class.objects.all()
self.fields["objects"].queryset = queryset
search_fields = getattr(model_class, "get_filter_fields", lambda: [])()
# Use select2 only if there are any searchable fields as it can't work without
if search_fields:
self.fields["objects"].widget = ModelSelect2MultipleWidget(
search_fields=search_fields,
queryset=queryset,
attrs={"data-minimum-input-length": 0, "class": "browser-default"},
)
def save_perms(self):
"""Save permissions for selected user/groups and selected/all objects."""
persons = self.cleaned_data["persons"]
groups = self.cleaned_data["groups"]
all_objects = self.cleaned_data["all_objects"]
objects = self.cleaned_data["objects"]
permission_name = f"{self.permission.content_type.app_label}.{self.permission.codename}"
created = 0
# Create permissions for users
for person in persons:
if getattr(person, "user", None):
# Global permission
if all_objects:
assign_perm(permission_name, person.user)
# Object permissions
for instance in objects:
assign_perm(permission_name, person.user, instance)
# Create permissions for users
for group in groups:
django_group = group.django_group
# Global permission
if all_objects:
assign_perm(permission_name, django_group)
# Object permissions
for instance in objects:
assign_perm(permission_name, django_group, instance)
class AccountRegisterForm(SignupForm, ExtensibleForm): class AccountRegisterForm(SignupForm, ExtensibleForm):
"""Form to register new user accounts.""" """Form to register new user accounts."""
...@@ -633,6 +746,7 @@ class OAuthApplicationForm(forms.ModelForm): ...@@ -633,6 +746,7 @@ class OAuthApplicationForm(forms.ModelForm):
"client_id", "client_id",
"client_secret", "client_secret",
"client_type", "client_type",
"algorithm",
"allowed_scopes", "allowed_scopes",
"redirect_uris", "redirect_uris",
"skip_authorization", "skip_authorization",
......
...@@ -8,7 +8,7 @@ msgid "" ...@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-28 16:18+0200\n" "POT-Creation-Date: 2021-11-29 09:59+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
......
...@@ -7,11 +7,10 @@ msgid "" ...@@ -7,11 +7,10 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-08-28 17:53+0200\n" "POT-Creation-Date: 2021-11-29 09:59+0100\n"
"PO-Revision-Date: 2021-10-28 14:37+0000\n" "PO-Revision-Date: 2021-10-28 14:37+0000\n"
"Last-Translator: Jonathan Weth <teckids@jonathanweth.de>\n" "Last-Translator: Jonathan Weth <teckids@jonathanweth.de>\n"
"Language-Team: German <https://translate.edugit.org/projects/aleksis/" "Language-Team: German <https://translate.edugit.org/projects/aleksis/aleksis-core-js/de/>\n"
"aleksis-core-js/de/>\n"
"Language: de_DE\n" "Language: de_DE\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
...@@ -33,6 +32,4 @@ msgstr "OK" ...@@ -33,6 +32,4 @@ msgstr "OK"
#: aleksis/core/static/js/main.js:127 #: aleksis/core/static/js/main.js:127
msgid "This page may contain outdated information since there is no internet connection." msgid "This page may contain outdated information since there is no internet connection."
msgstr "" msgstr "Diese Seite enthält vielleicht veraltete Informationen, da es keine Internetverbindung gibt."
"Diese Seite enthält vielleicht veraltete Informationen, da es keine "
"Internetverbindung gibt."
...@@ -8,7 +8,7 @@ msgid "" ...@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-28 16:18+0200\n" "POT-Creation-Date: 2021-11-29 09:59+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
......
...@@ -8,7 +8,7 @@ msgid "" ...@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-28 16:18+0200\n" "POT-Creation-Date: 2021-11-29 09:59+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
......
...@@ -8,7 +8,7 @@ msgid "" ...@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-28 16:18+0200\n" "POT-Creation-Date: 2021-11-29 09:59+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
......
...@@ -8,7 +8,7 @@ msgid "" ...@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-28 16:18+0200\n" "POT-Creation-Date: 2021-11-29 09:59+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
......
...@@ -211,6 +211,17 @@ MENUS = { ...@@ -211,6 +211,17 @@ MENUS = {
"icon": "done_all", "icon": "done_all",
"validators": ["menu_generator.validators.is_superuser"], "validators": ["menu_generator.validators.is_superuser"],
}, },
{
"name": _("Manage permissions"),
"url": "manage_user_global_permissions",
"icon": "shield",
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
"core.manage_permissions",
),
],
},
{ {
"name": _("Backend Admin"), "name": _("Backend Admin"),
"url": "admin:index", "url": "admin:index",
......
# flake8: noqa: DJ12 # flake8: noqa: DJ12
from datetime import datetime from datetime import datetime
from typing import Any, Callable, Optional, Union from typing import Any, Callable, List, Optional, Union
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
...@@ -11,6 +11,7 @@ from django.contrib.sites.managers import CurrentSiteManager ...@@ -11,6 +11,7 @@ from django.contrib.sites.managers import CurrentSiteManager
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.db import models from django.db import models
from django.db.models import JSONField, QuerySet from django.db.models import JSONField, QuerySet
from django.db.models.fields import CharField, TextField
from django.forms.forms import BaseForm from django.forms.forms import BaseForm
from django.forms.models import ModelForm, ModelFormMetaclass from django.forms.models import ModelForm, ModelFormMetaclass
from django.http import HttpResponse from django.http import HttpResponse
...@@ -276,6 +277,15 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase): ...@@ -276,6 +277,15 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
to.property_(_virtual_related, related_name) to.property_(_virtual_related, related_name)
@classmethod
def get_filter_fields(cls) -> List[str]:
"""Get names of all text-searchable fields of this model."""
fields = []
for field in cls.syncable_fields():
if isinstance(field, (CharField, TextField)):
fields.append(field.name)
return fields
@classmethod @classmethod
def syncable_fields( def syncable_fields(
cls, recursive: bool = True, exclude_remotes: list = [] cls, recursive: bool = True, exclude_remotes: list = []
......
...@@ -510,6 +510,12 @@ class Group(SchoolTermRelatedExtensibleModel): ...@@ -510,6 +510,12 @@ class Group(SchoolTermRelatedExtensibleModel):
) )
dj_group.save() dj_group.save()
@property
def django_group(self):
"""Get Django group for this group."""
dj_group, _ = DjangoGroup.objects.get_or_create(name=self.name)
return dj_group
class PersonGroupThrough(ExtensibleModel): class PersonGroupThrough(ExtensibleModel):
"""Through table for many-to-many relationship of group members. """Through table for many-to-many relationship of group members.
...@@ -1126,7 +1132,7 @@ class OAuthApplication(AbstractApplication): ...@@ -1126,7 +1132,7 @@ class OAuthApplication(AbstractApplication):
def allows_grant_type(self, *grant_types: set[str]) -> bool: def allows_grant_type(self, *grant_types: set[str]) -> bool:
allowed_grants = get_site_preferences()["auth__oauth_allowed_grants"] allowed_grants = get_site_preferences()["auth__oauth_allowed_grants"]
return bool(set(allowed_grants) & grant_types) return bool(set(allowed_grants) & set(grant_types))
class OAuthGrant(AbstractGrant): class OAuthGrant(AbstractGrant):
......