Merged develop into feature/VBV-704-eine-note-im-kn-für-ük
This commit is contained in:
commit
6f2b437a5c
|
|
@ -97,6 +97,7 @@ js-linting: &js-linting
|
|||
|
||||
default-steps: &default-steps
|
||||
- parallel:
|
||||
- step: *e2e
|
||||
- step: *e2e
|
||||
- step: *e2e
|
||||
- step: *python-tests
|
||||
|
|
|
|||
|
|
@ -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_REGION_NAME": "eu-central-1",
|
||||
"AWS_STORAGE_BUCKET_NAME": "myvbv-dev.iterativ.ch",
|
||||
"DATATRANS_HMAC_KEY": env.str("DATATRANS_HMAC_KEY", ""),
|
||||
"DATATRANS_BASIC_AUTH_KEY": env.str("DATATRANS_BASIC_AUTH_KEY", ""),
|
||||
"DATATRANS_HMAC_KEY": env.str("PIPELINES_DATATRANS_HMAC_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",
|
||||
"IT_DJANGO_DEBUG": "false",
|
||||
"IT_SERVE_VUE": "false",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type { CourseStatisticsType } from "@/gql/graphql";
|
|||
import AssignmentSummaryBox from "@/components/dashboard/AssignmentSummaryBox.vue";
|
||||
import FeedbackSummaryBox from "@/components/dashboard/FeedbackSummaryBox.vue";
|
||||
import CompetenceSummaryBox from "@/components/dashboard/CompetenceSummaryBox.vue";
|
||||
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
courseId: string;
|
||||
|
|
@ -95,4 +96,7 @@ onMounted(async () => {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex w-full flex-row justify-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPa
|
|||
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
|
||||
import { computed } from "vue";
|
||||
import { useCourseCircleProgress, useCourseDataWithCompletion } from "@/composables";
|
||||
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
|
||||
|
||||
export type DiagramType = "horizontal" | "horizontalSmall" | "singleSmall";
|
||||
|
||||
|
|
@ -54,23 +55,25 @@ const { inProgressCirclesCount, circlesCount } = useCourseCircleProgress(
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<h4
|
||||
v-if="diagramType === 'horizontal' && circles.length > 0"
|
||||
class="mb-4 font-bold"
|
||||
>
|
||||
{{
|
||||
$t("learningPathPage.progressText", {
|
||||
inProgressCount: inProgressCirclesCount,
|
||||
allCount: circlesCount,
|
||||
})
|
||||
}}
|
||||
</h4>
|
||||
<div :class="wrapperClasses">
|
||||
<LearningPathCircle
|
||||
v-for="circle in circles"
|
||||
:key="circle.id"
|
||||
:sectors="calculateCircleSectorData(circle)"
|
||||
></LearningPathCircle>
|
||||
<div v-if="circlesCount > 0">
|
||||
<h4 v-if="diagramType === 'horizontal'" class="mb-4 font-bold">
|
||||
{{
|
||||
$t("learningPathPage.progressText", {
|
||||
inProgressCount: inProgressCirclesCount,
|
||||
allCount: circlesCount,
|
||||
})
|
||||
}}
|
||||
</h4>
|
||||
<div :class="wrapperClasses">
|
||||
<LearningPathCircle
|
||||
v-for="circle in circles"
|
||||
:key="circle.id"
|
||||
:sectors="calculateCircleSectorData(circle)"
|
||||
></LearningPathCircle>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex justify-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ const orgAddress = computed({
|
|||
for="company-name"
|
||||
class="block text-sm font-medium leading-6 text-gray-900"
|
||||
>
|
||||
{{ $t("a.Name") }}
|
||||
{{ $t("a.Firmenname") }}
|
||||
</label>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useEntities } from "@/services/entities";
|
||||
import VueDatePicker from "@vuepic/vue-datepicker";
|
||||
import "@vuepic/vue-datepicker/dist/main.css";
|
||||
import { t } from "i18next";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import ItDatePicker from "@/components/ui/ItDatePicker.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: {
|
||||
|
|
@ -25,20 +24,12 @@ const props = defineProps<{
|
|||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const { countries } = useEntities();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const paymentMethods = [
|
||||
{ value: "credit_card", label: t("a.Debit-/Kreditkarte/Twint") },
|
||||
{ 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({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
|
|
@ -234,40 +225,8 @@ const address = computed({
|
|||
{{ $t("a.Geburtsdatum") }}
|
||||
</label>
|
||||
<div class="mt-2">
|
||||
<VueDatePicker
|
||||
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>
|
||||
<ItDatePicker v-model="address.birth_date"></ItDatePicker>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { useEntities } from "@/services/entities";
|
|||
import AvatarImage from "@/components/ui/AvatarImage.vue";
|
||||
import { ref } from "vue";
|
||||
import { type User, useUserStore } from "@/stores/user";
|
||||
import ItDatePicker from "@/components/ui/ItDatePicker.vue";
|
||||
import { normalizeSwissPhoneNumber } from "@/utils/phone";
|
||||
|
||||
const emit = defineEmits(["cancel", "save"]);
|
||||
|
||||
|
|
@ -21,7 +23,12 @@ const formData = ref({
|
|||
postal_code: user.postal_code,
|
||||
city: user.city,
|
||||
country_code: user.country?.country_code,
|
||||
|
||||
phone_number: user.phone_number,
|
||||
birth_date: user.birth_date,
|
||||
|
||||
organisation: user.organisation,
|
||||
organisation_detail_name: user.organisation_detail_name,
|
||||
organisation_street: user.organisation_street,
|
||||
organisation_street_number: user.organisation_street_number,
|
||||
organisation_postal_code: user.organisation_postal_code,
|
||||
|
|
@ -31,7 +38,8 @@ const formData = ref({
|
|||
});
|
||||
|
||||
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 };
|
||||
|
||||
typedProfileData.country = countries.value.find(
|
||||
|
|
@ -41,6 +49,10 @@ async function save() {
|
|||
(c) => c.country_code === organisation_country_code
|
||||
);
|
||||
|
||||
if (phone_number) {
|
||||
typedProfileData.phone_number = normalizeSwissPhoneNumber(phone_number);
|
||||
}
|
||||
|
||||
await user.updateUserProfile(typedProfileData);
|
||||
emit("save");
|
||||
}
|
||||
|
|
@ -126,6 +138,28 @@ async function avatarUpload(e: Event) {
|
|||
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"
|
||||
/>
|
||||
|
||||
<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">
|
||||
{{ $t("a.Profilbild") }}
|
||||
</label>
|
||||
|
|
@ -264,6 +298,22 @@ async function avatarUpload(e: Event) {
|
|||
{{ $t("a.Firmenanschrift") }}
|
||||
</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="w-full md:max-w-sm">
|
||||
<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')">
|
||||
{{ $t("general.cancel") }}
|
||||
</button>
|
||||
<button class="btn btn-primary" @click="save">
|
||||
<button class="btn btn-primary" data-cy="saveButton" @click="save">
|
||||
{{ $t("general.save") }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { useUserStore } from "@/stores/user";
|
|||
import { computed } from "vue";
|
||||
import { useEntities } from "@/services/entities";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import dayjs from "dayjs";
|
||||
import { displaySwissPhoneNumber } from "@/utils/phone";
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
|
@ -10,21 +12,20 @@ const user = useUserStore();
|
|||
const { organisations } = useEntities();
|
||||
|
||||
const privateAddress = computed(() => {
|
||||
let addressText = `${user.street} ${user.street_number}`.trim();
|
||||
if (user.postal_code || user.city) {
|
||||
if (addressText.length) {
|
||||
addressText += ", ";
|
||||
}
|
||||
addressText += `${user.postal_code} ${user.city}`;
|
||||
}
|
||||
if (user.country) {
|
||||
if (addressText.length) {
|
||||
addressText += ", ";
|
||||
}
|
||||
addressText += user.country.name;
|
||||
const textParts = [];
|
||||
|
||||
if (user.street || user.street_number) {
|
||||
textParts.push(`${user.street} ${user.street_number}`.trim());
|
||||
}
|
||||
|
||||
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(() => {
|
||||
|
|
@ -36,22 +37,25 @@ const organisationName = computed(() => {
|
|||
});
|
||||
|
||||
const orgAddress = computed(() => {
|
||||
let addressText =
|
||||
`${user.organisation_street} ${user.organisation_street_number}`.trim();
|
||||
if (user.organisation_postal_code || user.organisation_city) {
|
||||
if (addressText.length) {
|
||||
addressText += ", ";
|
||||
}
|
||||
addressText += `${user.organisation_postal_code} ${user.organisation_city}`;
|
||||
}
|
||||
if (user.organisation_country) {
|
||||
if (addressText.length) {
|
||||
addressText += ", ";
|
||||
}
|
||||
addressText += user.organisation_country.name;
|
||||
const textParts = [];
|
||||
|
||||
if (user.organisation_detail_name) {
|
||||
textParts.push(user.organisation_detail_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(() => {
|
||||
|
|
@ -67,20 +71,45 @@ const invoiceAddress = computed(() => {
|
|||
<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">
|
||||
<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>
|
||||
<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">
|
||||
{{ $t("a.E-Mail Adresse") }}
|
||||
</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">
|
||||
{{ $t("a.Privatadresse") }}
|
||||
</label>
|
||||
<div class="mb-3 sm:col-span-2 sm:mb-0">
|
||||
<template v-if="privateAddress">
|
||||
{{ privateAddress }}
|
||||
</template>
|
||||
<div class="mb-3 sm:col-span-2 sm:mb-0" data-cy="privateAddress">
|
||||
<div v-if="privateAddress.length">
|
||||
<span v-for="(line, index) in privateAddress" :key="index">
|
||||
{{ line }}
|
||||
<br />
|
||||
</span>
|
||||
</div>
|
||||
<span v-else class="text-gray-800">{{ $t("a.Keine Angabe") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -89,14 +118,19 @@ const invoiceAddress = computed(() => {
|
|||
<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">
|
||||
<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">
|
||||
{{ $t("a.Firmenanschrift") }}
|
||||
</label>
|
||||
<div class="sm:col-span-2">
|
||||
<template v-if="orgAddress">
|
||||
{{ orgAddress }}
|
||||
</template>
|
||||
<div v-if="orgAddress" data-cy="organisationAddress">
|
||||
<span v-for="(line, index) in orgAddress" :key="index">
|
||||
{{ line }}
|
||||
<br />
|
||||
</span>
|
||||
</div>
|
||||
<span v-else class="text-gray-800">{{ $t("a.Keine Angabe") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -1,18 +1,30 @@
|
|||
<script setup lang="ts">
|
||||
import ItRow from "@/components/ui/ItRow.vue";
|
||||
|
||||
defineProps<{
|
||||
export interface Props {
|
||||
avatarUrl: string;
|
||||
name: string;
|
||||
}>();
|
||||
extraInfo?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
extraInfo: "",
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ItRow>
|
||||
<template #firstRow>
|
||||
<slot name="leading"></slot>
|
||||
<img class="mr-2 h-11 w-11 rounded-full" :src="avatarUrl" />
|
||||
<p class="text-bold lg:leading-[45px]">{{ name }}</p>
|
||||
<img class="mr-2 h-11 w-11 rounded-full" :src="props.avatarUrl" />
|
||||
<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 #center>
|
||||
<slot name="center"></slot>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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 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 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 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,
|
||||
|
|
@ -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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -713,6 +713,7 @@ type CourseSessionUserObjectsType {
|
|||
avatar_url: String!
|
||||
role: String!
|
||||
circles: [CourseSessionUserExpertCircleType!]!
|
||||
optional_attendance: Boolean
|
||||
}
|
||||
|
||||
type CourseSessionUserExpertCircleType {
|
||||
|
|
|
|||
|
|
@ -192,6 +192,7 @@ export const COURSE_SESSION_DETAIL_QUERY = graphql(`
|
|||
title
|
||||
slug
|
||||
}
|
||||
optional_attendance
|
||||
}
|
||||
attendance_courses {
|
||||
id
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import VerticalBarChart from "@/components/ui/VerticalBarChart.vue";
|
|||
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
|
||||
import logger from "loglevel";
|
||||
import { reactive, ref } from "vue";
|
||||
import "@vuepic/vue-datepicker/dist/main.css";
|
||||
|
||||
const state = reactive({
|
||||
checkboxValue: true,
|
||||
|
|
|
|||
|
|
@ -224,6 +224,9 @@ watch(
|
|||
:name="`${csu.first_name} ${csu.last_name}`"
|
||||
:avatar-url="csu.avatar_url"
|
||||
:class="0 === index ? 'border-none' : ''"
|
||||
:extra-info="
|
||||
csu.optional_attendance ? `${$t('a.Optionale Anwesenheit')}` : ''
|
||||
"
|
||||
>
|
||||
<template #leading>
|
||||
<ItCheckbox
|
||||
|
|
|
|||
|
|
@ -6,9 +6,14 @@ import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
|||
import { computed, ref, watch } from "vue";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import _ from "lodash";
|
||||
import type { DashboardPersonCourseSessionType } from "@/services/dashboard";
|
||||
import {
|
||||
type DashboardPersonCourseSessionType,
|
||||
exportPersons,
|
||||
} from "@/services/dashboard";
|
||||
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");
|
||||
|
||||
|
|
@ -28,6 +33,7 @@ type MenuItem = {
|
|||
};
|
||||
|
||||
const { t } = useTranslation();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const { loading, dashboardPersons } = useDashboardPersonsDueDates(props.mode);
|
||||
|
||||
|
|
@ -227,6 +233,32 @@ function personRoleDisplayValue(personCourseSession: DashboardPersonCourseSessio
|
|||
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, () => {
|
||||
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>
|
||||
<span class="inline">{{ $t("general.back") }}</span>
|
||||
</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">
|
||||
<section
|
||||
v-if="filtersVisible"
|
||||
|
|
|
|||
|
|
@ -51,10 +51,6 @@ const submissionDeadline = computed(() => {
|
|||
?.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
|
||||
const stepIndex = useRouteQuery("step", "0", { transform: Number, mode: "push" });
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import WizardPage from "@/components/onboarding/WizardPage.vue";
|
|||
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import { profileNextRoute } from "@/services/onboarding";
|
||||
import { isOtherOrganisation, profileNextRoute } from "@/services/onboarding";
|
||||
import { useEntities } from "@/services/entities";
|
||||
import AvatarImage from "@/components/ui/AvatarImage.vue";
|
||||
|
||||
|
|
@ -13,9 +13,12 @@ const { t } = useTranslation();
|
|||
|
||||
const user = useUserStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const { organisations } = useEntities();
|
||||
|
||||
const organisationDetailName = ref<string>("");
|
||||
|
||||
const selectedOrganisation = ref({
|
||||
id: 0,
|
||||
name: t("a.Auswählen"),
|
||||
|
|
@ -35,7 +38,11 @@ watch(
|
|||
);
|
||||
|
||||
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);
|
||||
|
|
@ -56,15 +63,21 @@ async function avatarUpload(e: Event) {
|
|||
}
|
||||
}
|
||||
|
||||
watch(selectedOrganisation, async (organisation) => {
|
||||
async function updateUserProfile() {
|
||||
await user.updateUserProfile({
|
||||
organisation: organisation.id,
|
||||
organisation: selectedOrganisation.value.id,
|
||||
organisation_detail_name: organisationDetailName.value.trim(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const nextRoute = computed(() => {
|
||||
return profileNextRoute(route.params.courseType);
|
||||
});
|
||||
|
||||
async function navigateNextRoute() {
|
||||
await updateUserProfile();
|
||||
await router.push({ name: nextRoute.value });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -86,6 +99,19 @@ const nextRoute = computed(() => {
|
|||
|
||||
<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>
|
||||
<h3 class="mb-3">{{ $t("a.Profilbild") }}</h3>
|
||||
|
|
@ -117,18 +143,16 @@ const nextRoute = computed(() => {
|
|||
</template>
|
||||
|
||||
<template #footer>
|
||||
<router-link v-slot="{ navigate }" :to="{ name: nextRoute }" custom>
|
||||
<button
|
||||
:disabled="!validOrganisation"
|
||||
class="btn-blue flex items-center"
|
||||
role="link"
|
||||
data-cy="continue-button"
|
||||
@click="navigate"
|
||||
>
|
||||
{{ $t("general.next") }}
|
||||
<it-icon-arrow-right class="it-icon ml-2 h-6 w-6" />
|
||||
</button>
|
||||
</router-link>
|
||||
<button
|
||||
:disabled="!validOrganisation"
|
||||
class="btn-blue flex items-center"
|
||||
role="link"
|
||||
data-cy="continue-button"
|
||||
@click="navigateNextRoute"
|
||||
>
|
||||
{{ $t("general.next") }}
|
||||
<it-icon-arrow-right class="it-icon ml-2 h-6 w-6" />
|
||||
</button>
|
||||
</template>
|
||||
</WizardPage>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@ import DatatransCembraDeviceFingerprint from "@/components/onboarding/DatatransC
|
|||
import { getLocalSessionKey } from "@/statistics";
|
||||
import log from "loglevel";
|
||||
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({
|
||||
courseType: {
|
||||
|
|
@ -31,11 +37,14 @@ const userOrganisationName = computed(() => {
|
|||
}
|
||||
|
||||
// Those IDs do not represent a company
|
||||
// 1: Other broker
|
||||
// 2: Other insurance
|
||||
// 3: Other private insurance
|
||||
// 31: No company relation
|
||||
if ([1, 2, 3, 31].includes(user.organisation)) {
|
||||
if (
|
||||
[
|
||||
ORGANISATION_OTHER_BROKER_ID,
|
||||
ORGANISATION_OTHER_HEALTH_INSURANCE_ID,
|
||||
ORGANISATION_OTHER_PRIVATE_INSURANCE_ID,
|
||||
ORGANISATION_NO_COMPANY_ID,
|
||||
].includes(user.organisation)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -256,11 +265,10 @@ const executePayment = async () => {
|
|||
|
||||
<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")
|
||||
}}:
|
||||
<a href="mailto:vermittler@vbv-afa.ch" class="underline">
|
||||
vermittler@vbv-afa.ch
|
||||
</a>
|
||||
$t(
|
||||
"a.Fehler bei der Zahlung. Bitte versuche es erneut oder wähle eine andere Zahlungsmethode."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
|
||||
<h3 class="mb-4 mt-10">{{ $t("a.Adresse") }}</h3>
|
||||
|
|
@ -285,7 +293,7 @@ const executePayment = async () => {
|
|||
{{ formErrors.personal.join(", ") }}
|
||||
</p>
|
||||
|
||||
<section v-if="address.payment_method !== 'cembra_byjuno'">
|
||||
<section>
|
||||
<div class="mt-4">
|
||||
<button
|
||||
v-if="!withCompanyAddress"
|
||||
|
|
|
|||
|
|
@ -45,7 +45,11 @@ function startEditMode() {
|
|||
|
||||
<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">
|
||||
<button class="btn btn-secondary" @click="startEditMode">
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
data-cy="editProfileButton"
|
||||
@click="startEditMode"
|
||||
>
|
||||
{{ $t("a.Profil bearbeiten") }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -53,7 +53,17 @@ onMounted(() => {
|
|||
<div class="flex flex-col">
|
||||
<h2 class="mb-2">{{ user.first_name }} {{ user.last_name }}</h2>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
dashboardConfigs: DashboardCourseConfigType[],
|
||||
courseSlug: string
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
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[]) {
|
||||
if (courseType === "uk") {
|
||||
|
|
@ -10,3 +15,11 @@ export function profileNextRoute(courseType: string | string[]) {
|
|||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function isOtherOrganisation(orgId: number) {
|
||||
return [
|
||||
ORGANISATION_OTHER_BROKER_ID,
|
||||
ORGANISATION_OTHER_HEALTH_INSURANCE_ID,
|
||||
ORGANISATION_OTHER_PRIVATE_INSURANCE_ID,
|
||||
].includes(orgId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,22 +17,31 @@ describe("checkout.cy.js", () => {
|
|||
|
||||
cy.get('[data-cy="account-confirm-title"]').should(
|
||||
"contain",
|
||||
"Konto erstellen"
|
||||
"Konto erstellen",
|
||||
);
|
||||
cy.get('[data-cy="continue-button"]').click();
|
||||
|
||||
cy.get('[data-cy="account-profile-title"]').should(
|
||||
"contain",
|
||||
"Profil ergänzen"
|
||||
"Profil ergänzen",
|
||||
);
|
||||
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.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
|
||||
cy.get('[data-cy="account-checkout-title"]').should(
|
||||
"contain",
|
||||
"Lehrgang kaufen"
|
||||
"Lehrgang kaufen",
|
||||
);
|
||||
cy.get("#street-address").type("Eggersmatt");
|
||||
cy.get("#street-number").type("32");
|
||||
|
|
@ -40,7 +49,7 @@ describe("checkout.cy.js", () => {
|
|||
cy.get("#city").type("Zumholz");
|
||||
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-number").type("16");
|
||||
cy.get("#company-postal-code").type("3012");
|
||||
|
|
@ -60,7 +69,7 @@ describe("checkout.cy.js", () => {
|
|||
expect(ci.country).to.equal("CH");
|
||||
|
||||
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_number).to.equal("16");
|
||||
expect(ci.organisation_postal_code).to.equal("3012");
|
||||
|
|
@ -72,12 +81,32 @@ describe("checkout.cy.js", () => {
|
|||
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
|
||||
cy.get('[data-cy="pay-button"]').click();
|
||||
|
||||
cy.get('[data-cy="checkout-success-title"]').should(
|
||||
"contain",
|
||||
"Gratuliere"
|
||||
"Gratuliere",
|
||||
);
|
||||
// wait for payment callback
|
||||
cy.wait(3000);
|
||||
|
|
@ -86,7 +115,7 @@ describe("checkout.cy.js", () => {
|
|||
// back on dashboard page
|
||||
cy.get('[data-cy="db-course-title"]').should(
|
||||
"contain",
|
||||
"Versicherungsvermittler"
|
||||
"Versicherungsvermittler",
|
||||
);
|
||||
|
||||
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(
|
||||
"contain",
|
||||
"Konto erstellen"
|
||||
"Konto erstellen",
|
||||
);
|
||||
cy.get('[data-cy="continue-button"]').click();
|
||||
|
||||
cy.get('[data-cy="account-profile-title"]').should(
|
||||
"contain",
|
||||
"Profil ergänzen"
|
||||
"Profil ergänzen",
|
||||
);
|
||||
cy.get('[data-cy="dropdown-select"]').click();
|
||||
cy.get('[data-cy="dropdown-select-option-Baloise"]').click();
|
||||
|
|
@ -117,10 +146,10 @@ describe("checkout.cy.js", () => {
|
|||
// Adressdaten ausfüllen
|
||||
cy.get('[data-cy="account-checkout-title"]').should(
|
||||
"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-number").type("32");
|
||||
|
|
@ -132,32 +161,38 @@ describe("checkout.cy.js", () => {
|
|||
|
||||
cy.get('[data-cy="continue-pay"]').click();
|
||||
|
||||
cy.loadExternalApiRequestLog("request_username", "empty@example.com").then((entry) => {
|
||||
// ends with "/v1/transactions""
|
||||
expect(entry.api_url).to.contain("/v1/transactions");
|
||||
expect(entry.request_username).to.contain("empty@example.com");
|
||||
cy.loadExternalApiRequestLog("request_username", "empty@example.com").then(
|
||||
(entry) => {
|
||||
// ends with "/v1/transactions""
|
||||
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.currency).to.equal("CHF");
|
||||
expect(entry.api_request_data.autoSettle).to.equal(true);
|
||||
expect(entry.api_request_data.amount).to.equal(32400);
|
||||
expect(entry.api_request_data.currency).to.equal("CHF");
|
||||
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.lastName).to.equal("Leer");
|
||||
expect(entry.api_request_data.customer.street).to.equal("Eggersmatt 32");
|
||||
expect(entry.api_request_data.customer.zipCode).to.equal("1719");
|
||||
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.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.customer.firstName).to.equal("Flasche");
|
||||
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.zipCode).to.equal("1719");
|
||||
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.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.riskOwner).to.equal("IJ");
|
||||
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.repaymentType).to.equal(3);
|
||||
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.deviceFingerprintId).to.not.be.empty;
|
||||
|
||||
expect(true).to.be.true;
|
||||
});
|
||||
expect(true).to.be.true;
|
||||
},
|
||||
);
|
||||
|
||||
// check that results are stored on server
|
||||
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.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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -26,7 +26,7 @@ function getCurrentDate() {
|
|||
function verifyExportFileExists(fileName) {
|
||||
const downloadsFolder = Cypress.config("downloadsFolder");
|
||||
cy.readFile(
|
||||
path.join(downloadsFolder, `${fileName}_${getCurrentDate()}.xlsx`)
|
||||
path.join(downloadsFolder, `${fileName}_${getCurrentDate()}.xlsx`),
|
||||
).should("exist");
|
||||
}
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ function testExport(url, fileName) {
|
|||
describe("dashboardExport.cy.js", () => {
|
||||
beforeEach(() => {
|
||||
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", () => {
|
||||
testExport(
|
||||
"/statistic/test-lehrgang/assignment",
|
||||
"export_kompetenznachweis_elemente"
|
||||
"export_kompetenznachweis_elemente",
|
||||
);
|
||||
});
|
||||
|
||||
it("should download the feedback export", () => {
|
||||
testExport("/statistic/test-lehrgang/feedback", "export_feedback");
|
||||
});
|
||||
|
||||
it("should download the person export", () => {
|
||||
testExport("/dashboard/persons", "export_personen");
|
||||
});
|
||||
});
|
||||
|
||||
describe("as trainer", () => {
|
||||
|
|
@ -76,12 +80,16 @@ describe("dashboardExport.cy.js", () => {
|
|||
it("should download the competence elements export", () => {
|
||||
testExport(
|
||||
"/statistic/test-lehrgang/assignment",
|
||||
"export_kompetenznachweis_elemente"
|
||||
"export_kompetenznachweis_elemente",
|
||||
);
|
||||
});
|
||||
|
||||
it("should download the feedback export", () => {
|
||||
testExport("/statistic/test-lehrgang/feedback", "export_feedback");
|
||||
});
|
||||
|
||||
it("should download the person export", () => {
|
||||
testExport("/dashboard/persons", "export_personen");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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) => {
|
||||
for (let i = 0; i < answers.length; i++) {
|
||||
const answer = answers[i]
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -201,6 +201,7 @@ MIDDLEWARE = [
|
|||
"vbv_lernwelt.core.middleware.security.SecurityRequestResponseLoggingMiddleware",
|
||||
"wagtail.contrib.redirects.middleware.RedirectMiddleware",
|
||||
"vbv_lernwelt.core.middleware.auth.UserLoggedInCookieMiddleWare",
|
||||
# "vbv_lernwelt.debugtools.middleware.QueryCountDebugMiddleware",
|
||||
]
|
||||
|
||||
# STATIC
|
||||
|
|
@ -332,7 +333,6 @@ X_FRAME_OPTIONS = "DENY"
|
|||
# EMAIL
|
||||
# ------------------------------------------------------------------------------
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
|
||||
# FIXME how to send emails?
|
||||
EMAIL_BACKEND = env(
|
||||
"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"
|
||||
else:
|
||||
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", default="https://pay.sandbox.datatrans.com"
|
||||
"DATATRANS_PAY_URL",
|
||||
default="http://localhost:8000/server/fakeapi/datatrans/pay",
|
||||
)
|
||||
|
||||
# default settings for python sftpserver test-server
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ from vbv_lernwelt.dashboard.views import (
|
|||
export_attendance_as_xsl,
|
||||
export_competence_elements_as_xsl,
|
||||
export_feedback_as_xsl,
|
||||
export_persons_as_xsl,
|
||||
get_dashboard_config,
|
||||
get_dashboard_due_dates,
|
||||
get_dashboard_persons,
|
||||
|
|
@ -143,6 +144,7 @@ urlpatterns = [
|
|||
path(r"api/dashboard/export/competence_elements/", export_competence_elements_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/persons/", export_persons_as_xsl, name="export_persons_as_xsl"),
|
||||
|
||||
# course
|
||||
path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"),
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\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"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
|
@ -38,32 +38,32 @@ msgstr ""
|
|||
msgid "Nicht bestanden"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/assignment/export.py:203 vbv_lernwelt/assignment/export.py:206
|
||||
#: vbv_lernwelt/assignment/export.py:207
|
||||
#: vbv_lernwelt/assignment/export.py:204 vbv_lernwelt/assignment/export.py:208
|
||||
#: vbv_lernwelt/assignment/export.py:209
|
||||
msgid "Keine Daten"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/core/admin.py:32
|
||||
#: vbv_lernwelt/core/admin.py:38 vbv_lernwelt/sso/admin.py:83
|
||||
msgid "Personal info"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/core/admin.py:34
|
||||
#: vbv_lernwelt/core/admin.py:40
|
||||
msgid "Permissions"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/core/admin.py:45
|
||||
#: vbv_lernwelt/core/admin.py:51
|
||||
msgid "Important dates"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/core/admin.py:47
|
||||
#: vbv_lernwelt/core/admin.py:53
|
||||
msgid "Profile"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/core/admin.py:62
|
||||
#: vbv_lernwelt/core/admin.py:70
|
||||
msgid "Organisation"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/core/admin.py:75
|
||||
#: vbv_lernwelt/core/admin.py:83 vbv_lernwelt/sso/admin.py:86
|
||||
msgid "Additional data"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -87,31 +87,31 @@ msgstr ""
|
|||
msgid "Lehrgang-Seite"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/course/models.py:272
|
||||
#: vbv_lernwelt/dashboard/person_export.py:116
|
||||
msgid "Teilnehmer"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/course/models.py:273
|
||||
#: vbv_lernwelt/course/models.py:279
|
||||
msgid "Experte/Trainer"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/course/models.py:332
|
||||
#: vbv_lernwelt/course/models.py:339
|
||||
msgid "Dokumente im Circle ein/aus"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/course/models.py:336
|
||||
#: vbv_lernwelt/course/models.py:343
|
||||
msgid "Lernmentor-Funktion ein/aus"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/course/models.py:340
|
||||
#: vbv_lernwelt/course/models.py:347
|
||||
msgid "Kompetenzweise ein/aus"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/course/models.py:343
|
||||
#: vbv_lernwelt/course/models.py:350
|
||||
msgid "Versicherungsvermittler-Lehrgang"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/course/models.py:344
|
||||
#: vbv_lernwelt/course/models.py:350
|
||||
msgid "ÜK-Lehrgang"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -119,34 +119,66 @@ msgstr ""
|
|||
msgid "export_anwesenheit"
|
||||
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"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:116
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:128
|
||||
msgid "Nicht anwesend"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:123
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:135
|
||||
msgid "Vorname"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:124
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:136
|
||||
msgid "Nachname"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:125
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:137
|
||||
msgid "Email"
|
||||
msgstr "Email"
|
||||
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:126
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:127
|
||||
msgid "Lehrvertragsnummer"
|
||||
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
|
||||
msgid "export_feedback"
|
||||
msgstr ""
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\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"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
|
@ -88,30 +88,31 @@ msgid "Lehrgang-Seite"
|
|||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/course/models.py:278
|
||||
#: vbv_lernwelt/dashboard/person_export.py:116
|
||||
msgid "Teilnehmer"
|
||||
msgstr ""
|
||||
msgstr "Participant"
|
||||
|
||||
#: vbv_lernwelt/course/models.py:279
|
||||
msgid "Experte/Trainer"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/course/models.py:338
|
||||
#: vbv_lernwelt/course/models.py:339
|
||||
msgid "Dokumente im Circle ein/aus"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/course/models.py:342
|
||||
#: vbv_lernwelt/course/models.py:343
|
||||
msgid "Lernmentor-Funktion ein/aus"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/course/models.py:346
|
||||
#: vbv_lernwelt/course/models.py:347
|
||||
msgid "Kompetenzweise ein/aus"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/course/models.py:349
|
||||
#: vbv_lernwelt/course/models.py:350
|
||||
msgid "Versicherungsvermittler-Lehrgang"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/course/models.py:350
|
||||
#: vbv_lernwelt/course/models.py:351
|
||||
msgid "ÜK-Lehrgang"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -119,34 +120,66 @@ msgstr ""
|
|||
msgid "export_anwesenheit"
|
||||
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"
|
||||
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"
|
||||
msgstr "Présent"
|
||||
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:117
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:128
|
||||
msgid "Nicht anwesend"
|
||||
msgstr "Pas présent"
|
||||
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:124
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:135
|
||||
msgid "Vorname"
|
||||
msgstr "Prénom"
|
||||
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:125
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:136
|
||||
msgid "Nachname"
|
||||
msgstr "Nom de famille"
|
||||
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:126
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:137
|
||||
msgid "Email"
|
||||
msgstr "E-mail"
|
||||
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:127
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:138
|
||||
msgid "Lehrvertragsnummer"
|
||||
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
|
||||
msgid "export_feedback"
|
||||
msgstr "export_feedback"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\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"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
|
@ -88,30 +88,31 @@ msgid "Lehrgang-Seite"
|
|||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/course/models.py:278
|
||||
#: vbv_lernwelt/dashboard/person_export.py:116
|
||||
msgid "Teilnehmer"
|
||||
msgstr ""
|
||||
msgstr "Partecipante"
|
||||
|
||||
#: vbv_lernwelt/course/models.py:279
|
||||
msgid "Experte/Trainer"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/course/models.py:338
|
||||
#: vbv_lernwelt/course/models.py:339
|
||||
msgid "Dokumente im Circle ein/aus"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/course/models.py:342
|
||||
#: vbv_lernwelt/course/models.py:343
|
||||
msgid "Lernmentor-Funktion ein/aus"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/course/models.py:346
|
||||
#: vbv_lernwelt/course/models.py:347
|
||||
msgid "Kompetenzweise ein/aus"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/course/models.py:349
|
||||
#: vbv_lernwelt/course/models.py:350
|
||||
msgid "Versicherungsvermittler-Lehrgang"
|
||||
msgstr ""
|
||||
|
||||
#: vbv_lernwelt/course/models.py:350
|
||||
#: vbv_lernwelt/course/models.py:351
|
||||
msgid "ÜK-Lehrgang"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -119,34 +120,68 @@ msgstr ""
|
|||
msgid "export_anwesenheit"
|
||||
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"
|
||||
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"
|
||||
msgstr "Presente"
|
||||
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:117
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:128
|
||||
msgid "Nicht anwesend"
|
||||
msgstr "Non presente"
|
||||
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:124
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:135
|
||||
msgid "Vorname"
|
||||
msgstr "Nome"
|
||||
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:125
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:136
|
||||
msgid "Nachname"
|
||||
msgstr "Cognome"
|
||||
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:126
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:137
|
||||
msgid "Email"
|
||||
msgstr "Email"
|
||||
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:127
|
||||
#: vbv_lernwelt/course_session/services/export_attendance.py:138
|
||||
msgid "Lehrvertragsnummer"
|
||||
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
|
||||
msgid "export_feedback"
|
||||
msgstr "esportazione_feedback"
|
||||
|
|
|
|||
|
|
@ -71,5 +71,6 @@ class ProfileViewTest(APITestCase):
|
|||
"organisation_postal_code": "",
|
||||
"organisation_city": "",
|
||||
"organisation_country": None,
|
||||
"optional_attendance": [],
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ def query_competence_course_session_assignments(course_session_ids, circle_ids=N
|
|||
AssignmentType.CASEWORK.value,
|
||||
],
|
||||
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:
|
||||
continue
|
||||
|
|
@ -34,6 +39,11 @@ def query_competence_course_session_edoniq_tests(course_session_ids, circle_ids=
|
|||
for cset in CourseSessionEdoniqTest.objects.filter(
|
||||
course_session_id__in=course_session_ids,
|
||||
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:
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ TEST_MENTOR1_USER_ID = "d1f5f5a9-5b0a-4e1a-9e1a-9e9b5b5e1b1b"
|
|||
TEST_STUDENT1_VV_USER_ID = "5ff59857-8de5-415e-a387-4449f9a0337a"
|
||||
TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID = "7e8ebf0b-e6e2-4022-88f4-6e663ba0a9db"
|
||||
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_ZURICH_ID = -2
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from django.contrib.auth.models import Group, Permission
|
|||
from django.core.files import File
|
||||
from environs import Env
|
||||
|
||||
from vbv_lernwelt.core.model_utils import add_countries
|
||||
from vbv_lernwelt.media_files.models import UserImage
|
||||
|
||||
env = Env()
|
||||
|
|
@ -20,6 +21,7 @@ from vbv_lernwelt.core.constants import (
|
|||
TEST_SUPERVISOR1_USER_ID,
|
||||
TEST_TRAINER1_USER_ID,
|
||||
TEST_TRAINER2_USER_ID,
|
||||
TEST_USER_DATATRANS_HANNA_ID,
|
||||
TEST_USER_EMPTY_ID,
|
||||
)
|
||||
from vbv_lernwelt.core.models import User
|
||||
|
|
@ -78,7 +80,30 @@ default_users = [
|
|||
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):
|
||||
add_countries(small_set=True)
|
||||
|
||||
admin_group, created = Group.objects.get_or_create(name="admin_group")
|
||||
_content_creator_group, _created = Group.objects.get_or_create(
|
||||
name="content_creator_grop"
|
||||
|
|
@ -202,6 +227,8 @@ def create_default_users(default_password="test", set_avatar=False):
|
|||
language="de",
|
||||
)
|
||||
|
||||
hanna = create_datatrans_hanna_user()
|
||||
|
||||
for user_data in default_users:
|
||||
_create_student_user(**user_data)
|
||||
|
||||
|
|
|
|||
|
|
@ -17,8 +17,10 @@ from vbv_lernwelt.core.constants import (
|
|||
TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID,
|
||||
TEST_STUDENT3_USER_ID,
|
||||
TEST_TRAINER1_USER_ID,
|
||||
TEST_USER_DATATRANS_HANNA_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.course.consts import (
|
||||
COURSE_TEST_ID,
|
||||
|
|
@ -148,6 +150,11 @@ from vbv_lernwelt.shop.models import CheckoutInformation
|
|||
default=False,
|
||||
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(
|
||||
create_assignment_completion,
|
||||
assignment_completion_user_id,
|
||||
|
|
@ -167,6 +174,7 @@ def command(
|
|||
create_learning_mentor,
|
||||
set_only_is_uk_flag,
|
||||
set_only_is_vv_flag,
|
||||
set_optional_attendance_flag,
|
||||
):
|
||||
print("cypress reset data")
|
||||
CourseCompletion.objects.all().delete()
|
||||
|
|
@ -195,6 +203,9 @@ def command(
|
|||
password=make_password("test"),
|
||||
)
|
||||
|
||||
User.objects.filter(id=TEST_USER_DATATRANS_HANNA_ID).delete()
|
||||
create_datatrans_hanna_user()
|
||||
|
||||
cursor = connection.cursor()
|
||||
cursor.execute("truncate core_securityrequestresponselog;")
|
||||
cursor.execute("truncate core_externalapirequestlog;")
|
||||
|
|
@ -515,4 +526,17 @@ def command(
|
|||
course.configuration.is_uk = 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()
|
||||
|
|
|
|||
|
|
@ -114,8 +114,9 @@ class User(AbstractUser):
|
|||
blank=True,
|
||||
)
|
||||
|
||||
# fields gathered from cembra pay form
|
||||
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="")
|
||||
|
||||
# is only set by abacus invoice export code
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ class UserSerializer(serializers.ModelSerializer):
|
|||
course_session_experts = serializers.SerializerMethodField()
|
||||
country = CountrySerializer()
|
||||
organisation_country = CountrySerializer()
|
||||
optional_attendance = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
|
|
@ -83,6 +84,7 @@ class UserSerializer(serializers.ModelSerializer):
|
|||
"organisation_postal_code",
|
||||
"organisation_city",
|
||||
"organisation_country",
|
||||
"optional_attendance",
|
||||
]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
|
|
@ -108,6 +110,12 @@ class UserSerializer(serializers.ModelSerializer):
|
|||
|
||||
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):
|
||||
country_data = validated_data.pop("country", None)
|
||||
organisation_country_data = validated_data.pop("organisation_country", None)
|
||||
|
|
@ -131,6 +139,12 @@ class UserSerializer(serializers.ModelSerializer):
|
|||
return instance
|
||||
|
||||
|
||||
class CypressUserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class OrganisationSerializer(serializers.ModelSerializer):
|
||||
id = serializers.IntegerField(source="organisation_id", read_only=True)
|
||||
name = serializers.SerializerMethodField()
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ class CourseSessionUserAdmin(admin.ModelAdmin):
|
|||
"course_session",
|
||||
"role",
|
||||
"circles",
|
||||
"optional_attendance",
|
||||
# "created_at",
|
||||
# "updated_at",
|
||||
]
|
||||
|
|
@ -76,6 +77,7 @@ class CourseSessionUserAdmin(admin.ModelAdmin):
|
|||
list_filter = [
|
||||
"role",
|
||||
"course_session",
|
||||
"optional_attendance",
|
||||
]
|
||||
raw_id_fields = [
|
||||
"user",
|
||||
|
|
@ -97,7 +99,7 @@ class CourseSessionUserAdmin(admin.ModelAdmin):
|
|||
return ", ".join([c.title for c in obj.expert.all()])
|
||||
|
||||
fieldsets = [
|
||||
(None, {"fields": ("user", "course_session", "role")}),
|
||||
(None, {"fields": ("user", "course_session", "role", "optional_attendance")}),
|
||||
(
|
||||
"Expert/Trainer",
|
||||
{
|
||||
|
|
|
|||
|
|
@ -28,3 +28,10 @@ UK_COURSE_IDS = [
|
|||
COURSE_UK_TRAINING_FR,
|
||||
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
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ class CourseSessionUserObjectsType(ObjectType):
|
|||
circles = graphene.List(
|
||||
graphene.NonNull(CourseSessionUserExpertCircleType), required=True
|
||||
)
|
||||
optional_attendance = graphene.Boolean(required=False)
|
||||
|
||||
|
||||
class CircleDocumentObjectType(DjangoObjectType):
|
||||
|
|
@ -233,6 +234,7 @@ class CourseSessionObjectType(DjangoObjectType):
|
|||
)
|
||||
for circle in course_session_user.expert.all() # noqa
|
||||
],
|
||||
optional_attendance=course_session_user.optional_attendance, # noqa
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -283,6 +283,7 @@ class CourseSessionUser(models.Model):
|
|||
expert = models.ManyToManyField(
|
||||
"learnpath.Circle", related_name="expert", blank=True
|
||||
)
|
||||
optional_attendance = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
|
|
|
|||
|
|
@ -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])}"
|
||||
)
|
||||
|
|
@ -82,6 +82,10 @@ def _create_sheet(
|
|||
# headers
|
||||
# common user headers..., <attendance_course> <date>, status <attendance_course>, ..
|
||||
col_idx = add_user_headers(sheet)
|
||||
|
||||
sheet.cell(row=1, column=col_idx, value=str(_("Optionale Anwesenheit")))
|
||||
col_idx += 1
|
||||
|
||||
attendance_data = {}
|
||||
|
||||
for course in attendance_courses:
|
||||
|
|
@ -110,6 +114,13 @@ def _create_sheet(
|
|||
def _add_rows(sheet, users: list[CourseSessionUser], attendance_data):
|
||||
for row_idx, user in enumerate(users, start=2):
|
||||
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():
|
||||
user_dict = user_dict_map.get(str(user.user.id), {})
|
||||
status = user_dict.get("status", "") if user_dict else ""
|
||||
|
|
|
|||
|
|
@ -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.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.models import CourseSession, CourseSessionUser
|
||||
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.additional_json_data = {"Lehrvertragsnummer": 1234567890}
|
||||
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.additional_json_data = {"Lehrvertragsnummer": 1987654321}
|
||||
self.test_student2.save()
|
||||
|
|
@ -64,6 +69,7 @@ class AttendanceExportTestCase(ExportBaseTestCase):
|
|||
self.test_student1.last_name,
|
||||
self.test_student1.email,
|
||||
self.test_student1.additional_json_data["Lehrvertragsnummer"],
|
||||
"Ja",
|
||||
"Anwesend",
|
||||
],
|
||||
[
|
||||
|
|
@ -71,6 +77,7 @@ class AttendanceExportTestCase(ExportBaseTestCase):
|
|||
self.test_student2.last_name,
|
||||
self.test_student2.email,
|
||||
self.test_student2.additional_json_data["Lehrvertragsnummer"],
|
||||
"Nein",
|
||||
"Nicht anwesend",
|
||||
],
|
||||
[
|
||||
|
|
@ -78,6 +85,7 @@ class AttendanceExportTestCase(ExportBaseTestCase):
|
|||
self.test_student3.last_name,
|
||||
self.test_student3.email,
|
||||
None,
|
||||
"Nein",
|
||||
"Nicht anwesend",
|
||||
],
|
||||
]
|
||||
|
|
@ -95,6 +103,7 @@ class AttendanceExportTestCase(ExportBaseTestCase):
|
|||
"Nachname",
|
||||
"Email",
|
||||
"Lehrvertragsnummer",
|
||||
"Optionale Anwesenheit",
|
||||
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(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):
|
||||
self.attendance_course_zh.attendance_user_list = [
|
||||
|
|
@ -123,6 +132,7 @@ class AttendanceExportTestCase(ExportBaseTestCase):
|
|||
self.test_student2.last_name,
|
||||
self.test_student2.email,
|
||||
self.test_student2.additional_json_data["Lehrvertragsnummer"],
|
||||
"Nein",
|
||||
"Anwesend",
|
||||
],
|
||||
]
|
||||
|
|
@ -136,10 +146,10 @@ class AttendanceExportTestCase(ExportBaseTestCase):
|
|||
self.assertEqual(wb.sheetnames[0], "Test Bern 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"]
|
||||
self._check_export(wb, expected_data_zh, 2, 5)
|
||||
self._check_export(wb, expected_data_zh, 2, 6)
|
||||
|
||||
def test_french_export(self):
|
||||
activate("fr")
|
||||
|
|
@ -150,6 +160,7 @@ class AttendanceExportTestCase(ExportBaseTestCase):
|
|||
"Nom de famille",
|
||||
"E-mail",
|
||||
"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')}",
|
||||
]
|
||||
|
||||
|
|
@ -160,6 +171,7 @@ class AttendanceExportTestCase(ExportBaseTestCase):
|
|||
self.test_student1.last_name,
|
||||
self.test_student1.email,
|
||||
self.test_student1.additional_json_data["Lehrvertragsnummer"],
|
||||
"Oui",
|
||||
"Présent",
|
||||
],
|
||||
[
|
||||
|
|
@ -167,6 +179,7 @@ class AttendanceExportTestCase(ExportBaseTestCase):
|
|||
self.test_student2.last_name,
|
||||
self.test_student2.email,
|
||||
self.test_student2.additional_json_data["Lehrvertragsnummer"],
|
||||
"Non",
|
||||
"Pas présent",
|
||||
],
|
||||
[
|
||||
|
|
@ -174,10 +187,11 @@ class AttendanceExportTestCase(ExportBaseTestCase):
|
|||
self.test_student3.last_name,
|
||||
self.test_student3.email,
|
||||
None,
|
||||
"Non",
|
||||
"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):
|
||||
activate("it")
|
||||
|
|
@ -188,6 +202,7 @@ class AttendanceExportTestCase(ExportBaseTestCase):
|
|||
"Cognome",
|
||||
"Email",
|
||||
"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')}",
|
||||
]
|
||||
|
||||
|
|
@ -198,6 +213,7 @@ class AttendanceExportTestCase(ExportBaseTestCase):
|
|||
self.test_student1.last_name,
|
||||
self.test_student1.email,
|
||||
self.test_student1.additional_json_data["Lehrvertragsnummer"],
|
||||
"Sì",
|
||||
"Presente",
|
||||
],
|
||||
[
|
||||
|
|
@ -205,6 +221,7 @@ class AttendanceExportTestCase(ExportBaseTestCase):
|
|||
self.test_student2.last_name,
|
||||
self.test_student2.email,
|
||||
self.test_student2.additional_json_data["Lehrvertragsnummer"],
|
||||
"No",
|
||||
"Non presente",
|
||||
],
|
||||
[
|
||||
|
|
@ -212,7 +229,8 @@ class AttendanceExportTestCase(ExportBaseTestCase):
|
|||
self.test_student3.last_name,
|
||||
self.test_student3.email,
|
||||
None,
|
||||
"No",
|
||||
"Non presente",
|
||||
],
|
||||
]
|
||||
self._check_export(wb, expected_data_be, 4, 5)
|
||||
self._check_export(wb, expected_data_be, 4, 6)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import math
|
||||
from typing import List
|
||||
from typing import List, Tuple
|
||||
|
||||
import graphene
|
||||
|
||||
|
|
@ -7,7 +7,6 @@ import vbv_lernwelt.assignment.models
|
|||
from vbv_lernwelt.assignment.models import (
|
||||
AssignmentCompletion,
|
||||
AssignmentCompletionStatus,
|
||||
AssignmentType,
|
||||
)
|
||||
from vbv_lernwelt.competence.services import (
|
||||
query_competence_course_session_assignments,
|
||||
|
|
@ -98,14 +97,23 @@ def get_assignment_completion_metrics(
|
|||
assignment: vbv_lernwelt.assignment.models.Assignment,
|
||||
user_selection_ids: List[str] | None,
|
||||
urql_id_postfix: str = "",
|
||||
context=None,
|
||||
) -> AssignmentCompletionMetricsType:
|
||||
if not context:
|
||||
context = {}
|
||||
|
||||
if user_selection_ids:
|
||||
course_session_users = user_selection_ids
|
||||
else:
|
||||
course_session_users = CourseSessionUser.objects.filter(
|
||||
course_session=course_session,
|
||||
role=CourseSessionUser.Role.MEMBER,
|
||||
).values_list("user", flat=True)
|
||||
key = f"CourseSessionUser_{course_session.id}"
|
||||
if not key in context:
|
||||
course_session_users = CourseSessionUser.objects.filter(
|
||||
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(
|
||||
completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED.value,
|
||||
|
|
@ -139,33 +147,54 @@ def create_record(
|
|||
course_session_assignment: CourseSessionAssignment | CourseSessionEdoniqTest,
|
||||
user_selection_ids: List[str] | None,
|
||||
urql_id_postfix: str = "",
|
||||
) -> AssignmentStatisticsRecordType:
|
||||
if isinstance(course_session_assignment, CourseSessionAssignment):
|
||||
due_date = course_session_assignment.submission_deadline
|
||||
else:
|
||||
due_date = course_session_assignment.deadline
|
||||
context=None,
|
||||
) -> Tuple[AssignmentStatisticsRecordType, dict]:
|
||||
if not context:
|
||||
context = {}
|
||||
|
||||
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
|
||||
|
||||
return AssignmentStatisticsRecordType(
|
||||
# make sure it's unique, across all types of assignments!
|
||||
_id=f"{course_session_assignment._meta.model_name}#{course_session_assignment.id}@{urql_id_postfix}",
|
||||
# noqa
|
||||
course_session_id=str(course_session_assignment.course_session.id), # noqa
|
||||
circle_id=learning_content.get_circle().id, # noqa
|
||||
course_session_assignment_id=str(course_session_assignment.id), # noqa
|
||||
generation=course_session_assignment.course_session.generation, # noqa
|
||||
assignment_type_translation_key=due_date.assignment_type_translation_key,
|
||||
# noqa
|
||||
assignment_title=learning_content.content_assignment.title, # noqa
|
||||
metrics=get_assignment_completion_metrics( # noqa
|
||||
course_session=course_session_assignment.course_session, # noqa
|
||||
assignment=learning_content.content_assignment, # noqa
|
||||
user_selection_ids=user_selection_ids, # noqa
|
||||
urql_id_postfix=urql_id_postfix, # noqa
|
||||
return (
|
||||
AssignmentStatisticsRecordType(
|
||||
# make sure it's unique, across all types of assignments!
|
||||
_id=f"{course_session_assignment._meta.model_name}#{course_session_assignment.id}@{urql_id_postfix}",
|
||||
# noqa
|
||||
course_session_id=str(course_session_assignment.course_session.id), # noqa
|
||||
circle_id=circle_id, # noqa
|
||||
course_session_assignment_id=str(course_session_assignment.id), # noqa
|
||||
generation=course_session_assignment.course_session.generation, # noqa
|
||||
assignment_type_translation_key=due_date.assignment_type_translation_key,
|
||||
# noqa
|
||||
assignment_title=learning_content.content_assignment.title, # noqa
|
||||
metrics=get_assignment_completion_metrics( # noqa
|
||||
course_session=course_session_assignment.course_session, # noqa
|
||||
assignment=learning_content.content_assignment, # 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
|
||||
deadline=due_date.start, # noqa
|
||||
context,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -178,28 +207,26 @@ def assignments(
|
|||
) -> AssignmentsStatisticsType:
|
||||
if urql_id is None:
|
||||
urql_id = str(course_id)
|
||||
|
||||
course_sessions = CourseSession.objects.filter(
|
||||
id__in=course_session_selection_ids,
|
||||
)
|
||||
records: List[AssignmentStatisticsRecordType] = []
|
||||
context = {}
|
||||
|
||||
for course_session in course_sessions:
|
||||
for csa in query_competence_course_session_assignments(
|
||||
[course_session.id], circle_ids
|
||||
):
|
||||
record = create_record(csa, user_selection_ids, urql_id_postfix=urql_id)
|
||||
records.append(record)
|
||||
csas = query_competence_course_session_assignments(course_sessions, circle_ids)
|
||||
csets = query_competence_course_session_edoniq_tests(course_sessions, circle_ids)
|
||||
|
||||
for cset in query_competence_course_session_edoniq_tests(
|
||||
[course_session.id], circle_ids
|
||||
):
|
||||
record = create_record(
|
||||
course_session_assignment=cset,
|
||||
user_selection_ids=user_selection_ids,
|
||||
urql_id_postfix=urql_id,
|
||||
)
|
||||
records.append(record)
|
||||
for csa in csas:
|
||||
record, context = create_record(
|
||||
csa, user_selection_ids, urql_id_postfix=urql_id, context=context
|
||||
)
|
||||
records.append(record)
|
||||
|
||||
for cset in csets:
|
||||
record, context = create_record(
|
||||
cset, user_selection_ids, urql_id_postfix=urql_id, context=context
|
||||
)
|
||||
records.append(record)
|
||||
|
||||
return AssignmentsStatisticsType(
|
||||
_id=urql_id, # noqa
|
||||
|
|
|
|||
|
|
@ -41,25 +41,23 @@ def competences(
|
|||
completions = CourseCompletion.objects.filter(
|
||||
course_session_id__in=course_session_selection_ids,
|
||||
page_type="competence.PerformanceCriteria",
|
||||
).prefetch_related("course_session", "page")
|
||||
).select_related("course_session", "page")
|
||||
|
||||
if user_selection_ids is not None:
|
||||
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 = {
|
||||
lu.id: c
|
||||
for lu in learning_units.values()
|
||||
if (lu is not None and (c := lu.get_circle()) is not None)
|
||||
and (circle_ids is None or c.id in circle_ids)
|
||||
if lu and (c := lu.get_circle()) and (circle_ids is None or c.id in circle_ids)
|
||||
}
|
||||
|
||||
competence_records = {}
|
||||
|
||||
for completion in completions:
|
||||
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}"
|
||||
|
||||
competence_records.setdefault(combined_id, {}).setdefault(
|
||||
learning_unit,
|
||||
CompetenceRecordStatisticsType(
|
||||
_id=combined_id, # noqa
|
||||
title=learning_unit.title, # noqa
|
||||
course_session_id=completion.course_session.id, # noqa
|
||||
generation=completion.course_session.generation, # noqa
|
||||
circle_id=circle.id, # noqa
|
||||
success_count=0, # noqa
|
||||
fail_count=0, # noqa
|
||||
if combined_id not in competence_records:
|
||||
competence_records[combined_id] = {}
|
||||
|
||||
if learning_unit not in competence_records[combined_id]:
|
||||
competence_records[combined_id][
|
||||
learning_unit
|
||||
] = CompetenceRecordStatisticsType(
|
||||
_id=combined_id,
|
||||
title=learning_unit.title,
|
||||
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}",
|
||||
# noqa
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
if completion.completion_status == CourseCompletionStatus.SUCCESS.value:
|
||||
competence_records[combined_id][learning_unit].success_count += 1
|
||||
|
|
@ -99,7 +100,7 @@ def competences(
|
|||
for record in circle_records.values()
|
||||
]
|
||||
|
||||
success_count = sum([c.success_count for c in values])
|
||||
fail_count = sum([c.fail_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)
|
||||
|
||||
return values, success_count, fail_count
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
|
|
@ -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()
|
||||
|
|
@ -2,7 +2,7 @@ import base64
|
|||
from dataclasses import asdict, dataclass
|
||||
from datetime import date
|
||||
from enum import Enum
|
||||
from typing import List, Set, Tuple
|
||||
from typing import List, Tuple
|
||||
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponse
|
||||
|
|
@ -24,25 +24,27 @@ from vbv_lernwelt.competence.services import (
|
|||
query_competence_course_session_edoniq_tests,
|
||||
)
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.course.models import (
|
||||
CourseConfiguration,
|
||||
CourseSession,
|
||||
CourseSessionUser,
|
||||
)
|
||||
from vbv_lernwelt.course.models import CourseConfiguration, CourseSessionUser
|
||||
from vbv_lernwelt.course.views import logger
|
||||
from vbv_lernwelt.course_session.services.export_attendance import (
|
||||
ATTENDANCE_EXPORT_FILENAME,
|
||||
export_attendance,
|
||||
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.serializers import DueDateSerializer
|
||||
from vbv_lernwelt.feedback.export import (
|
||||
export_feedback_with_circle_restriction,
|
||||
FEEDBACK_EXPORT_FILE_NAME,
|
||||
)
|
||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||
from vbv_lernwelt.learnpath.models import Circle
|
||||
from vbv_lernwelt.self_evaluation_feedback.models import SelfEvaluationFeedback
|
||||
|
||||
|
|
@ -65,19 +67,6 @@ class RoleKeyType(Enum):
|
|||
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)
|
||||
class CourseConfig:
|
||||
course_id: str
|
||||
|
|
@ -92,157 +81,6 @@ class CourseConfig:
|
|||
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):
|
||||
course_session_ids = {cs["id"] for p in persons for cs in p["course_sessions"]}
|
||||
competence_assignments = query_competence_course_session_assignments(
|
||||
|
|
@ -285,7 +123,7 @@ def _persons_list_add_competence_metrics(persons):
|
|||
@api_view(["GET"])
|
||||
def get_dashboard_persons(request):
|
||||
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":
|
||||
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_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()
|
||||
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)
|
||||
|
||||
# find course session by id in `course_sessions`
|
||||
# Fetch future due dates in a single query using Q objects for complex filtering
|
||||
future_due_dates = DueDate.objects.filter(
|
||||
Q(course_session_id__in=course_session_ids),
|
||||
Q(end__gte=today) | Q(start__gte=today),
|
||||
).select_related("course_session")
|
||||
|
||||
result_due_dates = []
|
||||
for due_date in due_dates:
|
||||
data = DueDateSerializer(due_date).data
|
||||
course_session_map = {cs.id: cs for cs in course_sessions}
|
||||
|
||||
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:
|
||||
data["course_session"] = _create_course_session_dict(
|
||||
data["course_session"] = create_course_session_dict(
|
||||
cs, my_role=user_role(cs.roles), user_role=""
|
||||
)
|
||||
result_due_dates.append(data)
|
||||
|
||||
return Response(
|
||||
status=200,
|
||||
data=result_due_dates,
|
||||
)
|
||||
return Response(status=200, data=result_due_dates)
|
||||
|
||||
except PermissionDenied as e:
|
||||
raise e
|
||||
|
|
@ -595,6 +416,20 @@ def export_feedback_as_xsl(request):
|
|||
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(
|
||||
user: User, requested_coursesession_ids: List[str]
|
||||
) -> List[CourseSessionWithRoles]:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -765,7 +765,7 @@ def create_or_update_course_session_assignment(
|
|||
csa.submission_deadline.save()
|
||||
csa.evaluation_deadline.start = timezone.make_aware(
|
||||
start
|
||||
) + timezone.timedelta(days=45)
|
||||
) + timezone.timedelta(days=60)
|
||||
csa.evaluation_deadline.end = None
|
||||
csa.evaluation_deadline.save()
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -78,32 +78,32 @@ def create_customer_xml(checkout_information: CheckoutInformation):
|
|||
first_name=checkout_information.first_name,
|
||||
company_name=(
|
||||
checkout_information.organisation_detail_name
|
||||
if checkout_information.invoice_address == "org"
|
||||
if checkout_information.abacus_use_organisation_data()
|
||||
else ""
|
||||
),
|
||||
street=(
|
||||
checkout_information.organisation_street
|
||||
if checkout_information.invoice_address == "org"
|
||||
if checkout_information.abacus_use_organisation_data()
|
||||
else checkout_information.street
|
||||
),
|
||||
house_number=(
|
||||
checkout_information.organisation_street_number
|
||||
if checkout_information.invoice_address == "org"
|
||||
if checkout_information.abacus_use_organisation_data()
|
||||
else checkout_information.street_number
|
||||
),
|
||||
zip_code=(
|
||||
checkout_information.organisation_postal_code
|
||||
if checkout_information.invoice_address == "org"
|
||||
if checkout_information.abacus_use_organisation_data()
|
||||
else checkout_information.postal_code
|
||||
),
|
||||
city=(
|
||||
checkout_information.organisation_city
|
||||
if checkout_information.invoice_address == "org"
|
||||
if checkout_information.abacus_use_organisation_data()
|
||||
else checkout_information.city
|
||||
),
|
||||
country=(
|
||||
checkout_information.organisation_country_id
|
||||
if checkout_information.invoice_address == "org"
|
||||
if checkout_information.abacus_use_organisation_data()
|
||||
else checkout_information.country_id
|
||||
),
|
||||
language=customer.language,
|
||||
|
|
|
|||
|
|
@ -128,3 +128,15 @@ class CheckoutInformation(models.Model):
|
|||
self.abacus_order_id = new_abacus_order_id
|
||||
self.save()
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
import hashlib
|
||||
import hmac
|
||||
import uuid
|
||||
|
||||
import requests
|
||||
import structlog
|
||||
from django.conf import settings
|
||||
|
||||
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.models import CheckoutState
|
||||
|
||||
|
|
@ -73,6 +71,7 @@ def is_signature_valid(
|
|||
|
||||
def init_datatrans_transaction(
|
||||
user: User,
|
||||
refno: str,
|
||||
amount_chf_centimes: int,
|
||||
redirect_url_success: str,
|
||||
redirect_url_error: str,
|
||||
|
|
@ -91,8 +90,8 @@ def init_datatrans_transaction(
|
|||
"amount": amount_chf_centimes,
|
||||
"currency": "CHF",
|
||||
"language": user.language,
|
||||
"refno": str(uuid.uuid4()),
|
||||
"refno2": refno2,
|
||||
"refno": str(refno),
|
||||
"refno2": str(refno2),
|
||||
"webhook": {"url": webhook_url},
|
||||
"redirect": {
|
||||
"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:
|
||||
# payload["paymentMethods"] = ["INT"]
|
||||
if with_cembra_byjuno_invoice:
|
||||
payload["paymentMethods"] = ["INT"]
|
||||
if datatrans_customer_data:
|
||||
payload["customer"] = datatrans_customer_data
|
||||
if datatrans_int_data:
|
||||
|
|
|
|||
|
|
@ -163,6 +163,53 @@ class AbacusInvoiceTestCase(TestCase):
|
|||
assert "<Street>Laupenstrasse</Street>" 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):
|
||||
customer_xml = render_customer_xml(
|
||||
abacus_debitor_number=60000012,
|
||||
|
|
|
|||
|
|
@ -71,9 +71,8 @@ class CheckoutAPITestCase(APITestCase):
|
|||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
f"https://pay.sandbox.datatrans.com/v1/start/1234567890",
|
||||
response.json()["next_step_url"],
|
||||
self.assertTrue(
|
||||
response.json()["next_step_url"].endswith("v1/start/1234567890")
|
||||
)
|
||||
|
||||
ci = CheckoutInformation.objects.first()
|
||||
|
|
@ -154,9 +153,11 @@ class CheckoutAPITestCase(APITestCase):
|
|||
)
|
||||
|
||||
self.assertEqual(
|
||||
0,
|
||||
1,
|
||||
CheckoutInformation.objects.count(),
|
||||
)
|
||||
ci = CheckoutInformation.objects.first()
|
||||
self.assertEqual(ci.state, CheckoutState.FAILED)
|
||||
|
||||
def test_checkout_already_paid(self):
|
||||
# GIVEN
|
||||
|
|
@ -217,9 +218,8 @@ class CheckoutAPITestCase(APITestCase):
|
|||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
f"https://pay.sandbox.datatrans.com/v1/start/{transaction_id_next}",
|
||||
response.json()["next_step_url"],
|
||||
self.assertTrue(
|
||||
response.json()["next_step_url"].endswith(f"v1/start/{transaction_id_next}")
|
||||
)
|
||||
|
||||
# check that we have two checkouts
|
||||
|
|
@ -277,9 +277,8 @@ class CheckoutAPITestCase(APITestCase):
|
|||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
f"https://pay.sandbox.datatrans.com/v1/start/{transaction_id}",
|
||||
response.json()["next_step_url"],
|
||||
self.assertTrue(
|
||||
response.json()["next_step_url"].endswith(f"v1/start/{transaction_id}")
|
||||
)
|
||||
|
||||
@patch("vbv_lernwelt.shop.views.init_datatrans_transaction")
|
||||
|
|
@ -310,7 +309,6 @@ class CheckoutAPITestCase(APITestCase):
|
|||
|
||||
# THEN
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
f"https://pay.sandbox.datatrans.com/v1/start/{transaction_id}",
|
||||
response.json()["next_step_url"],
|
||||
self.assertTrue(
|
||||
response.json()["next_step_url"].endswith(f"v1/start/{transaction_id}")
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,10 +24,8 @@ class DatatransServiceTest(TestCase):
|
|||
|
||||
@override_settings(DATATRANS_BASIC_AUTH_KEY="BASIC_AUTH_KEY")
|
||||
@patch("vbv_lernwelt.shop.services.requests.post")
|
||||
@patch("vbv_lernwelt.shop.services.uuid.uuid4")
|
||||
def test_init_transaction_201(self, mock_uuid, mock_post):
|
||||
def test_init_transaction_201(self, mock_post):
|
||||
# GIVEN
|
||||
mock_uuid.return_value = uuid.uuid4()
|
||||
mock_post.return_value.status_code = 201
|
||||
mock_post.return_value.json.return_value = {
|
||||
"transactionId": 1234567890,
|
||||
|
|
@ -38,6 +36,7 @@ class DatatransServiceTest(TestCase):
|
|||
# WHEN
|
||||
transaction_id = init_datatrans_transaction(
|
||||
user=self.user,
|
||||
refno="123321",
|
||||
amount_chf_centimes=324_30,
|
||||
redirect_url_success=f"{REDIRECT_URL}/success",
|
||||
redirect_url_error=f"{REDIRECT_URL}/error",
|
||||
|
|
@ -64,11 +63,12 @@ class DatatransServiceTest(TestCase):
|
|||
with self.assertRaises(InitTransactionException):
|
||||
init_datatrans_transaction(
|
||||
user=self.user,
|
||||
refno="123321",
|
||||
amount_chf_centimes=324_30,
|
||||
redirect_url_success=f"/success",
|
||||
redirect_url_error=f"/error",
|
||||
redirect_url_cancel=f"/cancel",
|
||||
webhook_url=f"/webhook",
|
||||
redirect_url_success="/success",
|
||||
redirect_url_error="/error",
|
||||
redirect_url_cancel="/cancel",
|
||||
webhook_url="/webhook",
|
||||
refno2="",
|
||||
)
|
||||
|
||||
|
|
@ -80,7 +80,4 @@ class DatatransServiceTest(TestCase):
|
|||
url = get_payment_url(transaction_id)
|
||||
|
||||
# THEN
|
||||
self.assertEqual(
|
||||
url,
|
||||
f"https://pay.sandbox.datatrans.com/v1/start/{transaction_id}",
|
||||
)
|
||||
self.assertTrue(url.endswith(f"v1/start/{transaction_id}"))
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ def checkout_vv(request):
|
|||
sku = request.data["product"]
|
||||
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:
|
||||
product = Product.objects.get(sku=sku)
|
||||
|
|
@ -124,6 +124,38 @@ def checkout_vv(request):
|
|||
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}"
|
||||
|
||||
try:
|
||||
|
|
@ -138,10 +170,10 @@ def checkout_vv(request):
|
|||
"street": f'{request.data["address"]["street"]} {request.data["address"]["street_number"]}',
|
||||
"city": request.data["address"]["city"],
|
||||
"zipCode": request.data["address"]["postal_code"],
|
||||
"country": request.data["address"]["country_code"],
|
||||
"country": request.data["address"]["country_id"],
|
||||
"phone": request.data["address"]["phone_number"],
|
||||
"email": email,
|
||||
"birthDate": request.data["address"]["birth_date"],
|
||||
"birthDate": str(request.data["address"]["birth_date"]),
|
||||
"language": request.user.language,
|
||||
"ipAddress": ip_address,
|
||||
"type": "P",
|
||||
|
|
@ -154,6 +186,7 @@ def checkout_vv(request):
|
|||
}
|
||||
transaction_id = init_datatrans_transaction(
|
||||
user=request.user,
|
||||
refno=str(checkout_info.abacus_order_id),
|
||||
amount_chf_centimes=product.price,
|
||||
redirect_url_success=checkout_success_url(
|
||||
base_url=base_redirect_url, product_sku=sku
|
||||
|
|
@ -170,6 +203,8 @@ def checkout_vv(request):
|
|||
with_cembra_byjuno_invoice=with_cembra_byjuno_invoice,
|
||||
)
|
||||
except InitTransactionException as e:
|
||||
checkout_info.state = CheckoutState.FAILED.value
|
||||
checkout_info.save()
|
||||
if not settings.DEBUG:
|
||||
log.error("Transaction initiation failed", exc_info=True, error=str(e))
|
||||
capture_exception(e)
|
||||
|
|
@ -180,36 +215,8 @@ def checkout_vv(request):
|
|||
),
|
||||
)
|
||||
|
||||
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,
|
||||
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"],
|
||||
)
|
||||
checkout_info.transaction_id = transaction_id
|
||||
checkout_info.save()
|
||||
|
||||
return next_step_response(url=get_payment_url(transaction_id))
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue