diff --git a/.git-crypt/keys/default/0/70D75BE89768BB8DD5609A186CC392238DFE4008.gpg b/.git-crypt/keys/default/0/70D75BE89768BB8DD5609A186CC392238DFE4008.gpg new file mode 100644 index 00000000..7329d5db Binary files /dev/null and b/.git-crypt/keys/default/0/70D75BE89768BB8DD5609A186CC392238DFE4008.gpg differ diff --git a/.git-crypt/keys/default/0/72A3BBE62AD447B5A598326B07671799C2175B8A.gpg b/.git-crypt/keys/default/0/72A3BBE62AD447B5A598326B07671799C2175B8A.gpg index db281e85..b30ef07b 100644 Binary files a/.git-crypt/keys/default/0/72A3BBE62AD447B5A598326B07671799C2175B8A.gpg and b/.git-crypt/keys/default/0/72A3BBE62AD447B5A598326B07671799C2175B8A.gpg differ diff --git a/client/src/pages/cockpit/FeedbackPage.vue b/client/src/pages/cockpit/FeedbackPage.vue index 0267d5f4..e48dc6d4 100644 --- a/client/src/pages/cockpit/FeedbackPage.vue +++ b/client/src/pages/cockpit/FeedbackPage.vue @@ -11,7 +11,18 @@
-

{{ $t("feedback.feedbackPageTitle") }}

+
+

{{ $t("feedback.feedbackPageTitle") }}

+ +

