Skip to content
Commits on Source (40)
module.exports = {
extends: [
'plugin:vue/strongly-recommended',
"eslint:recommended",
"plugin:vue/strongly-recommended",
"prettier",
],
rules: {
'vue/no-unused-vars': 'off',
'vue/multi-word-component-names': 'off'
}
}
"vue/no-unused-vars": "off",
"vue/multi-word-component-names": "off",
},
env: {
browser: true,
node: true,
},
};
<!-- AlekSIS is developed on EduGit. GitHub only serves as
backup mirror and to help people find the project. If
possible, please submit your merge request on EduGit!
EduGit accepts logins with GitHub accounts.
-->
[ ] I have read the above and have no way to contribute on EduGit
[ ] I understand that GitHub's terms of service exclude young and
learning contributors, but still cannot contribute on EduGit
instead.
include:
- project: "AlekSIS/official/AlekSIS"
file: /ci/general.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/prepare/lock.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/test/test.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/test/lint.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/test/security.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/build/dist.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/publish/pypi.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/docker/image.yml
- project: "AlekSIS/official/AlekSIS"
file: "/ci/deploy/review.yml"
- project: "AlekSIS/official/AlekSIS"
file: "/ci/deploy/trigger_dist.yml"
- project: "AlekSIS/official/AlekSIS"
file: /ci/general.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/prepare/lock.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/test/test.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/test/lint.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/test/security.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/build/dist.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/publish/pypi.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/docker/image.yml
- project: "AlekSIS/official/AlekSIS"
file: "/ci/deploy/review.yml"
- project: "AlekSIS/official/AlekSIS"
file: "/ci/deploy/trigger_dist.yml"
# Byte-compiled / optimized / DLL files
*$py.class
*.py[cod]
__pycache__/
# Distribution / packaging
*.egg
*.egg-info/
.Python
.eggs/
.installed.cfg
build/
develop-eggs/
dist/
downloads/
eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
# Installer logs
pip-delete-this-directory.txt
pip-log.txt
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# pyenv
.python-version
# Environments
.env
.venv
ENV/
env/
venv/
# Editors
*~
DEADJOE
\#*#
# IntelliJ
.idea
.idea/
# Database
db.sqlite3
# Sphinx
docs/_build/
# TeX
*.aux
# Generated files
/node_modules/
/static/
/whoosh_index/
poetry.lock
.coverage
.mypy_cache/
.tox/
htmlcov/
maintenance_mode_state.txt
media/
package-lock.json
yarn.lock
# VSCode
.vscode/
.history/
*.code-workspace
/cache
# Add HTML files to avoid problems with unsupported Django templates
*.html
{
"extends": "stylelint-config-standard"
"extends": ["stylelint-config-standard", "stylelint-config-prettier"]
}
......@@ -6,6 +6,36 @@ All notable changes to this project will be documented in this file.
The format is based on `Keep a Changelog`_,
and this project adheres to `Semantic Versioning`_.
`2.12`_ - 2022-11-04
--------------------
Added
~~~~~
* Show also group ownerships on person detail page
* [Dev] Provide plain PDF template without header/footer for special layouts.
* [Dev] Introduce support for reformattinga and linting JS, Vue, and CSS files.
Changed
~~~~~~~
* OIDC scope "profile" now exposes the avatar instead of the official photo
* Language selection on Vue pages now runs via GraphQL queries.
* [Dev] Provide function to generate PDF files from fully-rendered templates.
* [Dev] Accept pre-created file object for PDF generation to define
the redirect URL in advance.
Fixed
~~~~~
* Error message in permission form was misleading.
* The logo in the PDF files was displayed at the wrong position.
* Sometimes the PDF files were not generated correctly
and images were displayed only partially.
* Invite Person view threw an error when personal invites existed
* Personal invites did not work
* Detailed information for done Celery tasks weren't saved.
`2.11.1`_ - 2022-08-31
----------------------
......@@ -926,3 +956,4 @@ Fixed
.. _2.10.2: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.10.2
.. _2.11: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.11
.. _2.11.1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.11.1
.. _2.12: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.12
......@@ -30,6 +30,7 @@ RUN apt-get -y update && \
eatmydata apt-get install -y --no-install-recommends \
build-essential \
chromium \
chromium-driver \
curl \
dumb-init \
gettext \
......
......@@ -68,9 +68,10 @@ Licence
Copyright © 2019, 2020, 2021, 2022 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 Lloyd Meins <meinsll@katharineum.de>
Copyright © 2021 magicfelix <felix@felix-zauberer.de>
Copyright © 2022 Benedict Suska <benedict.suska@teckids.org>
Copyright © 2022 Lukas Weichelt <lukas.weichelt@teckids.org>
Licenced under the EUPL, version 1.2 or later, by Teckids e.V. (Bonn, Germany).
......
......@@ -43,9 +43,10 @@ class CoreConfig(AppConfig):
([2019, 2020, 2021, 2022], "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], "Lloyd Meins", "meinsll@katharineum.de"),
([2021], "magicfelix", "felix@felix-zauberer.de"),
([2022], "Benedict Suska", "benedict.suska@teckids.org"),
([2022], "Lukas Weichelt", "lukas.weichelt@teckids.org"),
)
def ready(self):
......@@ -188,9 +189,9 @@ class CoreConfig(AppConfig):
claims["profile"] = django_request.build_absolute_uri(
request.user.person.get_absolute_url()
)
if request.user.person.photo:
if request.user.person.avatar:
claims["picture"] = django_request.build_absolute_uri(
request.user.person.photo.url
request.user.person.avatar.url
)
else:
claims["given_name"] = request.user.first_name
......
import Vue from "vue"
import VueRouter from "vue-router"
import Vuetify from "vuetify"
import "vuetify/dist/vuetify.min.css"
import Vue from "vue";
import VueRouter from "vue-router";
import Vuetify from "vuetify";
import "vuetify/dist/vuetify.min.css";
import ApolloClient from 'apollo-boost'
import VueApollo from 'vue-apollo'
import ApolloClient from "apollo-boost";
import VueApollo from "vue-apollo";
import "./css/global.scss"
import "./css/global.scss";
import VueI18n from "vue-i18n";
Vue.use(Vuetify)
Vue.use(VueRouter)
import messages from "./messages.json";
Vue.use(VueI18n);
const i18n = new VueI18n({
locale: "en",
fallbackLocale: "en",
messages,
});
// Using this function, apps can register their locale files
i18n.registerLocale = function (messages) {
for (let locale in messages) {
i18n.mergeLocaleMessage(locale, messages[locale]);
}
};
Vue.use(Vuetify);
Vue.use(VueRouter);
const vuetify = new Vuetify({
// TODO: load theme data dynamically
// - find a way to load template context data
// - include all site preferences
// - load menu stuff to render the sidenav
icons: {
iconfont: 'mdi', // default - only for display purposes
values: {
cancel: 'mdi-close-circle-outline',
delete: 'mdi-close-circle-outline',
success: 'mdi-check-circle-outline',
info: 'mdi-information-outline',
warning: 'mdi-alert-outline',
error: 'mdi-alert-octagon-outline',
prev: 'mdi-chevron-left',
next: 'mdi-chevron-right',
checkboxOn: 'mdi-checkbox-marked-outline',
checkboxIndeterminate: 'mdi-minus-box-outline',
edit: 'mdi-pencil-outline',
},
// TODO: load theme data dynamically
// - find a way to load template context data
// - include all site preferences
// - load menu stuff to render the sidenav
icons: {
iconfont: "mdi", // default - only for display purposes
values: {
cancel: "mdi-close-circle-outline",
delete: "mdi-close-circle-outline",
success: "mdi-check-circle-outline",
info: "mdi-information-outline",
warning: "mdi-alert-outline",
error: "mdi-alert-octagon-outline",
prev: "mdi-chevron-left",
next: "mdi-chevron-right",
checkboxOn: "mdi-checkbox-marked-outline",
checkboxIndeterminate: "mdi-minus-box-outline",
edit: "mdi-pencil-outline",
},
theme: {
dark: JSON.parse(document.getElementById("design-mode").textContent) === "dark",
themes: {
light: {
primary: JSON.parse(document.getElementById("primary-color").textContent),
secondary: JSON.parse(document.getElementById("secondary-color").textContent),
},
dark: {
primary: JSON.parse(document.getElementById("primary-color").textContent),
secondary: JSON.parse(document.getElementById("secondary-color").textContent),
},
},
},
theme: {
dark:
JSON.parse(document.getElementById("design-mode").textContent) === "dark",
themes: {
light: {
primary: JSON.parse(
document.getElementById("primary-color").textContent
),
secondary: JSON.parse(
document.getElementById("secondary-color").textContent
),
},
dark: {
primary: JSON.parse(
document.getElementById("primary-color").textContent
),
secondary: JSON.parse(
document.getElementById("secondary-color").textContent
),
},
},
lang: {
locales: JSON.parse(document.getElementById("language-info-list").textContent),
current: JSON.parse(document.getElementById("current-language").textContent),
}
})
},
});
const apolloClient = new ApolloClient({
uri: JSON.parse(document.getElementById("graphql-url").textContent)
})
uri: JSON.parse(document.getElementById("graphql-url").textContent),
});
import CacheNotification from "./components/CacheNotification.vue";
import LanguageForm from "./components/LanguageForm.vue";
......@@ -63,41 +86,55 @@ import SidenavSearch from "./components/SidenavSearch.vue";
Vue.component(MessageBox.name, MessageBox); // Load MessageBox globally as other components depend on it
Vue.use(VueApollo)
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
})
});
const router = new VueRouter({
mode: "history",
// routes: [
// { path: "/", component: "TheApp" },
// }
// routes: [
// { path: "/", component: "TheApp" },
// }
});
const app = new Vue({
el: '#app',
apolloProvider,
vuetify: vuetify,
// delimiters: ["<%","%>"] // FIXME: discuss new delimiters, [[ <% [{ {[ <[ (( …
data: () => ({
drawer: vuetify.framework.breakpoint.lgAndUp,
group: null, // what does this mean?
urls: window.Urls,
django: window.django,
// FIXME: maybe just use window.django in every component or find a suitable way to access this property everywhere
showCacheAlert: false,
languageCode: JSON.parse(document.getElementById("current-language").textContent),
}),
components: {
"cache-notification": CacheNotification,
"language-form": LanguageForm,
"notification-list": NotificationList,
"sidenav-search": SidenavSearch,
el: "#app",
apolloProvider,
vuetify: vuetify,
// delimiters: ["<%","%>"] // FIXME: discuss new delimiters, [[ <% [{ {[ <[ (( …
data: () => ({
drawer: vuetify.framework.breakpoint.lgAndUp,
group: null, // what does this mean?
urls: window.Urls,
django: window.django,
// FIXME: maybe just use window.django in every component or find a suitable way to access this property everywhere
showCacheAlert: false,
systemProperties: {
currentLanguage: "en",
availableLanguages: [],
},
router
})
}),
apollo: {
systemProperties: require("./systemProperties.graphql"),
},
watch: {
systemProperties: function (newProperties) {
this.$i18n.locale = newProperties.currentLanguage;
this.$vuetify.lang.current = newProperties.currentLanguage;
},
},
components: {
"cache-notification": CacheNotification,
"language-form": LanguageForm,
"notification-list": NotificationList,
"sidenav-search": SidenavSearch,
},
router,
i18n,
});
window.app = app;
window.router = router;
window.i18n = i18n;
<template>
<message-box :value="cache" type="warning">
{{ this.$root.django.gettext('This page may contain outdated information since there is no internet connection.') }}
{{ $t("alerts.page_cached") }}
</message-box>
</template>
<script>
export default {
name: "cache-notification",
data () {
return {
cache: false,
}
},
created() {
this.channel = new BroadcastChannel("cache-or-not");
this.channel.onmessage = (event) => {
this.cache = event.data === true;
}
},
destroyed(){
this.channel.close()
},
}
export default {
name: "CacheNotification",
data() {
return {
cache: false,
};
},
created() {
this.channel = new BroadcastChannel("cache-or-not");
this.channel.onmessage = (event) => {
this.cache = event.data === true;
};
},
destroyed() {
this.channel.close();
},
};
</script>
<template>
<form method="post" ref="form" :action="action" id="language-form">
<v-text-field
v-show="false"
name="csrfmiddlewaretoken"
:value="csrf_value"
type="hidden"
></v-text-field>
<v-text-field
v-show="false"
name="next"
:value="next_url"
type="hidden"
></v-text-field>
<v-text-field
v-show="false"
v-model="language"
name="language"
type="hidden"
></v-text-field>
<v-menu offset-y>
<template v-slot:activator="{ on, attrs }">
<v-btn
depressed
v-bind="attrs"
v-on="on"
color="primary"
<v-menu offset-y>
<template #activator="{ on, attrs }">
<v-btn depressed v-bind="attrs" v-on="on" color="primary">
<v-icon icon color="white">mdi-translate</v-icon>
{{ $i18n.locale }}
</v-btn>
</template>
<v-list id="language-dropdown" class="dropdown-content" min-width="150">
<v-skeleton-loader
v-if="!$root.systemProperties.availableLanguages"
class="mx-auto"
type="list-item, list-item, list-item"
></v-skeleton-loader>
<v-list-item-group
v-if="$root.systemProperties.availableLanguages"
v-model="$i18n.locale"
color="primary"
>
<v-list-item
v-for="languageOption in $root.systemProperties.availableLanguages"
:key="languageOption.code"
:value="languageOption.code"
@click="setLanguage(languageOption)"
>
<v-icon icon color="white">mdi-translate</v-icon>
{{ language }}
</v-btn>
</template>
<v-list id="language-dropdown" class="dropdown-content">
<v-list-item-group
v-model="language"
color="primary"
>
<v-list-item v-for="language_option in items" :key="language_option[0]" :value="language_option[0]" @click="submit(language_option[0])">
<v-list-item-title>{{ language_option[1] }}</v-list-item-title>
</v-list-item>
</v-list-item-group>
</v-list>
</v-menu>
</form>
<v-list-item-title>{{
languageOption.nameTranslated
}}</v-list-item-title>
</v-list-item>
</v-list-item-group>
</v-list>
</v-menu>
</template>
<script>
export default {
data: () => ({
items: JSON.parse(document.getElementById("language-info-list").textContent),
language: JSON.parse(document.getElementById("current-language").textContent),
}),
methods: {
submit: function (new_language) {
this.language = new_language;
this.$nextTick(() => {
this.$refs.form.submit();
});
},
export default {
data: function () {
return {
language: this.$i18n.locale,
};
},
methods: {
setLanguage: function (languageOption) {
document.cookie = languageOption.cookie;
this.$i18n.locale = languageOption.code;
this.$vuetify.lang.current = languageOption.code;
},
props: ["action", "csrf_value", "next_url"],
name: "language-form",
}
},
name: "LanguageForm",
};
</script>
<script>
export default {
name: "message-box",
// Due to this component being a wrapper to a v-alert, all props of this can be used (and overridden).
}
export default {
name: "MessageBox",
// Due to this component being a wrapper to a v-alert, all props of this can be used (and overridden).
};
</script>
<template>
<v-alert border="left" text v-bind="$attrs">
<slot>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
</slot>
</v-alert>
<v-alert border="left" text v-bind="$attrs">
<slot>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua.
</slot>
</v-alert>
</template>
<script>
export default {
methods: {
submit: function () {
this.$refs.form.submit()
},
export default {
methods: {
submit: function () {
this.$refs.form.submit();
},
props: ["action", "placeholder"],
name: "sidenav-search",
}
// FIXME: implement suggestions etc, use "loading" attribute
},
props: {
action: {
type: String,
required: true,
},
placeholder: {
type: String,
required: true,
},
},
name: "SidenavSearch",
};
// FIXME: implement suggestions etc, use "loading" attribute
</script>
<template>
<form method="get" ref="form" :action="action" id="search-form">
<v-text-field
:append-icon="'mdi-magnify'" @click:append="submit" single-line
id="search" name="q" type="search" enterkeyhint="search" :placeholder="placeholder"
:append-icon="'mdi-magnify'"
@click:append="submit"
single-line
id="search"
name="q"
type="search"
enterkeyhint="search"
:placeholder="placeholder"
></v-text-field>
</form>
</template>
......@@ -3,10 +3,8 @@
:mutation="require('./markNotificationRead.graphql')"
:variables="{ id: this.notification.id }"
>
<template v-slot="{ mutate, loading, error }">
<v-list-item
v-intersect="mutate"
>
<template #default="{ mutate, loading, error }">
<v-list-item v-intersect="mutate">
<v-list-item-content>
<v-list-item-title>{{ notification.title }}</v-list-item-title>
......@@ -22,7 +20,7 @@
<v-list-item-action v-if="notification.link">
<v-btn text :href="notification.link">
{{ $root.django.gettext('More information') }}
{{ $t("notifications.more_information") }}
</v-btn>
</v-list-item-action>
......@@ -35,9 +33,12 @@
</template>
<script>
export default {
props: {
notification: Object,
export default {
props: {
notification: {
type: Object,
required: true,
},
}
},
};
</script>
<template>
<ApolloQuery
:query="require('./myNotifications.graphql')"
:pollInterval="1000"
:poll-interval="1000"
>
<template v-slot="{ result: { error, data }, isLoading }">
<template #default="{ result: { error, data }, isLoading }">
<v-list two-line v-if="data && data.myNotifications.notifications.length">
<NotificationItem
v-for="notification in data.myNotifications.notifications"
......@@ -11,17 +11,19 @@
:notification="notification"
/>
</v-list>
<p v-else>{{ $root.django.gettext('No notifications available yet.') }}</p>
<p v-else>
{{ $root.django.gettext("No notifications available yet.") }}
</p>
</template>
</ApolloQuery>
</template>
<script>
import NotificationItem from "./NotificationItem.vue";
import NotificationItem from "./NotificationItem.vue";
export default {
components: {
NotificationItem,
},
}
export default {
components: {
NotificationItem,
},
};
</script>
......@@ -2,7 +2,14 @@
// HEADINGS //
//////////////
p, h1, h2, h3, h4, h5, h6, .card-title {
p,
h1,
h2,
h3,
h4,
h5,
h6,
.card-title {
overflow-wrap: break-word;
hyphens: auto;
}
......
import '@mdi/font/css/materialdesignicons.css'
import "@mdi/font/css/materialdesignicons.css";
import "./util"
import "./app"
import "./util";
import "./app";
{
"en": {
"notifications": {
"more_information": "More information",
"no_notifications": "No notifications available yet."
},
"alerts": {
"page_cached": "This page may contain outdated information since there is no internet connection."
}
},
"de": {
"notifications": {
"more_information": "Mehr Informationen",
"no_notifications": "Keine Benachrichtigungen verfügbar."
},
"alerts": {
"page_cached": "Diese Seite enthält vielleicht veraltete Informationen, da es keine Internetverbindung gibt."
}
}
}
{
systemProperties {
availableLanguages {
code
nameTranslated
cookie
}
currentLanguage
}
}