diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3b4a70be33ebf01ed8c37cdff7e93ceb0471aef2..151b99434bcb822d4d8ea812b60d7b7d9928438b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,11 +9,23 @@ and this project adheres to `Semantic Versioning`_. Unreleased ---------- +Added +~~~~~ + +* [OAuth] Expired tokens are now cleared in a periodic task + +Changed +~~~~~~~ + +* PWA icons can now be marked maskable + 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 `2.7`_ - 2022-01-24 ------------------- diff --git a/aleksis/core/models.py b/aleksis/core/models.py index f46a803b687cc9cde9a893c642586eac232fd84d..d5dc7d7befe1395b222ff492f1a9f8eed3994f69 100644 --- a/aleksis/core/models.py +++ b/aleksis/core/models.py @@ -316,7 +316,7 @@ class Person(ExtensibleModel): def initials(self): return f"{self.first_name[0]}{self.last_name[0]}".upper() - user_info_tracker = FieldTracker(fields=("first_name", "last_name", "email")) + user_info_tracker = FieldTracker(fields=("first_name", "last_name", "email", "user")) @property def member_of_recursive(self) -> QuerySet: @@ -336,16 +336,29 @@ class Person(ExtensibleModel): def save(self, *args, **kwargs): # Determine all fields that were changed since last load - dirty = self.pk is None or bool(self.user_info_tracker.changed()) + changed = self.user_info_tracker.changed() super().save(*args, **kwargs) - if self.user and dirty: - # Synchronise user fields to linked User object to keep it up to date - self.user.first_name = self.first_name - self.user.last_name = self.last_name - self.user.email = self.email - self.user.save() + if self.pk is None or bool(changed): + if "user" in changed: + # Clear groups of previous Django user + previous_user = changed["user"] + if previous_user is not None: + get_user_model().objects.get(pk=previous_user).groups.clear() + + if self.user: + if "first_name" in changed or "last_name" in changed or "email" in changed: + # Synchronise user fields to linked User object to keep it up to date + self.user.first_name = self.first_name + self.user.last_name = self.last_name + self.user.email = self.email + self.user.save() + + if "user" in changed: + # Synchronise groups to Django groups + for group in self.member_of.union(self.owner_of.all()).all(): + group.save(force=True) # Select a primary group if none is set self.auto_select_primary_group() diff --git a/aleksis/core/preferences.py b/aleksis/core/preferences.py index fe29f220f211616bf57d08c04e87a95b60c84365..f1fb0227eab16a152f29918e91b37d0808471812 100644 --- a/aleksis/core/preferences.py +++ b/aleksis/core/preferences.py @@ -112,6 +112,17 @@ class PWAIcon(PublicFilePreferenceMixin, FilePreference): required = False +@site_preferences_registry.register +class PWAIconMaskable(BooleanPreference): + """PWA icon is maskable.""" + + section = theme + name = "pwa_icon_maskable" + verbose_name = _("PWA-Icon is maskable") + default = True + required = False + + @site_preferences_registry.register class MailOutName(StringPreference): """Mail out name of your AlekSIS instance.""" diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py index 2defba33645df5cf2f16be4a195a6385d00e0ce4..d0547512292223c05771e7d3152c9f91764e2c8c 100644 --- a/aleksis/core/settings.py +++ b/aleksis/core/settings.py @@ -704,7 +704,7 @@ if _settings.get("dev.uwsgi.celery", DEBUG): UWSGI["attach-daemon"].append("celery -A aleksis.core beat") DEFAULT_FAVICON_PATHS = { - "pwa_icon": os.path.join(STATIC_ROOT, "img/aleksis-icon.png"), + "pwa_icon": os.path.join(STATIC_ROOT, "img/aleksis-icon-maskable.png"), "favicon": os.path.join(STATIC_ROOT, "img/aleksis-favicon.png"), } PWA_ICONS_CONFIG = { diff --git a/aleksis/core/static/img/aleksis-icon-maskable.png b/aleksis/core/static/img/aleksis-icon-maskable.png new file mode 100644 index 0000000000000000000000000000000000000000..6ec3bb2c04392e646c7d00c5ba3a316ebf99486e Binary files /dev/null and b/aleksis/core/static/img/aleksis-icon-maskable.png differ diff --git a/aleksis/core/static/img/aleksis-icon-maskable.svg b/aleksis/core/static/img/aleksis-icon-maskable.svg new file mode 100644 index 0000000000000000000000000000000000000000..e37f3c3606d748039e4dc03cc4b0f27f5de491e1 --- /dev/null +++ b/aleksis/core/static/img/aleksis-icon-maskable.svg @@ -0,0 +1,134 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + id="svg8" + width="256" + height="256" + version="1.1" + viewBox="0 0 67.73 67.73" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> + <title + id="title32">AlekSIS icon</title> + <defs + id="defs2"> + <linearGradient + id="shadow-gradient" + x1="-19.53" + x2="165.4" + y1="-19.53" + y2="165.4" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.2646,0,0,0.2646,109.82466,-4.9393086)"> + <stop + id="stop9444" + offset="0" /> + <stop + id="stop9446" + stop-opacity="0" + offset="1" /> + </linearGradient> + </defs> + <metadata + id="metadata5"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title>AlekSIS icon</dc:title> + <cc:license + rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" /> + <dc:date>2020-02-29</dc:date> + <dc:creator> + <cc:Agent> + <dc:title>Dominik George</dc:title> + </cc:Agent> + </dc:creator> + <dc:contributor> + <cc:Agent> + <dc:title>Julian Leucker</dc:title> + </cc:Agent> + </dc:contributor> + </cc:Work> + <cc:License + rdf:about="http://creativecommons.org/licenses/by-sa/4.0/"> + <cc:permits + rdf:resource="http://creativecommons.org/ns#Reproduction" /> + <cc:permits + rdf:resource="http://creativecommons.org/ns#Distribution" /> + <cc:requires + rdf:resource="http://creativecommons.org/ns#Notice" /> + <cc:requires + rdf:resource="http://creativecommons.org/ns#Attribution" /> + <cc:permits + rdf:resource="http://creativecommons.org/ns#DerivativeWorks" /> + <cc:requires + rdf:resource="http://creativecommons.org/ns#ShareAlike" /> + </cc:License> + </rdf:RDF> + </metadata> + <g + id="background-with-glow"> + <rect + id="background" + y="-3.357e-6" + width="67.73" + height="67.73" + rx="3.307" + ry="3.307" + fill="#0d5eaf" + stroke-width=".4104" /> + <path + id="glow" + transform="scale(.2646)" + d="m9.959 0.2578c-5.698 1.168-9.959 6.189-9.959 12.24v52.31a234.1 86.8 0 0 0 188.9 35.71 234.1 86.8 0 0 0 67.09-3.715v-84.3c0-4.319-2.17-8.112-5.482-10.36-0.01554-0.01049-0.03129-0.02083-0.04688-0.03125-0.2971-0.1993-0.6034-0.3849-0.918-0.5586-0.05211-0.02867-0.1037-0.05799-0.1562-0.08594-0.2939-0.1568-0.5968-0.3001-0.9043-0.4336-0.05654-0.02442-0.111-0.05257-0.168-0.07617-0.3308-0.1378-0.6709-0.2577-1.016-0.3672-0.03502-0.01105-0.06836-0.02636-0.1035-0.03711-0.3806-0.1172-0.7687-0.2158-1.164-0.2969h-236.1z" + fill="#fff" + opacity=".2" + stroke-width="1.551" /> + </g> + <g + id="favicon-bag" + transform="translate(-46.809258,-7.6499461)" + display="none"> + <use + xlink:href="#schoolbag" + transform="matrix(2.25,0,0,2.25,-30.62,0)" + fill="#ffffff" + x="0" + y="0" + width="100%" + height="100%" + id="use18" /> + </g> + <g + id="widgets-with-shadow" + transform="matrix(0.62859773,0,0,0.63035889,12.536993,12.511931)" + style="stroke-width:1.58861"> + <g + id="widgets" + fill="#ffffff" + style="stroke-width:2.18867"> + <path + id="schoolbag" + d="m 44.52,19.7 h 10.45 a 0.4353,0.4353 0 0 1 0.4353,0.4353 v 3.917 A 0.4353,0.4353 0 0 1 54.97,24.4876 H 44.52 a 0.4353,0.4353 0 0 1 -0.4353,-0.4353 v -3.917 A 0.4353,0.4353 0 0 1 44.52,19.7 Z M 55.4,18.394 v -0.8705 a 0.4353,0.4353 0 0 0 -0.4353,-0.4353 h -10.45 a 0.4353,0.4353 0 0 0 -0.4353,0.4353 v 0.8705 a 0.4353,0.4353 0 0 0 0.4353,0.4353 h 10.45 A 0.4353,0.4353 0 0 0 55.4,18.394 Z m -14.8,10.01 v 0.8705 a 2.176,2.176 0 0 0 2.176,2.176 h 13.93 a 2.176,2.176 0 0 0 2.176,-2.176 V 28.404 a 0.4353,0.4353 0 0 0 -0.4353,-0.4353 h -17.41 a 0.4353,0.4353 0 0 0 -0.4353,0.4353 z m -0.8705,-7.4 v -1.741 a 0.4353,0.4353 0 0 0 -0.4353,-0.4353 h -0.8705 a 1.306,1.306 0 0 0 -1.306,1.306 v 0.8706 a 0.4353,0.4353 0 0 0 0.4353,0.4353 h 1.741 a 0.4353,0.4353 0 0 0 0.4353,-0.4353 z m -2.612,1.741 v 4.788 a 1.306,1.306 0 0 0 1.306,1.306 h 0.8705 a 0.4353,0.4353 0 0 0 0.4353,-0.4353 v -5.659 A 0.4353,0.4353 0 0 0 39.294,22.3094 h -1.741 a 0.4353,0.4353 0 0 0 -0.4353,0.4353 z m 22.63,0 v 5.659 a 0.4353,0.4353 0 0 0 0.4353,0.4353 h 0.8705 a 1.306,1.306 0 0 0 1.306,-1.306 v -4.788 A 0.4353,0.4353 0 0 0 61.924,22.31 h -1.741 a 0.4353,0.4353 0 0 0 -0.4353,0.4353 z m 2.612,-1.741 v -0.8706 a 1.306,1.306 0 0 0 -1.306,-1.306 H 60.183 a 0.4353,0.4353 0 0 0 -0.4353,0.4353 v 1.741 a 0.4353,0.4353 0 0 0 0.4353,0.4353 h 1.741 a 0.4353,0.4353 0 0 0 0.4353,-0.4353 z m -3.482,-6.203 v 11.86 a 0.4353,0.4353 0 0 1 -0.4353,0.4353 h -17.41 A 0.4353,0.4353 0 0 1 40.5969,26.661 v -11.86 a 6.42,6.42 0 0 1 6.42,-6.42 h 0.3809 A 0.1632,0.1632 0 0 0 47.561,8.2178 V 7.5105 a 2.179,2.179 0 0 1 2.229,-2.176 c 1.188,0.0282 2.124,1.028 2.124,2.217 v 0.6664 a 0.1632,0.1632 0 0 0 0.1632,0.1632 h 0.3809 a 6.42,6.42 0 0 1 6.42,6.42 z m -10.45,-6.583 a 0.1632,0.1632 0 0 0 0.1632,0.1632 h 2.285 A 0.1632,0.1632 0 0 0 51.0389,8.218 V 7.5404 c 0,-0.7306 -0.5978,-1.348 -1.328,-1.336 a 1.307,1.307 0 0 0 -1.283,1.306 z m -0.4353,4.081 a 1.741,1.741 0 1 0 1.741,-1.741 1.741,1.741 0 0 0 -1.741,1.741 z m 8.27,5.223 a 1.307,1.307 0 0 0 -1.306,-1.306 h -10.45 a 1.307,1.307 0 0 0 -1.306,1.306 v 6.529 a 1.307,1.307 0 0 0 1.306,1.306 h 10.45 a 1.307,1.307 0 0 0 1.306,-1.306 z" + stroke-width="0.0864368" /> + <path + id="puzzle-ll" + d="m 8.484,37.13 c -1.759,0 -3.175,1.416 -3.175,3.175 v 18.89 c 0,1.759 1.416,3.175 3.175,3.175 h 18.93 c 1.759,0 3.175,-1.416 3.175,-3.175 v -18.89 c 0,-1.759 -1.416,-3.175 -3.175,-3.175 h -5.842 v 2.798 h -0.01188 c 0.0052,0.0528 0.01281,0.1048 0.01447,0.1586 0,2.002 -1.623,3.625 -3.625,3.625 -2.002,0 -3.625,-1.623 -3.625,-3.625 0.01013,-0.0551 0.02494,-0.1053 0.03669,-0.1586 H 14.32614 V 37.13 Z m 28.69,12.62 c 3e-6,2.002 -1.623,3.625 -3.625,3.625 -4.515,-0.8302 -3.163,-7.154 0,-7.251 2.002,0 3.625,1.623 3.625,3.625 z m -6.582,-3.623 h 2.798 v 7.247 h -2.798 z" + style="stroke-width:1.58861" /> + <path + id="puzzle-lr" + d="m 37.31,59.24 c 0,1.759 1.416,3.175 3.175,3.175 h 18.89 c 1.759,0 3.175,-1.416 3.175,-3.175 V 40.31 c 0,-1.759 -1.416,-3.175 -3.175,-3.175 h -18.89 c -1.759,0 -3.175,1.416 -3.175,3.175 v 5.842 h 2.798 v 0.0119 c 0.0528,-0.005 0.1048,-0.0128 0.1586,-0.0145 2.002,0 3.625,1.623 3.625,3.625 0,2.002 -1.623,3.625 -3.625,3.625 -0.0551,-0.0101 -0.1053,-0.0249 -0.1586,-0.0367 v 0.0351 H 37.31 Z" + style="stroke-width:1.58861" /> + <path + id="puzzle-ul" + d="m 31.48,20.23 c 0.9818,-0.9818 0.9818,-2.563 0,-3.544 L 20.94,6.146 c -0.9818,-0.9819 -2.563,-0.9819 -3.545,0 l -10.57,10.57 c -0.9818,0.9818 -0.9818,2.563 6e-6,3.544 l 10.54,10.54 c 0.9818,0.9819 2.563,0.9819 3.545,0 l 3.261,-3.261 -1.562,-1.562 0.0064,-0.0069 c -0.03229,-0.02696 -0.06563,-0.05145 -0.09668,-0.08057 -1.118,-1.118 -1.118,-2.93 0,-4.048 1.118,-1.118 2.93,-1.118 4.048,0 0.02514,0.0363 0.04485,0.07241 0.0681,0.109 l 0.0196,-0.01948 1.562,1.562 z M 8.42,29.199 c -1.118,-1.118 -1.118,-2.93 -6.1e-6,-4.048 2.9840001,-2.057 5.7590001,2.228 4.0480001,4.048 -1.118,1.118 -2.9300001,1.118 -4.0480001,0 z m 5.697,-1.652 -1.562,1.562 -4.045,-4.045 1.562,-1.562 z" + style="stroke-width:1.58861" /> + </g> + </g> +</svg> diff --git a/aleksis/core/tasks.py b/aleksis/core/tasks.py index 13eb76444b77e84ca7509958f02517c98b678296..97ccfa2270b9d87ab8698414ce87c53f06446d22 100644 --- a/aleksis/core/tasks.py +++ b/aleksis/core/tasks.py @@ -40,3 +40,11 @@ def backup_data() -> None: # Hand off to dbbackup's management commands management.call_command("dbbackup", *db_options) management.call_command("mediabackup", *media_options) + + +@app.task(run_every=timedelta(days=1)) +def clear_oauth_tokens(): + """Clear expired OAuth2 tokens.""" + from oauth2_provider.models import clear_tokens # noqa + + return clear_tokens() diff --git a/aleksis/core/templates/core/partials/meta.html b/aleksis/core/templates/core/partials/meta.html index cae0f93b801ab0157f61bbe319067ac49979002c..30b2ef099f0914f8ab68df34c047732293d91d5b 100644 --- a/aleksis/core/templates/core/partials/meta.html +++ b/aleksis/core/templates/core/partials/meta.html @@ -6,7 +6,7 @@ <meta name="description" content="{{ SITE_PREFERENCES.general__description }}"/> <meta name="generator" content="AlekSIS School Information System"/> -<meta name="theme-color" content="red"> +<meta name="theme-color" content="{{ SITE_PREFERENCES.theme__primary }}"> <meta name="mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-title" content="{{ SITE_PREFERENCES.general__title }}"> diff --git a/aleksis/core/tests/regression/test_regression.py b/aleksis/core/tests/regression/test_regression.py index 7ac541531db385ea8e28b454dc487458a45d7a66..b9d33e6981312817324b92d4fe67176b96763cda 100644 --- a/aleksis/core/tests/regression/test_regression.py +++ b/aleksis/core/tests/regression/test_regression.py @@ -1,3 +1,12 @@ +from django.contrib.auth import get_user_model + +import pytest + +from aleksis.core.models import Group, Person + +pytestmark = pytest.mark.django_db + + def test_all_settigns_registered(): """Tests for regressions of preferences not being registered. @@ -33,3 +42,43 @@ def test_custom_managers_return_correct_qs(): assert isinstance(Manager.from_queryset(QuerySet)().get_queryset(), QuerySet) _check_get_queryset(managers.GroupManager, managers.GroupQuerySet) + + +def test_reassign_user_to_person(): + """Tests that on re-assigning a user, groups are correctly synced. + + https://edugit.org/AlekSIS/official/AlekSIS-Core/-/issues/628 + """ + + User = get_user_model() + + group1 = Group.objects.create(name="Group 1") + group2 = Group.objects.create(name="Group 2") + + user1 = User.objects.create(username="user1") + user2 = User.objects.create(username="user2") + + person1 = Person.objects.create(first_name="Person", last_name="1", user=user1) + person2 = Person.objects.create(first_name="Person", last_name="2", user=user2) + + person1.member_of.set([group1]) + person2.member_of.set([group2]) + + assert user1.groups.count() == 1 + assert user2.groups.count() == 1 + assert user1.groups.first().name == "Group 1" + assert user2.groups.first().name == "Group 2" + + person1.user = None + person1.save() + assert user1.groups.count() == 0 + + person2.user = user1 + person2.save() + person1.user = user2 + person1.save() + + assert user1.groups.count() == 1 + assert user2.groups.count() == 1 + assert user1.groups.first().name == "Group 2" + assert user2.groups.first().name == "Group 1" diff --git a/aleksis/core/views.py b/aleksis/core/views.py index ec38875ad876f6cc7f6b8943a70254a28704195b..539807bf19434b3ae812afe9fa8f317063be8654 100644 --- a/aleksis/core/views.py +++ b/aleksis/core/views.py @@ -175,6 +175,7 @@ class ManifestView(View): { "src": favicon_img.faviconImage.url, "sizes": f"{favicon_img.size}x{favicon_img.size}", + "purpose": "maskable" if prefs["theme__pwa_icon_maskable"] else "any", } for favicon_img in pwa_imgs ] diff --git a/pyproject.toml b/pyproject.toml index 9c6f2e14986568e10b42597cbb10948681006aec..0587d6373842c71e486667f13036971d2cd9cb72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,7 +113,7 @@ django-allauth = "^0.47.0" django-uwsgi-ng = "^1.1.0" django-extensions = "^3.1.1" ipython = "^8.0.0" -django-oauth-toolkit = "^1.6.2" +django-oauth-toolkit = "^1.7.0" django-redis = "^5.0.0" django-storages = {version = "^1.11.1", optional = true} boto3 = {version = "^1.17.33", optional = true}