Skip to content
Snippets Groups Projects
Commit 95869ff1 authored by Jonathan Weth's avatar Jonathan Weth :keyboard:
Browse files

Merge branch 'merge-release-3.0' into 'master'

Release 3.0

See merge request !1237
parents 58e8dbb8 fca8762e
No related branches found
No related tags found
2 merge requests!1237Release 3.0,!1183Release 3.0
Pipeline #128398 failed
Showing
with 720 additions and 106 deletions
......@@ -9,12 +9,106 @@ and this project adheres to `Semantic Versioning`_.
Unreleased
----------
`3.0`_ - 2022-05-11
-------------------
Added
~~~~~
* GraphQL schema for Rooms
* Provide API endpoint for system status.
* [Dev] UpdateIndicator Vue Component to display the status of interactive pages
* [Dev] DeleteDialog Vue Component to unify item deletion in the new frontend
Changed
~~~~~~~
* Show message on successful logout to inform users properly.
* Phone number country now has to be configured in config file insted of frontend.
Fixed
~~~~~
* GraphQL endpoints for groups, persons, and notifications didn't expose all necessary fields.
* Loading indicator in toolbar was not shown at the complete loading progress.
* 404 page was sometimes shown while the page was still loading.
* Setting of page height in the iframe was not working correctly.
* App switched to offline state when the user was logged out/in.
* The `Stop Impersonation` button is not shown due to an oversee when changing the type of the whoAmI query to an object of UserType
* Offline fallback page for legacy pages was misleading sometimes.
* Route changes in the Legacy-Component iframe didn't trigger a scroll to the top
* Query strings did not get passed when navigating legacy pages inside of the SPA.
* Retry button on error 500 page did not trigger a reload of the page.
* When the Celery worker wasn't able to execute all tasks in time, notifications were sent multiple times.
* Changing the maintenance mode state spawned another SPA instance in the iframe
* Phone numbers couldn't be in regional format.
* System status view wasn't accessible through new frontend if a check failed.
* Progress page didn't show error message on failure.
* Dynamic routes were not removed/hidden when the respective object registering it was deleted.
* Django messages were not displayed in Vue frontend.
* Links to data check objects did not work properly.
* Backend cleanup task for Celery wasn't working.
* URLs in invitation email were broken.
* Invitation view didn't work.
* Invitation emails were using wrong styling.
* GraphQL queries and mutations did not log exceptions.
`3.0b3`_ - 2023-03-19
---------------------
Fixed
~~~~~
* Some GraphQL queries could return more data than permitted in related fields.
`3.0b2`_ - 2023-03-09
---------------------
Changed
~~~~~~~
* Change default network policy of the Apollo client to `cache-and-network`.
Fixed
~~~~~
* In case the status code of a response was not in the range between 200 and 299
but still indicates that the response should be delivered, e. g. in the case
of a redirected request, the service worker served the offline fallback page.
* In some cases, the resize listener for the IFrame in the `LegacyBaseTemplate`
did not trigger.
* [Dev] Allow apps to declare URLs in the non-legacy namespace again
`3.0b1`_ - 2023-02-27
---------------------
Added
~~~~~
* Support for two factor authentication via email codes and Webauthn.
`3.0b0`_ - 2023-02-15
---------------------
This release starts a new era of the AlekSIS® framework, by introducing a
dynamic frontend app written in Vue.js which communicates with the backend
through GraphQL. Support for legacy views (Django templates and
Materialize) was removed; while there is backwards compatibility for now,
this is only used by official apps until their views are fully migrated.
AlekSIS and its new frontend require Node.js version 18 or higher to run the
Vite bundler. On Debian, this means that Debian 12 (bookworm) is needed, or
Node.js must be installed from a third-party repository.
Removed
~~~~~~~
* Official support for views rendered server-side in Django is removed. The
`LegacyBaseTemplate` provided for backwards compatibility must not be used
by apps declaring a dependency on AlekSIS >= 3.0.
* Support for deploying AlekSIS in sub-URLs
* Support for production deployments without HTTPS
Deprecated
~~~~~~~~~~
......@@ -26,17 +120,17 @@ Added
~~~~~
* Notification drawer in top nav bar
* GraphQL queries and mutations for core data management
* [Dev] Introduce new mechanism to register classes over all apps.
* Data template for `room` model used for haystack search indexing moved to core.
* Support for two factor authentication via email codes and Webauthn.
* GraphQL queries for base system and some core data management
* [Dev] New mechanism to register classes over all apps (RegistryObject)
* Model for rooms
Changed
~~~~~~~
* Show languages in local language
* Rewrite of frontend using Vuetify
* The runuwsgi dev server now starts a Vite dev server with HMR in the
* 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
* OIDC scope "profile" now exposes the avatar instead of the official photo
* Based on Django 4.0
......@@ -55,26 +149,23 @@ Changed
Fixed
~~~~~
* The error page displayed when an ObjectOverview component is not able to get the required data was incomplete.
* In some cases, the IFrame for legacy pages was not properly sized for its content.
* When accessing the person overview page without a person ID, the avatar image was not displayed properly.
* The system tried to send notifications for done background tasks
in addition to tasks started in the foreground.
* 2FA via messages or phone calls didn't work.
in addition to tasks started in the foreground
* 2FA via messages or phone calls didn't work after a faulty dependency
update
* [Dev] Site reference on extensible models can no longer cause name clashes
because of its related name.
because of its related name
Removed
~~~~~~~
* Support for materialize-based frontend views (deprecated in 2.11)
* Legacy support for person iCal feed URLs.
* Django debug toolbar
* 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
`2.12.3` - 2023-03-07
---------------------
`2.12.3`_ - 2023-03-07
----------------------
Fixed
~~~~~
......@@ -1048,3 +1139,8 @@ Fixed
.. _2.12.1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.12.1
.. _2.12.2: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.12.2
.. _2.12.3: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.12.3
.. _3.0b0: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/3.0b0
.. _3.0b1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/3.0b1
.. _3.0b2: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/3.0b2
.. _3.0b3: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/3.0b3
.. _3.0: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/3.0
......@@ -2,7 +2,7 @@ FROM debian:bookworm-slim AS core
# Build arguments
ARG EXTRAS="ldap,s3,sentry"
ARG APP_VERSION="==2.10.1.dev0+20220801181456.7ba74939"
ARG APP_VERSION="==3.0b0"
# Configure Python to be nice inside Docker and pip to stfu
ENV PYTHONUNBUFFERED 1
......@@ -64,6 +64,7 @@ RUN set -e; \
${ALEKSIS_static__root} \
${ALEKSIS_media__root} \
${ALEKSIS_backup__location}; \
dpkg-divert --rename --add /usr/lib/$(py3versions -d)/EXTERNALLY-MANAGED; \
eatmydata pip install AlekSIS-Core\[$EXTRAS\]$APP_VERSION
# Define entrypoint, volumes and uWSGI running on port 8000
......
......@@ -68,7 +68,7 @@ Licence
Copyright © 2019, 2020, 2021, 2022, 2023 Dominik George <dominik.george@teckids.org>
Copyright © 2019, 2020, 2021, 2022 Tom Teichler <tom.teichler@teckids.org>
Copyright © 2019 mirabilos <thorsten.glaser@teckids.org>
Copyright © 2021, 2022 magicfelix <felix@felix-zauberer.de>
Copyright © 2021, 2022, 2023 magicfelix <felix@felix-zauberer.de>
Copyright © 2021 Lloyd Meins <meinsll@katharineum.de>
Copyright © 2022 Benedict Suska <benedict.suska@teckids.org>
Copyright © 2022 Lukas Weichelt <lukas.weichelt@teckids.org>
......
......@@ -3,6 +3,7 @@ from typing import Any, Optional
import django.apps
from django.apps import apps
from django.conf import settings
from django.contrib import messages
from django.http import HttpRequest
from django.utils.module_loading import autodiscover_modules
from django.utils.translation import gettext as _
......@@ -43,7 +44,7 @@ class CoreConfig(AppConfig):
([2019, 2020, 2021, 2022, 2023], "Dominik George", "dominik.george@teckids.org"),
([2019, 2020, 2021, 2022], "Tom Teichler", "tom.teichler@teckids.org"),
([2019], "mirabilos", "thorsten.glaser@teckids.org"),
([2021, 2022], "magicfelix", "felix@felix-zauberer.de"),
([2021, 2022, 2023], "magicfelix", "felix@felix-zauberer.de"),
([2021], "Lloyd Meins", "meinsll@katharineum.de"),
([2022], "Benedict Suska", "benedict.suska@teckids.org"),
([2022], "Lukas Weichelt", "lukas.weichelt@teckids.org"),
......@@ -144,6 +145,11 @@ class CoreConfig(AppConfig):
# Save the associated person to pick up defaults
user.person.save()
def user_logged_out(
self, sender: type, request: Optional[HttpRequest], user: "User", **kwargs
) -> None:
messages.success(request, _("You have been logged out successfully."))
@classmethod
def get_all_scopes(cls) -> dict[str, str]:
scopes = {
......
......@@ -19,6 +19,7 @@ from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget,
from dynamic_preferences.forms import PreferenceForm
from guardian.shortcuts import assign_perm
from invitations.forms import InviteForm
from maintenance_mode.core import get_maintenance_mode
from material import Fieldset, Layout, Row
from .mixins import ExtensibleForm, SchoolTermRelatedExtensibleForm
......@@ -860,3 +861,11 @@ class OAuthApplicationForm(forms.ModelForm):
"redirect_uris",
"skip_authorization",
)
class MaintenanceModeForm(forms.Form):
maintenance_mode = forms.BooleanField(
required=False,
initial=not get_maintenance_mode(),
widget=forms.HiddenInput,
)
......@@ -70,7 +70,7 @@ const apolloOpts = {
}
// Add a snackbar on all errors returned by the GraphQL endpoint
// If App is offline, don't add snackbar since only the ping query is active
if (!vm.$root.offline) {
if (!vm.$root.offline && !vm.$root.invalidation) {
vm.$root.snackbarItems.push({
id: crypto.randomUUID(),
timeout: 5000,
......@@ -79,7 +79,7 @@ const apolloOpts = {
});
}
}
if (networkError) {
if (networkError && !vm.$root.invalidation) {
// Set app offline globally on network errors
// This will cause the offline logic to kick in, starting a ping check or
// similar recovery strategies depending on the app/navigator state
......@@ -90,6 +90,7 @@ const apolloOpts = {
vm.$root.offline = true;
}
},
fetchPolicy: "cache-and-network",
},
},
};
......
......@@ -19,6 +19,13 @@ const dateTimeFormats = {
minute: "numeric",
second: "numeric",
},
longNumeric: {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
},
},
de: {
short: {
......@@ -39,6 +46,13 @@ const dateTimeFormats = {
minute: "numeric",
second: "numeric",
},
longNumeric: {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
},
},
};
......
......@@ -59,13 +59,14 @@ export default {
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 routePath =
path.charAt(path.length - 1) === "/" &&
this.$route.path.charAt(path.length - 1) !== "/"
? this.$route.path + "/"
: this.$route.path;
if (path !== routePath) {
this.$router.push(path);
this.$router.push(pathWithQueryString);
}
// Show loader if iframe starts to change its content, even if the $route stays the same
......@@ -77,13 +78,24 @@ export default {
const title = this.$refs.contentIFrame.contentWindow.document.title;
this.$root.$setPageTitle(title);
// Adapt height of IFrame according to the height of its contents once and listen to resize events
this.iFrameHeight =
this.$refs.contentIFrame.contentDocument.body.scrollHeight;
this.$refs.contentIFrame.contentWindow.onresize = () => {
// Adapt height of IFrame according to the height of its contents once and observe height changes
if (
this.$refs.contentIFrame.contentDocument &&
this.$refs.contentIFrame.contentDocument.body
) {
this.iFrameHeight =
this.$refs.contentIFrame.contentDocument.body.scrollHeight;
};
new ResizeObserver(() => {
if (
this.$refs.contentIFrame &&
this.$refs.contentIFrame.contentDocument &&
this.$refs.contentIFrame.contentDocument.body
) {
this.iFrameHeight =
this.$refs.contentIFrame.contentDocument.body.scrollHeight;
}
}).observe(this.$refs.contentIFrame.contentDocument.body);
}
this.$root.contentLoading = false;
},
......@@ -92,6 +104,10 @@ export default {
$route() {
// Show loading animation once route changes
this.$root.contentLoading = true;
// 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 });
},
},
name: "LegacyBaseTemplate",
......
......@@ -45,7 +45,10 @@
>
<v-icon>mdi-update</v-icon>
</v-btn>
<div v-if="whoAmI && whoAmI.isAuthenticated" class="d-flex">
<div
v-if="whoAmI && whoAmI.isAuthenticated && whoAmI.person"
class="d-flex"
>
<notification-list v-if="!whoAmI.person.isDummy" />
<account-menu
:account-menu="accountMenu"
......@@ -97,6 +100,7 @@
checkPermission($route.meta.permission) ||
$route.name === 'dashboard'
"
@mounted="routeComponentMounted"
/>
<error-page
v-else-if="
......@@ -226,6 +230,7 @@ import useRegisterSWMixin from "../../mixins/useRegisterSW";
import offlineMixin from "../../mixins/offline";
import menusMixin from "../../mixins/menus";
import routesMixin from "../../mixins/routes";
import error404Mixin from "../../mixins/error404";
export default {
data() {
......@@ -250,7 +255,13 @@ export default {
},
messages: {
query: gqlMessages,
pollInterval: 1000,
},
},
methods: {
routeComponentMounted() {
if (!this.$root.isLegacyBaseTemplate) {
this.$root.contentLoading = false;
}
},
},
watch: {
......@@ -272,10 +283,8 @@ export default {
},
$route: {
handler(newRoute) {
if (newRoute.matched.length == 0) {
this.error404 = true;
} else {
this.error404 = false;
if (this.$apollo.queries.messages) {
this.$apollo.queries.messages.refetch();
}
},
immediate: true,
......@@ -291,7 +300,13 @@ export default {
SideNav,
SnackbarItem,
},
mixins: [useRegisterSWMixin, offlineMixin, menusMixin, routesMixin],
mixins: [
useRegisterSWMixin,
offlineMixin,
menusMixin,
routesMixin,
error404Mixin,
],
};
</script>
......
......@@ -3,6 +3,7 @@ query ($permissions: [String]!) {
username
isAuthenticated
isAnonymous
isImpersonate
person {
photo {
url
......@@ -10,7 +11,6 @@ query ($permissions: [String]!) {
fullName
avatarUrl
isDummy
isImpersonate
}
permissions: globalPermissionsByName(permissions: $permissions) {
name
......
<template>
<v-expansion-panel>
<v-expansion-panel-header v-slot="{ open }">
<div class="d-flex justify-start align-center">
<v-avatar
x-large
v-if="accessToken.application.icon.absoluteUrl"
class="mr-4"
>
<img
:src="accessToken.application.icon.absoluteUrl"
:alt="accessToken.application.name"
/>
</v-avatar>
<v-avatar x-large v-else class="mr-4" color="secondary">
<v-icon color="white">mdi-apps</v-icon>
</v-avatar>
<div class="subtitle-1 font-weight-medium">
{{ accessToken.application.name }}
</div>
</div>
</v-expansion-panel-header>
<v-expansion-panel-content>
<v-list dense class="pa-0">
<v-list-item>
<v-list-item-content class="body-2">
{{
$t("oauth.authorized_application.access_since", {
date: $d(new Date(accessToken.created), "longNumeric"),
})
}}
·
{{
$t("oauth.authorized_application.valid_until", {
date: $d(new Date(accessToken.expires), "longNumeric"),
})
}}
</v-list-item-content>
<v-list-item-action>
<v-btn color="primary" @click="deleteItem(accessToken)">
{{ $t("oauth.authorized_application.revoke") }}
</v-btn>
</v-list-item-action>
</v-list-item>
<v-list-item v-if="accessToken.scopes && accessToken.scopes.length > 0">
<div class="pr-4">
<v-list-item-content class="body-2">
{{ $t("oauth.authorized_application.has_access_to") }}
</v-list-item-content>
</div>
<v-list dense class="pa-0 flex-grow-1">
<div v-for="(scope, idx) in accessToken.scopes" :key="scope.name">
<v-list-item>
<v-list-item-content class="body-2">
{{ scope.description }}
</v-list-item-content>
</v-list-item>
<v-divider v-if="idx < accessToken.scopes.length - 1" />
</div>
</v-list>
</v-list-item>
</v-list>
</v-expansion-panel-content>
</v-expansion-panel>
</template>
<script>
export default {
name: "AuthorizedApplication",
props: {
accessToken: {
type: Object,
required: true,
},
},
methods: {
deleteItem(item) {
this.$emit("delete-item", item);
},
},
};
</script>
<template>
<div>
<h1 class="mb-4">{{ $t("oauth.authorized_application.title") }}</h1>
<div v-if="$apollo.queries.accessTokens.loading">
<v-skeleton-loader type="card"></v-skeleton-loader>
</div>
<div v-else-if="accessTokens">
<v-card class="mb-4">
<v-card-title>
{{ $t("oauth.authorized_application.subtitle") }}
</v-card-title>
<v-card-text>
{{ $t("oauth.authorized_application.description") }}
</v-card-text>
<v-expansion-panels flat>
<authorized-application
v-for="(accessToken, index) in accessTokens"
:key="accessToken.id"
:access-token="accessToken"
@delete-item="openDeleteDialog"
/>
</v-expansion-panels>
</v-card>
</div>
<delete-dialog
:item="deleteItem"
:gql-mutation="require('./revokeOauthToken.graphql')"
:gql-query="require('./accessTokens.graphql')"
v-model="deleteDialog"
>
<template #title>
{{ $t("oauth.authorized_application.revoke_question") }}
</template>
<template #body>
<span v-if="deleteItem">{{ deleteItem.application.name }}</span>
</template>
<template #deleteContent>
{{ $t("oauth.authorized_application.revoke") }}
</template>
</delete-dialog>
</div>
</template>
<script>
import gqlAccessTokens from "./accessTokens.graphql";
import AuthorizedApplication from "./AuthorizedApplication.vue";
import DeleteDialog from "../generic/dialogs/DeleteDialog.vue";
export default {
name: "AuthorizedApplications",
components: { DeleteDialog, AuthorizedApplication },
data() {
return {
deleteDialog: false,
deleteItem: null,
};
},
methods: {
openDeleteDialog(item) {
this.deleteItem = item;
this.deleteDialog = true;
},
},
apollo: {
accessTokens: {
query: gqlAccessTokens,
},
},
};
</script>
{
accessTokens: oauthAccessTokens {
id
created
updated
expires
scopes {
name
description
}
application {
id
name
icon {
absoluteUrl
}
}
}
}
mutation ($id: ID!) {
revokeOauthToken(id: $id) {
ok
}
}
......@@ -33,7 +33,7 @@
</message-box>
</div>
<message-box
v-if="progress.state === 'ERROR'"
v-if="progress.state === 'FAILURE'"
dense
type="error"
transition="slide-x-transition"
......@@ -60,13 +60,13 @@
<v-card-actions
v-if="
progress &&
(progress.state === 'ERROR' || progress.state === 'SUCCESS')
(progress.state === 'FAILURE' || progress.state === 'SUCCESS')
"
>
<back-button :href="progress.meta.backUrl" text />
<v-spacer />
<v-btn
v-if="progress.meta.additionalButton"
v-if="progress.meta.additionalButton && progress.state === 'SUCCESS'"
:href="progress.meta.additionalButton.url"
text
color="primary"
......@@ -108,7 +108,7 @@ export default {
},
watch: {
state(newState) {
if (newState === "SUCCESS" || newState === "ERROR") {
if (newState === "SUCCESS" || newState === "FAILURE") {
this.$apollo.queries.celeryProgressByTaskId.stopPolling();
this.$apollo.mutate({
mutation: gqlCeleryProgressFetched,
......
<template>
<v-tooltip bottom>
<template #activator="{ on, attrs }">
<v-btn
right
icon
v-bind="attrs"
v-on="on"
@click="handleClick"
:loading="status === $options.UPDATING"
>
<v-icon v-if="status !== $options.UPDATING" :color="color">
{{ icon }}
</v-icon>
</v-btn>
</template>
<span>{{ text }}</span>
</v-tooltip>
</template>
<script>
export default {
ERROR: "ERROR", // Something went wrong
SAVED: "SAVED", // Everything alright
UPDATING: "UPDATING", // We are sending something to the server
CHANGES: "CHANGES", // the user changed something, but it has not been saved yet
name: "UpdateIndicator",
emits: ["manual-update"],
props: {
status: {
type: String,
required: true,
},
},
computed: {
text() {
switch (this.status) {
case this.$options.SAVED:
return this.$t("status.saved");
case this.$options.UPDATING:
return this.$t("status.updating");
case this.$options.CHANGES:
return this.$t("status.changes");
default:
return this.$t("status.error");
}
},
color() {
switch (this.status) {
case this.$options.SAVED:
return "success";
case this.$options.CHANGES:
return "secondary";
case this.$options.UPDATING:
return "secondary";
default:
return "error";
}
},
icon() {
switch (this.status) {
case this.$options.SAVED:
return "$success";
case this.$options.CHANGES:
return "mdi-dots-horizontal";
default:
return "$warning";
}
},
isAbleToClick() {
return (
this.status === this.$options.CHANGES ||
this.status === this.$options.ERROR
);
},
},
methods: {
handleClick() {
if (this.isAbleToClick) {
this.$emit("manual-update");
}
},
},
};
</script>
<template>
<ApolloMutation
v-if="dialogOpen"
:mutation="gqlMutation"
:variables="{ id: item.id }"
:update="update"
@done="close(true)"
>
<template #default="{ mutate, loading, error }">
<v-dialog v-model="dialogOpen" max-width="500px">
<v-card>
<v-card-title class="text-h5">
<slot name="title">
{{ $t("actions.confirm_deletion") }}
</slot>
</v-card-title>
<v-card-text>
<slot name="body">
<p class="text-body-1">{{ nameOfObject }}</p>
</slot>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="close(false)" :disabled="loading">
<slot name="cancelContent">
{{ $t("actions.cancel") }}
</slot>
</v-btn>
<v-btn
color="error"
text
@click="mutate"
:loading="loading"
:disabled="loading"
>
<slot name="deleteContent">
{{ $t("actions.delete") }}
</slot>
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-snackbar :value="error !== null">
{{ error }}
<template #action="{ attrs }">
<v-btn color="primary" text v-bind="attrs" @click="error = null" icon>
<v-icon>$close</v-icon>
</v-btn>
</template>
</v-snackbar>
</template>
</ApolloMutation>
</template>
<script>
export default {
name: "DeleteDialog",
computed: {
nameOfObject() {
return this.itemAttribute in this.item || {}
? this.item[this.itemAttribute]
: this.item.toString();
},
dialogOpen: {
get() {
return this.value;
},
set(val) {
this.$emit("input", val);
},
},
},
methods: {
update(store) {
if (!this.gqlQuery) {
// There is no GraphQL query to update
return;
}
// Read the data from cache for query
const storedData = store.readQuery({ query: this.gqlQuery });
if (!storedData) {
// There are no data in the cache yet
return;
}
const storedDataKey = Object.keys(storedData)[0];
// Remove item from stored data
const index = storedData[storedDataKey].findIndex(
(m) => m.id === this.item.id
);
storedData[storedDataKey].splice(index, 1);
// Write data back to the cache
store.writeQuery({ query: this.gqlQuery, data: storedData });
},
close(success) {
this.$emit("input", false);
if (success) {
this.$emit("success");
} else {
this.$emit("cancel");
}
},
},
props: {
value: {
type: Boolean,
required: true,
},
item: {
type: Object,
required: false,
default: () => ({}),
},
itemAttribute: {
type: String,
required: false,
default: "name",
},
gqlMutation: {
type: Object,
required: true,
},
gqlQuery: {
type: Object,
required: false,
default: null,
},
},
};
</script>
......@@ -65,11 +65,25 @@ const app = new Vue({
render: (h) => h(App),
data: () => ({
showCacheAlert: false,
contentLoading: false,
contentLoading: true,
offline: false,
backgroundActive: true,
invalidation: false,
snackbarItems: [],
}),
computed: {
matchedComponents() {
if (this.$route.matched.length > 0) {
return this.$route.matched.map(
(route) => route.components.default.name
);
}
return [];
},
isLegacyBaseTemplate() {
return this.matchedComponents.includes("LegacyBaseTemplate");
},
},
router,
i18n,
});
......
......@@ -73,9 +73,14 @@
},
"actions": {
"back": "Zurück",
"cancel": "Abbrechen",
"close": "Schließen",
"confirm_deletion": "Sind Sie sicher, dass sie dieses Objekt löschen wollen?",
"delete": "Löschen",
"edit": "Bearbeiten",
"search": "Suchen"
"save": "Speichern",
"search": "Suchen",
"stop_editing": "Bearbeiten beenden"
},
"administration": {
"backend_admin": {
......@@ -99,9 +104,9 @@
"imprint": "Impressum",
"logo": "Logo",
"no_permission": "Sie haben nicht die nötigen Berechtigungen, um diese Seite aufzurufen. Bitte loggen Sie sich mit einem anderen Account ein.",
"no_permission_message_long": "Sie haben nicht die nötigen Berechtigungen, um diese Seite aufzurufen. Bitte loggen Sie sich mit einem anderen Account ein.",
"no_permission_message_long": "Sie haben keine Berichtigung, diese Seite zu sehen. Bitte melden Sie sich mit einem anderen Konto an.",
"no_permission_message_short": "Keine Berechtigung",
"no_permission_redirect_text": "Zum Login gehen",
"no_permission_redirect_text": "Zur Anmeldung gehen",
"person_is_dummy": "Ihr Administratorenkonto ist mit keiner Person verknüpft. Deshalb wurde Ihr Konto mit einer Dummyperson verknüpft.",
"privacy_policy": "Datenschutzerklärung",
"user_not_linked_to_person": "Ihr Benutzerkonto ist nicht mit einer Person verknüpft. Das bedeutet, dass Sie keine schulbezogenen Informationen aufrufen können. Bitte wenden Sie sich an die Verwaltenden von AlekSIS an Ihrer Schule."
......@@ -173,6 +178,17 @@
"title": "OAuth-Anwendung",
"title_plural": "OAuth-Anwendungen"
},
"authorized_application": {
"access_since": "Zugriff seit {date}",
"description": "Die folgenden Drittanbieter-Anwendungen haben Zugriff auf Ihr Konto. Sie können den Zugriff für diejenigen, die Sie nicht mehr brauchen oder denen Sie nicht mehr vertrauen, jederzeit widerrufen.",
"has_access_to": "Hat Zugriff auf:",
"menu_title": "Drittanbieter-Anwendungen",
"revoke": "Zugriff widerrufen",
"revoke_question": "Sind Sie sicher, dass Sie den Zugriff für folgende Anwendung widerrufen wollen?",
"subtitle": "Drittanbieter-Anwendungen mit Zugriff auf Ihr Konto",
"title": "Drittanbieter-Anwendungen",
"valid_until": "Gültig bis {date}"
},
"authorized_token": {
"menu_title": "Autorisierte Anwendungen"
}
......@@ -225,5 +241,11 @@
"dismiss": "Verwerfen",
"new_version_available": "Es ist eine neue Version der App verfügbar",
"update": "Aktualisieren"
},
"status": {
"changes": "Sie haben nicht gespeicherte Änderungen.",
"error": "Beim Speichern der letzten Änderungen ist ein Fehler aufgetreten.",
"saved": "Alle Änderungen sind gespeichert.",
"updating": "Änderungen werden synchronisiert."
}
}
{
"about": {
"about_aleksis": "About AlekSIS®",
"page_title": "About AlekSIS®",
"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.",
"free_open_source_licence": "Free/Open Source Licence",
......@@ -15,10 +14,11 @@
"more_information_eupl": "More information about the EUPL",
"open_source": "Open Source",
"other_licence": "Other Licence",
"page_title": "About AlekSIS®",
"proprietary": "Proprietary",
"show_copyright": "Show copyright",
"source_code": "Source Code",
"website_of_aleksis": "Website of AlekSIS",
"show_copyright": "Show copyright"
"website_of_aleksis": "Website of AlekSIS"
},
"accounts": {
"change_password": {
......@@ -45,37 +45,42 @@
"menu_title": "Third-party Accounts"
},
"two_factor": {
"add_authentication_method": "Add Authentication Method",
"backup_codes_count": "You have no backup codes remaining.|You have only one backup code remaining.|You have {counter} backup codes remaining.",
"backup_codes_description": "If you can't use any of your devices, you can access your account using backup codes.",
"backup_codes_title": "Backup Codes",
"disable_button": "Disable Two-Factor Authentication",
"disable_description": "However we strongly discourage you to do so, you can also disable two-factor authentication for your account.",
"disable_title": "Disable Two-Factor Authentication",
"enable_button": "Enable Two-Factor Authentication",
"enable_description": "Two-factor authentication is not enabled for your account. Enable two-factor authentication for enhanced account security.",
"enable_title": "Two-Factor Authentication Currently Disabled",
"menu_title": "2FA",
"title": "Two-Factor Authentication",
"primary_device_title": "Primary Authentication Device",
"primary_device_description": "While logging in, AlekSIS will ask you to confirm the login with the following device. If this device is not available, you can use a backup device.",
"other_devices_title": "Other Authentication Devices",
"other_devices_description": "If your primary authentication device is not available during logging in, you can use one of these devices:",
"methods": {
"generator": "You generate one-time codes using a code generator.",
"call": "We will call you at your mobile phone and tell you a one-time code.",
"email": "We will send you one-time codes to your e-mail address.",
"generator": "You generate one-time codes using a code generator.",
"sms": "We will send you one-time codes to your mobile phone number.",
"call": "We will call you at your mobile phone and tell you a one-time code.",
"webauthn": "You use a security key (either as external device or integrated in your personal device).",
"yubikey": "You use a Yubikey to generate one-time codes."
},
"add_authentication_method": "Add Authentication Method",
"backup_codes_title": "Backup Codes",
"backup_codes_description": "If you can't use any of your devices, you can access your account using backup codes.",
"backup_codes_count": "You have no backup codes remaining.|You have only one backup code remaining.|You have {counter} backup codes remaining.",
"disable_title": "Disable Two-Factor Authentication",
"disable_description": "However we strongly discourage you to do so, you can also disable two-factor authentication for your account.",
"disable_button": "Disable Two-Factor Authentication",
"enable_title": "Two-Factor Authentication Currently Disabled",
"enable_description": "Two-factor authentication is not enabled for your account. Enable two-factor authentication for enhanced account security.",
"enable_button": "Enable Two-Factor Authentication"
"other_devices_description": "If your primary authentication device is not available during logging in, you can use one of these devices:",
"other_devices_title": "Other Authentication Devices",
"primary_device_description": "While logging in, AlekSIS will ask you to confirm the login with the following device. If this device is not available, you can use a backup device.",
"primary_device_title": "Primary Authentication Device",
"title": "Two-Factor Authentication"
}
},
"actions": {
"back": "Back",
"search": "Search",
"cancel": "Cancel",
"close": "Close",
"confirm_deletion": "Are you sure you want to delete this item?",
"delete": "Delete",
"edit": "Edit",
"close": "Close"
"save": "Save",
"search": "Search",
"stop_editing": "Stop editing"
},
"administration": {
"backend_admin": {
......@@ -97,13 +102,14 @@
"base": {
"about_aleksis": "About AlekSIS® — The Free School Information System",
"imprint": "Imprint",
"person_is_dummy": "Your administrator account is not linked to any person. Therefore, a dummy person has been linked to your account.",
"privacy_policy": "Privacy Policy",
"user_not_linked_to_person": "Your user account is not linked to a person. This means you cannot access any school-related information. Please contact the managers of AlekSIS at your school.",
"logo": "Logo",
"no_permission": "",
"no_permission_message_long": "You have no permission to view this page. Please login with an other account.",
"no_permission_message_short": "No permission",
"no_permission_redirect_text": "Go to login",
"logo": "Logo"
"person_is_dummy": "Your administrator account is not linked to any person. Therefore, a dummy person has been linked to your account.",
"privacy_policy": "Privacy Policy",
"user_not_linked_to_person": "Your user account is not linked to a person. This means you cannot access any school-related information. Please contact the managers of AlekSIS at your school."
},
"celery_progress": {
"error_message": "The operation couldn't be finished successfully.",
......@@ -111,11 +117,6 @@
"running_tasks": "1 running task | {number} running tasks",
"success_message": "The operation has been finished successfully."
},
"download_pdf": {
"title": "Downloading PDF file ...",
"download": "Download",
"notice": "If the download does not start automatically, please click the button below."
},
"dashboard": {
"dashboard_widget": {
"menu_title": "Dashboard Widgets",
......@@ -127,6 +128,14 @@
"data_check": {
"menu_title": "Data Checks"
},
"download_pdf": {
"download": "Download",
"notice": "If the download does not start automatically, please click the button below.",
"title": "Downloading PDF file ..."
},
"graphql": {
"snackbar_error_message": "There was an error retrieving the page data. Please try again."
},
"group": {
"additional_field": {
"menu_title": "Additional Fields",
......@@ -140,9 +149,9 @@
},
"groups_and_child_groups": "Groups and Child Groups",
"menu_title": "Groups",
"ownership": "Group ownership",
"title": "Group",
"title_plural": "Groups",
"ownership": "Group ownership"
"title_plural": "Groups"
},
"ical_feed": {
"menu_title": "Calendar Feeds"
......@@ -150,6 +159,13 @@
"legacy": {
"unworthy": "The life-form who created this view did not bow to the powers of the almighty Aleksolotl."
},
"network_errors": {
"back_to_start": "Back to home page",
"error_404": "404",
"offline_notification": "You are offline. Some features may not work and some data may not be up to date.",
"page_not_found": "The requested page or resource could not be found.",
"snackbar_error_message": "A network error occurred. Please try again."
},
"notifications": {
"mark_as_read": "Mark as read",
"more_information": "More information",
......@@ -162,8 +178,19 @@
"title": "OAuth Application",
"title_plural": "OAuth Applications"
},
"authorized_application": {
"access_since": "Access since {date}",
"description": "The following third-party applications have access to your account. You can revoke access at any time for those you don't need or trust anymore.",
"has_access_to": "Has access to:",
"menu_title": "Third-party Applications",
"revoke": "Revoke Access",
"revoke_question": "Are you sure you want to revoke access for this application?",
"subtitle": "Third-party Applications With Access to Your Account",
"title": "Third-party Applications",
"valid_until": "Valid until {date}"
},
"authorized_token": {
"menu_title": "Authorized Applications"
"menu_title": ""
}
},
"people": "People",
......@@ -174,32 +201,32 @@
},
"person": {
"account_menu_title": "Account",
"avatar": "Avatar",
"additional_image": "Additional Image",
"no_additional_image": "The person didn't upload an additional Image",
"logged_in_as": "Logged in as",
"menu_title": "Persons",
"page_title": "Person",
"title": "Person",
"title_plural": "Persons",
"mobile": "mobile phone",
"home": "home phone",
"details": "Contact details",
"avatar": "Avatar",
"children": "Children",
"guardians": "Guardians / Parents",
"no_persons": "No Persons",
"delete": "Delete",
"details": "Contact details",
"guardians": "Guardians / Parents",
"home": "home phone",
"impersonation": {
"impersonate": "Impersonate",
"stop": "Stop Impersonation",
"impersonating": "Impersonating"
"impersonating": "Impersonating",
"stop": "Stop Impersonation"
},
"invite": "Invite"
"invite": "Invite",
"logged_in_as": "Logged in as",
"menu_title": "Persons",
"mobile": "mobile phone",
"no_additional_image": "The person didn't upload an additional Image",
"no_persons": "No Persons",
"page_title": "Person",
"title": "Person",
"title_plural": "Persons"
},
"preferences": {
"person": {
"menu_title": "Preferences",
"change_preferences": "Preferences"
"change_preferences": "Preferences",
"menu_title": "Preferences"
},
"site": {
"menu_title": "Configuration"
......@@ -210,19 +237,15 @@
"title": "School Term",
"title_plural": "School Terms"
},
"network_errors": {
"error_404": "404",
"page_not_found": "The requested page or resource could not be found.",
"back_to_start": "Back to home page",
"snackbar_error_message": "A network error occurred. Please try again.",
"offline_notification": "You are offline. Some features may not work and some data may not be up to date."
},
"service_worker": {
"dismiss": "Dismiss",
"new_version_available": "A new version of the app is available",
"update": "Update",
"dismiss": "Dismiss"
"update": "Update"
},
"graphql": {
"snackbar_error_message": "There was an error retrieving the page data. Please try again."
"status": {
"changes": "You have unsaved changes.",
"error": "There has been an error while saving the latest changes.",
"saved": "All changes are saved.",
"updating": "Changes are being synced."
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment