Merged develop into feature/VBV-704-eine-note-im-kn-für-ük

This commit is contained in:
Elia Bieri 2024-07-31 12:54:26 +00:00
commit 6f2b437a5c
70 changed files with 1768 additions and 590 deletions

View File

@ -97,6 +97,7 @@ js-linting: &js-linting
default-steps: &default-steps default-steps: &default-steps
- parallel: - parallel:
- step: *e2e
- step: *e2e - step: *e2e
- step: *e2e - step: *e2e
- step: *python-tests - step: *python-tests

View File

@ -93,8 +93,12 @@ def main(app_name, image_name, environment_file):
"AWS_S3_SECRET_ACCESS_KEY": env.str("AWS_S3_SECRET_ACCESS_KEY", ""), "AWS_S3_SECRET_ACCESS_KEY": env.str("AWS_S3_SECRET_ACCESS_KEY", ""),
"AWS_S3_REGION_NAME": "eu-central-1", "AWS_S3_REGION_NAME": "eu-central-1",
"AWS_STORAGE_BUCKET_NAME": "myvbv-dev.iterativ.ch", "AWS_STORAGE_BUCKET_NAME": "myvbv-dev.iterativ.ch",
"DATATRANS_HMAC_KEY": env.str("DATATRANS_HMAC_KEY", ""), "DATATRANS_HMAC_KEY": env.str("PIPELINES_DATATRANS_HMAC_KEY", ""),
"DATATRANS_BASIC_AUTH_KEY": env.str("DATATRANS_BASIC_AUTH_KEY", ""), "DATATRANS_BASIC_AUTH_KEY": env.str(
"PIPELINES_DATATRANS_BASIC_AUTH_KEY", ""
),
"DATATRANS_API_ENDPOINT": "https://api.sandbox.datatrans.com",
"DATATRANS_PAY_URL": "https://pay.sandbox.datatrans.com",
"FILE_UPLOAD_STORAGE": "s3", "FILE_UPLOAD_STORAGE": "s3",
"IT_DJANGO_DEBUG": "false", "IT_DJANGO_DEBUG": "false",
"IT_SERVE_VUE": "false", "IT_SERVE_VUE": "false",

View File

