diff --git a/aleksis/core/__init__.py b/aleksis/core/__init__.py index 21a49f0785f3977d9a265059a32ec957c9e1eb3c..587b829c611913af41c69f2734a48235956ff787 100644 --- a/aleksis/core/__init__.py +++ b/aleksis/core/__init__.py @@ -1,5 +1,7 @@ import pkg_resources +from django.utils.translation import gettext_lazy as _ + try: from .celery import app as celery_app except ModuleNotFoundError: diff --git a/aleksis/core/apps.py b/aleksis/core/apps.py index 13e3b3ddcf1083e1cff0f8c5e3461103bb12f4b2..3139249b79c2afb701ec9405eea5a6cec1a4526b 100644 --- a/aleksis/core/apps.py +++ b/aleksis/core/apps.py @@ -13,6 +13,20 @@ class CoreConfig(AppConfig): name = "aleksis.core" verbose_name = "AlekSIS — The Free School Information System" + urls = { + "Repository": "https://edugit.org/AlekSIS/official/AlekSIS/", + } + licence = "EUPL-1.2+" + copyright = ( + ([2017, 2018, 2019, 2020], "Jonathan Weth", "wethjo@katharineum.de"), + ([2017, 2018, 2019], "Frank Poetzsch-Heffter", "p-h@katharineum.de"), + ([2018, 2019, 2020], "Hangzhi Yu", "yuha@katharineum.de"), + ([2018, 2019, 2020], "Julian Leucker", "leuckeju@katharineum.de"), + ([2019, 2020], "Dominik George", "dominik.george@teckids.org"), + ([2019, 2020], "mirabilos", "thorsten.glaser@teckids.org"), + ([2019, 2020], "Tom Teichler", "tom.teichler@teckids.org"), + ) + def config_updated(self, *args, **kwargs) -> None: clean_scss() diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py index eb4b3e07401fae2a830c530212747877ced2af93..81c00f57599429b58494b7e38fb5a6f5651fdbb1 100644 --- a/aleksis/core/settings.py +++ b/aleksis/core/settings.py @@ -1,7 +1,9 @@ import os import sys from glob import glob +from importlib import import_module +from django.apps import apps from django.utils.translation import gettext_lazy as _ from calendarweek.django import i18n_day_name_choices_lazy diff --git a/aleksis/core/templates/core/about.html b/aleksis/core/templates/core/about.html new file mode 100644 index 0000000000000000000000000000000000000000..5b9097f8e294b099151d90dc91fbb583bd9e6dc9 --- /dev/null +++ b/aleksis/core/templates/core/about.html @@ -0,0 +1,122 @@ +{# -*- engine:django -*- #} +{% extends "core/base.html" %} +{% load i18n %} + + +{% block browser_title %}{% blocktrans %}About AlekSIS{% endblocktrans %}{% endblock %} +{% block page_title %}{% blocktrans %}AlekSIS – The Free School Information System{% endblocktrans %}{% endblock %} + +{% block content %} + + <div class="row"> + <div class="col s12"> + <div class="card"> + <div class="card-content"> + <span class="card-title">{% blocktrans %}About AlekSIS{% endblocktrans %}</span> + <p> + {% blocktrans %} + This platform is powered by AlekSIS, a web-based school information system (SIS) which can be used + to manage and/or publish organisational subjects of educational institutions. AlekSIS is free software and + can be used by everyone. + {% endblocktrans %} + </p> + </div> + <div class="card-action"> + <a class="" href="https://aleksis.org/">{% trans "Website of AlekSIS" %}</a> + <a class="" href="https://edugit.org/AlekSIS/">{% trans "Source code" %}</a> + </div> + </div> + </div> + </div> + <div class="row"> + <div class="col s12"> + <div class="card"> + <div class="card-content"> + <span class="card-title">{% trans "Licence information" %}</span> + <p> + {% blocktrans %} + The core and the official apps of AlekSIS are licenced under the EUPL, version 1.2 or later. For licence + information from third-party apps, if installed, see directly at the respective components below. The + licences are marked like this: + {% endblocktrans %} + </p> + <br/> + <p> + <span class="chip green white-text">{% trans "Free/Open Source Licence" %}</span> + <span class="chip orange white-text">{% trans "Other Licence" %}</span> + </p> + </div> + <div class="card-action"> + <a href="https://eupl.eu">{% trans "Full licence text" %}</a> + <a href="https://joinup.ec.europa.eu/collection/eupl/guidelines-users-and-developers">{% trans "More information about the EUPL" %}</a> + </div> + </div> + </div> + </div> + + <div class="row"> + {% for app_config in app_configs %} + <div class="col s12 m12 l6"> + <div class="card " id="{{ app_config.name }}"> + <div class="card-content"> + {% if app_config.get_licence.1.isFsfLibre %} + <span class="chip green white-text right">Free Software</span> + {% elif app_config.get_licence.1.isOsiApproved %} + <span class="chip green white-text right">Open Source</span> + {% endif %} + + <span class="card-title">{{ app_config.get_name }} <small>{{ app_config.get_version }}</small></span> + + {% if app_config.get_copyright %} + <p> + {% for holder in app_config.get_copyright %} + Copyright © {{ holder.0 }} + + {% if holder.2 %} + <a href="mailto:{{ holder.2 }}">{{ holder.1 }}</a> + {% else %} + {{ holder.1 }} + {% endif %} + + <br/> + {% endfor %} + </p> + <br/> + {% endif %} + + {% if app_config.get_licence %} + {% with licence=app_config.get_licence %} + <p> + {% blocktrans with licence=licence.0 %} + This app is licenced under {{ licence }}. + {% endblocktrans %} + </p> + <br/> + <p> + {% for l in licence.2 %} + <a class="chip white-text {% if l.isOsiApproved or l.isFsfLibre %}green{% else %}orange{% endif %}" + href="{{ l.url }}"> + {{ l.name }} + </a> + {% endfor %} + </p> + {% endwith %} + {% endif %} + </div> + {% if app_config.get_urls %} + <div class="card-action"> + {% for url_name, url in app_config.get_urls.items %} + <a href="{{ url }}">{{ url_name }}</a> + {% endfor %} + </div> + {% endif %} + </div> + </div> + {% if forloop.counter|divisibleby:2 %} + </div> + <div class="row"> + {% endif %} + {% endfor %} + </div> + +{% endblock %} diff --git a/aleksis/core/templates/core/base.html b/aleksis/core/templates/core/base.html index 8ac5ed74a195be76a558ae30e780d493ae6c2ecf..fe536557737d05a04c93d9bfbedfcc63f1bbe60b 100644 --- a/aleksis/core/templates/core/base.html +++ b/aleksis/core/templates/core/base.html @@ -146,8 +146,8 @@ <div class="footer-copyright"> <div class="container"> <div class="left"> - <a class="blue-text text-lighten-4" href="https://aleksis.org/"> - AlekSIS — The Free School Information System + <a class="blue-text text-lighten-4" href="{% url "about_aleksis" %}"> + {% trans "About AlekSIS — The Free School Information System" %} </a> © The AlekSIS Team </div> diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py index 4bb7d10fbbaa6774d5f5beed3a1d7e034f020260..5de76472c91da9243edfa13c57c5ce29a7782e08 100644 --- a/aleksis/core/urls.py +++ b/aleksis/core/urls.py @@ -16,6 +16,7 @@ from . import views urlpatterns = [ path("", include("pwa.urls"), name="pwa"), path("offline/", views.offline, name="offline"), + path("about/", views.about, name="about_aleksis"), path("admin/", admin.site.urls), path("data_management/", views.data_management, name="data_management"), path("status/", views.system_status, name="system_status"), diff --git a/aleksis/core/util/apps.py b/aleksis/core/util/apps.py index 7eef538ae05265d3492bda331eaf75875229a4ae..dbf6159252c0b20aee9fcc422fb21360c7fbc1e0 100644 --- a/aleksis/core/util/apps.py +++ b/aleksis/core/util/apps.py @@ -1,5 +1,5 @@ from importlib import import_module -from typing import Any, List, Optional, Tuple +from typing import Any, List, Optional, Tuple, Sequence import django.apps from django.contrib.auth.signals import user_logged_in, user_logged_out @@ -7,6 +7,10 @@ from django.db.models.signals import post_migrate, pre_migrate from django.http import HttpRequest from constance.signals import config_updated +from license_expression import Licensing, LicenseSymbol +from spdx_license_list import LICENSES + +from .core_helpers import copyright_years class AppConfig(django.apps.AppConfig): @@ -41,6 +45,87 @@ class AppConfig(django.apps.AppConfig): # ImportErrors are non-fatal because checks are optional. pass + @classmethod + def get_name(cls): + return getattr(cls, "verbose_name", cls.name) + # TODO Try getting from distribution if not set + + @classmethod + def get_version(cls): + try: + from .. import __version__ # noqa + except ImportError: + __version__ = None + + return getattr(cls, "version", __version__) + + @classmethod + def get_licence(cls) -> Tuple: + licence = getattr(cls, "licence", None) + + default_dict = { + 'isDeprecatedLicenseId': False, + 'isFsfLibre': False, + 'isOsiApproved': False, + 'licenseId': 'unknown', + 'name': 'Unknown Licence', + 'referenceNumber': -1, + 'url': '', + } + + if licence: + licensing = Licensing(LICENSES.keys()) + parsed = licensing.parse(licence).simplify() + readable = parsed.render_as_readable() + + flags = { + "isFsfLibre": True, + "isOsiApproved": True, + } + + licence_dicts = [] + + for symbol in parsed.symbols: + licence_dict = LICENSES.get(symbol.key.rstrip("+"), None) + + if licence_dict is None: + licence_dict = default_dict + else: + licence_dict["url"] = "https://spdx.org/licenses/{}.html".format(licence_dict["licenseId"]) + + flags["isFsfLibre"] = flags["isFsfLibre"] and licence_dict["isFsfLibre"] + flags["isOsiApproved"] = flags["isOsiApproved"] and licence_dict["isOsiApproved"] + + licence_dicts.append(licence_dict) + + return (readable, flags, licence_dicts) + else: + return ("Unknown", [default_dict]) + + @classmethod + def get_urls(cls): + return getattr(cls, "urls", {}) + # TODO Try getting from distribution if not set + + @classmethod + def get_copyright(cls) -> Sequence[Tuple[str, str, str]]: + copyrights = getattr(cls, "copyright", tuple()) + + copyrights_processed = [] + + for copyright in copyrights: + copyrights_processed.append( + ( + copyright[0] if isinstance(copyright[0], str) else copyright_years(copyright[0]), + copyright[1], + copyright[2], + ) + ) + + return copyrights_processed + + # TODO Try getting from distribution if not set + def config_updated( self, key: Optional[str] = "", diff --git a/aleksis/core/util/core_helpers.py b/aleksis/core/util/core_helpers.py index 9f91154c30913ae14eff1332b5f6bea3207bfa86..13558233068626c8f288b70624b2646840826819 100644 --- a/aleksis/core/util/core_helpers.py +++ b/aleksis/core/util/core_helpers.py @@ -1,8 +1,10 @@ from datetime import datetime, timedelta +from itertools import groupby +from operator import itemgetter import os import pkgutil from importlib import import_module -from typing import Any, Callable, Sequence, Union +from typing import Any, Callable, Sequence, Union, List from uuid import uuid4 from django.conf import settings @@ -12,6 +14,18 @@ from django.utils import timezone from django.utils.functional import lazy +def copyright_years(years: Sequence[int], seperator: str = ", ", joiner: str = "–") -> str: + """ Takes a sequence of integegers and produces a string with ranges + + >>> copyright_years([1999, 2000, 2001, 2005, 2007, 2008, 2009]) + '1999–2001, 2005, 2007–2009' + """ + + ranges = [list(map(itemgetter(1), group)) for _, group in groupby(enumerate(years), lambda e: e[1]-e[0])] + years_strs = [str(range_[0]) if len(range_) == 1 else joiner.join([str(range_[0]), str(range_[-1])]) for range_ in ranges] + + return seperator.join(years_strs) + def dt_show_toolbar(request: HttpRequest) -> bool: from debug_toolbar.middleware import show_toolbar # noqa diff --git a/aleksis/core/views.py b/aleksis/core/views.py index d3ead324d90b313ffeec3a5068df24c90bc379f4..51a89e5552158188973b2c244cfdce63b8604b02 100644 --- a/aleksis/core/views.py +++ b/aleksis/core/views.py @@ -1,5 +1,7 @@ +from importlib import import_module from typing import Optional +from django.apps import apps from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied from django.http import Http404, HttpRequest, HttpResponse @@ -22,6 +24,7 @@ from .forms import ( from .models import Activity, Group, Notification, Person, School, DashboardWidget, Announcement from .tables import GroupsTable, PersonsTable from .util import messages +from .util.apps import AppConfig @person_required @@ -52,6 +55,14 @@ def offline(request): return render(request, "core/offline.html") +def about(request): + context = {} + + context["app_configs"] = list(filter(lambda a: isinstance(a, AppConfig), apps.get_app_configs())) + + return render(request, "core/about.html", context) + + @login_required def persons(request: HttpRequest) -> HttpResponse: context = {} diff --git a/pyproject.toml b/pyproject.toml index 02448d52ad00c58009c4ba7cb4244f2a3c7da808..dcc6ed027e169c417be226917d2f77955b461999 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,8 @@ django-memoize = "^2.2.1" django-haystack = "^3.0" celery-haystack = {version="^0.3.1", optional=true} django-dbbackup = "^3.3.0" +spdx-license-list = "^0.4.0" +license-expression = "^1.2" [tool.poetry.extras] ldap = ["django-auth-ldap"]