diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d4635a2169b2b0b5d6377614002d39532c926c15..50450c0854955fe4b8c6398f9bf5dda47f2f2cec 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,8 +9,27 @@ and this project adheres to `Semantic Versioning`_. Unreleased ---------- -`3.1.1` - 2023-07-01 --------------------- +`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 ~~~~~ @@ -165,13 +184,13 @@ 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 + * 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 + * 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` @@ -198,8 +217,8 @@ 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 + * It caused major performance issues and is not useful with the new + frontend anymore `2.12.3`_ - 2023-03-07 ---------------------- @@ -350,9 +369,7 @@ 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 - + * 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. @@ -556,11 +573,9 @@ 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 - + * 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 @@ -1183,3 +1198,4 @@ Fixed .. _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 diff --git a/Dockerfile b/Dockerfile index 2134ae81150a13dbab8282149a04df0fb8d20235..4a01dcc62c57fbcef4f56299f39f08d43675fb84 100644 --- a/Dockerfile +++ b/Dockerfile @@ -126,7 +126,7 @@ ONBUILD RUN set -e; \ eatmydata pip install $APPS; \ fi; \ eatmydata aleksis-admin vite build; \ - eatmydata aleksis-admin collectstatic --no-input; \ + 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; \ diff --git a/aleksis/core/frontend/components/LegacyBaseTemplate.vue b/aleksis/core/frontend/components/LegacyBaseTemplate.vue index 0105952fd08792c2cb4a472465d435a1f94a0eb3..6dc73749d8bb716bd32089e11afe36f6d93455c9 100644 --- a/aleksis/core/frontend/components/LegacyBaseTemplate.vue +++ b/aleksis/core/frontend/components/LegacyBaseTemplate.vue @@ -21,10 +21,9 @@ </message-box> <iframe v-else - :src="'/django' + $route.path + queryString" + :src="iFrameSrc" :height="iFrameHeight + 'px'" class="iframe-fullsize" - @load="load" ref="contentIFrame" ></iframe> </template> @@ -41,6 +40,7 @@ export default { data: function () { return { iFrameHeight: 0, + iFrameSrc: undefined, }; }, computed: { @@ -53,13 +53,16 @@ export default { }, }, methods: { + getIFrameURL() { + const location = this.$refs.contentIFrame.contentWindow.location; + const url = new URL(location); + return url; + }, /** Handle iframe data after inner page loaded */ load() { // Write new location of iframe back to Vue Router - const location = this.$refs.contentIFrame.contentWindow.location; - const url = new URL(location); - const path = url.pathname.replace(/^\/django/, ""); - const pathWithQueryString = path + encodeURI(url.search); + const path = this.getIFrameURL().pathname.replace(/^\/django/, ""); + const pathWithQueryString = path + encodeURI(this.getIFrameURL().search); const routePath = path.charAt(path.length - 1) === "/" && this.$route.path.charAt(path.length - 1) !== "/" @@ -103,15 +106,36 @@ export default { }, }, watch: { - $route() { + $route(newRoute) { // Show loading animation once route changes this.$root.contentLoading = true; + // Only reload iFrame content when navigation comes from outsite the iFrame + const path = this.getIFrameURL().pathname.replace(/^\/django/, ""); + const routePath = + path.charAt(path.length - 1) === "/" && + newRoute.path.charAt(path.length - 1) !== "/" + ? newRoute.path + "/" + : newRoute.path; + + if (path !== routePath) { + this.$refs.contentIFrame.contentWindow.location = + "/django" + this.$route.path + this.queryString; + } else { + this.$root.contentLoading = false; + } + // Scroll to top only when route changes to not affect form submits etc. // A small duration to avoid flashing of the UI this.$vuetify.goTo(0, { duration: 10 }); }, }, + mounted() { + this.$refs.contentIFrame.addEventListener("load", (e) => { + this.load(); + }); + this.iFrameSrc = "/django" + this.$route.path + this.queryString; + }, name: "LegacyBaseTemplate", }; </script> diff --git a/aleksis/core/frontend/components/notifications/NotificationList.vue b/aleksis/core/frontend/components/notifications/NotificationList.vue index 09b27197e5d62c5a5f15f7836d5b741925d21cc8..e48d9f2c203a2d2730af1a7208f3590667ede7bf 100644 --- a/aleksis/core/frontend/components/notifications/NotificationList.vue +++ b/aleksis/core/frontend/components/notifications/NotificationList.vue @@ -20,7 +20,7 @@ v-if=" myNotifications && myNotifications.person && - myNotifications.person.notifications.length > 0 + unreadNotifications.length > 0 " > mdi-bell-badge-outline @@ -38,7 +38,7 @@ v-if=" myNotifications.person && myNotifications.person.notifications && - unreadNotifications.length + myNotifications.person.notifications.length " > <v-subheader>{{ $t("notifications.notifications") }}</v-subheader> @@ -88,7 +88,9 @@ export default { }, computed: { unreadNotifications() { - return this.myNotifications.filter((n) => !n.read); + return this.myNotifications.person.notifications + ? this.myNotifications.person.notifications.filter((n) => !n.read) + : []; }, }, }; diff --git a/aleksis/core/mixins.py b/aleksis/core/mixins.py index 1426b9c5ff244c31286e31542e690aeb0e8e3b77..b55a700db5390895c2c827a9ec13931bc55dbac9 100644 --- a/aleksis/core/mixins.py +++ b/aleksis/core/mixins.py @@ -27,7 +27,7 @@ from dynamic_preferences.types import FilePreference from guardian.admin import GuardedModelAdmin from guardian.core import ObjectPermissionChecker from jsonstore.fields import IntegerField, JSONFieldMixin -from material.base import Layout, LayoutNode +from material.base import Fieldset, Layout, LayoutNode from polymorphic.base import PolymorphicModelBase from polymorphic.managers import PolymorphicManager from polymorphic.models import PolymorphicModel @@ -458,6 +458,20 @@ class ExtensibleForm(ModelForm, metaclass=_ExtensibleFormMetaclass): cls.base_layout.append(node) cls.layout = Layout(*cls.base_layout) + visit_nodes = [node] + while visit_nodes: + current_node = visit_nodes.pop() + if isinstance(current_node, Fieldset): + visit_nodes += node.elements + else: + field_name = ( + current_node if isinstance(current_node, str) else current_node.field_name + ) + field = fields_for_model(cls._meta.model, [field_name])[field_name] + cls._meta.fields.append(field_name) + cls.base_fields[field_name] = field + setattr(cls, field_name, field) + class BaseModelAdmin(GuardedModelAdmin, ObjectPermissionsModelAdmin): """A base class for ModelAdmin combining django-guardian and rules.""" diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py index 9b7792d56f441b5b848e0fd1603036f3cbbcbf15..44790d7cf405402424ee51042efc7b98e2597936 100644 --- a/aleksis/core/settings.py +++ b/aleksis/core/settings.py @@ -936,12 +936,19 @@ LOGGING["root"] = { "handlers": ["console"], "level": _settings.get("logging.level", "WARNING"), } +# Configure global log Format +LOGGING["formatters"]["verbose"] = { + "format": "{asctime} {levelname} {name}[{process}]: {msg}", + "style": "{", +} # Add null handler for selective silencing LOGGING["handlers"]["null"] = {"class": "logging.NullHandler"} # Make console logging independent of DEBUG LOGGING["handlers"]["console"]["filters"].remove("require_debug_true") # Use root log level for console del LOGGING["handlers"]["console"]["level"] +# Use verbose log format for console +LOGGING["handlers"]["console"]["formatter"] = "verbose" # Disable exception mails if not desired if not _settings.get("logging.mail_admins", True): LOGGING["loggers"]["django"]["handlers"].remove("mail_admins") @@ -957,6 +964,9 @@ LOGGING["loggers"]["celery"] = { "level": _settings.get("logging.level", "WARNING"), "propagate": False, } +# Set Django log levels +LOGGING["loggers"]["django"]["level"] = _settings.get("logging.level", "WARNING") +LOGGING["loggers"]["django.server"]["level"] = _settings.get("logging.level", "WARNING") # Rules and permissions diff --git a/aleksis/core/util/apps.py b/aleksis/core/util/apps.py index e370d6b125242381ab53f6c030c62d41c622d0ff..26f0752dc352cf0793ad7a9c7d70f95e015dd457 100644 --- a/aleksis/core/util/apps.py +++ b/aleksis/core/util/apps.py @@ -1,3 +1,4 @@ +import logging from importlib import metadata from typing import TYPE_CHECKING, Any, Optional, Sequence @@ -26,8 +27,11 @@ class AppConfig(django.apps.AppConfig): def __init_subclass__(cls): super().__init_subclass__() cls.default = True + cls._logger = logging.getLogger(f"{cls.__module__}.{cls.__name__}") def ready(self): + self._logger.debug("Running app.ready") + super().ready() # Register default listeners @@ -36,9 +40,12 @@ class AppConfig(django.apps.AppConfig): preference_updated.connect(self.preference_updated) user_logged_in.connect(self.user_logged_in) user_logged_out.connect(self.user_logged_out) + self._logger.debug("Default signal handlers connected") # Getting an app ready means it should look at its config once + self._logger.debug("Force-loading preferences") self.preference_updated(self) + self._logger.debug("Preferences loaded") def get_distribution_name(self): """Get distribution name of application package.""" @@ -282,6 +289,8 @@ class AppConfig(django.apps.AppConfig): return {} def _maintain_default_data(self): + self._logger.debug("Maintaining default data for %s", self.get_name()) + from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType @@ -292,10 +301,19 @@ class AppConfig(django.apps.AppConfig): for model in self.get_models(): if hasattr(model, "maintain_default_data"): # Method implemented by each model object; can be left out + self._logger.info( + "Maintaining default data of %s in %s", model._meta.model_name, self.get_name() + ) model.maintain_default_data() if hasattr(model, "extra_permissions"): + self._logger.info( + "Maintaining extra permissions for %s in %s", + model._meta.model_name, + self.get_name(), + ) ct = ContentType.objects.get_for_model(model) for perm, verbose_name in model.extra_permissions: + self._logger.debug("Creating %s (%s)", perm, verbose_name) Permission.objects.get_or_create( codename=perm, content_type=ct, diff --git a/aleksis/core/vite.config.js b/aleksis/core/vite.config.js index 287534f096c186bc428de742c4e5ff56b71e8eab..415d12aa00f6b8a2bf2747e47d5cc3e32fd7aaa3 100644 --- a/aleksis/core/vite.config.js +++ b/aleksis/core/vite.config.js @@ -201,7 +201,9 @@ export default defineConfig({ navigateFallback: "/", directoryIndex: null, navigateFallbackAllowlist: [ - new RegExp("^/(?!(django|admin|graphql|__icons__))[^.]*$"), + new RegExp( + "^/(?!(django|admin|graphql|__icons__|oauth/authorize))[^.]*$" + ), ], additionalManifestEntries: [ { url: "/", revision: crypto.randomUUID() }, @@ -215,7 +217,7 @@ export default defineConfig({ runtimeCaching: [ { urlPattern: new RegExp( - "^/(?!(django|admin|graphql|__icons__))[^.]*$" + "^/(?!(django|admin|graphql|__icons__|oauth/authorize))[^.]*$" ), handler: "CacheFirst", }, diff --git a/docs/admin/10_install.rst b/docs/admin/10_install.rst index 03ffc97b1e73b28561f2283338773672e4c41a73..b829cd2c92710efa6050f068319ef052e9b4138e 100644 --- a/docs/admin/10_install.rst +++ b/docs/admin/10_install.rst @@ -147,7 +147,7 @@ After that, you can install the aleksis meta-package, or only `aleksis-core`: .. code-block:: shell pip3 install --break-system-packages aleksis aleksis-admin vite build - aleksis-admin collectstatic + aleksis-admin collectstatic --clear aleksis-admin migrate aleksis-admin createinitialrevisions diff --git a/docs/dev/01_setup.rst b/docs/dev/01_setup.rst index f7f56f811bcd7b0bfca48fbf9793a026ff2a44bf..c98eb81925a370ad1e40ee8f000a70a8330892cc 100644 --- a/docs/dev/01_setup.rst +++ b/docs/dev/01_setup.rst @@ -7,7 +7,8 @@ by reading its documentation. Poetry makes a lot of stuff very easy, especially managing a virtual environment that contains AlekSIS and everything you need to run the -framework and selected apps. +framework and selected apps. The minimum supported version of Poetry +is 1.2.0. Also, `Yarn`_ is needed to resolve JavaScript dependencies. @@ -91,7 +92,7 @@ All three steps can be done with the ``poetry shell`` command and ALEKSIS_maintenance__debug=true ALEKSIS_database__password=aleksis poetry shell poetry run aleksis-admin vite build - poetry run aleksis-admin collectstatic + poetry run aleksis-admin collectstatic --clear poetry run aleksis-admin compilemessages poetry run aleksis-admin migrate poetry run aleksis-admin createinitialrevisions diff --git a/pyproject.toml b/pyproject.toml index 468bf0a08826da58217865f095e3aee7b202d380..49d4a237181de7a821fe3eb5715178ae47cd3fd6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,12 +28,9 @@ authors = [ "Benedict Suska <benedict.suska@teckids.de>", "Lukas Weichelt <lukas.weichelt@teckids.de>" ] -maintainers = [ - "Jonathan Weth <dev@jonathanweth.de>", - "Dominik George <dominik.george@teckids.org>" -] +maintainers = ["Jonathan Weth <dev@jonathanweth.de>", "Dominik George <dominik.george@teckids.org>"] license = "EUPL-1.2-or-later" -homepage = "https://aleksis.org/" +homepage = "https://aleksis.org" repository = "https://edugit.org/AlekSIS/official/AlekSIS-Core" documentation = "https://aleksis.org/AlekSIS-Core/docs/html/" keywords = ["SIS", "education", "school", "digitisation", "school apps"] @@ -49,11 +46,14 @@ classifiers = [ "Typing :: Typed", ] +[[tool.poetry.source]] +name = "PyPI" +priority = "primary" + [[tool.poetry.source]] name = "gitlab" url = "https://edugit.org/api/v4/projects/461/packages/pypi/simple" -secondary = true - +priority = "supplemental" [tool.poetry.dependencies] python = "^3.9" Django = "^4.1" @@ -128,24 +128,61 @@ graphene-django = ">=3.0.0, <=3.1.1" selenium = "^4.4.3" django-vite = "^2.0.2" graphene-django-cud = "^0.10.0" +uwsgi = "^2.0.21" [tool.poetry.extras] ldap = ["django-auth-ldap"] s3 = ["boto3", "django-storages"] sentry = ["sentry-sdk"] -[tool.poetry.dev-dependencies] -aleksis-builddeps = {version=">=2023.1.dev0", allow-prereleases=true} -selenium = "<4.10.0" -uwsgi = "^2.0" - [tool.poetry.scripts] aleksis-admin = 'aleksis.core.__main__:aleksis_cmd' +[tool.poetry.group.dev.dependencies] +django-stubs = "^4.2" + +safety = "^2.3.5" + +flake8 = "^6.0.0" +flake8-django = "^1.0.0" +flake8-fixme = "^1.1.1" +flake8-mypy = "^17.8.0" +flake8-bandit = "^4.1.1" +flake8-builtins = "^2.0.0" +flake8-docstrings = "^1.5.0" +flake8-rst-docstrings = "^0.3.0" + +black = ">=21.0" +flake8-black = "^0.3.0" + +isort = "^5.0.0" +flake8-isort = "^6.0.0" + +curlylint = "^0.13.0" + +[tool.poetry.group.test.dependencies] +pytest = "^7.2" +pytest-django = "^4.1" +pytest-django-testing-postgresql = "^0.2" +pytest-cov = "^4.0.0" +pytest-sugar = "^0.9.2" +selenium = "<4.10.0" +freezegun = "^1.1.0" + +[tool.poetry.group.docs] +optional = true + +[tool.poetry.group.docs.dependencies] +sphinx = "^7.0" +sphinxcontrib-django = "^2.3.0" +sphinxcontrib-svg2pdfconverter = "^1.1.1" +sphinx-autodoc-typehints = "^1.7" +sphinx_material = "^0.0.35" + [tool.black] line-length = 100 exclude = "/migrations/" [build-system] -requires = ["poetry>=1.0"] -build-backend = "poetry.masonry.api" +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api"