diff --git a/aleksis/core/assets/components/about/About.vue b/aleksis/core/assets/components/about/About.vue new file mode 100644 index 0000000000000000000000000000000000000000..6187d7c84503ce8633dc9a9559a88ac86b725d37 --- /dev/null +++ b/aleksis/core/assets/components/about/About.vue @@ -0,0 +1,16 @@ +<template> + <div class="mt-4 mb-4"> + <about-aleksis></about-aleksis> + <installed-apps-list /> + </div> +</template> + +<script> +import InstalledAppsList from "./InstalledAppsList.vue"; +import AboutAleksis from "./AboutAleksis.vue"; + +export default { + name: "About", + components: { AboutAleksis, InstalledAppsList }, +}; +</script> diff --git a/aleksis/core/assets/components/about/AboutAleksis.vue b/aleksis/core/assets/components/about/AboutAleksis.vue new file mode 100644 index 0000000000000000000000000000000000000000..2f39581d430a06b57bec0a9367983023ddbb9f95 --- /dev/null +++ b/aleksis/core/assets/components/about/AboutAleksis.vue @@ -0,0 +1,65 @@ +<template> + <v-row class="mb-3"> + <v-col cols="12"> + <v-card class="d-flex flex-column"> + <v-card-title>{{ $t("about.about_aleksis") }}</v-card-title> + <v-card-text> + <p class="text-body-1"> + {{ $t("about.about_aleksis_1") }} + </p> + <p class="text-body-1"> + {{ $t("about.about_aleksis_2") }} + </p> + </v-card-text> + <v-spacer></v-spacer> + <v-card-actions> + <v-btn text color="primary" href="https://aleksis.org/">{{ + $t("about.website_of_aleksis") + }}</v-btn> + <v-btn text color="primary" href="https://edugit.org/AlekSIS/">{{ + $t("about.source_code") + }}</v-btn> + </v-card-actions> + </v-card> + </v-col> + <v-col cols="12"> + <v-card class="d-flex flex-column"> + <v-card-title>{{ $t("about.licence_information") }}</v-card-title> + <v-card-text> + <p> + {{ $t("about.licence_information_1") }} + </p> + <p> + <v-chip color="green" text-color="white" small>{{ + $t("about.free_open_source_licence") + }}</v-chip> + <v-chip color="orange" text-color="white" small>{{ + $t("about.other_licence") + }}</v-chip> + </p> + </v-card-text> + <v-spacer></v-spacer> + <v-card-actions> + <v-btn text color="primary" href="https://eupl.eu">{{ + $t("about.full_licence_text") + }}</v-btn> + <v-btn + text + color="primary" + href="https://joinup.ec.europa.eu/collection/eupl/guidelines-users-and-developers" + > + {{ $t("about.more_information_eupl") }} + </v-btn> + </v-card-actions> + </v-card> + </v-col> + </v-row> +</template> + +<script> +export default { + name: "AboutAleksis", +}; +</script> + +<style scoped></style> diff --git a/aleksis/core/assets/components/about/InstalledAppCard.vue b/aleksis/core/assets/components/about/InstalledAppCard.vue new file mode 100644 index 0000000000000000000000000000000000000000..796808abd127324caf48d71e5e79afcc24da8380 --- /dev/null +++ b/aleksis/core/assets/components/about/InstalledAppCard.vue @@ -0,0 +1,131 @@ +<template> + <v-col cols="12" md="6" lg="6" xl="4" class="d-flex align-stretch"> + <v-card :id="app.name" class="d-flex flex-column flex-grow-1"> + <v-card-title> + {{ app.verboseName }} + </v-card-title> + + <v-card-subtitle class="text-body-1 black--text"> + {{ app.version }} + </v-card-subtitle> + + <v-card-text> + <v-row v-if="app.licence" class="mb-2"> + <v-col cols="6"> + {{ $t("about.licenced_under") }} <br /> + <strong class="text-body-1 black--text">{{ + app.licence.verboseName + }}</strong> + </v-col> + <v-col cols="6"> + {{ $t("about.licence_type") }} <br /> + <v-chip + v-if="app.licence.flags.isFsfLibre" + color="green" + text-color="white" + small + > + {{ $t("about.free_software") }} + </v-chip> + <v-chip + v-else-if="app.licence.flags.isOsiApproved" + color="green" + text-color="white" + small + > + {{ $t("about.open_source") }} + </v-chip> + <v-chip v-else color="orange" text-color="white" small> + {{ $t("about.proprietary") }} + </v-chip> + </v-col> + + <v-col cols="12" v-if="app.licence.licences.length !== 0"> + {{ $t("about.licence_consists_of") }} + <div + v-for="licence in app.licence.licences" + class="mb-2" + :key="licence.name" + > + <v-chip + v-if="licence.isOsiApproved || licence.isFsfLibre" + color="green" + text-color="green" + outlined + small + :href="licence.url" + > + {{ licence.name }} + </v-chip> + <v-chip + v-else + color="orange" + text-color="orange" + outlined + :href="licence.url" + > + {{ licence.name }} + </v-chip> + </div> + </v-col> + </v-row> + </v-card-text> + + <v-spacer></v-spacer> + + <v-card-actions v-if="app.urls.length !== 0"> + <v-btn text color="primary" @click="reveal = true"> + Show copyright + </v-btn> + <v-btn + v-for="url in app.urls" + color="primary" + text + :href="url.url" + :key="url.url" + > + {{ url.name }} + </v-btn> + </v-card-actions> + + <v-expand-transition> + <v-card + v-if="reveal" + class="transition-fast-in-fast-out v-card--reveal d-flex flex-column" + > + <v-card-text class="pb-0"> + <v-row> + <v-col cols="12" v-if="app.copyrights.length !== 0"> + <span v-for="(copyright, index) in app.copyrights" :key="index"> + Copyright © {{ copyright.years }} + <a :href="'mailto:' + copyright.email">{{ + copyright.name + }}</a> + <br /> + </span> + </v-col> + </v-row> + </v-card-text> + <v-spacer></v-spacer> + <v-card-actions class="pt-0"> + <v-btn text color="primary" @click="reveal = false"> Close </v-btn> + </v-card-actions> + </v-card> + </v-expand-transition> + </v-card> + </v-col> +</template> +<script> +export default { + name: "InstalledAppCard", + data: () => ({ + reveal: false, + }), + props: { + app: { + type: Object, + required: true, + }, + }, +}; +</script> diff --git a/aleksis/core/assets/components/about/InstalledAppsList.vue b/aleksis/core/assets/components/about/InstalledAppsList.vue new file mode 100644 index 0000000000000000000000000000000000000000..491e3df15ece49cf736f037212a0bc668a924bed --- /dev/null +++ b/aleksis/core/assets/components/about/InstalledAppsList.vue @@ -0,0 +1,39 @@ +<template> + <ApolloQuery :query="require('./installedApps.graphql')"> + <template #default="{ result: { error, data }, isLoading }"> + <v-row v-if="isLoading"> + <v-col + v-for="idx in 3" + :key="idx" + cols="12" + md="6" + lg="6" + xl="4" + class="d-flex align-stretch" + > + <v-card class="d-flex flex-column flex-grow-1 pa-4"> + <v-skeleton-loader + type="heading, actions, text@5" + ></v-skeleton-loader> + </v-card> + </v-col> + </v-row> + <v-row v-if="data.installedApps"> + <installed-app-card + v-for="app in data.installedApps" + :key="app.name" + :app="app" + /> + </v-row> + </template> + </ApolloQuery> +</template> + +<script> +import InstalledAppCard from "./InstalledAppCard.vue"; + +export default { + name: "InstalledAppsList", + components: { InstalledAppCard }, +}; +</script> diff --git a/aleksis/core/assets/components/about/installedApps.graphql b/aleksis/core/assets/components/about/installedApps.graphql new file mode 100644 index 0000000000000000000000000000000000000000..01ceaf99eb8d79d44ee7014d9f582d7dfeb54ace --- /dev/null +++ b/aleksis/core/assets/components/about/installedApps.graphql @@ -0,0 +1,29 @@ +{ + installedApps { + name + verboseName + version + copyrights { + years + name + email + } + licence { + verboseName + flags { + isFsfLibre + isOsiApproved + } + licences { + isFsfLibre + isOsiApproved + name + url + } + } + urls { + name + url + } + } +} diff --git a/aleksis/core/assets/css/global.scss b/aleksis/core/assets/css/global.scss index a0bcf72b5c28d72916dec80436395b476e7d3d5f..66e731dfe7d3a47498d4356b080dc1839898d1c1 100644 --- a/aleksis/core/assets/css/global.scss +++ b/aleksis/core/assets/css/global.scss @@ -21,3 +21,12 @@ h6, [v-cloak] { display: none; } + +.v-card--reveal { + bottom: 0; + opacity: 1 !important; + position: absolute; + width: 100%; + height: 100%; + overflow-y: scroll; +} diff --git a/aleksis/core/assets/index.js b/aleksis/core/assets/index.js index 47ec91d13c6f1f19adcbd174fef114dcc352b9d8..416029ed7a943ba2c2d0f3b20dc1c6783f38b4e0 100644 --- a/aleksis/core/assets/index.js +++ b/aleksis/core/assets/index.js @@ -2,3 +2,7 @@ import "@mdi/font/css/materialdesignicons.css"; import "./util"; import "./app"; + +import About from "./components/about/About.vue"; + +window.router.addRoute({ path: "/about", component: About }); diff --git a/aleksis/core/assets/messages.json b/aleksis/core/assets/messages.json index 32108b2be305238aceed554534f9232504d43045..3e1eb6879b650779e04d6c313f3c9868b592c6d5 100644 --- a/aleksis/core/assets/messages.json +++ b/aleksis/core/assets/messages.json @@ -6,6 +6,25 @@ }, "alerts": { "page_cached": "This page may contain outdated information since there is no internet connection." + }, + "about": { + "about_aleksis": "About AlekSIS®", + "licenced_under": "Licenced under", + "licence_type": "Licence Type", + "free_software": "Free Software", + "open_source": "Open Source", + "proprietary": "Proprietary", + "licence_consists_of": "The licence consists of", + "about_aleksis_1": "This platform is powered by AlekSIS®, a web-based school information system (SIS) which can be used to manage and/or publish organisational artifacts of educational institutions. AlekSIS is free software and can be used by anyone.", + "about_aleksis_2": "AlekSIS® is a registered trademark of the AlekSIS open source project, represented by Teckids e.V.", + "website_of_aleksis": "Website of AlekSIS", + "source_code": "Source Code", + "licence_information": "Licence Information", + "licence_information_1": "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, refer to the respective components below. The licences are marked like this:", + "free_open_source_licence": "Free/Open Source Licence", + "other_licence": "Other Licence", + "full_licence_text": "Full Licence Text", + "more_information_eupl": "More information about the EUPL" } }, "de": { @@ -15,6 +34,25 @@ }, "alerts": { "page_cached": "Diese Seite enthält vielleicht veraltete Informationen, da es keine Internetverbindung gibt." + }, + "about": { + "about_aleksis": "Über AlekSIS®", + "licenced_under": "Lizensiert unter", + "licence_type": "Lizenztyp", + "free_software": "Freie Software", + "open_source": "Open Source", + "proprietary": "Proprietär", + "licence_consists_of": "Die Lizenz besteht aus", + "about_aleksis_1": "Diese Plattform wird mit AlekSIS®, einem webbasierten Schulinformationssystem (SIS), welches für die Verwaltung und/oder Veröffentlichung von Bildungseinrichtungen verwendet werden kann. AlekSIS ist freie Software und kann von jedem benutzt werden.", + "about_aleksis_2": " AlekSIS® ist eine eingetragene Wortmarke des Open-Source-Projektes AlekSIS, vertreten durch den Teckids e.V.", + "website_of_aleksis": "Website von AlekSIS", + "source_code": "Quellcode", + "licence_information": "Lizenzinformationen", + "licence_information_1": " Der Core und die offiziellen Apps von AlekSIS sind unter der EUPL, Version 1.2 oder später, lizenziert. Für Lizenzinformationen zu Apps von Drittanbietern, wenn installiert, siehe direkt bei der jeweiligen App weiter unten auf dieser Seite. Die Lizenzen sind wie folgt markiert:", + "free_open_source_licence": "Freie/Open Source Lizenz", + "other_licence": "Andere Lizenz", + "full_licence_text": "Kompletter Lizenztext", + "more_information_eupl": "Weitere Informationen über die EUPL" } } } diff --git a/aleksis/core/schema.py b/aleksis/core/schema.py deleted file mode 100644 index cb6325c0640557314ae299a1d81719d910468391..0000000000000000000000000000000000000000 --- a/aleksis/core/schema.py +++ /dev/null @@ -1,143 +0,0 @@ -from django.conf import settings -from django.utils import translation - -import graphene -from graphene import ObjectType -from graphene_django import DjangoObjectType -from graphene_django.forms.mutation import DjangoModelFormMutation - -from .forms import PersonForm -from .models import Group, Notification, Person -from .util.core_helpers import get_app_module, get_app_packages, has_person -from .util.frontend_helpers import get_language_cookie - - -class NotificationType(DjangoObjectType): - class Meta: - model = Notification - - -class PersonType(DjangoObjectType): - class Meta: - model = Person - - full_name = graphene.Field(graphene.String) - - def resolve_full_name(root: Person, info, **kwargs): - return root.full_name - - -class GroupType(DjangoObjectType): - class Meta: - model = Group - - -class LanguageType(ObjectType): - code = graphene.String(required=True) - name = graphene.String(required=True) - name_local = graphene.String(required=True) - name_translated = graphene.String(required=True) - bidi = graphene.Boolean(required=True) - cookie = graphene.String(required=True) - - -class SystemPropertiesType(graphene.ObjectType): - current_language = graphene.String(required=True) - available_languages = graphene.List(LanguageType) - - def resolve_current_language(parent, info, **kwargs): - return info.context.LANGUAGE_CODE - - def resolve_available_languages(parent, info, **kwargs): - return [ - translation.get_language_info(code) | {"cookie": get_language_cookie(code)} - for code, name in settings.LANGUAGES - ] - - -class PersonMutation(DjangoModelFormMutation): - person = graphene.Field(PersonType) - - class Meta: - form_class = PersonForm - - -class MarkNotificationReadMutation(graphene.Mutation): - class Arguments: - id = graphene.ID() # noqa - - notification = graphene.Field(NotificationType) - - @classmethod - def mutate(cls, root, info, id): # noqa - notification = Notification.objects.get(pk=id) - # FIXME permissions - notification.read = True - notification.save() - - return notification - - -class Query(graphene.ObjectType): - ping = graphene.String(default_value="pong") - - notifications = graphene.List(NotificationType) - - persons = graphene.List(PersonType) - person_by_id = graphene.Field(PersonType, id=graphene.ID()) - who_am_i = graphene.Field(PersonType) - - system_properties = graphene.Field(SystemPropertiesType) - - def resolve_notifications(root, info, **kwargs): - # FIXME do permission stuff - return Notification.objects.all() - - def resolve_persons(root, info, **kwargs): - # FIXME do permission stuff - return Person.objects.all() - - def resolve_person_by_id(root, info, id): # noqa - return Person.objects.get(pk=id) - - def resolve_who_am_i(root, info, **kwargs): - if has_person(info.context.user): - return info.context.user.person - else: - return None - - def resolve_system_properties(root, info, **kwargs): - return True - - -class Mutation(graphene.ObjectType): - update_person = PersonMutation.Field() - - mark_notification_read = MarkNotificationReadMutation.Field() - - -def build_global_schema(): - """Build global GraphQL schema from all apps.""" - query_bases = [Query] - mutation_bases = [Mutation] - - for app in get_app_packages(): - schema_mod = get_app_module(app, "schema") - if not schema_mod: - # The app does not define a schema - continue - - if AppQuery := getattr(schema_mod, "Query", None): - query_bases.append(AppQuery) - if AppMutation := getattr(schema_mod, "Mutation", None): - mutation_bases.append(AppMutation) - - # Define classes using all query/mutation classes as mixins - # cf. https://docs.graphene-python.org/projects/django/en/latest/schema/#adding-to-the-schema - GlobalQuery = type("GlobalQuery", tuple(query_bases), {}) - GlobalMutation = type("GlobalMutation", tuple(mutation_bases), {}) - - return graphene.Schema(query=GlobalQuery, mutation=GlobalMutation) - - -schema = build_global_schema() diff --git a/aleksis/core/schema/__init__.py b/aleksis/core/schema/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..24f08bbe6e7d8b8b37160d583ee3757d6c5f3d11 --- /dev/null +++ b/aleksis/core/schema/__init__.py @@ -0,0 +1,81 @@ +from django.apps import apps + +import graphene + +from ..models import Notification, Person +from ..util.apps import AppConfig +from ..util.core_helpers import get_app_module, get_app_packages, has_person +from .group import GroupType # noqa +from .installed_apps import AppType +from .notification import MarkNotificationReadMutation, NotificationType +from .person import PersonMutation, PersonType +from .system_properties import SystemPropertiesType + + +class Query(graphene.ObjectType): + ping = graphene.String(default_value="pong") + + notifications = graphene.List(NotificationType) + + persons = graphene.List(PersonType) + person_by_id = graphene.Field(PersonType, id=graphene.ID()) + who_am_i = graphene.Field(PersonType) + + system_properties = graphene.Field(SystemPropertiesType) + installed_apps = graphene.List(AppType) + + def resolve_notifications(root, info, **kwargs): + # FIXME do permission stuff + return Notification.objects.all() + + def resolve_persons(root, info, **kwargs): + # FIXME do permission stuff + return Person.objects.all() + + def resolve_person_by_id(root, info, id): # noqa + return Person.objects.get(pk=id) + + def resolve_who_am_i(root, info, **kwargs): + if has_person(info.context.user): + return info.context.user.person + else: + return None + + def resolve_system_properties(root, info, **kwargs): + return True + + def resolve_installed_apps(root, info, **kwargs): + return [app for app in apps.get_app_configs() if isinstance(app, AppConfig)] + + +class Mutation(graphene.ObjectType): + update_person = PersonMutation.Field() + + mark_notification_read = MarkNotificationReadMutation.Field() + + +def build_global_schema(): + """Build global GraphQL schema from all apps.""" + query_bases = [Query] + mutation_bases = [Mutation] + + for app in get_app_packages(): + schema_mod = get_app_module(app, "schema") + if not schema_mod: + # The app does not define a schema + continue + + if AppQuery := getattr(schema_mod, "Query", None): + query_bases.append(AppQuery) + if AppMutation := getattr(schema_mod, "Mutation", None): + mutation_bases.append(AppMutation) + + # Define classes using all query/mutation classes as mixins + # cf. https://docs.graphene-python.org/projects/django/en/latest/schema/#adding-to-the-schema + GlobalQuery = type("GlobalQuery", tuple(query_bases), {}) + GlobalMutation = type("GlobalMutation", tuple(mutation_bases), {}) + + return graphene.Schema(query=GlobalQuery, mutation=GlobalMutation) + + +schema = build_global_schema() diff --git a/aleksis/core/schema/group.py b/aleksis/core/schema/group.py new file mode 100644 index 0000000000000000000000000000000000000000..327daff3dd09d54053b683ba4cefb71da8ba6e5b --- /dev/null +++ b/aleksis/core/schema/group.py @@ -0,0 +1,8 @@ +from graphene_django import DjangoObjectType + +from ..models import Group + + +class GroupType(DjangoObjectType): + class Meta: + model = Group diff --git a/aleksis/core/schema/installed_apps.py b/aleksis/core/schema/installed_apps.py new file mode 100644 index 0000000000000000000000000000000000000000..879575466ae2f06795c75c9b928fdc7f8cdb4499 --- /dev/null +++ b/aleksis/core/schema/installed_apps.py @@ -0,0 +1,58 @@ +import graphene +from graphene import ObjectType + + +class AppURLType(ObjectType): + name = graphene.String(required=True) + url = graphene.String(required=True) + + +class CopyrightType(ObjectType): + years = graphene.String(required=True) + name = graphene.String(required=True) + email = graphene.String(required=True) + + +class LicenceFlagsType(ObjectType): + isFsfLibre = graphene.Boolean(required=True) + isOsiApproved = graphene.Boolean(required=True) + + +class SubLicenceType(ObjectType): + isDeprecatedLicenseId = graphene.Boolean(default_value=False) + isFsfLibre = graphene.Boolean(default_value=False) + isOsiApproved = graphene.Boolean(default_value=False) + licenseId = graphene.String(required=True) + name = graphene.String(required=True) + referenceNumber = graphene.Int(default_value=-1) + url = graphene.String() + + +class LicenceType(ObjectType): + verbose_name = graphene.String(required=True) + flags = graphene.Field(LicenceFlagsType, required=True) + licences = graphene.List(SubLicenceType) + + +class AppType(ObjectType): + copyrights = graphene.List(CopyrightType) + licence = graphene.Field(LicenceType) + name = graphene.String(required=True) + verbose_name = graphene.String(required=True) + version = graphene.String() + urls = graphene.List(AppURLType) + + def resolve_verbose_name(root, info, **kwargs): + return root.get_name() + + def resolve_version(root, info, **kwargs): + return root.get_version() + + def resolve_licence(root, info, **kwargs): + return root.get_licence_dict() + + def resolve_urls(root, info, **kwargs): + return root.get_urls_dict() + + def resolve_copyrights(root, info, **kwargs): + return root.get_copyright_dicts() diff --git a/aleksis/core/schema/notification.py b/aleksis/core/schema/notification.py new file mode 100644 index 0000000000000000000000000000000000000000..114f92b32d9658208013144fe2d7c2d2b0157595 --- /dev/null +++ b/aleksis/core/schema/notification.py @@ -0,0 +1,25 @@ +import graphene +from graphene_django import DjangoObjectType + +from ..models import Notification + + +class NotificationType(DjangoObjectType): + class Meta: + model = Notification + + +class MarkNotificationReadMutation(graphene.Mutation): + class Arguments: + id = graphene.ID() # noqa + + notification = graphene.Field(NotificationType) + + @classmethod + def mutate(cls, root, info, id): # noqa + notification = Notification.objects.get(pk=id) + # FIXME permissions + notification.read = True + notification.save() + + return notification diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py new file mode 100644 index 0000000000000000000000000000000000000000..a3a22ce6779c7974cb61e5efeb454c1203fd8c73 --- /dev/null +++ b/aleksis/core/schema/person.py @@ -0,0 +1,23 @@ +import graphene +from graphene_django import DjangoObjectType +from graphene_django.forms.mutation import DjangoModelFormMutation + +from ..forms import PersonForm +from ..models import Person + + +class PersonType(DjangoObjectType): + class Meta: + model = Person + + full_name = graphene.Field(graphene.String) + + def resolve_full_name(root: Person, info, **kwargs): + return root.full_name + + +class PersonMutation(DjangoModelFormMutation): + person = graphene.Field(PersonType) + + class Meta: + form_class = PersonForm diff --git a/aleksis/core/schema/system_properties.py b/aleksis/core/schema/system_properties.py new file mode 100644 index 0000000000000000000000000000000000000000..3958df762b886d5b0349b838db09e46f11ca0ffc --- /dev/null +++ b/aleksis/core/schema/system_properties.py @@ -0,0 +1,29 @@ +from django.conf import settings +from django.utils import translation + +import graphene + +from ..util.frontend_helpers import get_language_cookie + + +class LanguageType(graphene.ObjectType): + code = graphene.String(required=True) + name = graphene.String(required=True) + name_local = graphene.String(required=True) + name_translated = graphene.String(required=True) + bidi = graphene.Boolean(required=True) + cookie = graphene.String(required=True) + + +class SystemPropertiesType(graphene.ObjectType): + current_language = graphene.String(required=True) + available_languages = graphene.List(LanguageType) + + def resolve_current_language(parent, info, **kwargs): + return info.context.LANGUAGE_CODE + + def resolve_available_languages(parent, info, **kwargs): + return [ + translation.get_language_info(code) | {"cookie": get_language_cookie(code)} + for code, name in settings.LANGUAGES + ] diff --git a/aleksis/core/templates/core/pages/about.html b/aleksis/core/templates/core/pages/about.html index 6c44d90cbfe51df457f2bd09f600dfb676b7b97d..3489a50e2395309b078a0c1b2fa053b6a728fac6 100644 --- a/aleksis/core/templates/core/pages/about.html +++ b/aleksis/core/templates/core/pages/about.html @@ -1,5 +1,5 @@ {# -*- engine:django -*- #} -{% extends "core/base.html" %} +{% extends "core/vue_base.html" %} {% load i18n %} @@ -7,121 +7,5 @@ {% 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 artifacts of educational institutions. AlekSIS is free software and - can be used by anyone. - {% endblocktrans %} - </p> - <p> - {% blocktrans %} - AlekSIS® is a registered trademark of the AlekSIS open source project, represented by Teckids e.V. - {% 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, refer to 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> - + <router-view></router-view> {% endblock %} diff --git a/aleksis/core/util/apps.py b/aleksis/core/util/apps.py index 5b3898dd835506ade932ebccf30e4cf0de15486e..49e359872784773836558c13081ae1290b4477c5 100644 --- a/aleksis/core/util/apps.py +++ b/aleksis/core/util/apps.py @@ -128,12 +128,28 @@ class AppConfig(django.apps.AppConfig): # We could not find a valid licence return ("Unknown", [default_dict]) + @classmethod + def get_licence_dict(cls): + """Get licence information of application package.""" + licence = cls.get_licence() + return { + "verbose_name": licence[0], + "flags": licence[1], + "licences": licence[2], + } + @classmethod def get_urls(cls): """Get list of URLs for this application package.""" return getattr(cls, "urls", {}) # TODO Try getting from distribution if not set + @classmethod + def get_urls_dict(cls): + """Get list of URLs for this application package.""" + urls = cls.get_urls() + return [{"name": key, "url": value} for key, value in urls.items()] + @classmethod def get_copyright(cls) -> Sequence[tuple[str, str, str]]: """Get copyright information tuples for application package.""" @@ -155,6 +171,12 @@ class AppConfig(django.apps.AppConfig): return copyrights_processed # TODO Try getting from distribution if not set + @classmethod + def get_copyright_dicts(cls): + """Get copyright information dictionaries for application package.""" + infos = cls.get_copyright() + return [{"years": info[0], "name": info[1], "email": info[2]} for info in infos] + def preference_updated( self, sender: Any,