diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c13f3964e44cab97fb4a26f7e60c2dd5b2e98421..373ae44ccda6be3c72bb809a8ac2821fc69dcecb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -19,6 +19,12 @@ Changed ~~~~~~~ * Rewrite of frontend using Vuetify +* 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 +* Persistent database connections are now health-checked as to not fail + requests * Incorporate SPDX license list for app licenses on About page * [Dev] The undocumented field `check` on `DataCheckResult` was renamed to `data_check` diff --git a/aleksis/core/__init__.py b/aleksis/core/__init__.py index 66d1ef788b034aea3d1518bc009e5be0ca05b3f2..df69a63b63a08043dd6de7a3344eee787f3acfd1 100644 --- a/aleksis/core/__init__.py +++ b/aleksis/core/__init__.py @@ -6,5 +6,3 @@ try: __version__ = metadata.distribution("AlekSIS-Core").version except Exception: __version__ = "unknown" - -default_app_config = "aleksis.core.apps.CoreConfig" diff --git a/aleksis/core/mixins.py b/aleksis/core/mixins.py index 6b54f8e40a1873a9ba35379136bc0c9723889d93..f4ae9a9d0c28f3539b1f4359785db555557c2bbc 100644 --- a/aleksis/core/mixins.py +++ b/aleksis/core/mixins.py @@ -6,7 +6,7 @@ from typing import Any, Callable, List, Optional, Union from django.conf import settings from django.contrib import messages -from django.contrib.auth.views import LoginView, SuccessURLAllowedHostsMixin +from django.contrib.auth.views import LoginView, RedirectURLMixin from django.contrib.contenttypes.models import ContentType from django.contrib.sites.managers import CurrentSiteManager from django.contrib.sites.models import Site @@ -465,7 +465,7 @@ class SuccessMessageMixin(ModelFormMixin): return super().form_valid(form) -class SuccessNextMixin(SuccessURLAllowedHostsMixin): +class SuccessNextMixin(RedirectURLMixin): redirect_field_name = "next" def get_success_url(self) -> str: @@ -492,8 +492,8 @@ class AdvancedDeleteView(DeleteView): success_message: Optional[str] = None - def delete(self, request, *args, **kwargs): - r = super().delete(request, *args, **kwargs) + def form_valid(self, form): + r = super().form_valid(form) if self.success_message: messages.success(self.request, self.success_message) return r diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py index 435169a49f867cc92d8e9c552eec14ec35d041fd..daafab787c5112d393e5e547e7ba0638149efd64 100644 --- a/aleksis/core/settings.py +++ b/aleksis/core/settings.py @@ -68,6 +68,8 @@ BASE_URL = _settings.get( "http.base_url", "http://localhost:8000" if DEBUG else f"https://{ALLOWED_HOSTS[0]}" ) +CSRF_TRUSTED_ORIGINS = _settings.get("http.trusted_origins", []) + # Application definition INSTALLED_APPS = [ "django.contrib.admin", @@ -128,7 +130,6 @@ INSTALLED_APPS = [ "material", "ckeditor", "ckeditor_uploader", - "django_js_reverse", "colorfield", "django_bleach", "favicon", @@ -212,6 +213,7 @@ DATABASES = { "HOST": _settings.get("database.host", "127.0.0.1"), "PORT": _settings.get("database.port", "5432"), "CONN_MAX_AGE": _settings.get("database.conn_max_age", None), + "CONN_HEALTH_CHECK": True, "OPTIONS": _settings.get("database.options", {}), } } @@ -225,6 +227,12 @@ DATABASE_OOT_LABELS = ["django_celery_results"] merge_app_settings("DATABASES", DATABASES, False) +PASSWORD_HASHERS = [ + "django.contrib.auth.hashers.ScryptPasswordHasher", + "django.contrib.auth.hashers.PBKDF2PasswordHasher", + "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher", +] + REDIS_HOST = _settings.get("redis.host", "localhost") REDIS_PORT = _settings.get("redis.port", 6379) REDIS_DB = _settings.get("redis.database", 0) @@ -239,15 +247,10 @@ REDIS_URL = ( if _settings.get("caching.redis.enabled", not IN_PYTEST): CACHES = { "default": { - "BACKEND": "django_redis.cache.RedisCache", + "BACKEND": "django.core.cache.backends.redis.RedisCache", "LOCATION": _settings.get("caching.redis.address", REDIS_URL), - "OPTIONS": { - "CLIENT_CLASS": "django_redis.client.DefaultClient", - }, } } - if REDIS_PASSWORD: - CACHES["default"]["OPTIONS"]["PASSWORD"] = REDIS_PASSWORD else: CACHES = { "default": { @@ -529,16 +532,14 @@ LANGUAGES = [ ] LANGUAGE_CODE = _settings.get("l10n.lang", "en") TIME_ZONE = _settings.get("l10n.tz", "UTC") -USE_I18N = True -USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.1/howto/static-files/ -STATIC_URL = _settings.get("static.url", "/static/") -MEDIA_URL = _settings.get("media.url", "/media/") +STATIC_URL = _settings.get("static.url", "static/") +MEDIA_URL = _settings.get("media.url", "media/") LOGIN_REDIRECT_URL = "index" LOGOUT_REDIRECT_URL = "index" diff --git a/aleksis/core/templates/core/base.html b/aleksis/core/templates/core/base.html index 582a709d2f3c0a6935ebb218de7ae117336fa7bc..6b9ff71ab8ca354166ec5436ac682f498974f798 100644 --- a/aleksis/core/templates/core/base.html +++ b/aleksis/core/templates/core/base.html @@ -26,9 +26,6 @@ {% include_css "Roboto900" %} <link rel="stylesheet" href="{% sass_src 'public/style.scss' %}"> - {# Add JS URL resolver #} - <script src="{% url "js_reverse" %}" type="text/javascript"></script> - {# Add i18n names for calendar (for use in datepicker) #} {# Passing the locale is not necessary for the scripts to work, but prevents caching issues #} <script src="{% url "javascript-catalog" %}?locale={{ LANGUAGE_CODE }}" type="text/javascript"></script> diff --git a/aleksis/core/templates/core/vue_base.html b/aleksis/core/templates/core/vue_base.html index 85abd8e79d4752aa79e771c5d251dfb1fe450f53..261c3bb81e9fd24b33726afe369b239546e31faf 100644 --- a/aleksis/core/templates/core/vue_base.html +++ b/aleksis/core/templates/core/vue_base.html @@ -26,9 +26,6 @@ {% include_css "Roboto700" %} {% include_css "Roboto900" %} - {# Add JS URL resolver #} - <script src="{% url "js_reverse" %}" type="text/javascript"></script> - {# Add i18n names for calendar (for use in datepicker) #} {# Passing the locale is not necessary for the scripts to work, but prevents caching issues #} <script src="{% url "javascript-catalog" %}?locale={{ LANGUAGE_CODE }}" type="text/javascript"></script> diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py index b23b6f1d98a5b54322a6fec86b67d4bf21b900eb..806a284ac9626dbe516f8910e72e6b38775b6435 100644 --- a/aleksis/core/urls.py +++ b/aleksis/core/urls.py @@ -8,7 +8,6 @@ from django.views.i18n import JavaScriptCatalog import calendarweek.django from ckeditor_uploader import views as ckeditor_uploader_views -from django_js_reverse.views import urls_js from health_check.urls import urlpatterns as health_urls from oauth2_provider.views import ConnectDiscoveryInfoView from rules.contrib.views import permission_required @@ -155,7 +154,6 @@ urlpatterns = [ name="ckeditor_browse", ), path("select2/", include("django_select2.urls")), - path("jsreverse.js", urls_js, name="js_reverse"), path("calendarweek_i18n.js", calendarweek.django.i18n_js, name="calendarweek_i18n_js"), path("gettext.js", JavaScriptCatalog.as_view(), name="javascript-catalog"), path( diff --git a/aleksis/core/util/apps.py b/aleksis/core/util/apps.py index 7bb7f7bdab7ac4675747f3b5fc4e75ab3760f697..0889280858a64a61cd8857a0c6eef4f872012cfc 100644 --- a/aleksis/core/util/apps.py +++ b/aleksis/core/util/apps.py @@ -20,8 +20,13 @@ if TYPE_CHECKING: class AppConfig(django.apps.AppConfig): """An extended version of DJango's AppConfig container.""" + default = False default_auto_field = "django.db.models.BigAutoField" + def __init_subclass__(cls): + super().__init_subclass__() + cls.default = True + def ready(self): super().ready() diff --git a/aleksis/core/views.py b/aleksis/core/views.py index 6f01d8e53b4ccad0cf9bccbce8fc4bc2b2b65008..bbbe7cc1871735e006c64f0fd8726d39312ec6a1 100644 --- a/aleksis/core/views.py +++ b/aleksis/core/views.py @@ -1380,9 +1380,8 @@ class SocialAccountDeleteView(DeleteView): def get_queryset(self): return SocialAccount.objects.filter(user=self.request.user) - def delete(self, request, *args, **kwargs): + def form_valid(self, form): self.object = self.get_object() - success_url = self.get_success_url() try: get_adapter(self.request).validate_disconnect( self.object, SocialAccount.objects.filter(user=self.request.user) @@ -1400,7 +1399,7 @@ class SocialAccountDeleteView(DeleteView): messages.success( self.request, _("The third-party account has been successfully disconnected.") ) - return HttpResponseRedirect(success_url) + return super().form_valid() def server_error( diff --git a/pyproject.toml b/pyproject.toml index 499a246b828687e3ccce563073a1219642266297..c9a5f25b70fabae18809434113aeed5cdef4a1d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ keywords = ["SIS", "education", "school", "digitisation", "school apps"] classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", - "Framework :: Django :: 3.0", + "Framework :: Django :: 4.0", "Intended Audience :: Developers", "Intended Audience :: Education", "Topic :: Education", @@ -56,7 +56,7 @@ secondary = true [tool.poetry.dependencies] python = "^3.9" -Django = "^3.2.5" +Django = "^4.1" django-any-js = "^1.1" django-menu-generator-ng = "^1.2.3" django-tables2 = "^2.1" @@ -80,7 +80,6 @@ django-filter = "^2.2.0" django-templated-email = "^3.0.0" html2text = "^2020.0.0" django-ckeditor = "^6.0.0" -django-js-reverse = "^0.9.1" calendarweek = "^0.5.0" Celery = {version="^5.2", extras=["django", "redis"]} django-celery-results = "^2.0.1" @@ -95,7 +94,7 @@ rules = "^3.0" django-cache-memoize = "^0.1.6" django-haystack = "^3.1" celery-haystack-ng = "^2.0" -django-dbbackup = "^3.3.0" +django-dbbackup = "^4.0.0" license-expression = "^30.0" django-reversion = "^5.0.0" django-favicon-plus-reloaded = "^1.1.5" @@ -109,7 +108,7 @@ bs4 = "^0.0.1" django-invitations = "^2.0.0" django-cleavejs = "^0.1.0" django-allauth = "^0.51.0" -django-uwsgi-ng = "^1.1.0" +django-uwsgi-ng = "^2.0" django-extensions = "^3.1.1" ipython = "^8.0.0" django-oauth-toolkit = "^2.0.0"