Client can complete learning contents
This commit is contained in:
parent
0d54437bf8
commit
c334c25f1c
|
|
@ -1,5 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
|
||||||
defineProps(['learningSequence'])
|
defineProps(['learningSequence'])
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -28,7 +29,10 @@ defineProps(['learningSequence'])
|
||||||
v-for="learningContent in learningUnit.learningContents"
|
v-for="learningContent in learningUnit.learningContents"
|
||||||
class="flex items-center gap-4 pb-3"
|
class="flex items-center gap-4 pb-3"
|
||||||
>
|
>
|
||||||
<it-icon-checkbox-unchecked/>
|
<div @click="$emit('toggleLearningContentCheckbox', learningContent)">
|
||||||
|
<it-icon-checkbox-checked v-if="learningContent.completed" />
|
||||||
|
<it-icon-checkbox-unchecked v-else />
|
||||||
|
</div>
|
||||||
<div>{{ learningContent.contents[0].type }}: {{ learningContent.title }}</div>
|
<div>{{ learningContent.contents[0].type }}: {{ learningContent.title }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { getCookieValue } from '@/router/guards';
|
||||||
|
|
||||||
|
class FetchError extends Error {
|
||||||
|
constructor(response, message = 'HTTP error ' + response.status) {
|
||||||
|
super(message);
|
||||||
|
this.response = response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const itFetch = (url, options) => {
|
||||||
|
return fetch(url, options).then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new FetchError(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const itPost = (url, data, options) => {
|
||||||
|
options = Object.assign({}, options);
|
||||||
|
|
||||||
|
const headers = Object.assign({
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json;charset=UTF-8',
|
||||||
|
}, options?.headers);
|
||||||
|
|
||||||
|
if (options?.headers) {
|
||||||
|
delete options.headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
options = Object.assign({
|
||||||
|
method: 'POST',
|
||||||
|
headers: headers,
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
}, options);
|
||||||
|
|
||||||
|
options.headers['X-CSRFToken'] = getCookieValue('csrftoken');
|
||||||
|
|
||||||
|
if (options.method === 'GET') {
|
||||||
|
delete options.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
return itFetch(url, options).then((response) => {
|
||||||
|
return response.json().catch(() => {
|
||||||
|
return Promise.resolve(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const itGet = (url) => {
|
||||||
|
return itPost(url, {}, {method: 'GET'});
|
||||||
|
};
|
||||||
|
|
@ -22,7 +22,7 @@ export const redirectToLoginIfRequired: NavigationGuardWithThis<undefined> = (to
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCookieValue = (cookieName: string): string => {
|
export const getCookieValue = (cookieName: string): string => {
|
||||||
// https://stackoverflow.com/questions/5639346/what-is-the-shortest-function-for-reading-a-cookie-by-name-in-javascript
|
// https://stackoverflow.com/questions/5639346/what-is-the-shortest-function-for-reading-a-cookie-by-name-in-javascript
|
||||||
const cookieValue = document.cookie.match('(^|[^;]+)\\s*' + cookieName + '\\s*=\\s*([^;]+)')
|
const cookieValue = document.cookie.match('(^|[^;]+)\\s*' + cookieName + '\\s*=\\s*([^;]+)')
|
||||||
if (!cookieValue) {
|
if (!cookieValue) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { defineStore } from 'pinia'
|
import {defineStore} from 'pinia'
|
||||||
// typed state https://stackoverflow.com/questions/71012513/when-using-pinia-and-typescript-how-do-you-use-an-action-to-set-the-state
|
// typed state https://stackoverflow.com/questions/71012513/when-using-pinia-and-typescript-how-do-you-use-an-action-to-set-the-state
|
||||||
|
|
||||||
export type UserState = {
|
export type UserState = {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios';
|
|
||||||
import * as log from 'loglevel';
|
import * as log from 'loglevel';
|
||||||
|
|
||||||
import MainNavigationBar from '../components/MainNavigationBar.vue';
|
import MainNavigationBar from '../components/MainNavigationBar.vue';
|
||||||
import LearningSequence from '../components/circle/LearningSequence.vue';
|
import LearningSequence from '../components/circle/LearningSequence.vue';
|
||||||
|
import { itGet, itPost } from '../fetchHelpers';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { LearningSequence, MainNavigationBar },
|
components: { LearningSequence, MainNavigationBar },
|
||||||
|
|
@ -15,58 +15,72 @@ export default {
|
||||||
learningSequences: [],
|
learningSequences: [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
toggleLearningContentCheckbox(learningContent) {
|
||||||
|
log.debug('toggleLearningContentCheckbox', learningContent);
|
||||||
|
console.log(learningContent);
|
||||||
|
|
||||||
|
itPost('/api/completion/complete_learning_content/', {
|
||||||
|
learning_content_key: learningContent.translation_key,
|
||||||
|
}).then((data) => {
|
||||||
|
console.log(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
log.debug('CircleView mounted', this.circleSlug);
|
log.debug('CircleView mounted', this.circleSlug);
|
||||||
axios({
|
itGet(`/learnpath/api/circle/${this.circleSlug}/`).then((data) => {
|
||||||
method: 'get',
|
this.circleData = data;
|
||||||
url: `/learnpath/api/circle/${this.circleSlug}/`,
|
itGet(`/api/completion/user_circle_completion/${this.circleData.translation_key}/`).then((completionData) => {
|
||||||
}).then((response) => {
|
|
||||||
log.debug(response.data);
|
|
||||||
this.circleData = response.data;
|
|
||||||
|
|
||||||
// aggregate wagtail data into LearningSequence > LearningUnit > LearningPackage hierarchy
|
// aggregate wagtail data into LearningSequence > LearningUnit > LearningPackage hierarchy
|
||||||
let learningSequence = null;
|
let learningSequence = null;
|
||||||
let learningUnit = null;
|
let learningUnit = null;
|
||||||
this.circleData.children.forEach((child) => {
|
this.circleData.children.forEach((child) => {
|
||||||
// FIXME add error detection if the data does not conform to expectations
|
// FIXME add error detection if the data does not conform to expectations
|
||||||
if (child.type === 'learnpath.LearningSequence') {
|
if(child.type === 'learnpath.LearningSequence') {
|
||||||
if (learningSequence) {
|
if(learningSequence) {
|
||||||
if (learningUnit) {
|
if(learningUnit) {
|
||||||
|
learningSequence.learningUnits.push(learningUnit);
|
||||||
|
}
|
||||||
|
this.learningSequences.push(learningSequence);
|
||||||
|
}
|
||||||
|
learningSequence = Object.assign(child, { learningUnits: [] });
|
||||||
|
learningUnit = { id: null, title: '', learningContents: [] };
|
||||||
|
} else if(child.type === 'learnpath.LearningUnit') {
|
||||||
|
if(learningUnit && learningUnit.learningContents.length) {
|
||||||
learningSequence.learningUnits.push(learningUnit);
|
learningSequence.learningUnits.push(learningUnit);
|
||||||
}
|
}
|
||||||
this.learningSequences.push(learningSequence);
|
learningUnit = Object.assign(child, { learningContents: [] });
|
||||||
|
} else {
|
||||||
|
// must be a LearningContent
|
||||||
|
if (child.translation_key in completionData.json_data.completed_learning_contents) {
|
||||||
|
child.completed = true;
|
||||||
|
}
|
||||||
|
learningUnit.learningContents.push(child);
|
||||||
}
|
}
|
||||||
learningSequence = Object.assign(child, { learningUnits: [] });
|
|
||||||
learningUnit = {id: null, title: '', learningContents: []};
|
|
||||||
} else if(child.type === 'learnpath.LearningUnit') {
|
|
||||||
if (learningUnit && learningUnit.learningContents.length) {
|
|
||||||
learningSequence.learningUnits.push(learningUnit);
|
|
||||||
}
|
|
||||||
learningUnit = Object.assign(child, { learningContents: [] });
|
|
||||||
} else {
|
|
||||||
// must be a LearningContent
|
|
||||||
learningUnit.learningContents.push(child);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (learningUnit) {
|
|
||||||
learningSequence.learningUnits.push(learningUnit);
|
|
||||||
}
|
|
||||||
this.learningSequences.push(learningSequence);
|
|
||||||
|
|
||||||
// sum minutes
|
|
||||||
this.learningSequences.forEach((learningSequence) => {
|
|
||||||
learningSequence.minutes = 0;
|
|
||||||
learningSequence.learningUnits.forEach((learningUnit) => {
|
|
||||||
learningUnit.minutes = 0;
|
|
||||||
learningUnit.learningContents.forEach((learningContent) => {
|
|
||||||
learningUnit.minutes += learningContent.minutes;
|
|
||||||
});
|
|
||||||
learningSequence.minutes += learningUnit.minutes;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if(learningUnit) {
|
||||||
|
learningSequence.learningUnits.push(learningUnit);
|
||||||
|
}
|
||||||
|
this.learningSequences.push(learningSequence);
|
||||||
|
|
||||||
|
// sum minutes
|
||||||
|
this.learningSequences.forEach((learningSequence) => {
|
||||||
|
learningSequence.minutes = 0;
|
||||||
|
learningSequence.learningUnits.forEach((learningUnit) => {
|
||||||
|
learningUnit.minutes = 0;
|
||||||
|
learningUnit.learningContents.forEach((learningContent) => {
|
||||||
|
learningUnit.minutes += learningContent.minutes;
|
||||||
|
});
|
||||||
|
learningSequence.minutes += learningUnit.minutes;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
log.debug(this.learningSequences);
|
||||||
});
|
});
|
||||||
|
|
||||||
log.debug(this.learningSequences);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,7 +121,7 @@ export default {
|
||||||
|
|
||||||
<div class="flex-auto bg-gray-100 px-4 py-8 lg:px-24">
|
<div class="flex-auto bg-gray-100 px-4 py-8 lg:px-24">
|
||||||
<div v-for="learningSequence in learningSequences">
|
<div v-for="learningSequence in learningSequences">
|
||||||
<LearningSequence :learning-sequence="learningSequence"></LearningSequence>
|
<LearningSequence :learning-sequence="learningSequence" @toggleLearningContentCheckbox="toggleLearningContentCheckbox"></LearningSequence>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -264,7 +264,7 @@ FIXTURE_DIRS = (str(APPS_DIR / "fixtures"),)
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-httponly
|
# https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-httponly
|
||||||
SESSION_COOKIE_HTTPONLY = True
|
SESSION_COOKIE_HTTPONLY = True
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-httponly
|
# https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-httponly
|
||||||
CSRF_COOKIE_HTTPONLY = True
|
CSRF_COOKIE_HTTPONLY = False
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-browser-xss-filter
|
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-browser-xss-filter
|
||||||
SECURE_BROWSER_XSS_FILTER = True
|
SECURE_BROWSER_XSS_FILTER = True
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#x-frame-options
|
# https://docs.djangoproject.com/en/dev/ref/settings/#x-frame-options
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from vbv_lernwelt.completion.views import complete_learning_content
|
from vbv_lernwelt.completion.views import complete_learning_content, request_user_circle_completion
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
path(r"user_circle_completion/<uuid:circle_key>/", request_user_circle_completion, name="request_user_circle_completion"),
|
||||||
path(r"complete_learning_content/", complete_learning_content, name="complete_learning_content"),
|
path(r"complete_learning_content/", complete_learning_content, name="complete_learning_content"),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,16 @@ from vbv_lernwelt.learnpath.models import LearningContent
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(['GET'])
|
||||||
|
def request_user_circle_completion(request, circle_key):
|
||||||
|
ucc = UserCircleCompletion.objects.get(
|
||||||
|
user=request.user,
|
||||||
|
circle_key=circle_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(status=200, data=UserCircleCompletionSerializer(ucc).data)
|
||||||
|
|
||||||
|
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
def complete_learning_content(request):
|
def complete_learning_content(request):
|
||||||
learning_content_key = request.data.get('learning_content_key')
|
learning_content_key = request.data.get('learning_content_key')
|
||||||
|
|
@ -18,7 +28,7 @@ def complete_learning_content(request):
|
||||||
|
|
||||||
circle_key = learning_content.get_parent().translation_key
|
circle_key = learning_content.get_parent().translation_key
|
||||||
|
|
||||||
LearningContentCompletion.objects.create(
|
LearningContentCompletion.objects.get_or_create(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
learning_content_key=learning_content_key,
|
learning_content_key=learning_content_key,
|
||||||
circle_key=circle_key,
|
circle_key=circle_key,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue