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

Merge branch '20-use-preferences-instead-of-managed-group-types-for-school-structure' into 'master'

Resolve "Use preferences instead of managed group types for school structure"

Closes #20

See merge request !45
parents b7b8d986 f00f99ce
No related branches found
Tags 0.1.0.dev2
1 merge request!45Resolve "Use preferences instead of managed group types for school structure"
Pipeline #189356 failed
......@@ -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()
......@@ -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();
},
},
};
......
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
......
......@@ -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": {
......
......@@ -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": {
......
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."
)
......@@ -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 = (
......
......@@ -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()
SCHOOL_GRADE_GROUP_TYPE_NAME = "School grade"
SCHOOL_CLASS_GROUP_TYPE_NAME = "School class"
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
schema: http://localhost:8000/graphql/
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment