Client can complete learning contents

This commit is contained in:
Daniel Egger 2022-06-08 17:47:08 +02:00
parent 0d54437bf8
commit c334c25f1c
8 changed files with 133 additions and 51 deletions

View File

@ -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>

View File

@ -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'});
};

View File

@ -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) {

View File

@ -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 = {

View File

@ -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,41 +15,53 @@ 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); learningSequence.learningUnits.push(learningUnit);
} }
this.learningSequences.push(learningSequence); this.learningSequences.push(learningSequence);
} }
learningSequence = Object.assign(child, { learningUnits: [] }); learningSequence = Object.assign(child, { learningUnits: [] });
learningUnit = {id: null, title: '', learningContents: []}; learningUnit = { id: null, title: '', learningContents: [] };
} else if(child.type === 'learnpath.LearningUnit') { } else if(child.type === 'learnpath.LearningUnit') {
if (learningUnit && learningUnit.learningContents.length) { if(learningUnit && learningUnit.learningContents.length) {
learningSequence.learningUnits.push(learningUnit); learningSequence.learningUnits.push(learningUnit);
} }
learningUnit = Object.assign(child, { learningContents: [] }); learningUnit = Object.assign(child, { learningContents: [] });
} else { } else {
// must be a LearningContent // must be a LearningContent
if (child.translation_key in completionData.json_data.completed_learning_contents) {
child.completed = true;
}
learningUnit.learningContents.push(child); learningUnit.learningContents.push(child);
} }
}); });
if (learningUnit) { if(learningUnit) {
learningSequence.learningUnits.push(learningUnit); learningSequence.learningUnits.push(learningUnit);
} }
this.learningSequences.push(learningSequence); this.learningSequences.push(learningSequence);
@ -68,6 +80,8 @@ export default {
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>

View File

@ -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

View File

@ -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"),
] ]

View File

@ -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,