@@ -64,15 +89,21 @@ const changeViewType = (viewType: ViewType) => {
>
-
-
-
@@ -101,6 +132,7 @@ const changeViewType = (viewType: ViewType) => {
@@ -114,6 +146,7 @@ const changeViewType = (viewType: ViewType) => {
+import { computed } from "vue";
+import type { LearningContentWithCompletion, TopicType } from "@/types";
+import LearningPathCircleColumn from "./LearningPathCircleColumn.vue";
+import { filterCircles } from "./utils";
+
+interface Props {
+ topic: TopicType;
+ topicIndex: number;
+ nextLearningContent?: LearningContentWithCompletion;
+ overrideCircleUrlBase?: string;
+ filter?: string;
+ isLastTopic: boolean;
+}
+
+const props = defineProps
();
+
+const isFirstCircle = (circleIndex: number) =>
+ props.topicIndex === 0 && circleIndex === 0;
+
+const isLastCircle = (circleIndex: number, numCircles: number) =>
+ props.isLastTopic && circleIndex === numCircles - 1;
+
+const filteredCircles = computed(() => {
+ return filterCircles(props.filter, props.topic.circles);
+});
+
+
+
+
+
+ {{ topic.title }}
+
+
+
+
+
+
diff --git a/client/src/pages/learningPath/learningPathPage/LearningPathPathView.vue b/client/src/pages/learningPath/learningPathPage/LearningPathPathView.vue
index 7150b719..0b6353ab 100644
--- a/client/src/pages/learningPath/learningPathPage/LearningPathPathView.vue
+++ b/client/src/pages/learningPath/learningPathPage/LearningPathPathView.vue
@@ -1,10 +1,10 @@
@@ -50,37 +60,16 @@ const scrollLearnPathDiagram = (offset: number) => {
ref="learnPathDiagram"
class="no-scrollbar flex h-96 snap-x flex-row overflow-auto py-5 sm:py-10"
>
-
-
- {{ topic.title }}
-
-
-
-
-
+ :topic-index="topicIndex"
+ :topic="topic"
+ :next-learning-content="nextLearningContent"
+ :override-circle-url-base="overrideCircleUrlBase"
+ :filter="filter"
+ :is-last-topic="topicIndex === topics.length - 1"
+ />
+import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
+import { COURSE_PROFILE_ALL_FILTER } from "@/constants";
+import type { DropdownSelectable } from "@/types";
+import { useTranslation } from "i18next-vue";
+import { computed } from "vue";
+
+interface Props {
+ profiles?: string[];
+ selected?: string;
+}
+
+const props = defineProps();
+const emit = defineEmits(["select"]);
+
+const { t } = useTranslation();
+const items = computed(() => {
+ return props.profiles?.map((p) => ({ id: p, name: t(`profile.${p}`) })) || [];
+});
+
+const selectedItem = computed(() => {
+ if (props.selected) {
+ return { id: props.selected || "", name: t(`profile.${props.selected}`) };
+ }
+ return {
+ id: COURSE_PROFILE_ALL_FILTER,
+ name: t(`profile.${COURSE_PROFILE_ALL_FILTER}`),
+ };
+});
+
+const updateFilter = (e: DropdownSelectable) => {
+ emit("select", e.id);
+};
+
+
+
+
+
{{ $t("a.Zulassungsprofil") }}:
+
+
+
+
+
diff --git a/client/src/pages/learningPath/learningPathPage/utils.ts b/client/src/pages/learningPath/learningPathPage/utils.ts
index 46410d94..d27f27f8 100644
--- a/client/src/pages/learningPath/learningPathPage/utils.ts
+++ b/client/src/pages/learningPath/learningPathPage/utils.ts
@@ -1,3 +1,6 @@
+import { useCurrentCourseSession } from "@/composables";
+import { COURSE_PROFILE_ALL_FILTER } from "@/constants";
+import { COURSE_QUERY } from "@/graphql/queries";
import type {
CircleSectorData,
CircleSectorProgress,
@@ -7,6 +10,8 @@ import {
someFinishedInLearningSequence,
} from "@/services/circle";
import type { CircleType } from "@/types";
+import { useQuery } from "@urql/vue";
+import { computed } from "vue";
export function calculateCircleSectorData(circle: CircleType): CircleSectorData[] {
return circle.learning_sequences.map((ls) => {
@@ -21,3 +26,42 @@ export function calculateCircleSectorData(circle: CircleType): CircleSectorData[
};
});
}
+
+export function useCourseFilter(courseSlug: string, courseSessionId?: string) {
+ const csId = computed(() => {
+ if (courseSessionId) {
+ return courseSessionId;
+ }
+ // assume we're on a page with a current course session
+ const courseSession = useCurrentCourseSession();
+ return courseSession.value.id;
+ });
+ const courseReactiveResult = useQuery({
+ query: COURSE_QUERY,
+ variables: { slug: courseSlug },
+ });
+ const courseReactive = computed(() => courseReactiveResult.data.value?.course);
+ const courseSessionUser = computed(() => {
+ return courseReactive.value?.course_session_users.find(
+ (e) => e?.course_session.id === csId.value
+ );
+ });
+ const filter = computed(() => {
+ return courseSessionUser.value?.chosen_profile || "";
+ });
+
+ return {
+ filter,
+ courseReactive,
+ courseSessionUser,
+ };
+}
+
+export function filterCircles(filter: string | undefined, circles: CircleType[]) {
+ if (filter === undefined || filter === "" || filter === COURSE_PROFILE_ALL_FILTER) {
+ return circles;
+ }
+ return circles.filter(
+ (circle) => circle.profiles.indexOf(filter as string) > -1 || circle.is_base_circle
+ );
+}
diff --git a/client/src/pages/onboarding/vv/AccountCourseProfile.vue b/client/src/pages/onboarding/vv/AccountCourseProfile.vue
new file mode 100644
index 00000000..c101a0e2
--- /dev/null
+++ b/client/src/pages/onboarding/vv/AccountCourseProfile.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+ {{ $t("a.Zulassungsprofil auswählen") }}
+
+
+
+ {{
+ $t(
+ "a.Wähle ein Zulassungsprofil, damit du deinen Lehrgang an der richtigen Stelle beginnen kannst. Du kannst ihn später jederzeit ändern."
+ )
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/pages/onboarding/vv/CheckoutAddress.vue b/client/src/pages/onboarding/vv/CheckoutAddress.vue
index 3bcfa0d3..918dc3a3 100644
--- a/client/src/pages/onboarding/vv/CheckoutAddress.vue
+++ b/client/src/pages/onboarding/vv/CheckoutAddress.vue
@@ -229,6 +229,7 @@ const executePayment = async () => {
redirect_url: fullHost,
address: addressData,
product: props.courseType,
+ chosen_profile: user.chosen_profile?.id || "",
with_cembra_byjuno_invoice: address.value.payment_method === "cembra_byjuno",
device_fingerprint_session_key: getLocalSessionKey(),
}).then((res: any) => {
diff --git a/client/src/pages/start/VVStartPage.vue b/client/src/pages/start/VVStartPage.vue
index 7cf3f310..1b55cde8 100644
--- a/client/src/pages/start/VVStartPage.vue
+++ b/client/src/pages/start/VVStartPage.vue
@@ -47,6 +47,14 @@ const { t } = useTranslation();
$t("Füge dein Profilbild hinzu und ergänze die fehlenden Angaben.")
}}
+
+ {{ $t("a.Zulassungsprofil auswählen") }}:
+ {{
+ $t(
+ "a.Wähle ein Zulassungsprofil, damit du deinen Lehrgang an der richtigen Stelle beginnen kannst."
+ )
+ }}
+
{{ $t("a.Lehrgang kaufen") }}:
{{
diff --git a/client/src/pages/userProfile/LearningPathProfilePage.vue b/client/src/pages/userProfile/LearningPathProfilePage.vue
index 34413b25..ca7921c9 100644
--- a/client/src/pages/userProfile/LearningPathProfilePage.vue
+++ b/client/src/pages/userProfile/LearningPathProfilePage.vue
@@ -1,11 +1,12 @@
+
+
+
+
{{ topic.title }} {{ filter }}
+
+
+
diff --git a/client/src/router/__tests__/onboarding.spec.ts b/client/src/router/__tests__/onboarding.spec.ts
index 199bc5e0..20278cb7 100644
--- a/client/src/router/__tests__/onboarding.spec.ts
+++ b/client/src/router/__tests__/onboarding.spec.ts
@@ -61,7 +61,7 @@ describe("Onboarding", () => {
mockNext
);
expect(mockNext).toHaveBeenCalledWith({
- name: "checkoutAddress",
+ name: "accountCourseProfile",
params: { courseType: testCase },
});
});
diff --git a/client/src/router/index.ts b/client/src/router/index.ts
index 1bbfcb90..618a7040 100644
--- a/client/src/router/index.ts
+++ b/client/src/router/index.ts
@@ -391,6 +391,12 @@ const router = createRouter({
component: () => import("@/pages/onboarding/uk/SetupComplete.vue"),
name: "setupComplete",
},
+ {
+ path: "account/course-profile",
+ component: () => import("@/pages/onboarding/vv/AccountCourseProfile.vue"),
+ name: "accountCourseProfile",
+ props: true,
+ },
{
path: "checkout/address",
component: () => import("@/pages/onboarding/vv/CheckoutAddress.vue"),
diff --git a/client/src/services/entities.ts b/client/src/services/entities.ts
index 2de7f01c..588627b1 100644
--- a/client/src/services/entities.ts
+++ b/client/src/services/entities.ts
@@ -13,14 +13,21 @@ export type Country = {
name: string;
};
+export type CourseProfile = {
+ id: number;
+ code: string;
+};
+
export function useEntities() {
const countries: Ref = ref([]);
const organisations: Ref = ref([]);
+ const courseProfiles: Ref = ref([]);
itGetCached("/api/core/entities/").then((res: any) => {
countries.value = res.countries;
organisations.value = res.organisations;
+ courseProfiles.value = res.courseProfiles;
});
- return { organisations, countries };
+ return { organisations, countries, courseProfiles };
}
diff --git a/client/src/services/onboarding.ts b/client/src/services/onboarding.ts
index 9c59ef48..1b2e4d5e 100644
--- a/client/src/services/onboarding.ts
+++ b/client/src/services/onboarding.ts
@@ -11,7 +11,7 @@ export function profileNextRoute(courseType: string | string[]) {
}
// vv- -> vv-de, vv-fr or vv-it
if (isString(courseType) && startsWith(courseType, "vv-")) {
- return "checkoutAddress";
+ return "accountCourseProfile";
}
return "";
}
diff --git a/client/src/stores/user.ts b/client/src/stores/user.ts
index 2c85dba7..6e9e9e53 100644
--- a/client/src/stores/user.ts
+++ b/client/src/stores/user.ts
@@ -1,6 +1,6 @@
import { bustItGetCache, itGetCached, itPost } from "@/fetchHelpers";
import { setI18nLanguage } from "@/i18nextWrapper";
-import type { Country } from "@/services/entities";
+import type { Country, CourseProfile } from "@/services/entities";
import { directUpload } from "@/services/files";
import dayjs from "dayjs";
import { defineStore } from "pinia";
@@ -44,6 +44,7 @@ export interface User {
organisation_postal_code: string;
organisation_city: string;
organisation_country: Country | null;
+ chosen_profile?: CourseProfile;
}
let defaultLanguage: AvailableLanguages = "de";
@@ -89,6 +90,7 @@ const initialUserState: User = {
organisation_postal_code: "",
organisation_city: "",
organisation_country: null,
+ chosen_profile: undefined,
};
async function setLocale(language: AvailableLanguages) {
@@ -176,5 +178,8 @@ export const useUserStore = defineStore({
await itPost("/api/core/me/", profileData, { method: "PUT" });
Object.assign(this.$state, profileData);
},
+ updateChosenCourseProfile(courseProfile: CourseProfile) {
+ Object.assign(this.$state, { chosen_profile: courseProfile });
+ },
},
});
diff --git a/client/src/types.ts b/client/src/types.ts
index 754ee1ec..b0f6b667 100644
--- a/client/src/types.ts
+++ b/client/src/types.ts
@@ -202,6 +202,7 @@ export interface Course {
title: string;
category_name: string;
slug: string;
+ profiles: string[];
configuration: CourseConfiguration;
}
diff --git a/client/tailwind.css b/client/tailwind.css
index 2417d110..14a41aea 100644
--- a/client/tailwind.css
+++ b/client/tailwind.css
@@ -93,7 +93,7 @@ textarea {
}
.link {
- @apply underline underline-offset-2;
+ @apply cursor-pointer underline underline-offset-2;
}
.link-large {
@@ -167,6 +167,14 @@ textarea {
top: 1rem;
transform: translateY(-50%);
}
+
+ .tag-inactive {
+ @apply rounded-full border-2 border-blue-900 px-4 py-2 font-semibold text-blue-900;
+ }
+
+ .tag-active {
+ @apply rounded-full bg-blue-900 px-4 py-2 font-semibold text-white;
+ }
}
@layer utilities {
@@ -181,7 +189,9 @@ textarea {
}
.no-scrollbar {
- -ms-overflow-style: none; /* IE and Edge */
- scrollbar-width: none; /* Firefox */
+ -ms-overflow-style: none;
+ /* IE and Edge */
+ scrollbar-width: none;
+ /* Firefox */
}
}
diff --git a/cypress/consts.js b/cypress/consts.js
index 43ef6b5b..a32c8474 100644
--- a/cypress/consts.js
+++ b/cypress/consts.js
@@ -1,16 +1,22 @@
// ids for cypress test data
-export const ADMIN_USER_ID = "872efd96-3bd7-4a1e-a239-2d72cad9f604"
-export const TEST_SUPERVISOR1_USER_ID = "a9a8b741-f115-4521-af2d-7dfef673b8c5"
-export const TEST_TRAINER1_USER_ID = "b9e71f59-c44f-4290-b93a-9b3151e9a2fc"
-export const TEST_TRAINER2_USER_ID = "299941ae-1e4b-4f45-8180-876c3ad340b4"
-export const TEST_STUDENT1_USER_ID = "65c73ad0-6d53-43a9-a4a4-64143f27b03a"
-export const TEST_STUDENT2_USER_ID = "19c40d94-15cc-4198-aaad-ef707c4b0900"
-export const TEST_STUDENT3_USER_ID = "bcf94dba-53bc-474b-a22d-e4af39aa042b"
-export const TEST_MENTOR1_USER_ID = "d1f5f5a9-5b0a-4e1a-9e1a-9e9b5b5e1b1b"
-export const TEST_STUDENT1_VV_USER_ID = "5ff59857-8de5-415e-a387-4449f9a0337a"
-export const TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID = "7e8ebf0b-e6e2-4022-88f4-6e663ba0a9db"
-export const TEST_USER_EMPTY_ID = "daecbabe-4ab9-4edf-a71f-4119042ccb02"
-
+export const ADMIN_USER_ID = "872efd96-3bd7-4a1e-a239-2d72cad9f604";
+export const TEST_SUPERVISOR1_USER_ID = "a9a8b741-f115-4521-af2d-7dfef673b8c5";
+export const TEST_TRAINER1_USER_ID = "b9e71f59-c44f-4290-b93a-9b3151e9a2fc";
+export const TEST_TRAINER2_USER_ID = "299941ae-1e4b-4f45-8180-876c3ad340b4";
+export const TEST_STUDENT1_USER_ID = "65c73ad0-6d53-43a9-a4a4-64143f27b03a";
+export const TEST_STUDENT2_USER_ID = "19c40d94-15cc-4198-aaad-ef707c4b0900";
+export const TEST_STUDENT3_USER_ID = "bcf94dba-53bc-474b-a22d-e4af39aa042b";
+export const TEST_MENTOR1_USER_ID = "d1f5f5a9-5b0a-4e1a-9e1a-9e9b5b5e1b1b";
+export const TEST_STUDENT1_VV_USER_ID = "5ff59857-8de5-415e-a387-4449f9a0337a";
+export const TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID =
+ "7e8ebf0b-e6e2-4022-88f4-6e663ba0a9db";
+export const TEST_USER_EMPTY_ID = "daecbabe-4ab9-4edf-a71f-4119042ccb02";
export const TEST_COURSE_SESSION_BERN_ID = -1;
export const TEST_COURSE_SESSION_ZURICH_ID = -2;
+export const TEST_COURSE_SESSION_VV_ID = 1;
+
+export const COURSE_PROFILE_LEBEN_ID = -1;
+export const COURSE_PROFILE_NICHTLEBEN_ID = -2;
+export const COURSE_PROFILE_KRANKENZUSATZ_ID = -3;
+export const COURSE_PROFILE_ALL_ID = -99;
diff --git a/cypress/e2e/checkout-vv/checkout.cy.js b/cypress/e2e/checkout-vv/checkout.cy.js
index e6007b44..d00c6cca 100644
--- a/cypress/e2e/checkout-vv/checkout.cy.js
+++ b/cypress/e2e/checkout-vv/checkout.cy.js
@@ -1,4 +1,9 @@
-import { TEST_USER_EMPTY_ID } from "../../consts";
+import {
+ COURSE_PROFILE_ALL_ID,
+ COURSE_PROFILE_NICHTLEBEN_ID,
+ TEST_COURSE_SESSION_VV_ID,
+ TEST_USER_EMPTY_ID,
+} from "../../consts";
import { login } from "../helpers";
describe("checkout.cy.js", () => {
@@ -32,6 +37,15 @@ describe("checkout.cy.js", () => {
cy.get("#organisationDetailName").type("FdH GmbH");
cy.get('[data-cy="continue-button"]').click();
+ cy.get('[data-cy="account-course-profile-title"]').should(
+ "have.text",
+ "Zulassungsprofil auswählen",
+ );
+
+ cy.get('[data-cy="dropdown-select"]').click();
+ cy.get('[data-cy="dropdown-select-option-Nichtleben"]').click();
+ cy.get('[data-cy="continue-button"]').click();
+
cy.loadUser("id", TEST_USER_EMPTY_ID).then((u) => {
expect(u.organisation_detail_name).to.equal("FdH GmbH");
// 2 -> andere Krankenversicherer
@@ -121,6 +135,12 @@ describe("checkout.cy.js", () => {
cy.loadCheckoutInformation("user_id", TEST_USER_EMPTY_ID).then((ci) => {
expect(ci.state).to.equal("paid");
});
+
+ cy.loadCourseSessionUser("user_id", TEST_USER_EMPTY_ID).then((csu) => {
+ expect(csu.role).to.equal("MEMBER");
+ expect(csu.course_session).to.equal(TEST_COURSE_SESSION_VV_ID);
+ expect(csu.chosen_profile).to.equal(COURSE_PROFILE_NICHTLEBEN_ID);
+ });
});
it("can checkout and pay Versicherungsvermittlerin with Cembra invoice", () => {
@@ -143,6 +163,15 @@ describe("checkout.cy.js", () => {
cy.get('[data-cy="dropdown-select-option-Baloise"]').click();
cy.get('[data-cy="continue-button"]').click();
+ cy.get('[data-cy="account-course-profile-title"]').should(
+ "have.text",
+ "Zulassungsprofil auswählen",
+ );
+
+ cy.get('[data-cy="dropdown-select"]').click();
+ cy.get('[data-cy="dropdown-select-option-Allbranche"]').click();
+ cy.get('[data-cy="continue-button"]').click();
+
// Adressdaten ausfüllen
cy.get('[data-cy="account-checkout-title"]').should(
"contain",
@@ -236,5 +265,32 @@ describe("checkout.cy.js", () => {
// 7 -> Baloise
expect(u.organisation).to.equal(7);
});
+
+ // pay
+ cy.get('[data-cy="pay-button"]').click();
+
+ cy.get('[data-cy="checkout-success-title"]').should(
+ "contain",
+ "Gratuliere",
+ );
+ // wait for payment callback
+ cy.wait(3000);
+ cy.get('[data-cy="start-vv-button"]').click();
+
+ // back on dashboard page
+ cy.get('[data-cy="db-course-title"]').should(
+ "contain",
+ "Versicherungsvermittler",
+ );
+
+ cy.loadCheckoutInformation("user_id", TEST_USER_EMPTY_ID).then((ci) => {
+ expect(ci.state).to.equal("paid");
+ });
+
+ cy.loadCourseSessionUser("user_id", TEST_USER_EMPTY_ID).then((csu) => {
+ expect(csu.role).to.equal("MEMBER");
+ expect(csu.course_session).to.equal(TEST_COURSE_SESSION_VV_ID);
+ expect(csu.chosen_profile).to.equal(COURSE_PROFILE_ALL_ID);
+ });
});
});
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index 857f1135..c5b6230e 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -189,6 +189,17 @@ Cypress.Commands.add("loadUser", (key, value) => {
});
+Cypress.Commands.add("loadCourseSessionUser", (key, value) => {
+ return loadObjectJson(
+ key,
+ value,
+ "vbv_lernwelt.course.models.CourseSessionUser",
+ "vbv_lernwelt.course.serializers.CypressCourseSessionUserSerializer",
+ true
+ );
+});
+
+
Cypress.Commands.add("makeSelfEvaluation", (answers) => {
for (let i = 0; i < answers.length; i++) {
const answer = answers[i]
diff --git a/git-pre-push.sh b/git-pre-push.sh
index 8cb53b69..fcf48120 100755
--- a/git-pre-push.sh
+++ b/git-pre-push.sh
@@ -7,7 +7,7 @@ echo 'prettier:check'
(cd client && npm run prettier:check)
echo 'lint and typecheck'
-(cd client && npm run lint && npm run typecheck)
+(cd client && npm run lint:errors && npm run typecheck)
echo 'python ufmt check'
ufmt check server
diff --git a/server/vbv_lernwelt/api/directory.py b/server/vbv_lernwelt/api/directory.py
index bc5d9673..9f843c87 100644
--- a/server/vbv_lernwelt/api/directory.py
+++ b/server/vbv_lernwelt/api/directory.py
@@ -4,6 +4,8 @@ from rest_framework.response import Response
from vbv_lernwelt.core.models import Country, Organisation
from vbv_lernwelt.core.serializers import CountrySerializer, OrganisationSerializer
+from vbv_lernwelt.learnpath.models import CourseProfile
+from vbv_lernwelt.learnpath.serializers import CourseProfileSerializer
@api_view(["GET"])
@@ -26,4 +28,13 @@ def list_entities(request):
countries = CountrySerializer(
Country.objects.all(), many=True, context=context
).data
- return Response({"organisations": organisations, "countries": countries})
+ course_profiles = CourseProfileSerializer(
+ CourseProfile.objects.all(), many=True, context=context
+ ).data
+ return Response(
+ {
+ "organisations": organisations,
+ "countries": countries,
+ "courseProfiles": course_profiles,
+ }
+ )
diff --git a/server/vbv_lernwelt/api/user.py b/server/vbv_lernwelt/api/user.py
index dccad63b..248030c2 100644
--- a/server/vbv_lernwelt/api/user.py
+++ b/server/vbv_lernwelt/api/user.py
@@ -1,4 +1,3 @@
-from django.shortcuts import get_object_or_404
from rest_framework.decorators import api_view, permission_classes
from rest_framework.generics import get_object_or_404
from rest_framework.permissions import IsAuthenticated
diff --git a/server/vbv_lernwelt/course/graphql/types.py b/server/vbv_lernwelt/course/graphql/types.py
index 32665319..934fa119 100644
--- a/server/vbv_lernwelt/course/graphql/types.py
+++ b/server/vbv_lernwelt/course/graphql/types.py
@@ -8,6 +8,7 @@ from graphql import GraphQLError
from rest_framework.exceptions import PermissionDenied
from vbv_lernwelt.competence.graphql.types import ActionCompetenceObjectType
+from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.models import (
CircleDocument,
Course,
@@ -29,8 +30,9 @@ from vbv_lernwelt.course_session.models import (
)
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
from vbv_lernwelt.iam.permissions import has_course_access
+from vbv_lernwelt.learnpath.consts import COURSE_PROFILE_ALL_ID
from vbv_lernwelt.learnpath.graphql.types import LearningPathObjectType
-from vbv_lernwelt.learnpath.models import Circle
+from vbv_lernwelt.learnpath.models import Circle, CourseProfile
logger = structlog.get_logger(__name__)
@@ -106,6 +108,12 @@ class CourseObjectType(DjangoObjectType):
graphene.NonNull(ActionCompetenceObjectType), required=True
)
configuration = graphene.Field(CourseConfigurationObjectType, required=True)
+ profiles = graphene.List(graphene.String)
+ course_session_users = graphene.List(
+ "vbv_lernwelt.course.graphql.types.CourseSessionUserType",
+ required=True,
+ id=graphene.String(),
+ )
class Meta:
model = Course
@@ -125,6 +133,22 @@ class CourseObjectType(DjangoObjectType):
def resolve_action_competences(root: Course, info):
return root.get_action_competences()
+ @staticmethod
+ def resolve_profiles(root: Course, info, **kwargs):
+ if root.configuration.is_vv:
+ return CourseProfile.objects.values_list("code", flat=True)
+ return []
+
+ @staticmethod
+ def resolve_course_session_users(root: Course, info, id=None, **kwargs):
+ # todo: restrict users that can be queried
+ if id is not None:
+ user = User.objects.get(id=id)
+ else:
+ user = info.context.user
+ users = CourseSessionUser.objects.filter(user=user, course_session__course=root)
+ return users
+
class CourseSessionUserExpertCircleType(ObjectType):
id = graphene.ID(required=True)
@@ -132,6 +156,21 @@ class CourseSessionUserExpertCircleType(ObjectType):
slug = graphene.String(required=True)
+class CourseSessionUserType(DjangoObjectType):
+ chosen_profile = graphene.String(required=True)
+ course_session = graphene.Field(
+ "vbv_lernwelt.course.graphql.types.CourseSessionObjectType", required=True
+ )
+
+ class Meta:
+ model = CourseSessionUser
+ fields = ["chosen_profile", "id"]
+
+ @staticmethod
+ def resolve_chosen_profile(root: CourseSessionUser, info, **kwargs):
+ return getattr(root.chosen_profile, "code", "")
+
+
class CourseSessionUserObjectsType(ObjectType):
"""
WORKAROUND:
diff --git a/server/vbv_lernwelt/course/management/commands/create_default_courses.py b/server/vbv_lernwelt/course/management/commands/create_default_courses.py
index 2798f21e..f1dde916 100644
--- a/server/vbv_lernwelt/course/management/commands/create_default_courses.py
+++ b/server/vbv_lernwelt/course/management/commands/create_default_courses.py
@@ -101,6 +101,7 @@ from vbv_lernwelt.learnpath.create_vv_new_learning_path import (
create_vv_new_learning_path,
create_vv_pruefung_learning_path,
)
+from vbv_lernwelt.learnpath.creators import assign_circles_to_profiles
from vbv_lernwelt.learnpath.models import (
Circle,
LearningContent,
@@ -222,6 +223,7 @@ def create_versicherungsvermittlerin_course(
create_vv_gewinnen_casework(course_id=course_id)
create_vv_reflection(course_id=course_id)
create_vv_new_learning_path(course_id=course_id)
+ assign_circles_to_profiles()
cs = CourseSession.objects.create(course_id=course_id, title=names[language])
diff --git a/server/vbv_lernwelt/course/migrations/0009_coursesessionuser_chosen_profile.py b/server/vbv_lernwelt/course/migrations/0009_coursesessionuser_chosen_profile.py
new file mode 100644
index 00000000..5ae47808
--- /dev/null
+++ b/server/vbv_lernwelt/course/migrations/0009_coursesessionuser_chosen_profile.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.2.20 on 2024-07-11 09:00
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("learnpath", "0017_auto_20240711_1100"),
+ ("course", "0008_auto_20240403_1132"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="coursesessionuser",
+ name="chosen_profile",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="learnpath.courseprofile",
+ ),
+ ),
+ ]
diff --git a/server/vbv_lernwelt/course/migrations/0010_alter_coursecompletion_completion_status.py b/server/vbv_lernwelt/course/migrations/0010_alter_coursecompletion_completion_status.py
new file mode 100644
index 00000000..b864ab57
--- /dev/null
+++ b/server/vbv_lernwelt/course/migrations/0010_alter_coursecompletion_completion_status.py
@@ -0,0 +1,27 @@
+# Generated by Django 4.2.13 on 2024-07-22 19:45
+
+from django.db import migrations, models
+
+import vbv_lernwelt.course.models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("course", "0009_coursesessionuser_chosen_profile"),
+ ]
+
+ operations = [
+ 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,
+ ),
+ ),
+ ]
diff --git a/server/vbv_lernwelt/course/migrations/0011_merge_20240807_1317.py b/server/vbv_lernwelt/course/migrations/0011_merge_20240807_1317.py
new file mode 100644
index 00000000..2d76afb8
--- /dev/null
+++ b/server/vbv_lernwelt/course/migrations/0011_merge_20240807_1317.py
@@ -0,0 +1,12 @@
+# Generated by Django 4.2.13 on 2024-08-07 11:17
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("course", "0009_coursesessionuser_required_attendance_and_more"),
+ ("course", "0010_alter_coursecompletion_completion_status"),
+ ]
+
+ operations = []
diff --git a/server/vbv_lernwelt/course/models.py b/server/vbv_lernwelt/course/models.py
index d85c6a9a..aa5ef372 100644
--- a/server/vbv_lernwelt/course/models.py
+++ b/server/vbv_lernwelt/course/models.py
@@ -285,6 +285,10 @@ class CourseSessionUser(models.Model):
)
optional_attendance = models.BooleanField(default=False)
+ chosen_profile = models.ForeignKey(
+ "learnpath.CourseProfile", on_delete=models.SET_NULL, blank=True, null=True
+ )
+
class Meta:
constraints = [
UniqueConstraint(
diff --git a/server/vbv_lernwelt/course/serializers.py b/server/vbv_lernwelt/course/serializers.py
index bd6d9f1a..0ff76b4f 100644
--- a/server/vbv_lernwelt/course/serializers.py
+++ b/server/vbv_lernwelt/course/serializers.py
@@ -8,8 +8,10 @@ from vbv_lernwelt.course.models import (
CourseCompletion,
CourseConfiguration,
CourseSession,
+ CourseSessionUser,
)
from vbv_lernwelt.iam.permissions import course_session_permissions
+from vbv_lernwelt.learnpath.models import CourseProfile
class CourseConfigurationSerializer(serializers.ModelSerializer):
@@ -31,10 +33,23 @@ class CourseSerializer(serializers.ModelSerializer):
configuration = CourseConfigurationSerializer(
read_only=True,
)
+ course_profiles = serializers.SerializerMethodField()
+
+ def get_course_profiles(self, obj):
+ if obj.configuration.is_vv:
+ return CourseProfile.objects.all().values_list("code", flat=True)
+ return []
class Meta:
model = Course
- fields = ["id", "title", "category_name", "slug", "configuration"]
+ fields = [
+ "id",
+ "title",
+ "category_name",
+ "slug",
+ "configuration",
+ "course_profiles",
+ ]
class CourseCategorySerializer(serializers.ModelSerializer):
@@ -103,6 +118,12 @@ class CourseSessionSerializer(serializers.ModelSerializer):
return []
+class CypressCourseSessionUserSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = CourseSessionUser
+ fields = "__all__"
+
+
class CircleDocumentSerializer(serializers.ModelSerializer):
learning_sequence = serializers.SerializerMethodField()
diff --git a/server/vbv_lernwelt/course/tests/test_update_course_profile.py b/server/vbv_lernwelt/course/tests/test_update_course_profile.py
new file mode 100644
index 00000000..c4a4b12c
--- /dev/null
+++ b/server/vbv_lernwelt/course/tests/test_update_course_profile.py
@@ -0,0 +1,164 @@
+from django.test import RequestFactory, TestCase
+from graphene.test import Client
+
+from vbv_lernwelt.core.admin import User
+from vbv_lernwelt.core.create_default_users import create_default_users
+from vbv_lernwelt.core.schema import schema
+from vbv_lernwelt.course.management.commands.create_default_courses import (
+ create_versicherungsvermittlerin_course,
+)
+from vbv_lernwelt.course.models import CourseSessionUser
+from vbv_lernwelt.learnpath.creators import create_course_profiles
+from vbv_lernwelt.learnpath.models import CourseProfile
+
+
+class CourseGraphQLTestCase(TestCase):
+ def setUp(self) -> None:
+ create_default_users()
+ create_course_profiles()
+ create_versicherungsvermittlerin_course()
+
+ def test_update_course_profile(self):
+ user = User.objects.get(username="student-vv@eiger-versicherungen.ch")
+ request = RequestFactory().get("/")
+ request.user = user
+ client = Client(schema=schema, context_value=request)
+ query = """
+ query CourseQuery($slug: String!) {
+ course(slug: $slug){
+ id
+ profiles
+ course_session_users {
+ id
+ __typename
+ chosen_profile
+ course_session {
+ id
+ }
+ }
+ }
+ }
+ """
+ slug = "versicherungsvermittler-in"
+ variables = {"slug": slug}
+ result = client.execute(query, variables=variables)
+
+ self.assertIsNone(result.get("errors"))
+ data = result.get("data")
+ course = data.get("course")
+ profiles = course.get("profiles")
+ self.assertEqual(
+ set(profiles),
+ set(["all", "nichtleben", "leben", "krankenzusatzversicherung"]),
+ )
+ course_session_user = course.get("course_session_users")[0]
+ chosen_profile = course_session_user.get("chosen_profile")
+
+ self.assertEqual(chosen_profile, "")
+
+ mutation = """
+ mutation UpdateCourseSessionProfile($input: CourseSessionProfileMutationInput!) {
+ update_course_session_profile(input: $input) {
+ clientMutationId
+ result {
+ __typename
+ ... on UpdateCourseProfileSuccess {
+ user {
+ id
+ chosen_profile
+ }
+ }
+ ... on UpdateCourseProfileError {
+ message
+ }
+ }
+ }
+ }
+ """
+
+ profile = "nichtleben"
+ input = {"course_profile": profile, "course_slug": slug}
+
+ mutation_result = client.execute(mutation, variables={"input": input})
+
+ self.assertIsNone(mutation_result.get("errors"))
+
+ second_query_result = client.execute(query, variables=variables)
+
+ self.assertIsNone(second_query_result.get("errors"))
+ data = second_query_result.get("data")
+ course = data.get("course")
+ profiles = course.get("profiles")
+ self.assertEqual(
+ set(profiles),
+ set(["all", "nichtleben", "leben", "krankenzusatzversicherung"]),
+ )
+ course_session_user = course.get("course_session_users")[0]
+ chosen_profile = course_session_user.get("chosen_profile")
+ self.assertEqual(chosen_profile, profile)
+
+ def test_mentor_profile_view(self):
+ user = User.objects.get(username="test-mentor1@example.com")
+ request = RequestFactory().get("/")
+ request.user = user
+ client = Client(schema=schema, context_value=request)
+
+ query = """
+ query courseQuery($slug: String!, $user: String) {
+ course(slug: $slug) {
+ id
+ title
+ slug
+ category_name
+ profiles
+ course_session_users(id: $user) {
+ id
+ __typename
+ chosen_profile
+ course_session {
+ id
+ __typename
+ }
+ }
+ __typename
+ }
+ }
+ """
+ student = User.objects.get(username="student-vv@eiger-versicherungen.ch")
+
+ slug = "versicherungsvermittler-in"
+ student_id = str(student.id)
+ variables = {"slug": slug, "user": student_id}
+ print(variables)
+ result = client.execute(query, variables=variables)
+ self.assertIsNone(result.get("errors"))
+ data = result.get("data")
+ course = data.get("course")
+ profiles = course.get("profiles")
+ self.assertEqual(
+ set(profiles),
+ set(["all", "nichtleben", "leben", "krankenzusatzversicherung"]),
+ )
+ course_session_user = course.get("course_session_users")[0]
+ chosen_profile = course_session_user.get("chosen_profile")
+ self.assertEqual(chosen_profile, "")
+
+ csu = CourseSessionUser.objects.get(
+ course_session__course__slug=slug, user=student
+ )
+ course_profile = CourseProfile.objects.get(code="nichtleben")
+ csu.chosen_profile = course_profile
+ csu.save()
+
+ second_result = client.execute(query, variables=variables)
+ self.assertIsNone(second_result.get("errors"))
+ data = second_result.get("data")
+ course = data.get("course")
+ profiles = course.get("profiles")
+ self.assertEqual(
+ set(profiles),
+ set(["all", "nichtleben", "leben", "krankenzusatzversicherung"]),
+ )
+ course_session_user = course.get("course_session_users")[0]
+ chosen_profile = course_session_user.get("chosen_profile")
+ self.assertEqual(chosen_profile, "nichtleben")
diff --git a/server/vbv_lernwelt/course_session/graphql/mutations.py b/server/vbv_lernwelt/course_session/graphql/mutations.py
index 1d00013b..11160149 100644
--- a/server/vbv_lernwelt/course_session/graphql/mutations.py
+++ b/server/vbv_lernwelt/course_session/graphql/mutations.py
@@ -1,7 +1,10 @@
import graphene
import structlog
+from graphene import relay
from rest_framework.exceptions import PermissionDenied
+from vbv_lernwelt.course.graphql.types import CourseSessionUserType
+from vbv_lernwelt.course.models import CourseSessionUser
from vbv_lernwelt.course_session.graphql.types import (
CourseSessionAttendanceCourseObjectType,
)
@@ -11,10 +14,33 @@ from vbv_lernwelt.course_session.services.attendance import (
update_attendance_list,
)
from vbv_lernwelt.iam.permissions import has_course_access
+from vbv_lernwelt.learnpath.models import CourseProfile
logger = structlog.get_logger(__name__)
+class UpdateCourseProfileSuccess(graphene.ObjectType):
+ user = graphene.Field(CourseSessionUserType(), required=True)
+
+
+class UpdateCourseProfileError(graphene.ObjectType):
+ message = graphene.String()
+
+
+class UpdateCourseProfileResult(graphene.Union):
+ class Meta:
+ types = (
+ UpdateCourseProfileError,
+ UpdateCourseProfileSuccess,
+ )
+
+ @classmethod
+ def resolve_type(cls, instance, info):
+ if type(instance).__name__ == "UpdateCourseProfileSuccess":
+ return UpdateCourseProfileSuccess
+ return UpdateCourseProfileError
+
+
class AttendanceUserInputType(graphene.InputObjectType):
user_id = graphene.UUID(required=True)
status = graphene.Field(
@@ -57,5 +83,40 @@ class AttendanceCourseUserMutation(graphene.Mutation):
)
+class CourseSessionProfileMutation(relay.ClientIDMutation):
+ class Input:
+ course_profile = graphene.String(required=True)
+ course_slug = graphene.String(required=True)
+
+ result = UpdateCourseProfileResult()
+
+ @classmethod
+ def mutate_and_get_payload(cls, root, info, **input):
+ course_profile = input.get("course_profile")
+ course_slug = input.get("course_slug")
+ user = info.context.user
+
+ try:
+ if course_profile == "":
+ profile = None
+ else:
+ profile = CourseProfile.objects.get(code=course_profile)
+
+ # csu = user.coursesessionuser_set.first()
+ csu = CourseSessionUser.objects.get(
+ course_session__course__slug=course_slug, user=user
+ )
+ csu.chosen_profile = profile
+ csu.save()
+ return cls(result=UpdateCourseProfileSuccess(user=csu))
+ except CourseProfile.DoesNotExist:
+ return cls(result=UpdateCourseProfileError("Course Profile does not exist"))
+ except CourseSessionUser.DoesNotExist:
+ return cls(
+ result=UpdateCourseProfileError("Course Session User does not exist")
+ )
+
+
class CourseSessionMutation:
update_course_session_attendance_course_users = AttendanceCourseUserMutation.Field()
+ update_course_session_profile = CourseSessionProfileMutation.Field()
diff --git a/server/vbv_lernwelt/learnpath/admin.py b/server/vbv_lernwelt/learnpath/admin.py
index 8c38f3f3..7de0335d 100644
--- a/server/vbv_lernwelt/learnpath/admin.py
+++ b/server/vbv_lernwelt/learnpath/admin.py
@@ -1,3 +1,8 @@
from django.contrib import admin
-# Register your models here.
+from vbv_lernwelt.learnpath.models import CourseProfile
+
+
+@admin.register(CourseProfile)
+class CourseProfileAdmin(admin.ModelAdmin):
+ pass
diff --git a/server/vbv_lernwelt/learnpath/consts.py b/server/vbv_lernwelt/learnpath/consts.py
new file mode 100644
index 00000000..f9c71875
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/consts.py
@@ -0,0 +1,9 @@
+COURSE_PROFILE_LEBEN_ID = -1
+COURSE_PROFILE_NICHTLEBEN_ID = -2
+COURSE_PROFILE_KRANKENZUSATZ_ID = -3
+COURSE_PROFILE_ALL_ID = -99
+
+COURSE_PROFILE_LEBEN_CODE = "leben"
+COURSE_PROFILE_NICHTLEBEN_CODE = "nichtleben"
+COURSE_PROFILE_KRANKENZUSATZ_CODE = "krankenzusatzversicherung"
+COURSE_PROFILE_ALL_CODE = "all"
diff --git a/server/vbv_lernwelt/learnpath/creators.py b/server/vbv_lernwelt/learnpath/creators.py
new file mode 100644
index 00000000..d238e6eb
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/creators.py
@@ -0,0 +1,192 @@
+import structlog
+
+from vbv_lernwelt.course.consts import (
+ COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID,
+ COURSE_VERSICHERUNGSVERMITTLERIN_ID,
+ COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID,
+)
+from vbv_lernwelt.learnpath.consts import (
+ COURSE_PROFILE_ALL_CODE,
+ COURSE_PROFILE_ALL_ID,
+ COURSE_PROFILE_KRANKENZUSATZ_CODE,
+ COURSE_PROFILE_KRANKENZUSATZ_ID,
+ COURSE_PROFILE_LEBEN_CODE,
+ COURSE_PROFILE_LEBEN_ID,
+ COURSE_PROFILE_NICHTLEBEN_CODE,
+ COURSE_PROFILE_NICHTLEBEN_ID,
+)
+
+logger = structlog.get_logger(__name__)
+
+
+def create_course_profiles():
+ from vbv_lernwelt.learnpath.models import CourseProfile
+
+ # Allbranche, Krankenzusatzversicherung, nicht Leben, Leben
+ CourseProfile.objects.get_or_create(
+ id=COURSE_PROFILE_ALL_ID, code=COURSE_PROFILE_ALL_CODE, order=1
+ )
+ CourseProfile.objects.get_or_create(
+ id=COURSE_PROFILE_KRANKENZUSATZ_ID,
+ code=COURSE_PROFILE_KRANKENZUSATZ_CODE,
+ order=2,
+ )
+ CourseProfile.objects.get_or_create(
+ id=COURSE_PROFILE_NICHTLEBEN_ID, code=COURSE_PROFILE_NICHTLEBEN_CODE, order=3
+ )
+ CourseProfile.objects.get_or_create(
+ id=COURSE_PROFILE_LEBEN_ID, code=COURSE_PROFILE_LEBEN_CODE, order=4
+ )
+
+
+def assign_circle_to_profile_curry(course_page):
+ from vbv_lernwelt.learnpath.models import Circle, CourseProfile
+
+ def assign_circle_to_profile(title, code):
+ try:
+ circle = Circle.objects.descendant_of(course_page).get(title=title)
+ course_profile = CourseProfile.objects.get(code=code)
+ circle.profiles.add(course_profile)
+ circle.save()
+ except Circle.DoesNotExist:
+ logger.warning("assign_circle_to_profile: circle not found", title=title)
+
+ return assign_circle_to_profile
+
+
+def make_base_circle_curry(course_page):
+ from vbv_lernwelt.learnpath.models import Circle
+
+ def make_base_circle(title):
+ try:
+ circle = Circle.objects.descendant_of(course_page).get(title=title)
+ circle.is_base_circle = True
+ circle.save()
+ except Circle.DoesNotExist:
+ logger.warning("assign_circle_to_profile: circle not found", title=title)
+
+ return make_base_circle
+
+
+def assign_de_circles_to_profiles():
+ from vbv_lernwelt.course.models import CoursePage
+
+ try:
+ course_page = CoursePage.objects.get(
+ course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID
+ )
+ except CoursePage.DoesNotExist:
+ logger.warning("Course does not exist yet")
+ return
+
+ assign_circle_to_profile = assign_circle_to_profile_curry(course_page)
+ make_base_circle = make_base_circle_curry(course_page)
+
+ assign_circle_to_profile("Fahrzeug", COURSE_PROFILE_NICHTLEBEN_CODE)
+ assign_circle_to_profile("Haushalt", COURSE_PROFILE_NICHTLEBEN_CODE)
+ assign_circle_to_profile("Rechtsstreitigkeiten", COURSE_PROFILE_NICHTLEBEN_CODE)
+ assign_circle_to_profile("Reisen", COURSE_PROFILE_NICHTLEBEN_CODE)
+ assign_circle_to_profile("Wohneigentum", COURSE_PROFILE_NICHTLEBEN_CODE)
+ assign_circle_to_profile(
+ "Selbstständigkeit", COURSE_PROFILE_NICHTLEBEN_CODE
+ ) # typo, but that's how it is in prod data
+ assign_circle_to_profile("KMU", COURSE_PROFILE_NICHTLEBEN_CODE)
+
+ assign_circle_to_profile("Einkommenssicherung", COURSE_PROFILE_LEBEN_CODE)
+ assign_circle_to_profile("Pensionierung", COURSE_PROFILE_LEBEN_CODE)
+ assign_circle_to_profile("Erben/Vererben", COURSE_PROFILE_LEBEN_CODE)
+ assign_circle_to_profile("Sparen", COURSE_PROFILE_LEBEN_CODE)
+ assign_circle_to_profile(
+ "Selbstständigkeit", COURSE_PROFILE_LEBEN_CODE
+ ) # typo, but that's how it is in prod data
+ assign_circle_to_profile("KMU", COURSE_PROFILE_LEBEN_CODE)
+
+ assign_circle_to_profile("Gesundheit", COURSE_PROFILE_KRANKENZUSATZ_CODE)
+
+ make_base_circle("Kickoff")
+ make_base_circle("Basis")
+ make_base_circle("Gewinnen")
+ make_base_circle("Prüfungsvorbereitung")
+ make_base_circle("Prüfung")
+
+
+def assign_fr_circles_to_profiles():
+ from vbv_lernwelt.course.models import CoursePage
+
+ try:
+ course_page = CoursePage.objects.get(
+ course_id=COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID
+ )
+ except CoursePage.DoesNotExist:
+ logger.warning("Course does not exist yet")
+ return
+
+ assign_circle_to_profile = assign_circle_to_profile_curry(course_page)
+ make_base_circle = make_base_circle_curry(course_page)
+
+ assign_circle_to_profile("Véhicule", COURSE_PROFILE_NICHTLEBEN_CODE)
+ assign_circle_to_profile("Ménage", COURSE_PROFILE_NICHTLEBEN_CODE)
+ assign_circle_to_profile("Litiges juridiques", COURSE_PROFILE_NICHTLEBEN_CODE)
+ assign_circle_to_profile("Voyages", COURSE_PROFILE_NICHTLEBEN_CODE)
+ assign_circle_to_profile("Propriété du logement", COURSE_PROFILE_NICHTLEBEN_CODE)
+ assign_circle_to_profile("Activité indépendante", COURSE_PROFILE_NICHTLEBEN_CODE)
+ assign_circle_to_profile("PME", COURSE_PROFILE_NICHTLEBEN_CODE)
+
+ assign_circle_to_profile("Garantie des revenus", COURSE_PROFILE_LEBEN_CODE)
+ assign_circle_to_profile("Retraite", COURSE_PROFILE_LEBEN_CODE)
+ assign_circle_to_profile("Hériter\xa0/\xa0léguer", COURSE_PROFILE_LEBEN_CODE)
+ assign_circle_to_profile("Épargne", COURSE_PROFILE_LEBEN_CODE)
+ assign_circle_to_profile("Activité indépendante", COURSE_PROFILE_LEBEN_CODE)
+ assign_circle_to_profile("PME", COURSE_PROFILE_LEBEN_CODE)
+
+ assign_circle_to_profile("Santé", COURSE_PROFILE_KRANKENZUSATZ_CODE)
+
+ make_base_circle("Lancement")
+ make_base_circle("Base")
+ make_base_circle("Acquisition")
+ make_base_circle("Préparation à l’examen")
+ make_base_circle("L’examen")
+
+
+def assign_it_circles_to_profiles():
+ from vbv_lernwelt.course.models import CoursePage
+
+ try:
+ course_page = CoursePage.objects.get(
+ course_id=COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID
+ )
+ except CoursePage.DoesNotExist:
+ logger.warning("Course does not exist yet")
+ return
+
+ assign_circle_to_profile = assign_circle_to_profile_curry(course_page)
+ make_base_circle = make_base_circle_curry(course_page)
+
+ assign_circle_to_profile("Veicolo", COURSE_PROFILE_NICHTLEBEN_CODE)
+ assign_circle_to_profile("Economia domestica", COURSE_PROFILE_NICHTLEBEN_CODE)
+ assign_circle_to_profile("Controversie giuridiche", COURSE_PROFILE_NICHTLEBEN_CODE)
+ assign_circle_to_profile("Viaggi", COURSE_PROFILE_NICHTLEBEN_CODE)
+ assign_circle_to_profile("Casa di proprietà", COURSE_PROFILE_NICHTLEBEN_CODE)
+ assign_circle_to_profile("Attività indipendente", COURSE_PROFILE_NICHTLEBEN_CODE)
+ assign_circle_to_profile("PMI", COURSE_PROFILE_NICHTLEBEN_CODE)
+
+ assign_circle_to_profile("Protezione del reddito", COURSE_PROFILE_LEBEN_CODE)
+ assign_circle_to_profile("Pensionamento", COURSE_PROFILE_LEBEN_CODE)
+ assign_circle_to_profile("Ereditare/lasciare in eredità", COURSE_PROFILE_LEBEN_CODE)
+ assign_circle_to_profile("Risparmio", COURSE_PROFILE_LEBEN_CODE)
+ assign_circle_to_profile("Attività indipendente", COURSE_PROFILE_LEBEN_CODE)
+ assign_circle_to_profile("PMI", COURSE_PROFILE_LEBEN_CODE)
+
+ assign_circle_to_profile("Salute", COURSE_PROFILE_KRANKENZUSATZ_CODE)
+
+ make_base_circle("Kickoff")
+ make_base_circle("Base")
+ make_base_circle("Acquisizione")
+ make_base_circle("Preparazione all'esame")
+ make_base_circle("Esame")
+
+
+def assign_circles_to_profiles():
+ assign_de_circles_to_profiles()
+ assign_fr_circles_to_profiles()
+ assign_it_circles_to_profiles()
diff --git a/server/vbv_lernwelt/learnpath/graphql/types.py b/server/vbv_lernwelt/learnpath/graphql/types.py
index 4e12cc4b..7d29e5fd 100644
--- a/server/vbv_lernwelt/learnpath/graphql/types.py
+++ b/server/vbv_lernwelt/learnpath/graphql/types.py
@@ -1,3 +1,5 @@
+import random
+
import graphene
import structlog
from graphene_django import DjangoObjectType
@@ -6,6 +8,7 @@ from vbv_lernwelt.core.utils import find_first_index
from vbv_lernwelt.course.graphql.interfaces import CoursePageInterface
from vbv_lernwelt.learnpath.models import (
Circle,
+ CourseProfile,
LearningContentAssignment,
LearningContentAttendanceCourse,
LearningContentDocumentList,
@@ -299,14 +302,12 @@ class CircleObjectType(DjangoObjectType):
learning_sequences = graphene.List(
graphene.NonNull(LearningSequenceObjectType), required=True
)
+ profiles = graphene.List(graphene.String, required=True)
class Meta:
model = Circle
interfaces = (CoursePageInterface,)
- fields = [
- "description",
- "goals",
- ]
+ fields = ["description", "goals", "is_base_circle"]
def resolve_learning_sequences(self: Circle, info, **kwargs):
circle_descendants = None
@@ -335,6 +336,10 @@ class CircleObjectType(DjangoObjectType):
if descendant.specific_class == LearningSequence
]
+ @staticmethod
+ def resolve_profiles(root: Circle, info, **kwargs):
+ return root.profiles.all()
+
class TopicObjectType(DjangoObjectType):
circles = graphene.List(graphene.NonNull(CircleObjectType), required=True)
diff --git a/server/vbv_lernwelt/learnpath/migrations/0017_auto_20240711_1100.py b/server/vbv_lernwelt/learnpath/migrations/0017_auto_20240711_1100.py
new file mode 100644
index 00000000..7eba3179
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/migrations/0017_auto_20240711_1100.py
@@ -0,0 +1,48 @@
+# Generated by Django 3.2.20 on 2024-07-11 09:00
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("learnpath", "0016_remove_learningunit_feedback_user"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="CourseProfile",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("code", models.CharField(max_length=255)),
+ ],
+ ),
+ migrations.CreateModel(
+ name="CourseProfileToCircle",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ],
+ ),
+ migrations.AddField(
+ model_name="circle",
+ name="profiles",
+ field=models.ManyToManyField(
+ related_name="circles", to="learnpath.CourseProfile"
+ ),
+ ),
+ ]
diff --git a/server/vbv_lernwelt/learnpath/migrations/0018_alter_courseprofile_options_circle_is_base_circle_and_more.py b/server/vbv_lernwelt/learnpath/migrations/0018_alter_courseprofile_options_circle_is_base_circle_and_more.py
new file mode 100644
index 00000000..8d9f3b72
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/migrations/0018_alter_courseprofile_options_circle_is_base_circle_and_more.py
@@ -0,0 +1,34 @@
+# Generated by Django 4.2.13 on 2024-07-30 07:03
+
+import modelcluster.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("learnpath", "0017_auto_20240711_1100"),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name="courseprofile",
+ options={"ordering": ["order"]},
+ ),
+ migrations.AddField(
+ model_name="circle",
+ name="is_base_circle",
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name="courseprofile",
+ name="order",
+ field=models.IntegerField(default=999),
+ ),
+ migrations.AlterField(
+ model_name="circle",
+ name="profiles",
+ field=modelcluster.fields.ParentalManyToManyField(
+ related_name="circles", to="learnpath.courseprofile"
+ ),
+ ),
+ ]
diff --git a/server/vbv_lernwelt/learnpath/migrations/0019_auto_20240730_0904.py b/server/vbv_lernwelt/learnpath/migrations/0019_auto_20240730_0904.py
new file mode 100644
index 00000000..46464f1e
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/migrations/0019_auto_20240730_0904.py
@@ -0,0 +1,20 @@
+# Generated by Django 4.2.13 on 2024-07-30 07:04
+
+from django.db import migrations
+
+from vbv_lernwelt.learnpath.creators import create_course_profiles
+
+
+def migrate(apps, schema_editor):
+ create_course_profiles()
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ (
+ "learnpath",
+ "0018_alter_courseprofile_options_circle_is_base_circle_and_more",
+ ),
+ ]
+
+ operations = [migrations.RunPython(migrate)]
diff --git a/server/vbv_lernwelt/learnpath/migrations/0020_auto_20240730_0905.py b/server/vbv_lernwelt/learnpath/migrations/0020_auto_20240730_0905.py
new file mode 100644
index 00000000..edac10fc
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/migrations/0020_auto_20240730_0905.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.2.13 on 2024-07-30 07:05
+
+from django.db import migrations
+
+from vbv_lernwelt.learnpath.creators import assign_circles_to_profiles
+
+
+def migrate(apps, schema_editor):
+ assign_circles_to_profiles()
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("learnpath", "0019_auto_20240730_0904"),
+ ]
+
+ operations = [migrations.RunPython(migrate)]
diff --git a/server/vbv_lernwelt/learnpath/models.py b/server/vbv_lernwelt/learnpath/models.py
index 20d7d517..a0a76f03 100644
--- a/server/vbv_lernwelt/learnpath/models.py
+++ b/server/vbv_lernwelt/learnpath/models.py
@@ -3,6 +3,7 @@ from typing import Tuple
from django.db import models
from django.utils.text import slugify
+from modelcluster.models import ParentalManyToManyField
from wagtail.admin.panels import FieldPanel, PageChooserPanel
from wagtail.fields import RichTextField, StreamField
from wagtail.models import Page
@@ -66,6 +67,25 @@ class Topic(CourseBasePage):
return f"{self.title}"
+class CourseProfile(models.Model):
+ code = models.CharField(max_length=255)
+ order = models.IntegerField(default=999)
+
+ def __str__(self) -> str:
+ return self.code
+
+ class Meta:
+ ordering = [
+ "order",
+ ]
+
+
+class CourseProfileToCircle(models.Model):
+ # this connects the course profile to a circle, because a circle can be in multiple profiles
+ # todo: to we even need a through model?
+ pass
+
+
class Circle(CourseBasePage):
parent_page_types = ["learnpath.LearningPath"]
subpage_types = [
@@ -95,9 +115,25 @@ class Circle(CourseBasePage):
goals = RichTextField(features=DEFAULT_RICH_TEXT_FEATURES_WITH_HEADER)
+ profiles = ParentalManyToManyField(CourseProfile, related_name="circles")
+
+ # base circles do never belong to a course profile and should also get displayed no matter what profile is chosen
+ is_base_circle = models.BooleanField(default=False)
+
+ # profile = models.ForeignKey(
+ # ApprovalProfile,
+ # null=True,
+ # blank=True,
+ # on_delete=models.SET_NULL,
+ # related_name="circles",
+ # help_text="Zulassungsprofil",
+ # )
+
content_panels = Page.content_panels + [
FieldPanel("description"),
FieldPanel("goals"),
+ FieldPanel("is_base_circle"),
+ FieldPanel("profiles"),
]
def get_frontend_url(self):
diff --git a/server/vbv_lernwelt/learnpath/serializers.py b/server/vbv_lernwelt/learnpath/serializers.py
index 3f6a3255..b03a7dbf 100644
--- a/server/vbv_lernwelt/learnpath/serializers.py
+++ b/server/vbv_lernwelt/learnpath/serializers.py
@@ -1,3 +1,4 @@
+from rest_framework import serializers
from rest_framework.fields import SerializerMethodField
from vbv_lernwelt.competence.serializers import (
@@ -6,6 +7,7 @@ from vbv_lernwelt.competence.serializers import (
from vbv_lernwelt.core.utils import get_django_content_type
from vbv_lernwelt.course.serializer_helpers import get_course_serializer_class
from vbv_lernwelt.learnpath.models import (
+ CourseProfile,
LearningContentAssignment,
LearningContentEdoniqTest,
LearningUnit,
@@ -98,3 +100,9 @@ class LearningContentAssignmentSerializer(
}
except Exception:
return None
+
+
+class CourseProfileSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = CourseProfile
+ fields = ["id", "code"]
diff --git a/server/vbv_lernwelt/media_files/migrations/0002_alter_contentimagerendition_file_and_more.py b/server/vbv_lernwelt/media_files/migrations/0002_alter_contentimagerendition_file_and_more.py
new file mode 100644
index 00000000..bf01ce1c
--- /dev/null
+++ b/server/vbv_lernwelt/media_files/migrations/0002_alter_contentimagerendition_file_and_more.py
@@ -0,0 +1,33 @@
+# Generated by Django 4.2.13 on 2024-07-22 19:45
+
+import wagtail.images.models
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("media_files", "0001_initial"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="contentimagerendition",
+ name="file",
+ field=wagtail.images.models.WagtailImageField(
+ height_field="height",
+ storage=wagtail.images.models.get_rendition_storage,
+ upload_to=wagtail.images.models.get_rendition_upload_to,
+ width_field="width",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="userimagerendition",
+ name="file",
+ field=wagtail.images.models.WagtailImageField(
+ height_field="height",
+ storage=wagtail.images.models.get_rendition_storage,
+ upload_to=wagtail.images.models.get_rendition_upload_to,
+ width_field="width",
+ ),
+ ),
+ ]
diff --git a/server/vbv_lernwelt/shop/migrations/0016_alter_checkoutinformation_refno2.py b/server/vbv_lernwelt/shop/migrations/0016_alter_checkoutinformation_refno2.py
new file mode 100644
index 00000000..847d190c
--- /dev/null
+++ b/server/vbv_lernwelt/shop/migrations/0016_alter_checkoutinformation_refno2.py
@@ -0,0 +1,17 @@
+# Generated by Django 3.2.20 on 2024-07-11 15:36
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("shop", "0015_cembra_fields"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="checkoutinformation",
+ name="refno2",
+ field=models.CharField(max_length=255),
+ ),
+ ]
diff --git a/server/vbv_lernwelt/shop/migrations/0017_checkoutinformation_chosen_profile.py b/server/vbv_lernwelt/shop/migrations/0017_checkoutinformation_chosen_profile.py
new file mode 100644
index 00000000..091915d3
--- /dev/null
+++ b/server/vbv_lernwelt/shop/migrations/0017_checkoutinformation_chosen_profile.py
@@ -0,0 +1,27 @@
+# Generated by Django 4.2.13 on 2024-07-30 07:03
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ (
+ "learnpath",
+ "0018_alter_courseprofile_options_circle_is_base_circle_and_more",
+ ),
+ ("shop", "0016_alter_checkoutinformation_refno2"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="checkoutinformation",
+ name="chosen_profile",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to="learnpath.courseprofile",
+ ),
+ ),
+ ]
diff --git a/server/vbv_lernwelt/shop/models.py b/server/vbv_lernwelt/shop/models.py
index 04694fc6..5a6da1ac 100644
--- a/server/vbv_lernwelt/shop/models.py
+++ b/server/vbv_lernwelt/shop/models.py
@@ -106,7 +106,9 @@ class CheckoutInformation(models.Model):
null=True,
blank=True,
)
-
+ chosen_profile = models.ForeignKey(
+ "learnpath.CourseProfile", on_delete=models.SET_NULL, null=True, blank=True
+ )
# webhook metadata
webhook_history = models.JSONField(default=list)
diff --git a/server/vbv_lernwelt/shop/tests/test_checkout_api.py b/server/vbv_lernwelt/shop/tests/test_checkout_api.py
index f5f46b37..23a8bac2 100644
--- a/server/vbv_lernwelt/shop/tests/test_checkout_api.py
+++ b/server/vbv_lernwelt/shop/tests/test_checkout_api.py
@@ -6,6 +6,8 @@ from rest_framework.test import APITestCase
from vbv_lernwelt.core.admin import User
from vbv_lernwelt.core.model_utils import add_countries
+from vbv_lernwelt.learnpath.consts import COURSE_PROFILE_ALL_CODE, COURSE_PROFILE_ALL_ID
+from vbv_lernwelt.learnpath.models import CourseProfile
from vbv_lernwelt.shop.const import VV_DE_PRODUCT_SKU
from vbv_lernwelt.shop.models import CheckoutInformation, CheckoutState, Product
from vbv_lernwelt.shop.services import InitTransactionException
@@ -50,6 +52,10 @@ class CheckoutAPITestCase(APITestCase):
is_active=True,
)
+ CourseProfile.objects.get_or_create(
+ id=COURSE_PROFILE_ALL_ID, code=COURSE_PROFILE_ALL_CODE
+ )
+
self.client.login(username=USER_USERNAME, password=USER_PASSWORD)
add_countries(small_set=True)
diff --git a/server/vbv_lernwelt/shop/views.py b/server/vbv_lernwelt/shop/views.py
index a245520a..58591f49 100644
--- a/server/vbv_lernwelt/shop/views.py
+++ b/server/vbv_lernwelt/shop/views.py
@@ -8,6 +8,8 @@ from rest_framework.permissions import IsAuthenticated
from sentry_sdk import capture_exception
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
+from vbv_lernwelt.learnpath.consts import COURSE_PROFILE_ALL_ID
+from vbv_lernwelt.learnpath.models import CourseProfile
from vbv_lernwelt.notify.email.email_services import EmailTemplate, send_email
from vbv_lernwelt.shop.const import (
VV_DE_PRODUCT_SKU,
@@ -92,6 +94,7 @@ def checkout_vv(request):
sku = request.data["product"]
base_redirect_url = request.data["redirect_url"]
+ chosen_profile_id = request.data.get("chosen_profile", COURSE_PROFILE_ALL_ID)
log.info("Checkout requested: sku", user_id=request.user.id, sku=sku)
@@ -106,6 +109,11 @@ def checkout_vv(request):
),
)
+ try:
+ chosen_profile = CourseProfile.objects.get(id=chosen_profile_id)
+ except CourseProfile.DoesNotExist:
+ chosen_profile = CourseProfile.objects.get(id=COURSE_PROFILE_ALL_ID)
+
checkouts = CheckoutInformation.objects.filter(
user=request.user,
product_sku=sku,
@@ -151,6 +159,7 @@ def checkout_vv(request):
"device_fingerprint_session_key", ""
),
# address
+ chosen_profile=chosen_profile,
**request.data["address"],
)
@@ -257,9 +266,11 @@ def create_vv_course_session_user(checkout_info: CheckoutInformation):
_, created = CourseSessionUser.objects.get_or_create(
user=checkout_info.user,
role=CourseSessionUser.Role.MEMBER,
+ chosen_profile=checkout_info.chosen_profile,
course_session=CourseSession.objects.get(
id=PRODUCT_SKU_TO_COURSE_SESSION_ID[checkout_info.product_sku]
),
+ # chosen_profile=bla,
)
if created: