diff --git a/aleksis/apps/cursus/apps.py b/aleksis/apps/cursus/apps.py index ba7bf20152de28ca8df9fabe8a80506b643af1d8..2100153526771424e6b04addd6bc55b3baa2d960 100644 --- a/aleksis/apps/cursus/apps.py +++ b/aleksis/apps/cursus/apps.py @@ -11,12 +11,3 @@ class DefaultConfig(AppConfig): } licence = "EUPL-1.2+" copyright_info = (([2023], "Jonathan Weth", "dev@jonathanweth.de"),) - - def _maintain_default_data(self): - super()._maintain_default_data() - - # Ensure that default group types for school structure exist - from .util.group_types import get_school_class_group_type, get_school_grade_group_type - - get_school_grade_group_type() - get_school_class_group_type() diff --git a/aleksis/apps/cursus/frontend/components/SchoolStructure.vue b/aleksis/apps/cursus/frontend/components/SchoolStructure.vue index 4ebb93ee5cc6aabba652e35b4d594d63eb5c7f7b..37dfe21c881958a6b09e5427c0716648ef814e0a 100644 --- a/aleksis/apps/cursus/frontend/components/SchoolStructure.vue +++ b/aleksis/apps/cursus/frontend/components/SchoolStructure.vue @@ -7,31 +7,48 @@ import SchoolTermField from "aleksis.core/components/school_term/SchoolTermField <template> <v-card> - <!-- Create grade form --> + <!-- Create first level group form --> <dialog-object-form - v-model="createGradeForm" - :fields="createGradeFields" - :default-item="createGradeDefaultItem" + v-model="createFirstLevelGroupForm" + :fields="firstLevelGroupFields" + :default-item="firstLevelGroupDefaultItem" :is-create="true" - create-item-i18n-key="cursus.school_structure.add_grade" - :gql-create-mutation="gqlCreateGrades" - :get-create-data="transformCreateGradeItem" - @cancel="createGradeForm = false" + :gql-create-mutation="gqlCreateFirstLevelGroup" + :get-create-data="transformFirstLevelGroupItem" + @cancel="createFirstLevelGroupForm = false" @save="updateSchoolStructure" - /> - <!-- Create class form --> + > + <template #title> + <span class="text-h5"> + {{ + $t("cursus.school_structure.add_title", { + name: schoolStructure.firstLevelType.name, + }) + }} + </span> + </template> + </dialog-object-form> + <!-- Create second level group form --> <dialog-object-form - v-model="createClassForm" - :fields="createClassFields" - :default-item="createClassDefaultItem" + v-model="createSecondLevelGroupForm" + :fields="secondLevelGroupFields" + :default-item="secondLevelGroupDefaultItem" :is-create="true" - create-item-i18n-key="cursus.school_structure.add_class" - :gql-create-mutation="gqlCreateClasses" - :get-create-data="transformCreateClassItemForGrade" - @cancel="createClassForm = false" + :gql-create-mutation="gqlCreateSecondLevelGroup" + :get-create-data="transformSecondLevelGroupItem" + @cancel="createFirstLevelGroupForm = false" @save="updateSchoolStructure" > - <!-- Hide parentGroups field - it is set on grade --> + <template #title> + <span class="text-h5"> + {{ + $t("cursus.school_structure.add_title", { + name: schoolStructure.secondLevelType.name, + }) + }} + </span> + </template> + <!-- Hide parentGroups field - it is set on first level group --> <!-- eslint-disable-next-line vue/valid-v-slot --> <template #parentGroups.field="{ on, attrs }"> <input type="hidden" v-bind="attrs" v-on="on" /> @@ -52,19 +69,27 @@ import SchoolTermField from "aleksis.core/components/school_term/SchoolTermField <v-card-actions> <create-button v-if="this.$data.currentTerm" - i18n-key="cursus.school_structure.add_grade" - @click="createGrade" - /> + @click="createFirstLevelGroup" + > + <v-icon left>$plus</v-icon> + {{ + $t("cursus.school_structure.add", { + name: schoolStructure.firstLevelType.name, + }) + }} + </create-button> </v-card-actions> </div> </div> - <!-- Grades --> + <!-- First level groups --> <v-container v-if="this.$data.currentTerm"> <v-row class="overflow-x-auto flex-nowrap slide-n-snap-x-container"> <!-- responsive 1, 2, 3, 4 col layout --> <v-col - v-for="grade in grades" - :key="grade.id" + v-for="firstGroup in schoolStructure + ? schoolStructure.firstLevelGroupsByTerm + : []" + :key="firstGroup.id" class="slide-n-snap-contained" cols="12" sm="6" @@ -74,27 +99,26 @@ import SchoolTermField from "aleksis.core/components/school_term/SchoolTermField > <v-card> <v-card-title class="justify-end"> - {{ $t("cursus.school_structure.grade") }} - <span class="ml-3 text-h4">{{ grade.shortName }}</span> + {{ schoolStructure.firstLevelType.name }} + <span class="ml-3 text-h4">{{ firstGroup.shortName }}</span> </v-card-title> <v-list :max-height="$vuetify.breakpoint.height - 333" class="overflow-y-auto slide-n-snap-y-container" > - <!-- class is a "forbidden" name in v-for --> <v-list-item - v-for="clas in grade.childGroups" - :key="clas.id" + v-for="secondGroup in firstGroup.childGroups" + :key="secondGroup.id" class="slide-n-snap-contained" > <v-card class="mx-3 my-2"> <div class="d-flex flex-nowrap justify-space-between"> <div> <v-card-title class="text-h4"> - {{ clas.shortName }} + {{ secondGroup.shortName }} </v-card-title> <v-card-subtitle> - {{ clas.name }} + {{ secondGroup.name }} </v-card-subtitle> </div> <div> @@ -104,15 +128,15 @@ import SchoolTermField from "aleksis.core/components/school_term/SchoolTermField class="px-2" > <v-chip - v-for="teacher in clas.owners" + v-for="teacher in secondGroup.owners" :key="teacher.id" :to="{ name: 'core.personById', params: { id: teacher.id }, }" - :outlined="true" + outlined > - {{ teacher.shortName }} + {{ teacher.shortName || teacher.lastName }} </v-chip> </v-chip-group> <v-card-actions> @@ -120,7 +144,7 @@ import SchoolTermField from "aleksis.core/components/school_term/SchoolTermField i18n-key="cursus.school_structure.timetable" :to="{ name: 'lesrooster.timetable_management', - params: { id: clas.id }, + params: { id: secondGroup.id }, }" /> </v-card-actions> @@ -133,10 +157,16 @@ import SchoolTermField from "aleksis.core/components/school_term/SchoolTermField <v-spacer /> <!-- MAYBE: ADD PLAN COURSES LINK --> <create-button - i18n-key="cursus.school_structure.add_class" color="secondary" - @click="createClass(grade.id)" - /> + @click="createSecondLevelGroup(firstGroup.id)" + > + <v-icon left>$plus</v-icon> + {{ + $t("cursus.school_structure.add", { + name: schoolStructure.secondLevelType.name, + }) + }} + </create-button> </v-card-actions> </v-card> </v-col> @@ -147,38 +177,38 @@ import SchoolTermField from "aleksis.core/components/school_term/SchoolTermField <script> import { - gqlSchoolGrades, - gqlCreateGrades, - gqlCreateClasses, + gqlFirstLevelGroups, + gqlCreateFirstLevelGroup, + gqlCreateSecondLevelGroup, } from "./schoolStructure.graphql"; export default { name: "SchoolStructure", data() { return { - createGradeForm: false, - createGradeFields: [ + createFirstLevelGroupForm: false, + firstLevelGroupFields: [ { - text: this.$t("cursus.school_structure.grade_fields.name"), + text: this.$t("cursus.school_structure.fields.name"), value: "name", }, { - text: this.$t("cursus.school_structure.grade_fields.short_name"), + text: this.$t("cursus.school_structure.fields.short_name"), value: "shortName", }, ], - createGradeDefaultItem: { + firstLevelGroupDefaultItem: { name: "", shortName: "", }, - createClassForm: false, - createClassFields: [ + createSecondLevelGroupForm: false, + secondLevelGroupFields: [ { - text: this.$t("cursus.school_structure.class_fields.name"), + text: this.$t("cursus.school_structure.fields.name"), value: "name", }, { - text: this.$t("cursus.school_structure.class_fields.short_name"), + text: this.$t("cursus.school_structure.fields.short_name"), value: "shortName", }, { @@ -186,18 +216,18 @@ export default { value: "parentGroups", }, ], - createClassDefaultItem: { + secondLevelGroupDefaultItem: { name: "", shortName: "", parentGroups: [], }, - createClassCurrentGradeID: 0, + createSecondLevelGroupFirstLevelGroupId: 0, currentTerm: null, }; }, apollo: { - grades: { - query: gqlSchoolGrades, + schoolStructure: { + query: gqlFirstLevelGroups, variables() { return { schoolTerm: this.$data.currentTerm.id, @@ -209,28 +239,28 @@ export default { }, }, methods: { - createGrade() { - this.$data.createGradeForm = true; + createFirstLevelGroup() { + this.$data.createFirstLevelGroupForm = true; }, - createClass(id) { - this.$data.createClassCurrentGradeID = id; - this.$data.createClassForm = true; + createSecondLevelGroup(id) { + this.$data.createSecondLevelGroupFirstLevelGroupId = id; + this.$data.createSecondLevelGroupForm = true; }, - transformCreateGradeItem(item) { + transformFirstLevelGroupItem(item) { return { ...item, schoolTerm: this.$data.currentTerm.id, }; }, - transformCreateClassItemForGrade(item) { + transformSecondLevelGroupItem(item) { return { ...item, schoolTerm: this.$data.currentTerm.id, - parentGroups: this.$data.createClassCurrentGradeID, + parentGroups: this.$data.createSecondLevelGroupFirstLevelGroupId, }; }, updateSchoolStructure() { - this.$apollo.queries.grades.refetch(); + this.$apollo.queries.schoolStructure.refetch(); }, }, }; diff --git a/aleksis/apps/cursus/frontend/components/schoolStructure.graphql b/aleksis/apps/cursus/frontend/components/schoolStructure.graphql index 269bee126f97789b705a31371b9782d6be1f60ed..4c88e651f7b197d7772af11745c8170b386b1f2a 100644 --- a/aleksis/apps/cursus/frontend/components/schoolStructure.graphql +++ b/aleksis/apps/cursus/frontend/components/schoolStructure.graphql @@ -1,23 +1,32 @@ -query gqlSchoolGrades($schoolTerm: ID!) { - grades: schoolGradesByTerm(schoolTerm: $schoolTerm) { - id - name - shortName - childGroups { +query gqlFirstLevelGroups($schoolTerm: ID!) { + schoolStructure { + firstLevelType { + name + } + secondLevelType { + name + } + firstLevelGroupsByTerm(schoolTerm: $schoolTerm) { id name shortName - owners { + childGroups { id + name shortName + owners { + id + shortName + lastName + } } } } } -mutation gqlCreateGrades($input: [BatchCreateGroupInput]!) { - createGrades(input: $input) { - grades: groups { +mutation gqlCreateFirstLevelGroup($input: [BatchCreateGroupInput]!) { + createFirstLevelGroups(input: $input) { + firstLevelGroups: groups { id name shortName @@ -28,9 +37,9 @@ mutation gqlCreateGrades($input: [BatchCreateGroupInput]!) { } } -mutation gqlCreateClasses($input: [BatchCreateGroupInput]!) { - createClasses(input: $input) { - classes: groups { +mutation gqlCreateSecondLevelGroup($input: [BatchCreateGroupInput]!) { + createSecondLevelGroups(input: $input) { + secondLevelGroups: groups { id name shortName diff --git a/aleksis/apps/cursus/frontend/messages/de.json b/aleksis/apps/cursus/frontend/messages/de.json index 6e955badb0d0432f100d1a94b0d94b84f982f919..eb98427701e627f717b136a881fb6351de4d9fab 100644 --- a/aleksis/apps/cursus/frontend/messages/de.json +++ b/aleksis/apps/cursus/frontend/messages/de.json @@ -30,17 +30,12 @@ "school_structure": { "menu_title": "Schulstruktur", "title": "Meine Schulstruktur aus", - "grade": "Jahrgang", - "add_grade": "Jahrgang hinzufügen", - "grade_fields": { - "short_name": "Kurzname", - "name": "Name" - }, - "add_class": "Klasse hinzufügen", - "class_fields": { + "fields": { "short_name": "Kurzname", "name": "Name" }, + "add": "{name} hinzufügen", + "add_title": "{name} hinzufügen", "timetable": "Stundenplan" }, "errors": { diff --git a/aleksis/apps/cursus/frontend/messages/en.json b/aleksis/apps/cursus/frontend/messages/en.json index 9e257d3fc21217a044245a681036c2b3bf7c1582..45081f4f504514045475a7cc735f2423f6723246 100644 --- a/aleksis/apps/cursus/frontend/messages/en.json +++ b/aleksis/apps/cursus/frontend/messages/en.json @@ -30,17 +30,12 @@ "school_structure": { "menu_title": "School Structure", "title": "My School Structure in", - "grade": "Grade", - "add_grade": "Add grade", - "grade_fields": { - "short_name": "Short Name", - "name": "Name" - }, - "add_class": "Add class", - "class_fields": { + "fields": { "short_name": "Short Name", "name": "Name" }, + "add": "Add {name}", + "add_title": "Add {name}", "timetable": "Timetable" }, "errors": { diff --git a/aleksis/apps/cursus/preferences.py b/aleksis/apps/cursus/preferences.py new file mode 100644 index 0000000000000000000000000000000000000000..b356b428c4de63617447fdaec1ec8118541144da --- /dev/null +++ b/aleksis/apps/cursus/preferences.py @@ -0,0 +1,35 @@ +from django.utils.translation import gettext_lazy as _ + +from dynamic_preferences.preferences import Section +from dynamic_preferences.types import ModelChoicePreference + +from aleksis.core.models import GroupType +from aleksis.core.registries import site_preferences_registry + +cursus = Section("cursus", verbose_name=_("Course management")) + + +@site_preferences_registry.register +class SchoolStructureFirstLevelGroupType(ModelChoicePreference): + section = cursus + name = "school_structure_first_level_group_type" + required = False + default = None + model = GroupType + verbose_name = _("School structure: Group type for first level (e. g. grades)") + help_text = _( + "You have to set this and the second level group type to use the school structure tool." + ) + + +@site_preferences_registry.register +class SchoolStructureSecondLevelGroupType(ModelChoicePreference): + section = cursus + name = "school_structure_second_level_group_type" + required = False + default = None + model = GroupType + verbose_name = _("School structure: Group type for second level (e. g. classes)") + help_text = _( + "You have to set this and the first level group type to use the school structure tool." + ) diff --git a/aleksis/apps/cursus/rules.py b/aleksis/apps/cursus/rules.py index 59443793c21ee7f25da1fc88363c10d4c02d85db..c7f65403466cba3ab6c1b0556f39f3fbeb10b79a 100644 --- a/aleksis/apps/cursus/rules.py +++ b/aleksis/apps/cursus/rules.py @@ -5,6 +5,7 @@ from aleksis.core.util.predicates import ( has_global_perm, has_object_perm, has_person, + is_site_preference_set, ) from .models import Course, Subject @@ -59,7 +60,12 @@ delete_course_predicate = view_course_predicate & ( ) add_perm("cursus.delete_course_rule", delete_course_predicate) -manage_school_structure_predicate = has_person & has_global_perm("cursus.manage_school_structure") +manage_school_structure_predicate = ( + has_person + & is_site_preference_set("cursus", "school_structure_first_level_group_type") + & is_site_preference_set("cursus", "school_structure_second_level_group_type") + & has_global_perm("cursus.manage_school_structure") +) add_perm("cursus.manage_school_structure_rule", manage_school_structure_predicate) view_cursus_menu_predicate = ( diff --git a/aleksis/apps/cursus/schema.py b/aleksis/apps/cursus/schema.py index 6d40775f80bee61d977746f1096e7dc6ebb1c66f..2fb60907d9aa7cbda4de6e9c956c3bc9d8145719 100644 --- a/aleksis/apps/cursus/schema.py +++ b/aleksis/apps/cursus/schema.py @@ -10,11 +10,6 @@ from graphene_django_cud.mutations import ( ) from guardian.shortcuts import get_objects_for_user -from aleksis.apps.cursus.settings import SCHOOL_CLASS_GROUP_TYPE_NAME, SCHOOL_GRADE_GROUP_TYPE_NAME -from aleksis.apps.cursus.util.group_types import ( - get_school_class_group_type, - get_school_grade_group_type, -) from aleksis.core.models import Group, Person from aleksis.core.schema.base import ( DjangoFilterMixin, @@ -24,8 +19,9 @@ from aleksis.core.schema.base import ( PermissionsTypeMixin, ) from aleksis.core.schema.group import GroupType as GraphQLGroupType +from aleksis.core.schema.group_type import GroupTypeType from aleksis.core.schema.person import PersonType as GraphQLPersonType -from aleksis.core.util.core_helpers import has_person +from aleksis.core.util.core_helpers import get_site_preferences, has_person from .models import Course, Subject @@ -198,7 +194,7 @@ class CourseBatchPatchMutation(PermissionBatchPatchMixin, DjangoBatchPatchMutati only_fields = ("id", "name", "subject", "teachers", "groups", "lesson_quota") -class CreateSchoolClassMutation(DjangoBatchCreateMutation): +class CreateSchoolStructureSecondLevelGroupsMutation(DjangoBatchCreateMutation): class Meta: model = Group permissions = ("core.add_group",) @@ -206,13 +202,15 @@ class CreateSchoolClassMutation(DjangoBatchCreateMutation): @classmethod def before_mutate(cls, root, info, input): # noqa - group_type = get_school_class_group_type() - for school_class in input: - school_class["group_type"] = group_type.pk + group_type = get_site_preferences()["cursus__school_structure_second_level_group_type"] + if not group_type: + raise PermissionDenied() + for group in input: + group["group_type"] = group_type.pk return input -class CreateSchoolGradeMutation(DjangoBatchCreateMutation): +class CreateSchoolStructureFirstLevelGroupsMutation(DjangoBatchCreateMutation): class Meta: model = Group permissions = ("core.add_group",) @@ -220,57 +218,84 @@ class CreateSchoolGradeMutation(DjangoBatchCreateMutation): @classmethod def before_mutate(cls, root, info, input): # noqa - group_type = get_school_grade_group_type() - for school_grade in input: - school_grade["group_type"] = group_type.pk + group_type = get_site_preferences()["cursus__school_structure_first_level_group_type"] + if not group_type: + raise PermissionDenied() + for group in input: + group["group_type"] = group_type.pk return input -class Query(graphene.ObjectType): - subjects = FilterOrderList(SubjectType) - courses = FilterOrderList(CourseType) - - school_classes = FilterOrderList(GraphQLGroupType) - school_grades = FilterOrderList(GraphQLGroupType) - school_grades_by_term = FilterOrderList(GraphQLGroupType, school_term=graphene.ID()) - - teachers = FilterOrderList(TeacherType) +class SchoolStructureQuery(graphene.ObjectType): + first_level_type = graphene.Field(GroupTypeType) + second_level_type = graphene.Field(GroupTypeType) + first_level_groups = FilterOrderList(GraphQLGroupType) + second_level_groups = FilterOrderList(GraphQLGroupType) + first_level_groups_by_term = FilterOrderList(GraphQLGroupType, school_term=graphene.ID()) - course_by_id = graphene.Field(CourseType, id=graphene.ID()) - courses_of_teacher = FilterOrderList(CourseType, teacher=graphene.ID()) + @staticmethod + def resolve_first_level_type(root, info, **kwargs): + return get_site_preferences()["cursus__school_structure_first_level_group_type"] - def resolve_course_by_id(root, info, id): # noqa - course = Course.objects.get(pk=id) - if not info.context.user.has_perm("cursus.view_course_rule", course): - raise PermissionDenied() - return course + @staticmethod + def resolve_second_level_type(root, info, **kwargs): + return get_site_preferences()["cursus__school_structure_second_level_group_type"] @staticmethod - def resolve_school_classes(root, info, **kwargs): + def resolve_first_level_groups(root, info, **kwargs): + group_type = get_site_preferences()["cursus__school_structure_first_level_group_type"] + if not group_type: + return [] return get_objects_for_user( info.context.user, "core.view_group", - Group.objects.filter(group_type__name=SCHOOL_CLASS_GROUP_TYPE_NAME), + Group.objects.filter(group_type=group_type), ) @staticmethod - def resolve_school_grades(root, info, **kwargs): + def resolve_second_level_groups(root, info, **kwargs): + group_type = get_site_preferences()["cursus__school_structure_second_level_group_type"] + if not group_type: + return [] return get_objects_for_user( info.context.user, "core.view_group", - Group.objects.filter(group_type__name=SCHOOL_GRADE_GROUP_TYPE_NAME), + Group.objects.filter(group_type=group_type), ) @staticmethod - def resolve_school_grades_by_term(root, info, school_term): + def resolve_first_level_groups_by_term(root, info, school_term): + group_type = get_site_preferences()["cursus__school_structure_first_level_group_type"] + print( + group_type, + Group.objects.filter(school_term=school_term).filter(group_type=group_type), + ) + if not group_type: + return [] return get_objects_for_user( info.context.user, "core.view_group", - Group.objects.filter(school_term__id=school_term).filter( - group_type__name=SCHOOL_GRADE_GROUP_TYPE_NAME - ), + Group.objects.filter(school_term=school_term).filter(group_type=group_type), ) + +class Query(graphene.ObjectType): + subjects = FilterOrderList(SubjectType) + courses = FilterOrderList(CourseType) + + school_structure = graphene.Field(SchoolStructureQuery) + + teachers = FilterOrderList(TeacherType) + + course_by_id = graphene.Field(CourseType, id=graphene.ID()) + courses_of_teacher = FilterOrderList(CourseType, teacher=graphene.ID()) + + def resolve_course_by_id(root, info, id): # noqa + course = Course.objects.get(pk=id) + if not info.context.user.has_perm("cursus.view_course_rule", course): + raise PermissionDenied() + return course + @staticmethod def resolve_teachers(root, info): return get_objects_for_user( @@ -289,6 +314,10 @@ class Query(graphene.ObjectType): # FIXME: Permission checking. But maybe it's done in get_queryset return teacher.courses_as_teacher.all() + @staticmethod + def resolve_school_structure(root, info): + return True + class Mutation(graphene.ObjectType): create_subjects = SubjectBatchCreateMutation.Field() @@ -299,5 +328,5 @@ class Mutation(graphene.ObjectType): delete_courses = CourseBatchDeleteMutation.Field() update_courses = CourseBatchPatchMutation.Field() - create_grades = CreateSchoolGradeMutation.Field() - create_classes = CreateSchoolClassMutation.Field() + create_first_level_groups = CreateSchoolStructureFirstLevelGroupsMutation.Field() + create_second_level_groups = CreateSchoolStructureSecondLevelGroupsMutation.Field() diff --git a/aleksis/apps/cursus/settings.py b/aleksis/apps/cursus/settings.py deleted file mode 100644 index 1065c8c679515b92214f591ca8c1962a2ff4bfba..0000000000000000000000000000000000000000 --- a/aleksis/apps/cursus/settings.py +++ /dev/null @@ -1,2 +0,0 @@ -SCHOOL_GRADE_GROUP_TYPE_NAME = "School grade" -SCHOOL_CLASS_GROUP_TYPE_NAME = "School class" diff --git a/aleksis/apps/cursus/util/group_types.py b/aleksis/apps/cursus/util/group_types.py deleted file mode 100644 index 71f1ebbcc4d4c7613aa678af24b1b3e154d01f24..0000000000000000000000000000000000000000 --- a/aleksis/apps/cursus/util/group_types.py +++ /dev/null @@ -1,17 +0,0 @@ -from aleksis.core.models import GroupType - -from ..settings import SCHOOL_CLASS_GROUP_TYPE_NAME, SCHOOL_GRADE_GROUP_TYPE_NAME - - -def get_school_grade_group_type(): - group_type, __ = GroupType.objects.managed_by_app("cursus").get_or_create( - name=SCHOOL_GRADE_GROUP_TYPE_NAME, managed_by_app_label="cursus" - ) - return group_type - - -def get_school_class_group_type(): - group_type, __ = GroupType.objects.managed_by_app("cursus").get_or_create( - name=SCHOOL_CLASS_GROUP_TYPE_NAME, managed_by_app_label="cursus" - ) - return group_type diff --git a/graphql.config.yml b/graphql.config.yml new file mode 100644 index 0000000000000000000000000000000000000000..385050a8aab4f7224dcf2ac2e06742b6df0ec198 --- /dev/null +++ b/graphql.config.yml @@ -0,0 +1 @@ +schema: http://localhost:8000/graphql/