{{ feedbackData.amount }} @@ -37,6 +48,9 @@ import type { FeedbackData, FeedbackType } from "@/types"; import FeedbackPageVV from "@/pages/cockpit/FeedbackPageVV.vue"; import FeedbackPageUK from "@/pages/cockpit/FeedbackPageUK.vue"; import { useExpertCockpitPageData } from "@/pages/cockpit/cockpitPage/composables"; +import { exportFeedback } from "@/services/dashboard"; +import { useUserStore } from "@/stores/user"; +import { openDataAsXls } from "@/utils/export"; const props = defineProps<{ courseSlug: string; @@ -46,9 +60,22 @@ const props = defineProps<{ log.debug("FeedbackPage created", props.circleId); const { loading } = useExpertCockpitPageData(props.courseSlug); const courseSession = useCurrentCourseSession(); +const userStore = useUserStore(); + const feedbackData = ref(undefined); const feedbackType = ref(undefined); +async function exportData() { + const data = await exportFeedback( + { + courseSessionIds: [Number(courseSession.value.id)], + circleIds: [Number(props.circleId)], + }, + userStore.language + ); + openDataAsXls(data.encoded_data, data.file_name); +} + onMounted(async () => { log.debug("FeedbackPage mounted"); feedbackData.value = await itGet( diff --git a/client/src/pages/cockpit/attendanceCheckPage/AttendanceCheckPage.vue b/client/src/pages/cockpit/attendanceCheckPage/AttendanceCheckPage.vue index 502c67b4..27460184 100644 --- a/client/src/pages/cockpit/attendanceCheckPage/AttendanceCheckPage.vue +++ b/client/src/pages/cockpit/attendanceCheckPage/AttendanceCheckPage.vue @@ -2,7 +2,7 @@ import ItCheckbox from "@/components/ui/ItCheckbox.vue"; import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue"; import ItPersonRow from "@/components/ui/ItPersonRow.vue"; -import { useCourseSessionDetailQuery } from "@/composables"; +import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables"; import type { AttendanceUserStatus } from "@/gql/graphql"; import { ATTENDANCE_CHECK_MUTATION } from "@/graphql/mutations"; import type { DropdownSelectable } from "@/types"; @@ -13,10 +13,15 @@ import { computed, onMounted, reactive, watch } from "vue"; import { useTranslation } from "i18next-vue"; import { ATTENDANCE_CHECK_QUERY } from "@/graphql/queries"; import { graphqlClient } from "@/graphql/client"; +import { exportAttendance } from "@/services/dashboard"; +import { openDataAsXls } from "@/utils/export"; +import { useUserStore } from "@/stores/user"; const { t } = useTranslation(); const attendanceMutation = useMutation(ATTENDANCE_CHECK_MUTATION); const courseSessionDetailResult = useCourseSessionDetailQuery(); +const userStore = useUserStore(); +const courseSession = useCurrentCourseSession(); const attendanceCourses = computed(() => { return courseSessionDetailResult.courseSessionDetail.value?.attendance_courses ?? []; @@ -26,6 +31,13 @@ const courseSessionDetail = computed(() => { return courseSessionDetailResult.courseSessionDetail.value; }); +const attendanceCourseCircleId = computed(() => { + const selectedAttendandeCourse = attendanceCourses.value.find( + (course) => course.id === state.attendanceCourseSelected.id + ); + return selectedAttendandeCourse?.learning_content?.circle?.id; +}); + const presenceCoursesDropdownOptions = computed(() => { return attendanceCourses.value.map( (attendanceCourse) => @@ -114,6 +126,17 @@ function editAgain() { state.attendanceSaved = false; } +async function exportData() { + const data = await exportAttendance( + { + courseSessionIds: [Number(courseSession.value.id)], + circleIds: [Number(attendanceCourseCircleId.value)], + }, + userStore.language + ); + openDataAsXls(data.encoded_data, data.file_name); +} + onMounted(() => { log.debug("AttendanceCheckPage mounted"); loadAttendanceData(); @@ -141,8 +164,18 @@ watch( {{ $t("general.back") }} -

{{ $t("Anwesenheit Präsenzkurse") }}
- +
+

{{ $t("Anwesenheit Präsenzkurse") }}

+ +
andreas.feuz@eiger-versicherungen.ch" in debi_content order_filepath = os.path.join( - tmppath, "order/myVBV_orde_60000012_20240215083312_6000000124.xml" + tmppath, "order/myVBV_orde_20240215083312_60000012_6000000124.xml" ) assert os.path.exists(order_filepath) with open(order_filepath) as order_file: diff --git a/server/vbv_lernwelt/dashboard/tests/test_views.py b/server/vbv_lernwelt/dashboard/tests/test_views.py index c98d5ae1..277bbf5b 100644 --- a/server/vbv_lernwelt/dashboard/tests/test_views.py +++ b/server/vbv_lernwelt/dashboard/tests/test_views.py @@ -1,4 +1,6 @@ from django.test import TestCase +from django.urls import reverse +from rest_framework.test import APITestCase from vbv_lernwelt.assignment.models import ( Assignment, @@ -9,7 +11,11 @@ from vbv_lernwelt.core.constants import ( TEST_COURSE_SESSION_BERN_ID, TEST_COURSE_SESSION_ZURICH_ID, TEST_STUDENT1_USER_ID, + TEST_STUDENT2_USER_ID, + TEST_STUDENT3_USER_ID, TEST_SUPERVISOR1_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 @@ -541,3 +547,79 @@ class ExportXlsTestCase(TestCase): [(TEST_COURSE_SESSION_ZURICH_ID, [circle_fahrzeug.id, circle_reisen.id])], allowed_circles, ) + + +class PersonsTestCase(APITestCase): + def setUp(self): + create_default_users() + create_test_course(include_uk=True, include_vv=False, with_sessions=True) + + self.student1 = User.objects.get(id=TEST_STUDENT1_USER_ID) + self.csu1_student1 = CourseSessionUser.objects.get( + user=self.student1, course_session__id=TEST_COURSE_SESSION_BERN_ID + ) + self.student2 = User.objects.get(id=TEST_STUDENT2_USER_ID) + self.csu1_student2 = CourseSessionUser.objects.get( + user=self.student2, course_session__id=TEST_COURSE_SESSION_ZURICH_ID + ) + + def test_get_course_sessions_with_roles_for_trainer(self): + trainer = User.objects.get(id=TEST_TRAINER1_USER_ID) + self.client.force_login(trainer) + + url = reverse( + "get_dashboard_persons", + ) + + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data), 4) + user_ids = [str(user["user_id"]) for user in response.data] + self.assertCountEqual( + user_ids, + [ + TEST_TRAINER1_USER_ID, + TEST_STUDENT1_USER_ID, + TEST_STUDENT2_USER_ID, + TEST_STUDENT3_USER_ID, + ], + ) + user1_index = user_ids.index(TEST_STUDENT1_USER_ID) + self.assertEqual( + response.data[user1_index]["course_sessions"][0]["id"], + str(CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID).id), + ) + + def test_get_course_sessions_with_roles_for_supervisor(self): + supervisor = User.objects.get(id=TEST_SUPERVISOR1_USER_ID) + self.client.force_login(supervisor) + + url = reverse( + "get_dashboard_persons", + ) + + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data), 5) + user_ids = [str(user["user_id"]) for user in response.data] + self.assertCountEqual( + user_ids, + [ + TEST_TRAINER1_USER_ID, + TEST_STUDENT1_USER_ID, + TEST_STUDENT2_USER_ID, + TEST_STUDENT3_USER_ID, + TEST_TRAINER2_USER_ID, + ], + ) + user2_index = user_ids.index(TEST_STUDENT2_USER_ID) + user2_cs = response.data[user2_index]["course_sessions"] + self.assertEqual(len(user2_cs), 2) + + user2_cs_ids = [cs["id"] for cs in user2_cs] + self.assertCountEqual( + user2_cs_ids, + [str(TEST_COURSE_SESSION_ZURICH_ID), str(TEST_COURSE_SESSION_BERN_ID)], + ) diff --git a/server/vbv_lernwelt/dashboard/views.py b/server/vbv_lernwelt/dashboard/views.py index 33962032..1cdf533a 100644 --- a/server/vbv_lernwelt/dashboard/views.py +++ b/server/vbv_lernwelt/dashboard/views.py @@ -173,6 +173,7 @@ def _create_person_list_with_roles(user): "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) @@ -185,10 +186,12 @@ def _create_person_list_with_roles(user): ) my_role = user_role(cs.roles) for csu in course_session_users: - person_data = create_user_dict(csu.user) - person_data["course_sessions"] = [ + 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 diff --git a/server/vbv_lernwelt/importer/services.py b/server/vbv_lernwelt/importer/services.py index 4abc1403..37090dca 100644 --- a/server/vbv_lernwelt/importer/services.py +++ b/server/vbv_lernwelt/importer/services.py @@ -847,6 +847,10 @@ def create_or_update_trainer(course: Course, data: Dict[str, Any], language="de" init_notification_settings(user) user.save() + # As the is never set this is the only way to determine the correct course + if user.language != language: + language = user.language + group = data["Klasse"].strip() # general expert handling @@ -884,7 +888,7 @@ def create_or_update_trainer(course: Course, data: Dict[str, Any], language="de" # circle expert handling circle_data = parse_circle_group_string(data["Circles"]) for circle_key in circle_data: - circle_name = LP_DATA[circle_key][language]["title"] + circle_slug = LP_DATA[circle_key][language]["slug"] # print(circle_name, groups) import_id = f"{data['Generation'].strip()} {group}" @@ -892,7 +896,7 @@ def create_or_update_trainer(course: Course, data: Dict[str, Any], language="de" import_id=import_id, group=group ).first() circle = Circle.objects.filter( - slug=f"{course.slug}-lp-circle-{circle_name.lower()}" + slug=f"{course.slug}-lp-circle-{circle_slug}" ).first() if course_session and circle: diff --git a/server/vbv_lernwelt/shop/invoice/abacus.py b/server/vbv_lernwelt/shop/invoice/abacus.py index def231ef..543f9fdd 100644 --- a/server/vbv_lernwelt/shop/invoice/abacus.py +++ b/server/vbv_lernwelt/shop/invoice/abacus.py @@ -63,7 +63,7 @@ def create_invoice_xml(checkout_information: CheckoutInformation): # YYYYMMDDhhmmss filename_datetime = checkout_information.created_at.strftime("%Y%m%d%H%M%S") - invoice_xml_filename = f"myVBV_orde_{customer.abacus_debitor_number}_{filename_datetime}_{checkout_information.abacus_order_id}.xml" + invoice_xml_filename = f"myVBV_orde_{filename_datetime}_{customer.abacus_debitor_number}_{checkout_information.abacus_order_id}.xml" return invoice_xml_filename, invoice_xml_content @@ -207,17 +207,20 @@ def render_customer_xml( address_data = SubElement(customer_element, "AddressData", mode="SAVE") SubElement(address_data, "AddressNumber").text = str(abacus_debitor_number) - SubElement(address_data, "Name").text = last_name - SubElement(address_data, "FirstName").text = first_name + SubElement(address_data, "Name").text = last_name[:100] + SubElement(address_data, "FirstName").text = first_name[:50] if company_name: - SubElement(address_data, "Text").text = company_name - SubElement(address_data, "Street").text = street - SubElement(address_data, "HouseNumber").text = house_number - SubElement(address_data, "ZIP").text = zip_code - SubElement(address_data, "City").text = city - SubElement(address_data, "Country").text = country - SubElement(address_data, "Language").text = language - SubElement(address_data, "Email").text = email + SubElement(address_data, "Text").text = company_name[:80] + SubElement(address_data, "Street").text = street[:50] + SubElement(address_data, "HouseNumber").text = house_number[:9] + # only take the numbers from zip_code + SubElement(address_data, "ZIP").text = "".join( + filter(lambda ch: str.isdigit(ch), zip_code) + )[:15] + SubElement(address_data, "City").text = city[:50] + SubElement(address_data, "Country").text = country[:4] + SubElement(address_data, "Language").text = language[:6] + SubElement(address_data, "Email").text = email[:65] return create_xml_string(container) diff --git a/server/vbv_lernwelt/shop/tests/test_abacus_invoice.py b/server/vbv_lernwelt/shop/tests/test_abacus_invoice.py index f9e5e171..e7171ba0 100644 --- a/server/vbv_lernwelt/shop/tests/test_abacus_invoice.py +++ b/server/vbv_lernwelt/shop/tests/test_abacus_invoice.py @@ -43,7 +43,7 @@ class AbacusInvoiceTestCase(TestCase): ) self.assertEqual( - invoice_xml_filename, "myVBV_orde_60000012_20240215083312_6000000124.xml" + invoice_xml_filename, "myVBV_orde_20240215083312_60000012_6000000124.xml" ) print(invoice_xml_content)