diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py index 359ec8bede87d872a6de0cf8929022df3fa08b64..06d32ce9b853c830f7fea73c6439c9298fc68ab1 100644 --- a/aleksis/core/forms.py +++ b/aleksis/core/forms.py @@ -26,7 +26,6 @@ from .models import ( Announcement, DashboardWidget, Group, - GroupType, OAuthApplication, Person, PersonInvitation, @@ -355,14 +354,6 @@ class GroupPreferenceForm(PreferenceForm): registry = group_preferences_registry -class EditGroupTypeForm(forms.ModelForm): - """Form to manage group types.""" - - class Meta: - model = GroupType - fields = ["name", "description"] - - class DashboardWidgetOrderForm(ExtensibleForm): pk = forms.ModelChoiceField( queryset=None, diff --git a/aleksis/core/frontend/components/generic/CRUDIterator.vue b/aleksis/core/frontend/components/generic/CRUDIterator.vue index 7f37088030b3911f44566ce70d0b398861b1f4a2..ecb2ec8e37889caef0b5bfab679766f8e5d0ec40 100644 --- a/aleksis/core/frontend/components/generic/CRUDIterator.vue +++ b/aleksis/core/frontend/components/generic/CRUDIterator.vue @@ -1,5 +1,7 @@ <template> <v-data-iterator + v-bind="$attrs" + v-on="$listeners" :items="items" :items-per-page="itemsPerPage" :loading="loading" diff --git a/aleksis/core/frontend/components/generic/CRUDList.vue b/aleksis/core/frontend/components/generic/CRUDList.vue index 61f752d82b0c6ef8d399a72ac66ff7e3893ab6a1..3f5a30defc28a334f5f0e1754ea2e207beaf52a9 100644 --- a/aleksis/core/frontend/components/generic/CRUDList.vue +++ b/aleksis/core/frontend/components/generic/CRUDList.vue @@ -56,6 +56,9 @@ <template #additionalActions="{ attrs, on }"> <slot name="additionalActions" :attrs="attrs" :on="on" /> </template> + <template #createComponent="createComponentProps"> + <slot name="createComponent" v-bind="createComponentProps" /> + </template> </c-r-u-d-bar> </template> diff --git a/aleksis/core/frontend/components/generic/TableLink.vue b/aleksis/core/frontend/components/generic/TableLink.vue new file mode 100644 index 0000000000000000000000000000000000000000..e8e88817cf7c9c77a4e451cfade9e3dcd230e5cd --- /dev/null +++ b/aleksis/core/frontend/components/generic/TableLink.vue @@ -0,0 +1,15 @@ +<template> + <router-link + v-bind="$attrs" + v-on="$listeners" + class="text-decoration-none primary--text font-weight-medium d-inline-block full-width" + > + <slot /> + </router-link> +</template> +<script> +export default { + name: "TableLink", + extends: "router-link", +}; +</script> diff --git a/aleksis/core/frontend/components/generic/forms/ColorField.vue b/aleksis/core/frontend/components/generic/forms/ColorField.vue index 865124f9b48e863397af4c73a4eff225709df8ee..b8a5024d10562653d6ce54bc98ebb953ed8a368b 100644 --- a/aleksis/core/frontend/components/generic/forms/ColorField.vue +++ b/aleksis/core/frontend/components/generic/forms/ColorField.vue @@ -14,7 +14,7 @@ v-bind="$attrs" v-on="$listeners" placeholder="#AABBCC" - :rules="mergedRules" + :rules="$rules().isHexColor(allowAlpha).build(rules)" > <template #prepend-inner> <v-icon :color="color" v-bind="attrs" v-on="on"> mdi-circle </v-icon> @@ -26,9 +26,12 @@ </template> <script> +import formRulesMixin from "../../../mixins/formRulesMixin"; + export default { - name: "DateField", + name: "ColorField", extends: "v-text-field", + mixins: [formRulesMixin], data() { return { menu: false, @@ -44,6 +47,11 @@ export default { required: false, default: () => [], }, + allowAlpha: { + type: Boolean, + required: false, + default: true, + }, }, computed: { color: { @@ -54,14 +62,6 @@ export default { this.$emit("input", newValue); }, }, - mergedRules() { - return [ - (value) => - /^(#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8}))?$/i.test(value) || - this.$t("forms.errors.invalid_color"), - ...this.rules, - ]; - }, }, }; </script> diff --git a/aleksis/core/frontend/components/generic/forms/PositiveSmallIntegerField.vue b/aleksis/core/frontend/components/generic/forms/PositiveSmallIntegerField.vue index 0b6ba51574745f093d266483fee82ae653ee91e9..bcbe7779563062f90e6302990712c06004fa67fc 100644 --- a/aleksis/core/frontend/components/generic/forms/PositiveSmallIntegerField.vue +++ b/aleksis/core/frontend/components/generic/forms/PositiveSmallIntegerField.vue @@ -2,16 +2,24 @@ <v-text-field v-bind="$attrs" v-on="on" - :rules="mergedRules" + :rules=" + $rules() + .isANumber.isAWholeNumber.isGreaterThan(0) + .isSmallerThan(32767) + .build(rules) + " type="number" inputmode="decimal" ></v-text-field> </template> <script> +import formRulesMixin from "../../../mixins/formRulesMixin"; + export default { name: "PositiveSmallIntegerField", extends: "v-text-field", + mixins: [formRulesMixin], props: { rules: { type: Array, @@ -27,27 +35,6 @@ export default { change: this.inputHandler("change"), }; }, - mergedRules() { - return [ - (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"), - ...this.rules, - ]; - }, }, methods: { inputHandler(name) { diff --git a/aleksis/core/frontend/components/generic/forms/SexSelect.vue b/aleksis/core/frontend/components/generic/forms/SexSelect.vue new file mode 100644 index 0000000000000000000000000000000000000000..88d506ae425a0bef5786d9805416d0ce83121b2c --- /dev/null +++ b/aleksis/core/frontend/components/generic/forms/SexSelect.vue @@ -0,0 +1,27 @@ +<script> +import sexChoiceMixin from "../../../mixins/sexChoiceMixin"; + +export default { + name: "SexSelect", + extends: ["v-autocomplete"], + mixins: [sexChoiceMixin], +}; +</script> + +<template> + <v-autocomplete v-bind="$attrs" v-on="$listeners" :items="sexChoices"> + <template #selection="{ item }"> + <v-icon left>{{ item.icon }}</v-icon> + <span>{{ item.text }}</span> + </template> + + <template #item="{ item }"> + <v-list-item-avatar> + <v-icon>{{ item.icon }}</v-icon> + </v-list-item-avatar> + <v-list-item-content> + <v-list-item-title>{{ item.text }}</v-list-item-title> + </v-list-item-content> + </template> + </v-autocomplete> +</template> diff --git a/aleksis/core/frontend/components/group/GroupChip.vue b/aleksis/core/frontend/components/group/GroupChip.vue new file mode 100644 index 0000000000000000000000000000000000000000..10393e3c399e5aa3e6e5faa6a96300ea5ffc68c1 --- /dev/null +++ b/aleksis/core/frontend/components/group/GroupChip.vue @@ -0,0 +1,24 @@ +<script> +export default { + name: "GroupChip", + extends: "v-chip", + props: { + group: { + type: Object, + required: true, + }, + }, +}; +</script> + +<template> + <v-chip + v-bind="$attrs" + v-on="$listeners" + :to="{ name: 'core.group', params: { id: group.id } }" + > + {{ group.name }} + </v-chip> +</template> + +<style scoped></style> diff --git a/aleksis/core/frontend/components/group_type/GroupType.vue b/aleksis/core/frontend/components/group_type/GroupType.vue new file mode 100644 index 0000000000000000000000000000000000000000..5aed5dab4a8c118c97b1f9d37a2c30ebd2179067 --- /dev/null +++ b/aleksis/core/frontend/components/group_type/GroupType.vue @@ -0,0 +1,90 @@ +<script> +import CRUDList from "../generic/CRUDList.vue"; + +import { + groupTypes, + createGroupTypes, + deleteGroupTypes, + updateGroupTypes, +} from "./groupType.graphql"; +import formRulesMixin from "../../mixins/formRulesMixin"; + +export default { + name: "GroupType", + components: { CRUDList }, + mixins: [formRulesMixin], + data() { + return { + headers: [ + { + text: this.$t("group.group_type.name"), + value: "name", + }, + { + text: this.$t("group.group_type.description"), + value: "description", + }, + ], + i18nKey: "group.group_type", + gqlQuery: groupTypes, + gqlCreateMutation: createGroupTypes, + gqlPatchMutation: updateGroupTypes, + gqlDeleteMutation: deleteGroupTypes, + defaultItem: { + name: "", + description: "", + }, + }; + }, + methods: { + getData({ id, name, description }) { + return { + id, + name, + description, + }; + }, + }, +}; +</script> + +<template> + <c-r-u-d-list + :headers="headers" + :i18n-key="i18nKey" + create-item-i18n-key="group.group_type.create" + :gql-query="gqlQuery" + :gql-create-mutation="gqlCreateMutation" + :gql-patch-mutation="gqlPatchMutation" + :gql-delete-mutation="gqlDeleteMutation" + :get-create-data="getData" + :get-patch-data="getData" + :default-item="defaultItem" + :enable-edit="true" + > + <!-- eslint-disable-next-line vue/valid-v-slot --> + <template #name.field="{ attrs, on, isCreate }"> + <div aria-required="true"> + <v-text-field + v-bind="attrs" + v-on="on" + required + :rules="$rules().required.build()" + ></v-text-field> + </div> + </template> + <!-- eslint-disable-next-line vue/valid-v-slot --> + <template #description.field="{ attrs, on, isCreate }"> + <div aria-required="true"> + <v-text-field + v-bind="attrs" + v-on="on" + required + :rules="$rules().required.build()" + ></v-text-field> + </div> + </template> + </c-r-u-d-list> +</template> + +<style scoped></style> diff --git a/aleksis/core/frontend/components/group_type/groupType.graphql b/aleksis/core/frontend/components/group_type/groupType.graphql new file mode 100644 index 0000000000000000000000000000000000000000..11ab2741287f251f819e4e7de41ea2ceafee69f0 --- /dev/null +++ b/aleksis/core/frontend/components/group_type/groupType.graphql @@ -0,0 +1,39 @@ +query groupTypes($orderBy: [String], $filters: JSONString) { + items: groupTypes(orderBy: $orderBy, filters: $filters) { + id + name + description + canEdit + canDelete + } +} + +mutation createGroupTypes($input: [BatchCreateGroupTypeInput]!) { + createGroupTypes(input: $input) { + items: groupTypes { + id + name + description + canEdit + canDelete + } + } +} + +mutation deleteGroupTypes($ids: [ID]!) { + deleteGroupTypes(ids: $ids) { + deletionCount + } +} + +mutation updateGroupTypes($input: [BatchPatchGroupTypeInput]!) { + updateGroupTypes(input: $input) { + items: groupTypes { + id + name + description + canEdit + canDelete + } + } +} diff --git a/aleksis/core/frontend/components/person/AvatarContent.vue b/aleksis/core/frontend/components/person/AvatarContent.vue index 7b0c0f670204872dea9309e1829124d101e2190a..f976448dffaa283987b5d0ddfc4765513bd8cc8d 100644 --- a/aleksis/core/frontend/components/person/AvatarContent.vue +++ b/aleksis/core/frontend/components/person/AvatarContent.vue @@ -38,7 +38,7 @@ export default { }, imageUrl: { type: String, - required: true, + required: false, default: null, }, }, diff --git a/aleksis/core/frontend/components/person/PersonActions.vue b/aleksis/core/frontend/components/person/PersonActions.vue index 8b040f27fa9d7ab794b87270f86661929c1ed3c7..9a90ee765ff63eb058c346962af79b32f7d6787d 100644 --- a/aleksis/core/frontend/components/person/PersonActions.vue +++ b/aleksis/core/frontend/components/person/PersonActions.vue @@ -82,30 +82,31 @@ </v-list-item> </button-menu> </template> - <confirm-dialog + <delete-dialog v-model="showDeleteConfirm" + :gql-delete-mutation="deleteMutation" + item-attribute="fullName" + :items="[person]" @confirm=" $router.push({ - name: 'core.deletePerson', - params: { id: person.id }, + name: 'core.persons', }) " - @cancel="showDeleteConfirm = false" > <template #title> {{ $t("person.confirm_delete") }} </template> - </confirm-dialog> + </delete-dialog> </div> </template> <script> -import gqlPersonActions from "./personActions.graphql"; -import ConfirmDialog from "../generic/dialogs/ConfirmDialog.vue"; +import { actions, deletePersons } from "./personActions.graphql"; +import DeleteDialog from "../generic/dialogs/DeleteDialog.vue"; export default { name: "PersonActions", - components: { ConfirmDialog }, + components: { DeleteDialog }, props: { id: { type: String, @@ -114,7 +115,7 @@ export default { }, apollo: { person: { - query: gqlPersonActions, + query: actions, variables() { return { id: this.id, @@ -125,6 +126,7 @@ export default { data() { return { showDeleteConfirm: false, + deleteMutation: deletePersons, }; }, }; diff --git a/aleksis/core/frontend/components/person/PersonList.vue b/aleksis/core/frontend/components/person/PersonList.vue new file mode 100644 index 0000000000000000000000000000000000000000..03e15f8466b51f1fa7b66eddeeddf96e787f3ecd --- /dev/null +++ b/aleksis/core/frontend/components/person/PersonList.vue @@ -0,0 +1,95 @@ +<script> +import CRUDList from "../generic/CRUDList.vue"; + +import { deletePersons, persons } from "./personList.graphql"; +import CreateButton from "../generic/buttons/CreateButton.vue"; +import SexSelect from "../generic/forms/SexSelect.vue"; +import GroupChip from "../group/GroupChip.vue"; +import TableLink from "../generic/TableLink.vue"; + +export default { + name: "Person", + components: { TableLink, GroupChip, SexSelect, CreateButton, CRUDList }, + data() { + return { + headers: [ + { + text: this.$t("person.first_name"), + value: "firstName", + }, + { + text: this.$t("person.last_name"), + value: "lastName", + }, + { + text: this.$t("person.short_name"), + value: "shortName", + }, + { + text: this.$t("person.primary_group"), + value: "primaryGroup", + }, + ], + i18nKey: "person", + gqlQuery: persons, + gqlDeleteMutation: deletePersons, + }; + }, +}; +</script> + +<template> + <c-r-u-d-list + :headers="headers" + :i18n-key="i18nKey" + :gql-query="gqlQuery" + :gql-delete-mutation="gqlDeleteMutation" + :enable-filter="true" + item-attribute="fullName" + > + <template #createComponent> + <create-button :to="{ name: 'core.createPerson' }" /> + </template> + + <template #filters="{ attrs, on }"> + <v-text-field + v-bind="attrs('name')" + v-on="on('name')" + :label="$t('person.name')" + /> + <v-text-field + v-bind="attrs('contact')" + v-on="on('contact')" + :label="$t('person.details')" + /> + <sex-select + v-bind="attrs('sex')" + v-on="on('sex')" + :label="$t('person.sex.field')" + /> + </template> + + <template #lastName="{ item }"> + <table-link :to="{ name: 'core.personById', params: { id: item.id } }"> + {{ item.lastName }} + </table-link> + </template> + + <template #firstName="{ item }"> + <table-link :to="{ name: 'core.personById', params: { id: item.id } }"> + {{ item.firstName }} + </table-link> + </template> + + <template #shortName="{ item }"> + <table-link :to="{ name: 'core.personById', params: { id: item.id } }"> + {{ item.shortName }} + </table-link> + </template> + + <template #primaryGroup="{ item }"> + <group-chip :group="item.primaryGroup" v-if="item.primaryGroup" /> + <span v-else>–</span> + </template> + </c-r-u-d-list> +</template> diff --git a/aleksis/core/frontend/components/person/personActions.graphql b/aleksis/core/frontend/components/person/personActions.graphql index 8bc316c3b567b0bf7673caba848c6b7186f85a74..508700fd1bdbafb7972bf6077aafbb111fe98424 100644 --- a/aleksis/core/frontend/components/person/personActions.graphql +++ b/aleksis/core/frontend/components/person/personActions.graphql @@ -2,6 +2,7 @@ query actions($id: ID!) { person: personById(id: $id) { id userid + fullName canEditPerson canDeletePerson canChangePersonPreferences @@ -9,3 +10,9 @@ query actions($id: ID!) { canImpersonatePerson } } + +mutation deletePersons($ids: [ID]!) { + deletePersons(ids: $ids) { + deletionCount + } +} diff --git a/aleksis/core/frontend/components/person/personList.graphql b/aleksis/core/frontend/components/person/personList.graphql new file mode 100644 index 0000000000000000000000000000000000000000..33ee027ddaf8564b61ff606960fcdf93c6017b8a --- /dev/null +++ b/aleksis/core/frontend/components/person/personList.graphql @@ -0,0 +1,21 @@ +query persons($orderBy: [String], $filters: JSONString) { + items: persons(orderBy: $orderBy, filters: $filters) { + id + firstName + lastName + shortName + fullName + primaryGroup { + id + name + } + canEdit + canDelete + } +} + +mutation deletePersons($ids: [ID]!) { + deletePersons(ids: $ids) { + deletionCount + } +} diff --git a/aleksis/core/frontend/messages/de.json b/aleksis/core/frontend/messages/de.json index eb867bc52e53f5fe17d54b3f9c492b8b7de5c872..c16ad41356a8742fa446ef2677ae3fada148f085 100644 --- a/aleksis/core/frontend/messages/de.json +++ b/aleksis/core/frontend/messages/de.json @@ -199,7 +199,10 @@ "group_type": { "menu_title": "Gruppentypen", "title": "Gruppentyp", - "title_plural": "Gruppentypen" + "title_plural": "Gruppentypen", + "create": "Gruppentyp erstellen", + "name": "Name", + "description": "Beschreibung" }, "groups_and_child_groups": "Gruppen und Kindgruppen", "menu_title": "Gruppen", @@ -290,7 +293,12 @@ "page_title": "Person", "title": "Person", "title_plural": "Personen", + "first_name": "Vorname", + "last_name": "Nachname", + "short_name": "Kürzel", + "name": "Name", "sex": { + "field": "Geschlecht", "m": "Männlich", "f": "Weiblich", "x": "Divers" diff --git a/aleksis/core/frontend/messages/en.json b/aleksis/core/frontend/messages/en.json index 39a810e6b0cf92aa0257453c1bf5273846112f9c..765fa5ca74e7111326a7137bd9d69d4c23acdcb5 100644 --- a/aleksis/core/frontend/messages/en.json +++ b/aleksis/core/frontend/messages/en.json @@ -165,7 +165,10 @@ "group_type": { "menu_title": "Group Types", "title": "Group Type", - "title_plural": "Group Types" + "title_plural": "Group Types", + "create": "Create Group Type", + "name": "Name", + "description": "Description" }, "groups_and_child_groups": "Groups and Child Groups", "menu_title": "Groups", @@ -256,13 +259,18 @@ "page_title": "Person", "title": "Person", "title_plural": "Persons", + "first_name": "First Name", + "last_name": "Last Name", + "short_name": "Short Name", + "name": "Name", "sex": { + "field": "Sex", "m": "Male", "f": "Female", "x": "Other" }, - "short_name": "Shortname", - "username": "Username" + "username": "Username", + "primary_group": "Primary Group" }, "preferences": { "person": { @@ -304,19 +312,13 @@ "error": "There has been an error while saving the latest changes.", "object_create_success": "The object was created successfully.", "object_delete_success": "The object was deleted successfully.", + "object_edit_success": "The object was updated successfully.", "objects_delete_success": "The objects were deleted successfully." }, "generic_messages": { "error": "An error occurred. Please try again.", "success": "The operation has been finished successfully." }, - "rooms": { - "menu_title": "Rooms", - "title_plural": "Rooms", - "name": "Name", - "short_name": "Short Name", - "create_room": "Create new room" - }, "forms": { "errors": { "required": "This field is required.", @@ -365,18 +367,6 @@ "A_5": "Sa", "A_6": "Su" }, - "selection": { - "num_items_selected": "No items selected | 1 item selected | {n} items selected" - }, - "holidays": { - "menu_title": "Holidays", - "title": "Holiday", - "title_plural": "Holidays", - "create_holiday": "Create Holiday", - "date_start": "Start Date", - "date_end": "End Date", - "holiday_name": "Name" - }, "personal_events": { "title": "Title", "description": "Description", diff --git a/aleksis/core/frontend/mixins/formRulesMixin.js b/aleksis/core/frontend/mixins/formRulesMixin.js new file mode 100644 index 0000000000000000000000000000000000000000..55aaa7225283e00d712595b1bbd41e2972118ac0 --- /dev/null +++ b/aleksis/core/frontend/mixins/formRulesMixin.js @@ -0,0 +1,91 @@ +/** + * @typedef {function(any): (boolean|string)} Rule + */ + +/** + * Mixin that provides generic default rules to avoid repetition + */ +export default { + methods: { + /** + * Interactive rule builder. + * + * Keep in mind that the order of adding rules matters. + * + * @example + * $rules.required.build(); + * $rules.build(additionalRules); + * $rules.isANumber.isSmallerThan(50).build(additionalRules); + * $rules.isANumber.isAWholeNumber.isGreaterThan(0).build(); + */ + $rules() { + const mixin = this; + return { + _rules: [], + /** + * Finish rule creating + * + * @param {Rule[]} additional Optional list of addtional rules to add + * @returns {Rule[]} the built array of rules + */ + build(additional = []) { + return [...this._rules, ...additional]; + }, + + get required() { + this._rules.push( + (value) => !!value || mixin.$t("forms.errors.required"), + ); + return this; + }, + + get isANumber() { + this._rules.push( + (value) => + !value || + !isNaN(parseFloat(value)) || + mixin.$t("forms.errors.not_a_number"), + ); + return this; + }, + get isAWholeNumber() { + this._rules.push( + (value) => + !value || + value % 1 === 0 || + mixin.$t("forms.errors.not_a_whole_number"), + ); + return this; + }, + isGreaterThan(min = 0) { + this._rules.push( + (value) => + !value || + parseInt(value) >= min || + mixin.$t("forms.errors.number_too_small"), + ); + return this; + }, + isSmallerThan(max = 0) { + this._rules.push( + (value) => + !value || + parseInt(value) <= max || + mixin.$t("forms.errors.number_too_big"), + ); + return this; + }, + isHexColor(allowAlpha = true) { + const regex = allowAlpha + ? /^(#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8}))?$/i + : /^(#([0-9a-f]{3}[0-9a-f]{3}?))?$/i; + this._rules.push( + (value) => + regex.test(value) || mixin.$t("forms.errors.invalid_color"), + ); + return this; + }, + }; + }, + }, +}; diff --git a/aleksis/core/frontend/mixins/sexChoiceMixin.js b/aleksis/core/frontend/mixins/sexChoiceMixin.js new file mode 100644 index 0000000000000000000000000000000000000000..e828bc6b8525c62a3225ca4bb273152c5005f09c --- /dev/null +++ b/aleksis/core/frontend/mixins/sexChoiceMixin.js @@ -0,0 +1,26 @@ +/** + * Mixin to supply choices for the supported sexes in AlekSIS + */ +export default { + computed: { + sexChoices() { + return [ + { + text: this.$t("person.sex.m"), + value: "m", + icon: "mdi-gender-male", + }, + { + text: this.$t("person.sex.f"), + value: "f", + icon: "mdi-gender-female", + }, + { + text: this.$t("person.sex.x"), + value: "x", + icon: "mdi-gender-non-binary", + }, + ]; + }, + }, +}; diff --git a/aleksis/core/frontend/routes.js b/aleksis/core/frontend/routes.js index 45d9d23202d3eed9323698c5faa2921b92294779..56114530c5cb16c6154764568c1d5d44543a75ee 100644 --- a/aleksis/core/frontend/routes.js +++ b/aleksis/core/frontend/routes.js @@ -89,10 +89,7 @@ const routes = [ children: [ { path: "/persons", - component: () => import("./components/LegacyBaseTemplate.vue"), - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, + component: () => import("./components/person/PersonList.vue"), name: "core.persons", meta: { inMenu: true, @@ -127,14 +124,6 @@ const routes = [ }, name: "core.editPerson", }, - { - path: "/persons/:id(\\d+)/delete/", - component: () => import("./components/LegacyBaseTemplate.vue"), - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, - name: "core.deletePerson", - }, { path: "/persons/:id(\\d+)/invite/", component: () => import("./components/LegacyBaseTemplate.vue"), @@ -192,10 +181,7 @@ const routes = [ }, { path: "/groups/group_types", - component: () => import("./components/LegacyBaseTemplate.vue"), - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, + component: () => import("./components/group_type/GroupType.vue"), name: "core.groupTypes", meta: { inMenu: true, @@ -205,31 +191,6 @@ const routes = [ permission: "core.view_grouptypes_rule", }, }, - { - path: "/groups/group_types/create", - component: () => import("./components/LegacyBaseTemplate.vue"), - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, - name: "core.createGroupType", - }, - { - path: "/groups/group_types/:id(\\d+)/delete", - component: () => import("./components/LegacyBaseTemplate.vue"), - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, - name: "core.deleteGroupType,", - }, - { - path: "/groups/group_types/:id(\\d+)/edit", - component: () => import("./components/LegacyBaseTemplate.vue"), - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, - name: "core.editGroupType", - }, - { path: "/groups/child_groups/", component: () => import("./components/LegacyBaseTemplate.vue"), diff --git a/aleksis/core/schema/__init__.py b/aleksis/core/schema/__init__.py index 63ad68ff12fa338ac0abbbc7714dec614ef89d0c..f6df0bd4fc8ea055d96ce7eab08bda84a3a03f38 100644 --- a/aleksis/core/schema/__init__.py +++ b/aleksis/core/schema/__init__.py @@ -28,6 +28,12 @@ from .celery_progress import CeleryProgressFetchedMutation, CeleryProgressType from .custom_menu import CustomMenuType from .dynamic_routes import DynamicRouteType from .group import GroupType +from .group_type import ( + GroupTypeBatchCreateMutation, + GroupTypeBatchDeleteMutation, + GroupTypeBatchPatchMutation, + GroupTypeType, +) from .holiday import ( HolidayBatchCreateMutation, HolidayBatchDeleteMutation, @@ -39,7 +45,7 @@ from .message import MessageType from .notification import MarkNotificationReadMutation, NotificationType from .oauth import OAuthAccessTokenType, OAuthBatchRevokeTokenMutation from .pdf import PDFFileType -from .person import PersonMutation, PersonType +from .person import PersonBatchDeleteMutation, PersonMutation, PersonType from .personal_event import ( PersonalEventBatchCreateMutation, PersonalEventBatchDeleteMutation, @@ -68,7 +74,7 @@ class Query(graphene.ObjectType): notifications = graphene.List(NotificationType) - persons = graphene.List(PersonType) + persons = FilterOrderList(PersonType) person_by_id = graphene.Field(PersonType, id=graphene.ID()) person_by_id_or_me = graphene.Field(PersonType, id=graphene.ID()) @@ -108,6 +114,8 @@ class Query(graphene.ObjectType): holidays = FilterOrderList(HolidayType) calendar = graphene.Field(CalendarBaseType) + group_types = FilterOrderList(GroupTypeType) + def resolve_ping(root, info, payload) -> str: return payload @@ -256,6 +264,7 @@ class Query(graphene.ObjectType): class Mutation(graphene.ObjectType): update_person = PersonMutation.Field() + delete_persons = PersonBatchDeleteMutation.Field() mark_notification_read = MarkNotificationReadMutation.Field() @@ -281,6 +290,10 @@ class Mutation(graphene.ObjectType): set_calendar_status = SetCalendarStatusMutation.Field() + create_group_types = GroupTypeBatchCreateMutation.Field() + delete_group_types = GroupTypeBatchDeleteMutation.Field() + update_group_types = GroupTypeBatchPatchMutation.Field() + def build_global_schema(): """Build global GraphQL schema from all apps.""" diff --git a/aleksis/core/schema/base.py b/aleksis/core/schema/base.py index a5ade224d4855752f2f55bd338176f3c1b71c375..346ae1c2eaae1f9b5dee34cc5fd0b89f48c74516 100644 --- a/aleksis/core/schema/base.py +++ b/aleksis/core/schema/base.py @@ -5,8 +5,13 @@ from django.core.exceptions import PermissionDenied from django.db.models import Model import graphene +import reversion from django_filters.filterset import FilterSet, filterset_factory from graphene_django import DjangoListField, DjangoObjectType +from graphene_django_cud.mutations.batch_create import DjangoBatchCreateMutation +from graphene_django_cud.mutations.batch_delete import DjangoBatchDeleteMutation +from graphene_django_cud.mutations.batch_patch import DjangoBatchPatchMutation +from reversion import set_comment, set_user from ..util.core_helpers import queryset_rules_filter @@ -94,6 +99,8 @@ class OptimisticResponseTypeMixin: class PermissionBatchPatchMixin: + """Mixin for permission checking during batch patch mutations.""" + class Meta: login_required = True @@ -106,6 +113,8 @@ class PermissionBatchPatchMixin: class PermissionBatchDeleteMixin: + """Mixin for permission checking during batch delete mutations.""" + class Meta: login_required = True @@ -118,6 +127,8 @@ class PermissionBatchDeleteMixin: class PermissionPatchMixin: + """Mixin for permission checking during patch mutations.""" + class Meta: login_required = True @@ -212,6 +223,34 @@ class FilterOrderList(DjangoListField): qs = qs.order_by(*order_by) - print(f"{filters=}") - return qs + + +class MutateWithRevisionMixin: + """Mixin for creating revision for mutation.""" + + @classmethod + def mutate(cls, root, info, *args, **kwargs): + with reversion.create_revision(): + set_user(info.context.user) + set_comment(cls.__name__) + super().mutate(root, info, *args, **kwargs) + + +class BaseBatchCreateMutation(MutateWithRevisionMixin, DjangoBatchCreateMutation): + class Meta: + abstract = True + + +class BaseBatchPatchMutation( + MutateWithRevisionMixin, PermissionBatchPatchMixin, DjangoBatchPatchMutation +): + class Meta: + abstract = True + + +class BaseBatchDeleteMutation( + MutateWithRevisionMixin, PermissionBatchDeleteMixin, DjangoBatchDeleteMutation +): + class Meta: + abstract = True diff --git a/aleksis/core/schema/group_type.py b/aleksis/core/schema/group_type.py new file mode 100644 index 0000000000000000000000000000000000000000..4d5e00cff5d187bd6fd63ce5e80244b597ec0a55 --- /dev/null +++ b/aleksis/core/schema/group_type.py @@ -0,0 +1,52 @@ +from graphene_django import DjangoObjectType +from guardian.shortcuts import get_objects_for_user + +from ..models import GroupType +from .base import ( + BaseBatchCreateMutation, + BaseBatchDeleteMutation, + BaseBatchPatchMutation, + DjangoFilterMixin, + PermissionsTypeMixin, +) + + +class GroupTypeType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType): + class Meta: + model = GroupType + fields = [ + "id", + "name", + "description", + ] + + @classmethod + def get_queryset(cls, queryset, info): + return get_objects_for_user(info.context.user, "core.view_grouptype", GroupType) + + +class GroupTypeBatchCreateMutation(BaseBatchCreateMutation): + class Meta: + model = GroupType + permissions = ("core.create_grouptype_rule",) + only_fields = ( + "name", + "description", + ) + + +class GroupTypeBatchDeleteMutation(BaseBatchDeleteMutation): + class Meta: + model = GroupType + permissions = ("core.delete_grouptype_rule",) + + +class GroupTypeBatchPatchMutation(BaseBatchPatchMutation): + class Meta: + model = GroupType + permissions = ("core.change_grouptype_rule",) + only_fields = ( + "id", + "name", + "description", + ) diff --git a/aleksis/core/schema/holiday.py b/aleksis/core/schema/holiday.py index b596f6feae003c26696d42725605299b0d7d1a1a..4de4a8c11fedf0b7612340fa28f6eb76637b164a 100644 --- a/aleksis/core/schema/holiday.py +++ b/aleksis/core/schema/holiday.py @@ -1,16 +1,12 @@ from graphene_django import DjangoObjectType -from graphene_django_cud.mutations import ( - DjangoBatchCreateMutation, - DjangoBatchDeleteMutation, - DjangoBatchPatchMutation, -) from guardian.shortcuts import get_objects_for_user from ..models import Holiday from .base import ( + BaseBatchCreateMutation, + BaseBatchDeleteMutation, + BaseBatchPatchMutation, DjangoFilterMixin, - PermissionBatchDeleteMixin, - PermissionBatchPatchMixin, PermissionsTypeMixin, ) @@ -31,20 +27,20 @@ class HolidayType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType): return get_objects_for_user(info.context.user, "core.view_holiday", queryset) -class HolidayBatchCreateMutation(DjangoBatchCreateMutation): +class HolidayBatchCreateMutation(BaseBatchCreateMutation): class Meta: model = Holiday permissions = ("core.create_holiday_rule",) only_fields = ("holiday_name", "date_start", "date_end") -class HolidayBatchDeleteMutation(PermissionBatchDeleteMixin, DjangoBatchDeleteMutation): +class HolidayBatchDeleteMutation(BaseBatchDeleteMutation): class Meta: model = Holiday permissions = ("core.delete_holiday_rule",) -class HolidayBatchPatchMutation(PermissionBatchPatchMixin, DjangoBatchPatchMutation): +class HolidayBatchPatchMutation(BaseBatchPatchMutation): class Meta: model = Holiday permissions = ("core.edit_holiday_rule",) diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index 90165cceb0fb33676e17aad5caf978ce7c124be5..3e2af57c2ea06317135e82d00aa0c149e0ecfa60 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -8,10 +8,16 @@ from graphene_django import DjangoObjectType from graphene_django.forms.mutation import DjangoModelFormMutation from guardian.shortcuts import get_objects_for_user +from ..filters import PersonFilter from ..forms import PersonForm from ..models import DummyPerson, Person from ..util.core_helpers import get_site_preferences, has_person -from .base import FieldFileType +from .base import ( + BaseBatchDeleteMutation, + DjangoFilterMixin, + FieldFileType, + PermissionsTypeMixin, +) from .notification import NotificationType @@ -22,7 +28,7 @@ class PersonPreferencesType(graphene.ObjectType): return parent["theme__design"] -class PersonType(DjangoObjectType): +class PersonType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType): class Meta: model = Person fields = [ @@ -51,6 +57,7 @@ class PersonType(DjangoObjectType): "owner_of", "member_of", ] + filterset_class = PersonFilter full_name = graphene.String() username = graphene.String() @@ -252,3 +259,9 @@ class PersonMutation(DjangoModelFormMutation): if not info.context.user.has_perm("core.edit_person_rule", form.instance): raise PermissionDenied() return super().perform_mutate(form, info) + + +class PersonBatchDeleteMutation(BaseBatchDeleteMutation): + class Meta: + model = Person + permissions = ("core.delete_person_rule",) diff --git a/aleksis/core/schema/personal_event.py b/aleksis/core/schema/personal_event.py index 99b1324790509c4a70eaa63abcaaca94d78863e1..a231bd47fda6c20abefe5007375a420f883679f1 100644 --- a/aleksis/core/schema/personal_event.py +++ b/aleksis/core/schema/personal_event.py @@ -4,15 +4,12 @@ from django.utils import timezone import graphene from graphene_django import DjangoObjectType -from graphene_django_cud.mutations import ( - DjangoBatchCreateMutation, - DjangoBatchDeleteMutation, - DjangoBatchPatchMutation, -) from ..models import PersonalEvent from .base import ( - PermissionBatchDeleteMixin, + BaseBatchCreateMutation, + BaseBatchDeleteMutation, + BaseBatchPatchMutation, PermissionBatchPatchMixin, ) @@ -37,7 +34,7 @@ class PersonalEventType(DjangoObjectType): recurrences = graphene.String() -class PersonalEventBatchCreateMutation(PermissionBatchPatchMixin, DjangoBatchCreateMutation): +class PersonalEventBatchCreateMutation(PermissionBatchPatchMixin, BaseBatchCreateMutation): class Meta: model = PersonalEvent permissions = ("core.create_personal_event_with_invitations_rule",) @@ -78,13 +75,13 @@ class PersonalEventBatchCreateMutation(PermissionBatchPatchMixin, DjangoBatchCre return value -class PersonalEventBatchDeleteMutation(PermissionBatchDeleteMixin, DjangoBatchDeleteMutation): +class PersonalEventBatchDeleteMutation(BaseBatchDeleteMutation): class Meta: model = PersonalEvent permissions = ("core.delete_personal_event_rule",) -class PersonalEventBatchPatchMutation(PermissionBatchPatchMixin, DjangoBatchPatchMutation): +class PersonalEventBatchPatchMutation(BaseBatchPatchMutation): class Meta: model = PersonalEvent permissions = ("core.change_personalevent",) diff --git a/aleksis/core/schema/room.py b/aleksis/core/schema/room.py index b91fc22f4e7ff77a836c9ba25d082200055bafc3..19a315ab06cffebd7f9af29d3def2a5ff4405f60 100644 --- a/aleksis/core/schema/room.py +++ b/aleksis/core/schema/room.py @@ -1,16 +1,12 @@ from graphene_django import DjangoObjectType -from graphene_django_cud.mutations import ( - DjangoBatchCreateMutation, - DjangoBatchDeleteMutation, - DjangoBatchPatchMutation, -) from guardian.shortcuts import get_objects_for_user from ..models import Room from .base import ( + BaseBatchCreateMutation, + BaseBatchDeleteMutation, + BaseBatchPatchMutation, DjangoFilterMixin, - PermissionBatchDeleteMixin, - PermissionBatchPatchMixin, PermissionsTypeMixin, ) @@ -30,20 +26,20 @@ class RoomType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType): return get_objects_for_user(info.context.user, "core.view_room", queryset) -class RoomBatchCreateMutation(PermissionBatchPatchMixin, DjangoBatchCreateMutation): +class RoomBatchCreateMutation(BaseBatchCreateMutation): class Meta: model = Room permissions = ("core.create_room_rule",) only_fields = ("id", "name", "short_name") -class RoomBatchDeleteMutation(PermissionBatchDeleteMixin, DjangoBatchDeleteMutation): +class RoomBatchDeleteMutation(BaseBatchDeleteMutation): class Meta: model = Room permissions = ("core.delete_room_rule",) -class RoomBatchPatchMutation(PermissionBatchPatchMixin, DjangoBatchPatchMutation): +class RoomBatchPatchMutation(BaseBatchPatchMutation): class Meta: model = Room permissions = ("core.edit_room_rule",) diff --git a/aleksis/core/schema/school_term.py b/aleksis/core/schema/school_term.py index f9af92105a2672eee1cdd4979568dd83cfcdd843..3b9d6e04e7b176017369a5f71bbb160eb94cd0c2 100644 --- a/aleksis/core/schema/school_term.py +++ b/aleksis/core/schema/school_term.py @@ -2,17 +2,13 @@ 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 ( - DjangoBatchCreateMutation, - DjangoBatchDeleteMutation, - DjangoBatchPatchMutation, -) from ..models import SchoolTerm from .base import ( + BaseBatchCreateMutation, + BaseBatchDeleteMutation, + BaseBatchPatchMutation, DjangoFilterMixin, - PermissionBatchDeleteMixin, - PermissionBatchPatchMixin, PermissionsTypeMixin, ) @@ -35,7 +31,7 @@ class SchoolTermType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType): return queryset -class SchoolTermBatchCreateMutation(PermissionBatchPatchMixin, DjangoBatchCreateMutation): +class SchoolTermBatchCreateMutation(BaseBatchCreateMutation): class Meta: model = SchoolTerm permissions = ("core.create_school_term_rule",) @@ -56,13 +52,13 @@ class SchoolTermBatchCreateMutation(PermissionBatchPatchMixin, DjangoBatchCreate ) -class SchoolTermBatchDeleteMutation(PermissionBatchDeleteMixin, DjangoBatchDeleteMutation): +class SchoolTermBatchDeleteMutation(BaseBatchDeleteMutation): class Meta: model = SchoolTerm permissions = ("core.delete_school_term_rule",) -class SchoolTermBatchPatchMutation(PermissionBatchPatchMixin, DjangoBatchPatchMutation): +class SchoolTermBatchPatchMutation(BaseBatchPatchMutation): class Meta: model = SchoolTerm permissions = ("core.edit_school_term_rule",) diff --git a/aleksis/core/tables.py b/aleksis/core/tables.py index a6c76d38568348418318acf6a819d196ed71b56a..f80c8412efc620497c04198a079c8a68a609c0c0 100644 --- a/aleksis/core/tables.py +++ b/aleksis/core/tables.py @@ -81,19 +81,6 @@ class GroupsTable(tables.Table): school_term = tables.Column() -class GroupTypesTable(tables.Table): - """Table to list group types.""" - - class Meta: - attrs = {"class": "highlight"} - - name = tables.LinkColumn("edit_group_type_by_id", args=[A("id")]) - description = tables.LinkColumn("edit_group_type_by_id", args=[A("id")]) - delete = tables.LinkColumn( - "delete_group_type_by_id", args=[A("id")], verbose_name=_("Delete"), text=_("Delete") - ) - - class DashboardWidgetTable(tables.Table): """Table to list dashboard widgets.""" diff --git a/aleksis/core/templates/core/group_type/edit.html b/aleksis/core/templates/core/group_type/edit.html deleted file mode 100644 index 843975b16bb6ccbd7cf8eeed8f2d5713f5320e23..0000000000000000000000000000000000000000 --- a/aleksis/core/templates/core/group_type/edit.html +++ /dev/null @@ -1,17 +0,0 @@ -{# -*- engine:django -*- #} - -{% extends "core/base.html" %} -{% load material_form i18n %} - -{% block browser_title %}{% blocktrans %}Edit group type{% endblocktrans %}{% endblock %} -{% block page_title %}{% blocktrans %}Edit group type{% endblocktrans %}{% endblock %} - -{% block content %} - - <form method="post"> - {% csrf_token %} - {% form form=edit_group_type_form %}{% endform %} - {% include "core/partials/save_button.html" %} - </form> - -{% endblock %} diff --git a/aleksis/core/templates/core/group_type/list.html b/aleksis/core/templates/core/group_type/list.html deleted file mode 100644 index e862cb0191533178546b7b481b0db3ce26fa70a0..0000000000000000000000000000000000000000 --- a/aleksis/core/templates/core/group_type/list.html +++ /dev/null @@ -1,18 +0,0 @@ -{# -*- engine:django -*- #} - -{% extends "core/base.html" %} - -{% load i18n %} -{% load render_table from django_tables2 %} - -{% block browser_title %}{% blocktrans %}Group types{% endblocktrans %}{% endblock %} -{% block page_title %}{% blocktrans %}Group types{% endblocktrans %}{% endblock %} - -{% block content %} - <a class="btn green waves-effect waves-light" href="{% url 'create_group_type' %}"> - <i class="material-icons iconify left" data-icon="mdi:add"></i> - {% trans "Create group type" %} - </a> - - {% render_table group_types_table %} -{% endblock %} diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py index c5b8b8f91d50a25f7c3c5b13db89fcb91266fb14..9a7ff023f3013ef2e221017abc382d0940e9e301 100644 --- a/aleksis/core/urls.py +++ b/aleksis/core/urls.py @@ -142,7 +142,6 @@ urlpatterns = [ views.EditPersonView.as_view(), name="edit_person_by_id", ), - path("persons/<int:id_>/delete/", views.delete_person, name="delete_person_by_id"), path( "persons/<int:pk>/invite/", views.InvitePersonByID.as_view(), @@ -156,18 +155,6 @@ urlpatterns = [ path("groups/<int:id_>/delete/", views.delete_group, name="delete_group_by_id"), path("", views.index, name="index"), path("dashboard/edit/", views.EditDashboardView.as_view(), name="edit_dashboard"), - path("groups/group_types/create", views.edit_group_type, name="create_group_type"), - path( - "groups/group_types/<int:id_>/delete/", - views.delete_group_type, - name="delete_group_type_by_id", - ), - path( - "groups/group_types/<int:id_>/edit/", - views.edit_group_type, - name="edit_group_type_by_id", - ), - path("groups/group_types/", views.group_types, name="group_types"), path("announcements/", views.announcements, name="announcements"), path("announcements/create/", views.announcement_form, name="add_announcement"), path( diff --git a/aleksis/core/views.py b/aleksis/core/views.py index 4b56a5dc2cb566c6ea41566dc2efa5c2482f73f1..635e4d85405c376c8183632ad7aba95443a930eb 100644 --- a/aleksis/core/views.py +++ b/aleksis/core/views.py @@ -84,7 +84,6 @@ from .forms import ( ChildGroupsForm, DashboardWidgetOrderFormSet, EditGroupForm, - EditGroupTypeForm, GroupPreferenceForm, InvitationCodeForm, MaintenanceModeForm, @@ -108,7 +107,6 @@ from .models import ( DashboardWidgetOrder, DataCheckResult, Group, - GroupType, OAuthApplication, Person, PersonInvitation, @@ -124,7 +122,6 @@ from .tables import ( GroupGlobalPermissionTable, GroupObjectPermissionTable, GroupsTable, - GroupTypesTable, InvitationsTable, PersonsTable, UserGlobalPermissionTable, @@ -654,21 +651,6 @@ def preferences( return render(request, "dynamic_preferences/form.html", context) -@permission_required("core.delete_person_rule", fn=objectgetter_optional(Person)) -def delete_person(request: HttpRequest, id_: int) -> HttpResponse: - """View to delete an person.""" - person = objectgetter_optional(Person)(request, id_) - - with reversion.create_revision(): - set_user(request.user) - person.save() - - person.delete() - messages.success(request, _("The person has been deleted.")) - - return redirect("persons") - - @permission_required("core.delete_group_rule", fn=objectgetter_optional(Group)) def delete_group(request: HttpRequest, id_: int) -> HttpResponse: """View to delete an group.""" @@ -683,61 +665,6 @@ def delete_group(request: HttpRequest, id_: int) -> HttpResponse: return redirect("groups") -@never_cache -@permission_required("core.change_grouptype_rule", fn=objectgetter_optional(GroupType, None, False)) -def edit_group_type(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse: - """View to edit or create a group_type.""" - context = {} - - group_type = objectgetter_optional(GroupType, None, False)(request, id_) - context["group_type"] = group_type - - if id_: - # Edit form for existing group_type - edit_group_type_form = EditGroupTypeForm(request.POST or None, instance=group_type) - else: - # Empty form to create a new group_type - edit_group_type_form = EditGroupTypeForm(request.POST or None) - - if edit_group_type_form.is_valid(): - edit_group_type_form.save(commit=True) - - messages.success(request, _("The group type has been saved.")) - - return redirect("group_types") - - context["edit_group_type_form"] = edit_group_type_form - - return render(request, "core/group_type/edit.html", context) - - -@pwa_cache -@permission_required("core.view_grouptypes_rule") -def group_types(request: HttpRequest) -> HttpResponse: - """List view for listing all group types.""" - context = {} - - # Get all group types - group_types = get_objects_for_user(request.user, "core.view_grouptype", GroupType) - - # Build table - group_types_table = GroupTypesTable(group_types) - RequestConfig(request).configure(group_types_table) - context["group_types_table"] = group_types_table - - return render(request, "core/group_type/list.html", context) - - -@permission_required("core.delete_grouptype_rule", fn=objectgetter_optional(GroupType, None, False)) -def delete_group_type(request: HttpRequest, id_: int) -> HttpResponse: - """View to delete an group_type.""" - group_type = objectgetter_optional(GroupType, None, False)(request, id_) - group_type.delete() - messages.success(request, _("The group type has been deleted.")) - - return redirect("group_types") - - @method_decorator(pwa_cache, name="dispatch") class DataCheckView(PermissionRequiredMixin, ListView): permission_required = "core.view_datacheckresults_rule"