@ -6,6 +6,7 @@ import type { CourseStatisticsType } from "@/gql/graphql";
import AssignmentSummaryBox from "@/components/dashboard/AssignmentSummaryBox.vue"; import AssignmentSummaryBox from "@/components/dashboard/AssignmentSummaryBox.vue";
import FeedbackSummaryBox from "@/components/dashboard/FeedbackSummaryBox.vue"; import FeedbackSummaryBox from "@/components/dashboard/FeedbackSummaryBox.vue";
import CompetenceSummaryBox from "@/components/dashboard/CompetenceSummaryBox.vue"; import CompetenceSummaryBox from "@/components/dashboard/CompetenceSummaryBox.vue";
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
const props = defineProps<{ const props = defineProps<{
courseId: string; courseId: string;
@ -95,4 +96,7 @@ onMounted(async () => {
/> />
</div> </div>
</div> </div>
<div v-else class="flex w-full flex-row justify-center">
<LoadingSpinner />
</div>
</template> </template>

View File

@ -3,6 +3,7 @@ import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPa
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils"; import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
import { computed } from "vue"; import { computed } from "vue";
import { useCourseCircleProgress, useCourseDataWithCompletion } from "@/composables"; import { useCourseCircleProgress, useCourseDataWithCompletion } from "@/composables";
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
export type DiagramType = "horizontal" | "horizontalSmall" | "singleSmall"; export type DiagramType = "horizontal" | "horizontalSmall" | "singleSmall";
@ -54,23 +55,25 @@ const { inProgressCirclesCount, circlesCount } = useCourseCircleProgress(
<template> <template>
<div> <div>
<h4 <div v-if="circlesCount > 0">
v-if="diagramType === 'horizontal' && circles.length > 0" <h4 v-if="diagramType === 'horizontal'" class="mb-4 font-bold">
class="mb-4 font-bold" {{
> $t("learningPathPage.progressText", {
{{ inProgressCount: inProgressCirclesCount,
$t("learningPathPage.progressText", { allCount: circlesCount,
inProgressCount: inProgressCirclesCount, })
allCount: circlesCount, }}
}) </h4>
}} <div :class="wrapperClasses">
</h4> <LearningPathCircle
<div :class="wrapperClasses"> v-for="circle in circles"
<LearningPathCircle :key="circle.id"
v-for="circle in circles" :sectors="calculateCircleSectorData(circle)"
:key="circle.id" ></LearningPathCircle>
:sectors="calculateCircleSectorData(circle)" </div>
></LearningPathCircle> </div>
<div v-else class="flex justify-center">
<LoadingSpinner />
</div> </div>
</div> </div>
</template> </template>

View File

@ -34,7 +34,7 @@ const orgAddress = computed({
for="company-name" for="company-name"
class="block text-sm font-medium leading-6 text-gray-900" class="block text-sm font-medium leading-6 text-gray-900"
> >
{{ $t("a.Name") }} {{ $t("a.Firmenname") }}
</label> </label>
<div class="mt-2"> <div class="mt-2">
<input <input

View File

@ -1,10 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from "vue"; import { computed } from "vue";
import { useEntities } from "@/services/entities"; import { useEntities } from "@/services/entities";
import VueDatePicker from "@vuepic/vue-datepicker";
import "@vuepic/vue-datepicker/dist/main.css"; import "@vuepic/vue-datepicker/dist/main.css";
import { t } from "i18next"; import { t } from "i18next";
import { useUserStore } from "@/stores/user"; import ItDatePicker from "@/components/ui/ItDatePicker.vue";
const props = defineProps<{ const props = defineProps<{
modelValue: { modelValue: {
@ -25,20 +24,12 @@ const props = defineProps<{
const emit = defineEmits(["update:modelValue"]); const emit = defineEmits(["update:modelValue"]);
const { countries } = useEntities(); const { countries } = useEntities();
const userStore = useUserStore();
const paymentMethods = [ const paymentMethods = [
{ value: "credit_card", label: t("a.Debit-/Kreditkarte/Twint") }, { value: "credit_card", label: t("a.Debit-/Kreditkarte/Twint") },
{ value: "cembra_byjuno", label: t("a.Rechnung") }, { value: "cembra_byjuno", label: t("a.Rechnung") },
]; ];
// TODO: remove after cembra is ready for production
const appEnv = import.meta.env.VITE_APP_ENVIRONMENT || "local";
if (appEnv.startsWith("prod")) {
paymentMethods.splice(1, 1);
}
// END TODO
const address = computed({ const address = computed({
get() { get() {
return props.modelValue; return props.modelValue;
@ -234,40 +225,8 @@ const address = computed({
{{ $t("a.Geburtsdatum") }} {{ $t("a.Geburtsdatum") }}
</label> </label>
<div class="mt-2"> <div class="mt-2">
<VueDatePicker <ItDatePicker v-model="address.birth_date"></ItDatePicker>
v-model="address.birth_date"
format="dd.MM.yyyy"
no-today
model-type="yyyy-MM-dd"
name="birth-date"
max-date="2007-01-01"
prevent-min-max-navigation
required
:enable-time-picker="false"
text-input
placeholder="15.06.1982"
start-date="1982-01-01"
:locale="userStore.language"
:cancel-text="$t('a.Abbrechen')"
:select-text="$t('a.Auswählen')"
></VueDatePicker>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<style>
/* Theming for date picker */
.dp__theme_light {
--dp-text-color: #585f63;
--dp-primary-color: #41b5fa;
--dp-border-color-focus: #3d6dcc;
}
:root {
--dp-font-family: "Buenos Aires" sans-serif;
--dp-border-radius: none;
--dp-font-size: 0.875rem;
--dp-cell-border-radius: none;
}
</style>

View File

@ -3,6 +3,8 @@ import { useEntities } from "@/services/entities";
import AvatarImage from "@/components/ui/AvatarImage.vue"; import AvatarImage from "@/components/ui/AvatarImage.vue";
import { ref } from "vue"; import { ref } from "vue";
import { type User, useUserStore } from "@/stores/user"; import { type User, useUserStore } from "@/stores/user";
import ItDatePicker from "@/components/ui/ItDatePicker.vue";
import { normalizeSwissPhoneNumber } from "@/utils/phone";
const emit = defineEmits(["cancel", "save"]); const emit = defineEmits(["cancel", "save"]);
@ -21,7 +23,12 @@ const formData = ref({
postal_code: user.postal_code, postal_code: user.postal_code,
city: user.city, city: user.city,
country_code: user.country?.country_code, country_code: user.country?.country_code,
phone_number: user.phone_number,
birth_date: user.birth_date,
organisation: user.organisation, organisation: user.organisation,
organisation_detail_name: user.organisation_detail_name,
organisation_street: user.organisation_street, organisation_street: user.organisation_street,
organisation_street_number: user.organisation_street_number, organisation_street_number: user.organisation_street_number,
organisation_postal_code: user.organisation_postal_code, organisation_postal_code: user.organisation_postal_code,
@ -31,7 +38,8 @@ const formData = ref({
}); });
async function save() { async function save() {
const { country_code, organisation_country_code, ...profileData } = formData.value; const { country_code, organisation_country_code, phone_number, ...profileData } =
formData.value;
const typedProfileData: Partial<User> = { ...profileData }; const typedProfileData: Partial<User> = { ...profileData };
typedProfileData.country = countries.value.find( typedProfileData.country = countries.value.find(
@ -41,6 +49,10 @@ async function save() {
(c) => c.country_code === organisation_country_code (c) => c.country_code === organisation_country_code
); );
if (phone_number) {
typedProfileData.phone_number = normalizeSwissPhoneNumber(phone_number);
}
await user.updateUserProfile(typedProfileData); await user.updateUserProfile(typedProfileData);
emit("save"); emit("save");
} }
@ -126,6 +138,28 @@ async function avatarUpload(e: Event) {
disabled disabled
class="disabled:bg-gray-50 mb-4 block w-full border-0 py-1.5 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 disabled:cursor-not-allowed disabled:text-gray-500 disabled:ring-gray-200 sm:max-w-sm sm:text-sm sm:leading-6" class="disabled:bg-gray-50 mb-4 block w-full border-0 py-1.5 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 disabled:cursor-not-allowed disabled:text-gray-500 disabled:ring-gray-200 sm:max-w-sm sm:text-sm sm:leading-6"
/> />
<label for="phone" class="block pb-1.5 leading-6">
{{ $t("a.Telefonnummer") }}
</label>
<div>
<input
id="phone"
v-model="formData.phone_number"
type="text"
name="phone"
autocomplete="phone-number"
class="disabled:bg-gray-50 mb-4 block w-full border-0 py-1.5 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 disabled:cursor-not-allowed disabled:text-gray-500 disabled:ring-gray-200 sm:max-w-sm sm:text-sm sm:leading-6"
/>
</div>
<label for="birth-date" class="block pb-1.5 leading-6">
{{ $t("a.Geburtsdatum") }}
</label>
<div class="mb-4 block w-full py-1.5 sm:max-w-sm sm:text-sm sm:leading-6">
<ItDatePicker v-model="formData.birth_date"></ItDatePicker>
</div>
<label class="block pb-1.5 leading-6"> <label class="block pb-1.5 leading-6">
{{ $t("a.Profilbild") }} {{ $t("a.Profilbild") }}
</label> </label>
@ -264,6 +298,22 @@ async function avatarUpload(e: Event) {
{{ $t("a.Firmenanschrift") }} {{ $t("a.Firmenanschrift") }}
</h4> </h4>
<div class="flex flex-col justify-start md:flex-row md:space-x-4">
<div class="w-full md:max-w-lg">
<label for="org-street-address" class="block pb-1.5 leading-6">
{{ $t("a.Firmenname") }}
</label>
<input
id="org-detail-name"
v-model="formData.organisation_detail_name"
type="text"
name="org-detail-name"
class="disabled:bg-gray-50 mb-4 block w-full border-0 py-1.5 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 disabled:cursor-not-allowed disabled:text-gray-500 disabled:ring-gray-200 sm:text-sm sm:leading-6"
/>
</div>
</div>
<div class="flex flex-col justify-start md:flex-row md:space-x-4"> <div class="flex flex-col justify-start md:flex-row md:space-x-4">
<div class="w-full md:max-w-sm"> <div class="w-full md:max-w-sm">
<label for="org-street-address" class="block pb-1.5 leading-6"> <label for="org-street-address" class="block pb-1.5 leading-6">
@ -389,7 +439,7 @@ async function avatarUpload(e: Event) {
<button class="btn btn-secondary" @click="emit('cancel')"> <button class="btn btn-secondary" @click="emit('cancel')">
{{ $t("general.cancel") }} {{ $t("general.cancel") }}
</button> </button>
<button class="btn btn-primary" @click="save"> <button class="btn btn-primary" data-cy="saveButton" @click="save">
{{ $t("general.save") }} {{ $t("general.save") }}
</button> </button>
</div> </div>

View File

@ -3,6 +3,8 @@ import { useUserStore } from "@/stores/user";
import { computed } from "vue"; import { computed } from "vue";
import { useEntities } from "@/services/entities"; import { useEntities } from "@/services/entities";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
import dayjs from "dayjs";
import { displaySwissPhoneNumber } from "@/utils/phone";
const { t } = useTranslation(); const { t } = useTranslation();
@ -10,21 +12,20 @@ const user = useUserStore();
const { organisations } = useEntities(); const { organisations } = useEntities();
const privateAddress = computed(() => { const privateAddress = computed(() => {
let addressText = `${user.street} ${user.street_number}`.trim(); const textParts = [];
if (user.postal_code || user.city) {
if (addressText.length) { if (user.street || user.street_number) {
addressText += ", "; textParts.push(`${user.street} ${user.street_number}`.trim());
}
addressText += `${user.postal_code} ${user.city}`;
}
if (user.country) {
if (addressText.length) {
addressText += ", ";
}
addressText += user.country.name;
} }
return addressText.trim(); if (user.postal_code || user.city) {
textParts.push(`${user.postal_code} ${user.city}`);
}
if (textParts.length && user.country) {
textParts.push(user.country.name);
}
return textParts;
}); });
const organisationName = computed(() => { const organisationName = computed(() => {
@ -36,22 +37,25 @@ const organisationName = computed(() => {
}); });
const orgAddress = computed(() => { const orgAddress = computed(() => {
let addressText = const textParts = [];
`${user.organisation_street} ${user.organisation_street_number}`.trim();
if (user.organisation_postal_code || user.organisation_city) { if (user.organisation_detail_name) {
if (addressText.length) { textParts.push(user.organisation_detail_name);
addressText += ", ";
}
addressText += `${user.organisation_postal_code} ${user.organisation_city}`;
}
if (user.organisation_country) {
if (addressText.length) {
addressText += ", ";
}
addressText += user.organisation_country.name;
} }
return addressText.trim(); if (user.organisation_street || user.organisation_street_number) {
textParts.push(
`${user.organisation_street} ${user.organisation_street_number}`.trim()
);
}
if (user.organisation_postal_code || user.organisation_city) {
textParts.push(`${user.organisation_postal_code} ${user.organisation_city}`);
}
if (textParts.length && user.organisation_country) {
textParts.push(user.organisation_country.name);
}
return textParts;
}); });
const invoiceAddress = computed(() => { const invoiceAddress = computed(() => {
@ -67,20 +71,45 @@ const invoiceAddress = computed(() => {
<h3 class="mb-2">{{ $t("a.Persönliche Informationen") }}</h3> <h3 class="mb-2">{{ $t("a.Persönliche Informationen") }}</h3>
<div class="sm:grid sm:grid-cols-3 sm:items-start sm:gap-8 sm:py-6"> <div class="sm:grid sm:grid-cols-3 sm:items-start sm:gap-8 sm:py-6">
<label class="block font-semibold leading-6">{{ $t("a.Vorname") }}</label> <label class="block font-semibold leading-6">{{ $t("a.Vorname") }}</label>
<div class="mb-3 sm:col-span-2 sm:mb-0">{{ user.first_name }}</div> <div class="mb-3 sm:col-span-2 sm:mb-0" data-cy="firstName">
{{ user.first_name }}
</div>
<label class="block font-semibold leading-6">{{ $t("a.Name") }}</label> <label class="block font-semibold leading-6">{{ $t("a.Name") }}</label>
<div class="mb-3 sm:col-span-2 sm:mb-0">{{ user.last_name }}</div> <div class="mb-3 sm:col-span-2 sm:mb-0" data-cy="lastName">
{{ user.last_name }}
</div>
<label class="block font-semibold leading-6"> <label class="block font-semibold leading-6">
{{ $t("a.E-Mail Adresse") }} {{ $t("a.E-Mail Adresse") }}
</label> </label>
<div class="mb-3 sm:col-span-2 sm:mb-0">{{ user.email }}</div> <div class="mb-3 sm:col-span-2 sm:mb-0" data-cy="email">{{ user.email }}</div>
<label class="block font-semibold leading-6">
{{ $t("a.Telefonnummer") }}
</label>
<div class="mb-3 sm:col-span-2 sm:mb-0" data-cy="phone">
<span v-if="user.phone_number">
{{ displaySwissPhoneNumber(user.phone_number) }}
</span>
<span v-else class="text-gray-800">{{ $t("a.Keine Angabe") }}</span>
</div>
<label class="block font-semibold leading-6">
{{ $t("a.Geburtsdatum") }}
</label>
<div class="mb-3 sm:col-span-2 sm:mb-0" data-cy="birthDate">
<span v-if="user.birth_date">
{{ dayjs(user.birth_date).format("DD.MM.YYYY") }}
</span>
<span v-else class="text-gray-800">{{ $t("a.Keine Angabe") }}</span>
</div>
<label class="block font-semibold leading-6"> <label class="block font-semibold leading-6">
{{ $t("a.Privatadresse") }} {{ $t("a.Privatadresse") }}
</label> </label>
<div class="mb-3 sm:col-span-2 sm:mb-0"> <div class="mb-3 sm:col-span-2 sm:mb-0" data-cy="privateAddress">
<template v-if="privateAddress"> <div v-if="privateAddress.length">
{{ privateAddress }} <span v-for="(line, index) in privateAddress" :key="index">
</template> {{ line }}
<br />
</span>
</div>
<span v-else class="text-gray-800">{{ $t("a.Keine Angabe") }}</span> <span v-else class="text-gray-800">{{ $t("a.Keine Angabe") }}</span>
</div> </div>
</div> </div>
@ -89,14 +118,19 @@ const invoiceAddress = computed(() => {
<h3 class="my-2">{{ $t("a.Geschäftsdaten") }}</h3> <h3 class="my-2">{{ $t("a.Geschäftsdaten") }}</h3>
<div class="sm:grid sm:grid-cols-3 sm:items-start sm:gap-8 sm:py-6"> <div class="sm:grid sm:grid-cols-3 sm:items-start sm:gap-8 sm:py-6">
<label class="block font-semibold leading-6">{{ $t("a.Unternehmen") }}</label> <label class="block font-semibold leading-6">{{ $t("a.Unternehmen") }}</label>
<div class="mb-3 sm:col-span-2 sm:mb-0">{{ organisationName }}</div> <div class="mb-3 sm:col-span-2 sm:mb-0" data-cy="organisationDetailName">
{{ organisationName }}
</div>
<label class="block font-semibold leading-6"> <label class="block font-semibold leading-6">
{{ $t("a.Firmenanschrift") }} {{ $t("a.Firmenanschrift") }}
</label> </label>
<div class="sm:col-span-2"> <div class="sm:col-span-2">
<template v-if="orgAddress"> <div v-if="orgAddress" data-cy="organisationAddress">
{{ orgAddress }} <span v-for="(line, index) in orgAddress" :key="index">
</template> {{ line }}
<br />
</span>
</div>
<span v-else class="text-gray-800">{{ $t("a.Keine Angabe") }}</span> <span v-else class="text-gray-800">{{ $t("a.Keine Angabe") }}</span>
</div> </div>
</div> </div>

View File

@ -0,0 +1,60 @@
<script setup lang="ts">
import { useUserStore } from "@/stores/user";
import VueDatePicker from "@vuepic/vue-datepicker";
import "@vuepic/vue-datepicker/dist/main.css";
import { defineProps, withDefaults, defineModel } from "vue";
const model = defineModel<string>();
export interface Props {
noToday?: boolean;
required?: boolean;
placeholder?: string;
startDate?: string;
maxDate?: string;
}
const props = withDefaults(defineProps<Props>(), {
required: false,
placeholder: "15.06.1982",
startDate: "1982-01-01",
maxDate: "2007-01-01",
});
const userStore = useUserStore();
</script>
<template>
<VueDatePicker
v-model="model"
format="dd.MM.yyyy"
model-type="yyyy-MM-dd"
name="date"
prevent-min-max-navigation
:required="props.required"
:enable-time-picker="false"
text-input
:no-today="props.noToday"
:max-date="props.maxDate"
:placeholder="props.placeholder"
:start-date="props.startDate"
:locale="userStore.language"
:cancel-text="$t('a.Abbrechen')"
:select-text="$t('a.Auswählen')"
></VueDatePicker>
</template>
<style>
.dp__theme_light {
--dp-text-color: #585f63;
--dp-primary-color: #41b5fa;
--dp-border-color-focus: #3d6dcc;
}
:root {
--dp-font-family: "Buenos Aires" sans-serif;
--dp-border-radius: none;
--dp-font-size: 0.875rem;
--dp-cell-border-radius: none;
}
</style>

View File

@ -1,18 +1,30 @@
<script setup lang="ts"> <script setup lang="ts">
import ItRow from "@/components/ui/ItRow.vue"; import ItRow from "@/components/ui/ItRow.vue";
defineProps<{ export interface Props {
avatarUrl: string; avatarUrl: string;
name: string; name: string;
}>(); extraInfo?: string;
}
const props = withDefaults(defineProps<Props>(), {
extraInfo: "",
});
</script> </script>
<template> <template>
<ItRow> <ItRow>
<template #firstRow> <template #firstRow>
<slot name="leading"></slot> <slot name="leading"></slot>
<img class="mr-2 h-11 w-11 rounded-full" :src="avatarUrl" /> <img class="mr-2 h-11 w-11 rounded-full" :src="props.avatarUrl" />
<p class="text-bold lg:leading-[45px]">{{ name }}</p> <div :class="props.extraInfo ? 'leading-5' : ''">
<p class="text-bold" :class="props.extraInfo ? '' : 'lg:leading-[45px]'">
{{ props.name }}
</p>
<p v-if="props.extraInfo" class="font-normal" data-cy="extra-info">
{{ props.extraInfo }}
</p>
</div>
</template> </template>
<template #center> <template #center>
<slot name="center"></slot> <slot name="center"></slot>

19
client/src/consts.ts Normal file
View File

@ -0,0 +1,19 @@
// Course IDs
export const COURSE_TEST_ID = -1;
export const COURSE_UK = -3;
export const COURSE_VERSICHERUNGSVERMITTLERIN_ID = -4;
export const COURSE_UK_FR = -5;
export const COURSE_UK_TRAINING = -6;
export const COURSE_UK_TRAINING_FR = -7;
export const COURSE_UK_IT = -8;
export const COURSE_UK_TRAINING_IT = -9;
export const COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID = -10;
export const COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID = -11;
export const COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID = -12;
export const COURSE_MOTORFAHRZEUG_PRUEFUNG_ID = -13;
// Organization IDs
export const ORGANISATION_OTHER_BROKER_ID = 1;
export const ORGANISATION_OTHER_HEALTH_INSURANCE_ID = 2;
export const ORGANISATION_OTHER_PRIVATE_INSURANCE_ID = 3;
export const ORGANISATION_NO_COMPANY_ID = 31;

View File

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

File diff suppressed because one or more lines are too long

View File

@ -713,6 +713,7 @@ type CourseSessionUserObjectsType {
avatar_url: String! avatar_url: String!
role: String! role: String!
circles: [CourseSessionUserExpertCircleType!]! circles: [CourseSessionUserExpertCircleType!]!
optional_attendance: Boolean
} }
type CourseSessionUserExpertCircleType { type CourseSessionUserExpertCircleType {

View File

@ -192,6 +192,7 @@ export const COURSE_SESSION_DETAIL_QUERY = graphql(`
title title
slug slug
} }
optional_attendance
} }
attendance_courses { attendance_courses {
id id

View File

@ -13,6 +13,7 @@ import VerticalBarChart from "@/components/ui/VerticalBarChart.vue";
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue"; import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
import logger from "loglevel"; import logger from "loglevel";
import { reactive, ref } from "vue"; import { reactive, ref } from "vue";
import "@vuepic/vue-datepicker/dist/main.css";
const state = reactive({ const state = reactive({
checkboxValue: true, checkboxValue: true,

View File

@ -224,6 +224,9 @@ watch(
:name="`${csu.first_name} ${csu.last_name}`" :name="`${csu.first_name} ${csu.last_name}`"
:avatar-url="csu.avatar_url" :avatar-url="csu.avatar_url"
:class="0 === index ? 'border-none' : ''" :class="0 === index ? 'border-none' : ''"
:extra-info="
csu.optional_attendance ? `${$t('a.Optionale Anwesenheit')}` : ''
"
> >
<template #leading> <template #leading>
<ItCheckbox <ItCheckbox

View File

@ -6,9 +6,14 @@ import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { computed, ref, watch } from "vue"; import { computed, ref, watch } from "vue";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
import _ from "lodash"; import _ from "lodash";
import type { DashboardPersonCourseSessionType } from "@/services/dashboard"; import {
type DashboardPersonCourseSessionType,
exportPersons,
} from "@/services/dashboard";
import { useRouteQuery } from "@vueuse/router"; import { useRouteQuery } from "@vueuse/router";
import type { DashboardPersonsPageMode } from "@/types"; import type { DashboardPersonsPageMode, StatisticsFilterItem } from "@/types";
import { useUserStore } from "@/stores/user";
import { exportDataAsXls } from "@/utils/export";
log.debug("DashboardPersonsPage created"); log.debug("DashboardPersonsPage created");
@ -28,6 +33,7 @@ type MenuItem = {
}; };
const { t } = useTranslation(); const { t } = useTranslation();
const userStore = useUserStore();
const { loading, dashboardPersons } = useDashboardPersonsDueDates(props.mode); const { loading, dashboardPersons } = useDashboardPersonsDueDates(props.mode);
@ -227,6 +233,32 @@ function personRoleDisplayValue(personCourseSession: DashboardPersonCourseSessio
return ""; return "";
} }
function exportData() {
const courseSessionIdsSet = new Set<string>();
// get all course session ids from users
if (selectedSession.value.id === UNFILTERED) {
for (const person of filteredPersons.value) {
for (const courseSession of person.course_sessions) {
courseSessionIdsSet.add(courseSession.id);
}
}
} else {
courseSessionIdsSet.add(selectedSession.value.id);
}
// construct StatisticsFilterItems for export call
const items: StatisticsFilterItem[] = [];
for (const csId of courseSessionIdsSet) {
items.push({
_id: "",
course_session_id: csId,
generation: "",
circle_id: "",
});
}
exportDataAsXls(items, exportPersons, userStore.language);
}
watch(selectedCourse, () => { watch(selectedCourse, () => {
selectedRegion.value = regions.value[0]; selectedRegion.value = regions.value[0];
}); });
@ -253,7 +285,18 @@ watch(selectedRegion, () => {
<it-icon-arrow-left class="-ml-1 mr-1 h-5 w-5"></it-icon-arrow-left> <it-icon-arrow-left class="-ml-1 mr-1 h-5 w-5"></it-icon-arrow-left>
<span class="inline">{{ $t("general.back") }}</span> <span class="inline">{{ $t("general.back") }}</span>
</router-link> </router-link>
<h2 class="my-4">{{ $t("a.Personen") }}</h2> <div class="mb-10 flex items-center justify-between">
<h2 class="my-4">{{ $t("a.Personen") }}</h2>
<button
v-if="userStore.course_session_experts.length > 0"
class="flex"
data-cy="export-button"
@click="exportData"
>
<it-icon-export></it-icon-export>
<span class="ml inline-block">{{ $t("a.Als Excel exportieren") }}</span>
</button>
</div>
<div class="bg-white px-4 py-2"> <div class="bg-white px-4 py-2">
<section <section
v-if="filtersVisible" v-if="filtersVisible"

View File

@ -51,10 +51,6 @@ const submissionDeadline = computed(() => {
?.submission_deadline; ?.submission_deadline;
}); });
// FIXME daniel: `useRouteQuery` from usevue is currently the reason that we have to
// fix the version of @vueuse/router and @vueuse/core to 10.1.0
// it fails with version 10.2.0. I have a reminder to check out the situation
// at the end of July 2023
// 0 = introduction, 1 - n = tasks, n+1 = submission // 0 = introduction, 1 - n = tasks, n+1 = submission
const stepIndex = useRouteQuery("step", "0", { transform: Number, mode: "push" }); const stepIndex = useRouteQuery("step", "0", { transform: Number, mode: "push" });

View File

@ -3,9 +3,9 @@ import WizardPage from "@/components/onboarding/WizardPage.vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue"; import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { computed, ref, watch } from "vue"; import { computed, ref, watch } from "vue";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import { useRoute } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { useTranslation } from "i18next-vue"; import { useTranslation } from "i18next-vue";
import { profileNextRoute } from "@/services/onboarding"; import { isOtherOrganisation, profileNextRoute } from "@/services/onboarding";
import { useEntities } from "@/services/entities"; import { useEntities } from "@/services/entities";
import AvatarImage from "@/components/ui/AvatarImage.vue"; import AvatarImage from "@/components/ui/AvatarImage.vue";
@ -13,9 +13,12 @@ const { t } = useTranslation();
const user = useUserStore(); const user = useUserStore();
const route = useRoute(); const route = useRoute();
const router = useRouter();
const { organisations } = useEntities(); const { organisations } = useEntities();
const organisationDetailName = ref<string>("");
const selectedOrganisation = ref({ const selectedOrganisation = ref({
id: 0, id: 0,
name: t("a.Auswählen"), name: t("a.Auswählen"),
@ -35,7 +38,11 @@ watch(
); );
const validOrganisation = computed(() => { const validOrganisation = computed(() => {
return selectedOrganisation.value.id !== 0; const organisationSelected = selectedOrganisation.value.id !== 0;
const organisationNameSet =
!isOtherOrganisation(selectedOrganisation.value.id) ||
!!organisationDetailName.value.trim();
return organisationSelected && organisationNameSet;
}); });
const avatarError = ref(false); const avatarError = ref(false);
@ -56,15 +63,21 @@ async function avatarUpload(e: Event) {
} }
} }
watch(selectedOrganisation, async (organisation) => { async function updateUserProfile() {
await user.updateUserProfile({ await user.updateUserProfile({
organisation: organisation.id, organisation: selectedOrganisation.value.id,
organisation_detail_name: organisationDetailName.value.trim(),
}); });
}); }
const nextRoute = computed(() => { const nextRoute = computed(() => {
return profileNextRoute(route.params.courseType); return profileNextRoute(route.params.courseType);
}); });
async function navigateNextRoute() {
await updateUserProfile();
await router.push({ name: nextRoute.value });
}
</script> </script>
<template> <template>
@ -86,6 +99,19 @@ const nextRoute = computed(() => {
<ItDropdownSelect v-model="selectedOrganisation" :items="organisations" /> <ItDropdownSelect v-model="selectedOrganisation" :items="organisations" />
<div v-if="isOtherOrganisation(selectedOrganisation.id)" class="my-8">
<label for="organisationDetailName" class="heading-3 block pb-1.5">
{{ $t("a.Firmenname") }}
</label>
<input
id="organisationDetailName"
v-model="organisationDetailName"
type="text"
name="phone"
class="block w-full border-0 py-1.5 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 disabled:cursor-not-allowed disabled:text-gray-500 disabled:ring-gray-200 sm:max-w-sm sm:text-sm sm:leading-6"
/>
</div>
<div class="mt-16 flex flex-col justify-between gap-12 lg:flex-row lg:gap-24"> <div class="mt-16 flex flex-col justify-between gap-12 lg:flex-row lg:gap-24">
<div> <div>
<h3 class="mb-3">{{ $t("a.Profilbild") }}</h3> <h3 class="mb-3">{{ $t("a.Profilbild") }}</h3>
@ -117,18 +143,16 @@ const nextRoute = computed(() => {
</template> </template>
<template #footer> <template #footer>
<router-link v-slot="{ navigate }" :to="{ name: nextRoute }" custom> <button
<button :disabled="!validOrganisation"
:disabled="!validOrganisation" class="btn-blue flex items-center"
class="btn-blue flex items-center" role="link"
role="link" data-cy="continue-button"
data-cy="continue-button" @click="navigateNextRoute"
@click="navigate" >
> {{ $t("general.next") }}
{{ $t("general.next") }} <it-icon-arrow-right class="it-icon ml-2 h-6 w-6" />
<it-icon-arrow-right class="it-icon ml-2 h-6 w-6" /> </button>
</button>
</router-link>
</template> </template>
</WizardPage> </WizardPage>
</template> </template>

View File

@ -13,6 +13,12 @@ import DatatransCembraDeviceFingerprint from "@/components/onboarding/DatatransC
import { getLocalSessionKey } from "@/statistics"; import { getLocalSessionKey } from "@/statistics";
import log from "loglevel"; import log from "loglevel";
import { normalizeSwissPhoneNumber, validatePhoneNumber } from "@/utils/phone"; import { normalizeSwissPhoneNumber, validatePhoneNumber } from "@/utils/phone";
import {
ORGANISATION_NO_COMPANY_ID,
ORGANISATION_OTHER_BROKER_ID,
ORGANISATION_OTHER_HEALTH_INSURANCE_ID,
ORGANISATION_OTHER_PRIVATE_INSURANCE_ID,
} from "@/consts";
const props = defineProps({ const props = defineProps({
courseType: { courseType: {
@ -31,11 +37,14 @@ const userOrganisationName = computed(() => {
} }
// Those IDs do not represent a company // Those IDs do not represent a company
// 1: Other broker if (
// 2: Other insurance [
// 3: Other private insurance ORGANISATION_OTHER_BROKER_ID,
// 31: No company relation ORGANISATION_OTHER_HEALTH_INSURANCE_ID,
if ([1, 2, 3, 31].includes(user.organisation)) { ORGANISATION_OTHER_PRIVATE_INSURANCE_ID,
ORGANISATION_NO_COMPANY_ID,
].includes(user.organisation)
) {
return null; return null;
} }
@ -256,11 +265,10 @@ const executePayment = async () => {
<p v-if="paymentError" class="text-bold mt-12 text-lg text-red-700"> <p v-if="paymentError" class="text-bold mt-12 text-lg text-red-700">
{{ {{
$t("a.Fehler bei der Zahlung. Bitte versuche es erneut oder kontaktiere uns") $t(
}}: "a.Fehler bei der Zahlung. Bitte versuche es erneut oder wähle eine andere Zahlungsmethode."
<a href="mailto:vermittler@vbv-afa.ch" class="underline"> )
vermittler@vbv-afa.ch }}
</a>
</p> </p>
<h3 class="mb-4 mt-10">{{ $t("a.Adresse") }}</h3> <h3 class="mb-4 mt-10">{{ $t("a.Adresse") }}</h3>
@ -285,7 +293,7 @@ const executePayment = async () => {
{{ formErrors.personal.join(", ") }} {{ formErrors.personal.join(", ") }}
</p> </p>
<section v-if="address.payment_method !== 'cembra_byjuno'"> <section>
<div class="mt-4"> <div class="mt-4">
<button <button
v-if="!withCompanyAddress" v-if="!withCompanyAddress"

View File

@ -45,7 +45,11 @@ function startEditMode() {
<div class="flex flex-grow flex-col space-y-4 px-8 py-8 md:px-16"> <div class="flex flex-grow flex-col space-y-4 px-8 py-8 md:px-16">
<div v-if="!editMode" class="flex justify-end space-x-4"> <div v-if="!editMode" class="flex justify-end space-x-4">
<button class="btn btn-secondary" @click="startEditMode"> <button
class="btn btn-secondary"
data-cy="editProfileButton"
@click="startEditMode"
>
{{ $t("a.Profil bearbeiten") }} {{ $t("a.Profil bearbeiten") }}
</button> </button>
</div> </div>

View File

@ -53,7 +53,17 @@ onMounted(() => {
<div class="flex flex-col"> <div class="flex flex-col">
<h2 class="mb-2">{{ user.first_name }} {{ user.last_name }}</h2> <h2 class="mb-2">{{ user.first_name }} {{ user.last_name }}</h2>
<p class="mb-2">{{ user.email }}</p> <p class="mb-2">{{ user.email }}</p>
<p class="text-gray-800">{{ $t("a.Teilnehmer") }}</p> <p class="text-gray-800">
{{ $t("a.Teilnehmer") }}
<span
v-if="
user.optional_attendance.some((id: string) => id === courseSession.id)
"
data-cy="optional-attendance"
>
{{ $t("a.Optionale Anwesenheit") }}
</span>
</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -221,6 +221,15 @@ export async function exportCompetenceElements(
}); });
} }
export async function exportPersons(
data: XlsExportRequestData,
language: string
): Promise<XlsExportResponseData> {
return await itPost("/api/dashboard/export/persons/", data, {
headers: { "Accept-Language": language },
});
}
export function courseIdForCourseSlug( export function courseIdForCourseSlug(
dashboardConfigs: DashboardCourseConfigType[], dashboardConfigs: DashboardCourseConfigType[],
courseSlug: string courseSlug: string

View File

@ -1,4 +1,9 @@
import { isString, startsWith } from "lodash"; import { isString, startsWith } from "lodash";
import {
ORGANISATION_OTHER_BROKER_ID,
ORGANISATION_OTHER_HEALTH_INSURANCE_ID,
ORGANISATION_OTHER_PRIVATE_INSURANCE_ID,
} from "@/consts";
export function profileNextRoute(courseType: string | string[]) { export function profileNextRoute(courseType: string | string[]) {
if (courseType === "uk") { if (courseType === "uk") {
@ -10,3 +15,11 @@ export function profileNextRoute(courseType: string | string[]) {
} }
return ""; return "";
} }
export function isOtherOrganisation(orgId: number) {
return [
ORGANISATION_OTHER_BROKER_ID,
ORGANISATION_OTHER_HEALTH_INSURANCE_ID,
ORGANISATION_OTHER_PRIVATE_INSURANCE_ID,
].includes(orgId);
}

View File

@ -17,22 +17,31 @@ describe("checkout.cy.js", () => {
cy.get('[data-cy="account-confirm-title"]').should( cy.get('[data-cy="account-confirm-title"]').should(
"contain", "contain",
"Konto erstellen" "Konto erstellen",
); );
cy.get('[data-cy="continue-button"]').click(); cy.get('[data-cy="continue-button"]').click();
cy.get('[data-cy="account-profile-title"]').should( cy.get('[data-cy="account-profile-title"]').should(
"contain", "contain",
"Profil ergänzen" "Profil ergänzen",
); );
cy.get('[data-cy="dropdown-select"]').click(); cy.get('[data-cy="dropdown-select"]').click();
cy.get('[data-cy="dropdown-select-option-Baloise"]').click(); cy.get(
'[data-cy="dropdown-select-option-andere Krankenversicherer"]',
).click();
cy.get("#organisationDetailName").type("FdH GmbH");
cy.get('[data-cy="continue-button"]').click(); cy.get('[data-cy="continue-button"]').click();
cy.loadUser("id", TEST_USER_EMPTY_ID).then((u) => {
expect(u.organisation_detail_name).to.equal("FdH GmbH");
// 2 -> andere Krankenversicherer
expect(u.organisation).to.equal(2);
});
// Adressdaten ausfüllen // Adressdaten ausfüllen
cy.get('[data-cy="account-checkout-title"]').should( cy.get('[data-cy="account-checkout-title"]').should(
"contain", "contain",
"Lehrgang kaufen" "Lehrgang kaufen",
); );
cy.get("#street-address").type("Eggersmatt"); cy.get("#street-address").type("Eggersmatt");
cy.get("#street-number").type("32"); cy.get("#street-number").type("32");
@ -40,7 +49,7 @@ describe("checkout.cy.js", () => {
cy.get("#city").type("Zumholz"); cy.get("#city").type("Zumholz");
cy.get('[data-cy="add-company-address"]').click(); cy.get('[data-cy="add-company-address"]').click();
cy.get("#company-name").type("Iterativ GmbH"); // cy.get("#company-name").clear().type("Iterativ GmbH");
cy.get("#company-street-address").type("Brückfeldstrasse"); cy.get("#company-street-address").type("Brückfeldstrasse");
cy.get("#company-street-number").type("16"); cy.get("#company-street-number").type("16");
cy.get("#company-postal-code").type("3012"); cy.get("#company-postal-code").type("3012");
@ -60,7 +69,7 @@ describe("checkout.cy.js", () => {
expect(ci.country).to.equal("CH"); expect(ci.country).to.equal("CH");
expect(ci.invoice_address).to.equal("org"); expect(ci.invoice_address).to.equal("org");
expect(ci.organisation_detail_name).to.equal("Iterativ GmbH"); expect(ci.organisation_detail_name).to.equal("FdH GmbH");
expect(ci.organisation_street).to.equal("Brückfeldstrasse"); expect(ci.organisation_street).to.equal("Brückfeldstrasse");
expect(ci.organisation_street_number).to.equal("16"); expect(ci.organisation_street_number).to.equal("16");
expect(ci.organisation_postal_code).to.equal("3012"); expect(ci.organisation_postal_code).to.equal("3012");
@ -72,12 +81,32 @@ describe("checkout.cy.js", () => {
expect(ci.state).to.equal("ongoing"); expect(ci.state).to.equal("ongoing");
}); });
cy.loadUser("id", TEST_USER_EMPTY_ID).then((u) => {
expect(u.first_name).to.equal("Flasche");
expect(u.last_name).to.equal("Leer");
expect(u.street).to.equal("Eggersmatt");
expect(u.street_number).to.equal("32");
expect(u.postal_code).to.equal("1719");
expect(u.city).to.equal("Zumholz");
expect(u.country).to.equal("CH");
expect(u.invoice_address).to.equal("org");
expect(u.organisation_detail_name).to.equal("FdH GmbH");
expect(u.organisation_street).to.equal("Brückfeldstrasse");
expect(u.organisation_street_number).to.equal("16");
expect(u.organisation_postal_code).to.equal("3012");
expect(u.organisation_city).to.equal("Bern");
// 2 -> andere Krankenversicherer
expect(u.organisation).to.equal(2);
});
// pay // pay
cy.get('[data-cy="pay-button"]').click(); cy.get('[data-cy="pay-button"]').click();
cy.get('[data-cy="checkout-success-title"]').should( cy.get('[data-cy="checkout-success-title"]').should(
"contain", "contain",
"Gratuliere" "Gratuliere",
); );
// wait for payment callback // wait for payment callback
cy.wait(3000); cy.wait(3000);
@ -86,7 +115,7 @@ describe("checkout.cy.js", () => {
// back on dashboard page // back on dashboard page
cy.get('[data-cy="db-course-title"]').should( cy.get('[data-cy="db-course-title"]').should(
"contain", "contain",
"Versicherungsvermittler" "Versicherungsvermittler",
); );
cy.loadCheckoutInformation("user_id", TEST_USER_EMPTY_ID).then((ci) => { cy.loadCheckoutInformation("user_id", TEST_USER_EMPTY_ID).then((ci) => {
@ -102,13 +131,13 @@ describe("checkout.cy.js", () => {
cy.get('[data-cy="account-confirm-title"]').should( cy.get('[data-cy="account-confirm-title"]').should(
"contain", "contain",
"Konto erstellen" "Konto erstellen",
); );
cy.get('[data-cy="continue-button"]').click(); cy.get('[data-cy="continue-button"]').click();
cy.get('[data-cy="account-profile-title"]').should( cy.get('[data-cy="account-profile-title"]').should(
"contain", "contain",
"Profil ergänzen" "Profil ergänzen",
); );
cy.get('[data-cy="dropdown-select"]').click(); cy.get('[data-cy="dropdown-select"]').click();
cy.get('[data-cy="dropdown-select-option-Baloise"]').click(); cy.get('[data-cy="dropdown-select-option-Baloise"]').click();
@ -117,10 +146,10 @@ describe("checkout.cy.js", () => {
// Adressdaten ausfüllen // Adressdaten ausfüllen
cy.get('[data-cy="account-checkout-title"]').should( cy.get('[data-cy="account-checkout-title"]').should(
"contain", "contain",
"Lehrgang kaufen" "Lehrgang kaufen",
); );
cy.get('#paymentMethod').select('cembra_byjuno'); cy.get("#paymentMethod").select("cembra_byjuno");
cy.get("#street-address").type("Eggersmatt"); cy.get("#street-address").type("Eggersmatt");
cy.get("#street-number").type("32"); cy.get("#street-number").type("32");
@ -132,32 +161,38 @@ describe("checkout.cy.js", () => {
cy.get('[data-cy="continue-pay"]').click(); cy.get('[data-cy="continue-pay"]').click();
cy.loadExternalApiRequestLog("request_username", "empty@example.com").then((entry) => { cy.loadExternalApiRequestLog("request_username", "empty@example.com").then(
// ends with "/v1/transactions"" (entry) => {
expect(entry.api_url).to.contain("/v1/transactions"); // ends with "/v1/transactions""
expect(entry.request_username).to.contain("empty@example.com"); expect(entry.api_url).to.contain("/v1/transactions");
expect(entry.request_username).to.contain("empty@example.com");
expect(entry.api_request_data.amount).to.equal(32400); expect(entry.api_request_data.amount).to.equal(32400);
expect(entry.api_request_data.currency).to.equal("CHF"); expect(entry.api_request_data.currency).to.equal("CHF");
expect(entry.api_request_data.autoSettle).to.equal(true); expect(entry.api_request_data.autoSettle).to.equal(true);
expect(entry.api_request_data.customer.firstName).to.equal("Flasche"); expect(entry.api_request_data.customer.firstName).to.equal("Flasche");
expect(entry.api_request_data.customer.lastName).to.equal("Leer"); expect(entry.api_request_data.customer.lastName).to.equal("Leer");
expect(entry.api_request_data.customer.street).to.equal("Eggersmatt 32"); expect(entry.api_request_data.customer.street).to.equal(
expect(entry.api_request_data.customer.zipCode).to.equal("1719"); "Eggersmatt 32",
expect(entry.api_request_data.customer.city).to.equal("Zumholz"); );
expect(entry.api_request_data.customer.country).to.equal("CH"); expect(entry.api_request_data.customer.zipCode).to.equal("1719");
expect(entry.api_request_data.customer.type).to.equal("P"); expect(entry.api_request_data.customer.city).to.equal("Zumholz");
expect(entry.api_request_data.customer.phone).to.equal("+41792018586"); expect(entry.api_request_data.customer.country).to.equal("CH");
expect(entry.api_request_data.customer.birthDate).to.equal("1982-06-09"); expect(entry.api_request_data.customer.type).to.equal("P");
expect(entry.api_request_data.customer.phone).to.equal("+41792018586");
expect(entry.api_request_data.customer.birthDate).to.equal(
"1982-06-09",
);
expect(entry.api_request_data.INT.repaymentType).to.equal(3); expect(entry.api_request_data.INT.repaymentType).to.equal(3);
expect(entry.api_request_data.INT.riskOwner).to.equal("IJ"); expect(entry.api_request_data.INT.riskOwner).to.equal("IJ");
expect(entry.api_request_data.INT.subtype).to.equal("INVOICE"); expect(entry.api_request_data.INT.subtype).to.equal("INVOICE");
expect(entry.api_request_data.INT.deviceFingerprintId).to.not.be.empty; expect(entry.api_request_data.INT.deviceFingerprintId).to.not.be.empty;
expect(true).to.be.true; expect(true).to.be.true;
}); },
);
// check that results are stored on server // check that results are stored on server
cy.loadCheckoutInformation("user_id", TEST_USER_EMPTY_ID).then((ci) => { cy.loadCheckoutInformation("user_id", TEST_USER_EMPTY_ID).then((ci) => {
@ -183,5 +218,23 @@ describe("checkout.cy.js", () => {
expect(ci.ip_address).to.not.be.empty; expect(ci.ip_address).to.not.be.empty;
expect(ci.device_fingerprint_session_key).to.not.be.empty; expect(ci.device_fingerprint_session_key).to.not.be.empty;
}); });
cy.loadUser("id", TEST_USER_EMPTY_ID).then((u) => {
expect(u.first_name).to.equal("Flasche");
expect(u.last_name).to.equal("Leer");
expect(u.street).to.equal("Eggersmatt");
expect(u.street_number).to.equal("32");
expect(u.postal_code).to.equal("1719");
expect(u.city).to.equal("Zumholz");
expect(u.country).to.equal("CH");
expect(u.phone_number).to.equal("+41792018586");
expect(u.birth_date).to.equal("1982-06-09");
expect(u.invoice_address).to.equal("prv");
// 7 -> Baloise
expect(u.organisation).to.equal(7);
});
}); });
}); });

View File

@ -0,0 +1,11 @@
import { login } from "../helpers";
describe("cockpitAttendaceCheck.cy.js", () => {
it("will display optional participants", () => {
cy.manageCommand("cypress_reset --set-optional-attendance-flag");
login("test-trainer1@example.com", "test");
cy.visit("/course/test-lehrgang/cockpit/attendance");
cy.get('[data-cy="extra-info"]').should("contain", "Optionale Anwesenheit");
});
});

View File

@ -26,7 +26,7 @@ function getCurrentDate() {
function verifyExportFileExists(fileName) { function verifyExportFileExists(fileName) {
const downloadsFolder = Cypress.config("downloadsFolder"); const downloadsFolder = Cypress.config("downloadsFolder");
cy.readFile( cy.readFile(
path.join(downloadsFolder, `${fileName}_${getCurrentDate()}.xlsx`) path.join(downloadsFolder, `${fileName}_${getCurrentDate()}.xlsx`),
).should("exist"); ).should("exist");
} }
@ -39,7 +39,7 @@ function testExport(url, fileName) {
describe("dashboardExport.cy.js", () => { describe("dashboardExport.cy.js", () => {
beforeEach(() => { beforeEach(() => {
cy.manageCommand( cy.manageCommand(
"cypress_reset --create-assignment-evaluation --create-feedback-responses --create-course-completion-performance-criteria --create-attendance-days" "cypress_reset --create-assignment-evaluation --create-feedback-responses --create-course-completion-performance-criteria --create-attendance-days",
); );
}); });
@ -55,13 +55,17 @@ describe("dashboardExport.cy.js", () => {
it("should download the competence elements export", () => { it("should download the competence elements export", () => {
testExport( testExport(
"/statistic/test-lehrgang/assignment", "/statistic/test-lehrgang/assignment",
"export_kompetenznachweis_elemente" "export_kompetenznachweis_elemente",
); );
}); });
it("should download the feedback export", () => { it("should download the feedback export", () => {
testExport("/statistic/test-lehrgang/feedback", "export_feedback"); testExport("/statistic/test-lehrgang/feedback", "export_feedback");
}); });
it("should download the person export", () => {
testExport("/dashboard/persons", "export_personen");
});
}); });
describe("as trainer", () => { describe("as trainer", () => {
@ -76,12 +80,16 @@ describe("dashboardExport.cy.js", () => {
it("should download the competence elements export", () => { it("should download the competence elements export", () => {
testExport( testExport(
"/statistic/test-lehrgang/assignment", "/statistic/test-lehrgang/assignment",
"export_kompetenznachweis_elemente" "export_kompetenznachweis_elemente",
); );
}); });
it("should download the feedback export", () => { it("should download the feedback export", () => {
testExport("/statistic/test-lehrgang/feedback", "export_feedback"); testExport("/statistic/test-lehrgang/feedback", "export_feedback");
}); });
it("should download the person export", () => {
testExport("/dashboard/persons", "export_personen");
});
}); });
}); });

View File

@ -0,0 +1,85 @@
import { TEST_USER_EMPTY_ID } from "../../consts";
import { login } from "../helpers";
describe("personalProfile.cy.js", () => {
beforeEach(() => {
cy.manageCommand("cypress_reset");
login("empty@example.com", "test");
cy.visit("/profile");
});
it("can edit all profile fields", () => {
cy.get('[data-cy="editProfileButton"]').click();
cy.get("#phone").type("079 201 85 86");
cy.get('[data-test="dp-input"]').type("09.06.1982{enter}");
cy.get("#street-address").type("Hafen");
cy.get("#street-number").type("123");
cy.get("#postal-code").type("DE-20095");
cy.get("#city").type("Hamburg");
cy.get("#country").select("DE");
// andere broker
cy.get("#organisation").select("1");
cy.get("#org-detail-name").type("Judihui GmbH");
cy.get("#org-street-address").type("Auf der Alm");
cy.get("#org-street-number").type("17");
cy.get("#org-postal-code").type("AT-6020");
cy.get("#org-city").type("Innsbruck");
cy.get("#org-country").select("AT");
cy.get("#invoice-address-organisation").click();
cy.get('[data-cy="saveButton"]').click();
// check displayed data
cy.get('[data-cy="firstName"]').should("contain", "Flasche");
cy.get('[data-cy="lastName"]').should("contain", "Leer");
cy.get('[data-cy="email"]').should("contain", "empty@example.com");
cy.get('[data-cy="phone"]').should("contain", "079 201 85 86");
cy.get('[data-cy="birthDate"]').should("contain", "09.06.1982");
cy.get('[data-cy="privateAddress"]').should("contain", "Hafen 123");
cy.get('[data-cy="privateAddress"]').should("contain", "DE-20095 Hamburg");
cy.get('[data-cy="privateAddress"]').should("contain", "Deutschland");
cy.get('[data-cy="organisationDetailName"]').should(
"contain",
"andere Broker",
);
cy.get('[data-cy="organisationAddress"]').should("contain", "Judihui GmbH");
cy.get('[data-cy="organisationAddress"]').should(
"contain",
"Auf der Alm 17",
);
cy.get('[data-cy="organisationAddress"]').should(
"contain",
"AT-6020 Innsbruck",
);
cy.get('[data-cy="organisationAddress"]').should("contain", "Österreich");
// check stored data
cy.loadUser("id", TEST_USER_EMPTY_ID).then((u) => {
expect(u.first_name).to.equal("Flasche");
expect(u.last_name).to.equal("Leer");
expect(u.street).to.equal("Hafen");
expect(u.street_number).to.equal("123");
expect(u.postal_code).to.equal("DE-20095");
expect(u.city).to.equal("Hamburg");
expect(u.country).to.equal("DE");
expect(u.invoice_address).to.equal("org");
expect(u.organisation_detail_name).to.equal("Judihui GmbH");
expect(u.organisation_street).to.equal("Auf der Alm");
expect(u.organisation_street_number).to.equal("17");
expect(u.organisation_postal_code).to.equal("AT-6020");
expect(u.organisation_city).to.equal("Innsbruck");
// 1 -> andere Broker
expect(u.organisation).to.equal(1);
});
});
});

View File

@ -0,0 +1,17 @@
import { login } from "../helpers";
import { TEST_STUDENT1_USER_ID } from "../../consts";
describe("publicProfileAttendance.cy.js", () => {
it("will display optional attendance", () => {
cy.manageCommand("cypress_reset --set-optional-attendance-flag");
login("test-trainer1@example.com", "test");
cy.visit(
`course/test-lehrgang/profile/${TEST_STUDENT1_USER_ID}/learning-path`,
);
cy.get('[data-cy="optional-attendance"]').should(
"contain",
"Optionale Anwesenheit",
);
});
});

View File

@ -178,6 +178,17 @@ Cypress.Commands.add("loadCheckoutInformation", (key, value) => {
) )
}) })
Cypress.Commands.add("loadUser", (key, value) => {
return loadObjectJson(
key,
value,
"vbv_lernwelt.core.models.User",
"vbv_lernwelt.core.serializers.CypressUserSerializer",
true
);
});
Cypress.Commands.add("makeSelfEvaluation", (answers) => { Cypress.Commands.add("makeSelfEvaluation", (answers) => {
for (let i = 0; i < answers.length; i++) { for (let i = 0; i < answers.length; i++) {
const answer = answers[i] const answer = answers[i]

Binary file not shown.

Binary file not shown.

View File

@ -201,6 +201,7 @@ MIDDLEWARE = [
"vbv_lernwelt.core.middleware.security.SecurityRequestResponseLoggingMiddleware", "vbv_lernwelt.core.middleware.security.SecurityRequestResponseLoggingMiddleware",
"wagtail.contrib.redirects.middleware.RedirectMiddleware", "wagtail.contrib.redirects.middleware.RedirectMiddleware",
"vbv_lernwelt.core.middleware.auth.UserLoggedInCookieMiddleWare", "vbv_lernwelt.core.middleware.auth.UserLoggedInCookieMiddleWare",
# "vbv_lernwelt.debugtools.middleware.QueryCountDebugMiddleware",
] ]
# STATIC # STATIC
@ -332,7 +333,6 @@ X_FRAME_OPTIONS = "DENY"
# EMAIL # EMAIL
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
# FIXME how to send emails?
EMAIL_BACKEND = env( EMAIL_BACKEND = env(
"DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend" "DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend"
) )
@ -682,10 +682,12 @@ if APP_ENVIRONMENT.startswith("prod"):
DATATRANS_PAY_URL = "https://pay.datatrans.com" DATATRANS_PAY_URL = "https://pay.datatrans.com"
else: else:
DATATRANS_API_ENDPOINT = env( DATATRANS_API_ENDPOINT = env(
"DATATRANS_API_ENDPOINT", default="https://api.sandbox.datatrans.com" "DATATRANS_API_ENDPOINT",
default="http://localhost:8000/server/fakeapi/datatrans/api",
) )
DATATRANS_PAY_URL = env( DATATRANS_PAY_URL = env(
"DATATRANS_PAY_URL", default="https://pay.sandbox.datatrans.com" "DATATRANS_PAY_URL",
default="http://localhost:8000/server/fakeapi/datatrans/pay",
) )
# default settings for python sftpserver test-server # default settings for python sftpserver test-server

View File

@ -44,6 +44,7 @@ from vbv_lernwelt.dashboard.views import (
export_attendance_as_xsl, export_attendance_as_xsl,
export_competence_elements_as_xsl, export_competence_elements_as_xsl,
export_feedback_as_xsl, export_feedback_as_xsl,
export_persons_as_xsl,
get_dashboard_config, get_dashboard_config,
get_dashboard_due_dates, get_dashboard_due_dates,
get_dashboard_persons, get_dashboard_persons,
@ -143,6 +144,7 @@ urlpatterns = [
path(r"api/dashboard/export/competence_elements/", export_competence_elements_as_xsl, path(r"api/dashboard/export/competence_elements/", export_competence_elements_as_xsl,
name="export_certificate_as_xsl"), name="export_certificate_as_xsl"),
path(r"api/dashboard/export/feedback/", export_feedback_as_xsl, name="export_feedback_as_xsl"), path(r"api/dashboard/export/feedback/", export_feedback_as_xsl, name="export_feedback_as_xsl"),
path(r"api/dashboard/export/persons/", export_persons_as_xsl, name="export_persons_as_xsl"),
# course # course
path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"), path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"),

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-06-18 15:24+0200\n" "POT-Creation-Date: 2024-07-27 20:59+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -38,32 +38,32 @@ msgstr ""
msgid "Nicht bestanden" msgid "Nicht bestanden"
msgstr "" msgstr ""
#: vbv_lernwelt/assignment/export.py:203 vbv_lernwelt/assignment/export.py:206 #: vbv_lernwelt/assignment/export.py:204 vbv_lernwelt/assignment/export.py:208
#: vbv_lernwelt/assignment/export.py:207 #: vbv_lernwelt/assignment/export.py:209
msgid "Keine Daten" msgid "Keine Daten"
msgstr "" msgstr ""
#: vbv_lernwelt/core/admin.py:32 #: vbv_lernwelt/core/admin.py:38 vbv_lernwelt/sso/admin.py:83
msgid "Personal info" msgid "Personal info"
msgstr "" msgstr ""
#: vbv_lernwelt/core/admin.py:34 #: vbv_lernwelt/core/admin.py:40
msgid "Permissions" msgid "Permissions"
msgstr "" msgstr ""
#: vbv_lernwelt/core/admin.py:45 #: vbv_lernwelt/core/admin.py:51
msgid "Important dates" msgid "Important dates"
msgstr "" msgstr ""
#: vbv_lernwelt/core/admin.py:47 #: vbv_lernwelt/core/admin.py:53
msgid "Profile" msgid "Profile"
msgstr "" msgstr ""
#: vbv_lernwelt/core/admin.py:62 #: vbv_lernwelt/core/admin.py:70
msgid "Organisation" msgid "Organisation"
msgstr "" msgstr ""
#: vbv_lernwelt/core/admin.py:75 #: vbv_lernwelt/core/admin.py:83 vbv_lernwelt/sso/admin.py:86
msgid "Additional data" msgid "Additional data"
msgstr "" msgstr ""
@ -87,31 +87,31 @@ msgstr ""
msgid "Lehrgang-Seite" msgid "Lehrgang-Seite"
msgstr "" msgstr ""
#: vbv_lernwelt/course/models.py:272 #: vbv_lernwelt/dashboard/person_export.py:116
msgid "Teilnehmer" msgid "Teilnehmer"
msgstr "" msgstr ""
#: vbv_lernwelt/course/models.py:273 #: vbv_lernwelt/course/models.py:279
msgid "Experte/Trainer" msgid "Experte/Trainer"
msgstr "" msgstr ""
#: vbv_lernwelt/course/models.py:332 #: vbv_lernwelt/course/models.py:339
msgid "Dokumente im Circle ein/aus" msgid "Dokumente im Circle ein/aus"
msgstr "" msgstr ""
#: vbv_lernwelt/course/models.py:336 #: vbv_lernwelt/course/models.py:343
msgid "Lernmentor-Funktion ein/aus" msgid "Lernmentor-Funktion ein/aus"
msgstr "" msgstr ""
#: vbv_lernwelt/course/models.py:340 #: vbv_lernwelt/course/models.py:347
msgid "Kompetenzweise ein/aus" msgid "Kompetenzweise ein/aus"
msgstr "" msgstr ""
#: vbv_lernwelt/course/models.py:343 #: vbv_lernwelt/course/models.py:350
msgid "Versicherungsvermittler-Lehrgang" msgid "Versicherungsvermittler-Lehrgang"
msgstr "" msgstr ""
#: vbv_lernwelt/course/models.py:344 #: vbv_lernwelt/course/models.py:350
msgid "ÜK-Lehrgang" msgid "ÜK-Lehrgang"
msgstr "" msgstr ""
@ -119,34 +119,66 @@ msgstr ""
msgid "export_anwesenheit" msgid "export_anwesenheit"
msgstr "" msgstr ""
#: vbv_lernwelt/course_session/services/export_attendance.py:92 #: vbv_lernwelt/course_session/services/export_attendance.py:86
msgid "Optionale Anwesenheit"
msgstr ""
#: vbv_lernwelt/course_session/services/export_attendance.py:96
msgid "Anwesenheit" msgid "Anwesenheit"
msgstr "" msgstr ""
#: vbv_lernwelt/course_session/services/export_attendance.py:116 #: vbv_lernwelt/course_session/services/export_attendance.py:119
msgid "Ja"
msgstr ""
#: vbv_lernwelt/course_session/services/export_attendance.py:119
msgid "Nein"
msgstr ""
#: vbv_lernwelt/course_session/services/export_attendance.py:128
msgid "Anwesend" msgid "Anwesend"
msgstr "" msgstr ""
#: vbv_lernwelt/course_session/services/export_attendance.py:116 #: vbv_lernwelt/course_session/services/export_attendance.py:128
msgid "Nicht anwesend" msgid "Nicht anwesend"
msgstr "" msgstr ""
#: vbv_lernwelt/course_session/services/export_attendance.py:123 #: vbv_lernwelt/course_session/services/export_attendance.py:135
msgid "Vorname" msgid "Vorname"
msgstr "" msgstr ""
#: vbv_lernwelt/course_session/services/export_attendance.py:124 #: vbv_lernwelt/course_session/services/export_attendance.py:136
msgid "Nachname" msgid "Nachname"
msgstr "" msgstr ""
#: vbv_lernwelt/course_session/services/export_attendance.py:125 #: vbv_lernwelt/course_session/services/export_attendance.py:137
msgid "Email" msgid "Email"
msgstr "Email" msgstr "Email"
#: vbv_lernwelt/course_session/services/export_attendance.py:126 #: vbv_lernwelt/course_session/services/export_attendance.py:127
msgid "Lehrvertragsnummer" msgid "Lehrvertragsnummer"
msgstr "" msgstr ""
#: vbv_lernwelt/dashboard/person_export.py:16
msgid "export_personen"
msgstr ""
#: vbv_lernwelt/dashboard/person_export.py:68
msgid "Telefon"
msgstr ""
#: vbv_lernwelt/dashboard/person_export.py:69
msgid "Rolle"
msgstr ""
#: vbv_lernwelt/dashboard/person_export.py:118
msgid "Trainer"
msgstr ""
#: vbv_lernwelt/dashboard/person_export.py:120
msgid "Regionenleiter"
msgstr ""
#: vbv_lernwelt/feedback/export.py:19 #: vbv_lernwelt/feedback/export.py:19
msgid "export_feedback" msgid "export_feedback"
msgstr "" msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-07-12 09:37+0200\n" "POT-Creation-Date: 2024-07-30 11:16+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -88,30 +88,31 @@ msgid "Lehrgang-Seite"
msgstr "" msgstr ""
#: vbv_lernwelt/course/models.py:278 #: vbv_lernwelt/course/models.py:278
#: vbv_lernwelt/dashboard/person_export.py:116
msgid "Teilnehmer" msgid "Teilnehmer"
msgstr "" msgstr "Participant"
#: vbv_lernwelt/course/models.py:279 #: vbv_lernwelt/course/models.py:279
msgid "Experte/Trainer" msgid "Experte/Trainer"
msgstr "" msgstr ""
#: vbv_lernwelt/course/models.py:338 #: vbv_lernwelt/course/models.py:339
msgid "Dokumente im Circle ein/aus" msgid "Dokumente im Circle ein/aus"
msgstr "" msgstr ""
#: vbv_lernwelt/course/models.py:342 #: vbv_lernwelt/course/models.py:343
msgid "Lernmentor-Funktion ein/aus" msgid "Lernmentor-Funktion ein/aus"
msgstr "" msgstr ""
#: vbv_lernwelt/course/models.py:346 #: vbv_lernwelt/course/models.py:347
msgid "Kompetenzweise ein/aus" msgid "Kompetenzweise ein/aus"
msgstr "" msgstr ""
#: vbv_lernwelt/course/models.py:349 #: vbv_lernwelt/course/models.py:350
msgid "Versicherungsvermittler-Lehrgang" msgid "Versicherungsvermittler-Lehrgang"
msgstr "" msgstr ""
#: vbv_lernwelt/course/models.py:350 #: vbv_lernwelt/course/models.py:351
msgid "ÜK-Lehrgang" msgid "ÜK-Lehrgang"
msgstr "" msgstr ""
@ -119,34 +120,66 @@ msgstr ""
msgid "export_anwesenheit" msgid "export_anwesenheit"
msgstr "export_presence" msgstr "export_presence"
#: vbv_lernwelt/course_session/services/export_attendance.py:92 #: vbv_lernwelt/course_session/services/export_attendance.py:86
msgid "Optionale Anwesenheit"
msgstr "Présence facultative"
#: vbv_lernwelt/course_session/services/export_attendance.py:96
msgid "Anwesenheit" msgid "Anwesenheit"
msgstr "Présence" msgstr "Présence"
#: vbv_lernwelt/course_session/services/export_attendance.py:117 #: vbv_lernwelt/course_session/services/export_attendance.py:119
msgid "Ja"
msgstr "Oui"
#: vbv_lernwelt/course_session/services/export_attendance.py:119
msgid "Nein"
msgstr "Non"
#: vbv_lernwelt/course_session/services/export_attendance.py:128
msgid "Anwesend" msgid "Anwesend"
msgstr "Présent" msgstr "Présent"
#: vbv_lernwelt/course_session/services/export_attendance.py:117 #: vbv_lernwelt/course_session/services/export_attendance.py:128
msgid "Nicht anwesend" msgid "Nicht anwesend"
msgstr "Pas présent" msgstr "Pas présent"
#: vbv_lernwelt/course_session/services/export_attendance.py:124 #: vbv_lernwelt/course_session/services/export_attendance.py:135
msgid "Vorname" msgid "Vorname"
msgstr "Prénom" msgstr "Prénom"
#: vbv_lernwelt/course_session/services/export_attendance.py:125 #: vbv_lernwelt/course_session/services/export_attendance.py:136
msgid "Nachname" msgid "Nachname"
msgstr "Nom de famille" msgstr "Nom de famille"
#: vbv_lernwelt/course_session/services/export_attendance.py:126 #: vbv_lernwelt/course_session/services/export_attendance.py:137
msgid "Email" msgid "Email"
msgstr "E-mail" msgstr "E-mail"
#: vbv_lernwelt/course_session/services/export_attendance.py:127 #: vbv_lernwelt/course_session/services/export_attendance.py:138
msgid "Lehrvertragsnummer" msgid "Lehrvertragsnummer"
msgstr "Numéro de contrat d'apprentissage" msgstr "Numéro de contrat d'apprentissage"
#: vbv_lernwelt/dashboard/person_export.py:16
msgid "export_personen"
msgstr "export_personnes"
#: vbv_lernwelt/dashboard/person_export.py:68
msgid "Telefon"
msgstr "Téléphone"
#: vbv_lernwelt/dashboard/person_export.py:69
msgid "Rolle"
msgstr "Rôle"
#: vbv_lernwelt/dashboard/person_export.py:118
msgid "Trainer"
msgstr "Formateur / Formatrice"
#: vbv_lernwelt/dashboard/person_export.py:120
msgid "Regionenleiter"
msgstr "Responsable CI"
#: vbv_lernwelt/feedback/export.py:19 #: vbv_lernwelt/feedback/export.py:19
msgid "export_feedback" msgid "export_feedback"
msgstr "export_feedback" msgstr "export_feedback"

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-07-12 09:36+0200\n" "POT-Creation-Date: 2024-07-30 11:16+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -88,30 +88,31 @@ msgid "Lehrgang-Seite"
msgstr "" msgstr ""
#: vbv_lernwelt/course/models.py:278 #: vbv_lernwelt/course/models.py:278
#: vbv_lernwelt/dashboard/person_export.py:116
msgid "Teilnehmer" msgid "Teilnehmer"
msgstr "" msgstr "Partecipante"
#: vbv_lernwelt/course/models.py:279 #: vbv_lernwelt/course/models.py:279
msgid "Experte/Trainer" msgid "Experte/Trainer"
msgstr "" msgstr ""
#: vbv_lernwelt/course/models.py:338 #: vbv_lernwelt/course/models.py:339
msgid "Dokumente im Circle ein/aus" msgid "Dokumente im Circle ein/aus"
msgstr "" msgstr ""
#: vbv_lernwelt/course/models.py:342 #: vbv_lernwelt/course/models.py:343
msgid "Lernmentor-Funktion ein/aus" msgid "Lernmentor-Funktion ein/aus"
msgstr "" msgstr ""
#: vbv_lernwelt/course/models.py:346 #: vbv_lernwelt/course/models.py:347
msgid "Kompetenzweise ein/aus" msgid "Kompetenzweise ein/aus"
msgstr "" msgstr ""
#: vbv_lernwelt/course/models.py:349 #: vbv_lernwelt/course/models.py:350
msgid "Versicherungsvermittler-Lehrgang" msgid "Versicherungsvermittler-Lehrgang"
msgstr "" msgstr ""
#: vbv_lernwelt/course/models.py:350 #: vbv_lernwelt/course/models.py:351
msgid "ÜK-Lehrgang" msgid "ÜK-Lehrgang"
msgstr "" msgstr ""
@ -119,34 +120,68 @@ msgstr ""
msgid "export_anwesenheit" msgid "export_anwesenheit"
msgstr "esportazione_presenza" msgstr "esportazione_presenza"
#: vbv_lernwelt/course_session/services/export_attendance.py:92 #: vbv_lernwelt/course_session/services/export_attendance.py:86
msgid "Optionale Anwesenheit"
msgstr "Presenza opzionale"
#: vbv_lernwelt/course_session/services/export_attendance.py:96
msgid "Anwesenheit" msgid "Anwesenheit"
msgstr "Presenza" msgstr "Presenza"
#: vbv_lernwelt/course_session/services/export_attendance.py:117 #: vbv_lernwelt/course_session/services/export_attendance.py:119
msgid "Ja"
msgstr "Sì"
#: vbv_lernwelt/course_session/services/export_attendance.py:119
msgid "Nein"
msgstr "No"
#: vbv_lernwelt/course_session/services/export_attendance.py:128
msgid "Anwesend" msgid "Anwesend"
msgstr "Presente" msgstr "Presente"
#: vbv_lernwelt/course_session/services/export_attendance.py:117 #: vbv_lernwelt/course_session/services/export_attendance.py:128
msgid "Nicht anwesend" msgid "Nicht anwesend"
msgstr "Non presente" msgstr "Non presente"
#: vbv_lernwelt/course_session/services/export_attendance.py:124 #: vbv_lernwelt/course_session/services/export_attendance.py:135
msgid "Vorname" msgid "Vorname"
msgstr "Nome" msgstr "Nome"
#: vbv_lernwelt/course_session/services/export_attendance.py:125 #: vbv_lernwelt/course_session/services/export_attendance.py:136
msgid "Nachname" msgid "Nachname"
msgstr "Cognome" msgstr "Cognome"
#: vbv_lernwelt/course_session/services/export_attendance.py:126 #: vbv_lernwelt/course_session/services/export_attendance.py:137
msgid "Email" msgid "Email"
msgstr "Email" msgstr "Email"
#: vbv_lernwelt/course_session/services/export_attendance.py:127 #: vbv_lernwelt/course_session/services/export_attendance.py:138
msgid "Lehrvertragsnummer" msgid "Lehrvertragsnummer"
msgstr "Numero di contratto di tirocinio" msgstr "Numero di contratto di tirocinio"
#: vbv_lernwelt/dashboard/person_export.py:16
#, fuzzy
#| msgid "export_anwesenheit"
msgid "export_personen"
msgstr "esportazione_persone"
#: vbv_lernwelt/dashboard/person_export.py:68
msgid "Telefon"
msgstr "Telefono"
#: vbv_lernwelt/dashboard/person_export.py:69
msgid "Rolle"
msgstr "Ruolo"
#: vbv_lernwelt/dashboard/person_export.py:118
msgid "Trainer"
msgstr "Trainer"
#: vbv_lernwelt/dashboard/person_export.py:120
msgid "Regionenleiter"
msgstr "Responsabile CI"
#: vbv_lernwelt/feedback/export.py:19 #: vbv_lernwelt/feedback/export.py:19
msgid "export_feedback" msgid "export_feedback"
msgstr "esportazione_feedback" msgstr "esportazione_feedback"

View File

@ -71,5 +71,6 @@ class ProfileViewTest(APITestCase):
"organisation_postal_code": "", "organisation_postal_code": "",
"organisation_city": "", "organisation_city": "",
"organisation_country": None, "organisation_country": None,
"optional_attendance": [],
}, },
) )

View File

@ -17,6 +17,11 @@ def query_competence_course_session_assignments(course_session_ids, circle_ids=N
AssignmentType.CASEWORK.value, AssignmentType.CASEWORK.value,
], ],
learning_content__content_assignment__competence_certificate__isnull=False, learning_content__content_assignment__competence_certificate__isnull=False,
).select_related(
"submission_deadline",
"learning_content",
"course_session",
"learning_content__content_assignment",
): ):
if circle_ids and csa.learning_content.get_circle().id not in circle_ids: if circle_ids and csa.learning_content.get_circle().id not in circle_ids:
continue continue
@ -34,6 +39,11 @@ def query_competence_course_session_edoniq_tests(course_session_ids, circle_ids=
for cset in CourseSessionEdoniqTest.objects.filter( for cset in CourseSessionEdoniqTest.objects.filter(
course_session_id__in=course_session_ids, course_session_id__in=course_session_ids,
learning_content__content_assignment__competence_certificate__isnull=False, learning_content__content_assignment__competence_certificate__isnull=False,
).select_related(
"deadline",
"learning_content",
"course_session",
"learning_content__content_assignment",
): ):
if circle_ids and cset.learning_content.get_circle().id not in circle_ids: if circle_ids and cset.learning_content.get_circle().id not in circle_ids:
continue continue

View File

@ -28,6 +28,7 @@ TEST_MENTOR1_USER_ID = "d1f5f5a9-5b0a-4e1a-9e1a-9e9b5b5e1b1b"
TEST_STUDENT1_VV_USER_ID = "5ff59857-8de5-415e-a387-4449f9a0337a" TEST_STUDENT1_VV_USER_ID = "5ff59857-8de5-415e-a387-4449f9a0337a"
TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID = "7e8ebf0b-e6e2-4022-88f4-6e663ba0a9db" TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID = "7e8ebf0b-e6e2-4022-88f4-6e663ba0a9db"
TEST_USER_EMPTY_ID = "daecbabe-4ab9-4edf-a71f-4119042ccb02" TEST_USER_EMPTY_ID = "daecbabe-4ab9-4edf-a71f-4119042ccb02"
TEST_USER_DATATRANS_HANNA_ID = "6bec1a0d-f852-47aa-a4de-072df6e07ad1"
TEST_COURSE_SESSION_BERN_ID = -1 TEST_COURSE_SESSION_BERN_ID = -1
TEST_COURSE_SESSION_ZURICH_ID = -2 TEST_COURSE_SESSION_ZURICH_ID = -2

View File

@ -4,6 +4,7 @@ from django.contrib.auth.models import Group, Permission
from django.core.files import File from django.core.files import File
from environs import Env from environs import Env
from vbv_lernwelt.core.model_utils import add_countries
from vbv_lernwelt.media_files.models import UserImage from vbv_lernwelt.media_files.models import UserImage
env = Env() env = Env()
@ -20,6 +21,7 @@ from vbv_lernwelt.core.constants import (
TEST_SUPERVISOR1_USER_ID, TEST_SUPERVISOR1_USER_ID,
TEST_TRAINER1_USER_ID, TEST_TRAINER1_USER_ID,
TEST_TRAINER2_USER_ID, TEST_TRAINER2_USER_ID,
TEST_USER_DATATRANS_HANNA_ID,
TEST_USER_EMPTY_ID, TEST_USER_EMPTY_ID,
) )
from vbv_lernwelt.core.models import User from vbv_lernwelt.core.models import User
@ -78,7 +80,30 @@ default_users = [
AVATAR_DIR = settings.APPS_DIR / "static" / "avatars" AVATAR_DIR = settings.APPS_DIR / "static" / "avatars"
def create_datatrans_hanna_user():
hanna, _ = User.objects.get_or_create(
id=TEST_USER_DATATRANS_HANNA_ID,
)
hanna.username = "datatrans.hanna.vbv@example.com"
hanna.email = "datatrans.hanna.vbv@example.com"
hanna.language = "de"
hanna.first_name = "Hanna"
hanna.last_name = "Vbv"
hanna.street = "Bahnstrasse"
hanna.street_number = "2"
hanna.postal_code = "8603"
hanna.city = "Schwerzenbach"
hanna.country_id = "CH"
hanna.birth_date = "1970-01-01"
hanna.phone_number = "+41792018586"
hanna.password = make_password("test")
hanna.save()
return hanna
def create_default_users(default_password="test", set_avatar=False): def create_default_users(default_password="test", set_avatar=False):
add_countries(small_set=True)
admin_group, created = Group.objects.get_or_create(name="admin_group") admin_group, created = Group.objects.get_or_create(name="admin_group")
_content_creator_group, _created = Group.objects.get_or_create( _content_creator_group, _created = Group.objects.get_or_create(
name="content_creator_grop" name="content_creator_grop"
@ -202,6 +227,8 @@ def create_default_users(default_password="test", set_avatar=False):
language="de", language="de",
) )
hanna = create_datatrans_hanna_user()
for user_data in default_users: for user_data in default_users:
_create_student_user(**user_data) _create_student_user(**user_data)

View File

@ -17,8 +17,10 @@ from vbv_lernwelt.core.constants import (
TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID, TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID,
TEST_STUDENT3_USER_ID, TEST_STUDENT3_USER_ID,
TEST_TRAINER1_USER_ID, TEST_TRAINER1_USER_ID,
TEST_USER_DATATRANS_HANNA_ID,
TEST_USER_EMPTY_ID, TEST_USER_EMPTY_ID,
) )
from vbv_lernwelt.core.create_default_users import create_datatrans_hanna_user
from vbv_lernwelt.core.models import Organisation, User from vbv_lernwelt.core.models import Organisation, User
from vbv_lernwelt.course.consts import ( from vbv_lernwelt.course.consts import (
COURSE_TEST_ID, COURSE_TEST_ID,
@ -148,6 +150,11 @@ from vbv_lernwelt.shop.models import CheckoutInformation
default=False, default=False,
help="Will set only the is_vv flag for the test course and enable learning mentors for the course", help="Will set only the is_vv flag for the test course and enable learning mentors for the course",
) )
@click.option(
"--set-optional-attendance-flag/--no-optional-attendance-flag",
default=False,
help="Will set the optional attendance flag for the test-student1@example.com",
)
def command( def command(
create_assignment_completion, create_assignment_completion,
assignment_completion_user_id, assignment_completion_user_id,
@ -167,6 +174,7 @@ def command(
create_learning_mentor, create_learning_mentor,
set_only_is_uk_flag, set_only_is_uk_flag,
set_only_is_vv_flag, set_only_is_vv_flag,
set_optional_attendance_flag,
): ):
print("cypress reset data") print("cypress reset data")
CourseCompletion.objects.all().delete() CourseCompletion.objects.all().delete()
@ -195,6 +203,9 @@ def command(
password=make_password("test"), password=make_password("test"),
) )
User.objects.filter(id=TEST_USER_DATATRANS_HANNA_ID).delete()
create_datatrans_hanna_user()
cursor = connection.cursor() cursor = connection.cursor()
cursor.execute("truncate core_securityrequestresponselog;") cursor.execute("truncate core_securityrequestresponselog;")
cursor.execute("truncate core_externalapirequestlog;") cursor.execute("truncate core_externalapirequestlog;")
@ -515,4 +526,17 @@ def command(
course.configuration.is_uk = False course.configuration.is_uk = False
course.configuration.enable_learning_mentor = False course.configuration.enable_learning_mentor = False
if set_optional_attendance_flag:
course_session_user = CourseSessionUser.objects.get(
user__id=TEST_STUDENT1_USER_ID
)
course_session_user.optional_attendance = True
course_session_user.save()
else:
course_session_user = CourseSessionUser.objects.get(
user_id=TEST_STUDENT1_USER_ID
)
course_session_user.optional_attendance = False
course_session_user.save()
course.configuration.save() course.configuration.save()

View File

@ -114,8 +114,9 @@ class User(AbstractUser):
blank=True, blank=True,
) )
# fields gathered from cembra pay form
birth_date = models.DateField(null=True, blank=True) birth_date = models.DateField(null=True, blank=True)
# phone number should be stored in the format +41792018586 (not validated)
phone_number = models.CharField(max_length=255, blank=True, default="") phone_number = models.CharField(max_length=255, blank=True, default="")
# is only set by abacus invoice export code # is only set by abacus invoice export code

View File

@ -55,6 +55,7 @@ class UserSerializer(serializers.ModelSerializer):
course_session_experts = serializers.SerializerMethodField() course_session_experts = serializers.SerializerMethodField()
country = CountrySerializer() country = CountrySerializer()
organisation_country = CountrySerializer() organisation_country = CountrySerializer()
optional_attendance = serializers.SerializerMethodField()
class Meta: class Meta:
model = User model = User
@ -83,6 +84,7 @@ class UserSerializer(serializers.ModelSerializer):
"organisation_postal_code", "organisation_postal_code",
"organisation_city", "organisation_city",
"organisation_country", "organisation_country",
"optional_attendance",
] ]
read_only_fields = [ read_only_fields = [
"id", "id",
@ -108,6 +110,12 @@ class UserSerializer(serializers.ModelSerializer):
return [str(_id) for _id in (supervisor_in_session_ids | expert_in_session_ids)] return [str(_id) for _id in (supervisor_in_session_ids | expert_in_session_ids)]
def get_optional_attendance(self, obj: User) -> bool:
optional_attendance_ids = CourseSessionUser.objects.filter(
user=obj, optional_attendance=True
).values_list("course_session__id", flat=True)
return [str(id) for id in optional_attendance_ids]
def update(self, instance, validated_data): def update(self, instance, validated_data):
country_data = validated_data.pop("country", None) country_data = validated_data.pop("country", None)
organisation_country_data = validated_data.pop("organisation_country", None) organisation_country_data = validated_data.pop("organisation_country", None)
@ -131,6 +139,12 @@ class UserSerializer(serializers.ModelSerializer):
return instance return instance
class CypressUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = "__all__"
class OrganisationSerializer(serializers.ModelSerializer): class OrganisationSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(source="organisation_id", read_only=True) id = serializers.IntegerField(source="organisation_id", read_only=True)
name = serializers.SerializerMethodField() name = serializers.SerializerMethodField()

View File

@ -64,6 +64,7 @@ class CourseSessionUserAdmin(admin.ModelAdmin):
"course_session", "course_session",
"role", "role",
"circles", "circles",
"optional_attendance",
# "created_at", # "created_at",
# "updated_at", # "updated_at",
] ]
@ -76,6 +77,7 @@ class CourseSessionUserAdmin(admin.ModelAdmin):
list_filter = [ list_filter = [
"role", "role",
"course_session", "course_session",
"optional_attendance",
] ]
raw_id_fields = [ raw_id_fields = [
"user", "user",
@ -97,7 +99,7 @@ class CourseSessionUserAdmin(admin.ModelAdmin):
return ", ".join([c.title for c in obj.expert.all()]) return ", ".join([c.title for c in obj.expert.all()])
fieldsets = [ fieldsets = [
(None, {"fields": ("user", "course_session", "role")}), (None, {"fields": ("user", "course_session", "role", "optional_attendance")}),
( (
"Expert/Trainer", "Expert/Trainer",
{ {

View File

@ -28,3 +28,10 @@ UK_COURSE_IDS = [
COURSE_UK_TRAINING_FR, COURSE_UK_TRAINING_FR,
COURSE_UK_TRAINING_IT, COURSE_UK_TRAINING_IT,
] ]
# Organization IDs
ORGANISATION_OTHER_BROKER_ID = 1
ORGANISATION_OTHER_HEALTH_INSURANCE_ID = 2
ORGANISATION_OTHER_PRIVATE_INSURANCE_ID = 3
ORGANISATION_NO_COMPANY_ID = 31

View File

@ -151,6 +151,7 @@ class CourseSessionUserObjectsType(ObjectType):
circles = graphene.List( circles = graphene.List(
graphene.NonNull(CourseSessionUserExpertCircleType), required=True graphene.NonNull(CourseSessionUserExpertCircleType), required=True
) )
optional_attendance = graphene.Boolean(required=False)
class CircleDocumentObjectType(DjangoObjectType): class CircleDocumentObjectType(DjangoObjectType):
@ -233,6 +234,7 @@ class CourseSessionObjectType(DjangoObjectType):
) )
for circle in course_session_user.expert.all() # noqa for circle in course_session_user.expert.all() # noqa
], ],
optional_attendance=course_session_user.optional_attendance, # noqa
) )
) )

View File

@ -0,0 +1,32 @@
# Generated by Django 4.2.13 on 2024-07-25 05:47
from django.db import migrations, models
import vbv_lernwelt.course.models
class Migration(migrations.Migration):
dependencies = [
("course", "0008_auto_20240403_1132"),
]
operations = [
migrations.AddField(
model_name="coursesessionuser",
name="optional_attendance",
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name="coursecompletion",
name="completion_status",
field=models.CharField(
choices=[
("SUCCESS", "Success"),
("FAIL", "Fail"),
("UNKNOWN", "Unknown"),
],
default=vbv_lernwelt.course.models.CourseCompletionStatus["UNKNOWN"],
max_length=255,
),
),
]

View File

@ -283,6 +283,7 @@ class CourseSessionUser(models.Model):
expert = models.ManyToManyField( expert = models.ManyToManyField(
"learnpath.Circle", related_name="expert", blank=True "learnpath.Circle", related_name="expert", blank=True
) )
optional_attendance = models.BooleanField(default=False)
class Meta: class Meta:
constraints = [ constraints = [

View File

@ -0,0 +1,30 @@
import djclick as click
import structlog
from django.db.models import Count
from vbv_lernwelt.course.models import CourseSessionUser
logger = structlog.get_logger(__name__)
@click.command()
def command():
VV_COURSE_SESSIONS = [1, 2, 3] # DE, FR, IT
# Aggregation of users per organisation for a specific course session
user_counts = (
CourseSessionUser.objects.filter(course_session__id__in=VV_COURSE_SESSIONS)
.values("user__organisation__organisation_id", "user__organisation__name_de")
.annotate(user_count=Count("id"))
.order_by("user__organisation__organisation_id")
)
for entry in user_counts:
print(
f"Organisation Name: {entry['user__organisation__name_de']}, "
f"User Count: {entry['user_count']}"
)
print(
f"Total number of users: {sum([entry['user_count'] for entry in user_counts])}"
)

View File

@ -82,6 +82,10 @@ def _create_sheet(
# headers # headers
# common user headers..., <attendance_course> <date>, status <attendance_course>, .. # common user headers..., <attendance_course> <date>, status <attendance_course>, ..
col_idx = add_user_headers(sheet) col_idx = add_user_headers(sheet)
sheet.cell(row=1, column=col_idx, value=str(_("Optionale Anwesenheit")))
col_idx += 1
attendance_data = {} attendance_data = {}
for course in attendance_courses: for course in attendance_courses:
@ -110,6 +114,13 @@ def _create_sheet(
def _add_rows(sheet, users: list[CourseSessionUser], attendance_data): def _add_rows(sheet, users: list[CourseSessionUser], attendance_data):
for row_idx, user in enumerate(users, start=2): for row_idx, user in enumerate(users, start=2):
col_idx = add_user_export_data(sheet, user, row_idx) col_idx = add_user_export_data(sheet, user, row_idx)
optional_attendance_text = (
str(_("Ja")) if user.optional_attendance else str(_("Nein"))
)
sheet.cell(row=row_idx, column=col_idx, value=optional_attendance_text)
col_idx += 1
for key, user_dict_map in attendance_data.items(): for key, user_dict_map in attendance_data.items():
user_dict = user_dict_map.get(str(user.user.id), {}) user_dict = user_dict_map.get(str(user.user.id), {})
status = user_dict.get("status", "") if user_dict else "" status = user_dict.get("status", "") if user_dict else ""

View File

@ -8,7 +8,7 @@ from vbv_lernwelt.core.constants import TEST_STUDENT1_USER_ID, TEST_STUDENT2_USE
from vbv_lernwelt.core.create_default_users import create_default_users from vbv_lernwelt.core.create_default_users import create_default_users
from vbv_lernwelt.core.models import User from vbv_lernwelt.core.models import User
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, CourseSessionUser
from vbv_lernwelt.course_session.services.export_attendance import export_attendance from vbv_lernwelt.course_session.services.export_attendance import export_attendance
@ -42,6 +42,11 @@ class AttendanceExportTestCase(ExportBaseTestCase):
self.test_student1 = User.objects.get(id=TEST_STUDENT1_USER_ID) self.test_student1 = User.objects.get(id=TEST_STUDENT1_USER_ID)
self.test_student1.additional_json_data = {"Lehrvertragsnummer": 1234567890} self.test_student1.additional_json_data = {"Lehrvertragsnummer": 1234567890}
self.test_student1.save() self.test_student1.save()
csu1 = CourseSessionUser.objects.get(
user=self.test_student1, course_session=self.course_session_be
)
csu1.optional_attendance = True
csu1.save()
self.test_student2 = User.objects.get(id=TEST_STUDENT2_USER_ID) self.test_student2 = User.objects.get(id=TEST_STUDENT2_USER_ID)
self.test_student2.additional_json_data = {"Lehrvertragsnummer": 1987654321} self.test_student2.additional_json_data = {"Lehrvertragsnummer": 1987654321}
self.test_student2.save() self.test_student2.save()
@ -64,6 +69,7 @@ class AttendanceExportTestCase(ExportBaseTestCase):
self.test_student1.last_name, self.test_student1.last_name,
self.test_student1.email, self.test_student1.email,
self.test_student1.additional_json_data["Lehrvertragsnummer"], self.test_student1.additional_json_data["Lehrvertragsnummer"],
"Ja",
"Anwesend", "Anwesend",
], ],
[ [
@ -71,6 +77,7 @@ class AttendanceExportTestCase(ExportBaseTestCase):
self.test_student2.last_name, self.test_student2.last_name,
self.test_student2.email, self.test_student2.email,
self.test_student2.additional_json_data["Lehrvertragsnummer"], self.test_student2.additional_json_data["Lehrvertragsnummer"],
"Nein",
"Nicht anwesend", "Nicht anwesend",
], ],
[ [
@ -78,6 +85,7 @@ class AttendanceExportTestCase(ExportBaseTestCase):
self.test_student3.last_name, self.test_student3.last_name,
self.test_student3.email, self.test_student3.email,
None, None,
"Nein",
"Nicht anwesend", "Nicht anwesend",
], ],
] ]
@ -95,6 +103,7 @@ class AttendanceExportTestCase(ExportBaseTestCase):
"Nachname", "Nachname",
"Email", "Email",
"Lehrvertragsnummer", "Lehrvertragsnummer",
"Optionale Anwesenheit",
f"Anwesenheit {csac.get_circle().title} {csac.due_date.start.strftime('%d.%m.%Y')}", f"Anwesenheit {csac.get_circle().title} {csac.due_date.start.strftime('%d.%m.%Y')}",
] ]
@ -103,7 +112,7 @@ class AttendanceExportTestCase(ExportBaseTestCase):
self.assertEqual(len(wb.sheetnames), 1) self.assertEqual(len(wb.sheetnames), 1)
self.assertEqual(wb.sheetnames[0], "Test Bern 2022 a") self.assertEqual(wb.sheetnames[0], "Test Bern 2022 a")
self._check_export(wb, self.expected_data_be, 4, 5) self._check_export(wb, self.expected_data_be, 4, 6)
def test_attendance_export_multiple_cs(self): def test_attendance_export_multiple_cs(self):
self.attendance_course_zh.attendance_user_list = [ self.attendance_course_zh.attendance_user_list = [
@ -123,6 +132,7 @@ class AttendanceExportTestCase(ExportBaseTestCase):
self.test_student2.last_name, self.test_student2.last_name,
self.test_student2.email, self.test_student2.email,
self.test_student2.additional_json_data["Lehrvertragsnummer"], self.test_student2.additional_json_data["Lehrvertragsnummer"],
"Nein",
"Anwesend", "Anwesend",
], ],
] ]
@ -136,10 +146,10 @@ class AttendanceExportTestCase(ExportBaseTestCase):
self.assertEqual(wb.sheetnames[0], "Test Bern 2022 a") self.assertEqual(wb.sheetnames[0], "Test Bern 2022 a")
self.assertEqual(wb.sheetnames[1], "Test Zürich 2022 a") self.assertEqual(wb.sheetnames[1], "Test Zürich 2022 a")
self._check_export(wb, self.expected_data_be, 4, 5) self._check_export(wb, self.expected_data_be, 4, 6)
wb.active = wb["Test Zürich 2022 a"] wb.active = wb["Test Zürich 2022 a"]
self._check_export(wb, expected_data_zh, 2, 5) self._check_export(wb, expected_data_zh, 2, 6)
def test_french_export(self): def test_french_export(self):
activate("fr") activate("fr")
@ -150,6 +160,7 @@ class AttendanceExportTestCase(ExportBaseTestCase):
"Nom de famille", "Nom de famille",
"E-mail", "E-mail",
"Numéro de contrat d'apprentissage", "Numéro de contrat d'apprentissage",
"Présence facultative",
f"Présence {self.attendance_course_be.get_circle().title} {self.attendance_course_be.due_date.start.strftime('%d.%m.%Y')}", f"Présence {self.attendance_course_be.get_circle().title} {self.attendance_course_be.due_date.start.strftime('%d.%m.%Y')}",
] ]
@ -160,6 +171,7 @@ class AttendanceExportTestCase(ExportBaseTestCase):
self.test_student1.last_name, self.test_student1.last_name,
self.test_student1.email, self.test_student1.email,
self.test_student1.additional_json_data["Lehrvertragsnummer"], self.test_student1.additional_json_data["Lehrvertragsnummer"],
"Oui",
"Présent", "Présent",
], ],
[ [
@ -167,6 +179,7 @@ class AttendanceExportTestCase(ExportBaseTestCase):
self.test_student2.last_name, self.test_student2.last_name,
self.test_student2.email, self.test_student2.email,
self.test_student2.additional_json_data["Lehrvertragsnummer"], self.test_student2.additional_json_data["Lehrvertragsnummer"],
"Non",
"Pas présent", "Pas présent",
], ],
[ [
@ -174,10 +187,11 @@ class AttendanceExportTestCase(ExportBaseTestCase):
self.test_student3.last_name, self.test_student3.last_name,
self.test_student3.email, self.test_student3.email,
None, None,
"Non",
"Pas présent", "Pas présent",
], ],
] ]
self._check_export(wb, expected_data_be, 4, 5) self._check_export(wb, expected_data_be, 4, 6)
def test_italian_export(self): def test_italian_export(self):
activate("it") activate("it")
@ -188,6 +202,7 @@ class AttendanceExportTestCase(ExportBaseTestCase):
"Cognome", "Cognome",
"Email", "Email",
"Numero di contratto di tirocinio", "Numero di contratto di tirocinio",
"Presenza opzionale",
f"Presenza {self.attendance_course_be.get_circle().title} {self.attendance_course_be.due_date.start.strftime('%d.%m.%Y')}", f"Presenza {self.attendance_course_be.get_circle().title} {self.attendance_course_be.due_date.start.strftime('%d.%m.%Y')}",
] ]
@ -198,6 +213,7 @@ class AttendanceExportTestCase(ExportBaseTestCase):
self.test_student1.last_name, self.test_student1.last_name,
self.test_student1.email, self.test_student1.email,
self.test_student1.additional_json_data["Lehrvertragsnummer"], self.test_student1.additional_json_data["Lehrvertragsnummer"],
"",
"Presente", "Presente",
], ],
[ [
@ -205,6 +221,7 @@ class AttendanceExportTestCase(ExportBaseTestCase):
self.test_student2.last_name, self.test_student2.last_name,
self.test_student2.email, self.test_student2.email,
self.test_student2.additional_json_data["Lehrvertragsnummer"], self.test_student2.additional_json_data["Lehrvertragsnummer"],
"No",
"Non presente", "Non presente",
], ],
[ [
@ -212,7 +229,8 @@ class AttendanceExportTestCase(ExportBaseTestCase):
self.test_student3.last_name, self.test_student3.last_name,
self.test_student3.email, self.test_student3.email,
None, None,
"No",
"Non presente", "Non presente",
], ],
] ]
self._check_export(wb, expected_data_be, 4, 5) self._check_export(wb, expected_data_be, 4, 6)

View File

@ -1,5 +1,5 @@
import math import math
from typing import List from typing import List, Tuple
import graphene import graphene
@ -7,7 +7,6 @@ import vbv_lernwelt.assignment.models
from vbv_lernwelt.assignment.models import ( from vbv_lernwelt.assignment.models import (
AssignmentCompletion, AssignmentCompletion,
AssignmentCompletionStatus, AssignmentCompletionStatus,
AssignmentType,
) )
from vbv_lernwelt.competence.services import ( from vbv_lernwelt.competence.services import (
query_competence_course_session_assignments, query_competence_course_session_assignments,
@ -98,14 +97,23 @@ def get_assignment_completion_metrics(
assignment: vbv_lernwelt.assignment.models.Assignment, assignment: vbv_lernwelt.assignment.models.Assignment,
user_selection_ids: List[str] | None, user_selection_ids: List[str] | None,
urql_id_postfix: str = "", urql_id_postfix: str = "",
context=None,
) -> AssignmentCompletionMetricsType: ) -> AssignmentCompletionMetricsType:
if not context:
context = {}
if user_selection_ids: if user_selection_ids:
course_session_users = user_selection_ids course_session_users = user_selection_ids
else: else:
course_session_users = CourseSessionUser.objects.filter( key = f"CourseSessionUser_{course_session.id}"
course_session=course_session, if not key in context:
role=CourseSessionUser.Role.MEMBER, course_session_users = CourseSessionUser.objects.filter(
).values_list("user", flat=True) course_session=course_session,
role=CourseSessionUser.Role.MEMBER,
).values_list("user", flat=True)
context[key] = course_session_users
else:
course_session_users = context[key]
evaluation_results = AssignmentCompletion.objects.filter( evaluation_results = AssignmentCompletion.objects.filter(
completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED.value, completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED.value,
@ -139,33 +147,54 @@ def create_record(
course_session_assignment: CourseSessionAssignment | CourseSessionEdoniqTest, course_session_assignment: CourseSessionAssignment | CourseSessionEdoniqTest,
user_selection_ids: List[str] | None, user_selection_ids: List[str] | None,
urql_id_postfix: str = "", urql_id_postfix: str = "",
) -> AssignmentStatisticsRecordType: context=None,
if isinstance(course_session_assignment, CourseSessionAssignment): ) -> Tuple[AssignmentStatisticsRecordType, dict]:
due_date = course_session_assignment.submission_deadline if not context:
else: context = {}
due_date = course_session_assignment.deadline
assignment_type = (
"CourseSessionAssignment"
if isinstance(course_session_assignment, CourseSessionAssignment)
else "CourseSessionEdoniqTest"
)
due_date = (
course_session_assignment.submission_deadline
if assignment_type == "CourseSessionAssignment"
else course_session_assignment.deadline
)
key = f"{assignment_type}_{course_session_assignment.learning_content.id}"
if not key in context:
context[key] = course_session_assignment.learning_content.get_circle().id
circle_id = context[key]
learning_content = course_session_assignment.learning_content learning_content = course_session_assignment.learning_content
return AssignmentStatisticsRecordType( return (
# make sure it's unique, across all types of assignments! AssignmentStatisticsRecordType(
_id=f"{course_session_assignment._meta.model_name}#{course_session_assignment.id}@{urql_id_postfix}", # make sure it's unique, across all types of assignments!
# noqa _id=f"{course_session_assignment._meta.model_name}#{course_session_assignment.id}@{urql_id_postfix}",
course_session_id=str(course_session_assignment.course_session.id), # noqa # noqa
circle_id=learning_content.get_circle().id, # noqa course_session_id=str(course_session_assignment.course_session.id), # noqa
course_session_assignment_id=str(course_session_assignment.id), # noqa circle_id=circle_id, # noqa
generation=course_session_assignment.course_session.generation, # noqa course_session_assignment_id=str(course_session_assignment.id), # noqa
assignment_type_translation_key=due_date.assignment_type_translation_key, generation=course_session_assignment.course_session.generation, # noqa
# noqa assignment_type_translation_key=due_date.assignment_type_translation_key,
assignment_title=learning_content.content_assignment.title, # noqa # noqa
metrics=get_assignment_completion_metrics( # noqa assignment_title=learning_content.content_assignment.title, # noqa
course_session=course_session_assignment.course_session, # noqa metrics=get_assignment_completion_metrics( # noqa
assignment=learning_content.content_assignment, # noqa course_session=course_session_assignment.course_session, # noqa
user_selection_ids=user_selection_ids, # noqa assignment=learning_content.content_assignment, # noqa
urql_id_postfix=urql_id_postfix, # noqa user_selection_ids=user_selection_ids, # noqa
urql_id_postfix=urql_id_postfix, # noqa
context=context, # noqa
),
details_url=due_date.url_expert, # noqa
deadline=due_date.start, # noqa
), ),
details_url=due_date.url_expert, # noqa context,
deadline=due_date.start, # noqa
) )
@ -178,28 +207,26 @@ def assignments(
) -> AssignmentsStatisticsType: ) -> AssignmentsStatisticsType:
if urql_id is None: if urql_id is None:
urql_id = str(course_id) urql_id = str(course_id)
course_sessions = CourseSession.objects.filter( course_sessions = CourseSession.objects.filter(
id__in=course_session_selection_ids, id__in=course_session_selection_ids,
) )
records: List[AssignmentStatisticsRecordType] = [] records: List[AssignmentStatisticsRecordType] = []
context = {}
for course_session in course_sessions: csas = query_competence_course_session_assignments(course_sessions, circle_ids)
for csa in query_competence_course_session_assignments( csets = query_competence_course_session_edoniq_tests(course_sessions, circle_ids)
[course_session.id], circle_ids
):
record = create_record(csa, user_selection_ids, urql_id_postfix=urql_id)
records.append(record)
for cset in query_competence_course_session_edoniq_tests( for csa in csas:
[course_session.id], circle_ids record, context = create_record(
): csa, user_selection_ids, urql_id_postfix=urql_id, context=context
record = create_record( )
course_session_assignment=cset, records.append(record)
user_selection_ids=user_selection_ids,
urql_id_postfix=urql_id, for cset in csets:
) record, context = create_record(
records.append(record) cset, user_selection_ids, urql_id_postfix=urql_id, context=context
)
records.append(record)
return AssignmentsStatisticsType( return AssignmentsStatisticsType(
_id=urql_id, # noqa _id=urql_id, # noqa

View File

@ -41,25 +41,23 @@ def competences(
completions = CourseCompletion.objects.filter( completions = CourseCompletion.objects.filter(
course_session_id__in=course_session_selection_ids, course_session_id__in=course_session_selection_ids,
page_type="competence.PerformanceCriteria", page_type="competence.PerformanceCriteria",
).prefetch_related("course_session", "page") ).select_related("course_session", "page")
if user_selection_ids is not None: if user_selection_ids is not None:
completions = completions.filter(user_id__in=user_selection_ids) completions = completions.filter(user_id__in=user_selection_ids)
competence_records = {} page_ids = completions.values_list("page_id", flat=True).distinct()
pages = Page.objects.filter(id__in=page_ids).specific()
learning_units = {page.id: page.specific.learning_unit for page in pages}
unique_page_ids = {completion.page.id for completion in completions}
learning_units = {
page_id: Page.objects.get(id=page_id).specific.learning_unit
for page_id in unique_page_ids
}
circles = { circles = {
lu.id: c lu.id: c
for lu in learning_units.values() for lu in learning_units.values()
if (lu is not None and (c := lu.get_circle()) is not None) if lu and (c := lu.get_circle()) and (circle_ids is None or c.id in circle_ids)
and (circle_ids is None or c.id in circle_ids)
} }
competence_records = {}
for completion in completions: for completion in completions:
learning_unit = learning_units.get(completion.page.id) learning_unit = learning_units.get(completion.page.id)
@ -73,20 +71,23 @@ def competences(
combined_id = f"{circle.id}-{completion.course_session.id}@{urql_id_postfix}" combined_id = f"{circle.id}-{completion.course_session.id}@{urql_id_postfix}"
competence_records.setdefault(combined_id, {}).setdefault( if combined_id not in competence_records:
learning_unit, competence_records[combined_id] = {}
CompetenceRecordStatisticsType(
_id=combined_id, # noqa if learning_unit not in competence_records[combined_id]:
title=learning_unit.title, # noqa competence_records[combined_id][
course_session_id=completion.course_session.id, # noqa learning_unit
generation=completion.course_session.generation, # noqa ] = CompetenceRecordStatisticsType(
circle_id=circle.id, # noqa _id=combined_id,
success_count=0, # noqa title=learning_unit.title,
fail_count=0, # noqa course_session_id=completion.course_session.id,
generation=completion.course_session.generation,
circle_id=circle.id,
success_count=0,
fail_count=0,
details_url=f"/course/{course_slug}/cockpit?courseSessionId={completion.course_session.id}", details_url=f"/course/{course_slug}/cockpit?courseSessionId={completion.course_session.id}",
# noqa # noqa
), )
)
if completion.completion_status == CourseCompletionStatus.SUCCESS.value: if completion.completion_status == CourseCompletionStatus.SUCCESS.value:
competence_records[combined_id][learning_unit].success_count += 1 competence_records[combined_id][learning_unit].success_count += 1
@ -99,7 +100,7 @@ def competences(
for record in circle_records.values() for record in circle_records.values()
] ]
success_count = sum([c.success_count for c in values]) success_count = sum(c.success_count for c in values)
fail_count = sum([c.fail_count for c in values]) fail_count = sum(c.fail_count for c in values)
return values, success_count, fail_count return values, success_count, fail_count

View File

@ -0,0 +1,123 @@
from io import BytesIO
from typing import Optional
import structlog
from django.utils.translation import gettext_lazy as _
from openpyxl import Workbook
from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.models import CourseSession
from vbv_lernwelt.course_session.services.export_attendance import (
add_user_headers,
make_export_filename,
sanitize_sheet_name,
)
from vbv_lernwelt.dashboard.utils import create_person_list_with_roles
PERSONS_EXPORT_FILENAME = _("export_personen")
logger = structlog.get_logger(__name__)
def export_persons(
user: User,
course_session_ids: list[str],
save_as_file: bool = False,
) -> Optional[bytes]:
if not course_session_ids:
return
wb = Workbook()
# remove the first sheet is just easier than keeping track of the active sheet
wb.remove(wb.active)
user_with_roles = create_person_list_with_roles(user, course_session_ids)
course_sessions = CourseSession.objects.filter(id__in=course_session_ids)
for cs in course_sessions:
_create_sheet(
wb,
cs.title,
cs.id,
user_with_roles,
)
if save_as_file:
wb.save(make_export_filename(PERSONS_EXPORT_FILENAME))
else:
output = BytesIO()
wb.save(output)
output.seek(0)
return output.getvalue()
def _create_sheet(
wb: Workbook,
title: str,
cs_id: int,
user_with_roles,
):
sheet = wb.create_sheet(title=sanitize_sheet_name(title))
if len(user_with_roles) == 0:
return sheet
# headers
# common user headers, Circle <title> <learningcontenttitle> bestanden, Circle <title> <learningcontenttitle> Resultat, ...
col_idx = add_user_headers(sheet)
sheet.cell(row=1, column=col_idx, value=str(_("Telefon")))
sheet.cell(row=1, column=col_idx + 1, value=str(_("Rolle")))
_add_rows(sheet, user_with_roles, cs_id)
return sheet
def _add_rows(
sheet,
users,
course_session_id,
):
idx_offset = 0
for row_idx, user in enumerate(users, start=2):
def get_user_cs_by_id(user_cs, cs_id):
return next((cs for cs in user_cs if int(cs.get("id")) == cs_id), None)
user_cs = get_user_cs_by_id(user["course_sessions"], course_session_id)
if not user_cs:
logger.warning(
"User not found in course session",
user_id=user["user_id"],
course_session_id=course_session_id,
)
idx_offset += 1
continue
user_role = _role_as_string(user_cs.get("user_role"))
idx = row_idx - idx_offset
sheet.cell(row=idx, column=1, value=user["first_name"])
sheet.cell(row=idx, column=2, value=user["last_name"])
sheet.cell(row=idx, column=3, value=user["email"])
sheet.cell(row=idx, column=4, value=user.get("Lehrvertragsnummer", ""))
sheet.cell(
row=idx,
column=5,
value=user.get("phone_number", ""),
)
sheet.cell(row=idx, column=6, value=user_role)
def _role_as_string(role):
if role == "MEMBER":
return str(_("Teilnehmer"))
elif role == "EXPERT":
return str(_("Trainer"))
elif role == "SUPERVISOR":
return str(_("Regionenleiter"))
else:
return role

View File

@ -0,0 +1,186 @@
import io
from django.utils.translation import activate
from openpyxl import load_workbook
from vbv_lernwelt.core.constants import (
TEST_STUDENT1_USER_ID,
TEST_STUDENT2_USER_ID,
TEST_STUDENT3_USER_ID,
TEST_TRAINER1_USER_ID,
TEST_TRAINER2_USER_ID,
)
from vbv_lernwelt.core.create_default_users import create_default_users
from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.creators.test_course import create_test_course
from vbv_lernwelt.course.models import CourseSession
from vbv_lernwelt.course_session.tests.test_attendance_export import ExportBaseTestCase
from vbv_lernwelt.dashboard.person_export import export_persons
from vbv_lernwelt.learnpath.models import Circle
class PersonsExportTestCase(ExportBaseTestCase):
def setUp(self):
super().setUp()
create_default_users()
create_test_course(include_vv=False, with_sessions=True)
self.course_session_be = CourseSession.objects.get(title="Test Bern 2022 a")
self.course_session_zh = CourseSession.objects.get(title="Test Zürich 2022 a")
self.circle_fahrzeug = Circle.objects.get(
slug="test-lehrgang-lp-circle-fahrzeug"
)
self.circle_reisen = Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug")
self.test_trainer1 = User.objects.get(id=TEST_TRAINER1_USER_ID)
self.test_trainer2 = User.objects.get(id=TEST_TRAINER2_USER_ID)
self.test_student1 = User.objects.get(id=TEST_STUDENT1_USER_ID)
self.test_student2 = User.objects.get(id=TEST_STUDENT2_USER_ID)
self.test_student3 = User.objects.get(id=TEST_STUDENT3_USER_ID)
self.test_student1_row = [
self.test_student1.first_name,
self.test_student1.last_name,
self.test_student1.email,
None,
None,
"Teilnehmer",
]
self.test_student2_row = [
self.test_student2.first_name,
self.test_student2.last_name,
self.test_student2.email,
None,
None,
"Teilnehmer",
]
self.test_student3_row = [
self.test_student3.first_name,
self.test_student3.last_name,
self.test_student3.email,
None,
None,
"Teilnehmer",
]
self.test_trainer1_row = [
self.test_trainer1.first_name,
self.test_trainer1.last_name,
self.test_trainer1.email,
None,
None,
"Trainer",
]
self.test_trainer2_row = [
self.test_trainer2.first_name,
self.test_trainer2.last_name,
self.test_trainer2.email,
None,
None,
"Trainer",
]
def _generate_expected_data(self, rows):
expected_data = [
self._make_header(),
]
for r in rows:
expected_data.append(r)
return expected_data
def _generate_workbook(self, user, course_session_ids):
export_data = io.BytesIO(
export_persons(user, course_session_ids, save_as_file=False)
)
return load_workbook(export_data)
def _make_header(self):
return [
"Vorname",
"Nachname",
"Email",
"Lehrvertragsnummer",
"Telefon",
"Rolle",
]
def test_export_persons(self):
wb = self._generate_workbook(self.test_trainer1, [self.course_session_be.id])
self.assertEqual(len(wb.sheetnames), 1)
self.assertEqual(wb.sheetnames[0], "Test Bern 2022 a")
wb.active = wb["Test Bern 2022 a"]
data = self._generate_expected_data(
[
self.test_student1_row,
self.test_student2_row,
self.test_student3_row,
self.test_trainer1_row,
]
)
self._check_export(wb, data, 4, 6)
wb = self._generate_workbook(self.test_trainer2, [self.course_session_zh.id])
self.assertEqual(len(wb.sheetnames), 1)
self.assertEqual(wb.sheetnames[0], "Test Zürich 2022 a")
wb.active = wb["Test Zürich 2022 a"]
data = self._generate_expected_data(
[self.test_student2_row, self.test_trainer2_row]
)
self._check_export(wb, data, 3, 6)
def test_cannot_export_other_session(self):
wb = self._generate_workbook(self.test_trainer1, [self.course_session_zh.id])
self.assertEqual(len(wb.sheetnames), 1)
self.assertEqual(wb.sheetnames[0], "Test Zürich 2022 a")
wb.active = wb["Test Zürich 2022 a"]
data = self._generate_expected_data([[None] * 6])
self._check_export(wb, data, 1, 6)
def test_export_in_fr(self):
activate("fr")
wb = self._generate_workbook(self.test_trainer1, [self.course_session_be.id])
self.assertEqual(len(wb.sheetnames), 1)
self.assertEqual(wb.sheetnames[0], "Test Bern 2022 a")
wb.active = wb["Test Bern 2022 a"]
header = [
"Prénom",
"Nom de famille",
"E-mail",
"Numéro de contrat d'apprentissage",
"Téléphone",
"Rôle",
]
self.assertEqual([cell.value for cell in wb.active[1]], header)
self.assertEqual(wb.active.cell(row=2, column=6).value, "Participant")
self.assertEqual(
wb.active.cell(row=5, column=6).value, "Formateur / Formatrice"
)
def test_export_in_it(self):
activate("it")
wb = self._generate_workbook(self.test_trainer1, [self.course_session_be.id])
self.assertEqual(len(wb.sheetnames), 1)
self.assertEqual(wb.sheetnames[0], "Test Bern 2022 a")
wb.active = wb["Test Bern 2022 a"]
header = [
"Nome",
"Cognome",
"Email",
"Numero di contratto di tirocinio",
"Telefono",
"Ruolo",
]
self.assertEqual([cell.value for cell in wb.active[1]], header)
self.assertEqual(wb.active.cell(row=2, column=6).value, "Partecipante")
self.assertEqual(wb.active.cell(row=5, column=6).value, "Trainer")

View File

@ -0,0 +1,179 @@
from dataclasses import dataclass
from typing import List, Set
from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
from vbv_lernwelt.learning_mentor.models import LearningMentor
@dataclass(frozen=True)
class CourseSessionWithRoles:
_original: CourseSession
roles: Set[str]
def __getattr__(self, name: str):
# Delegate attribute access to the _original CourseSession object
return getattr(self._original, name)
def save(self, *args, **kwargs):
raise NotImplementedError("This proxy object cannot be saved.")
def get_course_sessions_with_roles_for_user(user: User) -> List[CourseSessionWithRoles]:
result_course_sessions = {}
# participant/member/expert course sessions
csu_qs = CourseSessionUser.objects.filter(user=user).prefetch_related(
"course_session", "course_session__course"
)
for csu in csu_qs:
cs = csu.course_session
# member/expert is mutually exclusive...
cs.roles = {csu.role}
result_course_sessions[cs.id] = cs
# enrich with supervisor course sessions
csg_qs = CourseSessionGroup.objects.filter(supervisor=user).prefetch_related(
"course_session", "course_session__course"
)
for csg in csg_qs:
for cs in csg.course_session.all():
cs.roles = set()
cs = result_course_sessions.get(cs.id, cs)
cs.roles.add("SUPERVISOR")
result_course_sessions[cs.id] = cs
# enrich with mentor course sessions
lm_qs = LearningMentor.objects.filter(mentor=user).prefetch_related(
"course_session", "course_session__course"
)
for lm in lm_qs:
cs = lm.course_session
cs.roles = set()
cs = result_course_sessions.get(cs.id, cs)
cs.roles.add("LEARNING_MENTOR")
result_course_sessions[cs.id] = cs
return [
CourseSessionWithRoles(cs, cs.roles) for cs in result_course_sessions.values()
]
def has_cs_role(roles: Set[str]) -> bool:
return bool(roles & {"SUPERVISOR", "EXPERT", "MEMBER"})
def user_role(roles: Set[str]) -> str:
if "SUPERVISOR" in roles:
return "SUPERVISOR"
if "EXPERT" in roles:
return "EXPERT"
if "MEMBER" in roles:
return "MEMBER"
return "LEARNING_MENTOR"
def create_course_session_dict(course_session_object, my_role, user_role):
return {
"id": str(course_session_object.id),
"session_title": course_session_object.title,
"course_id": str(course_session_object.course.id),
"course_title": course_session_object.course.title,
"course_slug": course_session_object.course.slug,
"region": course_session_object.region,
"generation": course_session_object.generation,
"my_role": my_role,
"user_role": user_role,
"is_uk": course_session_object.course.configuration.is_uk,
"is_vv": course_session_object.course.configuration.is_vv,
}
def create_person_list_with_roles(
user, course_session_ids=None, include_private_data=False
):
def create_user_dict(user_object):
user_data = {
"user_id": user_object.id,
"first_name": user_object.first_name,
"last_name": user_object.last_name,
"email": user_object.email,
"avatar_url_small": user_object.avatar_url_small,
"avatar_url": user_object.avatar_url,
"course_sessions": [],
}
if include_private_data:
user_data["phone_number"] = user_object.phone_number
user_data["Lehrvertragsnummer"] = user_object.additional_json_data.get(
"Lehrvertragsnummer", ""
)
return user_data
course_sessions = get_course_sessions_with_roles_for_user(user)
result_persons = {}
for cs in course_sessions:
if has_cs_role(cs.roles) and cs.course.configuration.is_uk:
course_session_users = CourseSessionUser.objects.filter(
course_session=cs.id
).select_related("user")
my_role = user_role(cs.roles)
for csu in course_session_users:
person_data = result_persons.get(
csu.user.id, create_user_dict(csu.user)
)
person_data["course_sessions"].append(
create_course_session_dict(cs, my_role, csu.role)
)
result_persons[csu.user.id] = person_data
# add persons where request.user is mentor
for cs in course_sessions:
if "LEARNING_MENTOR" in cs.roles:
lm = LearningMentor.objects.filter(
mentor=user, course_session=cs.id
).first()
for participant in lm.participants.all():
course_session_entry = create_course_session_dict(
cs,
"LEARNING_MENTOR",
"LEARNING_MENTEE",
)
if participant.user.id not in result_persons:
person_data = create_user_dict(participant.user)
person_data["course_sessions"] = [course_session_entry]
result_persons[participant.user.id] = person_data
else:
# user is already in result_persons
result_persons[participant.user.id]["course_sessions"].append(
course_session_entry
)
# add persons where request.user is mentee
mentor_relation_qs = LearningMentor.objects.filter(
participants__user=user
).prefetch_related("mentor", "course_session")
for mentor_relation in mentor_relation_qs:
cs = mentor_relation.course_session
course_session_entry = create_course_session_dict(
cs,
"LEARNING_MENTEE",
"LEARNING_MENTOR",
)
if mentor_relation.mentor.id not in result_persons:
person_data = create_user_dict(mentor_relation.mentor)
person_data["course_sessions"] = [course_session_entry]
result_persons[mentor_relation.mentor.id] = person_data
else:
# user is already in result_persons
result_persons[mentor_relation.mentor.id]["course_sessions"].append(
course_session_entry
)
return result_persons.values()

View File

@ -2,7 +2,7 @@ import base64
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass
from datetime import date from datetime import date
from enum import Enum from enum import Enum
from typing import List, Set, Tuple from typing import List, Tuple
from django.db.models import Q from django.db.models import Q
from django.http import HttpResponse from django.http import HttpResponse
@ -24,25 +24,27 @@ from vbv_lernwelt.competence.services import (
query_competence_course_session_edoniq_tests, query_competence_course_session_edoniq_tests,
) )
from vbv_lernwelt.core.models import User from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.models import ( from vbv_lernwelt.course.models import CourseConfiguration, CourseSessionUser
CourseConfiguration,
CourseSession,
CourseSessionUser,
)
from vbv_lernwelt.course.views import logger from vbv_lernwelt.course.views import logger
from vbv_lernwelt.course_session.services.export_attendance import ( from vbv_lernwelt.course_session.services.export_attendance import (
ATTENDANCE_EXPORT_FILENAME, ATTENDANCE_EXPORT_FILENAME,
export_attendance, export_attendance,
make_export_filename, make_export_filename,
) )
from vbv_lernwelt.course_session_group.models import CourseSessionGroup from vbv_lernwelt.dashboard.person_export import export_persons, PERSONS_EXPORT_FILENAME
from vbv_lernwelt.dashboard.utils import (
CourseSessionWithRoles,
create_course_session_dict,
create_person_list_with_roles,
get_course_sessions_with_roles_for_user,
user_role,
)
from vbv_lernwelt.duedate.models import DueDate from vbv_lernwelt.duedate.models import DueDate
from vbv_lernwelt.duedate.serializers import DueDateSerializer from vbv_lernwelt.duedate.serializers import DueDateSerializer
from vbv_lernwelt.feedback.export import ( from vbv_lernwelt.feedback.export import (
export_feedback_with_circle_restriction, export_feedback_with_circle_restriction,
FEEDBACK_EXPORT_FILE_NAME, FEEDBACK_EXPORT_FILE_NAME,
) )
from vbv_lernwelt.learning_mentor.models import LearningMentor
from vbv_lernwelt.learnpath.models import Circle from vbv_lernwelt.learnpath.models import Circle
from vbv_lernwelt.self_evaluation_feedback.models import SelfEvaluationFeedback from vbv_lernwelt.self_evaluation_feedback.models import SelfEvaluationFeedback
@ -65,19 +67,6 @@ class RoleKeyType(Enum):
TRAINER = "Trainer" TRAINER = "Trainer"
@dataclass(frozen=True)
class CourseSessionWithRoles:
_original: CourseSession
roles: Set[str]
def __getattr__(self, name: str):
# Delegate attribute access to the _original CourseSession object
return getattr(self._original, name)
def save(self, *args, **kwargs):
raise NotImplementedError("This proxy object cannot be saved.")
@dataclass(frozen=True) @dataclass(frozen=True)
class CourseConfig: class CourseConfig:
course_id: str course_id: str
@ -92,157 +81,6 @@ class CourseConfig:
session_to_continue_id: str | None session_to_continue_id: str | None
def get_course_sessions_with_roles_for_user(user: User) -> List[CourseSessionWithRoles]:
result_course_sessions = {}
# participant/member/expert course sessions
csu_qs = CourseSessionUser.objects.filter(user=user).prefetch_related(
"course_session", "course_session__course"
)
for csu in csu_qs:
cs = csu.course_session
# member/expert is mutually exclusive...
cs.roles = {csu.role}
result_course_sessions[cs.id] = cs
# enrich with supervisor course sessions
csg_qs = CourseSessionGroup.objects.filter(supervisor=user).prefetch_related(
"course_session", "course_session__course"
)
for csg in csg_qs:
for cs in csg.course_session.all():
cs.roles = set()
cs = result_course_sessions.get(cs.id, cs)
cs.roles.add("SUPERVISOR")
result_course_sessions[cs.id] = cs
# enrich with mentor course sessions
lm_qs = LearningMentor.objects.filter(mentor=user).prefetch_related(
"course_session", "course_session__course"
)
for lm in lm_qs:
cs = lm.course_session
cs.roles = set()
cs = result_course_sessions.get(cs.id, cs)
cs.roles.add("LEARNING_MENTOR")
result_course_sessions[cs.id] = cs
return [
CourseSessionWithRoles(cs, cs.roles) for cs in result_course_sessions.values()
]
def has_cs_role(roles: Set[str]) -> bool:
return bool(roles & {"SUPERVISOR", "EXPERT", "MEMBER"})
def user_role(roles: Set[str]) -> str:
if "SUPERVISOR" in roles:
return "SUPERVISOR"
if "EXPERT" in roles:
return "EXPERT"
if "MEMBER" in roles:
return "MEMBER"
return "LEARNING_MENTOR"
def _create_course_session_dict(course_session_object, my_role, user_role):
return {
"id": str(course_session_object.id),
"session_title": course_session_object.title,
"course_id": str(course_session_object.course.id),
"course_title": course_session_object.course.title,
"course_slug": course_session_object.course.slug,
"region": course_session_object.region,
"generation": course_session_object.generation,
"my_role": my_role,
"user_role": user_role,
"is_uk": course_session_object.course.configuration.is_uk,
"is_vv": course_session_object.course.configuration.is_vv,
}
def _create_person_list_with_roles(user):
def create_user_dict(user_object):
return {
"user_id": user_object.id,
"first_name": user_object.first_name,
"last_name": user_object.last_name,
"email": user_object.email,
"avatar_url_small": user_object.avatar_url_small,
"avatar_url": user_object.avatar_url,
"course_sessions": [],
}
course_sessions = get_course_sessions_with_roles_for_user(user)
result_persons = {}
for cs in course_sessions:
if has_cs_role(cs.roles) and cs.course.configuration.is_uk:
course_session_users = CourseSessionUser.objects.filter(
course_session=cs.id
)
my_role = user_role(cs.roles)
for csu in course_session_users:
person_data = result_persons.get(
csu.user.id, create_user_dict(csu.user)
)
person_data["course_sessions"].append(
_create_course_session_dict(cs, my_role, csu.role)
)
result_persons[csu.user.id] = person_data
# add persons where request.user is mentor
for cs in course_sessions:
if "LEARNING_MENTOR" in cs.roles:
lm = LearningMentor.objects.filter(
mentor=user, course_session=cs.id
).first()
for participant in lm.participants.all():
course_session_entry = _create_course_session_dict(
cs,
"LEARNING_MENTOR",
"LEARNING_MENTEE",
)
if participant.user.id not in result_persons:
person_data = create_user_dict(participant.user)
person_data["course_sessions"] = [course_session_entry]
result_persons[participant.user.id] = person_data
else:
# user is already in result_persons
result_persons[participant.user.id]["course_sessions"].append(
course_session_entry
)
# add persons where request.user is mentee
mentor_relation_qs = LearningMentor.objects.filter(
participants__user=user
).prefetch_related("mentor", "course_session")
for mentor_relation in mentor_relation_qs:
cs = mentor_relation.course_session
course_session_entry = _create_course_session_dict(
cs,
"LEARNING_MENTEE",
"LEARNING_MENTOR",
)
if mentor_relation.mentor.id not in result_persons:
person_data = create_user_dict(mentor_relation.mentor)
person_data["course_sessions"] = [course_session_entry]
result_persons[mentor_relation.mentor.id] = person_data
else:
# user is already in result_persons
result_persons[mentor_relation.mentor.id]["course_sessions"].append(
course_session_entry
)
return result_persons.values()
def _persons_list_add_competence_metrics(persons): def _persons_list_add_competence_metrics(persons):
course_session_ids = {cs["id"] for p in persons for cs in p["course_sessions"]} course_session_ids = {cs["id"] for p in persons for cs in p["course_sessions"]}
competence_assignments = query_competence_course_session_assignments( competence_assignments = query_competence_course_session_assignments(
@ -285,7 +123,7 @@ def _persons_list_add_competence_metrics(persons):
@api_view(["GET"]) @api_view(["GET"])
def get_dashboard_persons(request): def get_dashboard_persons(request):
try: try:
persons = list(_create_person_list_with_roles(request.user)) persons = list(create_person_list_with_roles(request.user))
if request.GET.get("with_competence_metrics", "") == "true": if request.GET.get("with_competence_metrics", "") == "true":
persons = _persons_list_add_competence_metrics(persons) persons = _persons_list_add_competence_metrics(persons)
@ -307,45 +145,28 @@ def get_dashboard_due_dates(request):
course_sessions = get_course_sessions_with_roles_for_user(request.user) course_sessions = get_course_sessions_with_roles_for_user(request.user)
course_session_ids = [cs.id for cs in course_sessions] course_session_ids = [cs.id for cs in course_sessions]
all_due_dates = DueDate.objects.filter(
course_session__id__in=course_session_ids
)
# filter only future due dates
due_dates = []
today = date.today() today = date.today()
for due_date in all_due_dates:
# due_dates.append(due_date)
if due_date.end:
if due_date.end.date() >= today:
due_dates.append(due_date)
elif due_date.start:
if due_date.start.date() >= today:
due_dates.append(due_date)
due_dates.sort(key=lambda x: x.start) # Fetch future due dates in a single query using Q objects for complex filtering
future_due_dates = DueDate.objects.filter(
# find course session by id in `course_sessions` Q(course_session_id__in=course_session_ids),
Q(end__gte=today) | Q(start__gte=today),
).select_related("course_session")
result_due_dates = [] result_due_dates = []
for due_date in due_dates: course_session_map = {cs.id: cs for cs in course_sessions}
data = DueDateSerializer(due_date).data
for due_date in sorted(future_due_dates, key=lambda x: x.start):
data = DueDateSerializer(due_date).data
cs = course_session_map.get(due_date.course_session_id)
cs = next(
course_session
for course_session in course_sessions
if course_session.id == due_date.course_session.id
)
if cs: if cs:
data["course_session"] = _create_course_session_dict( data["course_session"] = create_course_session_dict(
cs, my_role=user_role(cs.roles), user_role="" cs, my_role=user_role(cs.roles), user_role=""
) )
result_due_dates.append(data) result_due_dates.append(data)
return Response( return Response(status=200, data=result_due_dates)
status=200,
data=result_due_dates,
)
except PermissionDenied as e: except PermissionDenied as e:
raise e raise e
@ -595,6 +416,20 @@ def export_feedback_as_xsl(request):
return _make_excel_response(data, FEEDBACK_EXPORT_FILE_NAME) return _make_excel_response(data, FEEDBACK_EXPORT_FILE_NAME)
@api_view(["POST"])
def export_persons_as_xsl(request):
requested_course_session_ids = request.data.get("courseSessionIds", [])
course_sessions_with_roles = _get_permitted_courses_sessions_for_user(
request.user, requested_course_session_ids
) # noqa
data = export_persons(
request.user,
[cswr.id for cswr in course_sessions_with_roles],
)
return _make_excel_response(data, PERSONS_EXPORT_FILENAME)
def _get_permitted_courses_sessions_for_user( def _get_permitted_courses_sessions_for_user(
user: User, requested_coursesession_ids: List[str] user: User, requested_coursesession_ids: List[str]
) -> List[CourseSessionWithRoles]: ) -> List[CourseSessionWithRoles]:

View File

@ -0,0 +1,38 @@
import time
import structlog
from django.db import connection, reset_queries
logger = structlog.get_logger(__name__)
class QueryCountDebugMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if not request.path.startswith("/api/") and not request.path.startswith(
"/server/"
):
return self.get_response(request)
reset_queries()
start_queries = len(connection.queries)
start_time = time.time()
response = self.get_response(request)
end_queries = len(connection.queries)
end_time = time.time()
total_queries = end_queries - start_queries
duration = end_time - start_time
logger.debug(
"query_count_middleware",
request_path=request.path,
queries=total_queries,
duration=duration,
)
return response

View File

@ -765,7 +765,7 @@ def create_or_update_course_session_assignment(
csa.submission_deadline.save() csa.submission_deadline.save()
csa.evaluation_deadline.start = timezone.make_aware( csa.evaluation_deadline.start = timezone.make_aware(
start start
) + timezone.timedelta(days=45) ) + timezone.timedelta(days=60)
csa.evaluation_deadline.end = None csa.evaluation_deadline.end = None
csa.evaluation_deadline.save() csa.evaluation_deadline.save()
else: else:

View File

@ -78,32 +78,32 @@ def create_customer_xml(checkout_information: CheckoutInformation):
first_name=checkout_information.first_name, first_name=checkout_information.first_name,
company_name=( company_name=(
checkout_information.organisation_detail_name checkout_information.organisation_detail_name
if checkout_information.invoice_address == "org" if checkout_information.abacus_use_organisation_data()
else "" else ""
), ),
street=( street=(
checkout_information.organisation_street checkout_information.organisation_street
if checkout_information.invoice_address == "org" if checkout_information.abacus_use_organisation_data()
else checkout_information.street else checkout_information.street
), ),
house_number=( house_number=(
checkout_information.organisation_street_number checkout_information.organisation_street_number
if checkout_information.invoice_address == "org" if checkout_information.abacus_use_organisation_data()
else checkout_information.street_number else checkout_information.street_number
), ),
zip_code=( zip_code=(
checkout_information.organisation_postal_code checkout_information.organisation_postal_code
if checkout_information.invoice_address == "org" if checkout_information.abacus_use_organisation_data()
else checkout_information.postal_code else checkout_information.postal_code
), ),
city=( city=(
checkout_information.organisation_city checkout_information.organisation_city
if checkout_information.invoice_address == "org" if checkout_information.abacus_use_organisation_data()
else checkout_information.city else checkout_information.city
), ),
country=( country=(
checkout_information.organisation_country_id checkout_information.organisation_country_id
if checkout_information.invoice_address == "org" if checkout_information.abacus_use_organisation_data()
else checkout_information.country_id else checkout_information.country_id
), ),
language=customer.language, language=customer.language,

View File

@ -128,3 +128,15 @@ class CheckoutInformation(models.Model):
self.abacus_order_id = new_abacus_order_id self.abacus_order_id = new_abacus_order_id
self.save() self.save()
return self return self
def abacus_address_type(self) -> str:
# always use priv for abacus and CembraPay
return (
self.INVOICE_ADDRESS_ORGANISATION
if self.invoice_address == self.INVOICE_ADDRESS_ORGANISATION
and not self.cembra_byjuno_invoice
else self.INVOICE_ADDRESS_PRIVATE
)
def abacus_use_organisation_data(self) -> bool:
return self.abacus_address_type() == self.INVOICE_ADDRESS_ORGANISATION

View File

@ -1,13 +1,11 @@
import hashlib import hashlib
import hmac import hmac
import uuid
import requests import requests
import structlog import structlog
from django.conf import settings from django.conf import settings
from vbv_lernwelt.core.admin import User from vbv_lernwelt.core.admin import User
from vbv_lernwelt.shop.const import VV_PRODUCT_NUMBER
from vbv_lernwelt.shop.datatrans.datatrans_api_client import DatatransApiClient from vbv_lernwelt.shop.datatrans.datatrans_api_client import DatatransApiClient
from vbv_lernwelt.shop.models import CheckoutState from vbv_lernwelt.shop.models import CheckoutState
@ -73,6 +71,7 @@ def is_signature_valid(
def init_datatrans_transaction( def init_datatrans_transaction(
user: User, user: User,
refno: str,
amount_chf_centimes: int, amount_chf_centimes: int,
redirect_url_success: str, redirect_url_success: str,
redirect_url_error: str, redirect_url_error: str,
@ -91,8 +90,8 @@ def init_datatrans_transaction(
"amount": amount_chf_centimes, "amount": amount_chf_centimes,
"currency": "CHF", "currency": "CHF",
"language": user.language, "language": user.language,
"refno": str(uuid.uuid4()), "refno": str(refno),
"refno2": refno2, "refno2": str(refno2),
"webhook": {"url": webhook_url}, "webhook": {"url": webhook_url},
"redirect": { "redirect": {
"successUrl": redirect_url_success, "successUrl": redirect_url_success,
@ -101,9 +100,8 @@ def init_datatrans_transaction(
}, },
} }
# FIXME: test with working cembra byjuno invoice customer? if with_cembra_byjuno_invoice:
# if with_cembra_byjuno_invoice: payload["paymentMethods"] = ["INT"]
# payload["paymentMethods"] = ["INT"]
if datatrans_customer_data: if datatrans_customer_data:
payload["customer"] = datatrans_customer_data payload["customer"] = datatrans_customer_data
if datatrans_int_data: if datatrans_int_data:

View File

@ -163,6 +163,53 @@ class AbacusInvoiceTestCase(TestCase):
assert "<Street>Laupenstrasse</Street>" in customer_xml_content assert "<Street>Laupenstrasse</Street>" in customer_xml_content
assert "<Country>CH</Country>" in customer_xml_content assert "<Country>CH</Country>" in customer_xml_content
def test_create_customer_xml_byjuno_cembra_with_company_address(self):
_pat = User.objects.get(username="patrizia.huggel@eiger-versicherungen.ch")
_pat.abacus_debitor_number = 60000011
_pat.save()
_ignore_checkout_information = CheckoutInformationFactory(
user=_pat, abacus_order_id=6_000_000_123
)
feuz = User.objects.get(username="andreas.feuz@eiger-versicherungen.ch")
feuz_checkout_info = CheckoutInformationFactory(
user=feuz,
transaction_id="24021508331287484",
first_name="Andreas",
last_name="Feuz",
street="Eggersmatt",
street_number="32",
postal_code="1719",
city="Zumholz",
country_id="CH",
invoice_address="org",
organisation_detail_name="VBV",
organisation_street="Laupenstrasse",
organisation_street_number="10",
organisation_postal_code="3000",
organisation_city="Bern",
organisation_country_id="CH",
cembra_byjuno_invoice=True,
)
feuz_checkout_info.created_at = datetime(2024, 2, 15, 8, 33, 12, 0)
customer_xml_filename, customer_xml_content = create_customer_xml(
checkout_information=feuz_checkout_info
)
print(customer_xml_content)
print(customer_xml_filename)
self.assertEqual("myVBV_debi_60000012.xml", customer_xml_filename)
assert "<CustomerNumber>60000012</CustomerNumber>" in customer_xml_content
assert (
"<Email>andreas.feuz@eiger-versicherungen.ch</Email>"
in customer_xml_content
)
assert "<AddressNumber>60000012</AddressNumber>" in customer_xml_content
assert "<Name>Feuz</Name>" in customer_xml_content
assert "<Text>VBV</Text>" not in customer_xml_content
assert "<Street>Eggersmatt</Street>" in customer_xml_content
def test_render_customer_xml(self): def test_render_customer_xml(self):
customer_xml = render_customer_xml( customer_xml = render_customer_xml(
abacus_debitor_number=60000012, abacus_debitor_number=60000012,

View File

@ -71,9 +71,8 @@ class CheckoutAPITestCase(APITestCase):
# THEN # THEN
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertTrue(
f"https://pay.sandbox.datatrans.com/v1/start/1234567890", response.json()["next_step_url"].endswith("v1/start/1234567890")
response.json()["next_step_url"],
) )
ci = CheckoutInformation.objects.first() ci = CheckoutInformation.objects.first()
@ -154,9 +153,11 @@ class CheckoutAPITestCase(APITestCase):
) )
self.assertEqual( self.assertEqual(
0, 1,
CheckoutInformation.objects.count(), CheckoutInformation.objects.count(),
) )
ci = CheckoutInformation.objects.first()
self.assertEqual(ci.state, CheckoutState.FAILED)
def test_checkout_already_paid(self): def test_checkout_already_paid(self):
# GIVEN # GIVEN
@ -217,9 +218,8 @@ class CheckoutAPITestCase(APITestCase):
# THEN # THEN
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertTrue(
f"https://pay.sandbox.datatrans.com/v1/start/{transaction_id_next}", response.json()["next_step_url"].endswith(f"v1/start/{transaction_id_next}")
response.json()["next_step_url"],
) )
# check that we have two checkouts # check that we have two checkouts
@ -277,9 +277,8 @@ class CheckoutAPITestCase(APITestCase):
# THEN # THEN
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertTrue(
f"https://pay.sandbox.datatrans.com/v1/start/{transaction_id}", response.json()["next_step_url"].endswith(f"v1/start/{transaction_id}")
response.json()["next_step_url"],
) )
@patch("vbv_lernwelt.shop.views.init_datatrans_transaction") @patch("vbv_lernwelt.shop.views.init_datatrans_transaction")
@ -310,7 +309,6 @@ class CheckoutAPITestCase(APITestCase):
# THEN # THEN
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertTrue(
f"https://pay.sandbox.datatrans.com/v1/start/{transaction_id}", response.json()["next_step_url"].endswith(f"v1/start/{transaction_id}")
response.json()["next_step_url"],
) )

View File

@ -24,10 +24,8 @@ class DatatransServiceTest(TestCase):
@override_settings(DATATRANS_BASIC_AUTH_KEY="BASIC_AUTH_KEY") @override_settings(DATATRANS_BASIC_AUTH_KEY="BASIC_AUTH_KEY")
@patch("vbv_lernwelt.shop.services.requests.post") @patch("vbv_lernwelt.shop.services.requests.post")
@patch("vbv_lernwelt.shop.services.uuid.uuid4") def test_init_transaction_201(self, mock_post):
def test_init_transaction_201(self, mock_uuid, mock_post):
# GIVEN # GIVEN
mock_uuid.return_value = uuid.uuid4()
mock_post.return_value.status_code = 201 mock_post.return_value.status_code = 201
mock_post.return_value.json.return_value = { mock_post.return_value.json.return_value = {
"transactionId": 1234567890, "transactionId": 1234567890,
@ -38,6 +36,7 @@ class DatatransServiceTest(TestCase):
# WHEN # WHEN
transaction_id = init_datatrans_transaction( transaction_id = init_datatrans_transaction(
user=self.user, user=self.user,
refno="123321",
amount_chf_centimes=324_30, amount_chf_centimes=324_30,
redirect_url_success=f"{REDIRECT_URL}/success", redirect_url_success=f"{REDIRECT_URL}/success",
redirect_url_error=f"{REDIRECT_URL}/error", redirect_url_error=f"{REDIRECT_URL}/error",
@ -64,11 +63,12 @@ class DatatransServiceTest(TestCase):
with self.assertRaises(InitTransactionException): with self.assertRaises(InitTransactionException):
init_datatrans_transaction( init_datatrans_transaction(
user=self.user, user=self.user,
refno="123321",
amount_chf_centimes=324_30, amount_chf_centimes=324_30,
redirect_url_success=f"/success", redirect_url_success="/success",
redirect_url_error=f"/error", redirect_url_error="/error",
redirect_url_cancel=f"/cancel", redirect_url_cancel="/cancel",
webhook_url=f"/webhook", webhook_url="/webhook",
refno2="", refno2="",
) )
@ -80,7 +80,4 @@ class DatatransServiceTest(TestCase):
url = get_payment_url(transaction_id) url = get_payment_url(transaction_id)
# THEN # THEN
self.assertEqual( self.assertTrue(url.endswith(f"v1/start/{transaction_id}"))
url,
f"https://pay.sandbox.datatrans.com/v1/start/{transaction_id}",
)

View File

@ -93,7 +93,7 @@ def checkout_vv(request):
sku = request.data["product"] sku = request.data["product"]
base_redirect_url = request.data["redirect_url"] base_redirect_url = request.data["redirect_url"]
log.info(f"Checkout requested: sku", user_id=request.user.id, sku=sku) log.info("Checkout requested: sku", user_id=request.user.id, sku=sku)
try: try:
product = Product.objects.get(sku=sku) product = Product.objects.get(sku=sku)
@ -124,6 +124,38 @@ def checkout_vv(request):
disable_save="fakeapi" in settings.DATATRANS_API_ENDPOINT disable_save="fakeapi" in settings.DATATRANS_API_ENDPOINT
) )
address_data = request.data["address"]
country_code = address_data.pop("country_code")
address_data["country_id"] = country_code
organisation_country_code = "CH"
if "organisation_country_code" in address_data:
organisation_country_code = address_data.pop("organisation_country_code")
address_data["organisation_country_id"] = organisation_country_code
if "birth_date" in address_data and address_data["birth_date"]:
address_data["birth_date"] = date.fromisoformat(address_data["birth_date"])
checkout_info = CheckoutInformation.objects.create(
user=request.user,
state=CheckoutState.ONGOING,
# product
product_sku=sku,
product_price=product.price,
product_name=product.name,
product_description=product.description,
email=email,
ip_address=ip_address,
cembra_byjuno_invoice=with_cembra_byjuno_invoice,
device_fingerprint_session_key=request.data.get(
"device_fingerprint_session_key", ""
),
# address
**request.data["address"],
)
checkout_info.set_increment_abacus_order_id()
refno2 = f"{request.user.abacus_debitor_number}_{VV_PRODUCT_NUMBER}" refno2 = f"{request.user.abacus_debitor_number}_{VV_PRODUCT_NUMBER}"
try: try:
@ -138,10 +170,10 @@ def checkout_vv(request):
"street": f'{request.data["address"]["street"]} {request.data["address"]["street_number"]}', "street": f'{request.data["address"]["street"]} {request.data["address"]["street_number"]}',
"city": request.data["address"]["city"], "city": request.data["address"]["city"],
"zipCode": request.data["address"]["postal_code"], "zipCode": request.data["address"]["postal_code"],
"country": request.data["address"]["country_code"], "country": request.data["address"]["country_id"],
"phone": request.data["address"]["phone_number"], "phone": request.data["address"]["phone_number"],
"email": email, "email": email,
"birthDate": request.data["address"]["birth_date"], "birthDate": str(request.data["address"]["birth_date"]),
"language": request.user.language, "language": request.user.language,
"ipAddress": ip_address, "ipAddress": ip_address,
"type": "P", "type": "P",
@ -154,6 +186,7 @@ def checkout_vv(request):
} }
transaction_id = init_datatrans_transaction( transaction_id = init_datatrans_transaction(
user=request.user, user=request.user,
refno=str(checkout_info.abacus_order_id),
amount_chf_centimes=product.price, amount_chf_centimes=product.price,
redirect_url_success=checkout_success_url( redirect_url_success=checkout_success_url(
base_url=base_redirect_url, product_sku=sku base_url=base_redirect_url, product_sku=sku
@ -170,6 +203,8 @@ def checkout_vv(request):
with_cembra_byjuno_invoice=with_cembra_byjuno_invoice, with_cembra_byjuno_invoice=with_cembra_byjuno_invoice,
) )
except InitTransactionException as e: except InitTransactionException as e:
checkout_info.state = CheckoutState.FAILED.value
checkout_info.save()
if not settings.DEBUG: if not settings.DEBUG:
log.error("Transaction initiation failed", exc_info=True, error=str(e)) log.error("Transaction initiation failed", exc_info=True, error=str(e))
capture_exception(e) capture_exception(e)
@ -180,36 +215,8 @@ def checkout_vv(request):
), ),
) )
address_data = request.data["address"] checkout_info.transaction_id = transaction_id
country_code = address_data.pop("country_code") checkout_info.save()
address_data["country_id"] = country_code
organisation_country_code = "CH"
if "organisation_country_code" in address_data:
organisation_country_code = address_data.pop("organisation_country_code")
address_data["organisation_country_id"] = organisation_country_code
if "birth_date" in address_data and address_data["birth_date"]:
address_data["birth_date"] = date.fromisoformat(address_data["birth_date"])
checkout_info = CheckoutInformation.objects.create(
user=request.user,
state=CheckoutState.ONGOING,
transaction_id=transaction_id,
# product
product_sku=sku,
product_price=product.price,
product_name=product.name,
product_description=product.description,
email=email,
ip_address=ip_address,
cembra_byjuno_invoice=with_cembra_byjuno_invoice,
device_fingerprint_session_key=request.data.get(
"device_fingerprint_session_key", ""
),
# address
**request.data["address"],
)
return next_step_response(url=get_payment_url(transaction_id)) return next_step_response(url=get_payment_url(transaction_id))