Merged in feature/VBV-354-termine (pull request #155)

Feature/VBV-354 termine

Approved-by: Daniel Egger
This commit is contained in:
Lorenz Padberg 2023-07-12 08:19:12 +00:00
commit 7e8773cd17
78 changed files with 1853 additions and 353 deletions

View File

@ -0,0 +1,15 @@
<script lang="ts" setup>
import { getDateString, getTimeString } from "@/components/dueDates/dueDatesUtils";
import type { Dayjs } from "dayjs";
const props = defineProps<{
singleDate?: Dayjs;
}>();
</script>
<template>
<span>
<it-icon-calendar-light class="it-icon h-5 w-5" />
{{ getDateString(props.singleDate) }}, {{ getTimeString(props.singleDate) }}
</span>
</template>

View File

@ -0,0 +1,26 @@
<script lang="ts" setup>
import { formatDate } from "@/components/dueDates/dueDatesUtils";
import type { DueDate } from "@/types";
const props = defineProps<{
dueDate: DueDate;
}>();
</script>
<template>
<div class="flex items-center justify-between py-4">
<div class="space-y-1">
<a class="text-bold underline" :href="props.dueDate.url" target="_blank">
{{ props.dueDate.title }}
</a>
<p class="text-small text-gray-900">
{{ props.dueDate.learning_content_description }}
<span v-if="props.dueDate.description !== ''">:</span>
{{ props.dueDate.description }}
</p>
</div>
<p>
{{ formatDate(props.dueDate.start, props.dueDate.end) }}
</p>
</div>
</template>

View File

@ -0,0 +1,39 @@
<template>
<div>
<ul>
<li
v-for="dueDate in dueDatesDisplayed"
:key="dueDate.id"
:class="{ 'first:border-t': props.showTopBorder, 'border-b': true }"
>
<DueDateSingle :due-date="dueDate"></DueDateSingle>
</li>
</ul>
<div v-if="allDueDates.length > props.maxCount" class="flex items-center pt-6">
<a href="">{{ $t("dueDates.showAllDueDates") }}</a>
<it-icon-arrow-right />
</div>
<div v-if="allDueDates.length === 0">{{ $t("dueDates.noDueDatesAvailable") }}</div>
</div>
</template>
<script lang="ts" setup>
import DueDateSingle from "@/components/dueDates/DueDateSingle.vue";
import type { DueDate } from "@/types";
import { computed } from "vue";
const props = defineProps<{
maxCount: number;
dueDates: DueDate[];
showTopBorder: boolean;
}>();
const allDueDates = computed(() => {
return props.dueDates;
});
const dueDatesDisplayed = computed(() => {
return props.dueDates.slice(0, props.maxCount);
});
</script>

View File

@ -0,0 +1,22 @@
<template>
<div>
<DueDatesList
:due-dates="allDueDates"
:max-count="props.maxCount"
:show-top-border="props.showTopBorder"
></DueDatesList>
</div>
</template>
<script lang="ts" setup>
import DueDatesList from "@/components/dueDates/DueDatesList.vue";
import { useCurrentCourseSession } from "@/composables";
const props = defineProps<{
maxCount: number;
showTopBorder: boolean;
}>();
const courseSession = useCurrentCourseSession();
const allDueDates = courseSession.value.due_dates;
</script>

View File

@ -0,0 +1,60 @@
import dayjs from "dayjs";
export const dueDatesTestData = () => {
return [
{
id: 1,
start: dayjs("2023-06-14T15:00:00+02:00"),
end: dayjs("2023-06-14T18:00:00+02:00"),
title: "Präsenzkurs Kickoff",
url: "/course/überbetriebliche-kurse/learn/kickoff/präsenzkurs-kickoff",
course_session: 2,
page: 383,
},
{
id: 2,
start: dayjs("2023-06-15T15:00:00+02:00"),
end: dayjs("2023-06-15T18:00:00+02:00"),
title: "Präsenzkurs Basis",
url: "/course/überbetriebliche-kurse/learn/basis/präsenzkurs-basis",
course_session: 2,
page: 397,
},
{
id: 3,
start: dayjs("2023-06-16T15:00:00+02:00"),
end: dayjs("2023-06-16T18:00:00+02:00"),
title: "Präsenzkurs Fahrzeug",
url: "/course/überbetriebliche-kurse/learn/fahrzeug/präsenzkurs-fahrzeug",
course_session: 2,
page: 413,
},
{
id: 4,
start: dayjs("2023-06-16T15:00:00+02:00"),
end: dayjs("2023-06-16T18:00:00+02:00"),
title: "Präsenzkurs Flugzeuge",
url: "/course/überbetriebliche-kurse/learn/fahrzeug/präsenzkurs-fahrzeug",
course_session: 2,
page: 413,
},
{
id: 5,
start: dayjs("2023-07-16T11:00:00+02:00"),
end: dayjs("2023-07-16T18:00:00+02:00"),
title: "Präsenzkurs Motorräder",
url: "/course/überbetriebliche-kurse/learn/fahrzeug/präsenzkurs-fahrzeug",
course_session: 2,
page: 413,
},
{
id: 6,
start: dayjs("2023-08-09T15:00:00+02:00"),
end: dayjs("2023-08-09T19:00:00+02:00"),
title: "Präsenzkurs Fahrräder",
url: "/course/überbetriebliche-kurse/learn/fahrzeug/präsenzkurs-fahrzeug",
course_session: 2,
page: 413,
},
];
};

View File

@ -0,0 +1,51 @@
import type { Dayjs } from "dayjs";
export const formatDate = (start: Dayjs, end: Dayjs) => {
const startDateString = getDateString(start);
const endDateString = getDateString(end);
// if start isundefined, dont show the day twice
if (!start.isValid() && !end.isValid()) {
return "Termin nicht festgelegt";
}
if (!start || (!start.isValid() && end.isValid())) {
return `${endDateString} ${getTimeString(end)} ${end.format("[Uhr]")}`;
}
if (!end || (!end.isValid() && start.isValid())) {
return `${startDateString} ${getTimeString(start)} ${start.format("[Uhr]")}`;
}
// if start and end are on the same day, dont show the day twice
if (startDateString === endDateString) {
return `${startDateString} ${getTimeString(start)} - ${getTimeString(
end
)} ${end.format("[Uhr]")}`;
}
return `${startDateString} ${getTimeString(start)} - ${endDateString} ${getTimeString(
end
)}`;
};
export const getTimeString = (date?: Dayjs) => {
if (date) {
return `${date.format("H:mm")}`;
}
return "";
};
export const getDateString = (date?: Dayjs) => {
if (date) {
return `${date.format("D. MMMM YYYY")}`;
}
return "";
};
export const getWeekday = (date: Dayjs) => {
if (date) {
return `${date.format("dd")}`;
}
return "";
};

View File

@ -1,11 +1,13 @@
import type { ResultOf, TypedDocumentNode as DocumentNode, } from '@graphql-typed-document-node/core'; import type { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core';
import type { FragmentDefinitionNode } from 'graphql';
import type { Incremental } from './graphql';
export type FragmentType<TDocumentType extends DocumentNode<any, any>> = TDocumentType extends DocumentNode< export type FragmentType<TDocumentType extends DocumentTypeDecoration<any, any>> = TDocumentType extends DocumentTypeDecoration<
infer TType, infer TType,
any any
> >
? TType extends { ' $fragmentName'?: infer TKey } ? [TType] extends [{ ' $fragmentName'?: infer TKey }]
? TKey extends string ? TKey extends string
? { ' $fragmentRefs'?: { [key in TKey]: TType } } ? { ' $fragmentRefs'?: { [key in TKey]: TType } }
: never : never
@ -14,35 +16,51 @@ export type FragmentType<TDocumentType extends DocumentNode<any, any>> = TDocume
// return non-nullable if `fragmentType` is non-nullable // return non-nullable if `fragmentType` is non-nullable
export function useFragment<TType>( export function useFragment<TType>(
_documentNode: DocumentNode<TType, any>, _documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentNode<TType, any>> fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>
): TType; ): TType;
// return nullable if `fragmentType` is nullable // return nullable if `fragmentType` is nullable
export function useFragment<TType>( export function useFragment<TType>(
_documentNode: DocumentNode<TType, any>, _documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentNode<TType, any>> | null | undefined fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null | undefined
): TType | null | undefined; ): TType | null | undefined;
// return array of non-nullable if `fragmentType` is array of non-nullable // return array of non-nullable if `fragmentType` is array of non-nullable
export function useFragment<TType>( export function useFragment<TType>(
_documentNode: DocumentNode<TType, any>, _documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentNode<TType, any>>> fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
): ReadonlyArray<TType>; ): ReadonlyArray<TType>;
// return array of nullable if `fragmentType` is array of nullable // return array of nullable if `fragmentType` is array of nullable
export function useFragment<TType>( export function useFragment<TType>(
_documentNode: DocumentNode<TType, any>, _documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentNode<TType, any>>> | null | undefined fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): ReadonlyArray<TType> | null | undefined; ): ReadonlyArray<TType> | null | undefined;
export function useFragment<TType>( export function useFragment<TType>(
_documentNode: DocumentNode<TType, any>, _documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentNode<TType, any>> | ReadonlyArray<FragmentType<DocumentNode<TType, any>>> | null | undefined fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): TType | ReadonlyArray<TType> | null | undefined { ): TType | ReadonlyArray<TType> | null | undefined {
return fragmentType as any; return fragmentType as any;
} }
export function makeFragmentData< export function makeFragmentData<
F extends DocumentNode, F extends DocumentTypeDecoration<any, any>,
FT extends ResultOf<F> FT extends ResultOf<F>
>(data: FT, _fragment: F): FragmentType<F> { >(data: FT, _fragment: F): FragmentType<F> {
return data as FragmentType<F>; return data as FragmentType<F>;
} }
export function isFragmentReady<TQuery, TFrag>(
queryNode: DocumentTypeDecoration<TQuery, any>,
fragmentNode: TypedDocumentNode<TFrag>,
data: FragmentType<TypedDocumentNode<Incremental<TFrag>, any>> | null | undefined
): data is FragmentType<typeof fragmentNode> {
const deferredFields = (queryNode as { __meta__?: { deferredFields: Record<string, (keyof TFrag)[]> } }).__meta__
?.deferredFields;
if (!deferredFields) return true;
const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined;
const fragName = fragDef?.name?.value;
const fields = (fragName && deferredFields[fragName]) || [];
return fields.length > 0 && fields.every(field => data && field in data);
}

View File

@ -10,7 +10,7 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-
* 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle. * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
* 3. It does not support dead code elimination, so it will add unused operations. * 3. It does not support dead code elimination, so it will add unused operations.
* *
* Therefore it is highly recommended to use the babel-plugin for production. * Therefore it is highly recommended to use the babel or swc plugin for production.
*/ */
const documents = { const documents = {
"\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n send_feedback(input: $input) {\n feedback_response {\n id\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument, "\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n send_feedback(input: $input) {\n feedback_response {\n id\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument,
@ -25,7 +25,7 @@ const documents = {
* *
* @example * @example
* ```ts * ```ts
* const query = gql(`query GetUser($id: ID!) { user(id: $id) { name } }`); * const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`);
* ``` * ```
* *
* The query argument is unknown! * The query argument is unknown!

View File

@ -5,33 +5,35 @@ export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] }; export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> }; export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> }; export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = { [_ in K]?: never };
export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
/** All built-in and custom scalars, mapped to their actual values */ /** All built-in and custom scalars, mapped to their actual values */
export type Scalars = { export type Scalars = {
ID: string; ID: { input: string; output: string; }
String: string; String: { input: string; output: string; }
Boolean: boolean; Boolean: { input: boolean; output: boolean; }
Int: number; Int: { input: number; output: number; }
Float: number; Float: { input: number; output: number; }
/** /**
* The `DateTime` scalar type represents a DateTime * The `DateTime` scalar type represents a DateTime
* value as specified by * value as specified by
* [iso8601](https://en.wikipedia.org/wiki/ISO_8601). * [iso8601](https://en.wikipedia.org/wiki/ISO_8601).
*/ */
DateTime: any; DateTime: { input: any; output: any; }
/** /**
* The `GenericScalar` scalar type represents a generic * The `GenericScalar` scalar type represents a generic
* GraphQL scalar value that could be: * GraphQL scalar value that could be:
* String, Boolean, Int, Float, List or Object. * String, Boolean, Int, Float, List or Object.
*/ */
GenericScalar: any; GenericScalar: { input: any; output: any; }
JSONStreamField: any; JSONStreamField: { input: any; output: any; }
/** /**
* Allows use of a JSON String for input / output from the GraphQL schema. * Allows use of a JSON String for input / output from the GraphQL schema.
* *
* Use of this type is *not recommended* as you lose the benefits of having a defined, static * Use of this type is *not recommended* as you lose the benefits of having a defined, static
* schema (one of the key benefits of GraphQL). * schema (one of the key benefits of GraphQL).
*/ */
JSONString: any; JSONString: { input: any; output: any; }
}; };
/** An enumeration. */ /** An enumeration. */
@ -61,19 +63,19 @@ export type AssignmentCompletionMutation = {
export type AssignmentCompletionObjectType = { export type AssignmentCompletionObjectType = {
__typename?: 'AssignmentCompletionObjectType'; __typename?: 'AssignmentCompletionObjectType';
additional_json_data: Scalars['JSONString']; additional_json_data: Scalars['JSONString']['output'];
assignment: AssignmentObjectType; assignment: AssignmentObjectType;
assignment_user: UserType; assignment_user: UserType;
completion_data?: Maybe<Scalars['GenericScalar']>; completion_data?: Maybe<Scalars['GenericScalar']['output']>;
completion_status: AssignmentAssignmentCompletionCompletionStatusChoices; completion_status: AssignmentAssignmentCompletionCompletionStatusChoices;
created_at: Scalars['DateTime']; created_at: Scalars['DateTime']['output'];
evaluation_grade?: Maybe<Scalars['Float']>; evaluation_grade?: Maybe<Scalars['Float']['output']>;
evaluation_points?: Maybe<Scalars['Float']>; evaluation_points?: Maybe<Scalars['Float']['output']>;
evaluation_submitted_at?: Maybe<Scalars['DateTime']>; evaluation_submitted_at?: Maybe<Scalars['DateTime']['output']>;
evaluation_user?: Maybe<UserType>; evaluation_user?: Maybe<UserType>;
id: Scalars['ID']; id: Scalars['ID']['output'];
submitted_at?: Maybe<Scalars['DateTime']>; submitted_at?: Maybe<Scalars['DateTime']['output']>;
updated_at: Scalars['DateTime']; updated_at: Scalars['DateTime']['output'];
}; };
/** An enumeration. */ /** An enumeration. */
@ -86,24 +88,24 @@ export type AssignmentCompletionStatus =
export type AssignmentObjectType = CoursePageInterface & { export type AssignmentObjectType = CoursePageInterface & {
__typename?: 'AssignmentObjectType'; __typename?: 'AssignmentObjectType';
assignment_type: AssignmentAssignmentAssignmentTypeChoices; assignment_type: AssignmentAssignmentAssignmentTypeChoices;
content_type?: Maybe<Scalars['String']>; content_type?: Maybe<Scalars['String']['output']>;
/** Zeitaufwand als Text */ /** Zeitaufwand als Text */
effort_required: Scalars['String']; effort_required: Scalars['String']['output'];
/** Beschreibung der Bewertung */ /** Beschreibung der Bewertung */
evaluation_description: Scalars['String']; evaluation_description: Scalars['String']['output'];
/** URL zum Beurteilungsinstrument */ /** URL zum Beurteilungsinstrument */
evaluation_document_url: Scalars['String']; evaluation_document_url: Scalars['String']['output'];
evaluation_tasks?: Maybe<Scalars['JSONStreamField']>; evaluation_tasks?: Maybe<Scalars['JSONStreamField']['output']>;
frontend_url?: Maybe<Scalars['String']>; frontend_url?: Maybe<Scalars['String']['output']>;
id?: Maybe<Scalars['ID']>; id?: Maybe<Scalars['ID']['output']>;
/** Erläuterung der Ausgangslage */ /** Erläuterung der Ausgangslage */
intro_text: Scalars['String']; intro_text: Scalars['String']['output'];
live?: Maybe<Scalars['Boolean']>; live?: Maybe<Scalars['Boolean']['output']>;
performance_objectives?: Maybe<Scalars['JSONStreamField']>; performance_objectives?: Maybe<Scalars['JSONStreamField']['output']>;
slug?: Maybe<Scalars['String']>; slug?: Maybe<Scalars['String']['output']>;
tasks?: Maybe<Scalars['JSONStreamField']>; tasks?: Maybe<Scalars['JSONStreamField']['output']>;
title?: Maybe<Scalars['String']>; title?: Maybe<Scalars['String']['output']>;
translation_key?: Maybe<Scalars['String']>; translation_key?: Maybe<Scalars['String']['output']>;
}; };
/** An enumeration. */ /** An enumeration. */
@ -116,69 +118,69 @@ export type CoreUserLanguageChoices =
| 'IT'; | 'IT';
export type CoursePageInterface = { export type CoursePageInterface = {
content_type?: Maybe<Scalars['String']>; content_type?: Maybe<Scalars['String']['output']>;
frontend_url?: Maybe<Scalars['String']>; frontend_url?: Maybe<Scalars['String']['output']>;
id?: Maybe<Scalars['ID']>; id?: Maybe<Scalars['ID']['output']>;
live?: Maybe<Scalars['Boolean']>; live?: Maybe<Scalars['Boolean']['output']>;
slug?: Maybe<Scalars['String']>; slug?: Maybe<Scalars['String']['output']>;
title?: Maybe<Scalars['String']>; title?: Maybe<Scalars['String']['output']>;
translation_key?: Maybe<Scalars['String']>; translation_key?: Maybe<Scalars['String']['output']>;
}; };
export type CourseType = { export type CourseType = {
__typename?: 'CourseType'; __typename?: 'CourseType';
category_name: Scalars['String']; category_name: Scalars['String']['output'];
id: Scalars['ID']; id: Scalars['ID']['output'];
learning_path?: Maybe<LearningPathType>; learning_path?: Maybe<LearningPathType>;
slug: Scalars['String']; slug: Scalars['String']['output'];
title: Scalars['String']; title: Scalars['String']['output'];
}; };
export type ErrorType = { export type ErrorType = {
__typename?: 'ErrorType'; __typename?: 'ErrorType';
field: Scalars['String']; field: Scalars['String']['output'];
messages: Array<Scalars['String']>; messages: Array<Scalars['String']['output']>;
}; };
export type FeedbackResponse = Node & { export type FeedbackResponse = Node & {
__typename?: 'FeedbackResponse'; __typename?: 'FeedbackResponse';
created_at: Scalars['DateTime']; created_at: Scalars['DateTime']['output'];
data?: Maybe<Scalars['GenericScalar']>; data?: Maybe<Scalars['GenericScalar']['output']>;
/** The ID of the object */ /** The ID of the object */
id: Scalars['ID']; id: Scalars['ID']['output'];
}; };
export type LearningPathType = CoursePageInterface & { export type LearningPathType = CoursePageInterface & {
__typename?: 'LearningPathType'; __typename?: 'LearningPathType';
content_type?: Maybe<Scalars['String']>; content_type?: Maybe<Scalars['String']['output']>;
depth: Scalars['Int']; depth: Scalars['Int']['output'];
draft_title: Scalars['String']; draft_title: Scalars['String']['output'];
expire_at?: Maybe<Scalars['DateTime']>; expire_at?: Maybe<Scalars['DateTime']['output']>;
expired: Scalars['Boolean']; expired: Scalars['Boolean']['output'];
first_published_at?: Maybe<Scalars['DateTime']>; first_published_at?: Maybe<Scalars['DateTime']['output']>;
frontend_url?: Maybe<Scalars['String']>; frontend_url?: Maybe<Scalars['String']['output']>;
go_live_at?: Maybe<Scalars['DateTime']>; go_live_at?: Maybe<Scalars['DateTime']['output']>;
has_unpublished_changes: Scalars['Boolean']; has_unpublished_changes: Scalars['Boolean']['output'];
id?: Maybe<Scalars['ID']>; id?: Maybe<Scalars['ID']['output']>;
last_published_at?: Maybe<Scalars['DateTime']>; last_published_at?: Maybe<Scalars['DateTime']['output']>;
latest_revision_created_at?: Maybe<Scalars['DateTime']>; latest_revision_created_at?: Maybe<Scalars['DateTime']['output']>;
live?: Maybe<Scalars['Boolean']>; live?: Maybe<Scalars['Boolean']['output']>;
locked: Scalars['Boolean']; locked: Scalars['Boolean']['output'];
locked_at?: Maybe<Scalars['DateTime']>; locked_at?: Maybe<Scalars['DateTime']['output']>;
locked_by?: Maybe<UserType>; locked_by?: Maybe<UserType>;
numchild: Scalars['Int']; numchild: Scalars['Int']['output'];
owner?: Maybe<UserType>; owner?: Maybe<UserType>;
path: Scalars['String']; path: Scalars['String']['output'];
/** Die informative Beschreibung, dargestellt in Suchmaschinen-Ergebnissen unter der Überschrift. */ /** Die informative Beschreibung, dargestellt in Suchmaschinen-Ergebnissen unter der Überschrift. */
search_description: Scalars['String']; search_description: Scalars['String']['output'];
/** Der Titel der Seite, dargestellt in Suchmaschinen-Ergebnissen als die verlinkte Überschrift. */ /** Der Titel der Seite, dargestellt in Suchmaschinen-Ergebnissen als die verlinkte Überschrift. */
seo_title: Scalars['String']; seo_title: Scalars['String']['output'];
/** Ob ein Link zu dieser Seite in automatisch generierten Menüs auftaucht. */ /** Ob ein Link zu dieser Seite in automatisch generierten Menüs auftaucht. */
show_in_menus: Scalars['Boolean']; show_in_menus: Scalars['Boolean']['output'];
slug?: Maybe<Scalars['String']>; slug?: Maybe<Scalars['String']['output']>;
title?: Maybe<Scalars['String']>; title?: Maybe<Scalars['String']['output']>;
translation_key?: Maybe<Scalars['String']>; translation_key?: Maybe<Scalars['String']['output']>;
url_path: Scalars['String']; url_path: Scalars['String']['output'];
}; };
export type Mutation = { export type Mutation = {
@ -194,19 +196,19 @@ export type MutationSendFeedbackArgs = {
export type MutationUpsertAssignmentCompletionArgs = { export type MutationUpsertAssignmentCompletionArgs = {
assignment_id: Scalars['ID']; assignment_id: Scalars['ID']['input'];
assignment_user_id?: InputMaybe<Scalars['ID']>; assignment_user_id?: InputMaybe<Scalars['ID']['input']>;
completion_data_string?: InputMaybe<Scalars['String']>; completion_data_string?: InputMaybe<Scalars['String']['input']>;
completion_status?: InputMaybe<AssignmentCompletionStatus>; completion_status?: InputMaybe<AssignmentCompletionStatus>;
course_session_id: Scalars['ID']; course_session_id: Scalars['ID']['input'];
evaluation_grade?: InputMaybe<Scalars['Float']>; evaluation_grade?: InputMaybe<Scalars['Float']['input']>;
evaluation_points?: InputMaybe<Scalars['Float']>; evaluation_points?: InputMaybe<Scalars['Float']['input']>;
}; };
/** An object with an ID */ /** An object with an ID */
export type Node = { export type Node = {
/** The ID of the object */ /** The ID of the object */
id: Scalars['ID']; id: Scalars['ID']['output'];
}; };
export type Query = { export type Query = {
@ -218,32 +220,32 @@ export type Query = {
export type QueryAssignmentArgs = { export type QueryAssignmentArgs = {
id?: InputMaybe<Scalars['ID']>; id?: InputMaybe<Scalars['ID']['input']>;
slug?: InputMaybe<Scalars['String']>; slug?: InputMaybe<Scalars['String']['input']>;
}; };
export type QueryAssignmentCompletionArgs = { export type QueryAssignmentCompletionArgs = {
assignment_id: Scalars['ID']; assignment_id: Scalars['ID']['input'];
assignment_user_id?: InputMaybe<Scalars['ID']>; assignment_user_id?: InputMaybe<Scalars['ID']['input']>;
course_session_id: Scalars['ID']; course_session_id: Scalars['ID']['input'];
}; };
export type QueryCourseArgs = { export type QueryCourseArgs = {
id?: InputMaybe<Scalars['Int']>; id?: InputMaybe<Scalars['Int']['input']>;
}; };
export type SendFeedbackInput = { export type SendFeedbackInput = {
clientMutationId?: InputMaybe<Scalars['String']>; clientMutationId?: InputMaybe<Scalars['String']['input']>;
course_session: Scalars['Int']; course_session: Scalars['Int']['input'];
data?: InputMaybe<Scalars['GenericScalar']>; data?: InputMaybe<Scalars['GenericScalar']['input']>;
page: Scalars['String']; page: Scalars['String']['input'];
}; };
export type SendFeedbackPayload = { export type SendFeedbackPayload = {
__typename?: 'SendFeedbackPayload'; __typename?: 'SendFeedbackPayload';
clientMutationId?: Maybe<Scalars['String']>; clientMutationId?: Maybe<Scalars['String']['output']>;
/** May contain more than one error for same field. */ /** May contain more than one error for same field. */
errors?: Maybe<Array<Maybe<ErrorType>>>; errors?: Maybe<Array<Maybe<ErrorType>>>;
feedback_response?: Maybe<FeedbackResponse>; feedback_response?: Maybe<FeedbackResponse>;
@ -251,14 +253,14 @@ export type SendFeedbackPayload = {
export type UserType = { export type UserType = {
__typename?: 'UserType'; __typename?: 'UserType';
avatar_url: Scalars['String']; avatar_url: Scalars['String']['output'];
email: Scalars['String']; email: Scalars['String']['output'];
first_name: Scalars['String']; first_name: Scalars['String']['output'];
id: Scalars['ID']; id: Scalars['ID']['output'];
language: CoreUserLanguageChoices; language: CoreUserLanguageChoices;
last_name: Scalars['String']; last_name: Scalars['String']['output'];
/** Erforderlich. 150 Zeichen oder weniger. Nur Buchstaben, Ziffern und @/./+/-/_. */ /** Erforderlich. 150 Zeichen oder weniger. Nur Buchstaben, Ziffern und @/./+/-/_. */
username: Scalars['String']; username: Scalars['String']['output'];
}; };
export type SendFeedbackMutationMutationVariables = Exact<{ export type SendFeedbackMutationMutationVariables = Exact<{
@ -269,29 +271,29 @@ export type SendFeedbackMutationMutationVariables = Exact<{
export type SendFeedbackMutationMutation = { __typename?: 'Mutation', send_feedback?: { __typename?: 'SendFeedbackPayload', feedback_response?: { __typename?: 'FeedbackResponse', id: string } | null, errors?: Array<{ __typename?: 'ErrorType', field: string, messages: Array<string> } | null> | null } | null }; export type SendFeedbackMutationMutation = { __typename?: 'Mutation', send_feedback?: { __typename?: 'SendFeedbackPayload', feedback_response?: { __typename?: 'FeedbackResponse', id: string } | null, errors?: Array<{ __typename?: 'ErrorType', field: string, messages: Array<string> } | null> | null } | null };
export type UpsertAssignmentCompletionMutationVariables = Exact<{ export type UpsertAssignmentCompletionMutationVariables = Exact<{
assignmentId: Scalars['ID']; assignmentId: Scalars['ID']['input'];
courseSessionId: Scalars['ID']; courseSessionId: Scalars['ID']['input'];
assignmentUserId?: InputMaybe<Scalars['ID']>; assignmentUserId?: InputMaybe<Scalars['ID']['input']>;
completionStatus: AssignmentCompletionStatus; completionStatus: AssignmentCompletionStatus;
completionDataString: Scalars['String']; completionDataString: Scalars['String']['input'];
evaluationGrade?: InputMaybe<Scalars['Float']>; evaluationGrade?: InputMaybe<Scalars['Float']['input']>;
evaluationPoints?: InputMaybe<Scalars['Float']>; evaluationPoints?: InputMaybe<Scalars['Float']['input']>;
}>; }>;
export type UpsertAssignmentCompletionMutation = { __typename?: 'Mutation', upsert_assignment_completion?: { __typename?: 'AssignmentCompletionMutation', assignment_completion?: { __typename?: 'AssignmentCompletionObjectType', id: string, completion_status: AssignmentAssignmentCompletionCompletionStatusChoices, submitted_at?: any | null, evaluation_submitted_at?: any | null, evaluation_grade?: number | null, evaluation_points?: number | null, completion_data?: any | null } | null } | null }; export type UpsertAssignmentCompletionMutation = { __typename?: 'Mutation', upsert_assignment_completion?: { __typename?: 'AssignmentCompletionMutation', assignment_completion?: { __typename?: 'AssignmentCompletionObjectType', id: string, completion_status: AssignmentAssignmentCompletionCompletionStatusChoices, submitted_at?: any | null, evaluation_submitted_at?: any | null, evaluation_grade?: number | null, evaluation_points?: number | null, completion_data?: any | null } | null } | null };
export type AssignmentCompletionQueryQueryVariables = Exact<{ export type AssignmentCompletionQueryQueryVariables = Exact<{
assignmentId: Scalars['ID']; assignmentId: Scalars['ID']['input'];
courseSessionId: Scalars['ID']; courseSessionId: Scalars['ID']['input'];
assignmentUserId?: InputMaybe<Scalars['ID']>; assignmentUserId?: InputMaybe<Scalars['ID']['input']>;
}>; }>;
export type AssignmentCompletionQueryQuery = { __typename?: 'Query', assignment?: { __typename?: 'AssignmentObjectType', assignment_type: AssignmentAssignmentAssignmentTypeChoices, content_type?: string | null, effort_required: string, evaluation_description: string, evaluation_document_url: string, evaluation_tasks?: any | null, id?: string | null, intro_text: string, performance_objectives?: any | null, slug?: string | null, tasks?: any | null, title?: string | null, translation_key?: string | null } | null, assignment_completion?: { __typename?: 'AssignmentCompletionObjectType', id: string, completion_status: AssignmentAssignmentCompletionCompletionStatusChoices, submitted_at?: any | null, evaluation_submitted_at?: any | null, evaluation_grade?: number | null, evaluation_points?: number | null, completion_data?: any | null, evaluation_user?: { __typename?: 'UserType', id: string } | null, assignment_user: { __typename?: 'UserType', id: string } } | null }; export type AssignmentCompletionQueryQuery = { __typename?: 'Query', assignment?: { __typename?: 'AssignmentObjectType', assignment_type: AssignmentAssignmentAssignmentTypeChoices, content_type?: string | null, effort_required: string, evaluation_description: string, evaluation_document_url: string, evaluation_tasks?: any | null, id?: string | null, intro_text: string, performance_objectives?: any | null, slug?: string | null, tasks?: any | null, title?: string | null, translation_key?: string | null } | null, assignment_completion?: { __typename?: 'AssignmentCompletionObjectType', id: string, completion_status: AssignmentAssignmentCompletionCompletionStatusChoices, submitted_at?: any | null, evaluation_submitted_at?: any | null, evaluation_grade?: number | null, evaluation_points?: number | null, completion_data?: any | null, evaluation_user?: { __typename?: 'UserType', id: string } | null, assignment_user: { __typename?: 'UserType', id: string } } | null };
export type CourseQueryQueryVariables = Exact<{ export type CourseQueryQueryVariables = Exact<{
courseId: Scalars['Int']; courseId: Scalars['Int']['input'];
}>; }>;

View File

@ -11,9 +11,9 @@
"assignmentSubmitted": "Du hast deine Ergebnisse erfolgreich abgegeben.", "assignmentSubmitted": "Du hast deine Ergebnisse erfolgreich abgegeben.",
"confirmSubmitPerson": "Hiermit bestätige ich, dass die folgende Person meine Ergebnisse bewerten soll.", "confirmSubmitPerson": "Hiermit bestätige ich, dass die folgende Person meine Ergebnisse bewerten soll.",
"confirmSubmitResults": "Hiermit bestätige ich, dass ich die Zusammenfassung meiner Ergebnisse überprüft habe und so abgeben will.", "confirmSubmitResults": "Hiermit bestätige ich, dass ich die Zusammenfassung meiner Ergebnisse überprüft habe und so abgeben will.",
"dueDateIntroduction": "Reiche deine Ergebnisse pünktlich ein bis am {{date}} um {{time}} Uhr ein.", "dueDateIntroduction": "Reiche deine Ergebnisse pünktlich ein bis am: ",
"dueDateNotSet": "Keine Abgabedaten wurden erfasst für diese Durchführung", "dueDateNotSet": "Keine Abgabedaten wurden erfasst für diese Durchführung",
"dueDateSubmission": "Einreichungstermin: {{date}}", "dueDateSubmission": "Einreichungstermin:",
"dueDateTitle": "Abgabetermin", "dueDateTitle": "Abgabetermin",
"edit": "Bearbeiten", "edit": "Bearbeiten",
"effortTitle": "Zeitaufwand", "effortTitle": "Zeitaufwand",
@ -85,11 +85,14 @@
}, },
"dashboard": { "dashboard": {
"courses": "Lehrgang", "courses": "Lehrgang",
"dueDatesTitle": "Termine",
"nocourses": "Du wurdest noch keinem Lehrgang zugewiesen.", "nocourses": "Du wurdest noch keinem Lehrgang zugewiesen.",
"welcome": "Willkommen, {{name}}" "welcome": "Willkommen, {{name}}"
}, },
"dueDates": { "dueDates": {
"nextDueDates": "Nächste Termine" "nextDueDates": "Nächste Termine",
"noDueDatesAvailable": "Keine Termine vorhanden",
"showAllDueDates": "Alle Termine anzeigen"
}, },
"feedback": { "feedback": {
"answers": "Antworten", "answers": "Antworten",
@ -185,6 +188,7 @@
"learningPathPage": { "learningPathPage": {
"currentCircle": "Aktueller Circle", "currentCircle": "Aktueller Circle",
"listView": "Listenansicht", "listView": "Listenansicht",
"nextDueDates": "Nächste Termine",
"nextStep": "Nächster Schritt", "nextStep": "Nächster Schritt",
"pathView": "Pfadansicht", "pathView": "Pfadansicht",
"progressText": "Du hast {{ inProgressCount }} von {{ allCount }} Circles bearbeitet", "progressText": "Du hast {{ inProgressCount }} von {{ allCount }} Circles bearbeitet",

View File

@ -11,9 +11,9 @@
"assignmentSubmitted": "Tes résultats ont bien été transmis.", "assignmentSubmitted": "Tes résultats ont bien été transmis.",
"confirmSubmitPerson": "Par la présente, je confirme que la personne suivante doit évaluer mes résultats.", "confirmSubmitPerson": "Par la présente, je confirme que la personne suivante doit évaluer mes résultats.",
"confirmSubmitResults": "Par la présente, je confirme que jai vérifié la synthèse de mes résultats et que je souhaite la remettre telle quelle.", "confirmSubmitResults": "Par la présente, je confirme que jai vérifié la synthèse de mes résultats et que je souhaite la remettre telle quelle.",
"dueDateIntroduction": "Envoie tes résultats dans les délais avant le {{date}} à {{time}} heures.", "dueDateIntroduction": "Envoie tes résultats dans les délais avant le:",
"dueDateNotSet": "Aucune date de remise na été spécifiée pour cette opération.", "dueDateNotSet": "Aucune date de remise na été spécifiée pour cette opération.",
"dueDateSubmission": "Date de clôture : {{date}}", "dueDateSubmission": "Date de clôture: ",
"dueDateTitle": "Date de remise", "dueDateTitle": "Date de remise",
"edit": "Traiter", "edit": "Traiter",
"effortTitle": "Temps nécessaire", "effortTitle": "Temps nécessaire",

View File

@ -11,9 +11,9 @@
"assignmentSubmitted": "I tuoi risultati sono stati consegnati con successo.", "assignmentSubmitted": "I tuoi risultati sono stati consegnati con successo.",
"confirmSubmitPerson": "Confermo che i miei risultati dovranno essere valutati dalla seguente persona.", "confirmSubmitPerson": "Confermo che i miei risultati dovranno essere valutati dalla seguente persona.",
"confirmSubmitResults": "Confermo di aver controllato il riepilogo dei miei risultati e di volerli consegnare.", "confirmSubmitResults": "Confermo di aver controllato il riepilogo dei miei risultati e di volerli consegnare.",
"dueDateIntroduction": "Presenta i tuoi risultati entro il {{date}} alle {{time}}.", "dueDateIntroduction": "Presenta i tuoi risultati entro il:",
"dueDateNotSet": "Non sono stati registrati dati di consegna per questo svolgimento", "dueDateNotSet": "Non sono stati registrati dati di consegna per questo svolgimento",
"dueDateSubmission": "Termine di presentazione: {{date}}", "dueDateSubmission": "Termine di presentazione: ",
"dueDateTitle": "Termine di consegna", "dueDateTitle": "Termine di consegna",
"edit": "Modificare", "edit": "Modificare",
"effortTitle": "Tempo richiesto", "effortTitle": "Tempo richiesto",

View File

@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import DueDatesList from "@/components/dueDates/DueDatesList.vue";
import LearningPathDiagramSmall from "@/components/learningPath/LearningPathDiagramSmall.vue"; import LearningPathDiagramSmall from "@/components/learningPath/LearningPathDiagramSmall.vue";
import { useCourseSessionsStore } from "@/stores/courseSessions"; import { useCourseSessionsStore } from "@/stores/courseSessions";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
@ -15,6 +16,7 @@ onMounted(async () => {
log.debug("DashboardPage mounted"); log.debug("DashboardPage mounted");
}); });
const allDueDates = courseSessionsStore.allDueDates();
const getNextStepLink = (courseSession: CourseSession) => { const getNextStepLink = (courseSession: CourseSession) => {
return computed(() => { return computed(() => {
if (courseSessionsStore.hasCockpit(courseSession)) { if (courseSessionsStore.hasCockpit(courseSession)) {
@ -69,22 +71,13 @@ const getNextStepLink = (courseSession: CourseSession) => {
<p>{{ $t("dashboard.nocourses") }}</p> <p>{{ $t("dashboard.nocourses") }}</p>
</div> </div>
<div> <div>
<h3 class="mb-6">Termine</h3> <h3 class="mb-6">{{ $t("dashboard.dueDatesTitle") }}</h3>
<ul class="bg-white p-4"> <DueDatesList
<li class="flex flex-row py-4"> class="bg-white p-6"
{{ $t("Zur Zeit sind keine Termine vorhanden") }} :due-dates="allDueDates"
</li> :max-count="10"
<!-- li class="flex flex-row border-b py-4"> :show-top-border="false"
<p class="text-bold w-60">Austausch mit Trainer</p> ></DueDatesList>
<p class="grow">Fr, 24. November 2022, 11 Uhr</p>
<p class="underline">Details anschauen</p>
</li>
<li class="flex flex-row py-4">
<p class="text-bold w-60">Vernetzen - Live Session</p>
<p class="grow">Di, 4. Dezember 2022, 10.30 Uhr</p>
<p class="underline">Details anschauen</p>
</li -->
</ul>
</div> </div>
</div> </div>
</main> </main>

View File

@ -216,6 +216,15 @@ function log(data: any) {
ls-end ls-end
<it-icon-ls-end /> <it-icon-ls-end />
</div> </div>
<div class="inline-flex flex-col">
calendar
<it-icon-calendar />
</div>
<div class="inline-flex flex-col">
calendar-light
<it-icon-calenda-light />
</div>
</div> </div>
<div class="mb-8 mt-8 flex flex-col flex-wrap gap-4 lg:flex-row"> <div class="mb-8 mt-8 flex flex-col flex-wrap gap-4 lg:flex-row">

View File

@ -6,7 +6,7 @@ import AssignmentSubmissionResponses from "@/pages/learningPath/learningContentP
import type { import type {
Assignment, Assignment,
AssignmentCompletion, AssignmentCompletion,
CourseSessionAssignmentDetails, CourseSessionAssignment,
CourseSessionUser, CourseSessionUser,
} from "@/types"; } from "@/types";
import { useQuery } from "@urql/vue"; import { useQuery } from "@urql/vue";
@ -23,12 +23,12 @@ const props = defineProps<{
log.debug("AssignmentEvaluationPage created", props.assignmentId, props.userId); log.debug("AssignmentEvaluationPage created", props.assignmentId, props.userId);
interface StateInterface { interface StateInterface {
courseSessionAssignmentDetails: CourseSessionAssignmentDetails | undefined; courseSessionAssignment: CourseSessionAssignment | undefined;
assignmentUser: CourseSessionUser | undefined; assignmentUser: CourseSessionUser | undefined;
} }
const state: StateInterface = reactive({ const state: StateInterface = reactive({
courseSessionAssignmentDetails: undefined, courseSessionAssignment: undefined,
assignmentUser: undefined, assignmentUser: undefined,
}); });

View File

@ -61,7 +61,7 @@ function editTask(task: AssignmentEvaluationTask) {
const assignmentDetail = computed(() => findAssignmentDetail(props.assignment.id)); const assignmentDetail = computed(() => findAssignmentDetail(props.assignment.id));
const dueDate = computed(() => const dueDate = computed(() =>
dayjs(assignmentDetail.value?.evaluationDeadlineDateTimeUtc) dayjs(assignmentDetail.value?.evaluation_deadline_start)
); );
const inEvaluationTask = computed( const inEvaluationTask = computed(

View File

@ -56,12 +56,12 @@ const assignmentDetail = computed(() =>
<div v-if="assignmentDetail"> <div v-if="assignmentDetail">
<span> <span>
Abgabetermin: Abgabetermin:
{{ dayjs(assignmentDetail.submissionDeadlineDateTimeUtc).format("DD.MM.YYYY") }} {{ dayjs(assignmentDetail.submission_deadline_start).format("DD.MM.YYYY") }}
</span> </span>
- -
<span> <span>
Freigabetermin: Freigabetermin:
{{ dayjs(assignmentDetail.evaluationDeadlineDateTimeUtc).format("DD.MM.YYYY") }} {{ dayjs(assignmentDetail.evaluation_deadline_start).format("DD.MM.YYYY") }}
</span> </span>
</div> </div>

View File

@ -1,7 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import DateEmbedding from "@/components/dueDates/DateEmbedding.vue";
import type { Assignment } from "@/types"; import type { Assignment } from "@/types";
import { useRouteQuery } from "@vueuse/router"; import { useRouteQuery } from "@vueuse/router";
import type { Dayjs } from "dayjs"; import type { Dayjs } from "dayjs";
import log from "loglevel";
interface Props { interface Props {
assignment: Assignment; assignment: Assignment;
@ -12,6 +14,10 @@ const props = withDefaults(defineProps<Props>(), {
dueDate: undefined, dueDate: undefined,
}); });
log.debug("AssignmentIntroductionView created", props.assignment, props.dueDate);
// TODO: Test if submission deadline is set correctly, and evaluation_deadline is set.
const step = useRouteQuery("step"); const step = useRouteQuery("step");
</script> </script>
@ -37,12 +43,8 @@ const step = useRouteQuery("step");
<h3 class="mb-4 mt-8">{{ $t("assignment.dueDateTitle") }}</h3> <h3 class="mb-4 mt-8">{{ $t("assignment.dueDateTitle") }}</h3>
<p v-if="props.dueDate" class="text-large"> <p v-if="props.dueDate" class="text-large">
{{ {{ $t("assignment.dueDateIntroduction") }}
$t("assignment.dueDateIntroduction", { <DateEmbedding :single-date="dueDate"></DateEmbedding>
date: dueDate!.format("DD.MM.YYYY"),
time: dueDate!.format("HH:mm"),
})
}}
</p> </p>
<p v-else class="text-large"> <p v-else class="text-large">
{{ $t("assignment.dueDateNotSet") }} {{ $t("assignment.dueDateNotSet") }}

View File

@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import DateEmbedding from "@/components/dueDates/DateEmbedding.vue";
import ItButton from "@/components/ui/ItButton.vue"; import ItButton from "@/components/ui/ItButton.vue";
import ItCheckbox from "@/components/ui/ItCheckbox.vue"; import ItCheckbox from "@/components/ui/ItCheckbox.vue";
import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue"; import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
@ -119,7 +120,8 @@ const onSubmit = async () => {
</a> </a>
</div> </div>
<p class="pt-6"> <p class="pt-6">
{{ $t("assignment.dueDateSubmission", { date: dueDate.format("DD.MM.YYYY") }) }} {{ $t("assignment.dueDateSubmission") }}
<DateEmbedding :single-date="dueDate"></DateEmbedding>
</p> </p>
<ItButton <ItButton
class="mt-6" class="mt-6"

View File

@ -13,7 +13,7 @@ import type {
Assignment, Assignment,
AssignmentCompletion, AssignmentCompletion,
AssignmentTask, AssignmentTask,
CourseSessionAssignmentDetails, CourseSessionAssignment,
CourseSessionUser, CourseSessionUser,
LearningContentAssignment, LearningContentAssignment,
} from "@/types"; } from "@/types";
@ -29,11 +29,11 @@ const courseSession = useCurrentCourseSession();
const userStore = useUserStore(); const userStore = useUserStore();
interface State { interface State {
courseSessionAssignmentDetails: CourseSessionAssignmentDetails | undefined; courseSessionAssignment: CourseSessionAssignment | undefined;
} }
const state: State = reactive({ const state: State = reactive({
courseSessionAssignmentDetails: undefined, courseSessionAssignment: undefined,
}); });
const props = defineProps<{ const props = defineProps<{
@ -80,7 +80,7 @@ onMounted(async () => {
props.learningContent props.learningContent
); );
state.courseSessionAssignmentDetails = useCourseSessionsStore().findAssignmentDetails( state.courseSessionAssignment = useCourseSessionsStore().findCourseSessionAssignment(
props.learningContent.id props.learningContent.id
); );
@ -123,7 +123,7 @@ const showPreviousButton = computed(() => stepIndex.value != 0);
const showNextButton = computed(() => stepIndex.value + 1 < numPages.value); const showNextButton = computed(() => stepIndex.value + 1 < numPages.value);
const showExitButton = computed(() => numPages.value === stepIndex.value + 1); const showExitButton = computed(() => numPages.value === stepIndex.value + 1);
const dueDate = computed(() => const dueDate = computed(() =>
dayjs(state.courseSessionAssignmentDetails?.submissionDeadlineDateTimeUtc) dayjs(state.courseSessionAssignment?.submission_deadline_start)
); );
const currentTask = computed(() => { const currentTask = computed(() => {
if (stepIndex.value > 0 && stepIndex.value <= numTasks.value) { if (stepIndex.value > 0 && stepIndex.value <= numTasks.value) {

View File

@ -1,8 +1,8 @@
<template> <template>
<div class="mb-12 grid grid-cols-icon-card gap-x-4 grid-areas-icon-card"> <div class="mb-12 grid grid-cols-icon-card gap-x-4 grid-areas-icon-card">
<it-icon-calendar class="w-[60px] grid-in-icon" /> <it-icon-calendar-light class="w-[60px] grid-in-icon" />
<h2 class="text-large font-bold grid-in-title">Datum</h2> <h2 class="text-large font-bold grid-in-title">Datum</h2>
<p class="grid-in-value">{{ start }} - {{ end }}</p> <p class="grid-in-value">{{ formatDate(start, end) }}</p>
</div> </div>
<div class="mb-12 grid grid-cols-icon-card gap-x-4 grid-areas-icon-card"> <div class="mb-12 grid grid-cols-icon-card gap-x-4 grid-areas-icon-card">
<it-icon-location class="w-[60px] grid-in-icon" /> <it-icon-location class="w-[60px] grid-in-icon" />
@ -20,6 +20,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { formatDate } from "@/components/dueDates/dueDatesUtils";
import type { CourseSessionAttendanceCourse } from "@/types"; import type { CourseSessionAttendanceCourse } from "@/types";
import dayjs from "dayjs"; import dayjs from "dayjs";
import LocalizedFormat from "dayjs/plugin/localizedFormat"; import LocalizedFormat from "dayjs/plugin/localizedFormat";
@ -32,9 +33,8 @@ export interface Props {
const props = defineProps<Props>(); const props = defineProps<Props>();
dayjs.extend(LocalizedFormat); dayjs.extend(LocalizedFormat);
const format = "LLLL"; const start = computed(() => dayjs(props.attendanceCourse.start));
const start = computed(() => dayjs(props.attendanceCourse.start).format(format)); const end = computed(() => dayjs(props.attendanceCourse.end));
const end = computed(() => dayjs(props.attendanceCourse.end).format(format));
const location = computed(() => props.attendanceCourse.location); const location = computed(() => props.attendanceCourse.location);
const trainer = computed(() => props.attendanceCourse.trainer); const trainer = computed(() => props.attendanceCourse.trainer);
</script> </script>

View File

@ -1,13 +0,0 @@
<template>
<p class="mb-4 font-bold">{{ $t("dueDates.nextDueDates") }}:</p>
<!-- ul>
<li class="border-b border-t py-3">
<p class="pr-12">24. November 2022, 11 Uhr - Austausch mit Trainer</p>
</li>
<li class="border-b py-3">
<p class="pr-12">4. Dezember 2022, 10.30 Uhr - Vernetzen - Live Session</p>
</li>
</ul>
<p class="pt-2 underline">Alle Termine anzeigen</p -->
<p class="min-h-[150px] pt-2">{{ $t("Zur Zeit sind keine Termine vorhanden") }}</p>
</template>

View File

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import * as d3 from "d3"; import * as d3 from "d3";
import * as log from "loglevel";
import { computed, onMounted } from "vue"; import { computed, onMounted } from "vue";
// @ts-ignore // @ts-ignore
@ -17,7 +16,6 @@ const props = defineProps<{
}>(); }>();
onMounted(async () => { onMounted(async () => {
log.debug("LearningPathCircle mounted");
render(); render();
}); });

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import LearningPathAppointmentsMock from "@/pages/learningPath/learningPathPage/LearningPathAppointmentsMock.vue"; import DueDatesShortList from "@/components/dueDates/DueDatesShortList.vue";
import LearningPathListView from "@/pages/learningPath/learningPathPage/LearningPathListView.vue"; import LearningPathListView from "@/pages/learningPath/learningPathPage/LearningPathListView.vue";
import LearningPathPathView from "@/pages/learningPath/learningPathPage/LearningPathPathView.vue"; import LearningPathPathView from "@/pages/learningPath/learningPathPage/LearningPathPathView.vue";
import CircleProgress from "@/pages/learningPath/learningPathPage/LearningPathProgress.vue"; import CircleProgress from "@/pages/learningPath/learningPathPage/LearningPathProgress.vue";
@ -74,7 +74,7 @@ const changeViewType = (viewType: ViewType) => {
<!-- Top --> <!-- Top -->
<div class="flex flex-row justify-between space-x-8 bg-gray-200 p-6 sm:p-12"> <div class="flex flex-row justify-between space-x-8 bg-gray-200 p-6 sm:p-12">
<!-- Left --> <!-- Left -->
<div class="flex flex-col justify-between"> <div class="flex w-1/2 flex-col justify-between">
<div> <div>
<p class="font-bold"> <p class="font-bold">
{{ $t("learningPathPage.welcomeBack") }} {{ $t("learningPathPage.welcomeBack") }}
@ -91,8 +91,11 @@ const changeViewType = (viewType: ViewType) => {
</div> </div>
<!-- Right --> <!-- Right -->
<div v-if="!useMobileLayout" class="max-w-md"> <div v-if="!useMobileLayout" class="flex-grow">
<LearningPathAppointmentsMock></LearningPathAppointmentsMock> <div class="text-bold pb-3">
{{ $t("learningPathPage.nextDueDates") }}
</div>
<DueDatesShortList :max-count="2" :show-top-border="true"></DueDatesShortList>
</div> </div>
</div> </div>

View File

@ -109,7 +109,7 @@ export function findAssignmentDetail(assignmentId: number) {
(lc) => lc.assignmentId === assignmentId (lc) => lc.assignmentId === assignmentId
); );
return courseSessionsStore.findAssignmentDetails(learningContent?.id); return courseSessionsStore.findCourseSessionAssignment(learningContent?.id);
} }
export function maxAssignmentPoints(assignment: Assignment) { export function maxAssignmentPoints(assignment: Assignment) {

View File

@ -3,13 +3,15 @@ import { deleteCircleDocument } from "@/services/files";
import type { import type {
CircleDocument, CircleDocument,
CourseSession, CourseSession,
CourseSessionAssignmentDetails, CourseSessionAssignment,
CourseSessionAttendanceCourse, CourseSessionAttendanceCourse,
CourseSessionUser, CourseSessionUser,
DueDate,
ExpertSessionUser, ExpertSessionUser,
} from "@/types"; } from "@/types";
import eventBus from "@/utils/eventBus"; import eventBus from "@/utils/eventBus";
import { useLocalStorage } from "@vueuse/core"; import { useLocalStorage } from "@vueuse/core";
import dayjs from "dayjs";
import uniqBy from "lodash/uniqBy"; import uniqBy from "lodash/uniqBy";
import log from "loglevel"; import log from "loglevel";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
@ -25,7 +27,6 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
async function loadCourseSessionsData(reload = false) { async function loadCourseSessionsData(reload = false) {
log.debug("loadCourseSessionsData called"); log.debug("loadCourseSessionsData called");
allCourseSessions.value = await itGetCached(`/api/course/sessions/`, { allCourseSessions.value = await itGetCached(`/api/course/sessions/`, {
reload: reload, reload: reload,
}); });
@ -39,6 +40,10 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
reload: reload, reload: reload,
})) as CourseSessionUser[]; })) as CourseSessionUser[];
cs.users = users; cs.users = users;
cs.due_dates.forEach((dueDate) => {
dueDate.start = dayjs(dueDate.start);
dueDate.end = dayjs(dueDate.end);
});
}) })
); );
@ -190,6 +195,16 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
currentCourseSession.value?.documents.push(document); currentCourseSession.value?.documents.push(document);
} }
function allDueDates() {
const allDueDatesReturn: DueDate[] = [];
allCourseSessions.value?.forEach((cs) => {
allDueDatesReturn.push(...cs.due_dates);
});
allDueDatesReturn.sort((a, b) => dayjs(a.end).diff(dayjs(b.end)));
return allDueDatesReturn;
}
async function startUpload() { async function startUpload() {
log.debug("loadCourseSessionsData called"); log.debug("loadCourseSessionsData called");
allCourseSessions.value = await itPost(`/api/core/file/start`, { allCourseSessions.value = await itPost(`/api/core/file/start`, {
@ -215,17 +230,17 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
): CourseSessionAttendanceCourse | undefined { ): CourseSessionAttendanceCourse | undefined {
if (currentCourseSession.value) { if (currentCourseSession.value) {
return currentCourseSession.value.attendance_courses.find( return currentCourseSession.value.attendance_courses.find(
(attendanceCourse) => attendanceCourse.learningContentId === contentId (attendanceCourse) => attendanceCourse.learning_content_id === contentId
); );
} }
} }
function findAssignmentDetails( function findCourseSessionAssignment(
contentId?: number contentId?: number
): CourseSessionAssignmentDetails | undefined { ): CourseSessionAssignment | undefined {
if (contentId && currentCourseSession.value) { if (contentId && currentCourseSession.value) {
return currentCourseSession.value.assignment_details_list.find( return currentCourseSession.value.assignments.find(
(assignmentDetails) => assignmentDetails.learningContentId === contentId (a) => a.learning_content_id === contentId
); );
} }
} }
@ -244,7 +259,8 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => {
startUpload, startUpload,
removeDocument, removeDocument,
findAttendanceCourse, findAttendanceCourse,
findAssignmentDetails, findCourseSessionAssignment,
allDueDates,
// use `useCurrentCourseSession` whenever possible // use `useCurrentCourseSession` whenever possible
currentCourseSession, currentCourseSession,

View File

@ -1,5 +1,6 @@
import type { AssignmentCompletionStatus as AssignmentCompletionStatusGenerated } from "@/gql/graphql"; import type { AssignmentCompletionStatus as AssignmentCompletionStatusGenerated } from "@/gql/graphql";
import type { Circle } from "@/services/circle"; import type { Circle } from "@/services/circle";
import type { Dayjs } from "dayjs";
import type { Component } from "vue"; import type { Component } from "vue";
export type LoginMethod = "local" | "sso"; export type LoginMethod = "local" | "sso";
@ -412,17 +413,24 @@ export interface CircleDocument {
} }
export interface CourseSessionAttendanceCourse { export interface CourseSessionAttendanceCourse {
learningContentId: number; id: number;
course_session_id: number;
learning_content_id: number;
start: string; start: string;
end: string; end: string;
location: string; location: string;
trainer: string; trainer: string;
due_date_id: number;
} }
export interface CourseSessionAssignmentDetails { export interface CourseSessionAssignment {
learningContentId: number; id: number;
submissionDeadlineDateTimeUtc: string; course_session_id: number;
evaluationDeadlineDateTimeUtc: string; learning_content_id: number;
submission_deadline_id: number;
submission_deadline_start: string;
evaluation_deadline_id: number;
evaluation_deadline_start: string;
} }
export interface CourseSession { export interface CourseSession {
@ -439,9 +447,10 @@ export interface CourseSession {
course_url: string; course_url: string;
media_library_url: string; media_library_url: string;
attendance_courses: CourseSessionAttendanceCourse[]; attendance_courses: CourseSessionAttendanceCourse[];
assignment_details_list: CourseSessionAssignmentDetails[]; assignments: CourseSessionAssignment[];
documents: CircleDocument[]; documents: CircleDocument[];
users: CourseSessionUser[]; users: CourseSessionUser[];
due_dates: DueDate[];
} }
export type Role = "MEMBER" | "EXPERT" | "TUTOR"; export type Role = "MEMBER" | "EXPERT" | "TUTOR";
@ -556,3 +565,15 @@ export interface UserAssignmentCompletionStatus {
completion_status: AssignmentCompletionStatus; completion_status: AssignmentCompletionStatus;
evaluation_grade: number | null; evaluation_grade: number | null;
} }
export type DueDate = {
id: number;
start: Dayjs;
end: Dayjs;
title: string;
learning_content_description: string;
description: string;
url: string;
course_session: number | null;
page: number | null;
};

Binary file not shown.

View File

@ -116,10 +116,12 @@ LOCAL_APPS = [
"vbv_lernwelt.learnpath", "vbv_lernwelt.learnpath",
"vbv_lernwelt.competence", "vbv_lernwelt.competence",
"vbv_lernwelt.media_library", "vbv_lernwelt.media_library",
"vbv_lernwelt.course_session",
"vbv_lernwelt.feedback", "vbv_lernwelt.feedback",
"vbv_lernwelt.files", "vbv_lernwelt.files",
"vbv_lernwelt.notify", "vbv_lernwelt.notify",
"vbv_lernwelt.assignment", "vbv_lernwelt.assignment",
"vbv_lernwelt.duedate",
] ]
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS

View File

@ -1,7 +1,10 @@
import json import json
import random
from datetime import datetime, timedelta
import wagtail_factories import wagtail_factories
from django.conf import settings from django.conf import settings
from django.utils import timezone
from slugify import slugify from slugify import slugify
from wagtail.models import Site from wagtail.models import Site
from wagtail.rich_text import RichText from wagtail.rich_text import RichText
@ -34,7 +37,15 @@ from vbv_lernwelt.course.models import (
CourseSession, CourseSession,
CourseSessionUser, CourseSessionUser,
) )
from vbv_lernwelt.learnpath.models import Circle from vbv_lernwelt.course_session.models import (
CourseSessionAssignment,
CourseSessionAttendanceCourse,
)
from vbv_lernwelt.learnpath.models import (
Circle,
LearningContentAssignment,
LearningContentAttendanceCourse,
)
from vbv_lernwelt.learnpath.tests.learning_path_factories import ( from vbv_lernwelt.learnpath.tests.learning_path_factories import (
CircleFactory, CircleFactory,
LearningContentAssignmentFactory, LearningContentAssignmentFactory,
@ -79,16 +90,19 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
create_test_media_library() create_test_media_library()
if with_sessions: if with_sessions:
now = timezone.now()
# course sessions # course sessions
cs_bern = CourseSession.objects.create( cs_bern = CourseSession.objects.create(
course_id=COURSE_TEST_ID, course_id=COURSE_TEST_ID,
title="Test Bern 2022 a", title="Test Bern 2022 a",
id=TEST_COURSE_SESSION_BERN_ID, id=TEST_COURSE_SESSION_BERN_ID,
start_date=now,
) )
cs_zurich = CourseSession.objects.create( cs_zurich = CourseSession.objects.create(
course_id=COURSE_TEST_ID, course_id=COURSE_TEST_ID,
title="Test Zürich 2022 a", title="Test Zürich 2022 a",
id=TEST_COURSE_SESSION_ZURICH_ID, id=TEST_COURSE_SESSION_ZURICH_ID,
start_date=now,
) )
trainer1 = User.objects.get(email="test-trainer1@example.com") trainer1 = User.objects.get(email="test-trainer1@example.com")
@ -115,6 +129,18 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
course_session=cs_zurich, course_session=cs_zurich,
user=student2, user=student2,
) )
course = Course.objects.get(id=COURSE_TEST_ID)
for cs in CourseSession.objects.filter(course_id=COURSE_TEST_ID):
for assignment in LearningContentAssignment.objects.descendant_of(
course.coursepage
):
create_course_session_assignment(cs, assignment)
for (
attendance_course
) in LearningContentAttendanceCourse.objects.descendant_of(
course.coursepage
):
create_course_session_attendance_course(cs, attendance_course)
return course return course
@ -143,6 +169,45 @@ def create_test_assignment_submitted_data(assignment, course_session, user):
) )
def create_course_session_assignment(course_session, assignment):
csa, created = CourseSessionAssignment.objects.get_or_create(
course_session=course_session,
learning_content=assignment,
)
if course_session.start_date is None:
course_session.start_date = datetime.now() + timedelta(days=12)
course_session.save()
submission_deadline = csa.submission_deadline
if submission_deadline:
submission_deadline.start = course_session.start_date + timedelta(days=14)
submission_deadline.save()
evaluation_deadline = csa.evaluation_deadline
if evaluation_deadline:
evaluation_deadline.start = course_session.start_date + timedelta(days=28)
evaluation_deadline.save()
return csa
def create_course_session_attendance_course(course_session, course):
casc = CourseSessionAttendanceCourse.objects.create(
course_session=course_session,
learning_content=course,
location="Handelsschule KV Bern, Zimmer 123, Eigerstrasse 16, 3012 Bern",
trainer="Roland Grossenbacher, roland.grossenbacher@helvetia.ch",
)
random_week = random.randint(1, 26)
casc.due_date.start = timezone.make_aware(
datetime(2023, 6, 14, 8, 30) + timedelta(weeks=random_week)
)
casc.due_date.end = timezone.make_aware(
datetime(2023, 6, 14, 17, 0) + timedelta(weeks=random_week)
)
casc.due_date.save()
return casc
def create_test_course_with_categories(apps=None, schema_editor=None): def create_test_course_with_categories(apps=None, schema_editor=None):
if apps is not None: if apps is not None:
Course = apps.get_model("course", "Course") Course = apps.get_model("course", "Course")

View File

@ -1,7 +1,9 @@
import os import os
import random import random
from datetime import datetime, timedelta
import djclick as click import djclick as click
from django.utils import timezone
from vbv_lernwelt.assignment.creators.create_assignments import ( from vbv_lernwelt.assignment.creators.create_assignments import (
create_uk_basis_prep_assignment, create_uk_basis_prep_assignment,
@ -68,6 +70,10 @@ from vbv_lernwelt.course.models import (
CourseSessionUser, CourseSessionUser,
) )
from vbv_lernwelt.course.services import mark_course_completion from vbv_lernwelt.course.services import mark_course_completion
from vbv_lernwelt.course_session.models import (
CourseSessionAssignment,
CourseSessionAttendanceCourse,
)
from vbv_lernwelt.feedback.creators.create_demo_feedback import create_feedback from vbv_lernwelt.feedback.creators.create_demo_feedback import create_feedback
from vbv_lernwelt.importer.services import ( from vbv_lernwelt.importer.services import (
import_course_sessions_from_excel, import_course_sessions_from_excel,
@ -237,35 +243,34 @@ def create_course_uk_de():
cs = CourseSession.objects.create( cs = CourseSession.objects.create(
course_id=COURSE_UK, course_id=COURSE_UK,
title="Bern 2023 a", title="Bern 2023 a",
attendance_courses=[
{
"learningContentId": LearningContentAttendanceCourse.objects.get(
slug="überbetriebliche-kurse-lp-circle-fahrzeug-lc-präsenzkurs-fahrzeug"
).id,
"start": "2023-05-23T08:30:00+0200",
"end": "2023-05-23T17:00:00+0200",
"location": "Handelsschule KV Bern, Zimmer 123, Eigerstrasse 16, 3012 Bern",
"trainer": "Roland Grossenbacher, roland.grossenbacher@helvetia.ch",
}
],
assignment_details_list=[
{
"learningContentId": LearningContentAssignment.objects.get(
slug="überbetriebliche-kurse-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"
).id,
"submissionDeadlineDateTimeUtc": "2023-06-13T19:00:00Z",
"evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z",
},
{
"learningContentId": LearningContentAssignment.objects.get(
slug="überbetriebliche-kurse-lp-circle-fahrzeug-lc-fahrzeug-mein-erstes-auto"
).id,
"submissionDeadlineDateTimeUtc": "2023-06-13T19:00:00Z",
"evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z",
},
],
) )
for i, cs in enumerate(CourseSession.objects.filter(course_id=COURSE_UK_TRAINING)):
create_course_session_assignments(
cs,
f"{course.slug}-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice",
i=i,
)
csac = CourseSessionAttendanceCourse.objects.create(
course_session=cs,
learning_content=LearningContentAttendanceCourse.objects.get(
slug="überbetriebliche-kurse-lp-circle-fahrzeug-lc-präsenzkurs-fahrzeug"
),
location="Handelsschule KV Bern, Zimmer 123, Eigerstrasse 16, 3012 Bern",
trainer="Roland Grossenbacher, roland.grossenbacher@helvetia.ch",
)
# TODO: create dates schlauer
random_week = random.randint(1, 26)
csac.due_date.start = timezone.make_aware(
datetime(2023, 6, 14, 8, 30) + timedelta(weeks=random_week)
)
csac.due_date.end = timezone.make_aware(
datetime(2023, 6, 14, 17, 0) + timedelta(weeks=random_week)
)
csac.due_date.save()
# figma demo users and data # figma demo users and data
csu = CourseSessionUser.objects.create( csu = CourseSessionUser.objects.create(
course_session=cs, course_session=cs,
@ -537,24 +542,12 @@ def create_course_training_de():
f"{current_dir}/../../../importer/tests/Schulungen_Teilnehmende.xlsx", f"{current_dir}/../../../importer/tests/Schulungen_Teilnehmende.xlsx",
) )
for cs in CourseSession.objects.filter(course_id=COURSE_UK_TRAINING): for i, cs in enumerate(CourseSession.objects.filter(course_id=COURSE_UK_TRAINING)):
cs.assignment_details_list = [ create_course_session_assignments(
{ cs,
"learningContentId": LearningContentAssignment.objects.get( f"{course.slug}-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice",
slug=f"{course.slug}-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice" i=i,
).id, )
"submissionDeadlineDateTimeUtc": "2023-06-13T19:00:00Z",
"evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z",
},
{
"learningContentId": LearningContentAssignment.objects.get(
slug=f"{course.slug}-lp-circle-fahrzeug-lc-fahrzeug-mein-erstes-auto"
).id,
"submissionDeadlineDateTimeUtc": "2023-06-13T19:00:00Z",
"evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z",
},
]
cs.save()
# attach users as trainers to ÜK course # attach users as trainers to ÜK course
course_uk = Course.objects.filter(id=COURSE_UK).first() course_uk = Course.objects.filter(id=COURSE_UK).first()
@ -586,6 +579,24 @@ def create_course_training_de():
csu.save() csu.save()
def create_course_session_assignments(course_session, assignment_slug, i=1):
csa = CourseSessionAssignment.objects.create(
course_session=course_session,
learning_content=LearningContentAssignment.objects.get(slug=assignment_slug),
)
if course_session.start_date is None:
course_session.start_date = datetime.now() + timedelta(days=i * 12)
course_session.save()
submission_deadline = csa.submission_deadline
if submission_deadline:
submission_deadline.start = course_session.start_date + timedelta(days=14)
submission_deadline.save()
evaluation_deadline = csa.evaluation_deadline
if evaluation_deadline:
evaluation_deadline.start = course_session.start_date + timedelta(days=28)
evaluation_deadline.save()
def create_course_training_fr(): def create_course_training_fr():
# Test Lehrgang für üK Trainer FR # Test Lehrgang für üK Trainer FR
course = create_versicherungsvermittlerin_with_categories( course = create_versicherungsvermittlerin_with_categories(
@ -622,22 +633,22 @@ def create_course_training_fr():
) )
for cs in CourseSession.objects.filter(course_id=COURSE_UK_TRAINING_FR): for cs in CourseSession.objects.filter(course_id=COURSE_UK_TRAINING_FR):
cs.assignment_details_list = [ # cs.assignment_details_list = [
{ # {
"learningContentId": LearningContentAssignment.objects.get( # "learningContentId": LearningContentAssignment.objects.get(
slug=f"{course.slug}-lp-circle-véhicule-lc-vérification-dune-police-dassurance-de-véhicule-à-moteur" # slug=f"{course.slug}-lp-circle-véhicule-lc-vérification-dune-police-dassurance-de-véhicule-à-moteur"
).id, # ).id,
"submissionDeadlineDateTimeUtc": "2023-06-13T19:00:00Z", # "submissionDeadlineDateTimeUtc": "2023-06-13T19:00:00Z",
"evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z", # "evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z",
}, # },
{ # {
"learningContentId": LearningContentAssignment.objects.get( # "learningContentId": LearningContentAssignment.objects.get(
slug=f"{course.slug}-lp-circle-véhicule-lc-véhicule-à-moteur-ma-première-voiture" # slug=f"{course.slug}-lp-circle-véhicule-lc-véhicule-à-moteur-ma-première-voiture"
).id, # ).id,
"submissionDeadlineDateTimeUtc": "2023-06-13T19:00:00Z", # "submissionDeadlineDateTimeUtc": "2023-06-13T19:00:00Z",
"evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z", # "evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z",
}, # },
] # ]
cs.save() cs.save()
# attach users as trainers to ÜK course # attach users as trainers to ÜK course
@ -709,22 +720,22 @@ def create_course_training_it():
) )
for cs in CourseSession.objects.filter(course_id=COURSE_UK_TRAINING_IT): for cs in CourseSession.objects.filter(course_id=COURSE_UK_TRAINING_IT):
cs.assignment_details_list = [ # cs.assignment_details_list = [
{ # {
"learningContentId": LearningContentAssignment.objects.get( # "learningContentId": LearningContentAssignment.objects.get(
slug=f"{course.slug}-lp-circle-veicolo-lc-verifica-di-una-polizza-di-assicurazione-veicoli-a-motore" # slug=f"{course.slug}-lp-circle-veicolo-lc-verifica-di-una-polizza-di-assicurazione-veicoli-a-motore"
).id, # ).id,
"submissionDeadlineDateTimeUtc": "2023-06-20T19:00:00Z", # "submissionDeadlineDateTimeUtc": "2023-06-20T19:00:00Z",
"evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z", # "evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z",
}, # },
{ # {
"learningContentId": LearningContentAssignment.objects.get( # "learningContentId": LearningContentAssignment.objects.get(
slug=f"{course.slug}-lp-circle-veicolo-lc-veicolo-la-mia-prima-auto" # slug=f"{course.slug}-lp-circle-veicolo-lc-veicolo-la-mia-prima-auto"
).id, # ).id,
"submissionDeadlineDateTimeUtc": "2023-06-20T19:00:00Z", # "submissionDeadlineDateTimeUtc": "2023-06-20T19:00:00Z",
"evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z", # "evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z",
}, # },
] # ]
cs.save() cs.save()
# attach users as trainers to ÜK course # attach users as trainers to ÜK course

View File

@ -0,0 +1,17 @@
# Generated by Django 3.2.13 on 2023-06-14 14:02
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("course", "0004_import_fields"),
]
operations = [
migrations.RemoveField(
model_name="coursesession",
name="attendance_courses",
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 3.2.13 on 2023-06-21 14:19
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("course", "0005_remove_coursesession_attendance_courses"),
]
operations = [
migrations.RemoveField(
model_name="coursesession",
name="assignment_details_list",
),
]

View File

@ -2,7 +2,6 @@ from django.db import models
from django.db.models import UniqueConstraint from django.db.models import UniqueConstraint
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_jsonform.models.fields import JSONField
from grapple.models import GraphQLString from grapple.models import GraphQLString
from wagtail.models import Page from wagtail.models import Page
@ -193,24 +192,6 @@ class CourseSession(models.Model):
Das anhängen kann via CourseSessionUser oder "Schulklasse (TODO)" geschehen Das anhängen kann via CourseSessionUser oder "Schulklasse (TODO)" geschehen
""" """
ATTENDANCE_COURSES_SCHEMA = {
"type": "array",
"items": {
"type": "object",
"properties": {
"learningContentId": {
"type": "number",
"title": "ID des Lerninhalts",
"required": True,
},
"start": {"type": "string", "format": "datetime"},
"end": {"type": "string", "format": "datetime"},
"location": {"type": "string"},
"trainer": {"type": "string"},
},
},
}
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
@ -226,11 +207,6 @@ class CourseSession(models.Model):
start_date = models.DateField(null=True, blank=True) start_date = models.DateField(null=True, blank=True)
end_date = models.DateField(null=True, blank=True) end_date = models.DateField(null=True, blank=True)
attendance_courses = JSONField(
schema=ATTENDANCE_COURSES_SCHEMA, blank=True, default=list
)
assignment_details_list = models.JSONField(default=list, blank=True)
additional_json_data = models.JSONField(default=dict, blank=True) additional_json_data = models.JSONField(default=dict, blank=True)
def __str__(self): def __str__(self):

View File

@ -7,6 +7,16 @@ from vbv_lernwelt.course.models import (
CourseCompletion, CourseCompletion,
CourseSession, CourseSession,
) )
from vbv_lernwelt.course_session.models import (
CourseSessionAssignment,
CourseSessionAttendanceCourse,
)
from vbv_lernwelt.course_session.serializers import (
CourseSessionAssignmentSerializer,
CourseSessionAttendanceCourseSerializer,
)
from vbv_lernwelt.duedate.models import DueDate
from vbv_lernwelt.duedate.serializers import DueDateSerializer
class CourseSerializer(serializers.ModelSerializer): class CourseSerializer(serializers.ModelSerializer):
@ -50,6 +60,9 @@ class CourseSessionSerializer(serializers.ModelSerializer):
competence_url = serializers.SerializerMethodField() competence_url = serializers.SerializerMethodField()
media_library_url = serializers.SerializerMethodField() media_library_url = serializers.SerializerMethodField()
documents = serializers.SerializerMethodField() documents = serializers.SerializerMethodField()
attendance_courses = serializers.SerializerMethodField()
assignments = serializers.SerializerMethodField()
due_dates = serializers.SerializerMethodField()
def get_course(self, obj): def get_course(self, obj):
return CourseSerializer(obj.course).data return CourseSerializer(obj.course).data
@ -75,6 +88,20 @@ class CourseSessionSerializer(serializers.ModelSerializer):
) )
return CircleDocumentSerializer(documents, many=True).data return CircleDocumentSerializer(documents, many=True).data
def get_attendance_courses(self, obj):
return CourseSessionAttendanceCourseSerializer(
CourseSessionAttendanceCourse.objects.filter(course_session=obj), many=True
).data
def get_assignments(self, obj):
return CourseSessionAssignmentSerializer(
CourseSessionAssignment.objects.filter(course_session=obj), many=True
).data
def get_due_dates(self, obj):
due_dates = DueDate.objects.filter(course_session=obj)
return DueDateSerializer(due_dates, many=True).data
class Meta: class Meta:
model = CourseSession model = CourseSession
fields = [ fields = [
@ -87,13 +114,14 @@ class CourseSessionSerializer(serializers.ModelSerializer):
"end_date", "end_date",
"additional_json_data", "additional_json_data",
"attendance_courses", "attendance_courses",
"assignment_details_list", "assignments",
"learning_path_url", "learning_path_url",
"cockpit_url", "cockpit_url",
"competence_url", "competence_url",
"media_library_url", "media_library_url",
"course_url", "course_url",
"documents", "documents",
"due_dates",
] ]

View File

@ -0,0 +1,29 @@
from django.contrib import admin
from vbv_lernwelt.course_session.models import (
CourseSessionAssignment,
CourseSessionAttendanceCourse,
)
@admin.register(CourseSessionAttendanceCourse)
class CourseSessionAttendanceCourseAdmin(admin.ModelAdmin):
# Inline fields are not possible for the DueDate model, because it is not a ForeignKey relatoion.
readonly_fields = ["course_session", "learning_content", "due_date"]
list_display = [
"course_session",
"learning_content",
"trainer",
]
list_filter = ["course_session__course", "course_session"]
@admin.register(CourseSessionAssignment)
class CourseSessionAssignmentAdmin(admin.ModelAdmin):
# Inline fields are not possible for the DueDate model, because it is not a ForeignKey relatoion.
readonly_fields = ["course_session", "learning_content"]
list_display = [
"course_session",
"learning_content",
]
list_filter = ["course_session__course"]

View File

@ -0,0 +1,13 @@
from django.apps import AppConfig
class CourseSessionConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "vbv_lernwelt.course_session"
def ready(self):
try:
# pylint: disable=unused-import,import-outside-toplevel
import vbv_lernwelt.course_session.signals # noqa F401
except ImportError:
pass

View File

@ -0,0 +1,26 @@
import factory
from factory.django import DjangoModelFactory
from vbv_lernwelt.course.factories import CourseFactory
from vbv_lernwelt.course.models import CourseSession
from vbv_lernwelt.course_session.models import (
CourseSessionAssignment,
CourseSessionAttendanceCourse,
)
class CourseSessionAssignmentFactory(DjangoModelFactory):
class Meta:
model = CourseSessionAssignment
class CourseSessionAttendanceCourseFactory(DjangoModelFactory):
class Meta:
model = CourseSessionAttendanceCourse
class CourseSessionFactory(DjangoModelFactory):
class Meta:
model = CourseSession
course = CourseFactory()

View File

@ -0,0 +1,56 @@
# Generated by Django 3.2.13 on 2023-06-14 15:01
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("learnpath", "0007_learningunit_title_hidden"),
("course", "0005_remove_coursesession_attendance_courses"),
("duedate", "0002_auto_20230614_1500"),
]
operations = [
migrations.CreateModel(
name="CourseSessionAttendanceCourse",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("location", models.CharField(blank=True, default="", max_length=255)),
("trainer", models.CharField(blank=True, default="", max_length=255)),
(
"course_session",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="course.coursesession",
),
),
(
"due_date",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="attendance_course_due_date",
to="duedate.duedate",
),
),
(
"learning_content",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="learnpath.learningcontentattendancecourse",
),
),
],
),
]

View File

@ -0,0 +1,61 @@
# Generated by Django 3.2.13 on 2023-06-21 14:19
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("course", "0006_remove_coursesession_assignment_details_list"),
("learnpath", "0007_learningunit_title_hidden"),
("duedate", "0003_alter_duedate_course_session"),
("course_session", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="CourseSessionAssignment",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"course_session",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="course.coursesession",
),
),
(
"evaluation_deadline",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="assignment_evaluation_deadline",
to="duedate.duedate",
),
),
(
"learning_content",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="learnpath.learningcontentassignment",
),
),
(
"submission_deadline",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="assignment_submission_deadline",
to="duedate.duedate",
),
),
],
),
]

View File

@ -0,0 +1,35 @@
# Generated by Django 3.2.13 on 2023-06-28 11:21
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("duedate", "0006_auto_20230627_1553"),
("course_session", "0002_coursesessionassignment"),
]
operations = [
migrations.AlterField(
model_name="coursesessionassignment",
name="evaluation_deadline",
field=models.OneToOneField(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="assignment_evaluation_deadline",
to="duedate.duedate",
),
),
migrations.AlterField(
model_name="coursesessionassignment",
name="submission_deadline",
field=models.OneToOneField(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="assignment_submission_deadline",
to="duedate.duedate",
),
),
]

View File

@ -0,0 +1,131 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from vbv_lernwelt.assignment.models import AssignmentType
from vbv_lernwelt.duedate.models import DueDate
class CourseSessionAttendanceCourse(models.Model):
"""
Präsenztag Durchührung
Kann über einen Zeitraum von meheren Tagen gehen.
"""
course_session = models.ForeignKey(
"course.CourseSession",
on_delete=models.CASCADE,
)
learning_content = models.ForeignKey(
"learnpath.LearningContentAttendanceCourse",
on_delete=models.CASCADE,
)
due_date = models.OneToOneField(
"duedate.DueDate",
on_delete=models.CASCADE,
related_name="attendance_course_due_date",
)
location = models.CharField(max_length=255, blank=True, default="")
trainer = models.CharField(max_length=255, blank=True, default="")
def save(self, *args, **kwargs):
if not self.pk:
title = ""
url = ""
page = None
if self.learning_content_id:
title = self.learning_content.title
page = self.learning_content.page_ptr
url = self.learning_content.get_frontend_url()
self.due_date = DueDate.objects.create(
url=url,
title=f"{title}",
learning_content_description=f"{_('Präsenzkurs')}",
description="",
course_session=self.course_session,
page=page,
)
super().save(*args, **kwargs)
def __str__(self):
return f"{self.course_session} - {self.learning_content}"
class CourseSessionAssignment(models.Model):
"""
Auftrag
- Geletitete Fallarbeit ist eine speziefische ausprägung eines Auftrags (assignment_type)
"""
course_session = models.ForeignKey(
"course.CourseSession",
on_delete=models.CASCADE,
)
learning_content = models.ForeignKey(
"learnpath.LearningContentAssignment",
on_delete=models.CASCADE,
)
submission_deadline = models.OneToOneField(
"duedate.DueDate",
on_delete=models.CASCADE,
related_name="assignment_submission_deadline",
null=True,
)
evaluation_deadline = models.OneToOneField(
"duedate.DueDate",
on_delete=models.CASCADE,
related_name="assignment_evaluation_deadline",
null=True,
)
def save(self, *args, **kwargs):
if not self.pk:
title = ""
url = ""
page = None
assignment_type_description = ""
if self.learning_content_id:
title = self.learning_content.title
page = self.learning_content.page_ptr
url = self.learning_content.get_frontend_url()
assignment_type = self.learning_content.assignment_type
assignment_type_descriptions = {
AssignmentType.CASEWORK.name: _("Geleitete Fallarbeit"),
AssignmentType.PREP_ASSIGNMENT.name: _("Vorbereitungsauftrag"),
AssignmentType.REFLECTION.name: _("Reflexion"),
}
assignment_type_description = assignment_type_descriptions.get(
assignment_type, ""
)
if assignment_type in {
AssignmentType.CASEWORK.value,
AssignmentType.PREP_ASSIGNMENT.value,
}: # Reflexion
self.submission_deadline = DueDate.objects.create(
url=url,
title=f"{title}",
learning_content_description=assignment_type_description,
description=f"{_('Abgabe Termin')}",
course_session=self.course_session,
page=page,
)
if assignment_type == AssignmentType.CASEWORK.value:
self.evaluation_deadline = DueDate.objects.create(
url=url,
title=f"{title}",
learning_content_description=assignment_type_description,
description=f"{_('Freigabe Termin Bewertungen')}",
course_session=self.course_session,
page=page,
)
super().save(*args, **kwargs)
def __str__(self):
return f"{self.course_session} - {self.learning_content}"

View File

@ -0,0 +1,55 @@
from rest_framework import serializers
from vbv_lernwelt.course_session.models import (
CourseSessionAssignment,
CourseSessionAttendanceCourse,
)
class CourseSessionAttendanceCourseSerializer(serializers.ModelSerializer):
start = serializers.SerializerMethodField()
end = serializers.SerializerMethodField()
class Meta:
model = CourseSessionAttendanceCourse
fields = [
"id",
"course_session_id",
"learning_content_id",
"due_date_id",
"location",
"trainer",
"start",
"end",
]
def get_start(self, obj):
return obj.due_date.start
def get_end(self, obj):
return obj.due_date.end
class CourseSessionAssignmentSerializer(serializers.ModelSerializer):
submission_deadline_start = serializers.SerializerMethodField()
evaluation_deadline_start = serializers.SerializerMethodField()
class Meta:
model = CourseSessionAssignment
fields = [
"id",
"course_session_id",
"learning_content_id",
"submission_deadline_id",
"submission_deadline_start",
"evaluation_deadline_id",
"evaluation_deadline_start",
]
def get_evaluation_deadline_start(self, obj):
if obj.evaluation_deadline:
return obj.evaluation_deadline.start
def get_submission_deadline_start(self, obj):
if obj.submission_deadline:
return obj.submission_deadline.start

View File

@ -0,0 +1,29 @@
from datetime import datetime
from django.test import TestCase
from django.utils import timezone
from vbv_lernwelt.core.create_default_users import create_default_users
from vbv_lernwelt.course.creators.test_course import create_test_course
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
from vbv_lernwelt.duedate.models import DueDate
class CourseSessionModelsTestCase(TestCase):
def setUp(self) -> None:
create_default_users()
create_test_course(with_sessions=True)
def test_course_session_attendance_course(self):
csac = CourseSessionAttendanceCourse.objects.all().first()
due_date = csac.due_date
deadline_date = datetime(
2023, 7, 6, 8, 30, tzinfo=timezone.get_current_timezone()
)
due_date.start = deadline_date
due_date.save()
this_date = DueDate.objects.get(pk=due_date.pk)
self.assertEqual(this_date.start, deadline_date)

View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

View File

@ -0,0 +1,29 @@
from django.contrib import admin
from wagtail.models import Page
from vbv_lernwelt.duedate.models import DueDate
from vbv_lernwelt.learnpath.models import (
LearningContentAttendanceCourse,
LearningContentTest,
)
# Register your models here.
@admin.register(DueDate)
class DueDateAdmin(admin.ModelAdmin):
date_hierarchy = "end"
list_display = ["title", "course_session", "start", "end", "is_undefined"]
list_filter = ["course_session"]
readonly_fields = ["course_session", "page"]
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "page":
if request.resolver_match.kwargs.get("object_id"):
object_id = int(request.resolver_match.kwargs.get("object_id"))
csd = DueDate.objects.get(id=object_id)
kwargs["queryset"] = Page.objects.descendant_of(
csd.course_session.course.coursepage
).exact_type(LearningContentAttendanceCourse, LearningContentTest)
else:
kwargs["queryset"] = Page.objects.none()
return super().formfield_for_foreignkey(db_field, request, **kwargs)

View File

@ -0,0 +1,13 @@
from django.apps import AppConfig
class DueDatesConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "vbv_lernwelt.duedate"
def ready(self):
try:
# pylint: disable=unused-import,import-outside-toplevel
import vbv_lernwelt.course.signals # noqa F401
except ImportError:
pass

View File

@ -0,0 +1,24 @@
import datetime
import structlog
from django.utils import timezone
from factory.django import DjangoModelFactory
from .models import DueDate
logger = structlog.get_logger(__name__)
def get_date(date_string):
return datetime.datetime.strptime(
date_string,
"%b %d %Y",
).astimezone(timezone.get_current_timezone())
class DueDateFactory(DjangoModelFactory):
class Meta:
model = DueDate
title = "Prüfung Versicherungsvermittler/-in"
end = get_date("Jan 01 2021")

View File

@ -0,0 +1,48 @@
# Generated by Django 3.2.13 on 2023-06-14 09:29
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("course", "0004_import_fields"),
]
operations = [
migrations.CreateModel(
name="DueDate",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("start", models.DateTimeField(db_index=True, null=True)),
("end", models.DateTimeField(db_index=True, null=True)),
("title", models.CharField(default="Termin", max_length=1024)),
("url", models.URLField(blank=True, max_length=1024, null=True)),
(
"learning_content_id",
models.CharField(blank=True, max_length=255, null=True),
),
(
"course_session",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="events",
to="course.coursesession",
),
),
],
),
]

View File

@ -0,0 +1,29 @@
# Generated by Django 3.2.13 on 2023-06-14 13:00
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("wagtailcore", "0083_workflowcontenttype"),
("duedate", "0001_initial"),
]
operations = [
migrations.RemoveField(
model_name="duedate",
name="learning_content_id",
),
migrations.AddField(
model_name="duedate",
name="page",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="wagtailcore.page",
),
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 3.2.13 on 2023-06-21 14:19
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("course", "0006_remove_coursesession_assignment_details_list"),
("duedate", "0002_auto_20230614_1500"),
]
operations = [
migrations.AlterField(
model_name="duedate",
name="course_session",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="duedates",
to="course.coursesession",
),
),
]

View File

@ -0,0 +1,29 @@
# Generated by Django 3.2.13 on 2023-06-21 15:03
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("duedate", "0003_alter_duedate_course_session"),
]
operations = [
migrations.AddField(
model_name="duedate",
name="content_type",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype",
),
),
migrations.AddField(
model_name="duedate",
name="object_id",
field=models.PositiveIntegerField(null=True),
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 3.2.13 on 2023-06-22 09:38
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("duedate", "0004_auto_20230621_1703"),
]
operations = [
migrations.RemoveField(
model_name="duedate",
name="content_type",
),
migrations.RemoveField(
model_name="duedate",
name="object_id",
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.13 on 2023-06-27 13:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("duedate", "0005_auto_20230622_1138"),
]
operations = [
migrations.AddField(
model_name="duedate",
name="description",
field=models.CharField(default="Abgabetermin", max_length=1024),
),
migrations.AddField(
model_name="duedate",
name="learning_content_description",
field=models.CharField(default="GeleiteteFallarbeit", max_length=1024),
),
]

View File

@ -0,0 +1,42 @@
# Generated by Django 3.2.13 on 2023-07-03 15:41
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("course", "0006_remove_coursesession_assignment_details_list"),
("duedate", "0006_auto_20230627_1553"),
]
operations = [
migrations.AlterField(
model_name="duedate",
name="course_session",
field=models.ForeignKey(
blank=True,
default=1,
on_delete=django.db.models.deletion.CASCADE,
related_name="duedates",
to="course.coursesession",
),
preserve_default=False,
),
migrations.AlterField(
model_name="duedate",
name="description",
field=models.CharField(default="", max_length=1024),
),
migrations.AlterField(
model_name="duedate",
name="learning_content_description",
field=models.CharField(default="", max_length=1024),
),
migrations.AlterField(
model_name="duedate",
name="url",
field=models.URLField(blank=True, default="", max_length=1024),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.13 on 2023-07-11 09:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("duedate", "0007_auto_20230703_1741"),
]
operations = [
migrations.AlterField(
model_name="duedate",
name="start",
field=models.DateTimeField(blank=True, db_index=True, null=True),
),
migrations.AlterField(
model_name="duedate",
name="url",
field=models.CharField(blank=True, default="", max_length=1024),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 3.2.13 on 2023-07-12 07:05
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("course", "0006_remove_coursesession_assignment_details_list"),
("duedate", "0008_auto_20230711_1116"),
]
operations = [
migrations.AlterField(
model_name="duedate",
name="course_session",
field=models.ForeignKey(
blank=True,
on_delete=django.db.models.deletion.CASCADE,
to="course.coursesession",
),
),
]

View File

@ -0,0 +1,38 @@
# Generated by Django 3.2.13 on 2023-07-12 07:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("duedate", "0009_alter_duedate_course_session"),
]
operations = [
migrations.AlterField(
model_name="duedate",
name="description",
field=models.CharField(blank=True, default="", max_length=1024),
),
migrations.AlterField(
model_name="duedate",
name="end",
field=models.DateTimeField(blank=True, db_index=True, null=True),
),
migrations.AlterField(
model_name="duedate",
name="learning_content_description",
field=models.CharField(blank=True, default="", max_length=1024),
),
migrations.AlterField(
model_name="duedate",
name="start",
field=models.DateTimeField(db_index=True, null=True),
),
migrations.AlterField(
model_name="duedate",
name="title",
field=models.CharField(default="", max_length=1024),
),
]

View File

@ -0,0 +1,84 @@
import datetime
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from wagtail.models import Page
from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.models import CourseSession
class DueDate(models.Model):
start = models.DateTimeField(null=True, db_index=True)
end = models.DateTimeField(null=True, blank=True, db_index=True)
title = models.CharField(default="", max_length=1024)
learning_content_description = models.CharField(
default="", blank=True, max_length=1024
)
description = models.CharField(default="", blank=True, max_length=1024)
url = models.CharField(default="", blank=True, max_length=1024)
course_session = models.ForeignKey(
"course.CourseSession",
on_delete=models.CASCADE,
blank=True,
)
page = models.ForeignKey(Page, on_delete=models.SET_NULL, null=True, blank=True)
def Meta(self):
ordering = ["start", "end"]
verbose_name = _("Termin")
help = "The start date is mandatory. You can set the end date if you want to have a deadline with a duration."
def __str__(self):
if self.is_undefined:
return f"DueDate: {self.title} undefined"
start_str = self.start.strftime("%Y-%m-%d %H:%M") if self.start else "-"
result = f"DueDate: {self.title} {start_str}"
if self.end:
end_str = self.end.strftime("%Y-%m-%d %H:%M") if self.end else "-"
result += f" - {end_str}"
return result
@property
def is_undefined(self):
return self.start is None
@property
def duration(self):
if self.end is None:
return datetime.timedelta(0)
return self.end - self.start
@classmethod
def get_users_next_events_qs(
cls, user: User, course_session: CourseSession = None, limit=10
):
"""
Returns a queryset of all due dates that are relevant for the given user.
If course_session is given, only due dates for that course_session are returned.
The user is determined by via a course session user of a course_assignment.
"""
qs = cls.get_next_due_dates_qs()
if course_session:
qs = qs.filter(
course_session=course_session,
course_session__course_assignment__user=user,
)
else:
qs = qs.filter(course_session__course_assignment__user=user)
qs = qs.order_by("start")[:limit]
return qs
@classmethod
def get_next_due_dates_qs(cls):
now = timezone.now()
qs = cls.objects.filter(start__gte=now)
return qs

View File

@ -0,0 +1,9 @@
from rest_framework import serializers
from vbv_lernwelt.duedate.models import DueDate
class DueDateSerializer(serializers.ModelSerializer):
class Meta:
model = DueDate
fields = "__all__"

View File

@ -0,0 +1,30 @@
# class TesDueDatetModel(TestCase):
#
# def test_get_next_duedate_qs_is_really_next(self):
# pass
# start = timezone.now() - datetime.timedelta(days=18)
# generate_duedates(start=start)
# self.assertEqual(DueDate.objects.count(), 20)
# self.assertEqual(DueDate.get_next_duedates_qs().count(), 2)
# def test_event_model_factory_validation(self):
# e = DueDateFactory()
# e.start = get_date("Jan 01 2021")
# e.end = get_date("Jan 02 2021")
# e.validate()
# self.assertTrue(True)
#
# def test_event_model_factory_validation_invalid(self):
# e = DueDateFactory()
# e.start = get_date("Jan 04 2021")
# e.end = get_date("Jan 02 2021")
# self.assertRaises(ValueError, e.validate)
#
#
# def generate_duedates(start=timezone.now()):
# for i in range(20):
# DueDateFactory(
# title=f"{i}",
# start=start + datetime.timedelta(days=i),
# end=start + datetime.timedelta(days=i, hours=1),
# )

View File

@ -0,0 +1,9 @@
# class TestDueDateSerializer(TestCase):
# def test_duedate_serializer(self):
# pass
# create_test_course()
# self.assertEqual(DueDate.objects.count(), 1)
#
# duedates = DueDate.objects.all()
# result = DueDateSerializer(duedates, many=True).data
# self.assertEqual(result[0]["title"], "Prüfung Versicherungsvermittler/-in")

View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@ -5,6 +5,7 @@ from openpyxl.reader.excel import load_workbook
from vbv_lernwelt.core.models import User from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
from vbv_lernwelt.importer.utils import ( from vbv_lernwelt.importer.utils import (
calc_header_tuple_list_from_pyxl_sheet, calc_header_tuple_list_from_pyxl_sheet,
parse_circle_group_string, parse_circle_group_string,
@ -109,42 +110,37 @@ def create_or_update_course_session(
cs.save() cs.save()
for circle in circles: for circle in circles:
attendance_course_lp_qs = None
if language == "de": if language == "de":
attendance_course_lp_qs = LearningContentAttendanceCourse.objects.filter( attendance_course_lp_qs = LearningContentAttendanceCourse.objects.filter(
slug=f"{course.slug}-lp-circle-{circle.lower()}-lc-präsenzkurs-{circle.lower()}" slug=f"{course.slug}-lp-circle-{circle.lower()}-lc-präsenzkurs-{circle.lower()}"
) )
add_attendance_course_date(cs, attendance_course_lp_qs, circle, data)
elif language == "fr": elif language == "fr":
# todo: this is a hack remove me # todo: this is a hack remove me
attendance_course_lp_qs = LearningContentAttendanceCourse.objects.filter( attendance_course_lp_qs = LearningContentAttendanceCourse.objects.filter(
slug=f"{course.slug}-lp-circle-véhicule-lc-cours-de-présence-véhicule-à-moteur" slug=f"{course.slug}-lp-circle-véhicule-lc-cours-de-présence-véhicule-à-moteur"
) )
add_attendance_course_date(cs, attendance_course_lp_qs, circle, data)
elif language == "it": elif language == "it":
# todo: this is a hack remove me # todo: this is a hack remove me
attendance_course_lp_qs = LearningContentAttendanceCourse.objects.filter( attendance_course_lp_qs = LearningContentAttendanceCourse.objects.filter(
slug=f"{course.slug}-lp-circle-veicolo-lc-corso-di-presenza-veicolo" slug=f"{course.slug}-lp-circle-veicolo-lc-corso-di-presenza-veicolo"
) )
print(attendance_course_lp_qs)
add_attendance_course_date(cs, attendance_course_lp_qs, circle, data) if attendance_course_lp_qs and attendance_course_lp_qs.exists():
csa = CourseSessionAttendanceCourse.objects.create(
course_session=cs,
learning_content=attendance_course_lp_qs.first(),
location=data[f"{circle} Raum"],
trainer="",
)
csa.due_date.start = try_parse_datetime(data[f"{circle} Start"])[1]
csa.due_date.end = try_parse_datetime(data[f"{circle} Ende"])[1]
csa.due_date.save()
return cs return cs
def add_attendance_course_date(course_session, attendance_course_lp_qs, circle, data):
if attendance_course_lp_qs.exists():
course_session.attendance_courses.append(
{
"learningContentId": attendance_course_lp_qs.first().id,
"start": try_parse_datetime(data[f"{circle} Start"])[1].isoformat(),
"end": try_parse_datetime(data[f"{circle} Ende"])[1].isoformat(),
"location": data[f"{circle} Raum"],
"trainer": "",
}
)
course_session.save()
def import_trainers_from_excel(course: Course, filename: str, language="de"): def import_trainers_from_excel(course: Course, filename: str, language="de"):
workbook = load_workbook(filename=filename) workbook = load_workbook(filename=filename)
sheet = workbook["Schulungen Trainer"] sheet = workbook["Schulungen Trainer"]
@ -157,6 +153,7 @@ def import_trainers_from_excel(course: Course, filename: str, language="de"):
def create_or_update_trainer(course: Course, data: Dict[str, Any], language="de"): def create_or_update_trainer(course: Course, data: Dict[str, Any], language="de"):
logger.debug( logger.debug(
"create_or_update_trainer", "create_or_update_trainer",
course=course.title,
data=data, data=data,
label="import", label="import",
) )

View File

@ -5,6 +5,7 @@ from openpyxl.reader.excel import load_workbook
from vbv_lernwelt.course.creators.test_course import create_test_course from vbv_lernwelt.course.creators.test_course import create_test_course
from vbv_lernwelt.course.models import CourseSession from vbv_lernwelt.course.models import CourseSession
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
from vbv_lernwelt.importer.services import create_or_update_course_session from vbv_lernwelt.importer.services import create_or_update_course_session
from vbv_lernwelt.importer.utils import calc_header_tuple_list_from_pyxl_sheet from vbv_lernwelt.importer.utils import calc_header_tuple_list_from_pyxl_sheet
@ -61,20 +62,12 @@ class CreateOrUpdateCourseSessionTestCase(TestCase):
self.assertEqual(cs.region, "Deutschschweiz") self.assertEqual(cs.region, "Deutschschweiz")
self.assertEqual(cs.group, "A") self.assertEqual(cs.group, "A")
attendance_course = cs.attendance_courses[0] attendance_course = CourseSessionAttendanceCourse.objects.first()
attendance_course = { self.assertEqual(
k: v attendance_course.due_date.start.isoformat(), "2023-06-06T11:30:00+00:00"
for k, v in attendance_course.items() )
if k not in ["learningContentId", "location"] self.assertEqual(
} attendance_course.due_date.end.isoformat(), "2023-06-06T13:00:00+00:00"
self.assertDictEqual(
attendance_course,
{
"start": "2023-06-06T13:30:00",
"end": "2023-06-06T15:00:00",
"trainer": "",
},
) )
def test_update_course_session(self): def test_update_course_session(self):
@ -112,18 +105,10 @@ class CreateOrUpdateCourseSessionTestCase(TestCase):
self.assertEqual(cs.region, "Deutschschweiz") self.assertEqual(cs.region, "Deutschschweiz")
self.assertEqual(cs.group, "A") self.assertEqual(cs.group, "A")
attendance_course = cs.attendance_courses[0] attendance_course = CourseSessionAttendanceCourse.objects.first()
attendance_course = { self.assertEqual(
k: v attendance_course.due_date.start.isoformat(), "2023-06-06T11:30:00+00:00"
for k, v in attendance_course.items() )
if k not in ["learningContentId", "location"] self.assertEqual(
} attendance_course.due_date.end.isoformat(), "2023-06-06T13:00:00+00:00"
self.assertDictEqual(
attendance_course,
{
"start": "2023-06-06T13:30:00",
"end": "2023-06-06T15:00:00",
"trainer": "",
},
) )

View File

@ -0,0 +1,22 @@
# Generated by Django 3.2.13 on 2023-06-28 11:21
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("assignment", "0004_assignment_assignment_type"),
("learnpath", "0007_learningunit_title_hidden"),
]
operations = [
migrations.AlterField(
model_name="learningcontentassignment",
name="content_assignment",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to="assignment.assignment"
),
),
]

View File

@ -279,6 +279,10 @@ class LearningContent(CourseBasePage):
class LearningContentAttendanceCourse(LearningContent): class LearningContentAttendanceCourse(LearningContent):
"""
Präsenzkurs
"""
parent_page_types = ["learnpath.Circle"] parent_page_types = ["learnpath.Circle"]
subpage_types = [] subpage_types = []
@ -346,7 +350,6 @@ class LearningContentAssignment(LearningContent):
content_assignment = models.ForeignKey( content_assignment = models.ForeignKey(
"assignment.Assignment", "assignment.Assignment",
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name="+",
) )
assignment_type = models.CharField( assignment_type = models.CharField(

View File

@ -0,0 +1,162 @@
# Generated by Django 3.2.13 on 2023-07-12 07:05
import django.db.models.deletion
import django.utils.timezone
import jsonfield.fields
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("notify", "0001_initial"),
]
operations = [
migrations.AlterModelOptions(
name="notification",
options={
"ordering": ["-timestamp"],
"verbose_name": "Notification",
"verbose_name_plural": "Notifications",
},
),
migrations.AlterField(
model_name="notification",
name="action_object_content_type",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="notify_action_object",
to="contenttypes.contenttype",
verbose_name="action object content type",
),
),
migrations.AlterField(
model_name="notification",
name="action_object_object_id",
field=models.CharField(
blank=True,
max_length=255,
null=True,
verbose_name="action object object id",
),
),
migrations.AlterField(
model_name="notification",
name="actor_content_type",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="notify_actor",
to="contenttypes.contenttype",
verbose_name="actor content type",
),
),
migrations.AlterField(
model_name="notification",
name="actor_object_id",
field=models.CharField(max_length=255, verbose_name="actor object id"),
),
migrations.AlterField(
model_name="notification",
name="data",
field=jsonfield.fields.JSONField(
blank=True, null=True, verbose_name="data"
),
),
migrations.AlterField(
model_name="notification",
name="deleted",
field=models.BooleanField(
db_index=True, default=False, verbose_name="deleted"
),
),
migrations.AlterField(
model_name="notification",
name="description",
field=models.TextField(blank=True, null=True, verbose_name="description"),
),
migrations.AlterField(
model_name="notification",
name="emailed",
field=models.BooleanField(
db_index=True, default=False, verbose_name="emailed"
),
),
migrations.AlterField(
model_name="notification",
name="level",
field=models.CharField(
choices=[
("success", "success"),
("info", "info"),
("warning", "warning"),
("error", "error"),
],
default="info",
max_length=20,
verbose_name="level",
),
),
migrations.AlterField(
model_name="notification",
name="public",
field=models.BooleanField(
db_index=True, default=True, verbose_name="public"
),
),
migrations.AlterField(
model_name="notification",
name="recipient",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="notifications",
to=settings.AUTH_USER_MODEL,
verbose_name="recipient",
),
),
migrations.AlterField(
model_name="notification",
name="target_content_type",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="notify_target",
to="contenttypes.contenttype",
verbose_name="target content type",
),
),
migrations.AlterField(
model_name="notification",
name="target_object_id",
field=models.CharField(
blank=True, max_length=255, null=True, verbose_name="target object id"
),
),
migrations.AlterField(
model_name="notification",
name="timestamp",
field=models.DateTimeField(
db_index=True,
default=django.utils.timezone.now,
verbose_name="timestamp",
),
),
migrations.AlterField(
model_name="notification",
name="unread",
field=models.BooleanField(
db_index=True, default=True, verbose_name="unread"
),
),
migrations.AlterField(
model_name="notification",
name="verb",
field=models.CharField(max_length=255, verbose_name="verb"),
),
]

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Outline" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<path
d="m23.71,4.86h-2.86v-1.53c0-.41-.34-.75-.75-.75s-.75.34-.75.75v1.53h-8.71v-1.53c0-.41-.34-.75-.75-.75s-.75.34-.75.75v1.53h-2.86c-1.24,0-2.25,1.01-2.25,2.25v15.77c0,1.24,1.01,2.25,2.25,2.25h17.43c1.24,0,2.25-1.01,2.25-2.25V7.11c0-1.24-1.01-2.25-2.25-2.25Zm-17.43,1.5h17.43c.41,0,.75.34.75.75v2.21H5.54v-2.21c0-.41.34-.75.75-.75Zm17.43,17.27H6.29c-.41,0-.75-.34-.75-.75v-12.06h18.93v12.06c0,.41-.34.75-.75.75Z"/>
</svg>

After

Width:  |  Height:  |  Size: 550 B