Skip to content
Commits on Source (28)
......@@ -6,6 +6,30 @@ 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`_.
`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
----------------------
......@@ -377,3 +401,4 @@ Fixed
.. _2.0rc5: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0rc5
.. _2.0rc6: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0rc6
.. _2.0rc7: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0rc7
.. _2.0: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0
......@@ -13,7 +13,6 @@ from allauth.account.forms import SignupForm
from allauth.account.utils import get_user_model, setup_user_email
from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget, Select2Widget
from dynamic_preferences.forms import PreferenceForm
from guardian.core import ObjectPermissionChecker
from material import Fieldset, Layout, Row
from .mixins import ExtensibleForm, SchoolTermRelatedExtensibleForm
......@@ -34,56 +33,8 @@ from .registries import (
from .util.core_helpers import get_site_preferences
class PersonAccountForm(forms.ModelForm):
"""Form to assign user accounts to persons in the frontend."""
class Meta:
model = Person
fields = ["last_name", "first_name", "user"]
widgets = {"user": Select2Widget(attrs={"class": "browser-default"})}
new_user = forms.CharField(required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Fields displayed only for informational purposes
self.fields["first_name"].disabled = True
self.fields["last_name"].disabled = True
def clean(self) -> None:
user = get_user_model()
if self.cleaned_data.get("new_user", None):
if self.cleaned_data.get("user", None):
# The user selected both an existing user and provided a name to create a new one
self.add_error(
"new_user",
_("You cannot set a new username when also selecting an existing user."),
)
elif user.objects.filter(username=self.cleaned_data["new_user"]).exists():
# The user tried to create a new user with the name of an existing user
self.add_error("new_user", _("This username is already in use."))
else:
# Create new User object and assign to form field for existing user
new_user_obj = user.objects.create_user(
self.cleaned_data["new_user"],
self.instance.email,
first_name=self.instance.first_name,
last_name=self.instance.last_name,
)
self.cleaned_data["user"] = new_user_obj
# Formset for batch-processing of assignments of users to persons
PersonsAccountsFormSet = forms.modelformset_factory(
Person, form=PersonAccountForm, max_num=0, extra=0
)
class EditPersonForm(ExtensibleForm):
"""Form to edit an existing person object in the frontend."""
class PersonForm(ExtensibleForm):
"""Form to edit or add a person object in the frontend."""
layout = Layout(
Fieldset(
......@@ -142,25 +93,49 @@ class EditPersonForm(ExtensibleForm):
required=False, label=_("New user"), help_text=_("Create a new account")
)
def __init__(self, request: HttpRequest, *args, **kwargs):
def __init__(self, *args, **kwargs):
request = kwargs.pop("request", None)
super().__init__(*args, **kwargs)
# Disable non-editable fields
person_fields = set([field.name for field in Person.syncable_fields()]).intersection(
set(self.fields)
)
allowed_person_fields = get_site_preferences()["account__editable_fields_person"]
if self.instance:
checker = ObjectPermissionChecker(request.user)
checker.prefetch_perms([self.instance])
if (
request
and self.instance
and not request.user.has_perm("core.change_person", self.instance)
):
# First, disable all fields
for field in self.fields:
self.fields[field].disabled = True
for field in person_fields:
if not checker.has_perm(f"core.change_person_field_{field}", self.instance):
self.fields[field].disabled = True
# Then, activate allowed fields
for field in allowed_person_fields:
self.fields[field].disabled = False
def clean(self) -> None:
# Use code implemented in dedicated form to verify user selection
return PersonAccountForm.clean(self)
user = get_user_model()
if self.cleaned_data.get("new_user", None):
if self.cleaned_data.get("user", None):
# The user selected both an existing user and provided a name to create a new one
self.add_error(
"new_user",
_("You cannot set a new username when also selecting an existing user."),
)
elif user.objects.filter(username=self.cleaned_data["new_user"]).exists():
# The user tried to create a new user with the name of an existing user
self.add_error("new_user", _("This username is already in use."))
else:
# Create new User object and assign to form field for existing user
new_user_obj = user.objects.create_user(
self.cleaned_data["new_user"],
self.instance.email,
first_name=self.instance.first_name,
last_name=self.instance.last_name,
)
self.cleaned_data["user"] = new_user_obj
class EditGroupForm(SchoolTermRelatedExtensibleForm):
......
......@@ -8,7 +8,7 @@ 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-10-28 16:18+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......
......@@ -3,32 +3,36 @@
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-08-28 17:53+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"PO-Revision-Date: 2021-10-28 14:37+0000\n"
"Last-Translator: Jonathan Weth <teckids@jonathanweth.de>\n"
"Language-Team: German <https://translate.edugit.org/projects/aleksis/"
"aleksis-core-js/de/>\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.8\n"
#: aleksis/core/static/js/main.js:15
msgid "Today"
msgstr ""
msgstr "Heute"
#: aleksis/core/static/js/main.js:16
msgid "Cancel"
msgstr ""
msgstr "Abbrechen"
#: aleksis/core/static/js/main.js:17
msgid "OK"
msgstr ""
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."
......@@ -8,7 +8,7 @@ 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-10-28 16:18+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......
......@@ -8,7 +8,7 @@ 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-10-28 16:18+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......
......@@ -8,7 +8,7 @@ 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-10-28 16:18+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......
......@@ -8,7 +8,7 @@ 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-10-28 16:18+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......
......@@ -267,17 +267,6 @@ MENUS = {
)
],
},
{
"name": _("Persons and accounts"),
"url": "persons_accounts",
"icon": "person_add",
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
"core.link_persons_accounts_rule",
)
],
},
{
"name": _("Groups and child groups"),
"url": "groups_child_groups",
......
# Generated by Django 3.2.8 on 2021-10-24 13:43
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0020_pdf_file_person_optional'),
]
operations = [
migrations.AlterModelOptions(
name='globalpermissions',
options={'default_permissions': (), 'managed': False, 'permissions': (('view_system_status', 'Can view system status'), ('manage_data', 'Can manage data'), ('impersonate', 'Can impersonate'), ('search', 'Can use search'), ('change_site_preferences', 'Can change site preferences'), ('change_person_preferences', 'Can change person preferences'), ('change_group_preferences', 'Can change group preferences'), ('add_oauth_applications', 'Can add oauth applications'), ('list_oauth_applications', 'Can list oauth applications'), ('view_oauth_applications', 'Can view oauth applications'), ('update_oauth_applications', 'Can update oauth applications'), ('delete_oauth_applications', 'Can delete oauth applications'), ('test_pdf', 'Can test PDF generation'))},
),
]
# Generated by Django 3.2.4 on 2021-07-24 13:14
import os
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0021_drop_persons_accounts_perm'),
('favicon', '0004_faviconimg_favicon_size_rel_unique'),
]
def _migrate_favicons(apps, schema_editor):
FaviconImg = apps.get_model('favicon', "FaviconImg")
for favicon_img in FaviconImg.objects.all():
old_name = favicon_img.faviconImage.name
new_name = os.path.join("public", old_name)
storage = favicon_img.faviconImage.storage
if storage.exists(old_name):
storage.save(new_name, favicon_img.faviconImage.file)
favicon_img.faviconImage.name = new_name
favicon_img.save()
operations = [
migrations.RunPython(_migrate_favicons)
]
......@@ -32,6 +32,7 @@ from model_utils import FieldTracker
from model_utils.models import TimeStampedModel
from phonenumber_field.modelfields import PhoneNumberField
from polymorphic.models import PolymorphicModel
from templated_email import send_templated_mail
from aleksis.core.data_checks import BrokenDashboardWidgetDataCheck, DataCheck, DataCheckRegistry
......@@ -329,6 +330,22 @@ class Person(ExtensibleModel):
if force or not self.primary_group:
self.primary_group = self.member_of.filter(**{f"{field}__regex": pattern}).first()
def notify_about_changed_data(
self, changed_fields: Iterable[str], recipients: Optional[List[str]] = None
):
"""Notify (configured) recipients about changed data of this person."""
context = {"person": self, "changed_fields": changed_fields}
recipients = recipients or [
get_site_preferences()["account__person_change_notification_contact"]
]
send_templated_mail(
template_name="person_changed",
from_email=self.mail_sender_via,
headers={"Reply-To": self.mail_sender, "Sender": self.mail_sender,},
recipient_list=recipients,
context=context,
)
class DummyPerson(Person):
"""A dummy person that is not stored into the database.
......@@ -952,7 +969,6 @@ class GlobalPermissions(GlobalPermissionModel):
class Meta(GlobalPermissionModel.Meta):
permissions = (
("view_system_status", _("Can view system status")),
("link_persons_accounts", _("Can link persons to accounts")),
("manage_data", _("Can manage data")),
("impersonate", _("Can impersonate")),
("search", _("Can use search")),
......
......@@ -2,7 +2,6 @@ import rules
from .models import AdditionalField, Announcement, Group, GroupType, Person
from .util.predicates import (
contains_site_preference_value,
has_any_object,
has_global_perm,
has_object_perm,
......@@ -80,10 +79,6 @@ delete_person_predicate = has_person & (
)
rules.add_perm("core.delete_person_rule", delete_person_predicate)
# Link persons with accounts
link_persons_accounts_predicate = has_person & has_global_perm("core.link_persons_accounts")
rules.add_perm("core.link_persons_accounts_rule", link_persons_accounts_predicate)
# View groups
view_groups_predicate = has_person & (
has_global_perm("core.view_group") | has_any_object("core.view_group", Group)
......@@ -158,12 +153,7 @@ rules.add_perm("core.view_system_status_rule", view_system_status_predicate)
rules.add_perm(
"core.view_people_menu_rule",
has_person
& (
view_persons_predicate
| view_groups_predicate
| link_persons_accounts_predicate
| assign_child_groups_to_groups_predicate
),
& (view_persons_predicate | view_groups_predicate | assign_child_groups_to_groups_predicate),
)
# View person personal details
......@@ -350,15 +340,3 @@ rules.add_perm("core.upload_files_ckeditor_rule", upload_files_ckeditor_predicat
test_pdf_generation_predicate = has_person & has_global_perm("core.test_pdf")
rules.add_perm("core.test_pdf_rule", test_pdf_generation_predicate)
# Generate rules for syncable fields
for field in Person._meta.fields:
perm = (
has_global_perm("core.edit_person")
| has_object_perm("core.edit_person")
| (
is_current_person
& contains_site_preference_value("account", "editable_fields_person", field.name)
)
)
rules.add_perm(f"core.change_person_field_{field.name}_rule", perm)
......@@ -523,9 +523,8 @@ SASS_PROCESSOR_CUSTOM_FUNCTIONS = {
"get-preference": "aleksis.core.util.sass_helpers.get_preference",
}
SASS_PROCESSOR_INCLUDE_DIRS = [
_settings.get("materialize.sass_path", JS_ROOT + "/materialize-css/sass/"),
STATIC_ROOT + "/materialize-css/sass/",
STATIC_ROOT,
_settings.get("materialize.sass_path", os.path.join(JS_ROOT, "materialize-css", "sass")),
os.path.join(STATIC_ROOT, "public"),
]
ADMINS = _settings.get("contact.admins", [AUTH_INITIAL_SUPERUSER["email"]])
......@@ -636,6 +635,7 @@ PWA_ICONS_CONFIG = {
"apple_splash": [192],
"microsoft": [144],
}
FAVICON_PATH = os.path.join("public", "favicon")
SERVICE_WORKER_PATH = os.path.join(STATIC_ROOT, "js", "serviceworker.js")
......