diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cf02f39e36aa6fa715f8e3c73c8069433c44f497..de300997009040e387f232f87e548db1227f1d3d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,8 +5,6 @@ include: 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" diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a8339eb430c913975a3557b197afce858dcd70b8..d4635a2169b2b0b5d6377614002d39532c926c15 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,13 +9,22 @@ and this project adheres to `Semantic Versioning`_. Unreleased ---------- +`3.1.1` - 2023-07-01 +-------------------- + Fixed ~~~~~ * Progress page didn't work properly. * About page failed to load for apps with an unknown licence. - -`3.1`_ - 2022-05-30 +* QUeries for persons with partial permissions failed. +* Some pages couldn't be scrolled when a task progress popup was open. +* Notification query failed on admin users without persons. +* Querying for notification caused unnecessary database requests. +* Loading bar didn't disappear on some pages after loading was finished. +* Support newer versions of django-oauth-toolkit. + +`3.1`_ - 2023-05-30 ------------------- Changed @@ -35,7 +44,7 @@ Fixed * Polling behavior of the whoAmI and permission queries was improved. * Confirmation e-mail contained a wrong link. -`3.0`_ - 2022-05-11 +`3.0`_ - 2023-05-11 ------------------- Added @@ -78,7 +87,7 @@ Fixed * 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. +* Invitation emails were using wrong styling. * GraphQL queries and mutations did not log exceptions. `3.0b3`_ - 2023-03-19 @@ -1173,3 +1182,4 @@ Fixed .. _3.0b3: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.0b3 .. _3.0: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.0 .. _3.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.1 +.. _3.1.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.1.1 diff --git a/aleksis/core/frontend/app/dateTimeFormats.js b/aleksis/core/frontend/app/dateTimeFormats.js index b5d498f29c1c1a7bcfd2e59f64c695cadfcd9027..567f9a196f61052ff0481c1f975a6c0d2433964e 100644 --- a/aleksis/core/frontend/app/dateTimeFormats.js +++ b/aleksis/core/frontend/app/dateTimeFormats.js @@ -29,7 +29,7 @@ const dateTimeFormats = { shortTime: { hour: "numeric", minute: "numeric", - } + }, }, de: { short: { @@ -60,7 +60,7 @@ const dateTimeFormats = { shortTime: { hour: "numeric", minute: "numeric", - } + }, }, }; diff --git a/aleksis/core/frontend/components/LegacyBaseTemplate.vue b/aleksis/core/frontend/components/LegacyBaseTemplate.vue index 790fc10c592bac5acc1e3bbadabcae63e065804f..0105952fd08792c2cb4a472465d435a1f94a0eb3 100644 --- a/aleksis/core/frontend/components/LegacyBaseTemplate.vue +++ b/aleksis/core/frontend/components/LegacyBaseTemplate.vue @@ -71,7 +71,9 @@ export default { // Show loader if iframe starts to change its content, even if the $route stays the same this.$refs.contentIFrame.contentWindow.onpagehide = () => { - this.$root.contentLoading = true; + if (this.$root.isLegacyBaseTemplate) { + this.$root.contentLoading = true; + } }; // Write title of iframe to SPA window diff --git a/aleksis/core/frontend/components/app/App.vue b/aleksis/core/frontend/components/app/App.vue index 9c3a9ced9b3e950a8c4955b96bc6afa5f79355b9..52e2801f3953024fd5071679b92ff1a1bb716f01 100644 --- a/aleksis/core/frontend/components/app/App.vue +++ b/aleksis/core/frontend/components/app/App.vue @@ -316,7 +316,7 @@ export default { </script> <style> -div[aria-required=true] .v-input .v-label::after { +div[aria-required="true"] .v-input .v-label::after { content: " *"; color: red; } diff --git a/aleksis/core/frontend/components/celery_progress/CeleryProgressBottom.vue b/aleksis/core/frontend/components/celery_progress/CeleryProgressBottom.vue index 912fb779241ef577c6f900abc78a26008df008f6..1866c3d819ace7f6d329926f6dbe7bdc3b9b0276 100644 --- a/aleksis/core/frontend/components/celery_progress/CeleryProgressBottom.vue +++ b/aleksis/core/frontend/components/celery_progress/CeleryProgressBottom.vue @@ -1,5 +1,11 @@ <template> - <v-bottom-sheet :value="show" persistent hide-overlay max-width="400px"> + <v-bottom-sheet + :value="show" + persistent + hide-overlay + max-width="400px" + ref="sheet" + > <v-expansion-panels accordion v-model="open"> <v-expansion-panel> <v-expansion-panel-header color="primary" class="white--text px-4"> @@ -33,6 +39,13 @@ export default { data() { return { open: 0 }; }, + mounted() { + // Vuetify uses the hideScroll method to disable scrolling by setting an event listener + // to the window. As event listeners can only be removed by referencing the listener + // method and because vuetify this method is called on every state change of the dialog, + // we simply replace the method in this component instance + this.$refs.sheet.hideScroll = this.$refs.sheet.showScroll; + }, computed: { show() { return this.celeryProgressByUser && this.celeryProgressByUser.length > 0; diff --git a/aleksis/core/frontend/components/generic/InlineCRUDList.vue b/aleksis/core/frontend/components/generic/InlineCRUDList.vue index 1a31de5fc285831d43eb713725b292473253d5a3..ef1a17a91cb037586cd67f7e9abafcda1ee796b3 100644 --- a/aleksis/core/frontend/components/generic/InlineCRUDList.vue +++ b/aleksis/core/frontend/components/generic/InlineCRUDList.vue @@ -28,14 +28,11 @@ :class="elevationClass" :items-per-page="15" :search="search" - :sort-by.sync="sortBy" :sort-desc.sync="sortDesc" multi-sort - @update:sort-by="handleSortChange" @update:sort-desc="handleSortChange" - :show-select="generatedActions.length > 0" selectable-key="canDelete" @item-selected="handleItemSelected" @@ -45,31 +42,28 @@ <template #top> <v-toolbar flat class="height-fit child-height-fit"> <v-row class="flex-wrap gap align-baseline pt-4"> - <v-toolbar-title class="d-flex flex-wrap w-100 gap"> + <v-toolbar-title class="d-flex flex-wrap w-100 gap"> <filter-button - class="my-1 button-40" - :num-filters="numFilters" - v-if="filter" - @click="requestFilter" - @clear="clearFilters" + class="my-1 button-40" + :num-filters="numFilters" + v-if="filter" + @click="requestFilter" + @clear="clearFilters" /> - <filter-dialog - v-model="filterDialog" - :filters="filters" - @filters="handleFiltersChanged" - > - <template #default="slotProps"> - <slot name="filters" - v-if="filter" - v-bind="slotProps" - > - <message-box type="error"> - Please implement your filter dialog! - </message-box> - </slot> - </template> - </filter-dialog> + <filter-dialog + v-model="filterDialog" + :filters="filters" + @filters="handleFiltersChanged" + > + <template #default="slotProps"> + <slot name="filters" v-if="filter" v-bind="slotProps"> + <message-box type="error"> + Please implement your filter dialog! + </message-box> + </slot> + </template> + </filter-dialog> <div class="my-1"> <v-text-field @@ -87,33 +81,36 @@ ></v-text-field> </div> - <div v-if="generatedActions.length > 0 && selectedItems.length > 0" class="my-1"> + <div + v-if="generatedActions.length > 0 && selectedItems.length > 0" + class="my-1" + > <v-autocomplete - auto-select-first - clearable - :items="generatedActions" - v-model="selectedAction" - return-object - :label="$t('actions.select_action')" - item-text="name" - outlined - dense - :hint="$tc('selection.num_items_selected', selectedItems.length)" - persistent-hint - append-outer-icon="$send" - @click:append-outer="handleAction" + auto-select-first + clearable + :items="generatedActions" + v-model="selectedAction" + return-object + :label="$t('actions.select_action')" + item-text="name" + outlined + dense + :hint=" + $tc('selection.num_items_selected', selectedItems.length) + " + persistent-hint + append-outer-icon="$send" + @click:append-outer="handleAction" > <template #item="{ item, attrs, on }"> - <v-list-item - dense - v-bind="attrs" - v-on="on" - > + <v-list-item dense v-bind="attrs" v-on="on"> <v-list-item-icon v-if="item.icon"> - <v-icon v-text="item.icon"/> + <v-icon v-text="item.icon" /> </v-list-item-icon> <v-list-item-content> - <v-list-item-title v-text="item.name"></v-list-item-title> + <v-list-item-title + v-text="item.name" + ></v-list-item-title> </v-list-item-content> </v-list-item> </template> @@ -121,80 +118,79 @@ </div> </v-toolbar-title> - <v-spacer class="flex-grow-0 flex-sm-grow-1 mx-n1 mx-sm-0"></v-spacer> - <slot - v-if="!editMode && showCreate" - name="createComponent" - :attrs='{ - value: createMode, - getCreateData: getCreateData, - defaultItem: defaultItem, - gqlQuery: gqlQuery, - gqlCreateMutation: gqlCreateMutation, - gqlPatchMutation: gqlPatchMutation, - isCreate: true, - fields: editableHeaders, - createItemI18nKey: createItemI18nKey, - }' - - :on="{ - input: i => i ? requestCreate() : null, - cancel: cancelCreate, - save: handleCreateDone, - error: handleError, - }" - - :create-mode="createMode" - :form-field-slot-name="formFieldSlotName" - + <v-spacer + class="flex-grow-0 flex-sm-grow-1 mx-n1 mx-sm-0" + ></v-spacer> + <slot + v-if="!editMode && showCreate" + name="createComponent" + :attrs="{ + value: createMode, + getCreateData: getCreateData, + defaultItem: defaultItem, + gqlQuery: gqlQuery, + gqlCreateMutation: gqlCreateMutation, + gqlPatchMutation: gqlPatchMutation, + isCreate: true, + fields: editableHeaders, + createItemI18nKey: createItemI18nKey, + }" + :on="{ + input: (i) => (i ? requestCreate() : null), + cancel: cancelCreate, + save: handleCreateDone, + error: handleError, + }" + :create-mode="createMode" + :form-field-slot-name="formFieldSlotName" + > + <dialog-object-form + v-model="createMode" + :get-create-data="getCreateData" + :default-item="defaultItem" + :gql-create-mutation="gqlCreateMutation" + :gql-patch-mutation="gqlPatchMutation" + :isCreate="true" + :fields="editableHeaders" + :create-item-i18n-key="createItemI18nKey" + @cancel="cancelCreate" + @save="handleCreateDone" + @error="handleError" > - <dialog-object-form - v-model="createMode" - :get-create-data="getCreateData" - :default-item="defaultItem" - :gql-create-mutation="gqlCreateMutation" - :gql-patch-mutation="gqlPatchMutation" - :isCreate="true" - :fields="editableHeaders" - :create-item-i18n-key="createItemI18nKey" - @cancel="cancelCreate" - @save="handleCreateDone" - @error="handleError" - > - <template #activator="{ props }"> - <create-button - color="secondary" - @click="requestCreate" - :disabled="createMode" - /> - </template> + <template #activator="{ props }"> + <create-button + color="secondary" + @click="requestCreate" + :disabled="createMode" + /> + </template> - <template - v-for="header in editableHeaders" - #[formFieldSlotName(header)]="{ item, isCreate, on, attrs }" - > - <slot - :name="formFieldSlotName(header)" - :attrs="attrs" - :on="on" - :item="item" - :isCreate="isCreate" - /> - </template> - </dialog-object-form> - </slot> - <edit-button - v-if="!editMode && editingEnabled" - @click="requestEdit" - :disabled="createMode" - /> - <cancel-button v-if="editMode" @click="cancelEdit" /> - <save-button - v-if="editMode" - @click="saveEdit" - :loading="loading" - :disabled="!valid" - /> + <template + v-for="header in editableHeaders" + #[formFieldSlotName(header)]="{ item, isCreate, on, attrs }" + > + <slot + :name="formFieldSlotName(header)" + :attrs="attrs" + :on="on" + :item="item" + :isCreate="isCreate" + /> + </template> + </dialog-object-form> + </slot> + <edit-button + v-if="!editMode && editingEnabled" + @click="requestEdit" + :disabled="createMode" + /> + <cancel-button v-if="editMode" @click="cancelEdit" /> + <save-button + v-if="editMode" + @click="saveEdit" + :loading="loading" + :disabled="!valid" + /> </v-row> </v-toolbar> </template> @@ -229,10 +225,7 @@ </template> <template #item.actions="{ item }"> - <slot - name="actions" - :item="item" - /> + <slot name="actions" :item="item" /> <v-btn v-if="'canDelete' in item && item.canDelete" icon @@ -287,13 +280,15 @@ export default { return { orderBy: this.gqlOrderBy, filters: this.filterString, - } + }; }, error: function (error) { this.handleError(error); }, result: function (data) { - this.editableItems = data.data ? JSON.parse(JSON.stringify(data.data.items)) : []; + this.editableItems = data.data + ? JSON.parse(JSON.stringify(data.data.items)) + : []; }, }; }, @@ -328,14 +323,18 @@ export default { computed: { tableHeaders() { return this.headers - .concat(this.deletionEnabled ? [ - { - text: this.$t("actions.title"), - value: "actions", - sortable: false, - align: "right", - }, - ] : []) + .concat( + this.deletionEnabled + ? [ + { + text: this.$t("actions.title"), + value: "actions", + sortable: false, + align: "right", + }, + ] + : [] + ) .filter((header) => this.hiddenColumns.indexOf(header.value) === -1); }, editableHeaders() { @@ -345,13 +344,24 @@ export default { return this.elevated ? "elevation-2" : ""; }, editingEnabled() { - return this.gqlPatchMutation && this.items && this.items.some(i => i.canEdit); + return ( + this.gqlPatchMutation && this.items && this.items.some((i) => i.canEdit) + ); }, deletionEnabled() { - return this.gqlDeleteMutation && this.items && this.items.some(i => i.canDelete); + return ( + this.gqlDeleteMutation && + this.items && + this.items.some((i) => i.canDelete) + ); }, multipleDeletionEnabled() { - return this.multipleDeletion && this.gqlDeleteMultipleMutation && this.items && this.items.some(i => i.canDelete); + return ( + this.multipleDeletion && + this.gqlDeleteMultipleMutation && + this.items && + this.items.some((i) => i.canDelete) + ); }, numFilters() { // This needs to use the json string, as vue reactivity doesn't work for objects with dynamic properties @@ -366,14 +376,14 @@ export default { { name: this.$t("actions.delete"), icon: "$deleteContent", - handler: items => { + handler: (items) => { this.itemsToDelete = items; this.multipleDeletionDialog = true; }, clearSelection: true, - } - ] - } + }, + ]; + }, }, props: { i18nKey: { @@ -472,7 +482,7 @@ export default { type: Boolean, required: false, default: true, - } + }, }, methods: { requestCreate() { @@ -624,21 +634,24 @@ export default { }; }, snakeCase(string) { - return string.replace(/\W+/g, " ") - .split(/ |\B(?=[A-Z])/) - .map(word => word.toLowerCase()) - .join('_'); + return string + .replace(/\W+/g, " ") + .split(/ |\B(?=[A-Z])/) + .map((word) => word.toLowerCase()) + .join("_"); }, orderKey(value, desc) { - const key = this.headers.find(header => header.value === value).orderKey || this.snakeCase(value); + const key = + this.headers.find((header) => header.value === value).orderKey || + this.snakeCase(value); return (desc ? "-" : "") + key; }, handleSortChange() { - this.gqlOrderBy = this.sortBy.map( - (value, key) => this.orderKey(value, this.sortDesc[key]) - ) + this.gqlOrderBy = this.sortBy.map((value, key) => + this.orderKey(value, this.sortDesc[key]) + ); }, - handleItemSelected({item, value}) { + handleItemSelected({ item, value }) { if (value) { this.selectedItems.push(item); } else { @@ -648,10 +661,10 @@ export default { } } }, - handleToggleAll({items, value}) { + handleToggleAll({ items, value }) { if (value) { // There is a bug in vuetify: items contains all elements, even those that aren't selectable - this.selectedItems = items.filter(item => item.canDelete || false); + this.selectedItems = items.filter((item) => item.canDelete || false); } else { this.selectedItems = []; } @@ -659,12 +672,10 @@ export default { }, checkSelectAll(newItems) { if (this.allSelected) { - this.handleToggleAll( - { - items: newItems, - value: true, - } - ) + this.handleToggleAll({ + items: newItems, + value: true, + }); } }, handleAction() { @@ -689,7 +700,8 @@ export default { .gap { gap: 0.5rem; } -.height-fit, .child-height-fit > * { +.height-fit, +.child-height-fit > * { height: fit-content !important; } diff --git a/aleksis/core/frontend/components/generic/ObjectCRUDList.vue b/aleksis/core/frontend/components/generic/ObjectCRUDList.vue index 99fd458ab48a9516c5c4727083dba6438c87947e..c3cd91d4c2a5002de18cc5802cff5e1ddc50584a 100644 --- a/aleksis/core/frontend/components/generic/ObjectCRUDList.vue +++ b/aleksis/core/frontend/components/generic/ObjectCRUDList.vue @@ -13,7 +13,9 @@ import DialogObjectForm from "./dialogs/DialogObjectForm.vue"; > <template #loading> <slot name="loading"> - <v-skeleton-loader type="card-heading, list-item-avatar-two-line@3, actions" /> + <v-skeleton-loader + type="card-heading, list-item-avatar-two-line@3, actions" + /> </slot> </template> @@ -37,24 +39,38 @@ import DialogObjectForm from "./dialogs/DialogObjectForm.vue"; > <v-list> <template v-for="(item, index) in items"> - <v-list-item - :key="item.id" - > + <v-list-item :key="item.id"> <v-list-item-avatar> - <slot name="listIteratorItemAvatar" :item="item" :index="index" /> + <slot + name="listIteratorItemAvatar" + :item="item" + :index="index" + /> </v-list-item-avatar> <v-list-item-content> - <slot name="listIteratorItemContent" :item="item" :index="index"> + <slot + name="listIteratorItemContent" + :item="item" + :index="index" + > <v-list-item-title> {{ item.name }} </v-list-item-title> </slot> </v-list-item-content> <v-list-item-action> - <v-btn v-if="editingEnabled && item.canEdit" icon @click="handleEdit(item)"> + <v-btn + v-if="editingEnabled && item.canEdit" + icon + @click="handleEdit(item)" + > <v-icon>mdi-pencil-outline</v-icon> </v-btn> - <v-btn v-if="deletionEnabled && item.canDelete" icon @click="handleDelete(item)"> + <v-btn + v-if="deletionEnabled && item.canDelete" + icon + @click="handleDelete(item)" + > <v-icon>mdi-delete-outline</v-icon> </v-btn> </v-list-item-action> @@ -214,7 +230,7 @@ export default { type: Number, required: false, default: 5, - } + }, }, components: { CreateButton, @@ -239,11 +255,11 @@ export default { error: function (error) { this.handleError(error); }, - update (data) { + update(data) { return this.getGqlData(data); }, - } - } + }; + }, }, methods: { handleCreate() { @@ -260,9 +276,7 @@ export default { this.isCreate = false; this.objectFormModel = true; }, - handleDelete() { - - }, + handleDelete() {}, handleCreateDone() { this.$apollo.queries.items.refetch(); }, @@ -290,11 +304,7 @@ export default { }, computed: { creatingEnabled() { - return ( - this.gqlCreateMutation && - this.fields && - this.defaultItem - ); + return this.gqlCreateMutation && this.fields && this.defaultItem; }, editingEnabled() { return ( @@ -313,7 +323,7 @@ export default { }, title() { return this.titleI18nKey ? this.$t(this.titleI18nKey) : this.titleString; - } + }, }, }; </script> diff --git a/aleksis/core/frontend/components/generic/buttons/BaseButton.vue b/aleksis/core/frontend/components/generic/buttons/BaseButton.vue index 875b01b4105c1223b7011ad203af10f39adb2798..22d6a64c334ad78285d88e90f7767b43a6fdf35f 100644 --- a/aleksis/core/frontend/components/generic/buttons/BaseButton.vue +++ b/aleksis/core/frontend/components/generic/buttons/BaseButton.vue @@ -1,8 +1,8 @@ <template> <v-btn v-bind="{ ...$props, ...$attrs }" @click="$emit('click', $event)"> <slot> - <v-icon v-if="iconText" v-text="iconText" left/> - <span v-t="i18nKey"/> + <v-icon v-if="iconText" v-text="iconText" left /> + <span v-t="i18nKey" /> </slot> </v-btn> </template> diff --git a/aleksis/core/frontend/components/generic/buttons/FilterButton.vue b/aleksis/core/frontend/components/generic/buttons/FilterButton.vue index b379d1d1a4e744ddfd3b657dd6cdb74695002e9e..ea8aa88edcbb23a7283c650eed7beb5d0a22c05b 100644 --- a/aleksis/core/frontend/components/generic/buttons/FilterButton.vue +++ b/aleksis/core/frontend/components/generic/buttons/FilterButton.vue @@ -1,29 +1,23 @@ <template> <secondary-action-button - v-bind="$attrs" - v-on="$listeners" - :i18n-key="i18nKey" + v-bind="$attrs" + v-on="$listeners" + :i18n-key="i18nKey" > - <v-icon v-text="icon" left/> - <v-badge - color="secondary" - :value="numFilters" - :content="numFilters" - inline - > - <span v-t="i18nKey"/> + <v-icon v-text="icon" left /> + <v-badge color="secondary" :value="numFilters" :content="numFilters" inline> + <span v-t="i18nKey" /> </v-badge> <v-btn - icon - @click.stop="$emit('clear')" - small - v-if="numFilters" - class="mr-n1" + icon + @click.stop="$emit('clear')" + small + v-if="numFilters" + class="mr-n1" > <v-icon>$clear</v-icon> </v-btn> </secondary-action-button> - </template> <script> @@ -31,12 +25,14 @@ import SecondaryActionButton from "./SecondaryActionButton.vue"; export default { name: "FilterButton", - components: {SecondaryActionButton}, + components: { SecondaryActionButton }, extends: SecondaryActionButton, computed: { icon() { - return this.hasFilters || this.numFilters > 0 ? "$filterSet" : "$filterEmpty"; - } + return this.hasFilters || this.numFilters > 0 + ? "$filterSet" + : "$filterEmpty"; + }, }, props: { i18nKey: { @@ -53,7 +49,7 @@ export default { type: Number, required: false, default: 0, - } + }, }, }; </script> diff --git a/aleksis/core/frontend/components/generic/dialogs/DeleteDialog.vue b/aleksis/core/frontend/components/generic/dialogs/DeleteDialog.vue index 97f3c87f3098b55b145aed8d4d8a0e01a7a0d681..550ca6f9669ad49f47d7efa587a30c2444846fc8 100644 --- a/aleksis/core/frontend/components/generic/dialogs/DeleteDialog.vue +++ b/aleksis/core/frontend/components/generic/dialogs/DeleteDialog.vue @@ -1,6 +1,6 @@ <script setup> import CancelButton from "../buttons/CancelButton.vue"; -import DeleteButton from "../buttons/DeleteButton.vue" +import DeleteButton from "../buttons/DeleteButton.vue"; import MobileFullscreenDialog from "./MobileFullscreenDialog.vue"; import PrimaryActionButton from "../buttons/PrimaryActionButton.vue"; </script> @@ -15,31 +15,27 @@ import PrimaryActionButton from "../buttons/PrimaryActionButton.vue"; > <template #default="{ mutate, loading, error }"> <mobile-fullscreen-dialog v-model="dialogOpen"> - <template #title> - <slot name="title"> - {{ $t("actions.confirm_deletion") }} - </slot> - </template> - <template #content> - <slot name="body"> - <p class="text-body-1">{{ nameOfObject }}</p> + <template #title> + <slot name="title"> + {{ $t("actions.confirm_deletion") }} + </slot> + </template> + <template #content> + <slot name="body"> + <p class="text-body-1">{{ nameOfObject }}</p> + </slot> + </template> + <template #actions> + <cancel-button @click="close(false)" :disabled="loading"> + <slot name="cancelContent"> + <v-icon left>$cancel</v-icon> + {{ $t("actions.cancel") }} </slot> - </template> - <template #actions> - <cancel-button @click="close(false)" :disabled="loading"> - <slot name="cancelContent"> - <v-icon left>$cancel</v-icon> - {{ $t("actions.cancel") }} - </slot> - </cancel-button> - <delete-button - @click="mutate" - :loading="loading" - :disabled="loading" - > - <slot name="deleteContent" /> - </delete-button> - </template> + </cancel-button> + <delete-button @click="mutate" :loading="loading" :disabled="loading"> + <slot name="deleteContent" /> + </delete-button> + </template> </mobile-fullscreen-dialog> <v-snackbar :value="error !== null"> {{ error }} @@ -76,11 +72,11 @@ export default { if (this.gqlQuery.hasOwnProperty("options")) { return { ...this.gqlQuery.options, - variables: JSON.parse(this.gqlQuery.previousVariablesJson) - } + variables: JSON.parse(this.gqlQuery.previousVariablesJson), + }; } - return {query: this.gqlQuery} - } + return { query: this.gqlQuery }; + }, }, methods: { update(store) { diff --git a/aleksis/core/frontend/components/generic/dialogs/DeleteMultipleDialog.vue b/aleksis/core/frontend/components/generic/dialogs/DeleteMultipleDialog.vue index b1842ea8f60592801863765b80585a7ab8776b8b..6e80c26d98796af5a66309f6502b03334f0261b9 100644 --- a/aleksis/core/frontend/components/generic/dialogs/DeleteMultipleDialog.vue +++ b/aleksis/core/frontend/components/generic/dialogs/DeleteMultipleDialog.vue @@ -1,6 +1,6 @@ <script setup> import CancelButton from "../buttons/CancelButton.vue"; -import DeleteButton from "../buttons/DeleteButton.vue" +import DeleteButton from "../buttons/DeleteButton.vue"; import MobileFullscreenDialog from "./MobileFullscreenDialog.vue"; </script> @@ -14,36 +14,29 @@ import MobileFullscreenDialog from "./MobileFullscreenDialog.vue"; > <template #default="{ mutate, loading, error }"> <mobile-fullscreen-dialog v-model="dialogOpen"> - <template #title> - <slot name="title"> - {{ $t("actions.confirm_deletion_multiple") }} + <template #title> + <slot name="title"> + {{ $t("actions.confirm_deletion_multiple") }} + </slot> + </template> + <template #content> + <slot name="body"> + <ul class="text-body-1"> + <li v-for="item in items">{{ nameOfItem(item) }}</li> + </ul> + </slot> + </template> + <template #actions> + <cancel-button @click="close(false)" :disabled="loading"> + <slot name="cancelContent"> + <v-icon left>$cancel</v-icon> + {{ $t("actions.cancel") }} </slot> - </template> - <template #content> - <slot name="body"> - <ul class="text-body-1"> - <li v-for="item in items">{{ nameOfItem(item) }}</li> - </ul> - </slot> - </template> - <template #actions> - <cancel-button - @click="close(false)" - :disabled="loading" - > - <slot name="cancelContent"> - <v-icon left>$cancel</v-icon> - {{ $t("actions.cancel") }} - </slot> - </cancel-button> - <delete-button - @click="mutate" - :loading="loading" - :disabled="loading" - > - <slot name="deleteContent" /> - </delete-button> - </template> + </cancel-button> + <delete-button @click="mutate" :loading="loading" :disabled="loading"> + <slot name="deleteContent" /> + </delete-button> + </template> </mobile-fullscreen-dialog> </template> </ApolloMutation> @@ -63,17 +56,17 @@ export default { }, }, ids() { - return this.items.map(item => item[this.itemId]); + return this.items.map((item) => item[this.itemId]); }, query() { if (this.gqlQuery.hasOwnProperty("options")) { return { ...this.gqlQuery.options, - variables: JSON.parse(this.gqlQuery.previousVariablesJson) - } + variables: JSON.parse(this.gqlQuery.previousVariablesJson), + }; } - return {query: this.gqlQuery} - } + return { query: this.gqlQuery }; + }, }, methods: { update(store) { @@ -98,7 +91,7 @@ export default { console.debug("Removing item from store:", item); // Remove item from stored data const index = storedData[storedDataKey].findIndex( - (m) => m.id === item.id + (m) => m.id === item.id ); storedData[storedDataKey].splice(index, 1); } diff --git a/aleksis/core/frontend/components/generic/dialogs/DialogObjectForm.vue b/aleksis/core/frontend/components/generic/dialogs/DialogObjectForm.vue index c1cdf384ecddb025d7750fa590f21802e432b891..8d9bad8c7c3b6f9bbd384009406eb79e26311d51 100644 --- a/aleksis/core/frontend/components/generic/dialogs/DialogObjectForm.vue +++ b/aleksis/core/frontend/components/generic/dialogs/DialogObjectForm.vue @@ -156,7 +156,12 @@ export default { save() { this.loading = true; - if (!this.itemModel || this.isCreate && !this.gqlCreateMutation || !this.isCreate && !this.gqlPatchMutation) return; + if ( + !this.itemModel || + (this.isCreate && !this.gqlCreateMutation) || + (!this.isCreate && !this.gqlPatchMutation) + ) + return; let mutation = this.isCreate ? this.gqlCreateMutation @@ -227,17 +232,19 @@ export default { updateModel() { // Only update the model if the dialog is hidden or has just been mounted if (this.forceModelItemUpdate || !this.firstInitDone || !this.dialog) { - this.itemModel = JSON.parse(JSON.stringify(this.isCreate ? this.defaultItem : this.editItem)); + this.itemModel = JSON.parse( + JSON.stringify(this.isCreate ? this.defaultItem : this.editItem) + ); } - } + }, }, mounted() { this.updateModel(); this.firstInitDone = true; this.$watch("isCreate", this.updateModel); - this.$watch("defaultItem", this.updateModel, { deep: true, }); - this.$watch("editItem", this.updateModel, { deep: true, }); + this.$watch("defaultItem", this.updateModel, { deep: true }); + this.$watch("editItem", this.updateModel, { deep: true }); }, }; </script> diff --git a/aleksis/core/frontend/components/generic/dialogs/FilterDialog.vue b/aleksis/core/frontend/components/generic/dialogs/FilterDialog.vue index f8b75641b871060b24bcd550d6c8150cdbf03bcf..f1ead340b44ca0982cf0726607146a08f1c57c42 100644 --- a/aleksis/core/frontend/components/generic/dialogs/FilterDialog.vue +++ b/aleksis/core/frontend/components/generic/dialogs/FilterDialog.vue @@ -1,22 +1,23 @@ <template> - <mobile-fullscreen-dialog - v-bind="$attrs" - v-on="$listeners" - > + <mobile-fullscreen-dialog v-bind="$attrs" v-on="$listeners"> <template #title>{{ $t("actions.filter") }}</template> <template #content> <form ref="form" @submit.prevent="save"> - <slot - :attrs="attrs" - :on="on" - ></slot> + <slot :attrs="attrs" :on="on"></slot> </form> </template> <template #actions> - <cancel-button i18n-key="actions.clear_filters" @click="clearFilters"></cancel-button> - <save-button i18n-key="actions.filter" icon-text="$filterEmpty" @click="save"></save-button> + <cancel-button + i18n-key="actions.clear_filters" + @click="clearFilters" + ></cancel-button> + <save-button + i18n-key="actions.filter" + icon-text="$filterEmpty" + @click="save" + ></save-button> </template> </mobile-fullscreen-dialog> </template> @@ -28,12 +29,12 @@ import SaveButton from "../buttons/SaveButton.vue"; export default { name: "FilterDialog", - components: {SaveButton, CancelButton, MobileFullscreenDialog}, + components: { SaveButton, CancelButton, MobileFullscreenDialog }, props: { filters: { type: Object, required: true, - } + }, }, methods: { save() { @@ -49,26 +50,25 @@ export default { }, clearFilters() { this.$refs.form.reset(); - this.$emit("filters", {}) + this.$emit("filters", {}); this.$emit("input", false); }, on(field) { return { - change: i => this.filters[field] = i, - input: i => this.filters[field] = i - } + change: (i) => (this.filters[field] = i), + input: (i) => (this.filters[field] = i), + }; }, attrs(field, defaultValue) { if ([null, undefined].includes(this.filters[field]) && !!defaultValue) { - this.filters[field] = defaultValue + this.filters[field] = defaultValue; } return { - value: this.filters[field] - } - } + value: this.filters[field], + }; + }, }, -} +}; </script> -<style scoped> -</style> +<style scoped></style> diff --git a/aleksis/core/frontend/components/generic/forms/ColorField.vue b/aleksis/core/frontend/components/generic/forms/ColorField.vue index d52c2b5187f6dc1d1254948a51d72924575d7d63..a2313cb9036a21e24922f2b171bd52788eb6a915 100644 --- a/aleksis/core/frontend/components/generic/forms/ColorField.vue +++ b/aleksis/core/frontend/components/generic/forms/ColorField.vue @@ -17,21 +17,11 @@ :rules="rules" > <template #prepend-inner> - <v-icon - :color="color" - v-bind="attrs" - v-on="on" - > - mdi-circle - </v-icon> + <v-icon :color="color" v-bind="attrs" v-on="on"> mdi-circle </v-icon> </template> </v-text-field> </template> - <v-color-picker - v-if="menu" - v-model="color" - ref="picker" - ></v-color-picker> + <v-color-picker v-if="menu" v-model="color" ref="picker"></v-color-picker> </v-menu> </template> @@ -43,14 +33,16 @@ export default { return { menu: false, rules: [ - value => /^(#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8}))?$/i.test(value) || this.$t("forms.errors.invalid_color"), + (value) => + /^(#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8}))?$/i.test(value) || + this.$t("forms.errors.invalid_color"), ], - } + }; }, props: { value: { type: String, - default: undefined + default: undefined, }, }, computed: { @@ -60,10 +52,10 @@ export default { }, set(newValue) { this.$emit("input", newValue); - } - } + }, + }, }, -} +}; </script> <style scoped></style> diff --git a/aleksis/core/frontend/components/generic/forms/DateField.vue b/aleksis/core/frontend/components/generic/forms/DateField.vue index fded64cafc47d622be4d71a06fa9829fccba8f40..0ac11bc4b7a4c7c898ec7fc5a40c99eb73f12ce1 100644 --- a/aleksis/core/frontend/components/generic/forms/DateField.vue +++ b/aleksis/core/frontend/components/generic/forms/DateField.vue @@ -47,7 +47,8 @@ export default { innerDate: this.value, openDueToFocus: true, rules: [ - (value) => !value || !!Date.parse(value) || this.$t("forms.errors.invalid_date"), + (value) => + !value || !!Date.parse(value) || this.$t("forms.errors.invalid_date"), ], }; }, diff --git a/aleksis/core/frontend/components/generic/forms/ForeignKeyField.vue b/aleksis/core/frontend/components/generic/forms/ForeignKeyField.vue index 4da1ab6b273b129b0100b9ca5066370dbaa5887b..62b93382be643f67840e458eddb78f6404fce151 100644 --- a/aleksis/core/frontend/components/generic/forms/ForeignKeyField.vue +++ b/aleksis/core/frontend/components/generic/forms/ForeignKeyField.vue @@ -1,11 +1,11 @@ <template> <v-autocomplete - v-bind="$attrs" - v-on="$listeners" - :items="items" - item-value="id" - :item-text="itemName" - class="fc-my-auto" + v-bind="$attrs" + v-on="$listeners" + :items="items" + item-value="id" + :item-text="itemName" + class="fc-my-auto" > <template #append-outer> <v-btn icon @click="menu = true"> @@ -14,7 +14,7 @@ <slot name="createComponent" - :attrs='{ + :attrs="{ value: menu, defaultItem: defaultItem, gqlQuery: gqlQuery, @@ -24,11 +24,10 @@ fields: fields, getCreateData: getCreateData, createItemI18nKey: createItemI18nKey, - }' - + }" :on="{ - input: i => menu = i, - cancel: () => menu = false, + input: (i) => (menu = i), + cancel: () => (menu = false), save: handleSave, update: handleUpdate, }" @@ -48,16 +47,17 @@ :create-item-i18n-key="createItemI18nKey" :get-create-data="getCreateData" > - <template v-for="(_, name) in $scopedSlots" :slot="name" slot-scope="slotData"> - <slot :name="name" v-bind="slotData"/> + <template + v-for="(_, name) in $scopedSlots" + :slot="name" + slot-scope="slotData" + > + <slot :name="name" v-bind="slotData" /> </template> </dialog-object-form> </slot> - <closeable-snackbar - :color="snackbarState" - v-model="snackbar" - > + <closeable-snackbar :color="snackbarState" v-model="snackbar"> {{ snackbarText }} </closeable-snackbar> </template> @@ -70,7 +70,7 @@ import DialogObjectForm from "../dialogs/DialogObjectForm.vue"; export default { name: "ForeignKeyField", - components: {CloseableSnackbar, DialogObjectForm}, + components: { CloseableSnackbar, DialogObjectForm }, extends: "v-autocomplete", data() { return { @@ -78,7 +78,7 @@ export default { snackbar: false, snackbarState: "error", snackbarText: "", - } + }; }, apollo: { items() { @@ -91,7 +91,7 @@ export default { methods: { handleUpdate(store, createdObject) { // Read the data from cache for query - const storedData = store.readQuery({query: this.gqlQuery}); + const storedData = store.readQuery({ query: this.gqlQuery }); if (!storedData) { // There are no data in the cache yet @@ -104,7 +104,7 @@ export default { storedData[storedDataKey].push(createdObject); // Write data back to the cache - store.writeQuery({query: this.gqlQuery, data: storedData}); + store.writeQuery({ query: this.gqlQuery, data: storedData }); }, handleSave(data) { let newItem = @@ -172,7 +172,7 @@ export default { default: "actions.create", }, }, -} +}; </script> <style scoped> diff --git a/aleksis/core/frontend/components/generic/forms/PositiveSmallIntegerField.vue b/aleksis/core/frontend/components/generic/forms/PositiveSmallIntegerField.vue index e225f2abc6e21db9f78569ea9a701564768be46a..5deaab572366481ee22ff147028bfb32a79e00eb 100644 --- a/aleksis/core/frontend/components/generic/forms/PositiveSmallIntegerField.vue +++ b/aleksis/core/frontend/components/generic/forms/PositiveSmallIntegerField.vue @@ -1,10 +1,10 @@ <template> <v-text-field - v-bind="$attrs" - v-on="on" - :rules="rules" - type="number" - inputmode="decimal" + v-bind="$attrs" + v-on="on" + :rules="rules" + type="number" + inputmode="decimal" ></v-text-field> </template> @@ -18,29 +18,39 @@ export default { if (!isNaN(num) && num >= 0 && num <= 32767 && num % 1 === 0) { this.$emit("input", parseInt(event)); } - } + }, }, data() { return { rules: [ - value => !value || !isNaN(parseInt(value)) || this.$t("forms.errors.not_a_number"), - value => !value || value % 1 === 0 || this.$t("forms.errors.not_a_whole_number"), - value => !value || parseInt(value) >= 0 || this.$t("forms.errors.number_too_small"), - value => !value || parseInt(value) <= 32767 || this.$t("forms.errors.number_too_big"), - ] - } + (value) => + !value || + !isNaN(parseInt(value)) || + this.$t("forms.errors.not_a_number"), + (value) => + !value || + value % 1 === 0 || + this.$t("forms.errors.not_a_whole_number"), + (value) => + !value || + parseInt(value) >= 0 || + this.$t("forms.errors.number_too_small"), + (value) => + !value || + parseInt(value) <= 32767 || + this.$t("forms.errors.number_too_big"), + ], + }; }, computed: { on() { return { ...this.$listeners, input: this.handleInput, - } - } + }; + }, }, -} +}; </script> -<style scoped> - -</style> +<style scoped></style> diff --git a/aleksis/core/frontend/components/generic/forms/WeekDayField.vue b/aleksis/core/frontend/components/generic/forms/WeekDayField.vue index 3103faa2e4019266dc4882f9c1642bacedc46479..4e8c359343f799c43974ea923a27c8648bfa314c 100644 --- a/aleksis/core/frontend/components/generic/forms/WeekDayField.vue +++ b/aleksis/core/frontend/components/generic/forms/WeekDayField.vue @@ -1,14 +1,13 @@ <template> <v-autocomplete - v-bind="$attrs" - v-on="$listeners" - :items="items" - :item-value="valueKey" + v-bind="$attrs" + v-on="$listeners" + :items="items" + :item-value="valueKey" ></v-autocomplete> </template> <script> - export default { name: "WeekDayField", extends: "v-autocomplete", @@ -50,20 +49,20 @@ export default { valueInt: 6, text: this.$t("weekdays.A_6"), }, - ] - } + ], + }; }, props: { returnInt: { type: Boolean, default: false, required: false, - } + }, }, computed: { valueKey() { return this.returnInt ? "valueInt" : "value"; - } + }, }, -} +}; </script> diff --git a/aleksis/core/frontend/components/notifications/NotificationList.vue b/aleksis/core/frontend/components/notifications/NotificationList.vue index 1c51c00661816fe3e244e6d511a0e70c747dbca6..09b27197e5d62c5a5f15f7836d5b741925d21cc8 100644 --- a/aleksis/core/frontend/components/notifications/NotificationList.vue +++ b/aleksis/core/frontend/components/notifications/NotificationList.vue @@ -20,7 +20,7 @@ v-if=" myNotifications && myNotifications.person && - myNotifications.person.unreadNotificationsCount > 0 + myNotifications.person.notifications.length > 0 " > mdi-bell-badge-outline @@ -38,7 +38,7 @@ v-if=" myNotifications.person && myNotifications.person.notifications && - myNotifications.person.notifications.length + unreadNotifications.length " > <v-subheader>{{ $t("notifications.notifications") }}</v-subheader> @@ -86,5 +86,10 @@ export default { pollInterval: 30000, }, }, + computed: { + unreadNotifications() { + return this.myNotifications.filter((n) => !n.read); + }, + }, }; </script> diff --git a/aleksis/core/frontend/components/notifications/myNotifications.graphql b/aleksis/core/frontend/components/notifications/myNotifications.graphql index b8287ea2f50664f556d82bdb58e4b508c7ece1d4..89e91562086607e6c9e7649fa1295d234d189bd3 100644 --- a/aleksis/core/frontend/components/notifications/myNotifications.graphql +++ b/aleksis/core/frontend/components/notifications/myNotifications.graphql @@ -1,7 +1,6 @@ { myNotifications: whoAmI { person { - unreadNotificationsCount notifications { id title diff --git a/aleksis/core/frontend/components/room/RoomInlineList.vue b/aleksis/core/frontend/components/room/RoomInlineList.vue index 781414151b497be592b4a45c6ec6efecce338123..9221e83da03b65c43564f8584df226cbc9f27be0 100644 --- a/aleksis/core/frontend/components/room/RoomInlineList.vue +++ b/aleksis/core/frontend/components/room/RoomInlineList.vue @@ -28,7 +28,13 @@ import InlineCRUDList from "../generic/InlineCRUDList.vue"; </template> <script> -import { rooms, createRoom, deleteRoom, deleteRooms, updateRooms } from "./room.graphql"; +import { + rooms, + createRoom, + deleteRoom, + deleteRooms, + updateRooms, +} from "./room.graphql"; export default { name: "RoomInlineList", diff --git a/aleksis/core/frontend/components/room/room.graphql b/aleksis/core/frontend/components/room/room.graphql index d30034e90aff4620fef8a3bef58fe1ed0e7db938..d8cd9138dd0fa5c4f3af7a7d47fe7b44ed94f6fd 100644 --- a/aleksis/core/frontend/components/room/room.graphql +++ b/aleksis/core/frontend/components/room/room.graphql @@ -27,9 +27,9 @@ mutation deleteRoom($id: ID!) { } mutation deleteRooms($ids: [ID]!) { - deleteRooms(ids: $ids) { - deletionCount - } + deleteRooms(ids: $ids) { + deletionCount + } } mutation updateRooms($input: [BatchPatchRoomInput]!) { diff --git a/aleksis/core/frontend/components/school_term/SchoolTermField.vue b/aleksis/core/frontend/components/school_term/SchoolTermField.vue index 337bcfc0c1481eb9e5e727352c1902691d33fe71..d8b4c1b0491e0444ea5dad3edb2b88756e6954ea 100644 --- a/aleksis/core/frontend/components/school_term/SchoolTermField.vue +++ b/aleksis/core/frontend/components/school_term/SchoolTermField.vue @@ -4,14 +4,14 @@ import DateField from "../generic/forms/DateField.vue"; <template> <foreign-key-field - :gql-patch-mutation="{}" - :gql-create-mutation="gqlCreateMutation" - :gql-query="gqlQuery" - :fields="fields" - create-item-i18n-key="school_term.create_school_term" - :default-item="defaultItem" - v-bind="$attrs" - v-on="$listeners" + :gql-patch-mutation="{}" + :gql-create-mutation="gqlCreateMutation" + :gql-query="gqlQuery" + :fields="fields" + create-item-i18n-key="school_term.create_school_term" + :default-item="defaultItem" + v-bind="$attrs" + v-on="$listeners" > <template #name.field="{ attrs, on }"> <div aria-required="true"> @@ -58,12 +58,11 @@ import DateField from "../generic/forms/DateField.vue"; <script> import ForeignKeyField from "../generic/forms/ForeignKeyField.vue"; -import {createSchoolTerm, schoolTerms,} from "./schoolTerm.graphql"; - +import { createSchoolTerm, schoolTerms } from "./schoolTerm.graphql"; export default { name: "SchoolTermField", - components: {ForeignKeyField}, + components: { ForeignKeyField }, data() { return { gqlQuery: schoolTerms, @@ -88,11 +87,9 @@ export default { dateEnd: "", }, required: [(value) => !!value || this.$t("forms.errors.required")], - } + }; }, -} +}; </script> -<style scoped> - -</style> +<style scoped></style> diff --git a/aleksis/core/frontend/components/school_term/SchoolTermInlineList.vue b/aleksis/core/frontend/components/school_term/SchoolTermInlineList.vue index 1195ec8fe358876288e8083d2785d35e5a89a55a..c109f88e0c5213de7c4a86e31701f4b1276db74d 100644 --- a/aleksis/core/frontend/components/school_term/SchoolTermInlineList.vue +++ b/aleksis/core/frontend/components/school_term/SchoolTermInlineList.vue @@ -18,11 +18,7 @@ import DateField from "../generic/forms/DateField.vue"; > <template #name.field="{ attrs, on, item }"> <div aria-required="true"> - <v-text-field - v-bind="attrs" - v-on="on" - :rules="required" - ></v-text-field> + <v-text-field v-bind="attrs" v-on="on" :rules="required"></v-text-field> </div> </template> diff --git a/aleksis/core/frontend/plugins/aleksis.js b/aleksis/core/frontend/plugins/aleksis.js index 2304404e7eb80c4150f07eeb38f888aaee88a60f..7354df14ecaeb5b640dca791dbdaaf3e3e460214 100644 --- a/aleksis/core/frontend/plugins/aleksis.js +++ b/aleksis/core/frontend/plugins/aleksis.js @@ -147,10 +147,11 @@ AleksisVue.install = function (Vue) { * Load vuetifys built-in translations */ Vue.prototype.$loadVuetifyMessages = function () { - for(const [locale, messages] of Object.entries(langs)) { - this.$i18n.mergeLocaleMessage(locale, {$vuetify: messages}) + + for (const [locale, messages] of Object.entries(langs)) { + this.$i18n.mergeLocaleMessage(locale, { $vuetify: messages }); } - } + }; /** * Invalidate state and force reload from server. diff --git a/aleksis/core/migrations/0049_oauthapplication_post_logout_redirect_uris.py b/aleksis/core/migrations/0049_oauthapplication_post_logout_redirect_uris.py new file mode 100644 index 0000000000000000000000000000000000000000..a14526b2f0c32077e873c9b3bcd0cb21ee9e0cd6 --- /dev/null +++ b/aleksis/core/migrations/0049_oauthapplication_post_logout_redirect_uris.py @@ -0,0 +1,21 @@ +# Generated by Django 4.1.9 on 2023-06-17 10:59 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("sites", "0002_alter_domain_unique"), + ("core", "0048_delete_personalicalurl"), + ] + + operations = [ + migrations.AddField( + model_name="oauthapplication", + name="post_logout_redirect_uris", + field=models.TextField( + blank=True, help_text="Allowed Post Logout URIs list, space separated" + ), + ), + ] diff --git a/aleksis/core/schema/__init__.py b/aleksis/core/schema/__init__.py index c5d5ddd502a2e8ebeb39c48288b94b05989c8d09..b59430c774aeff5f2513a1edaa41e56c962ffb9a 100644 --- a/aleksis/core/schema/__init__.py +++ b/aleksis/core/schema/__init__.py @@ -9,7 +9,6 @@ from haystack.inputs import AutoQuery from haystack.query import SearchQuerySet from haystack.utils.loading import UnifiedIndex -from .base import FilterOrderList from ..models import ( CustomMenu, DynamicRoute, @@ -19,11 +18,11 @@ from ..models import ( PDFFile, Person, Room, - SchoolTerm, TaskUserAssignment, ) from ..util.apps import AppConfig from ..util.core_helpers import get_allowed_object_ids, get_app_module, get_app_packages, has_person +from .base import FilterOrderList from .celery_progress import CeleryProgressFetchedMutation, CeleryProgressType from .custom_menu import CustomMenuType from .dynamic_routes import DynamicRouteType @@ -34,12 +33,18 @@ from .notification import MarkNotificationReadMutation, NotificationType from .oauth import OAuthAccessTokenType, OAuthRevokeTokenMutation from .pdf import PDFFileType from .person import PersonMutation, PersonType -from .room import RoomBatchDeleteMutation, RoomBatchPatchMutation, RoomCreateMutation, RoomDeleteMutation, RoomType +from .room import ( + RoomBatchDeleteMutation, + RoomBatchPatchMutation, + RoomCreateMutation, + RoomDeleteMutation, + RoomType, +) from .school_term import ( + SchoolTermBatchDeleteMutation, SchoolTermBatchPatchMutation, SchoolTermCreateMutation, SchoolTermDeleteMutation, - SchoolTermBatchDeleteMutation, SchoolTermType, ) from .search import SearchResultType diff --git a/aleksis/core/schema/base.py b/aleksis/core/schema/base.py index 8084bf594c62ac19ef759ff393c1a3a75342ca1c..198217379e257e427aa1332ae7bce7734d139c18 100644 --- a/aleksis/core/schema/base.py +++ b/aleksis/core/schema/base.py @@ -3,9 +3,9 @@ import json from django.contrib.contenttypes.models import ContentType from django.core.exceptions import PermissionDenied from django.db.models import Model -from django_filters.filterset import FilterSet, filterset_factory import graphene +from django_filters.filterset import FilterSet, filterset_factory from graphene_django import DjangoListField, DjangoObjectType from ..util.core_helpers import queryset_rules_filter @@ -82,7 +82,7 @@ class PermissionBatchPatchMixin: login_required = True @classmethod - def check_permissions(cls, root, info, input): + def check_permissions(cls, root, info, input): # noqa if info.context.user.has_perms(cls._meta.permissions, root): return @@ -94,7 +94,7 @@ class PermissionBatchDeleteMixin: login_required = True @classmethod - def check_permissions(cls, root, info, input): + def check_permissions(cls, root, info, input): # noqa if info.context.user.has_perms(cls._meta.permissions, root): return @@ -106,7 +106,7 @@ class PermissionPatchMixin: login_required = True @classmethod - def check_permissions(cls, root, info, input, id, obj): + def check_permissions(cls, root, info, input, id, obj): # noqa if info.context.user.has_perms(cls._meta.permissions, root): return @@ -136,21 +136,23 @@ class DjangoFilterMixin: if not fields: # Django filter doesn't allow to filter without explicit fields - raise NotImplementedError(f"{cls.__name__}.Meta must contain filter_fields or a filterset_class") + raise NotImplementedError( + f"{cls.__name__}.Meta must contain filter_fields or a filterset_class" + ) fs = filterset_factory(model=model, fields=fields) return fs @classmethod - def filter(cls, filters, queryset): + def filter(cls, filters, queryset): # noqa filterset_class = cls.get_filterset() filterset: FilterSet = filterset_class(filters, queryset) return filterset.qs class FilterOrderList(DjangoListField): - """Generic filterable Field for lists of django models + """Generic filterable Field for lists of django models. After the models are filtered, they can be filtered again (e.g. for permissions using the get_queryset method inside the @@ -164,9 +166,18 @@ class FilterOrderList(DjangoListField): @staticmethod def list_resolver( - django_object_type, resolver, default_manager, root, info, order_by=None, filters=None, **args + django_object_type, + resolver, + default_manager, + root, + info, + order_by=None, + filters=None, + **args, ): - qs = DjangoListField.list_resolver(django_object_type, resolver, default_manager, root, info, **args) + qs = DjangoListField.list_resolver( + django_object_type, resolver, default_manager, root, info, **args + ) if filters is not None: if isinstance(filters, str): diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index 6eed201487348034b2d9b1cba616046b1794dbf2..90165cceb0fb33676e17aad5caf978ce7c124be5 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -55,14 +55,26 @@ class PersonType(DjangoObjectType): full_name = graphene.String() username = graphene.String() userid = graphene.ID() - photo = graphene.Field(FieldFileType) - avatar = graphene.Field(FieldFileType) + photo = graphene.Field(FieldFileType, required=False) + avatar = graphene.Field(FieldFileType, required=False) avatar_url = graphene.String() avatar_content_url = graphene.String() - secondary_image_url = graphene.String() + secondary_image_url = graphene.String(required=False) + + street = graphene.String(required=False) + housenumber = graphene.String(required=False) + postal_code = graphene.String(required=False) + place = graphene.String(required=False) + + phone_number = graphene.String(required=False) + mobile_number = graphene.String(required=False) + email = graphene.String(required=False) + + date_of_birth = graphene.String(required=False) + place_of_birth = graphene.String(required=False) notifications = graphene.List(NotificationType) - unread_notifications_count = graphene.Int() + unread_notifications_count = graphene.Int(required=False) is_dummy = graphene.Boolean() preferences = graphene.Field(PersonPreferencesType) @@ -150,7 +162,11 @@ class PersonType(DjangoObjectType): return root.user.id if root.user else None def resolve_unread_notifications_count(root, info, **kwargs): # noqa - return root.unread_notifications_count + if root.pk and has_person(info.context) and root == info.context.user.person: + return root.unread_notifications_count + elif root.pk: + return 0 + return None def resolve_photo(root, info, **kwargs): if info.context.user.has_perm("core.view_photo_rule", root): @@ -199,11 +215,11 @@ class PersonType(DjangoObjectType): return root.is_dummy if hasattr(root, "is_dummy") else False def resolve_notifications(root: Person, info, **kwargs): - if has_person(info.context.user) and info.context.user.person == root: + if root.pk and has_person(info.context) and root == info.context.user.person: return root.notifications.filter(send_at__lte=timezone.now()).order_by( "read", "-created" ) - raise PermissionDenied() + return [] def resolve_can_edit_person(root, info, **kwargs): # noqa return info.context.user.has_perm("core.edit_person_rule", root) diff --git a/aleksis/core/schema/room.py b/aleksis/core/schema/room.py index e1e478af053a3d46b42c7032a27700bb74b8289c..575130971671cfa79743b985918b3ad1621da53f 100644 --- a/aleksis/core/schema/room.py +++ b/aleksis/core/schema/room.py @@ -1,17 +1,21 @@ from graphene_django import DjangoObjectType -from graphene_django_cud.mutations import DjangoBatchDeleteMutation, DjangoBatchPatchMutation, DjangoCreateMutation +from graphene_django_cud.mutations import ( + DjangoBatchDeleteMutation, + DjangoBatchPatchMutation, + DjangoCreateMutation, +) from ..models import Room from .base import ( DeleteMutation, DjangoFilterMixin, + PermissionBatchDeleteMixin, PermissionBatchPatchMixin, PermissionsTypeMixin, - PermissionBatchDeleteMixin, ) -class RoomType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType): +class RoomType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType): class Meta: model = Room fields = ("id", "name", "short_name") diff --git a/aleksis/core/schema/school_term.py b/aleksis/core/schema/school_term.py index 092ae0e4b5e567210325ee5eca21f0e1b376d73f..798d1c62eb960d2c850a918345cbc03252a88820 100644 --- a/aleksis/core/schema/school_term.py +++ b/aleksis/core/schema/school_term.py @@ -1,8 +1,12 @@ -from django.core.exceptions import ValidationError, PermissionDenied +from django.core.exceptions import PermissionDenied, ValidationError from django.utils.translation import gettext as _ from graphene_django import DjangoObjectType -from graphene_django_cud.mutations import DjangoBatchDeleteMutation, DjangoBatchPatchMutation, DjangoCreateMutation +from graphene_django_cud.mutations import ( + DjangoBatchDeleteMutation, + DjangoBatchPatchMutation, + DjangoCreateMutation, +) from ..models import SchoolTerm from .base import ( diff --git a/docs/conf.py b/docs/conf.py index 7d5706e4ab2a87ab9431f8ea7dcab000685cd0b4..919c2ec87fab03d7e7e6c7c982d695f76d559c43 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,13 +25,13 @@ django.setup() # -- Project information ----------------------------------------------------- project = "AlekSIS-Core" -copyright = "2019-2022 The AlekSIS team" +copyright = "2019-2023 The AlekSIS team" author = "The AlekSIS Team" # The short X.Y version version = "3.1" # The full version, including alpha/beta/rc tags -release = "3.1.1.dev0" +release = "3.1.2.dev0" # -- General configuration --------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index 31c7180aa202ebb38bcf984419fd6fe98e263520..468bf0a08826da58217865f095e3aee7b202d380 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "AlekSIS-Core" -version = "3.1.1.dev0" +version = "3.1.2.dev0" packages = [ { include = "aleksis" } ] @@ -136,6 +136,7 @@ sentry = ["sentry-sdk"] [tool.poetry.dev-dependencies] aleksis-builddeps = {version=">=2023.1.dev0", allow-prereleases=true} +selenium = "<4.10.0" uwsgi = "^2.0" [tool.poetry.scripts]