Merged in feature/bugfix-attendance-check-dropdown (pull request #200)
Feature/bugfix attendance check dropdown
This commit is contained in:
commit
2db7a5186f
|
|
@ -39,7 +39,8 @@ onMounted(async () => {
|
||||||
const { gradedUsers, assignmentSubmittedUsers } =
|
const { gradedUsers, assignmentSubmittedUsers } =
|
||||||
await loadAssignmentCompletionStatusData(
|
await loadAssignmentCompletionStatusData(
|
||||||
props.learningContentAssignment.content_assignment_id,
|
props.learningContentAssignment.content_assignment_id,
|
||||||
props.courseSession.id
|
props.courseSession.id,
|
||||||
|
props.learningContentAssignment.id
|
||||||
);
|
);
|
||||||
state.gradedUsers = gradedUsers;
|
state.gradedUsers = gradedUsers;
|
||||||
state.assignmentSubmittedUsers = assignmentSubmittedUsers;
|
state.assignmentSubmittedUsers = assignmentSubmittedUsers;
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,15 @@ import ItPersonRow from "@/components/ui/ItPersonRow.vue";
|
||||||
import { useCurrentCourseSession } from "@/composables";
|
import { useCurrentCourseSession } from "@/composables";
|
||||||
import type { AttendanceUserStatus } from "@/gql/graphql";
|
import type { AttendanceUserStatus } from "@/gql/graphql";
|
||||||
import { ATTENDANCE_CHECK_MUTATION } from "@/graphql/mutations";
|
import { ATTENDANCE_CHECK_MUTATION } from "@/graphql/mutations";
|
||||||
import { ATTENDANCE_CHECK_QUERY } from "@/graphql/queries";
|
|
||||||
import { useCockpitStore } from "@/stores/cockpit";
|
import { useCockpitStore } from "@/stores/cockpit";
|
||||||
import type { DropdownSelectable } from "@/types";
|
import type { DropdownSelectable } from "@/types";
|
||||||
import { useMutation, useQuery } from "@urql/vue";
|
import { useMutation } from "@urql/vue";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
import { computed, reactive, watch } from "vue";
|
import { computed, onMounted, reactive, watch } from "vue";
|
||||||
import { useTranslation } from "i18next-vue";
|
import { useTranslation } from "i18next-vue";
|
||||||
|
import { ATTENDANCE_CHECK_QUERY } from "@/graphql/queries";
|
||||||
|
import { graphqlClient } from "@/graphql/client";
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const cockpitStore = useCockpitStore();
|
const cockpitStore = useCockpitStore();
|
||||||
|
|
@ -28,17 +29,13 @@ const presenceCoursesDropdownOptions = computed(() => {
|
||||||
(attendanceCourse) =>
|
(attendanceCourse) =>
|
||||||
({
|
({
|
||||||
id: attendanceCourse.id,
|
id: attendanceCourse.id,
|
||||||
name: `${t("Präsenzkurs")} ${dayjs(attendanceCourse.start).format(
|
name: `${t("Präsenzkurs")} ${attendanceCourse.circle_title} ${dayjs(
|
||||||
"DD.MM.YYYY"
|
attendanceCourse.start
|
||||||
)}`,
|
).format("DD.MM.YYYY")}`,
|
||||||
} as DropdownSelectable)
|
} as DropdownSelectable)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const attendanceCourseSelected = computed(
|
|
||||||
() => state.attendanceCourseSelected.id != "-1"
|
|
||||||
);
|
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
userPresence: new Map<string, boolean>(),
|
userPresence: new Map<string, boolean>(),
|
||||||
attendanceCourseSelected: presenceCoursesDropdownOptions.value[0],
|
attendanceCourseSelected: presenceCoursesDropdownOptions.value[0],
|
||||||
|
|
@ -46,13 +43,11 @@ const state = reactive({
|
||||||
attendanceSaved: false,
|
attendanceSaved: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const attendanceQuery = useQuery({
|
function resetState() {
|
||||||
query: ATTENDANCE_CHECK_QUERY,
|
state.userPresence = new Map<string, boolean>();
|
||||||
pause: true,
|
state.disclaimerConfirmed = false;
|
||||||
variables: {
|
state.attendanceSaved = false;
|
||||||
courseSessionId: state.attendanceCourseSelected.id.toString(),
|
}
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
type UserPresence = {
|
type UserPresence = {
|
||||||
|
|
@ -79,9 +74,19 @@ const onSubmit = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadAttendanceData = async () => {
|
const loadAttendanceData = async () => {
|
||||||
const res = await attendanceQuery.executeQuery();
|
resetState();
|
||||||
|
// with changing variables `useQuery` does not seem to work correctly
|
||||||
|
const res = await graphqlClient.query(
|
||||||
|
ATTENDANCE_CHECK_QUERY,
|
||||||
|
{
|
||||||
|
courseSessionId: state.attendanceCourseSelected.id.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
requestPolicy: "network-only",
|
||||||
|
}
|
||||||
|
);
|
||||||
const attendanceUserList =
|
const attendanceUserList =
|
||||||
res?.data?.value?.course_session_attendance_course?.attendance_user_list ?? [];
|
res.data?.course_session_attendance_course?.attendance_user_list ?? [];
|
||||||
for (const user of attendanceUserList) {
|
for (const user of attendanceUserList) {
|
||||||
if (!user) continue;
|
if (!user) continue;
|
||||||
state.userPresence.set(user.user_id.toString(), user.status === "PRESENT");
|
state.userPresence.set(user.user_id.toString(), user.status === "PRESENT");
|
||||||
|
|
@ -91,15 +96,23 @@ const loadAttendanceData = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadAttendanceData();
|
onMounted(() => {
|
||||||
watch(state.attendanceCourseSelected, () => {
|
log.debug("AttendanceCheckPage mounted");
|
||||||
loadAttendanceData();
|
loadAttendanceData();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => state.attendanceCourseSelected,
|
||||||
|
() => {
|
||||||
|
log.debug("attendanceCourseSelected changed", state.attendanceCourseSelected);
|
||||||
|
loadAttendanceData();
|
||||||
|
}
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="bg-gray-200">
|
<div class="bg-gray-200">
|
||||||
<div class="container-large">
|
<div v-if="courseSession" class="container-large">
|
||||||
<nav class="py-4 pb-4">
|
<nav class="py-4 pb-4">
|
||||||
<router-link
|
<router-link
|
||||||
class="btn-text inline-flex items-center pl-0"
|
class="btn-text inline-flex items-center pl-0"
|
||||||
|
|
@ -117,7 +130,6 @@ watch(state.attendanceCourseSelected, () => {
|
||||||
></ItDropdownSelect>
|
></ItDropdownSelect>
|
||||||
<div v-if="!state.attendanceSaved" class="flex flex-row items-center">
|
<div v-if="!state.attendanceSaved" class="flex flex-row items-center">
|
||||||
<ItCheckbox
|
<ItCheckbox
|
||||||
:disabled="!attendanceCourseSelected"
|
|
||||||
:checkbox-item="{
|
:checkbox-item="{
|
||||||
value: true,
|
value: true,
|
||||||
checked: state.disclaimerConfirmed,
|
checked: state.disclaimerConfirmed,
|
||||||
|
|
@ -158,7 +170,7 @@ watch(state.attendanceCourseSelected, () => {
|
||||||
>
|
>
|
||||||
<template #leading>
|
<template #leading>
|
||||||
<ItCheckbox
|
<ItCheckbox
|
||||||
:disabled="!attendanceCourseSelected || state.attendanceSaved"
|
:disabled="state.attendanceSaved"
|
||||||
:checkbox-item="{
|
:checkbox-item="{
|
||||||
value: true,
|
value: true,
|
||||||
checked: state.userPresence.get(csu.user_id.toString()) as boolean,
|
checked: state.userPresence.get(csu.user_id.toString()) as boolean,
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@ onMounted(async () => {
|
||||||
const { assignmentSubmittedUsers, gradedUsers, total } =
|
const { assignmentSubmittedUsers, gradedUsers, total } =
|
||||||
await loadAssignmentCompletionStatusData(
|
await loadAssignmentCompletionStatusData(
|
||||||
props.learningContentAssignment.content_assignment_id,
|
props.learningContentAssignment.content_assignment_id,
|
||||||
props.courseSession.id
|
props.courseSession.id,
|
||||||
|
props.learningContentAssignment.id
|
||||||
);
|
);
|
||||||
|
|
||||||
state.submissionProgressStatusCount = {
|
state.submissionProgressStatusCount = {
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ function setActiveClasses(translationKey: string) {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<router-link
|
<router-link
|
||||||
:to="`/course/${props.courseSlug}/cockpit/attendanceCheck`"
|
:to="`/course/${props.courseSlug}/cockpit/attendance`"
|
||||||
class="btn-secondary min-w-min"
|
class="btn-secondary min-w-min"
|
||||||
>
|
>
|
||||||
{{ $t("Anwesenheit prüfen") }}
|
{{ $t("Anwesenheit prüfen") }}
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@ const router = createRouter({
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "attendanceCheck",
|
path: "attendance",
|
||||||
component: () =>
|
component: () =>
|
||||||
import("@/pages/cockpit/attendanceCheckPage/AttendanceCheckPage.vue"),
|
import("@/pages/cockpit/attendanceCheckPage/AttendanceCheckPage.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,8 @@ export function calcLearningContentAssignments(learningPath?: LearningPath) {
|
||||||
|
|
||||||
export async function loadAssignmentCompletionStatusData(
|
export async function loadAssignmentCompletionStatusData(
|
||||||
assignmentId: number,
|
assignmentId: number,
|
||||||
courseSessionId: number
|
courseSessionId: number,
|
||||||
|
learningContentId: number
|
||||||
) {
|
) {
|
||||||
const cockpitStore = useCockpitStore();
|
const cockpitStore = useCockpitStore();
|
||||||
|
|
||||||
|
|
@ -46,7 +47,9 @@ export async function loadAssignmentCompletionStatusData(
|
||||||
const assignmentSubmittedUsers: CourseSessionUser[] = [];
|
const assignmentSubmittedUsers: CourseSessionUser[] = [];
|
||||||
for (const csu of courseSessionUsers) {
|
for (const csu of courseSessionUsers) {
|
||||||
const userAssignmentStatus = assignmentCompletionData.find(
|
const userAssignmentStatus = assignmentCompletionData.find(
|
||||||
(s) => s.assignment_user_id === csu.user_id
|
(s) =>
|
||||||
|
s.assignment_user_id === csu.user_id &&
|
||||||
|
s.learning_content_page_id === learningContentId
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
userAssignmentStatus?.completion_status === "SUBMITTED" ||
|
userAssignmentStatus?.completion_status === "SUBMITTED" ||
|
||||||
|
|
|
||||||
|
|
@ -425,6 +425,7 @@ export interface CourseSessionAttendanceCourse {
|
||||||
location: string;
|
location: string;
|
||||||
trainer: string;
|
trainer: string;
|
||||||
due_date_id: number;
|
due_date_id: number;
|
||||||
|
circle_title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CourseSessionAssignment {
|
export interface CourseSessionAssignment {
|
||||||
|
|
@ -568,6 +569,7 @@ export interface UserAssignmentCompletionStatus {
|
||||||
assignment_user_id: string;
|
assignment_user_id: string;
|
||||||
completion_status: AssignmentCompletionStatus;
|
completion_status: AssignmentCompletionStatus;
|
||||||
evaluation_grade: number | null;
|
evaluation_grade: number | null;
|
||||||
|
learning_content_page_id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DueDate = {
|
export type DueDate = {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,13 @@ def request_assignment_completion_status(request, assignment_id, course_session_
|
||||||
qs = AssignmentCompletion.objects.filter(
|
qs = AssignmentCompletion.objects.filter(
|
||||||
course_session_id=course_session_id,
|
course_session_id=course_session_id,
|
||||||
assignment_id=assignment_id,
|
assignment_id=assignment_id,
|
||||||
).values("id", "assignment_user_id", "completion_status", "evaluation_grade")
|
).values(
|
||||||
|
"id",
|
||||||
|
"assignment_user_id",
|
||||||
|
"completion_status",
|
||||||
|
"evaluation_grade",
|
||||||
|
"learning_content_page_id",
|
||||||
|
)
|
||||||
return Response(status=200, data=qs)
|
return Response(status=200, data=qs)
|
||||||
|
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ class CourseSessionAttendanceCourseAdmin(admin.ModelAdmin):
|
||||||
"start_date",
|
"start_date",
|
||||||
"end_date",
|
"end_date",
|
||||||
"trainer",
|
"trainer",
|
||||||
|
"location",
|
||||||
]
|
]
|
||||||
list_filter = ["course_session__course", "course_session"]
|
list_filter = ["course_session__course", "course_session"]
|
||||||
|
|
||||||
|
|
@ -38,12 +39,10 @@ class CourseSessionAttendanceCourseAdmin(admin.ModelAdmin):
|
||||||
end_date.admin_order_field = "due_date__end"
|
end_date.admin_order_field = "due_date__end"
|
||||||
|
|
||||||
def circle(self, obj):
|
def circle(self, obj):
|
||||||
try:
|
circle = obj.get_circle()
|
||||||
return obj.learning_content.get_ancestors().exact_type(Circle).first().title
|
if circle:
|
||||||
except Exception:
|
return circle.title
|
||||||
# noop
|
return ""
|
||||||
pass
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Create a method that serves as a form field
|
# Create a method that serves as a form field
|
||||||
def circle_display(self, obj=None):
|
def circle_display(self, obj=None):
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from django_jsonform.models.fields import JSONField as JSONSchemaField
|
||||||
|
|
||||||
from vbv_lernwelt.assignment.models import AssignmentType
|
from vbv_lernwelt.assignment.models import AssignmentType
|
||||||
from vbv_lernwelt.duedate.models import DueDate
|
from vbv_lernwelt.duedate.models import DueDate
|
||||||
|
from vbv_lernwelt.learnpath.models import Circle
|
||||||
|
|
||||||
|
|
||||||
class CourseSessionAttendanceCourse(models.Model):
|
class CourseSessionAttendanceCourse(models.Model):
|
||||||
|
|
@ -73,6 +74,14 @@ class CourseSessionAttendanceCourse(models.Model):
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_circle(self):
|
||||||
|
try:
|
||||||
|
return self.learning_content.get_ancestors().exact_type(Circle).first()
|
||||||
|
except Exception:
|
||||||
|
# noop
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.course_session} - {self.learning_content}"
|
return f"{self.course_session} - {self.learning_content}"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ from vbv_lernwelt.course_session.models import (
|
||||||
class CourseSessionAttendanceCourseSerializer(serializers.ModelSerializer):
|
class CourseSessionAttendanceCourseSerializer(serializers.ModelSerializer):
|
||||||
start = serializers.SerializerMethodField()
|
start = serializers.SerializerMethodField()
|
||||||
end = serializers.SerializerMethodField()
|
end = serializers.SerializerMethodField()
|
||||||
|
circle_title = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CourseSessionAttendanceCourse
|
model = CourseSessionAttendanceCourse
|
||||||
|
|
@ -21,6 +22,7 @@ class CourseSessionAttendanceCourseSerializer(serializers.ModelSerializer):
|
||||||
"trainer",
|
"trainer",
|
||||||
"start",
|
"start",
|
||||||
"end",
|
"end",
|
||||||
|
"circle_title",
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_start(self, obj):
|
def get_start(self, obj):
|
||||||
|
|
@ -29,6 +31,12 @@ class CourseSessionAttendanceCourseSerializer(serializers.ModelSerializer):
|
||||||
def get_end(self, obj):
|
def get_end(self, obj):
|
||||||
return obj.due_date.end
|
return obj.due_date.end
|
||||||
|
|
||||||
|
def get_circle_title(self, obj):
|
||||||
|
circle = obj.get_circle()
|
||||||
|
if circle:
|
||||||
|
return circle.title
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
class CourseSessionAssignmentSerializer(serializers.ModelSerializer):
|
class CourseSessionAssignmentSerializer(serializers.ModelSerializer):
|
||||||
submission_deadline_start = serializers.SerializerMethodField()
|
submission_deadline_start = serializers.SerializerMethodField()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue