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.
The format is based on `Keep a Changelog`_,
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
---------------------
......@@ -464,3 +498,4 @@ Fixed
.. _2.0: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0
.. _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.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
https://joinup.ec.europa.eu/collection/eupl/guidelines-users-and-developers
(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
.. _European Union Public Licence: https://eupl.eu/
.. _EduGit: https://edugit.org/AlekSIS/official/AlekSIS
.. _trademark policy: https://aleksis.org/pages/about
......@@ -153,5 +153,6 @@ class CoreConfig(AppConfig):
"address": _("Full home postal address"),
"email": _("Email address"),
"phone": _("Home and mobile phone"),
"groups": _("Groups"),
}
return scopes
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.utils.translation import gettext as _
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 aleksis.core.models import Group, GroupType, Person, SchoolTerm
......@@ -72,3 +77,94 @@ class PersonFilter(FilterSet):
class Meta:
model = Person
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 typing import Callable, Sequence
from typing import Any, Callable, Dict, Sequence
from django import forms
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.core.exceptions import ValidationError
from django.db.models import QuerySet
from django.http import HttpRequest
......@@ -10,9 +12,10 @@ from django.utils.translation import gettext_lazy as _
from allauth.account.adapter import get_adapter
from allauth.account.forms import SignupForm
from allauth.account.utils import get_user_model, setup_user_email
from allauth.account.utils import setup_user_email
from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget, Select2Widget
from dynamic_preferences.forms import PreferenceForm
from guardian.shortcuts import assign_perm
from material import Fieldset, Layout, Row
from .mixins import ExtensibleForm, SchoolTermRelatedExtensibleForm
......@@ -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):
"""Form to register new user accounts."""
......@@ -633,6 +746,7 @@ class OAuthApplicationForm(forms.ModelForm):
"client_id",
"client_secret",
"client_type",
"algorithm",
"allowed_scopes",
"redirect_uris",
"skip_authorization",
......
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......
......@@ -7,11 +7,10 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: Jonathan Weth <teckids@jonathanweth.de>\n"
"Language-Team: German <https://translate.edugit.org/projects/aleksis/"
"aleksis-core-js/de/>\n"
"Language-Team: German <https://translate.edugit.org/projects/aleksis/aleksis-core-js/de/>\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
......@@ -33,6 +32,4 @@ msgstr "OK"
#: aleksis/core/static/js/main.js:127
msgid "This page may contain outdated information since there is no internet connection."
msgstr ""
"Diese Seite enthält vielleicht veraltete Informationen, da es keine "
"Internetverbindung gibt."
msgstr "Diese Seite enthält vielleicht veraltete Informationen, da es keine Internetverbindung gibt."
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......
......@@ -211,6 +211,17 @@ MENUS = {
"icon": "done_all",
"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"),
"url": "admin:index",
......
# flake8: noqa: DJ12
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.contrib import messages
......@@ -11,6 +11,7 @@ from django.contrib.sites.managers import CurrentSiteManager
from django.contrib.sites.models import Site
from django.db import models
from django.db.models import JSONField, QuerySet
from django.db.models.fields import CharField, TextField
from django.forms.forms import BaseForm
from django.forms.models import ModelForm, ModelFormMetaclass
from django.http import HttpResponse
......@@ -276,6 +277,15 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
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
def syncable_fields(
cls, recursive: bool = True, exclude_remotes: list = []
......
......@@ -510,6 +510,12 @@ class Group(SchoolTermRelatedExtensibleModel):
)
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):
"""Through table for many-to-many relationship of group members.
......@@ -1126,7 +1132,7 @@ class OAuthApplication(AbstractApplication):
def allows_grant_type(self, *grant_types: set[str]) -> bool:
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):
......