Merge branch 'develop' into feature/VBV-496-dash-regionalleiter
This commit is contained in:
commit
e6aa6f0cbe
|
|
@ -1,4 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import RichText from "@/components/ui/RichText.vue";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
|
@ -18,8 +20,7 @@ withDefaults(defineProps<Props>(), {
|
||||||
<div class="flex flex-col justify-between bg-white p-8 pb-4 lg:flex-row lg:pb-8">
|
<div class="flex flex-col justify-between bg-white p-8 pb-4 lg:flex-row lg:pb-8">
|
||||||
<div class="mb-4 lg:mb-0">
|
<div class="mb-4 lg:mb-0">
|
||||||
<h3 class="mb-4">{{ title }}</h3>
|
<h3 class="mb-4">{{ title }}</h3>
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
<RichText class="mb-4" :content="description" />
|
||||||
<p class="default-wagtail-rich-text mb-4" v-html="description"></p>
|
|
||||||
<a
|
<a
|
||||||
v-if="externalLink"
|
v-if="externalLink"
|
||||||
:href="link"
|
:href="link"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { setExternalLinksToOpenInNewTab } from "@/utils/dom";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
content?: string;
|
||||||
|
openLinksInNewTab?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
content: "",
|
||||||
|
openLinksInNewTab: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedContent = computed(() => {
|
||||||
|
if (!props.openLinksInNewTab || !props.content) return props.content;
|
||||||
|
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(props.content, "text/html");
|
||||||
|
const anchors = doc.querySelectorAll("a");
|
||||||
|
|
||||||
|
setExternalLinksToOpenInNewTab(anchors);
|
||||||
|
|
||||||
|
return doc.body.innerHTML;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
|
<section class="default-wagtail-rich-text" v-html="updatedContent"></section>
|
||||||
|
</template>
|
||||||
|
|
@ -16,6 +16,7 @@ import { useMutation } from "@urql/vue";
|
||||||
import dayjs, { Dayjs } from "dayjs";
|
import dayjs, { Dayjs } from "dayjs";
|
||||||
import * as log from "loglevel";
|
import * as log from "loglevel";
|
||||||
import { computed, reactive } from "vue";
|
import { computed, reactive } from "vue";
|
||||||
|
import RichText from "@/components/ui/RichText.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
assignmentUser: CourseSessionUser;
|
assignmentUser: CourseSessionUser;
|
||||||
|
|
@ -188,10 +189,11 @@ const evaluationUser = computed(() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<RichText
|
||||||
class="default-wagtail-rich-text mb-2 font-bold"
|
class="mb-2 font-bold"
|
||||||
v-html="task.value.description"
|
:content="task.value.description"
|
||||||
></div>
|
open-links-in-new-tab
|
||||||
|
/>
|
||||||
|
|
||||||
<section class="mb-4">
|
<section class="mb-4">
|
||||||
<div
|
<div
|
||||||
|
|
@ -199,12 +201,14 @@ const evaluationUser = computed(() => {
|
||||||
subTaskByPoints(task, evaluationForTask(task).points)?.value.title
|
subTaskByPoints(task, evaluationForTask(task).points)?.value.title
|
||||||
"
|
"
|
||||||
></div>
|
></div>
|
||||||
<p
|
|
||||||
class="default-wagtail-rich-text"
|
<RichText
|
||||||
v-html="
|
:content="
|
||||||
subTaskByPoints(task, evaluationForTask(task).points)?.value.description
|
subTaskByPoints(task, evaluationForTask(task).points)?.value.description
|
||||||
"
|
"
|
||||||
></p>
|
open-links-in-new-tab
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="text-sm text-gray-800">
|
<div class="text-sm text-gray-800">
|
||||||
{{ evaluationForTask(task).points }} Punkte
|
{{ evaluationForTask(task).points }} Punkte
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { useMutation } from "@urql/vue";
|
||||||
import { useDebounceFn } from "@vueuse/core";
|
import { useDebounceFn } from "@vueuse/core";
|
||||||
import * as log from "loglevel";
|
import * as log from "loglevel";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
import RichText from "@/components/ui/RichText.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
assignmentUser: CourseSessionUser;
|
assignmentUser: CourseSessionUser;
|
||||||
|
|
@ -114,11 +115,10 @@ const evaluateAssignmentCompletionDebounced = useDebounceFn(
|
||||||
/>
|
/>
|
||||||
<label :for="String(index)" class="ml-4 block">
|
<label :for="String(index)" class="ml-4 block">
|
||||||
<div>{{ subTask.value.title }}</div>
|
<div>{{ subTask.value.title }}</div>
|
||||||
<div
|
<RichText
|
||||||
v-if="subTask.value.description"
|
v-if="subTask.value.description"
|
||||||
class="default-wagtail-rich-text"
|
:content="subTask.value.description"
|
||||||
v-html="subTask.value.description"
|
/>
|
||||||
></div>
|
|
||||||
<div class="text-sm text-gray-800">{{ subTask.value.points }} Punkte</div>
|
<div class="text-sm text-gray-800">{{ subTask.value.points }} Punkte</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
|
import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
|
||||||
import type { CircleType } from "@/types";
|
import type { CircleType } from "@/types";
|
||||||
|
import RichText from "@/components/ui/RichText.vue";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
circle: CircleType;
|
circle: CircleType;
|
||||||
|
|
@ -14,13 +15,7 @@ const emit = defineEmits(["closemodal"]);
|
||||||
<ItFullScreenModal :show="show" @closemodal="emit('closemodal')">
|
<ItFullScreenModal :show="show" @closemodal="emit('closemodal')">
|
||||||
<div v-if="circle" class="container-medium">
|
<div v-if="circle" class="container-medium">
|
||||||
<h2 data-cy="lc-title">{{ $t("a.Übersicht") }}: Circle «{{ circle.title }}»</h2>
|
<h2 data-cy="lc-title">{{ $t("a.Übersicht") }}: Circle «{{ circle.title }}»</h2>
|
||||||
|
<RichText v-if="circle.goals" class="my-4" :content="circle.goals" />
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
|
||||||
<div
|
|
||||||
v-if="circle.goals"
|
|
||||||
class="default-wagtail-rich-text my-4"
|
|
||||||
v-html="circle.goals"
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
</ItFullScreenModal>
|
</ItFullScreenModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,13 @@ defineEmits(["exit"]);
|
||||||
<div>
|
<div>
|
||||||
<div class="absolute bottom-0 top-0 w-full bg-white">
|
<div class="absolute bottom-0 top-0 w-full bg-white">
|
||||||
<CoursePreviewBar v-if="courseSessionsStore.hasCourseSessionPreview" />
|
<CoursePreviewBar v-if="courseSessionsStore.hasCourseSessionPreview" />
|
||||||
<div class="h-content overflow-y-auto">
|
<div
|
||||||
|
:class="{
|
||||||
|
'h-content': !courseSessionsStore.hasCourseSessionPreview,
|
||||||
|
'h-content-preview': courseSessionsStore.hasCourseSessionPreview,
|
||||||
|
}"
|
||||||
|
class="overflow-y-auto"
|
||||||
|
>
|
||||||
<header
|
<header
|
||||||
class="relative flex h-12 w-full items-center justify-between bg-white px-4 lg:h-16 lg:px-8"
|
class="relative flex h-12 w-full items-center justify-between bg-white px-4 lg:h-16 lg:px-8"
|
||||||
>
|
>
|
||||||
|
|
@ -34,12 +40,17 @@ defineEmits(["exit"]);
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
$nav-height: 86px;
|
$nav-height: 86px; // navigation height
|
||||||
|
$nav-height-preview: $nav-height + 64px; //navigation height + preview bar height
|
||||||
|
|
||||||
.h-content {
|
.h-content {
|
||||||
height: calc(100vh - $nav-height);
|
height: calc(100vh - $nav-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h-content-preview {
|
||||||
|
height: calc(100vh - $nav-height-preview);
|
||||||
|
}
|
||||||
|
|
||||||
.nav {
|
.nav {
|
||||||
height: $nav-height;
|
height: $nav-height;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import LearningContentParent from "@/pages/learningPath/learningContentPage/LearningContentParent.vue";
|
import LearningContentParent from "@/pages/learningPath/learningContentPage/LearningContentParent.vue";
|
||||||
import * as log from "loglevel";
|
import * as log from "loglevel";
|
||||||
import { computed, getCurrentInstance, onUpdated } from "vue";
|
import { computed } from "vue";
|
||||||
import { useCourseDataWithCompletion } from "@/composables";
|
import { useCourseDataWithCompletion } from "@/composables";
|
||||||
import { stringifyParse } from "@/utils/utils";
|
import { stringifyParse } from "@/utils/utils";
|
||||||
|
|
||||||
|
|
@ -20,24 +20,6 @@ const learningContent = computed(() =>
|
||||||
const circle = computed(() => {
|
const circle = computed(() => {
|
||||||
return courseData.findCircle(props.circleSlug);
|
return courseData.findCircle(props.circleSlug);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUpdated(() => {
|
|
||||||
const vueInstance = getCurrentInstance();
|
|
||||||
if (vueInstance) {
|
|
||||||
// VBV-489: open external links in new tab
|
|
||||||
const rootElement: HTMLElement = vueInstance.proxy?.$el;
|
|
||||||
const anchors = rootElement.querySelectorAll("a");
|
|
||||||
anchors.forEach((anchor: HTMLAnchorElement) => {
|
|
||||||
if (
|
|
||||||
/^https?:\/\//i.test(anchor.href) &&
|
|
||||||
!anchor.href.includes(window.location.hostname)
|
|
||||||
) {
|
|
||||||
anchor.setAttribute("target", "_blank");
|
|
||||||
anchor.setAttribute("rel", "noopener noreferrer");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import type { Assignment } from "@/types";
|
||||||
import { useRouteQuery } from "@vueuse/router";
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import RichText from "@/components/ui/RichText.vue";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assignment: Assignment;
|
assignment: Assignment;
|
||||||
|
|
@ -20,12 +21,12 @@ const step = useRouteQuery("step");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
<RichText
|
||||||
<p
|
|
||||||
v-if="props.assignment.intro_text"
|
v-if="props.assignment.intro_text"
|
||||||
class="default-wagtail-rich-text text-large"
|
class="text-large"
|
||||||
v-html="props.assignment.intro_text"
|
:content="props.assignment.intro_text"
|
||||||
></p>
|
open-links-in-new-tab
|
||||||
|
/>
|
||||||
|
|
||||||
<h3 class="mb-4 mt-8">{{ $t("assignment.taskDefinitionTitle") }}</h3>
|
<h3 class="mb-4 mt-8">{{ $t("assignment.taskDefinitionTitle") }}</h3>
|
||||||
<p class="text-large">
|
<p class="text-large">
|
||||||
|
|
@ -74,11 +75,12 @@ const step = useRouteQuery("step");
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<h3 class="mb-4 mt-8">{{ $t("a.Bewertung") }}</h3>
|
<h3 class="mb-4 mt-8">{{ $t("a.Bewertung") }}</h3>
|
||||||
<p
|
<RichText
|
||||||
v-if="props.assignment.evaluation_description"
|
v-if="props.assignment.evaluation_description"
|
||||||
class="default-wagtail-rich-text text-large"
|
class="text-large"
|
||||||
v-html="props.assignment.evaluation_description"
|
:content="props.assignment.evaluation_description"
|
||||||
></p>
|
open-links-in-new-tab
|
||||||
|
/>
|
||||||
<p v-if="props.assignment.evaluation_document_url">
|
<p v-if="props.assignment.evaluation_document_url">
|
||||||
<a
|
<a
|
||||||
:href="props.assignment.evaluation_document_url"
|
:href="props.assignment.evaluation_document_url"
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import type {
|
||||||
AssignmentTaskCompletionData,
|
AssignmentTaskCompletionData,
|
||||||
UserDataText,
|
UserDataText,
|
||||||
} from "@/types";
|
} from "@/types";
|
||||||
|
import RichText from "@/components/ui/RichText.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
assignment: Assignment;
|
assignment: Assignment;
|
||||||
|
|
@ -19,7 +20,6 @@ const emit = defineEmits<{
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
|
||||||
<div
|
<div
|
||||||
v-for="task in props.assignment.tasks ?? []"
|
v-for="task in props.assignment.tasks ?? []"
|
||||||
:key="task.id"
|
:key="task.id"
|
||||||
|
|
@ -36,10 +36,11 @@ const emit = defineEmits<{
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="taskBlock in task.value.content" :key="taskBlock.id">
|
<div v-for="taskBlock in task.value.content" :key="taskBlock.id">
|
||||||
<p
|
<RichText
|
||||||
class="default-wagtail-rich-text pt-6 text-base font-bold"
|
class="pt-6 text-base font-bold"
|
||||||
v-html="taskBlock.value.text"
|
:content="taskBlock.value.text"
|
||||||
></p>
|
open-links-in-new-tab
|
||||||
|
/>
|
||||||
<p
|
<p
|
||||||
v-if="
|
v-if="
|
||||||
props.assignmentCompletionData &&
|
props.assignmentCompletionData &&
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { useDebounceFn } from "@vueuse/core";
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
import { computed, reactive } from "vue";
|
import { computed, reactive } from "vue";
|
||||||
import AttachmentSection from "@/pages/learningPath/learningContentPage/assignment/AttachmentSection.vue";
|
import AttachmentSection from "@/pages/learningPath/learningContentPage/assignment/AttachmentSection.vue";
|
||||||
|
import RichText from "@/components/ui/RichText.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
assignmentId: string;
|
assignmentId: string;
|
||||||
|
|
@ -123,7 +124,11 @@ const taskFileInfo = computed(() => {
|
||||||
<div class="flex flex-col space-y-10">
|
<div class="flex flex-col space-y-10">
|
||||||
<div v-for="(block, index) in props.task.value.content" :key="block.id">
|
<div v-for="(block, index) in props.task.value.content" :key="block.id">
|
||||||
<div v-if="block.type === 'explanation'">
|
<div v-if="block.type === 'explanation'">
|
||||||
<p class="default-wagtail-rich-text text-large" v-html="block.value.text"></p>
|
<RichText
|
||||||
|
class="text-large"
|
||||||
|
:content="block.value.text"
|
||||||
|
open-links-in-new-tab
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="block.type === 'user_confirmation'">
|
<div v-if="block.type === 'user_confirmation'">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import LearningContentSimpleLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentSimpleLayout.vue";
|
import LearningContentSimpleLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentSimpleLayout.vue";
|
||||||
import type { LearningContent } from "@/types";
|
import type { LearningContent } from "@/types";
|
||||||
|
import RichText from "@/components/ui/RichText.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
content: LearningContent;
|
content: LearningContent;
|
||||||
|
|
@ -12,13 +13,12 @@ const props = defineProps<{
|
||||||
:title="props.content.title"
|
:title="props.content.title"
|
||||||
:learning-content="props.content"
|
:learning-content="props.content"
|
||||||
>
|
>
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
|
||||||
<div class="container-medium">
|
<div class="container-medium">
|
||||||
<p
|
<RichText
|
||||||
v-if="props.content.description"
|
v-if="props.content.description"
|
||||||
class="default-wagtail-rich-text my-4"
|
class="my-4"
|
||||||
v-html="props.content.description"
|
:content="props.content.description"
|
||||||
></p>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</LearningContentSimpleLayout>
|
</LearningContentSimpleLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import LearningContentSimpleLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentSimpleLayout.vue";
|
import LearningContentSimpleLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentSimpleLayout.vue";
|
||||||
import type { LearningContentRichText } from "@/types";
|
import type { LearningContentRichText } from "@/types";
|
||||||
|
import RichText from "@/components/ui/RichText.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
content: LearningContentRichText;
|
content: LearningContentRichText;
|
||||||
|
|
@ -14,17 +15,13 @@ const props = defineProps<{
|
||||||
>
|
>
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
<div class="container-medium">
|
<div class="container-medium">
|
||||||
<p
|
<RichText
|
||||||
v-if="props.content.description"
|
v-if="props.content.description"
|
||||||
class="default-wagtail-rich-text my-4"
|
class="my-4"
|
||||||
v-html="props.content.description"
|
:content="props.content.description"
|
||||||
></p>
|
/>
|
||||||
|
|
||||||
<div
|
<RichText v-if="props.content.text" class="my-4" :content="props.content.text" />
|
||||||
v-if="props.content.text"
|
|
||||||
class="default-wagtail-rich-text my-4"
|
|
||||||
v-html="props.content.text"
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
</LearningContentSimpleLayout>
|
</LearningContentSimpleLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { useMediaLibraryStore } from "@/stores/mediaLibrary";
|
||||||
import * as log from "loglevel";
|
import * as log from "loglevel";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import type { MediaLibraryCategoryPage, MediaLibraryContentPage } from "@/types";
|
import type { MediaLibraryCategoryPage, MediaLibraryContentPage } from "@/types";
|
||||||
|
import RichText from "@/components/ui/RichText.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
categorySlug: string;
|
categorySlug: string;
|
||||||
|
|
@ -47,11 +48,11 @@ const mediaCategory = computed(() => {
|
||||||
<div class="flex justify-between md:flex-col lg:flex-row">
|
<div class="flex justify-between md:flex-col lg:flex-row">
|
||||||
<div class="lg:w-6/12">
|
<div class="lg:w-6/12">
|
||||||
<h1 class="mb-4 lg:mb-8" data-cy="hf-title">{{ mediaCategory.title }}</h1>
|
<h1 class="mb-4 lg:mb-8" data-cy="hf-title">{{ mediaCategory.title }}</h1>
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
<RichText
|
||||||
<p
|
class="text-large"
|
||||||
class="default-wagtail-rich-text text-large"
|
:content="mediaCategory.description"
|
||||||
v-html="mediaCategory.description"
|
open-links-in-new-tab
|
||||||
></p>
|
></RichText>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<img
|
<img
|
||||||
|
|
@ -64,11 +65,11 @@ const mediaCategory = computed(() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container-large">
|
<div class="container-large">
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
<RichText
|
||||||
<section
|
class="mb-20 mt-8 lg:w-2/3"
|
||||||
class="default-wagtail-rich-text mb-20 mt-8 lg:w-2/3"
|
:content="mediaCategory.body"
|
||||||
v-html="mediaCategory.body"
|
open-links-in-new-tab
|
||||||
></section>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
export const setExternalLinksToOpenInNewTab = (
|
||||||
|
anchors: NodeListOf<HTMLAnchorElement>
|
||||||
|
) => {
|
||||||
|
const httpRegex = /^https?:\/\//i;
|
||||||
|
anchors.forEach((anchor: HTMLAnchorElement) => {
|
||||||
|
if (
|
||||||
|
httpRegex.test(anchor.href) &&
|
||||||
|
!anchor.href.includes(window.location.hostname)
|
||||||
|
) {
|
||||||
|
anchor.setAttribute("target", "_blank");
|
||||||
|
anchor.setAttribute("rel", "noopener noreferrer");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -20,6 +20,7 @@ DEFAULT_RICH_TEXT_FEATURES_WITH_HEADER = [
|
||||||
ADMIN_USER_ID = "872efd96-3bd7-4a1e-a239-2d72cad9f604"
|
ADMIN_USER_ID = "872efd96-3bd7-4a1e-a239-2d72cad9f604"
|
||||||
TEST_SUPERVISOR1_USER_ID = "a9a8b741-f115-4521-af2d-7dfef673b8c5"
|
TEST_SUPERVISOR1_USER_ID = "a9a8b741-f115-4521-af2d-7dfef673b8c5"
|
||||||
TEST_TRAINER1_USER_ID = "b9e71f59-c44f-4290-b93a-9b3151e9a2fc"
|
TEST_TRAINER1_USER_ID = "b9e71f59-c44f-4290-b93a-9b3151e9a2fc"
|
||||||
|
TEST_TRAINER2_USER_ID = "299941ae-1e4b-4f45-8180-876c3ad340b4"
|
||||||
TEST_STUDENT1_USER_ID = "65c73ad0-6d53-43a9-a4a4-64143f27b03a"
|
TEST_STUDENT1_USER_ID = "65c73ad0-6d53-43a9-a4a4-64143f27b03a"
|
||||||
TEST_STUDENT2_USER_ID = "19c40d94-15cc-4198-aaad-ef707c4b0900"
|
TEST_STUDENT2_USER_ID = "19c40d94-15cc-4198-aaad-ef707c4b0900"
|
||||||
TEST_STUDENT3_USER_ID = "bcf94dba-53bc-474b-a22d-e4af39aa042b"
|
TEST_STUDENT3_USER_ID = "bcf94dba-53bc-474b-a22d-e4af39aa042b"
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ from vbv_lernwelt.core.constants import (
|
||||||
TEST_STUDENT3_USER_ID,
|
TEST_STUDENT3_USER_ID,
|
||||||
TEST_SUPERVISOR1_USER_ID,
|
TEST_SUPERVISOR1_USER_ID,
|
||||||
TEST_TRAINER1_USER_ID,
|
TEST_TRAINER1_USER_ID,
|
||||||
|
TEST_TRAINER2_USER_ID,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.core.models import User
|
from vbv_lernwelt.core.models import User
|
||||||
|
|
||||||
|
|
@ -299,6 +300,13 @@ def create_default_users(default_password="test"):
|
||||||
last_name="Trainer1",
|
last_name="Trainer1",
|
||||||
avatar_url="/static/avatars/uk1.patrizia.huggel.jpg",
|
avatar_url="/static/avatars/uk1.patrizia.huggel.jpg",
|
||||||
)
|
)
|
||||||
|
_create_student_user(
|
||||||
|
id=TEST_TRAINER2_USER_ID,
|
||||||
|
email="test-trainer2@example.com",
|
||||||
|
first_name="Test",
|
||||||
|
last_name="Trainer2",
|
||||||
|
avatar_url="/static/avatars/uk1.christian.koller.jpg",
|
||||||
|
)
|
||||||
_create_student_user(
|
_create_student_user(
|
||||||
id=TEST_STUDENT1_USER_ID,
|
id=TEST_STUDENT1_USER_ID,
|
||||||
email="test-student1@example.com",
|
email="test-student1@example.com",
|
||||||
|
|
|
||||||
|
|
@ -213,6 +213,14 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
|
||||||
)
|
)
|
||||||
csu.expert.add(Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug"))
|
csu.expert.add(Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug"))
|
||||||
|
|
||||||
|
trainer2 = User.objects.get(email="test-trainer2@example.com")
|
||||||
|
csu = CourseSessionUser.objects.create(
|
||||||
|
course_session=cs_zurich,
|
||||||
|
user=trainer2,
|
||||||
|
role=CourseSessionUser.Role.EXPERT,
|
||||||
|
)
|
||||||
|
csu.expert.add(Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug"))
|
||||||
|
|
||||||
CourseSessionUser.objects.create(
|
CourseSessionUser.objects.create(
|
||||||
course_session=cs_bern, user=User.objects.get(id=TEST_STUDENT1_USER_ID)
|
course_session=cs_bern, user=User.objects.get(id=TEST_STUDENT1_USER_ID)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,23 @@ from django.test import TestCase
|
||||||
from vbv_lernwelt.core.constants import TEST_STUDENT1_USER_ID
|
from vbv_lernwelt.core.constants import TEST_STUDENT1_USER_ID
|
||||||
from vbv_lernwelt.core.create_default_users import create_default_users
|
from vbv_lernwelt.core.create_default_users import create_default_users
|
||||||
from vbv_lernwelt.core.models import User
|
from vbv_lernwelt.core.models import User
|
||||||
from vbv_lernwelt.course.creators.test_course import create_test_course
|
from vbv_lernwelt.course.creators.test_course import (
|
||||||
from vbv_lernwelt.course.models import CourseCompletion, CourseSession
|
create_test_course,
|
||||||
|
create_test_uk_circle_fahrzeug,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.course.models import (
|
||||||
|
CourseCompletion,
|
||||||
|
CourseSession,
|
||||||
|
CourseSessionUser,
|
||||||
|
)
|
||||||
from vbv_lernwelt.course.services import mark_course_completion
|
from vbv_lernwelt.course.services import mark_course_completion
|
||||||
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
|
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
|
||||||
from vbv_lernwelt.course_session.services.attendance import (
|
from vbv_lernwelt.course_session.services.attendance import (
|
||||||
AttendanceUserStatus,
|
AttendanceUserStatus,
|
||||||
update_attendance_list,
|
update_attendance_list,
|
||||||
)
|
)
|
||||||
|
from vbv_lernwelt.learnpath.models import Circle, LearningPath
|
||||||
|
from vbv_lernwelt.notify.email.reminders.attendance import get_recipients
|
||||||
|
|
||||||
|
|
||||||
class AttendanceServicesTestCase(TestCase):
|
class AttendanceServicesTestCase(TestCase):
|
||||||
|
|
@ -88,3 +97,36 @@ class AttendanceServicesTestCase(TestCase):
|
||||||
self.assertEqual(cc.user, student)
|
self.assertEqual(cc.user, student)
|
||||||
self.assertEqual(cc.completion_status, "FAIL")
|
self.assertEqual(cc.completion_status, "FAIL")
|
||||||
self.assertEqual(cc.page_id, self.attendance_course.learning_content.id)
|
self.assertEqual(cc.page_id, self.attendance_course.learning_content.id)
|
||||||
|
|
||||||
|
|
||||||
|
class AttendanceReminderTestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
create_default_users()
|
||||||
|
create_test_course(include_vv=False, with_sessions=True)
|
||||||
|
self.course_session = CourseSession.objects.get(title="Test Bern 2022 a")
|
||||||
|
self.attendance_course = (
|
||||||
|
self.course_session.coursesessionattendancecourse_set.first()
|
||||||
|
)
|
||||||
|
self.trainer = User.objects.get(username="test-trainer1@example.com")
|
||||||
|
self.other_circle_title = "Something different"
|
||||||
|
lp = LearningPath.objects.get(title="Test Lernpfad")
|
||||||
|
create_test_uk_circle_fahrzeug(lp, title=self.other_circle_title)
|
||||||
|
csu = CourseSessionUser.objects.get(user=self.trainer)
|
||||||
|
fahrzeug = Circle.objects.get(title="Fahrzeug")
|
||||||
|
csu.expert.add(fahrzeug)
|
||||||
|
|
||||||
|
def test_reminderOnlySendsToMembersAndRelevantExperts(self):
|
||||||
|
# promote user to expert, but in another circle
|
||||||
|
csu = CourseSessionUser.objects.get(user__email="test-student3@example.com")
|
||||||
|
other_circle = Circle.objects.get(title=self.other_circle_title)
|
||||||
|
csu.role = CourseSessionUser.Role.EXPERT
|
||||||
|
csu.save()
|
||||||
|
csu.expert.add(other_circle)
|
||||||
|
|
||||||
|
expected_csu = {
|
||||||
|
"test-student1@example.com",
|
||||||
|
"test-student2@example.com",
|
||||||
|
"test-trainer1@example.com",
|
||||||
|
}
|
||||||
|
csus = get_recipients(self.attendance_course)
|
||||||
|
self.assertEqual(set([u.user.email for u in csus]), expected_csu)
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ class EdoniqUserExportTestCase(TestCase):
|
||||||
users = fetch_course_session_users(
|
users = fetch_course_session_users(
|
||||||
[COURSE_TEST_ID], role=CourseSessionUser.Role.EXPERT, excluded_domains=[]
|
[COURSE_TEST_ID], role=CourseSessionUser.Role.EXPERT, excluded_domains=[]
|
||||||
)
|
)
|
||||||
self.assertEqual(len(users), 1)
|
self.assertEqual(len(users), 2)
|
||||||
|
|
||||||
def test_remove_eiger_versicherungen(self):
|
def test_remove_eiger_versicherungen(self):
|
||||||
user1 = User.objects.get(id=TEST_STUDENT1_USER_ID)
|
user1 = User.objects.get(id=TEST_STUDENT1_USER_ID)
|
||||||
|
|
@ -60,7 +60,7 @@ class EdoniqUserExportTestCase(TestCase):
|
||||||
|
|
||||||
def test_export_students_and_trainers(self):
|
def test_export_students_and_trainers(self):
|
||||||
users = fetch_course_session_all_users([COURSE_TEST_ID], excluded_domains=[])
|
users = fetch_course_session_all_users([COURSE_TEST_ID], excluded_domains=[])
|
||||||
self.assertEqual(len(users), 4)
|
self.assertEqual(len(users), 5)
|
||||||
|
|
||||||
def test_deduplicates_users(self):
|
def test_deduplicates_users(self):
|
||||||
trainer1 = User.objects.get(id=TEST_TRAINER1_USER_ID)
|
trainer1 = User.objects.get(id=TEST_TRAINER1_USER_ID)
|
||||||
|
|
@ -72,7 +72,7 @@ class EdoniqUserExportTestCase(TestCase):
|
||||||
user=trainer1,
|
user=trainer1,
|
||||||
)
|
)
|
||||||
users = fetch_course_session_all_users([COURSE_TEST_ID], excluded_domains=[])
|
users = fetch_course_session_all_users([COURSE_TEST_ID], excluded_domains=[])
|
||||||
self.assertEqual(len(users), 4)
|
self.assertEqual(len(users), 5)
|
||||||
|
|
||||||
def test_response_csv(self):
|
def test_response_csv(self):
|
||||||
users = fetch_course_session_users([COURSE_TEST_ID], excluded_domains=[])
|
users = fetch_course_session_users([COURSE_TEST_ID], excluded_domains=[])
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,12 @@ from collections import Counter
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
|
from django.db.models import QuerySet
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from vbv_lernwelt.course.models import CourseSessionUser
|
from vbv_lernwelt.course.models import CourseSessionUser
|
||||||
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
|
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
|
||||||
|
from vbv_lernwelt.learnpath.models import Circle
|
||||||
from vbv_lernwelt.notify.services import NotificationService
|
from vbv_lernwelt.notify.services import NotificationService
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
@ -32,7 +34,7 @@ def send_attendance_reminder_notifications():
|
||||||
)
|
)
|
||||||
for attendance_course in attendance_courses:
|
for attendance_course in attendance_courses:
|
||||||
cs_id = attendance_course.course_session.id
|
cs_id = attendance_course.course_session.id
|
||||||
csu = CourseSessionUser.objects.filter(course_session_id=cs_id)
|
csu = get_recipients(attendance_course)
|
||||||
logger.info(
|
logger.info(
|
||||||
"Sending attendance course reminder notification",
|
"Sending attendance course reminder notification",
|
||||||
start_time=start.isoformat(),
|
start_time=start.isoformat(),
|
||||||
|
|
@ -52,3 +54,18 @@ def send_attendance_reminder_notifications():
|
||||||
label="attendance_course_reminder_notification_job",
|
label="attendance_course_reminder_notification_job",
|
||||||
)
|
)
|
||||||
return dict(results_counter)
|
return dict(results_counter)
|
||||||
|
|
||||||
|
|
||||||
|
def get_recipients(
|
||||||
|
attendance_course: CourseSessionAttendanceCourse,
|
||||||
|
) -> QuerySet["CourseSessionUser"]:
|
||||||
|
cs_id = attendance_course.course_session.id
|
||||||
|
circle_page = attendance_course.learning_content.get_parent_circle()
|
||||||
|
circle = Circle.objects.get(page_ptr=circle_page.id)
|
||||||
|
members = CourseSessionUser.objects.filter(
|
||||||
|
course_session_id=cs_id, role=CourseSessionUser.Role.MEMBER
|
||||||
|
)
|
||||||
|
experts = CourseSessionUser.objects.filter(
|
||||||
|
course_session_id=cs_id, role=CourseSessionUser.Role.EXPERT, expert=circle
|
||||||
|
)
|
||||||
|
return members | experts
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue