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,
any
>
? TType extends { ' $fragmentName'?: infer TKey }
? [TType] extends [{ ' $fragmentName'?: infer TKey }]
? TKey extends string
? { ' $fragmentRefs'?: { [key in TKey]: TType } }
: never
@ -14,35 +16,51 @@ export type FragmentType<TDocumentType extends DocumentNode<any, any>> = TDocume
// return non-nullable if `fragmentType` is non-nullable
export function useFragment<TType>(
_documentNode: DocumentNode<TType, any>,
fragmentType: FragmentType<DocumentNode<TType, any>>
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>
): TType;
// return nullable if `fragmentType` is nullable
export function useFragment<TType>(
_documentNode: DocumentNode<TType, any>,
fragmentType: FragmentType<DocumentNode<TType, any>> | null | undefined
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null | undefined
): TType | null | undefined;
// return array of non-nullable if `fragmentType` is array of non-nullable
export function useFragment<TType>(
_documentNode: DocumentNode<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentNode<TType, any>>>
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
): ReadonlyArray<TType>;
// return array of nullable if `fragmentType` is array of nullable
export function useFragment<TType>(
_documentNode: DocumentNode<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentNode<TType, any>>> | null | undefined
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): ReadonlyArray<TType> | null | undefined;
export function useFragment<TType>(
_documentNode: DocumentNode<TType, any>,
fragmentType: FragmentType<DocumentNode<TType, any>> | ReadonlyArray<FragmentType<DocumentNode<TType, any>>> | null | undefined
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): TType | ReadonlyArray<TType> | null | undefined {
return fragmentType as any;
}
export function makeFragmentData<
F extends DocumentNode,
F extends DocumentTypeDecoration<any, any>,
FT extends ResultOf<F>
>(data: FT, _fragment: F): 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.
* 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 = {
"\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
* ```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!

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 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 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 */
export type Scalars = {
ID: string;
String: string;
Boolean: boolean;
Int: number;
Float: number;
ID: { input: string; output: string; }
String: { input: string; output: string; }
Boolean: { input: boolean; output: boolean; }
Int: { input: number; output: number; }
Float: { input: number; output: number; }
/**
* The `DateTime` scalar type represents a DateTime
* value as specified by
* [iso8601](https://en.wikipedia.org/wiki/ISO_8601).
*/
DateTime: any;
DateTime: { input: any; output: any; }
/**
* The `GenericScalar` scalar type represents a generic
* GraphQL scalar value that could be:
* String, Boolean, Int, Float, List or Object.
*/
GenericScalar: any;
JSONStreamField: any;
GenericScalar: { input: any; output: any; }
JSONStreamField: { input: any; output: any; }
/**
* 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
* schema (one of the key benefits of GraphQL).
*/
JSONString: any;
JSONString: { input: any; output: any; }
};
/** An enumeration. */
@ -61,19 +63,19 @@ export type AssignmentCompletionMutation = {
export type AssignmentCompletionObjectType = {
__typename?: 'AssignmentCompletionObjectType';
additional_json_data: Scalars['JSONString'];
additional_json_data: Scalars['JSONString']['output'];
assignment: AssignmentObjectType;
assignment_user: UserType;
completion_data?: Maybe<Scalars['GenericScalar']>;
completion_data?: Maybe<Scalars['GenericScalar']['output']>;
completion_status: AssignmentAssignmentCompletionCompletionStatusChoices;
created_at: Scalars['DateTime'];
evaluation_grade?: Maybe<Scalars['Float']>;
evaluation_points?: Maybe<Scalars['Float']>;
evaluation_submitted_at?: Maybe<Scalars['DateTime']>;
created_at: Scalars['DateTime']['output'];
evaluation_grade?: Maybe<Scalars['Float']['output']>;
evaluation_points?: Maybe<Scalars['Float']['output']>;
evaluation_submitted_at?: Maybe<Scalars['DateTime']['output']>;
evaluation_user?: Maybe<UserType>;
id: Scalars['ID'];
submitted_at?: Maybe<Scalars['DateTime']>;
updated_at: Scalars['DateTime'];
id: Scalars['ID']['output'];
submitted_at?: Maybe<Scalars['DateTime']['output']>;
updated_at: Scalars['DateTime']['output'];
};
/** An enumeration. */
@ -86,24 +88,24 @@ export type AssignmentCompletionStatus =
export type AssignmentObjectType = CoursePageInterface & {
__typename?: 'AssignmentObjectType';
assignment_type: AssignmentAssignmentAssignmentTypeChoices;
content_type?: Maybe<Scalars['String']>;
content_type?: Maybe<Scalars['String']['output']>;
/** Zeitaufwand als Text */
effort_required: Scalars['String'];
effort_required: Scalars['String']['output'];
/** Beschreibung der Bewertung */
evaluation_description: Scalars['String'];
evaluation_description: Scalars['String']['output'];
/** URL zum Beurteilungsinstrument */
evaluation_document_url: Scalars['String'];
evaluation_tasks?: Maybe<Scalars['JSONStreamField']>;
frontend_url?: Maybe<Scalars['String']>;
id?: Maybe<Scalars['ID']>;
evaluation_document_url: Scalars['String']['output'];
evaluation_tasks?: Maybe<Scalars['JSONStreamField']['output']>;
frontend_url?: Maybe<Scalars['String']['output']>;
id?: Maybe<Scalars['ID']['output']>;
/** Erläuterung der Ausgangslage */
intro_text: Scalars['String'];
live?: Maybe<Scalars['Boolean']>;
performance_objectives?: Maybe<Scalars['JSONStreamField']>;
slug?: Maybe<Scalars['String']>;
tasks?: Maybe<Scalars['JSONStreamField']>;
title?: Maybe<Scalars['String']>;
translation_key?: Maybe<Scalars['String']>;
intro_text: Scalars['String']['output'];
live?: Maybe<Scalars['Boolean']['output']>;
performance_objectives?: Maybe<Scalars['JSONStreamField']['output']>;
slug?: Maybe<Scalars['String']['output']>;
tasks?: Maybe<Scalars['JSONStreamField']['output']>;
title?: Maybe<Scalars['String']['output']>;
translation_key?: Maybe<Scalars['String']['output']>;
};
/** An enumeration. */
@ -116,69 +118,69 @@ export type CoreUserLanguageChoices =
| 'IT';
export type CoursePageInterface = {
content_type?: Maybe<Scalars['String']>;
frontend_url?: Maybe<Scalars['String']>;
id?: Maybe<Scalars['ID']>;
live?: Maybe<Scalars['Boolean']>;
slug?: Maybe<Scalars['String']>;
title?: Maybe<Scalars['String']>;
translation_key?: Maybe<Scalars['String']>;
content_type?: Maybe<Scalars['String']['output']>;
frontend_url?: Maybe<Scalars['String']['output']>;
id?: Maybe<Scalars['ID']['output']>;
live?: Maybe<Scalars['Boolean']['output']>;
slug?: Maybe<Scalars['String']['output']>;
title?: Maybe<Scalars['String']['output']>;
translation_key?: Maybe<Scalars['String']['output']>;
};
export type CourseType = {
__typename?: 'CourseType';
category_name: Scalars['String'];
id: Scalars['ID'];
category_name: Scalars['String']['output'];
id: Scalars['ID']['output'];
learning_path?: Maybe<LearningPathType>;
slug: Scalars['String'];
title: Scalars['String'];
slug: Scalars['String']['output'];
title: Scalars['String']['output'];
};
export type ErrorType = {
__typename?: 'ErrorType';
field: Scalars['String'];
messages: Array<Scalars['String']>;
field: Scalars['String']['output'];
messages: Array<Scalars['String']['output']>;
};
export type FeedbackResponse = Node & {
__typename?: 'FeedbackResponse';
created_at: Scalars['DateTime'];
data?: Maybe<Scalars['GenericScalar']>;
created_at: Scalars['DateTime']['output'];
data?: Maybe<Scalars['GenericScalar']['output']>;
/** The ID of the object */
id: Scalars['ID'];
id: Scalars['ID']['output'];
};
export type LearningPathType = CoursePageInterface & {
__typename?: 'LearningPathType';
content_type?: Maybe<Scalars['String']>;
depth: Scalars['Int'];
draft_title: Scalars['String'];
expire_at?: Maybe<Scalars['DateTime']>;
expired: Scalars['Boolean'];
first_published_at?: Maybe<Scalars['DateTime']>;
frontend_url?: Maybe<Scalars['String']>;
go_live_at?: Maybe<Scalars['DateTime']>;
has_unpublished_changes: Scalars['Boolean'];
id?: Maybe<Scalars['ID']>;
last_published_at?: Maybe<Scalars['DateTime']>;
latest_revision_created_at?: Maybe<Scalars['DateTime']>;
live?: Maybe<Scalars['Boolean']>;
locked: Scalars['Boolean'];
locked_at?: Maybe<Scalars['DateTime']>;
content_type?: Maybe<Scalars['String']['output']>;
depth: Scalars['Int']['output'];
draft_title: Scalars['String']['output'];
expire_at?: Maybe<Scalars['DateTime']['output']>;
expired: Scalars['Boolean']['output'];
first_published_at?: Maybe<Scalars['DateTime']['output']>;
frontend_url?: Maybe<Scalars['String']['output']>;
go_live_at?: Maybe<Scalars['DateTime']['output']>;
has_unpublished_changes: Scalars['Boolean']['output'];
id?: Maybe<Scalars['ID']['output']>;
last_published_at?: Maybe<Scalars['DateTime']['output']>;
latest_revision_created_at?: Maybe<Scalars['DateTime']['output']>;
live?: Maybe<Scalars['Boolean']['output']>;
locked: Scalars['Boolean']['output'];
locked_at?: Maybe<Scalars['DateTime']['output']>;
locked_by?: Maybe<UserType>;
numchild: Scalars['Int'];
numchild: Scalars['Int']['output'];
owner?: Maybe<UserType>;
path: Scalars['String'];
path: Scalars['String']['output'];
/** 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. */
seo_title: Scalars['String'];
seo_title: Scalars['String']['output'];
/** Ob ein Link zu dieser Seite in automatisch generierten Menüs auftaucht. */
show_in_menus: Scalars['Boolean'];
slug?: Maybe<Scalars['String']>;
title?: Maybe<Scalars['String']>;
translation_key?: Maybe<Scalars['String']>;
url_path: Scalars['String'];
show_in_menus: Scalars['Boolean']['output'];
slug?: Maybe<Scalars['String']['output']>;
title?: Maybe<Scalars['String']['output']>;
translation_key?: Maybe<Scalars['String']['output']>;
url_path: Scalars['String']['output'];
};
export type Mutation = {
@ -194,19 +196,19 @@ export type MutationSendFeedbackArgs = {
export type MutationUpsertAssignmentCompletionArgs = {
assignment_id: Scalars['ID'];
assignment_user_id?: InputMaybe<Scalars['ID']>;
completion_data_string?: InputMaybe<Scalars['String']>;
assignment_id: Scalars['ID']['input'];
assignment_user_id?: InputMaybe<Scalars['ID']['input']>;
completion_data_string?: InputMaybe<Scalars['String']['input']>;
completion_status?: InputMaybe<AssignmentCompletionStatus>;
course_session_id: Scalars['ID'];
evaluation_grade?: InputMaybe<Scalars['Float']>;
evaluation_points?: InputMaybe<Scalars['Float']>;
course_session_id: Scalars['ID']['input'];
evaluation_grade?: InputMaybe<Scalars['Float']['input']>;
evaluation_points?: InputMaybe<Scalars['Float']['input']>;
};
/** An object with an ID */
export type Node = {
/** The ID of the object */
id: Scalars['ID'];
id: Scalars['ID']['output'];
};
export type Query = {
@ -218,32 +220,32 @@ export type Query = {
export type QueryAssignmentArgs = {
id?: InputMaybe<Scalars['ID']>;
slug?: InputMaybe<Scalars['String']>;
id?: InputMaybe<Scalars['ID']['input']>;
slug?: InputMaybe<Scalars['String']['input']>;
};
export type QueryAssignmentCompletionArgs = {
assignment_id: Scalars['ID'];
assignment_user_id?: InputMaybe<Scalars['ID']>;
course_session_id: Scalars['ID'];
assignment_id: Scalars['ID']['input'];
assignment_user_id?: InputMaybe<Scalars['ID']['input']>;
course_session_id: Scalars['ID']['input'];
};
export type QueryCourseArgs = {
id?: InputMaybe<Scalars['Int']>;
id?: InputMaybe<Scalars['Int']['input']>;
};
export type SendFeedbackInput = {
clientMutationId?: InputMaybe<Scalars['String']>;
course_session: Scalars['Int'];
data?: InputMaybe<Scalars['GenericScalar']>;
page: Scalars['String'];
clientMutationId?: InputMaybe<Scalars['String']['input']>;
course_session: Scalars['Int']['input'];
data?: InputMaybe<Scalars['GenericScalar']['input']>;
page: Scalars['String']['input'];
};
export type SendFeedbackPayload = {
__typename?: 'SendFeedbackPayload';
clientMutationId?: Maybe<Scalars['String']>;
clientMutationId?: Maybe<Scalars['String']['output']>;
/** May contain more than one error for same field. */
errors?: Maybe<Array<Maybe<ErrorType>>>;
feedback_response?: Maybe<FeedbackResponse>;
@ -251,14 +253,14 @@ export type SendFeedbackPayload = {
export type UserType = {
__typename?: 'UserType';
avatar_url: Scalars['String'];
email: Scalars['String'];
first_name: Scalars['String'];
id: Scalars['ID'];
avatar_url: Scalars['String']['output'];
email: Scalars['String']['output'];
first_name: Scalars['String']['output'];
id: Scalars['ID']['output'];
language: CoreUserLanguageChoices;
last_name: Scalars['String'];
last_name: Scalars['String']['output'];
/** Erforderlich. 150 Zeichen oder weniger. Nur Buchstaben, Ziffern und @/./+/-/_. */
username: Scalars['String'];
username: Scalars['String']['output'];
};
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 UpsertAssignmentCompletionMutationVariables = Exact<{
assignmentId: Scalars['ID'];
courseSessionId: Scalars['ID'];
assignmentUserId?: InputMaybe<Scalars['ID']>;
assignmentId: Scalars['ID']['input'];
courseSessionId: Scalars['ID']['input'];
assignmentUserId?: InputMaybe<Scalars['ID']['input']>;
completionStatus: AssignmentCompletionStatus;
completionDataString: Scalars['String'];
evaluationGrade?: InputMaybe<Scalars['Float']>;
evaluationPoints?: InputMaybe<Scalars['Float']>;
completionDataString: Scalars['String']['input'];
evaluationGrade?: InputMaybe<Scalars['Float']['input']>;
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 AssignmentCompletionQueryQueryVariables = Exact<{
assignmentId: Scalars['ID'];
courseSessionId: Scalars['ID'];
assignmentUserId?: InputMaybe<Scalars['ID']>;
assignmentId: Scalars['ID']['input'];
courseSessionId: Scalars['ID']['input'];
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 CourseQueryQueryVariables = Exact<{
courseId: Scalars['Int'];
courseId: Scalars['Int']['input'];
}>;

View File

@ -11,9 +11,9 @@
"assignmentSubmitted": "Du hast deine Ergebnisse erfolgreich abgegeben.",
"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.",
"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",
"dueDateSubmission": "Einreichungstermin: {{date}}",
"dueDateSubmission": "Einreichungstermin:",
"dueDateTitle": "Abgabetermin",
"edit": "Bearbeiten",
"effortTitle": "Zeitaufwand",
@ -85,11 +85,14 @@
},
"dashboard": {
"courses": "Lehrgang",
"dueDatesTitle": "Termine",
"nocourses": "Du wurdest noch keinem Lehrgang zugewiesen.",
"welcome": "Willkommen, {{name}}"
},
"dueDates": {
"nextDueDates": "Nächste Termine"
"nextDueDates": "Nächste Termine",
"noDueDatesAvailable": "Keine Termine vorhanden",
"showAllDueDates": "Alle Termine anzeigen"
},
"feedback": {
"answers": "Antworten",
@ -185,6 +188,7 @@
"learningPathPage": {
"currentCircle": "Aktueller Circle",
"listView": "Listenansicht",
"nextDueDates": "Nächste Termine",
"nextStep": "Nächster Schritt",
"pathView": "Pfadansicht",
"progressText": "Du hast {{ inProgressCount }} von {{ allCount }} Circles bearbeitet",

View File

@ -11,9 +11,9 @@
"assignmentSubmitted": "Tes résultats ont bien été transmis.",
"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.",
"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.",
"dueDateSubmission": "Date de clôture : {{date}}",
"dueDateSubmission": "Date de clôture: ",
"dueDateTitle": "Date de remise",
"edit": "Traiter",
"effortTitle": "Temps nécessaire",

View File

@ -11,9 +11,9 @@
"assignmentSubmitted": "I tuoi risultati sono stati consegnati con successo.",
"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.",
"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",
"dueDateSubmission": "Termine di presentazione: {{date}}",
"dueDateSubmission": "Termine di presentazione: ",
"dueDateTitle": "Termine di consegna",
"edit": "Modificare",
"effortTitle": "Tempo richiesto",

View File

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

View File

@ -216,6 +216,15 @@ function log(data: any) {
ls-end
<it-icon-ls-end />
</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 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 {
Assignment,
AssignmentCompletion,
CourseSessionAssignmentDetails,
CourseSessionAssignment,
CourseSessionUser,
} from "@/types";
import { useQuery } from "@urql/vue";
@ -23,12 +23,12 @@ const props = defineProps<{
log.debug("AssignmentEvaluationPage created", props.assignmentId, props.userId);
interface StateInterface {
courseSessionAssignmentDetails: CourseSessionAssignmentDetails | undefined;
courseSessionAssignment: CourseSessionAssignment | undefined;
assignmentUser: CourseSessionUser | undefined;
}
const state: StateInterface = reactive({
courseSessionAssignmentDetails: undefined,
courseSessionAssignment: undefined,
assignmentUser: undefined,
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
<template>
<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>
<p class="grid-in-value">{{ start }} - {{ end }}</p>
<p class="grid-in-value">{{ formatDate(start, end) }}</p>
</div>
<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" />
@ -20,6 +20,7 @@
</template>
<script setup lang="ts">
import { formatDate } from "@/components/dueDates/dueDatesUtils";
import type { CourseSessionAttendanceCourse } from "@/types";
import dayjs from "dayjs";
import LocalizedFormat from "dayjs/plugin/localizedFormat";
@ -32,9 +33,8 @@ export interface Props {
const props = defineProps<Props>();
dayjs.extend(LocalizedFormat);
const format = "LLLL";
const start = computed(() => dayjs(props.attendanceCourse.start).format(format));
const end = computed(() => dayjs(props.attendanceCourse.end).format(format));
const start = computed(() => dayjs(props.attendanceCourse.start));
const end = computed(() => dayjs(props.attendanceCourse.end));
const location = computed(() => props.attendanceCourse.location);
const trainer = computed(() => props.attendanceCourse.trainer);
</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">
import * as d3 from "d3";
import * as log from "loglevel";
import { computed, onMounted } from "vue";
// @ts-ignore
@ -17,7 +16,6 @@ const props = defineProps<{
}>();
onMounted(async () => {
log.debug("LearningPathCircle mounted");
render();
});

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import type { AssignmentCompletionStatus as AssignmentCompletionStatusGenerated } from "@/gql/graphql";
import type { Circle } from "@/services/circle";
import type { Dayjs } from "dayjs";
import type { Component } from "vue";
export type LoginMethod = "local" | "sso";
@ -412,17 +413,24 @@ export interface CircleDocument {
}
export interface CourseSessionAttendanceCourse {
learningContentId: number;
id: number;
course_session_id: number;
learning_content_id: number;
start: string;
end: string;
location: string;
trainer: string;
due_date_id: number;
}
export interface CourseSessionAssignmentDetails {
learningContentId: number;
submissionDeadlineDateTimeUtc: string;
evaluationDeadlineDateTimeUtc: string;
export interface CourseSessionAssignment {
id: number;
course_session_id: number;
learning_content_id: number;
submission_deadline_id: number;
submission_deadline_start: string;
evaluation_deadline_id: number;
evaluation_deadline_start: string;
}
export interface CourseSession {
@ -439,9 +447,10 @@ export interface CourseSession {
course_url: string;
media_library_url: string;
attendance_courses: CourseSessionAttendanceCourse[];
assignment_details_list: CourseSessionAssignmentDetails[];
assignments: CourseSessionAssignment[];
documents: CircleDocument[];
users: CourseSessionUser[];
due_dates: DueDate[];
}
export type Role = "MEMBER" | "EXPERT" | "TUTOR";
@ -556,3 +565,15 @@ export interface UserAssignmentCompletionStatus {
completion_status: AssignmentCompletionStatus;
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.competence",
"vbv_lernwelt.media_library",
"vbv_lernwelt.course_session",
"vbv_lernwelt.feedback",
"vbv_lernwelt.files",
"vbv_lernwelt.notify",
"vbv_lernwelt.assignment",
"vbv_lernwelt.duedate",
]
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS

View File

@ -1,7 +1,10 @@
import json
import random
from datetime import datetime, timedelta
import wagtail_factories
from django.conf import settings
from django.utils import timezone
from slugify import slugify
from wagtail.models import Site
from wagtail.rich_text import RichText
@ -34,7 +37,15 @@ from vbv_lernwelt.course.models import (
CourseSession,
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 (
CircleFactory,
LearningContentAssignmentFactory,
@ -79,16 +90,19 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
create_test_media_library()
if with_sessions:
now = timezone.now()
# course sessions
cs_bern = CourseSession.objects.create(
course_id=COURSE_TEST_ID,
title="Test Bern 2022 a",
id=TEST_COURSE_SESSION_BERN_ID,
start_date=now,
)
cs_zurich = CourseSession.objects.create(
course_id=COURSE_TEST_ID,
title="Test Zürich 2022 a",
id=TEST_COURSE_SESSION_ZURICH_ID,
start_date=now,
)
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,
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
@ -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):
if apps is not None:
Course = apps.get_model("course", "Course")

View File

@ -1,7 +1,9 @@
import os
import random
from datetime import datetime, timedelta
import djclick as click
from django.utils import timezone
from vbv_lernwelt.assignment.creators.create_assignments import (
create_uk_basis_prep_assignment,
@ -68,6 +70,10 @@ from vbv_lernwelt.course.models import (
CourseSessionUser,
)
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.importer.services import (
import_course_sessions_from_excel,
@ -237,35 +243,34 @@ def create_course_uk_de():
cs = CourseSession.objects.create(
course_id=COURSE_UK,
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
csu = CourseSessionUser.objects.create(
course_session=cs,
@ -537,24 +542,12 @@ def create_course_training_de():
f"{current_dir}/../../../importer/tests/Schulungen_Teilnehmende.xlsx",
)
for cs in CourseSession.objects.filter(course_id=COURSE_UK_TRAINING):
cs.assignment_details_list = [
{
"learningContentId": LearningContentAssignment.objects.get(
slug=f"{course.slug}-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=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()
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,
)
# attach users as trainers to ÜK course
course_uk = Course.objects.filter(id=COURSE_UK).first()
@ -586,6 +579,24 @@ def create_course_training_de():
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():
# Test Lehrgang für üK Trainer FR
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):
cs.assignment_details_list = [
{
"learningContentId": LearningContentAssignment.objects.get(
slug=f"{course.slug}-lp-circle-véhicule-lc-vérification-dune-police-dassurance-de-véhicule-à-moteur"
).id,
"submissionDeadlineDateTimeUtc": "2023-06-13T19:00:00Z",
"evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z",
},
{
"learningContentId": LearningContentAssignment.objects.get(
slug=f"{course.slug}-lp-circle-véhicule-lc-véhicule-à-moteur-ma-première-voiture"
).id,
"submissionDeadlineDateTimeUtc": "2023-06-13T19:00:00Z",
"evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z",
},
]
# cs.assignment_details_list = [
# {
# "learningContentId": LearningContentAssignment.objects.get(
# slug=f"{course.slug}-lp-circle-véhicule-lc-vérification-dune-police-dassurance-de-véhicule-à-moteur"
# ).id,
# "submissionDeadlineDateTimeUtc": "2023-06-13T19:00:00Z",
# "evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z",
# },
# {
# "learningContentId": LearningContentAssignment.objects.get(
# slug=f"{course.slug}-lp-circle-véhicule-lc-véhicule-à-moteur-ma-première-voiture"
# ).id,
# "submissionDeadlineDateTimeUtc": "2023-06-13T19:00:00Z",
# "evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z",
# },
# ]
cs.save()
# 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):
cs.assignment_details_list = [
{
"learningContentId": LearningContentAssignment.objects.get(
slug=f"{course.slug}-lp-circle-veicolo-lc-verifica-di-una-polizza-di-assicurazione-veicoli-a-motore"
).id,
"submissionDeadlineDateTimeUtc": "2023-06-20T19:00:00Z",
"evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z",
},
{
"learningContentId": LearningContentAssignment.objects.get(
slug=f"{course.slug}-lp-circle-veicolo-lc-veicolo-la-mia-prima-auto"
).id,
"submissionDeadlineDateTimeUtc": "2023-06-20T19:00:00Z",
"evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z",
},
]
# cs.assignment_details_list = [
# {
# "learningContentId": LearningContentAssignment.objects.get(
# slug=f"{course.slug}-lp-circle-veicolo-lc-verifica-di-una-polizza-di-assicurazione-veicoli-a-motore"
# ).id,
# "submissionDeadlineDateTimeUtc": "2023-06-20T19:00:00Z",
# "evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z",
# },
# {
# "learningContentId": LearningContentAssignment.objects.get(
# slug=f"{course.slug}-lp-circle-veicolo-lc-veicolo-la-mia-prima-auto"
# ).id,
# "submissionDeadlineDateTimeUtc": "2023-06-20T19:00:00Z",
# "evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z",
# },
# ]
cs.save()
# 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.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from django_jsonform.models.fields import JSONField
from grapple.models import GraphQLString
from wagtail.models import Page
@ -193,24 +192,6 @@ class CourseSession(models.Model):
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)
updated_at = models.DateTimeField(auto_now=True)
@ -226,11 +207,6 @@ class CourseSession(models.Model):
start_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)
def __str__(self):

View File

@ -7,6 +7,16 @@ from vbv_lernwelt.course.models import (
CourseCompletion,
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):
@ -50,6 +60,9 @@ class CourseSessionSerializer(serializers.ModelSerializer):
competence_url = serializers.SerializerMethodField()
media_library_url = serializers.SerializerMethodField()
documents = serializers.SerializerMethodField()
attendance_courses = serializers.SerializerMethodField()
assignments = serializers.SerializerMethodField()
due_dates = serializers.SerializerMethodField()
def get_course(self, obj):
return CourseSerializer(obj.course).data
@ -75,6 +88,20 @@ class CourseSessionSerializer(serializers.ModelSerializer):
)
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:
model = CourseSession
fields = [
@ -87,13 +114,14 @@ class CourseSessionSerializer(serializers.ModelSerializer):
"end_date",
"additional_json_data",
"attendance_courses",
"assignment_details_list",
"assignments",
"learning_path_url",
"cockpit_url",
"competence_url",
"media_library_url",
"course_url",
"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.course.models import Course, CourseSession, CourseSessionUser
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
from vbv_lernwelt.importer.utils import (
calc_header_tuple_list_from_pyxl_sheet,
parse_circle_group_string,
@ -109,42 +110,37 @@ def create_or_update_course_session(
cs.save()
for circle in circles:
attendance_course_lp_qs = None
if language == "de":
attendance_course_lp_qs = LearningContentAttendanceCourse.objects.filter(
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":
# todo: this is a hack remove me
attendance_course_lp_qs = LearningContentAttendanceCourse.objects.filter(
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":
# todo: this is a hack remove me
attendance_course_lp_qs = LearningContentAttendanceCourse.objects.filter(
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
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"):
workbook = load_workbook(filename=filename)
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"):
logger.debug(
"create_or_update_trainer",
course=course.title,
data=data,
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.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.utils import calc_header_tuple_list_from_pyxl_sheet
@ -61,20 +62,12 @@ class CreateOrUpdateCourseSessionTestCase(TestCase):
self.assertEqual(cs.region, "Deutschschweiz")
self.assertEqual(cs.group, "A")
attendance_course = cs.attendance_courses[0]
attendance_course = {
k: v
for k, v in attendance_course.items()
if k not in ["learningContentId", "location"]
}
self.assertDictEqual(
attendance_course,
{
"start": "2023-06-06T13:30:00",
"end": "2023-06-06T15:00:00",
"trainer": "",
},
attendance_course = CourseSessionAttendanceCourse.objects.first()
self.assertEqual(
attendance_course.due_date.start.isoformat(), "2023-06-06T11:30:00+00:00"
)
self.assertEqual(
attendance_course.due_date.end.isoformat(), "2023-06-06T13:00:00+00:00"
)
def test_update_course_session(self):
@ -112,18 +105,10 @@ class CreateOrUpdateCourseSessionTestCase(TestCase):
self.assertEqual(cs.region, "Deutschschweiz")
self.assertEqual(cs.group, "A")
attendance_course = cs.attendance_courses[0]
attendance_course = {
k: v
for k, v in attendance_course.items()
if k not in ["learningContentId", "location"]
}
self.assertDictEqual(
attendance_course,
{
"start": "2023-06-06T13:30:00",
"end": "2023-06-06T15:00:00",
"trainer": "",
},
attendance_course = CourseSessionAttendanceCourse.objects.first()
self.assertEqual(
attendance_course.due_date.start.isoformat(), "2023-06-06T11:30:00+00:00"
)
self.assertEqual(
attendance_course.due_date.end.isoformat(), "2023-06-06T13:00:00+00:00"
)

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):
"""
Präsenzkurs
"""
parent_page_types = ["learnpath.Circle"]
subpage_types = []
@ -346,7 +350,6 @@ class LearningContentAssignment(LearningContent):
content_assignment = models.ForeignKey(
"assignment.Assignment",
on_delete=models.PROTECT,
related_name="+",
)
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