wip: Add user certificate query, modify components

This commit is contained in:
Christian Cueni 2024-04-22 15:49:31 +02:00
parent 2318135f50
commit 073c2a8a60
10 changed files with 255 additions and 29 deletions

View File

@ -19,6 +19,7 @@ const documents = {
"\n query attendanceCheckQuery($courseSessionId: ID!) {\n course_session_attendance_course(id: $courseSessionId) {\n id\n attendance_user_list {\n user_id\n status\n }\n }\n }\n": types.AttendanceCheckQueryDocument,
"\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n solution_sample {\n id\n url\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n first_name\n last_name\n }\n assignment_user {\n avatar_url\n first_name\n last_name\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n": types.AssignmentCompletionQueryDocument,
"\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateQueryDocument,
"\n query competenceCertificateForUserQuery(\n $courseSlug: String!\n $courseSessionId: ID!\n $userId: UUID!\n ) {\n competence_certificate_list_for_user(course_slug: $courseSlug, user_id: $userId) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateForUserQueryDocument,
"\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n configuration {\n id\n enable_circle_documents\n enable_learning_mentor\n enable_competence_certificates\n }\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n": types.CourseSessionDetailDocument,
"\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n configuration {\n id\n enable_circle_documents\n enable_learning_mentor\n enable_competence_certificates\n is_uk\n }\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": types.CourseQueryDocument,
"\n query dashboardConfig {\n dashboard_config {\n id\n slug\n name\n dashboard_type\n course_configuration {\n id\n enable_circle_documents\n enable_learning_mentor\n enable_competence_certificates\n is_uk\n }\n }\n }\n": types.DashboardConfigDocument,
@ -67,6 +68,10 @@ export function graphql(source: "\n query assignmentCompletionQuery(\n $assi
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query competenceCertificateQuery($courseSlug: String!, $courseSessionId: ID!) {\n competence_certificate_list(course_slug: $courseSlug) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query competenceCertificateForUserQuery(\n $courseSlug: String!\n $courseSessionId: ID!\n $userId: UUID!\n ) {\n competence_certificate_list_for_user(course_slug: $courseSlug, user_id: $userId) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query competenceCertificateForUserQuery(\n $courseSlug: String!\n $courseSessionId: ID!\n $userId: UUID!\n ) {\n competence_certificate_list_for_user(course_slug: $courseSlug, user_id: $userId) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n completion(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

File diff suppressed because one or more lines are too long

View File

@ -21,6 +21,7 @@ type Query {
learning_content_document_list: LearningContentDocumentListObjectType
competence_certificate(id: ID, slug: String): CompetenceCertificateObjectType
competence_certificate_list(id: ID, slug: String, course_id: ID, course_slug: String): CompetenceCertificateListObjectType
competence_certificate_list_for_user(id: ID, slug: String, course_id: ID, course_slug: String, user_id: UUID): CompetenceCertificateListObjectType
assignment(id: ID, slug: String): AssignmentObjectType
assignment_completion(assignment_id: ID!, course_session_id: ID!, learning_content_page_id: ID, assignment_user_id: UUID): AssignmentCompletionObjectType
}

View File

@ -117,6 +117,42 @@ export const COMPETENCE_NAVI_CERTIFICATE_QUERY = graphql(`
}
`);
export const COMPETENCE_NAVI_CERTIFICATE_FOR_USER_QUERY = graphql(`
query competenceCertificateForUserQuery(
$courseSlug: String!
$courseSessionId: ID!
$userId: UUID!
) {
competence_certificate_list_for_user(course_slug: $courseSlug, user_id: $userId) {
...CoursePageFields
competence_certificates {
...CoursePageFields
assignments {
...CoursePageFields
assignment_type
max_points
completion(course_session_id: $courseSessionId) {
id
completion_status
submitted_at
evaluation_points
evaluation_max_points
evaluation_passed
}
learning_content {
...CoursePageFields
circle {
id
title
slug
}
}
}
}
}
}
`);
export const COURSE_SESSION_DETAIL_QUERY = graphql(`
query courseSessionDetail($courseSessionId: ID!) {
course_session(id: $courseSessionId) {

View File

@ -15,6 +15,7 @@ log.debug("CompetenceCertificateComponent setup");
const props = defineProps<{
competenceCertificate: CompetenceCertificate;
detailView: boolean;
frontendUrl?: string;
}>();
const totalPointsEvaluatedAssignments = computed(() => {
@ -40,6 +41,12 @@ const progressStatusCount = computed(() => {
props.competenceCertificate.assignments
);
});
const frontendUrl = computed(() => {
return props.frontendUrl
? props.frontendUrl
: props.competenceCertificate.frontend_url;
});
</script>
<template>
@ -90,7 +97,7 @@ const progressStatusCount = computed(() => {
<div v-if="!props.detailView">
<router-link
:to="competenceCertificate.frontend_url"
:to="frontendUrl"
class="btn-text mt-4 inline-flex items-center py-2 pl-0"
:data-cy="`certificate-${competenceCertificate.slug}-detail-link`"
>

View File

@ -1,6 +1,9 @@
<script setup lang="ts">
import log from "loglevel";
import { COMPETENCE_NAVI_CERTIFICATE_QUERY } from "@/graphql/queries";
import {
COMPETENCE_NAVI_CERTIFICATE_FOR_USER_QUERY,
COMPETENCE_NAVI_CERTIFICATE_QUERY,
} from "@/graphql/queries";
import { useQuery } from "@urql/vue";
import { computed, onMounted } from "vue";
import type { CompetenceCertificate } from "@/types";
@ -10,27 +13,45 @@ import {
assignmentsMaxEvaluationPoints,
assignmentsUserPoints,
} from "@/pages/competence/utils";
import { useRoute } from "vue-router";
const props = defineProps<{
courseSlug: string;
userId?: string;
}>();
log.debug("CompetenceCertificateListPage setup", props);
const courseSession = useCurrentCourseSession();
const route = useRoute();
const certificatesQuery = useQuery({
query: COMPETENCE_NAVI_CERTIFICATE_QUERY,
variables: {
courseSlug: props.courseSlug,
courseSessionId: courseSession.value.id,
},
});
const certificatesQuery = (() => {
if (props.userId) {
return useQuery({
query: COMPETENCE_NAVI_CERTIFICATE_FOR_USER_QUERY,
variables: {
courseSlug: props.courseSlug,
courseSessionId: courseSession.value.id,
userId: props.userId,
},
});
} else {
return useQuery({
query: COMPETENCE_NAVI_CERTIFICATE_QUERY,
variables: {
courseSlug: props.courseSlug,
courseSessionId: courseSession.value.id,
},
});
}
})();
const competenceCertificates = computed(() => {
const certificates = props.userId
? certificatesQuery.data?.value?.competence_certificate_list_for_user
: certificatesQuery.data?.value?.competence_certificate_list;
return (
(certificatesQuery.data.value?.competence_certificate_list
?.competence_certificates as unknown as CompetenceCertificate[]) ?? []
(certificates?.competence_certificates as unknown as CompetenceCertificate[]) ?? []
);
});
@ -52,6 +73,17 @@ const numAssignmentsEvaluated = computed(() => {
}).length;
});
const certificateFrontendUrl = function (frontendUrl: string) {
if (props.userId) {
const pathSegments = frontendUrl.split("/");
const lastSegment = pathSegments[pathSegments.length - 1];
// Assuming you want to navigate to the current path + last segment
return `${route.path}/${lastSegment}`;
}
return frontendUrl;
};
onMounted(async () => {
// log.debug("AssignmentView mounted", props.assignmentId, props.userId);
});
@ -92,6 +124,7 @@ onMounted(async () => {
<CompetenceCertificateComponent
:competence-certificate="competenceCertificate"
:detail-view="false"
:frontend-url="certificateFrontendUrl(competenceCertificate.frontend_url)"
></CompetenceCertificateComponent>
</div>
</div>

View File

@ -5,27 +5,36 @@ import SelfEvaluationAndFeedbackList from "@/components/selfEvaluationFeedback/S
import SelfEvaluationAndFeedbackOverview from "@/components/selfEvaluationFeedback/SelfEvaluationAndFeedbackOverview.vue";
import { useCurrentCourseSession } from "@/composables";
import CompetenceCertificateListPage from "@/pages/competence/CompetenceCertificateListPage.vue";
import CompetenceCertificateDetailPage from "@/pages/competence/CompetenceCertificateDetailPage.vue";
import { useRoute } from "vue-router";
type SubMenuType = "OVERVIEW" | "EVALUATIONS" | "COMPETENCES";
const subMenuOptions: SubMenuType[] = ["OVERVIEW", "EVALUATIONS", "COMPETENCES"];
type SubMenuType = "OVERVIEW" | "EVALUATIONS" | "CERTIFICATES" | "CERTIFICATE_DETAIL";
const subMenuOptions: SubMenuType[] = [
"OVERVIEW",
"EVALUATIONS",
"CERTIFICATES",
"CERTIFICATE_DETAIL",
];
const props = defineProps<{
userId: string;
courseSlug: string;
certificateSlug?: string;
}>();
interface SubMenuItem {
type: SubMenuType;
label: string;
url: string;
inMenu: boolean;
}
const MENU_ENTRIES: SubMenuItem[] = [
const SUBPAGES: SubMenuItem[] = [
{
type: "OVERVIEW",
label: "a.Übersicht",
url: `/course/${props.courseSlug}/profile/${props.userId}/competence`,
inMenu: true,
},
{
type: "EVALUATIONS",
@ -33,36 +42,52 @@ const MENU_ENTRIES: SubMenuItem[] = [
? "a.Selbst- und Fremdeinschätzungen"
: "a.Selbsteinschätzungen",
url: `/course/${props.courseSlug}/profile/${props.userId}/competence/evaluations`,
inMenu: true,
},
];
if (useCurrentCourseSession().value.course.configuration.is_uk) {
MENU_ENTRIES.push({
type: "COMPETENCES",
label: "Kompetenznachweise",
url: `/course/${props.courseSlug}/profile/${props.userId}/competence/competences`,
});
SUBPAGES.push(
{
type: "CERTIFICATES",
label: "Kompetenznachweise",
url: `/course/${props.courseSlug}/profile/${props.userId}/competence/certificates`,
inMenu: true,
},
{
type: "CERTIFICATE_DETAIL",
label: "",
url: "",
inMenu: false,
}
);
}
const route = useRoute();
const active = computed(() => {
const index = subMenuOptions.indexOf(route.meta?.subpage);
const index = subMenuOptions.findIndex((option) =>
option.startsWith(route.meta?.subpage)
);
if (index > -1) {
return MENU_ENTRIES[index];
return SUBPAGES[index];
}
return MENU_ENTRIES[0];
return SUBPAGES[0];
});
</script>
<template>
<CockpitProfileContent>
<template #side>
<div v-for="(entry, index) in MENU_ENTRIES" :key="index" class="mb-2">
<div
v-for="(entry, index) in SUBPAGES.filter((p) => p.inMenu)"
:key="index"
class="mb-2"
>
<router-link
v-if="active"
:to="entry.url"
class="flex w-full items-center space-x-2 p-2 pr-4 text-blue-900 hover:bg-gray-200 lg:pr-8"
:class="{ 'text-bold bg-gray-200': active.type === entry.type }"
:class="{ 'text-bold bg-gray-200': entry.type.startsWith(active.type) }"
>
<span>{{ $t(entry.label) }}</span>
</router-link>
@ -80,8 +105,15 @@ const active = computed(() => {
:profile-user-id="props.userId"
/>
<CompetenceCertificateListPage
v-else-if="active.type === 'COMPETENCES'"
v-else-if="active.type === 'CERTIFICATES'"
:course-slug="useCurrentCourseSession().value.course.slug"
:user-id="userId"
/>
<CompetenceCertificateDetailPage
v-else-if="active.type === 'CERTIFICATE_DETAIL'"
:course-slug="useCurrentCourseSession().value.course.slug"
:certificate-slug="certificateSlug ? certificateSlug : ''"
:user-id="userId"
/>
</div>
</template>

View File

@ -176,22 +176,31 @@ const router = createRouter({
name: "profileCompetence",
children: [
{
path: "", // Default path (e.g., /competence)
path: "",
name: "competenceMain",
meta: { subpage: "OVERVIEW" },
component: () => import("@/pages/userProfile/CompetenceProfilePage.vue"),
},
{
path: "evaluations", // Subpath (e.g., /competence/evaluations)
path: "evaluations",
name: "competenceEvaluations",
meta: { subpage: "EVALUATIONS" },
component: () => import("@/pages/userProfile/CompetenceProfilePage.vue"),
},
{
path: "competences", // Another subpath (e.g., /competence/competences)
path: "certificates",
name: "profileCompetences",
meta: { subpage: "COMPETENCES" },
meta: { subpage: "CERTIFICATES" },
component: () => import("@/pages/userProfile/CompetenceProfilePage.vue"),
children: [
{
path: ":certificateSlug",
props: true,
meta: { subpage: "CERTIFICATE_DETAIL" },
component: () =>
import("@/pages/userProfile/CompetenceProfilePage.vue"),
},
],
},
],
},

View File

@ -101,6 +101,10 @@ class AssignmentObjectType(DjangoObjectType):
lp = self.find_attached_learning_content()
if lp:
learning_content_page_id = lp.id
if not assignment_user_id:
assignment_user_id = getattr(info.context, "assignment_user_id", None)
return resolve_assignment_completion(
info=info,
course_session_id=course_session_id,

View File

@ -9,6 +9,8 @@ from vbv_lernwelt.competence.models import (
CompetenceCertificateList,
)
from vbv_lernwelt.course.graphql.types import resolve_course_page
from vbv_lernwelt.course.models import CourseSessionUser
from vbv_lernwelt.iam.permissions import can_view_profile
class CompetenceCertificateQuery(object):
@ -24,6 +26,15 @@ class CompetenceCertificateQuery(object):
course_slug=graphene.String(),
)
competence_certificate_list_for_user = graphene.Field(
CompetenceCertificateListObjectType,
id=graphene.ID(),
slug=graphene.String(),
course_id=graphene.ID(),
course_slug=graphene.String(),
user_id=graphene.UUID(),
)
def resolve_competence_certificate(root, info, id=None, slug=None):
return resolve_course_page(CompetenceCertificate, root, info, id=id, slug=slug)
@ -39,3 +50,26 @@ class CompetenceCertificateQuery(object):
course_id=course_id,
course_slug=course_slug,
)
def resolve_competence_certificate_list_for_user(
root, info, id=None, slug=None, course_id=None, course_slug=None, user_id=None
):
try:
course_session_user = CourseSessionUser.objects.get(user__id=user_id)
except CourseSessionUser.DoesNotExist:
return None
if not can_view_profile(info.context.user, course_session_user):
return None
setattr(info.context, "assignment_user_id", user_id)
return resolve_course_page(
CompetenceCertificateList,
root,
info,
id=id,
slug=slug,
course_id=course_id,
course_slug=course_slug,
)