Merged develop into master
This commit is contained in:
commit
08608bf828
|
|
@ -0,0 +1,39 @@
|
||||||
|
describe('Survey', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.exec("python ../server/manage.py prepare_bookmarks_for_cypress");
|
||||||
|
|
||||||
|
cy.viewport('macbook-15');
|
||||||
|
cy.startGraphQLCapture();
|
||||||
|
cy.login('rahel.cueni', 'test', true);
|
||||||
|
cy.get('body').contains('Neues Wissen erwerben');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should bookmark content block', () => {
|
||||||
|
cy.visit('/module/lohn-und-budget/');
|
||||||
|
|
||||||
|
cy.get('.content-component').contains('Das folgende Interview').parent().parent().as('interviewContent');
|
||||||
|
|
||||||
|
cy.get('@interviewContent').within(() => {
|
||||||
|
cy.get('.bookmark-actions__bookmark').click();
|
||||||
|
cy.get('.bookmark-actions__add-note').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('[data-cy=bookmark-note]').within(() => {
|
||||||
|
cy.get('.skillbox-input').type('Hallo Velo');
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('[data-cy=modal-save-button]').click();
|
||||||
|
|
||||||
|
cy.get('@interviewContent').within(() => {
|
||||||
|
cy.get('.bookmark-actions__edit-note').click();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('[data-cy=bookmark-note]').within(() => {
|
||||||
|
cy.get('.skillbox-input').clear().type('Hello Bike');
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('[data-cy=modal-save-button]').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
// -- This is will overwrite an existing command --
|
// -- This is will overwrite an existing command --
|
||||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||||
|
|
||||||
|
// todo: replace with apollo call
|
||||||
Cypress.Commands.add("login", (username, password, visitLogin=false) => {
|
Cypress.Commands.add("login", (username, password, visitLogin=false) => {
|
||||||
if (visitLogin) {
|
if (visitLogin) {
|
||||||
cy.visit('/login');
|
cy.visit('/login');
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
import FullScreenLayout from '@/layouts/FullScreenLayout';
|
import FullScreenLayout from '@/layouts/FullScreenLayout';
|
||||||
import PublicLayout from '@/layouts/PublicLayout';
|
import PublicLayout from '@/layouts/PublicLayout';
|
||||||
import Modal from '@/components/Modal';
|
import Modal from '@/components/Modal';
|
||||||
import MobileNavigation from '@/components/MobileNavigation';
|
import MobileNavigation from '@/components/book-navigation/MobileNavigation';
|
||||||
import NewContentBlockWizard from '@/components/content-block-form/NewContentBlockWizard';
|
import NewContentBlockWizard from '@/components/content-block-form/NewContentBlockWizard';
|
||||||
import EditContentBlockWizard from '@/components/content-block-form/EditContentBlockWizard';
|
import EditContentBlockWizard from '@/components/content-block-form/EditContentBlockWizard';
|
||||||
import NewRoomEntryWizard from '@/components/rooms/room-entries/NewRoomEntryWizard';
|
import NewRoomEntryWizard from '@/components/rooms/room-entries/NewRoomEntryWizard';
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,35 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="assignment-with-submissions">
|
<div class="assignment-with-submissions">
|
||||||
<!--<h1 class="assignment-with-submissions__title">{{assignment.assignment}}</h1>-->
|
|
||||||
<h4 class="assignment-with-submissions__heading">Aufgabe</h4>
|
|
||||||
|
|
||||||
<p class="assignment-with-submissions__text">{{assignment.assignment}}</p>
|
<p class="assignment-with-submissions__text">{{assignment.assignment}}</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a class="button button--primary submissions-page__back" @click="$emit('back')">Aufgabe im Modul anzeigen</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="assignment-with-submissions__solution" v-if="assignment.solution">
|
<div class="assignment-with-submissions__solution" v-if="assignment.solution">
|
||||||
<h4 class="assignment-with-submissions__heading">Lösung</h4>
|
<h4 class="assignment-with-submissions__heading">Lösung</h4>
|
||||||
<p class="assignment-with-submissions__solution-text">{{assignment.solution}}</p>
|
<p class="assignment-with-submissions__solution-text">{{assignment.solution}}</p>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="!assignment.submissions.length">Zu diesem Auftrag sind noch keine Ergebnisse vorhanden</p>
|
<p class="assignment-with-submissions__no-submissions" v-if="!assignment.submissions.length">Zu diesem Auftrag sind noch keine Ergebnisse vorhanden</p>
|
||||||
|
|
||||||
|
<div v-if="assignment.submissions.length" class="assignment-with-submissions__submissions submissions">
|
||||||
|
<div class="submissions__header student-submission-row submission-header">
|
||||||
|
<p class="submission-header__title">Lernende</p>
|
||||||
|
<p class="submission-header__title">Ergebnisse</p>
|
||||||
|
<p class="submission-header__title">Feedback</p>
|
||||||
|
</div>
|
||||||
<router-link
|
<router-link
|
||||||
:to="submissionLink(submission)"
|
:to="submissionLink(submission)"
|
||||||
v-for="(submission, index) in submissions"
|
v-for="submission in submissions"
|
||||||
class="assignment-with-submissions__link"
|
class="assignment-with-submissions__link"
|
||||||
:key="index">
|
:key="submission.id">
|
||||||
<student-submission class="assignment-with-submissions__submission"
|
<student-submission class="assignment-with-submissions__submission"
|
||||||
:submission="submission"
|
:submission="submission"
|
||||||
>
|
>
|
||||||
</student-submission>
|
</student-submission>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -80,6 +91,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&__text {
|
&__text {
|
||||||
|
font-size: toRem(26px);
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,10 +107,27 @@
|
||||||
|
|
||||||
&__link {
|
&__link {
|
||||||
display: block;
|
display: block;
|
||||||
&:first-of-type {
|
}
|
||||||
border-top: 1px solid $color-silver-dark;
|
|
||||||
}
|
&__submissions {
|
||||||
|
margin-top: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__no-submissions {
|
||||||
|
margin-top: $large-spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.submissions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submission-header {
|
||||||
|
&__title {
|
||||||
|
color: $color-silver-dark;
|
||||||
|
font-family: $sans-serif-font-family;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,14 @@
|
||||||
<div class="chapter">
|
<div class="chapter">
|
||||||
<h3 :id="'chapter-' + index">{{chapter.title}}</h3>
|
<h3 :id="'chapter-' + index">{{chapter.title}}</h3>
|
||||||
|
|
||||||
|
<bookmark-actions
|
||||||
|
class="chapter__bookmark-actions"
|
||||||
|
@add-note="addNote"
|
||||||
|
@edit-note="editNote"
|
||||||
|
:bookmarked="chapter.bookmark"
|
||||||
|
@bookmark="bookmark(!chapter.bookmark)"
|
||||||
|
:note="note"
|
||||||
|
></bookmark-actions>
|
||||||
<p class="chapter__description">
|
<p class="chapter__description">
|
||||||
{{chapter.description}}
|
{{chapter.description}}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -18,15 +26,20 @@
|
||||||
<script>
|
<script>
|
||||||
import ContentBlock from '@/components/ContentBlock';
|
import ContentBlock from '@/components/ContentBlock';
|
||||||
import AddContentButton from '@/components/AddContentButton';
|
import AddContentButton from '@/components/AddContentButton';
|
||||||
|
import BookmarkActions from '@/components/notes/BookmarkActions';
|
||||||
|
|
||||||
import {mapGetters} from 'vuex';
|
import {mapGetters} from 'vuex';
|
||||||
import {isHidden} from '@/helpers/content-block';
|
import {isHidden} from '@/helpers/content-block';
|
||||||
import {meQuery} from '@/graphql/queries';
|
import {meQuery} from '@/graphql/queries';
|
||||||
|
|
||||||
|
import UPDATE_CHAPTER_BOOKMARK_MUTATION from '@/graphql/gql/mutations/updateChapterBookmark.gql';
|
||||||
|
import CHAPTER_QUERY from '@/graphql/gql/chapterQuery.gql';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['chapter', 'index'],
|
props: ['chapter', 'index'],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
|
BookmarkActions,
|
||||||
ContentBlock,
|
ContentBlock,
|
||||||
AddContentButton
|
AddContentButton
|
||||||
},
|
},
|
||||||
|
|
@ -45,6 +58,12 @@
|
||||||
schoolClass() {
|
schoolClass() {
|
||||||
return this.me.selectedClass;
|
return this.me.selectedClass;
|
||||||
},
|
},
|
||||||
|
note() {
|
||||||
|
if (!(this.chapter && this.chapter.bookmark)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return this.chapter.bookmark.note;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -53,6 +72,64 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
bookmark(bookmarked) {
|
||||||
|
const id = this.chapter.id;
|
||||||
|
this.$apollo.mutate({
|
||||||
|
mutation: UPDATE_CHAPTER_BOOKMARK_MUTATION,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
chapter: id,
|
||||||
|
bookmarked
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update: (store, response) => {
|
||||||
|
const query = CHAPTER_QUERY;
|
||||||
|
const variables = {id};
|
||||||
|
const data = store.readQuery({
|
||||||
|
query,
|
||||||
|
variables
|
||||||
|
});
|
||||||
|
|
||||||
|
const chapter = data.chapter;
|
||||||
|
|
||||||
|
if (bookmarked) {
|
||||||
|
chapter.bookmark = {
|
||||||
|
__typename: 'ChapterBookmarkNode',
|
||||||
|
note: null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
chapter.bookmark = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.chapter = chapter;
|
||||||
|
|
||||||
|
store.writeQuery({
|
||||||
|
data,
|
||||||
|
query,
|
||||||
|
variables
|
||||||
|
});
|
||||||
|
},
|
||||||
|
optimisticResponse: {
|
||||||
|
__typename: 'Mutation',
|
||||||
|
updateChapterBookmark: {
|
||||||
|
__typename: 'UpdateChapterBookmarkPayload',
|
||||||
|
success: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
addNote(id) {
|
||||||
|
this.$store.dispatch('addNote', {
|
||||||
|
content: id,
|
||||||
|
parent: this.chapter.id
|
||||||
|
});
|
||||||
|
},
|
||||||
|
editNote() {
|
||||||
|
this.$store.dispatch('editNote', this.chapter.bookmark.note);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
apollo: {
|
apollo: {
|
||||||
me: meQuery
|
me: meQuery
|
||||||
}
|
}
|
||||||
|
|
@ -63,6 +140,12 @@
|
||||||
@import "@/styles/_mixins.scss";
|
@import "@/styles/_mixins.scss";
|
||||||
|
|
||||||
.chapter {
|
.chapter {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&__bookmark-actions {
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
&__description {
|
&__description {
|
||||||
@include lead-paragraph;
|
@include lead-paragraph;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="student-submission">
|
<div class="student-submission student-submission-row">
|
||||||
<div class="student-submission__student-name">
|
<div class="student-submission__student-name">
|
||||||
{{name}}
|
{{name}}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -9,6 +9,9 @@
|
||||||
<student-submission-document :document="submission.document" class="entry-document"></student-submission-document>
|
<student-submission-document :document="submission.document" class="entry-document"></student-submission-document>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="student-submission__feedback entry" v-if="submission.submissionFeedback">
|
||||||
|
<p class="entry__text" :class="{'entry__text--final': submission.submissionFeedback.final}">{{submission.submissionFeedback.text | trimToLength(50)}}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -50,18 +53,13 @@
|
||||||
@import "@/styles/_functions.scss";
|
@import "@/styles/_functions.scss";
|
||||||
|
|
||||||
.student-submission {
|
.student-submission {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 170px 1fr;
|
|
||||||
grid-column-gap: 80px;
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: 1px solid $color-silver-dark;
|
|
||||||
padding: 15px 0;
|
|
||||||
|
|
||||||
&__student-name {
|
&__student-name {
|
||||||
font-size: toRem(17px);
|
font-size: toRem(17px);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
font-family: $sans-serif-font-family;
|
font-family: $sans-serif-font-family;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__entry {
|
&__entry {
|
||||||
font-size: toRem(14px);
|
font-size: toRem(14px);
|
||||||
font-family: $sans-serif-font-family;
|
font-family: $sans-serif-font-family;
|
||||||
|
|
@ -71,4 +69,13 @@
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.entry {
|
||||||
|
&__text {
|
||||||
|
color: $color-silver-dark;
|
||||||
|
&--final {
|
||||||
|
color: $color-charcoal-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,34 @@
|
||||||
<template>
|
<template>
|
||||||
<nav class="top-navigation" :class="{'top-navigation--mobile': mobile}">
|
<nav class="top-navigation" :class="{'top-navigation--mobile': mobile}">
|
||||||
<router-link to="/book/topic/berufliche-grundbildung" active-class="top-navigation__link--active"
|
<div class="top-navigation__item">
|
||||||
:class="{'top-navigation__link--active': isActive('book')}"
|
<router-link to="/book/topic/berufliche-grundbildung" active-class="top-navigation__link--active"
|
||||||
class="top-navigation__link">Inhalte
|
:class="{'top-navigation__link--active': isActive('book')}"
|
||||||
</router-link>
|
@click.native="hideMobileNavigation"
|
||||||
<router-link to="/rooms" active-class="top-navigation__link--active" class="top-navigation__link">Räume
|
class="top-navigation__link">Inhalte
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link to="/portfolio" active-class="top-navigation__link--active" class="top-navigation__link">Portfolio
|
|
||||||
</router-link>
|
<mobile-subnavigation v-if="mobile"></mobile-subnavigation>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="top-navigation__item">
|
||||||
|
<router-link to="/rooms" active-class="top-navigation__link--active" @click.native="hideMobileNavigation"
|
||||||
|
class="top-navigation__link">Räume
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="top-navigation__item">
|
||||||
|
<router-link to="/portfolio" active-class="top-navigation__link--active" @click.native="hideMobileNavigation"
|
||||||
|
class="top-navigation__link">Portfolio
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import MobileSubnavigation from '@/components/book-navigation/MobileSubnavigation';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
mobile: {
|
mobile: {
|
||||||
|
|
@ -19,9 +36,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
MobileSubnavigation
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
isActive(linkName) {
|
isActive(linkName) {
|
||||||
return linkName === 'book' && this.$route.path.indexOf('module') > -1;
|
return linkName === 'book' && this.$route.path.indexOf('module') > -1;
|
||||||
|
},
|
||||||
|
hideMobileNavigation() {
|
||||||
|
this.$store.dispatch('showMobileNavigation', false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -52,13 +76,31 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
#{$parent}__link {
|
#{$parent}__link {
|
||||||
color: rgba($color-white, 0.6);
|
color: $color-white;
|
||||||
@include heading-4;
|
@include heading-4;
|
||||||
line-height: 2em;
|
line-height: 2.5em;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5*$small-spacing;
|
||||||
|
|
||||||
&--active {
|
&:only-child {
|
||||||
color: $color-white;
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#{$parent}__item {
|
||||||
|
border-bottom: 1px solid $color-white;
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
order: 3;
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
&:nth-child(2) {
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
&:nth-child(3) {
|
||||||
|
order: 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,7 @@
|
||||||
<book-topic-navigation></book-topic-navigation>
|
<book-topic-navigation></book-topic-navigation>
|
||||||
</sub-navigation-item>
|
</sub-navigation-item>
|
||||||
<sub-navigation-item title="Instrument">
|
<sub-navigation-item title="Instrument">
|
||||||
<router-link tag="div" class="book-subnavigation__item" to="/instruments/sprache-kommunikation">Sprache und Kommunikation</router-link>
|
<instrument-navigation></instrument-navigation>
|
||||||
<router-link tag="div" class="book-subnavigation__item" to="/instruments/gesellschaft">Gesellschaft</router-link>
|
|
||||||
</sub-navigation-item>
|
</sub-navigation-item>
|
||||||
<!--<sub-navigation-item title="News">-->
|
<!--<sub-navigation-item title="News">-->
|
||||||
<!--<template slot="title">-->
|
<!--<template slot="title">-->
|
||||||
|
|
@ -18,11 +17,13 @@
|
||||||
<script>
|
<script>
|
||||||
import SubNavigationItem from '@/components/book-navigation/SubNavigationItem';
|
import SubNavigationItem from '@/components/book-navigation/SubNavigationItem';
|
||||||
import BookTopicNavigation from '@/components/book-navigation/BookTopicNavigation';
|
import BookTopicNavigation from '@/components/book-navigation/BookTopicNavigation';
|
||||||
|
import InstrumentNavigation from '@/components/book-navigation/InstrumentNavigation';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
SubNavigationItem,
|
SubNavigationItem,
|
||||||
BookTopicNavigation
|
BookTopicNavigation,
|
||||||
|
InstrumentNavigation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<nav class="book-topics">
|
<nav class="book-topics">
|
||||||
<router-link :to="{name: 'topic', params: {topicSlug: topic.slug}}"
|
<router-link :to="{name: 'topic', params: {topicSlug: topic.slug}}"
|
||||||
tag="div"
|
@click.native="hideMobileNavigation"
|
||||||
class="book-topics__topic book-subnavigation__item"
|
tag="div"
|
||||||
:class="{'book-topics__topic--active': topic.active}"
|
class="book-topics__topic book-subnavigation__item"
|
||||||
v-for="topic in topics"
|
:class="{'book-topics__topic--active': topic.active, 'book-subnavigation__item--mobile': mobile}"
|
||||||
:key="topic.id">
|
v-for="topic in topics"
|
||||||
|
:key="topic.id">
|
||||||
{{topic.order}}.
|
{{topic.order}}.
|
||||||
{{topic.title}}
|
{{topic.title}}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
@ -16,16 +17,24 @@
|
||||||
import ALL_TOPICS_QUERY from '@/graphql/gql/allTopicsQuery.gql';
|
import ALL_TOPICS_QUERY from '@/graphql/gql/allTopicsQuery.gql';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
props: {
|
||||||
|
mobile: {
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
topics: [
|
topics: []
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
topicId(id) {
|
topicId(id) {
|
||||||
return atob(id)
|
return atob(id)
|
||||||
|
},
|
||||||
|
hideMobileNavigation() {
|
||||||
|
this.$store.dispatch('showMobileNavigation', false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<router-link tag="div" class="book-subnavigation__item"
|
||||||
|
:class="{'book-subnavigation__item--mobile': mobile}"
|
||||||
|
@click.native="hideMobileNavigation"
|
||||||
|
to="/instruments/sprache-kommunikation">Sprache und
|
||||||
|
Kommunikation
|
||||||
|
</router-link>
|
||||||
|
<router-link tag="div" class="book-subnavigation__item"
|
||||||
|
:class="{'book-subnavigation__item--mobile': mobile}"
|
||||||
|
@click.native="hideMobileNavigation"
|
||||||
|
to="/instruments/gesellschaft">Gesellschaft
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
mobile: {
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
hideMobileNavigation() {
|
||||||
|
this.$store.dispatch('showMobileNavigation', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -64,9 +64,9 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
||||||
grid-template-columns: 1fr 50px;
|
grid-template-columns: 1fr 50px;
|
||||||
grid-template-rows: 50px 100px auto 100px;
|
grid-template-rows: 50px max-content auto 100px;
|
||||||
|
|
||||||
grid-template-areas: "m m" "m m" "s s";
|
grid-template-areas: "m m" "m m" "s s" "s s";
|
||||||
|
|
||||||
&--with-subnavigation {
|
&--with-subnavigation {
|
||||||
grid-template-areas: "m m" "m m" "sub sub" "s s";
|
grid-template-areas: "m m" "m m" "sub sub" "s s";
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
<template>
|
||||||
|
<div class="mobile-subnavigation">
|
||||||
|
<div class="mobile-subnavigation__section">
|
||||||
|
<h3 class="mobile-subnavigation__title">Themen</h3>
|
||||||
|
<book-topic-navigation :mobile="true"></book-topic-navigation>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mobile-subnavigation__section">
|
||||||
|
<h3 class="mobile-subnavigation__title">Instrumente</h3>
|
||||||
|
<instrument-navigation :mobile="true"></instrument-navigation>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BookTopicNavigation from '@/components/book-navigation/BookTopicNavigation';
|
||||||
|
import InstrumentNavigation from '@/components/book-navigation/InstrumentNavigation';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
BookTopicNavigation,
|
||||||
|
InstrumentNavigation
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import "@/styles/_variables.scss";
|
||||||
|
@import "@/styles/_mixins.scss";
|
||||||
|
|
||||||
|
.mobile-subnavigation {
|
||||||
|
&__title {
|
||||||
|
@include small-text;
|
||||||
|
color: rgba($color-white, 0.6);
|
||||||
|
|
||||||
|
margin-bottom: $small-spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__section {
|
||||||
|
margin-bottom: $medium-spacing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -49,12 +49,16 @@
|
||||||
|
|
||||||
.book-subnavigation {
|
.book-subnavigation {
|
||||||
&__item {
|
&__item {
|
||||||
font-family: $sans-serif-font-family;
|
@include small-text;
|
||||||
font-size: toRem(14px);
|
margin-bottom: $small-spacing;
|
||||||
margin-bottom: $medium-spacing;
|
|
||||||
color: $color-silver-dark;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
color: $color-silver-dark;
|
||||||
|
|
||||||
|
&--mobile {
|
||||||
|
color: $color-white;
|
||||||
|
}
|
||||||
|
|
||||||
&:last-of-type {
|
&:last-of-type {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="content-component" :class="{'content-component--bookmarked': bookmarked}">
|
<div class="content-component" :class="{'content-component--bookmarked': bookmarked}">
|
||||||
<bookmark-actions
|
<bookmark-actions
|
||||||
v-if="showBookmarkActions()"
|
v-if="showBookmarkActions"
|
||||||
@add-note="addNote(component.id)"
|
@add-note="addNote(component.id)"
|
||||||
@edit-note="editNote"
|
@edit-note="editNote"
|
||||||
@bookmark="bookmarkContent(component.id, !bookmarked)"
|
@bookmark="bookmarkContent(component.id, !bookmarked)"
|
||||||
|
|
@ -72,6 +72,9 @@
|
||||||
note() {
|
note() {
|
||||||
const bookmark = this.bookmarks && this.bookmarks.find(bookmark => bookmark.uuid === this.component.id);
|
const bookmark = this.bookmarks && this.bookmarks.find(bookmark => bookmark.uuid === this.component.id);
|
||||||
return bookmark && bookmark.note;
|
return bookmark && bookmark.note;
|
||||||
|
},
|
||||||
|
showBookmarkActions() {
|
||||||
|
return this.component.type !== 'content_list' && this.component.type !== 'basic_knowledge' && !this.editModule;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -136,9 +139,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
|
||||||
showBookmarkActions() {
|
|
||||||
return this.component.type !== 'content_list' && this.component.type !== 'basic_knowledge' && !this.editModule;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,39 +7,22 @@
|
||||||
<solution :value="solution" v-if="assignment.solution"></solution>
|
<solution :value="solution" v-if="assignment.solution"></solution>
|
||||||
|
|
||||||
<template v-if="isStudent">
|
<template v-if="isStudent">
|
||||||
<div class="assignment__submission">
|
<submission-form
|
||||||
|
v-if="isStudent"
|
||||||
|
@turnIn="turnIn"
|
||||||
|
@saveInput="saveInput"
|
||||||
|
@reopen="reopen"
|
||||||
|
@changeDocumentUrl="changeDocumentUrl"
|
||||||
|
:user-input="submission"
|
||||||
|
placeholder="Ergebnis erfassen"
|
||||||
|
action="Ergebnis mit Lehrperson teilen"
|
||||||
|
shared-msg="Das Ergebnis wurde mit der Lehrperson geteilt."
|
||||||
|
:saved="!unsaved"
|
||||||
|
>
|
||||||
|
</submission-form>
|
||||||
|
|
||||||
<div class="assignment__inputs">
|
<div v-if="this.assignment.submission.submissionFeedback" class="assignment__feedback">
|
||||||
<submission-form
|
<p>{{feedbackText}}</p>
|
||||||
@input="saveInput"
|
|
||||||
:submission="submission"
|
|
||||||
:saved="!unsaved"
|
|
||||||
:final="final"
|
|
||||||
></submission-form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="assignment__actions" v-if="!final">
|
|
||||||
<button
|
|
||||||
class="assignment__submit button button--primary button--white-bg"
|
|
||||||
@click="turnIn"
|
|
||||||
>Ergebnis mit Lehrperson teilen
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div v-if="assignment.submission.document">
|
|
||||||
<document-block
|
|
||||||
:value="{url: assignment.submission.document}"
|
|
||||||
show-trash-icon
|
|
||||||
v-on:trash="changeDocumentUrl('')"
|
|
||||||
></document-block>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<simple-file-upload
|
|
||||||
v-on:link-change-url="changeDocumentUrl"
|
|
||||||
:value="assignment.submission.document"
|
|
||||||
></simple-file-upload>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<final-submission :submission="assignment.submission" v-if="final" @reopen="reopen"></final-submission>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="!isStudent">
|
<template v-if="!isStudent">
|
||||||
|
|
@ -59,6 +42,7 @@
|
||||||
import cloneDeep from 'lodash/cloneDeep'
|
import cloneDeep from 'lodash/cloneDeep'
|
||||||
|
|
||||||
import FinalSubmission from '@/components/content-blocks/assignment/FinalSubmission';
|
import FinalSubmission from '@/components/content-blocks/assignment/FinalSubmission';
|
||||||
|
import SubmissionInput from '@/components/content-blocks/assignment/SubmissionInput';
|
||||||
import SubmissionForm from '@/components/content-blocks/assignment/SubmissionForm';
|
import SubmissionForm from '@/components/content-blocks/assignment/SubmissionForm';
|
||||||
import DocumentForm from '@/components/content-forms/DocumentForm';
|
import DocumentForm from '@/components/content-forms/DocumentForm';
|
||||||
import DocumentBlock from '@/components/content-blocks/DocumentBlock';
|
import DocumentBlock from '@/components/content-blocks/DocumentBlock';
|
||||||
|
|
@ -71,10 +55,11 @@
|
||||||
components: {
|
components: {
|
||||||
DocumentBlock,
|
DocumentBlock,
|
||||||
DocumentForm,
|
DocumentForm,
|
||||||
SubmissionForm,
|
SubmissionInput,
|
||||||
FinalSubmission,
|
FinalSubmission,
|
||||||
Solution,
|
Solution,
|
||||||
SimpleFileUpload
|
SimpleFileUpload,
|
||||||
|
SubmissionForm
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -95,6 +80,10 @@
|
||||||
},
|
},
|
||||||
id() {
|
id() {
|
||||||
return this.assignment.id ? this.assignment.id.replace(/=/g, '') : ''
|
return this.assignment.id ? this.assignment.id.replace(/=/g, '') : ''
|
||||||
|
},
|
||||||
|
feedbackText() {
|
||||||
|
let feedback = this.assignment.submission.submissionFeedback;
|
||||||
|
return `Feedback von ${feedback.teacher.firstName} ${feedback.teacher.lastName}: ${feedback.text}`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -266,25 +255,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__submission {
|
&__feedback {
|
||||||
@include input-box-shadow;
|
margin-top: $medium-spacing;
|
||||||
background-color: $color-white;
|
|
||||||
border-radius: $input-border-radius;
|
|
||||||
border: 1px solid $color-silver;
|
|
||||||
padding: $medium-spacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__inputs {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__submit {
|
|
||||||
margin-right: $medium-spacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="final-submission">
|
<div class="final-submission">
|
||||||
<document-block
|
<document-block
|
||||||
v-if="submission.document"
|
v-if="userInput.document"
|
||||||
:value="{url: submission.document}"
|
:value="{url: userInput.document}"
|
||||||
class="final-submission__document"
|
class="final-submission__document"
|
||||||
></document-block>
|
></document-block>
|
||||||
<div class="final-submission__explanation">
|
<div class="final-submission__explanation">
|
||||||
<info-icon class="final-submission__explanation-icon"></info-icon>
|
<info-icon class="final-submission__explanation-icon"></info-icon>
|
||||||
<span class="final-submission__explanation-text">Das Ergebnis wurde mit der Lehrperson geteilt</span>
|
<span class="final-submission__explanation-text">{{sharedMsg}}</span>
|
||||||
<a class="final-submission__reopen" @click="$emit('reopen')">Bearbeiten</a>
|
<a class="final-submission__reopen" @click="$emit('reopen')">Bearbeiten</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
import {newLineToParagraph} from '@/helpers/text';
|
import {newLineToParagraph} from '@/helpers/text';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['submission'],
|
props: ['userInput', 'sharedMsg'],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
text() {
|
text() {
|
||||||
return newLineToParagraph(this.submission.text);
|
return newLineToParagraph(this.userInput.text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,77 +1,123 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="submission-form__text-answer submission-form">
|
<div class="feedback__submission submission-form-container">
|
||||||
<textarea
|
<div class="submission-form-container__inputs">
|
||||||
v-auto-grow
|
<submission-input
|
||||||
rows="1"
|
@input="saveInput"
|
||||||
class="submission-form__textarea"
|
:input-text="userInput.text"
|
||||||
placeholder="Ergebnis erfassen"
|
:saved="saved"
|
||||||
:readonly="final"
|
:final="final"
|
||||||
:value="submission.text"
|
:placeholder="placeholder"
|
||||||
@input="$emit('input', $event.target.value)"
|
:reopen="reopenSubmission"
|
||||||
></textarea>
|
></submission-input>
|
||||||
<div class="submission-form__save-status submission-form__save-status--saved" v-if="saved">
|
</div>
|
||||||
<tick-circle-icon class="submission-form__save-status-icon"></tick-circle-icon>
|
|
||||||
|
<div class="submission-form-container__actions" v-if="!final">
|
||||||
|
<button class="submission-form-container__submit button button--primary button--white-bg"
|
||||||
|
@click="$emit('turnIn')"
|
||||||
|
>{{action}}
|
||||||
|
</button>
|
||||||
|
<div v-if="userInput.document">
|
||||||
|
<document-block
|
||||||
|
:value="{url: userInput.document}"
|
||||||
|
show-trash-icon
|
||||||
|
v-on:trash="changeDocumentUrl('')"
|
||||||
|
></document-block>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<simple-file-upload
|
||||||
|
v-if="allowsDocuments"
|
||||||
|
v-on:link-change-url="changeDocumentUrl"
|
||||||
|
:value="userInput.document"
|
||||||
|
class="submission-form-container__document"
|
||||||
|
></simple-file-upload>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<final-submission
|
||||||
|
v-if="final"
|
||||||
|
:user-input="userInput"
|
||||||
|
:shared-msg="sharedMsg"
|
||||||
|
@reopen="$emit('reopen')"></final-submission>
|
||||||
</div>
|
</div>
|
||||||
<div class="submission-form__save-status submission-form__save-status--unsaved" v-if="!saved">
|
|
||||||
<loading-icon class="submission-form__save-status-icon submission-form__saving-icon"></loading-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import TickCircleIcon from '@/components/icons/TickCircleIcon';
|
import SubmissionInput from '@/components/content-blocks/assignment/SubmissionInput';
|
||||||
import LoadingIcon from '@/components/icons/LoadingIcon';
|
import FinalSubmission from '@/components/content-blocks/assignment/FinalSubmission';
|
||||||
|
import SimpleFileUpload from '@/components/SimpleFileUpload';
|
||||||
|
import DocumentBlock from '@/components/content-blocks/DocumentBlock';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['submission', 'saved', 'final'],
|
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
TickCircleIcon,
|
SubmissionInput,
|
||||||
LoadingIcon
|
FinalSubmission,
|
||||||
}
|
SimpleFileUpload,
|
||||||
|
DocumentBlock
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
userInput: Object,
|
||||||
|
saved: Boolean,
|
||||||
|
placeholder: String,
|
||||||
|
action: String,
|
||||||
|
reopen: Function,
|
||||||
|
document: String,
|
||||||
|
sharedMsg: String
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
final() {
|
||||||
|
return !!this.userInput && this.userInput.final
|
||||||
|
},
|
||||||
|
allowsDocuments() {
|
||||||
|
return 'document' in this.userInput;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
reopenSubmission() {
|
||||||
|
this.$emit('reopen');
|
||||||
|
},
|
||||||
|
saveInput(input) {
|
||||||
|
this.$emit('saveInput', input);
|
||||||
|
},
|
||||||
|
changeDocumentUrl(documentUrl) {
|
||||||
|
this.$emit('changeDocumentUrl', documentUrl);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import "@/styles/_variables.scss";
|
@import '@/styles/_mixins.scss';
|
||||||
@import "@/styles/_mixins.scss";
|
|
||||||
|
|
||||||
.submission-form {
|
.submission-form-container {
|
||||||
|
|
||||||
|
@include input-box-shadow;
|
||||||
|
background-color: $color-white;
|
||||||
|
border-radius: $input-border-radius;
|
||||||
|
border: 1px solid $color-silver;
|
||||||
|
padding: $medium-spacing;
|
||||||
|
|
||||||
|
&__inputs {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__submit {
|
||||||
|
margin-right: $medium-spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
align-items: center;
|
||||||
justify-content: space-between;
|
}
|
||||||
|
|
||||||
&__textarea {
|
&__document {
|
||||||
display: flex;
|
&:hover {
|
||||||
width: 95%;
|
cursor: pointer;
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
line-height: 1.5;
|
|
||||||
border: 0;
|
|
||||||
min-height: 110px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__save-status {
|
|
||||||
position: relative;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__save-status-icon {
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
fill: $color-silver-dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__saving-icon {
|
|
||||||
animation: spin 2.5s linear infinite;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
<template>
|
||||||
|
<div class="submission-form__text-answer submission-form">
|
||||||
|
<textarea
|
||||||
|
v-auto-grow
|
||||||
|
rows="1"
|
||||||
|
class="submission-form__textarea"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:readonly="final"
|
||||||
|
:value="inputText"
|
||||||
|
@input="$emit('input', $event.target.value)"
|
||||||
|
></textarea>
|
||||||
|
<div class="submission-form__save-status submission-form__save-status--saved" v-if="saved">
|
||||||
|
<tick-circle-icon class="submission-form__save-status-icon"></tick-circle-icon>
|
||||||
|
</div>
|
||||||
|
<div class="submission-form__save-status submission-form__save-status--unsaved" v-if="!saved">
|
||||||
|
<loading-icon class="submission-form__save-status-icon submission-form__saving-icon"></loading-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import TickCircleIcon from '@/components/icons/TickCircleIcon';
|
||||||
|
import LoadingIcon from '@/components/icons/LoadingIcon';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
inputText: String,
|
||||||
|
saved: Boolean,
|
||||||
|
final: Boolean,
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: 'Ergebnis erfassen'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
TickCircleIcon,
|
||||||
|
LoadingIcon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import "@/styles/_variables.scss";
|
||||||
|
@import "@/styles/_mixins.scss";
|
||||||
|
|
||||||
|
.submission-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
&__textarea {
|
||||||
|
display: flex;
|
||||||
|
width: 95%;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
border: 0;
|
||||||
|
min-height: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__save-status {
|
||||||
|
position: relative;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__save-status-icon {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
fill: $color-silver-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__saving-icon {
|
||||||
|
animation: spin 2.5s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -5,7 +5,17 @@
|
||||||
<img
|
<img
|
||||||
:src="module.heroImage"
|
:src="module.heroImage"
|
||||||
alt="" class="module__hero">
|
alt="" class="module__hero">
|
||||||
<div class="module__intro" v-html="module.intro"></div>
|
|
||||||
|
<div class="module__intro-wrapper">
|
||||||
|
<bookmark-actions
|
||||||
|
class="module__bookmark-actions"
|
||||||
|
@add-note="addNote"
|
||||||
|
@edit-note="editNote"
|
||||||
|
:bookmarked="module.bookmark"
|
||||||
|
:note="note"
|
||||||
|
@bookmark="bookmark(!module.bookmark)"></bookmark-actions>
|
||||||
|
<div class="module__intro" v-html="module.intro"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3 id="objectives">Lernziele</h3>
|
<h3 id="objectives">Lernziele</h3>
|
||||||
|
|
||||||
|
|
@ -26,13 +36,17 @@
|
||||||
|
|
||||||
import UPDATE_OBJECTIVE_PROGRESS_MUTATION from '@/graphql/gql/mutations/updateObjectiveProgress.gql';
|
import UPDATE_OBJECTIVE_PROGRESS_MUTATION from '@/graphql/gql/mutations/updateObjectiveProgress.gql';
|
||||||
import UPDATE_LAST_MODULE_MUTATION from '@/graphql/gql/mutations/updateLastModule.gql';
|
import UPDATE_LAST_MODULE_MUTATION from '@/graphql/gql/mutations/updateLastModule.gql';
|
||||||
|
import UPDATE_MODULE_BOOKMARK_MUTATION from '@/graphql/gql/mutations/updateModuleBookmark.gql';
|
||||||
import OBJECTIVE_QUERY from '@/graphql/gql/objectiveQuery.gql';
|
import OBJECTIVE_QUERY from '@/graphql/gql/objectiveQuery.gql';
|
||||||
import ME_QUERY from '@/graphql/gql/meQuery.gql';
|
import ME_QUERY from '@/graphql/gql/meQuery.gql';
|
||||||
|
import MODULE_QUERY from '@/graphql/gql/moduleByIdQuery.gql';
|
||||||
|
|
||||||
import {withoutOwnerFirst} from '@/helpers/sorting';
|
import {withoutOwnerFirst} from '@/helpers/sorting';
|
||||||
|
import BookmarkActions from '@/components/notes/BookmarkActions';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
BookmarkActions,
|
||||||
ObjectiveGroups,
|
ObjectiveGroups,
|
||||||
ObjectiveGroupControl,
|
ObjectiveGroupControl,
|
||||||
AddObjectiveGroupButton,
|
AddObjectiveGroupButton,
|
||||||
|
|
@ -67,6 +81,12 @@
|
||||||
},
|
},
|
||||||
isStudent() {
|
isStudent() {
|
||||||
return !this.me.permissions.includes('users.can_manage_school_class_content');
|
return !this.me.permissions.includes('users.can_manage_school_class_content');
|
||||||
|
},
|
||||||
|
note() {
|
||||||
|
if (!(this.module && this.module.bookmark)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return this.module.bookmark.note;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -112,6 +132,61 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
bookmark(bookmarked) {
|
||||||
|
const id = this.module.id;
|
||||||
|
this.$apollo.mutate({
|
||||||
|
mutation: UPDATE_MODULE_BOOKMARK_MUTATION,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
module: id,
|
||||||
|
bookmarked
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update: (store, response) => {
|
||||||
|
const query = MODULE_QUERY;
|
||||||
|
const variables = {id};
|
||||||
|
const data = store.readQuery({
|
||||||
|
query,
|
||||||
|
variables
|
||||||
|
});
|
||||||
|
|
||||||
|
const module = data.module;
|
||||||
|
|
||||||
|
if (bookmarked) {
|
||||||
|
module.bookmark = {
|
||||||
|
__typename: 'ModuleBookmarkNode',
|
||||||
|
note: null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
module.bookmark = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.module = module;
|
||||||
|
|
||||||
|
store.writeQuery({
|
||||||
|
data,
|
||||||
|
query,
|
||||||
|
variables
|
||||||
|
});
|
||||||
|
},
|
||||||
|
optimisticResponse: {
|
||||||
|
__typename: 'Mutation',
|
||||||
|
updateModuleBookmark: {
|
||||||
|
__typename: 'UpdateModuleBookmarkPayload',
|
||||||
|
success: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
addNote(id) {
|
||||||
|
this.$store.dispatch('addNote', {
|
||||||
|
content: id,
|
||||||
|
parent: this.module.id
|
||||||
|
});
|
||||||
|
},
|
||||||
|
editNote() {
|
||||||
|
this.$store.dispatch('editNote', this.module.bookmark.note);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
apollo: {
|
apollo: {
|
||||||
|
|
@ -138,6 +213,7 @@
|
||||||
.module {
|
.module {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
|
|
||||||
@include desktop {
|
@include desktop {
|
||||||
width: 800px;
|
width: 800px;
|
||||||
}
|
}
|
||||||
|
|
@ -157,6 +233,10 @@
|
||||||
@include meta-title;
|
@include meta-title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__intro-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
&__intro {
|
&__intro {
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
margin-bottom: 3em;
|
margin-bottom: 3em;
|
||||||
|
|
@ -171,5 +251,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__bookmark-actions {
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="bookmark-actions">
|
<div class="bookmark-actions">
|
||||||
<a class="bookmark-actions__action" @click="$emit('bookmark')"
|
<a class="bookmark-actions__action bookmark-actions__bookmark" @click="$emit('bookmark')"
|
||||||
:class="{'bookmark-actions__action--bookmarked': bookmarked}">
|
:class="{'bookmark-actions__action--bookmarked': bookmarked}">
|
||||||
<bookmark-icon :bookmarked="bookmarked"></bookmark-icon>
|
<bookmark-icon :bookmarked="bookmarked"></bookmark-icon>
|
||||||
</a>
|
</a>
|
||||||
<a class="bookmark-actions__action" v-if="bookmarked && !note" @click="$emit('add-note')">
|
<a class="bookmark-actions__action bookmark-actions__add-note" v-if="bookmarked && !note" @click="$emit('add-note')">
|
||||||
<add-note-icon></add-note-icon>
|
<add-note-icon></add-note-icon>
|
||||||
</a>
|
</a>
|
||||||
<a class="bookmark-actions__action bookmark-actions__action--noted" @click="$emit('edit-note')" v-if="note">
|
<a class="bookmark-actions__action bookmark-actions__edit-note bookmark-actions__action--noted" @click="$emit('edit-note')" v-if="note">
|
||||||
<note-icon></note-icon>
|
<note-icon></note-icon>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,8 @@
|
||||||
<script>
|
<script>
|
||||||
import NoteForm from '@/components/notes/NoteForm';
|
import NoteForm from '@/components/notes/NoteForm';
|
||||||
|
|
||||||
import ADD_NOTE_MUTATION from '@/graphql/gql/mutations/addNote.gql';
|
|
||||||
import CONTENT_BLOCK_QUERY from '@/graphql/gql/contentBlockQuery.gql';
|
|
||||||
|
|
||||||
import {mapGetters} from 'vuex';
|
import {mapGetters} from 'vuex';
|
||||||
|
import {constructNoteMutation} from '@/helpers/new-note-mutation.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
|
@ -22,68 +20,32 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['currentContent', 'currentContentBlock'])
|
...mapGetters(['currentContent', 'currentContentBlock', 'currentNoteParent'])
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
addNote(note) {
|
addNote(n) {
|
||||||
const content = this.currentContent;
|
const content = this.currentContent;
|
||||||
const contentBlock = this.currentContentBlock;
|
const contentBlock = this.currentContentBlock;
|
||||||
const text = note.text;
|
const parent = this.currentNoteParent;
|
||||||
|
const text = n.text;
|
||||||
this.$apollo.mutate({
|
let note = {};
|
||||||
mutation: ADD_NOTE_MUTATION,
|
if (content > '') {
|
||||||
variables: {
|
note = {
|
||||||
input: {
|
content,
|
||||||
note: {
|
contentBlock,
|
||||||
content,
|
text
|
||||||
contentBlock,
|
|
||||||
text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
update: (store, {data: {addNote: {note}}}) => {
|
|
||||||
const query = CONTENT_BLOCK_QUERY;
|
|
||||||
const variables = {id: contentBlock};
|
|
||||||
const data = store.readQuery({
|
|
||||||
query,
|
|
||||||
variables
|
|
||||||
});
|
|
||||||
|
|
||||||
const bookmarks = data.contentBlock.bookmarks;
|
|
||||||
|
|
||||||
let index = bookmarks.findIndex(element => {
|
|
||||||
return element.uuid === content;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (index > -1) {
|
|
||||||
let el = bookmarks[index];
|
|
||||||
el.note = note;
|
|
||||||
bookmarks.splice(index, 1, el);
|
|
||||||
}
|
|
||||||
|
|
||||||
data.contentBlock.bookmarks = bookmarks;
|
|
||||||
|
|
||||||
store.writeQuery({
|
|
||||||
data,
|
|
||||||
query,
|
|
||||||
variables
|
|
||||||
});
|
|
||||||
},
|
|
||||||
optimisticResponse: {
|
|
||||||
__typename: 'Mutation',
|
|
||||||
addNote: {
|
|
||||||
__typename: 'AddNotePayload',
|
|
||||||
note: {
|
|
||||||
__typename: 'NoteNode',
|
|
||||||
id: -1,
|
|
||||||
text: text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}).then(() => {
|
} else {
|
||||||
this.$store.dispatch('hideModal');
|
note = {
|
||||||
});
|
parent,
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$apollo
|
||||||
|
.mutate(constructNoteMutation(note))
|
||||||
|
.then(this.hide);
|
||||||
},
|
},
|
||||||
hide() {
|
hide() {
|
||||||
this.$store.dispatch('hideModal');
|
this.$store.dispatch('hideModal');
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
<modal :hide-header="true" :small="true">
|
<modal :hide-header="true" :small="true">
|
||||||
<modal-input v-on:input="localNote.text = $event"
|
<modal-input v-on:input="localNote.text = $event"
|
||||||
placeholder="Notiz erfassen"
|
placeholder="Notiz erfassen"
|
||||||
|
data-cy="bookmark-note"
|
||||||
:value="localNote.text"
|
:value="localNote.text"
|
||||||
></modal-input>
|
></modal-input>
|
||||||
<div slot="footer">
|
<div slot="footer">
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,11 @@ query AssignmentWithSubmissions($id: ID!) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
submissionFeedback {
|
||||||
|
id
|
||||||
|
text
|
||||||
|
final
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,13 @@ fragment AssignmentParts on AssignmentNode {
|
||||||
text
|
text
|
||||||
final
|
final
|
||||||
document
|
document
|
||||||
|
submissionFeedback {
|
||||||
|
id
|
||||||
|
text
|
||||||
|
teacher {
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,12 @@ fragment ChapterParts on ChapterNode {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
description
|
description
|
||||||
|
bookmark {
|
||||||
|
note {
|
||||||
|
id
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
contentBlocks {
|
contentBlocks {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,10 @@ fragment ModuleParts on ModuleNode {
|
||||||
slug
|
slug
|
||||||
heroImage
|
heroImage
|
||||||
solutionsEnabled
|
solutionsEnabled
|
||||||
|
bookmark {
|
||||||
|
note {
|
||||||
|
id
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
mutation UpdateChapterBookmark($input: UpdateChapterBookmarkInput!) {
|
||||||
|
updateChapterBookmark(input: $input) {
|
||||||
|
success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
mutation UpdateSubmissionFeedback($input: UpdateSubmissionFeedbackInput!) {
|
||||||
|
updateSubmissionFeedback(input: $input){
|
||||||
|
successful
|
||||||
|
updatedSubmissionFeedback {
|
||||||
|
id
|
||||||
|
text
|
||||||
|
final
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
mutation UpdateModuleBookmark($input: UpdateModuleBookmarkInput!) {
|
||||||
|
updateModuleBookmark(input: $input) {
|
||||||
|
success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,12 @@ query StudentSubmissions($id: ID!) {
|
||||||
}
|
}
|
||||||
assignment {
|
assignment {
|
||||||
title
|
title
|
||||||
|
assignment
|
||||||
|
}
|
||||||
|
submissionFeedback {
|
||||||
|
id
|
||||||
|
text
|
||||||
|
final
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
import ADD_NOTE_MUTATION from '@/graphql/gql/mutations/addNote.gql';
|
||||||
|
import CONTENT_BLOCK_QUERY from '@/graphql/gql/contentBlockQuery.gql';
|
||||||
|
import CHAPTER_QUERY from '@/graphql/gql/chapterQuery.gql';
|
||||||
|
import MODULE_QUERY from '@/graphql/gql/moduleByIdQuery.gql';
|
||||||
|
|
||||||
|
const getBlockType = id => atob(id).split(':')[0]
|
||||||
|
|
||||||
|
export const constructNoteMutation = (n) => {
|
||||||
|
let update = () => {
|
||||||
|
};
|
||||||
|
|
||||||
|
if (n.contentBlock) { // has a content block, so it is a content block bookmark
|
||||||
|
update = (store, {data: {addNote: {note}}}) => {
|
||||||
|
const query = CONTENT_BLOCK_QUERY;
|
||||||
|
const variables = {id: n.contentBlock};
|
||||||
|
const data = store.readQuery({
|
||||||
|
query,
|
||||||
|
variables
|
||||||
|
});
|
||||||
|
|
||||||
|
const bookmarks = data.contentBlock.bookmarks;
|
||||||
|
|
||||||
|
let index = bookmarks.findIndex(element => {
|
||||||
|
return element.uuid === n.content;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (index > -1) {
|
||||||
|
let el = bookmarks[index];
|
||||||
|
el.note = note;
|
||||||
|
bookmarks.splice(index, 1, el);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.contentBlock.bookmarks = bookmarks;
|
||||||
|
|
||||||
|
store.writeQuery({
|
||||||
|
data,
|
||||||
|
query,
|
||||||
|
variables
|
||||||
|
});
|
||||||
|
};
|
||||||
|
} else { // it's a chapter bookmark or a module bookmark
|
||||||
|
update = (store, {data: {addNote: {note}}}) => {
|
||||||
|
const type = getBlockType(n.parent) === 'ChapterNode' ? 'chapter' : 'module';
|
||||||
|
const query = type === 'chapter' ? CHAPTER_QUERY : MODULE_QUERY;
|
||||||
|
const variables = {id: n.parent};
|
||||||
|
const data = store.readQuery({
|
||||||
|
query,
|
||||||
|
variables
|
||||||
|
});
|
||||||
|
|
||||||
|
const bookmark = data[type].bookmark;
|
||||||
|
|
||||||
|
bookmark.note = note;
|
||||||
|
|
||||||
|
data[type].bookmark = bookmark;
|
||||||
|
|
||||||
|
store.writeQuery({
|
||||||
|
data,
|
||||||
|
query,
|
||||||
|
variables
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
mutation: ADD_NOTE_MUTATION,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
note: n
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update,
|
||||||
|
optimisticResponse: {
|
||||||
|
__typename: 'Mutation',
|
||||||
|
addNote: {
|
||||||
|
__typename: 'AddNotePayload',
|
||||||
|
note: {
|
||||||
|
__typename: 'NoteNode',
|
||||||
|
id: -1,
|
||||||
|
text: n.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,37 +1,71 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="article submission-page">
|
<div class="submission-page">
|
||||||
<div class="article__header">
|
<div class="submission-page__header submission-header">
|
||||||
<h1 class="article__title">{{studentSubmission.assignment.title}}</h1>
|
<h2>Aufgabe</h2>
|
||||||
<h2 class="article__subtitle">{{fullName}}</h2>
|
<p>{{studentSubmission.assignment.assignment}}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="article__content article-content">
|
<div class="submission-page__content submission-content">
|
||||||
|
<h2>Ergebnis von {{fullName}}</h2>
|
||||||
|
<p v-html="text"></p>
|
||||||
<p v-if="studentSubmission.document && studentSubmission.document.length > 0" class="article-content__document">
|
<p v-if="studentSubmission.document && studentSubmission.document.length > 0" class="article-content__document">
|
||||||
<a :href="studentSubmission.document" class="entry-document__link link" target="_blank">
|
<a :href="studentSubmission.document" class="entry-document__link link" target="_blank">
|
||||||
<student-submission-document :document="studentSubmission.document"></student-submission-document>
|
<student-submission-document :document="studentSubmission.document"></student-submission-document>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p class="article-content__text" v-html="text"></p>
|
</div>
|
||||||
|
<div class="submission-page__feedback feedback">
|
||||||
|
<submission-form
|
||||||
|
v-if="studentSubmission"
|
||||||
|
@turnIn="turnIn"
|
||||||
|
@saveInput="saveInput"
|
||||||
|
@reopen="reopen"
|
||||||
|
:user-input="feedback"
|
||||||
|
placholder="Feedback erfassen"
|
||||||
|
action="Feedback teilen"
|
||||||
|
shared-msg="Dieses Feedback wurde geteilt."
|
||||||
|
:saved="!unsaved"
|
||||||
|
>
|
||||||
|
<div v-if="!final" class="feedback-submission__emojis emojis">
|
||||||
|
<span v-for="(emoji, index) in emojis"
|
||||||
|
:key="index"
|
||||||
|
@click="addEmoji(emoji)"
|
||||||
|
class="emojis__emoji">{{emoji}}</span>
|
||||||
|
</div>
|
||||||
|
</submission-form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {newLineToParagraph} from '@/helpers/text';
|
import {newLineToParagraph} from '@/helpers/text';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
|
||||||
import StudentSubmissionDocument from '@/components/StudentSubmissionDocument';
|
import StudentSubmissionDocument from '@/components/StudentSubmissionDocument';
|
||||||
import STUDENT_SUBMISSIONS_QUERY from '@/graphql/gql/studentSubmissionQuery.gql';
|
import STUDENT_SUBMISSIONS_QUERY from '@/graphql/gql/studentSubmissionQuery.gql';
|
||||||
|
import UPDATE_FEEDBACK_MUTATION from '@/graphql/gql/mutations/updateFeedback.gql';
|
||||||
|
import SubmissionForm from '@/components/content-blocks/assignment/SubmissionForm';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
StudentSubmissionDocument
|
StudentSubmissionDocument,
|
||||||
|
SubmissionForm
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
final() {
|
||||||
|
return !!this.studentSubmission && this.studentSubmission.final;
|
||||||
|
},
|
||||||
text() {
|
text() {
|
||||||
return newLineToParagraph(this.studentSubmission.text);
|
return newLineToParagraph(this.studentSubmission.text);
|
||||||
},
|
},
|
||||||
fullName() {
|
fullName() {
|
||||||
return `${this.studentSubmission.student.firstName} ${this.studentSubmission.student.lastName}`
|
return `${this.studentSubmission.student.firstName} ${this.studentSubmission.student.lastName}`
|
||||||
|
},
|
||||||
|
feedback() {
|
||||||
|
return this.studentSubmission.submissionFeedback ? this.studentSubmission.submissionFeedback : {
|
||||||
|
text: '',
|
||||||
|
final: false
|
||||||
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -48,6 +82,98 @@
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
addEmoji(emoji) {
|
||||||
|
const feedbackText = this.feedback.text + emoji;
|
||||||
|
this.updateFeedbackText(feedbackText);
|
||||||
|
},
|
||||||
|
_save: debounce(function () {
|
||||||
|
this.saving++;
|
||||||
|
this.$apollo.mutate({
|
||||||
|
mutation: UPDATE_FEEDBACK_MUTATION,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
submissionFeedback: {
|
||||||
|
studentSubmission: this.studentSubmission.id,
|
||||||
|
text: this.studentSubmission.submissionFeedback.text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update: this.updateCache
|
||||||
|
}).then(() => {
|
||||||
|
this.saving--;
|
||||||
|
if (this.saving === 0) {
|
||||||
|
this.unsaved = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 500),
|
||||||
|
saveInput: function (feedbackText) {
|
||||||
|
this.unsaved = true;
|
||||||
|
/*
|
||||||
|
We update the assignment on this component, so the changes are reflected on it. The server does not return
|
||||||
|
the updated entity, to prevent the UI to update when the user is entering his input
|
||||||
|
*/
|
||||||
|
this.updateFeedbackText(feedbackText);
|
||||||
|
this._save();
|
||||||
|
},
|
||||||
|
turnIn() {
|
||||||
|
this.$apollo.mutate({
|
||||||
|
mutation: UPDATE_FEEDBACK_MUTATION,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
submissionFeedback: {
|
||||||
|
studentSubmission: this.studentSubmission.id,
|
||||||
|
text: this.studentSubmission.submissionFeedback.text,
|
||||||
|
final: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update: this.updateCache
|
||||||
|
});
|
||||||
|
},
|
||||||
|
reopen() {
|
||||||
|
if (!this.studentSubmission.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$apollo.mutate({
|
||||||
|
mutation: UPDATE_FEEDBACK_MUTATION,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
submissionFeedback: {
|
||||||
|
studentSubmission: this.studentSubmission.id,
|
||||||
|
text: this.studentSubmission.submissionFeedback.text,
|
||||||
|
final: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update: this.updateCache
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateCache(store, {data: {updateSubmissionFeedback: {successful, updatedSubmissionFeedback}}}) {
|
||||||
|
try {
|
||||||
|
if (successful) {
|
||||||
|
const query = STUDENT_SUBMISSIONS_QUERY;
|
||||||
|
const variables = {
|
||||||
|
id: this.studentSubmission.id
|
||||||
|
};
|
||||||
|
const data = store.readQuery({query, variables});
|
||||||
|
|
||||||
|
data.studentSubmission.submissionFeedback = Object.assign({}, updatedSubmissionFeedback);
|
||||||
|
store.writeQuery({query, variables, data});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
// Query did not exist in the cache, and apollo throws a generic Error. Do nothing
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateFeedbackText(text) {
|
||||||
|
this.studentSubmission = Object.assign({}, this.studentSubmission, {
|
||||||
|
submissionFeedback: Object.assign({}, this.studentSubmission.submissionFeedback, {text: text})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
studentSubmission: {
|
studentSubmission: {
|
||||||
|
|
@ -59,21 +185,39 @@
|
||||||
lastName: ''
|
lastName: ''
|
||||||
},
|
},
|
||||||
text: '',
|
text: '',
|
||||||
document: ''
|
document: '',
|
||||||
}
|
submissionFeedback: {
|
||||||
|
text: '',
|
||||||
|
final: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unsaved: false,
|
||||||
|
saving: 0,
|
||||||
|
emojis: ['👍', '👎', '🙂', '😐', '😕', '🙁', '😮', '😉', '🙄', '❕', '❔', '🧐', '🤩', '🤗', '🤬', '🤢']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.article-content {
|
@import "@/styles/_variables.scss";
|
||||||
&__document {
|
@import "@/styles/_functions.scss";
|
||||||
margin-bottom: 1rem;
|
|
||||||
|
.submission-page {
|
||||||
|
&__content {
|
||||||
|
margin-top: 1.5*$large-spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__text /deep/ > p {
|
&__feedback {
|
||||||
margin-bottom: 1em;
|
margin-top: $large-spacing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emojis {
|
||||||
|
font-size: toRem(32px);
|
||||||
|
|
||||||
|
&__emoji {
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="submissions-page">
|
<div class="submissions-page">
|
||||||
<div>
|
<h2 class="submissions-page__heading">Aufgabe</h2>
|
||||||
<a class="button button--primary submissions-page__back" @click="back">Zurück zur Aufgabe</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<assignment-with-submissions v-if="!$apollo.queries.assignment.loading"
|
<assignment-with-submissions v-if="!$apollo.queries.assignment.loading"
|
||||||
:assignment="assignment"></assignment-with-submissions>
|
:assignment="assignment"
|
||||||
|
@back="back"></assignment-with-submissions>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import AssignmentWithSubmissions from '@/components/AssignmentWithSubmissions';
|
import AssignmentWithSubmissions from '@/components/AssignmentWithSubmissions';
|
||||||
|
|
||||||
import ASSIGNMENT_WITH_SUBMISSIONS_QUERY from '@/graphql/gql/assignmentWithSubmissionsQuery.gql';
|
import ASSIGNMENT_WITH_SUBMISSIONS_QUERY from '@/graphql/gql/assignmentWithSubmissionsQuery.gql';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -54,15 +51,21 @@
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import "@/styles/_mixins.scss";
|
@import "@/styles/_mixins.scss";
|
||||||
|
@import "@/styles/_variables.scss";
|
||||||
|
|
||||||
.submissions-page {
|
.submissions-page {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
||||||
grid-row-gap: $large-spacing;
|
|
||||||
grid-template-rows: auto 1fr;
|
grid-template-rows: auto 1fr;
|
||||||
|
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-left: $large-spacing;
|
||||||
|
margin-right: $large-spacing;
|
||||||
|
|
||||||
@include desktop {
|
@include desktop {
|
||||||
width: 800px;
|
margin-left: $medium-spacing;
|
||||||
|
margin-right: $medium-spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ export default new Vuex.Store({
|
||||||
parentProject: null,
|
parentProject: null,
|
||||||
currentNote: null,
|
currentNote: null,
|
||||||
currentProjectEntry: null,
|
currentProjectEntry: null,
|
||||||
|
currentNoteParent: '',
|
||||||
imageUrl: '',
|
imageUrl: '',
|
||||||
infographic: {
|
infographic: {
|
||||||
id: 0,
|
id: 0,
|
||||||
|
|
@ -46,6 +47,7 @@ export default new Vuex.Store({
|
||||||
currentContent: state => state.currentContent,
|
currentContent: state => state.currentContent,
|
||||||
currentContentBlock: state => state.currentContentBlock,
|
currentContentBlock: state => state.currentContentBlock,
|
||||||
currentNote: state => state.currentNote,
|
currentNote: state => state.currentNote,
|
||||||
|
currentNoteParent: state => state.currentNoteParent,
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
|
@ -61,6 +63,7 @@ export default new Vuex.Store({
|
||||||
commit('setCurrentRoomEntry', '');
|
commit('setCurrentRoomEntry', '');
|
||||||
commit('setCurrentContent', '');
|
commit('setCurrentContent', '');
|
||||||
commit('setCurrentContentBlock', '');
|
commit('setCurrentContentBlock', '');
|
||||||
|
commit('setCurrentNoteParent', '');
|
||||||
commit('setContentBlockPosition', {});
|
commit('setContentBlockPosition', {});
|
||||||
commit('setParentRoom', null);
|
commit('setParentRoom', null);
|
||||||
commit('setParentModule', '');
|
commit('setParentModule', '');
|
||||||
|
|
@ -126,8 +129,12 @@ export default new Vuex.Store({
|
||||||
dispatch('showModal', 'edit-project-entry-wizard');
|
dispatch('showModal', 'edit-project-entry-wizard');
|
||||||
},
|
},
|
||||||
addNote({commit, dispatch}, payload) {
|
addNote({commit, dispatch}, payload) {
|
||||||
commit('setCurrentContentBlock', payload.contentBlock);
|
if (payload.contentBlock) {
|
||||||
commit('setCurrentContent', payload.content);
|
commit('setCurrentContentBlock', payload.contentBlock);
|
||||||
|
commit('setCurrentContent', payload.content);
|
||||||
|
} else {
|
||||||
|
commit('setCurrentNoteParent', payload.parent);
|
||||||
|
}
|
||||||
dispatch('showModal', 'new-note-wizard');
|
dispatch('showModal', 'new-note-wizard');
|
||||||
},
|
},
|
||||||
editNote({commit, dispatch}, payload) {
|
editNote({commit, dispatch}, payload) {
|
||||||
|
|
@ -240,6 +247,9 @@ export default new Vuex.Store({
|
||||||
},
|
},
|
||||||
setEditModule(state, payload) {
|
setEditModule(state, payload) {
|
||||||
state.editModule = payload;
|
state.editModule = payload;
|
||||||
|
},
|
||||||
|
setCurrentNoteParent(state, payload) {
|
||||||
|
state.currentNoteParent = payload;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
@import "@/styles/_variables.scss";
|
||||||
|
@import "@/styles/_mixins.scss";
|
||||||
|
|
||||||
|
.student-submission-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 100px 1fr 1fr;
|
||||||
|
grid-column-gap: 20px;
|
||||||
|
|
||||||
|
@include desktop {
|
||||||
|
grid-template-columns: 170px 1fr 1fr;
|
||||||
|
grid-column-gap: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid $color-silver-dark;
|
||||||
|
padding: 15px 0;
|
||||||
|
}
|
||||||
|
|
@ -20,3 +20,4 @@
|
||||||
@import "solutions";
|
@import "solutions";
|
||||||
@import "password_forms";
|
@import "password_forms";
|
||||||
@import "public-page";
|
@import "public-page";
|
||||||
|
@import "student-submission";
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -3,7 +3,7 @@ import random
|
||||||
import factory
|
import factory
|
||||||
|
|
||||||
from books.factories import ModuleFactory
|
from books.factories import ModuleFactory
|
||||||
from .models import Assignment, StudentSubmission
|
from .models import Assignment, StudentSubmission, SubmissionFeedback
|
||||||
|
|
||||||
from core.factories import fake
|
from core.factories import fake
|
||||||
|
|
||||||
|
|
@ -24,3 +24,12 @@ class StudentSubmissionFactory(factory.django.DjangoModelFactory):
|
||||||
text = factory.LazyAttribute(lambda x: fake.sentence(nb_words=random.randint(4, 8)))
|
text = factory.LazyAttribute(lambda x: fake.sentence(nb_words=random.randint(4, 8)))
|
||||||
assignment = factory.SubFactory(AssignmentFactory)
|
assignment = factory.SubFactory(AssignmentFactory)
|
||||||
final = False
|
final = False
|
||||||
|
|
||||||
|
|
||||||
|
class SubmissionFeedbackFactory(factory.django.DjangoModelFactory):
|
||||||
|
class Meta:
|
||||||
|
model = SubmissionFeedback
|
||||||
|
|
||||||
|
text = factory.LazyAttribute(lambda x: fake.sentence(nb_words=random.randint(4, 8)))
|
||||||
|
student_submission = factory.SubFactory(StudentSubmissionFactory)
|
||||||
|
final = False
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Generated by Django 2.0.6 on 2019-11-12 14:13
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django_extensions.db.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('assignments', '0005_assignment_solution'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SubmissionFeedback',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
|
||||||
|
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
|
||||||
|
('text', models.TextField(blank=True)),
|
||||||
|
('student_submission', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='feedback', to='assignments.StudentSubmission')),
|
||||||
|
('teacher', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedbacks', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ('-modified', '-created'),
|
||||||
|
'get_latest_by': 'modified',
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.0.6 on 2019-11-13 12:47
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('assignments', '0006_submissionfeedback'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='submissionfeedback',
|
||||||
|
name='final',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Generated by Django 2.0.6 on 2019-11-13 14:30
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('assignments', '0007_submissionfeedback_final'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='submissionfeedback',
|
||||||
|
name='student_submission',
|
||||||
|
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='assignments.StudentSubmission'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 2.0.6 on 2019-11-13 14:33
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('assignments', '0008_auto_20191113_1430'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='submissionfeedback',
|
||||||
|
name='id',
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='submissionfeedback',
|
||||||
|
name='student_submission',
|
||||||
|
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='assignments.StudentSubmission'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Generated by Django 2.0.6 on 2019-12-10 14:27
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('assignments', '0009_auto_20191113_1433'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='submissionfeedback',
|
||||||
|
name='student_submission',
|
||||||
|
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='submission_feedback', serialize=False, to='assignments.StudentSubmission'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -37,3 +37,11 @@ class StudentSubmission(TimeStampedModel):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '{} - {}'.format(self.student.full_name, self.text)
|
return '{} - {}'.format(self.student.full_name, self.text)
|
||||||
|
|
||||||
|
|
||||||
|
class SubmissionFeedback(TimeStampedModel):
|
||||||
|
text = models.TextField(blank=True)
|
||||||
|
teacher = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, related_name='feedbacks')
|
||||||
|
student_submission = models.OneToOneField(StudentSubmission, on_delete=models.CASCADE, primary_key=True, related_name='submission_feedback')
|
||||||
|
final = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,10 @@ class AssignmentInput(InputObjectType):
|
||||||
answer = graphene.String(required=True)
|
answer = graphene.String(required=True)
|
||||||
document = graphene.String()
|
document = graphene.String()
|
||||||
final = graphene.Boolean()
|
final = graphene.Boolean()
|
||||||
|
|
||||||
|
|
||||||
|
class SubmissionFeedbackInput(InputObjectType):
|
||||||
|
id = graphene.ID()
|
||||||
|
student_submission = graphene.ID(required=True)
|
||||||
|
text = graphene.String(required=True)
|
||||||
|
final = graphene.Boolean()
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import graphene
|
import graphene
|
||||||
from graphene import relay
|
from graphene import relay
|
||||||
|
from graphql_relay import from_global_id
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
from api.utils import get_object
|
from api.utils import get_object
|
||||||
from assignments.models import Assignment
|
from assignments.models import Assignment, SubmissionFeedback
|
||||||
from assignments.schema.types import AssignmentNode, StudentSubmissionNode
|
from assignments.schema.types import AssignmentNode, StudentSubmissionNode, SubmissionFeedbackNode
|
||||||
from .inputs import AssignmentInput
|
from .inputs import AssignmentInput, SubmissionFeedbackInput
|
||||||
|
|
||||||
|
|
||||||
class UpdateAssignment(relay.ClientIDMutation):
|
class UpdateAssignment(relay.ClientIDMutation):
|
||||||
|
|
@ -30,5 +32,35 @@ class UpdateAssignment(relay.ClientIDMutation):
|
||||||
return cls(successful=True, updated_assignment=assignment, submission=submission, errors=None)
|
return cls(successful=True, updated_assignment=assignment, submission=submission, errors=None)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateSubmissionFeedback(relay.ClientIDMutation):
|
||||||
|
class Input:
|
||||||
|
submission_feedback = graphene.Argument(SubmissionFeedbackInput)
|
||||||
|
|
||||||
|
updated_submission_feedback = graphene.Field(SubmissionFeedbackNode)
|
||||||
|
successful = graphene.Boolean()
|
||||||
|
errors = graphene.List(graphene.String)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def mutate_and_get_payload(cls, root, info, **kwargs):
|
||||||
|
submission_feedback_data = kwargs.get('submission_feedback')
|
||||||
|
user = info.context.user
|
||||||
|
student_submission_id = from_global_id(submission_feedback_data['student_submission'])[1]
|
||||||
|
|
||||||
|
if not user.has_perm('users.can_manage_school_class_content'):
|
||||||
|
raise PermissionDenied('Missing permissions')
|
||||||
|
|
||||||
|
(submission_feedback, created) = SubmissionFeedback.objects.get_or_create(teacher=user,
|
||||||
|
student_submission_id=student_submission_id)
|
||||||
|
|
||||||
|
final = submission_feedback_data.get('final') if 'final' in submission_feedback_data else submission_feedback.final
|
||||||
|
|
||||||
|
submission_feedback.final = final
|
||||||
|
submission_feedback.text = submission_feedback_data.get('text')
|
||||||
|
submission_feedback.save()
|
||||||
|
|
||||||
|
return cls(successful=True, updated_submission_feedback=submission_feedback, errors=None)
|
||||||
|
|
||||||
|
|
||||||
class AssignmentMutations(object):
|
class AssignmentMutations(object):
|
||||||
update_assignment = UpdateAssignment.Field()
|
update_assignment = UpdateAssignment.Field()
|
||||||
|
update_submission_feedback = UpdateSubmissionFeedback.Field()
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,48 @@
|
||||||
import graphene
|
import graphene
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||||
from graphene import relay
|
from graphene import relay
|
||||||
from graphene_django import DjangoObjectType
|
from graphene_django import DjangoObjectType
|
||||||
|
|
||||||
from assignments.models import Assignment, StudentSubmission
|
from assignments.models import Assignment, StudentSubmission, SubmissionFeedback
|
||||||
from books.utils import are_solutions_enabled_for
|
from books.utils import are_solutions_enabled_for
|
||||||
|
|
||||||
|
|
||||||
|
class SubmissionFeedbackNode(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = SubmissionFeedback
|
||||||
|
filter_fields = []
|
||||||
|
interfaces = (relay.Node,)
|
||||||
|
|
||||||
|
|
||||||
class StudentSubmissionNode(DjangoObjectType):
|
class StudentSubmissionNode(DjangoObjectType):
|
||||||
|
submission_feedback = graphene.Field(SubmissionFeedbackNode)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = StudentSubmission
|
model = StudentSubmission
|
||||||
filter_fields = []
|
filter_fields = []
|
||||||
interfaces = (relay.Node,)
|
interfaces = (relay.Node,)
|
||||||
|
|
||||||
|
def resolve_submission_feedback(self, info, **kwargs):
|
||||||
|
|
||||||
|
user = info.context.user
|
||||||
|
|
||||||
|
if not hasattr(self, 'submission_feedback'):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# teacher path
|
||||||
|
if user.has_perm('users.can_manage_school_class_content'):
|
||||||
|
if self.submission_feedback.teacher == user:
|
||||||
|
return self.submission_feedback
|
||||||
|
else:
|
||||||
|
raise PermissionDenied('Missing permissions')
|
||||||
|
|
||||||
|
# student path
|
||||||
|
|
||||||
|
if self.submission_feedback.final:
|
||||||
|
return self.submission_feedback
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class AssignmentNode(DjangoObjectType):
|
class AssignmentNode(DjangoObjectType):
|
||||||
submission = graphene.Field(StudentSubmissionNode)
|
submission = graphene.Field(StudentSubmissionNode)
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ class AssignmentPermissionsTestCase(DefaultUserTestCase):
|
||||||
self.assignment_id = to_global_id('AssignmentNode', self.assignment.pk)
|
self.assignment_id = to_global_id('AssignmentNode', self.assignment.pk)
|
||||||
self.module_id = to_global_id('ModuleNode', self.assignment.module.pk)
|
self.module_id = to_global_id('ModuleNode', self.assignment.module.pk)
|
||||||
|
|
||||||
|
|
||||||
def _submit_submission(self, user=None):
|
def _submit_submission(self, user=None):
|
||||||
mutation = '''
|
mutation = '''
|
||||||
mutation UpdateAssignment($input: UpdateAssignmentInput!) {
|
mutation UpdateAssignment($input: UpdateAssignmentInput!) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,203 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# ITerativ GmbH
|
||||||
|
# http://www.iterativ.ch/
|
||||||
|
#
|
||||||
|
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
|
||||||
|
#
|
||||||
|
# Created on 2019-11-13
|
||||||
|
# @author: chrigu <christian.cueni@iterativ.ch>
|
||||||
|
|
||||||
|
from graphql_relay import to_global_id
|
||||||
|
|
||||||
|
from api.test_utils import create_client, DefaultUserTestCase
|
||||||
|
from assignments.models import Assignment, StudentSubmission
|
||||||
|
from users.factories import SchoolClassFactory
|
||||||
|
from ..factories import AssignmentFactory, StudentSubmissionFactory, SubmissionFeedbackFactory
|
||||||
|
|
||||||
|
|
||||||
|
class SubmissionFeedbackTestCase(DefaultUserTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(SubmissionFeedbackTestCase, self).setUp()
|
||||||
|
|
||||||
|
self.assignment = AssignmentFactory(
|
||||||
|
owner=self.teacher
|
||||||
|
)
|
||||||
|
self.assignment_id = to_global_id('AssignmentNode', self.assignment.pk)
|
||||||
|
|
||||||
|
self.student_submission = StudentSubmissionFactory(assignment=self.assignment, student=self.student1, final=False)
|
||||||
|
self.student_submission_id = to_global_id('StudentSubmissionNode', self.student_submission.pk)
|
||||||
|
|
||||||
|
school_class = SchoolClassFactory()
|
||||||
|
school_class.users.add(self.student1)
|
||||||
|
school_class.users.add(self.teacher)
|
||||||
|
school_class.users.add(self.teacher2)
|
||||||
|
|
||||||
|
def _create_submission_feedback(self, user, final, text, student_submission_id):
|
||||||
|
mutation = '''
|
||||||
|
mutation UpdateSubmissionFeedback($input: UpdateSubmissionFeedbackInput!) {
|
||||||
|
updateSubmissionFeedback(input: $input){
|
||||||
|
updatedSubmissionFeedback {
|
||||||
|
id
|
||||||
|
text
|
||||||
|
final
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
client = create_client(user)
|
||||||
|
|
||||||
|
return client.execute(mutation, variables={
|
||||||
|
'input': {
|
||||||
|
"submissionFeedback": {
|
||||||
|
"studentSubmission": student_submission_id,
|
||||||
|
"text": text,
|
||||||
|
"final": final
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
def _fetch_assignment_student(self, user):
|
||||||
|
client = create_client(user)
|
||||||
|
query = '''
|
||||||
|
query AssignmentWithSubmissions($id: ID!) {
|
||||||
|
assignment(id: $id) {
|
||||||
|
title
|
||||||
|
submission {
|
||||||
|
id
|
||||||
|
text
|
||||||
|
document
|
||||||
|
submissionFeedback {
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
return client.execute(query, variables={
|
||||||
|
'id': self.assignment_id
|
||||||
|
})
|
||||||
|
|
||||||
|
def _fetch_assignment_teacher(self, user):
|
||||||
|
client = create_client(user)
|
||||||
|
query = '''
|
||||||
|
query AssignmentWithSubmissions($id: ID!) {
|
||||||
|
assignment(id: $id) {
|
||||||
|
title
|
||||||
|
submissions {
|
||||||
|
id
|
||||||
|
text
|
||||||
|
document
|
||||||
|
submissionFeedback {
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
return client.execute(query, variables={
|
||||||
|
'id': self.assignment_id
|
||||||
|
})
|
||||||
|
|
||||||
|
def _fetch_submission_feedback(self, user):
|
||||||
|
client = create_client(user)
|
||||||
|
query = '''
|
||||||
|
query AssignmentWithSubmissions($id: ID!) {
|
||||||
|
assignment(id: $id) {
|
||||||
|
title
|
||||||
|
submissions {
|
||||||
|
id
|
||||||
|
text
|
||||||
|
document
|
||||||
|
submissionFeedback {
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
return client.execute(query, variables={
|
||||||
|
'id': self.assignment_id
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_teacher_can_create_feedback(self):
|
||||||
|
|
||||||
|
result = self._create_submission_feedback(self.teacher, False, 'Balalal', self.student_submission_id)
|
||||||
|
|
||||||
|
self.assertIsNone(result.get('errors'))
|
||||||
|
self.assertIsNotNone(result.get('data').get('updateSubmissionFeedback').get('updatedSubmissionFeedback').get('id'))
|
||||||
|
|
||||||
|
def test_student_cannot_create_feedback(self):
|
||||||
|
|
||||||
|
result = self._create_submission_feedback(self.student1, False, 'Balalal', self.student_submission_id)
|
||||||
|
self.assertIsNotNone(result.get('errors'))
|
||||||
|
|
||||||
|
def test_teacher_can_update_feedback(self):
|
||||||
|
|
||||||
|
assignment = AssignmentFactory(
|
||||||
|
owner=self.teacher
|
||||||
|
)
|
||||||
|
|
||||||
|
student_submission = StudentSubmissionFactory(assignment=assignment, student=self.student1, final=False)
|
||||||
|
submission_feedback = SubmissionFeedbackFactory(teacher=self.teacher, final=False,
|
||||||
|
student_submission=student_submission)
|
||||||
|
submission_feedback_id = to_global_id('SubmissionFeedback', submission_feedback.pk)
|
||||||
|
|
||||||
|
result = self._create_submission_feedback(self.teacher, True, 'Some', submission_feedback_id)
|
||||||
|
|
||||||
|
self.assertIsNone(result.get('errors'))
|
||||||
|
|
||||||
|
submission_feedback_response = result.get('data').get('updateSubmissionFeedback').get('updatedSubmissionFeedback')
|
||||||
|
|
||||||
|
self.assertTrue(submission_feedback_response.get('final'))
|
||||||
|
self.assertEqual(submission_feedback_response.get('text'), 'Some')
|
||||||
|
|
||||||
|
def test_rogue_teacher_cannot_update_feedback(self):
|
||||||
|
|
||||||
|
assignment = AssignmentFactory(
|
||||||
|
owner=self.teacher
|
||||||
|
)
|
||||||
|
|
||||||
|
student_submission = StudentSubmissionFactory(assignment=assignment, student=self.student1, final=False)
|
||||||
|
submission_feedback = SubmissionFeedbackFactory(teacher=self.teacher, final=False,
|
||||||
|
student_submission=student_submission)
|
||||||
|
submission_feedback_id = to_global_id('SubmissionFeedback', submission_feedback.pk)
|
||||||
|
|
||||||
|
result = self._create_submission_feedback(self.teacher2, True, 'Some', submission_feedback_id)
|
||||||
|
|
||||||
|
self.assertIsNotNone(result.get('errors'))
|
||||||
|
|
||||||
|
def test_student_does_not_see_non_final_feedback(self):
|
||||||
|
|
||||||
|
SubmissionFeedbackFactory(teacher=self.teacher, final=False, student_submission=self.student_submission)
|
||||||
|
result = self._fetch_assignment_student(self.student1)
|
||||||
|
|
||||||
|
self.assertIsNone(result.get('data').get('submissionFeedback'))
|
||||||
|
|
||||||
|
def test_student_does_see_final_feedback(self):
|
||||||
|
|
||||||
|
submission_feedback = SubmissionFeedbackFactory(teacher=self.teacher, final=True,
|
||||||
|
student_submission=self.student_submission)
|
||||||
|
result = self._fetch_assignment_student(self.student1)
|
||||||
|
self.assertEqual(result.get('data').get('assignment').get('submission').get('submissionFeedback')
|
||||||
|
.get('text'), submission_feedback.text)
|
||||||
|
|
||||||
|
def test_teacher_can_see_feedback_for_submission(self):
|
||||||
|
submission_feedback = SubmissionFeedbackFactory(teacher=self.teacher, final=False,
|
||||||
|
student_submission=self.student_submission)
|
||||||
|
self.student_submission.final = True
|
||||||
|
self.student_submission.save()
|
||||||
|
|
||||||
|
result = self._fetch_assignment_teacher(self.teacher)
|
||||||
|
self.assertEqual(result.get('data').get('assignment').get('submissions')[0].get('submissionFeedback')
|
||||||
|
.get('text'), submission_feedback.text)
|
||||||
|
|
||||||
|
def test_rogue_teacher_cannot_see_feedback(self):
|
||||||
|
SubmissionFeedbackFactory(teacher=self.teacher, final=False,
|
||||||
|
student_submission=self.student_submission)
|
||||||
|
self.student_submission.final = True
|
||||||
|
self.student_submission.save()
|
||||||
|
|
||||||
|
result = self._fetch_assignment_teacher(self.teacher2)
|
||||||
|
self.assertIsNone(result.get('data').get('assignment').get('submissions')[0].get('submissionFeedback'))
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Generated by Django 2.0.6 on 2019-11-28 16:01
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import wagtail.core.blocks
|
||||||
|
import wagtail.core.fields
|
||||||
|
import wagtail.images.blocks
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('basicknowledge', '0003_auto_20190912_1228'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='basicknowledge',
|
||||||
|
name='contents',
|
||||||
|
field=wagtail.core.fields.StreamField([('text_block', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.RichTextBlock(features=['bold', 'ul']))])), ('image_block', wagtail.images.blocks.ImageChooserBlock()), ('link_block', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.TextBlock()), ('url', wagtail.core.blocks.URLBlock())])), ('video_block', wagtail.core.blocks.StructBlock([('url', wagtail.core.blocks.URLBlock())])), ('document_block', wagtail.core.blocks.StructBlock([('url', wagtail.core.blocks.URLBlock())])), ('section_title', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.TextBlock())])), ('infogram_block', wagtail.core.blocks.StructBlock([('id', wagtail.core.blocks.TextBlock()), ('title', wagtail.core.blocks.TextBlock())])), ('genially_block', wagtail.core.blocks.StructBlock([('id', wagtail.core.blocks.TextBlock())])), ('thinglink_block', wagtail.core.blocks.StructBlock([('id', wagtail.core.blocks.TextBlock())])), ('subtitle', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.TextBlock())]))], blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Generated by Django 2.0.6 on 2019-11-28 16:01
|
||||||
|
|
||||||
|
import assignments.models
|
||||||
|
from django.db import migrations
|
||||||
|
import surveys.models
|
||||||
|
import wagtail.core.blocks
|
||||||
|
import wagtail.core.fields
|
||||||
|
import wagtail.images.blocks
|
||||||
|
import wagtail.snippets.blocks
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('books', '0015_contentblock_bookmarks'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='contentblock',
|
||||||
|
name='contents',
|
||||||
|
field=wagtail.core.fields.StreamField([('text_block', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.RichTextBlock(features=['ul']))])), ('basic_knowledge', wagtail.core.blocks.StructBlock([('description', wagtail.core.blocks.RichTextBlock(required=False)), ('basic_knowledge', wagtail.core.blocks.PageChooserBlock(required=True, target_model=['basicknowledge.BasicKnowledge']))])), ('assignment', wagtail.core.blocks.StructBlock([('assignment_id', wagtail.snippets.blocks.SnippetChooserBlock(assignments.models.Assignment))])), ('survey', wagtail.core.blocks.StructBlock([('survey_id', wagtail.snippets.blocks.SnippetChooserBlock(surveys.models.Survey))])), ('image_block', wagtail.images.blocks.ImageChooserBlock()), ('image_url_block', wagtail.core.blocks.StructBlock([('title', wagtail.core.blocks.TextBlock()), ('url', wagtail.core.blocks.URLBlock())])), ('link_block', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.TextBlock()), ('url', wagtail.core.blocks.URLBlock())])), ('solution', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.RichTextBlock(features=['ul']))], icon='tick')), ('video_block', wagtail.core.blocks.StructBlock([('url', wagtail.core.blocks.URLBlock())])), ('document_block', wagtail.core.blocks.StructBlock([('url', wagtail.core.blocks.URLBlock())])), ('infogram_block', wagtail.core.blocks.StructBlock([('id', wagtail.core.blocks.TextBlock()), ('title', wagtail.core.blocks.TextBlock())])), ('genially_block', wagtail.core.blocks.StructBlock([('id', wagtail.core.blocks.TextBlock())])), ('thinglink_block', wagtail.core.blocks.StructBlock([('id', wagtail.core.blocks.TextBlock())])), ('subtitle', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.TextBlock())])), ('module_room_slug', wagtail.core.blocks.StructBlock([('title', wagtail.core.blocks.TextBlock())])), ('content_list_item', wagtail.core.blocks.StreamBlock([('text_block', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.RichTextBlock(features=['ul']))])), ('basic_knowledge', wagtail.core.blocks.StructBlock([('description', wagtail.core.blocks.RichTextBlock(required=False)), ('basic_knowledge', wagtail.core.blocks.PageChooserBlock(required=True, target_model=['basicknowledge.BasicKnowledge']))])), ('assignment', wagtail.core.blocks.StructBlock([('assignment_id', wagtail.snippets.blocks.SnippetChooserBlock(assignments.models.Assignment))])), ('survey', wagtail.core.blocks.StructBlock([('survey_id', wagtail.snippets.blocks.SnippetChooserBlock(surveys.models.Survey))])), ('image_block', wagtail.images.blocks.ImageChooserBlock()), ('image_url_block', wagtail.core.blocks.StructBlock([('title', wagtail.core.blocks.TextBlock()), ('url', wagtail.core.blocks.URLBlock())])), ('link_block', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.TextBlock()), ('url', wagtail.core.blocks.URLBlock())])), ('solution', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.RichTextBlock(features=['ul']))], icon='tick')), ('video_block', wagtail.core.blocks.StructBlock([('url', wagtail.core.blocks.URLBlock())])), ('document_block', wagtail.core.blocks.StructBlock([('url', wagtail.core.blocks.URLBlock())])), ('infogram_block', wagtail.core.blocks.StructBlock([('id', wagtail.core.blocks.TextBlock()), ('title', wagtail.core.blocks.TextBlock())])), ('genially_block', wagtail.core.blocks.StructBlock([('id', wagtail.core.blocks.TextBlock())])), ('thinglink_block', wagtail.core.blocks.StructBlock([('id', wagtail.core.blocks.TextBlock())])), ('subtitle', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.TextBlock())])), ('module_room_slug', wagtail.core.blocks.StructBlock([('title', wagtail.core.blocks.TextBlock())]))]))], blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -5,8 +5,8 @@ from graphene_django.filter import DjangoFilterConnectionField
|
||||||
|
|
||||||
from api.utils import get_object
|
from api.utils import get_object
|
||||||
from books.utils import are_solutions_enabled_for
|
from books.utils import are_solutions_enabled_for
|
||||||
from notes.models import ContentBlockBookmark
|
from notes.models import ContentBlockBookmark, ChapterBookmark, ModuleBookmark
|
||||||
from notes.schema import ContentBlockBookmarkNode
|
from notes.schema import ContentBlockBookmarkNode, ChapterBookmarkNode, ModuleBookmarkNode
|
||||||
from rooms.models import ModuleRoomSlug
|
from rooms.models import ModuleRoomSlug
|
||||||
from ..models import Book, Topic, Module, Chapter, ContentBlock
|
from ..models import Book, Topic, Module, Chapter, ContentBlock
|
||||||
|
|
||||||
|
|
@ -66,6 +66,7 @@ class ContentBlockNode(DjangoObjectType):
|
||||||
|
|
||||||
class ChapterNode(DjangoObjectType):
|
class ChapterNode(DjangoObjectType):
|
||||||
content_blocks = DjangoFilterConnectionField(ContentBlockNode)
|
content_blocks = DjangoFilterConnectionField(ContentBlockNode)
|
||||||
|
bookmark = graphene.Field(ChapterBookmarkNode)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Chapter
|
model = Chapter
|
||||||
|
|
@ -96,6 +97,11 @@ class ChapterNode(DjangoObjectType):
|
||||||
|
|
||||||
return publisher_content_blocks.union(user_created_content_blocks)
|
return publisher_content_blocks.union(user_created_content_blocks)
|
||||||
|
|
||||||
|
def resolve_bookmark(self, info, **kwags):
|
||||||
|
return ChapterBookmark.objects.filter(
|
||||||
|
user=info.context.user,
|
||||||
|
chapter=self
|
||||||
|
).first()
|
||||||
|
|
||||||
class ModuleNode(DjangoObjectType):
|
class ModuleNode(DjangoObjectType):
|
||||||
pk = graphene.Int()
|
pk = graphene.Int()
|
||||||
|
|
@ -103,6 +109,7 @@ class ModuleNode(DjangoObjectType):
|
||||||
topic = graphene.Field('books.schema.queries.TopicNode')
|
topic = graphene.Field('books.schema.queries.TopicNode')
|
||||||
hero_image = graphene.String()
|
hero_image = graphene.String()
|
||||||
solutions_enabled = graphene.Boolean()
|
solutions_enabled = graphene.Boolean()
|
||||||
|
bookmark = graphene.Field(ModuleBookmarkNode)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Module
|
model = Module
|
||||||
|
|
@ -132,6 +139,12 @@ class ModuleNode(DjangoObjectType):
|
||||||
teacher = info.context.user.get_teacher()
|
teacher = info.context.user.get_teacher()
|
||||||
return self.solutions_enabled_by.filter(pk=teacher.pk).exists() if teacher is not None else False
|
return self.solutions_enabled_by.filter(pk=teacher.pk).exists() if teacher is not None else False
|
||||||
|
|
||||||
|
def resolve_bookmark(self, info, **kwags):
|
||||||
|
return ModuleBookmark.objects.filter(
|
||||||
|
user=info.context.user,
|
||||||
|
module=self
|
||||||
|
).first()
|
||||||
|
|
||||||
|
|
||||||
class TopicNode(DjangoObjectType):
|
class TopicNode(DjangoObjectType):
|
||||||
pk = graphene.Int()
|
pk = graphene.Int()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
from django.test import TestCase, RequestFactory
|
||||||
|
from graphene.test import Client
|
||||||
|
from graphql_relay import to_global_id
|
||||||
|
|
||||||
|
from api.schema import schema
|
||||||
|
from api.utils import get_object, get_graphql_mutation
|
||||||
|
from books.models import ContentBlock, Chapter
|
||||||
|
from books.factories import ModuleFactory
|
||||||
|
from core.factories import UserFactory
|
||||||
|
from notes.factories import ChapterBookmarkFactory, ModuleBookmarkFactory
|
||||||
|
from notes.models import Note, ModuleBookmark, ChapterBookmark
|
||||||
|
|
||||||
|
|
||||||
|
class NoteMutationTestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.module = ModuleFactory()
|
||||||
|
self.chapter = Chapter(title='Hello')
|
||||||
|
self.module.add_child(instance=self.chapter)
|
||||||
|
self.user = user = UserFactory(username='aschi')
|
||||||
|
content_block = ContentBlock(title='bla', slug='bla')
|
||||||
|
self.chapter.specific.add_child(instance=content_block)
|
||||||
|
ChapterBookmarkFactory.create(chapter=self.chapter, user=user)
|
||||||
|
ModuleBookmarkFactory.create(module=self.module, user=user)
|
||||||
|
|
||||||
|
request = RequestFactory().get('/')
|
||||||
|
request.user = user
|
||||||
|
|
||||||
|
self.client = Client(schema=schema, context_value=request)
|
||||||
|
self.add_mutation = get_graphql_mutation('addNote.gql')
|
||||||
|
self.update_mutation = get_graphql_mutation('updateNote.gql')
|
||||||
|
|
||||||
|
|
||||||
|
class NewNoteMutationTestCase(NoteMutationTestCase):
|
||||||
|
def test_add_chapter_note(self):
|
||||||
|
self.assertEqual(Note.objects.count(), 0)
|
||||||
|
text = 'Hello World'
|
||||||
|
result = self.client.execute(self.add_mutation, variables={
|
||||||
|
'input': {
|
||||||
|
'note': {
|
||||||
|
'parent': to_global_id('ChapterNode', self.chapter.pk),
|
||||||
|
'text': text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
self.assertIsNone(result.get('errors'))
|
||||||
|
|
||||||
|
self.assertEqual(Note.objects.count(), 1)
|
||||||
|
self.assertEqual(Note.objects.first().text, text)
|
||||||
|
|
||||||
|
def test_add_module_note(self):
|
||||||
|
self.assertEqual(Note.objects.count(), 0)
|
||||||
|
text = 'Hello World'
|
||||||
|
result = self.client.execute(self.add_mutation, variables={
|
||||||
|
'input': {
|
||||||
|
'note': {
|
||||||
|
'parent': to_global_id('ModuleNode', self.module.pk),
|
||||||
|
'text': text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
self.assertIsNone(result.get('errors'))
|
||||||
|
|
||||||
|
self.assertEqual(Note.objects.count(), 1)
|
||||||
|
self.assertEqual(Note.objects.first().text, text)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateNoteMutationTestCase(NoteMutationTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(UpdateNoteMutationTestCase, self).setUp()
|
||||||
|
|
||||||
|
self.chapter_note = Note.objects.create(text='Hello World')
|
||||||
|
self.module_note = Note.objects.create(text='Hello World')
|
||||||
|
|
||||||
|
self.chapter_bookmark = ChapterBookmark.objects.get(user=self.user, chapter=self.chapter)
|
||||||
|
self.chapter_bookmark.note = self.chapter_note
|
||||||
|
self.chapter_bookmark.save()
|
||||||
|
|
||||||
|
self.module_bookmark = ModuleBookmark.objects.get(user=self.user, module=self.module)
|
||||||
|
self.module_bookmark.note = self.module_note
|
||||||
|
self.module_bookmark.save()
|
||||||
|
|
||||||
|
def test_change_module_note(self):
|
||||||
|
self.assertEqual(Note.objects.count(), 2)
|
||||||
|
self.assertEqual(self.module_bookmark.note.text, 'Hello World')
|
||||||
|
|
||||||
|
new_text = 'Salut monde'
|
||||||
|
result = self.client.execute(self.update_mutation, variables={
|
||||||
|
'input': {
|
||||||
|
'note': {
|
||||||
|
'id': to_global_id('NoteNode', self.module_bookmark.note.pk),
|
||||||
|
'text': new_text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
self.assertIsNone(result.get('errors'))
|
||||||
|
|
||||||
|
bookmark = ModuleBookmark.objects.get(user=self.user, module=self.module)
|
||||||
|
self.assertEqual(bookmark.note.text, new_text)
|
||||||
|
|
||||||
|
self.assertEqual(Note.objects.count(), 2)
|
||||||
|
|
||||||
|
def test_change_chapter_note(self):
|
||||||
|
self.assertEqual(Note.objects.count(), 2)
|
||||||
|
self.assertEqual(self.chapter_bookmark.note.text, 'Hello World')
|
||||||
|
|
||||||
|
new_text = 'Salut monde'
|
||||||
|
result = self.client.execute(self.update_mutation, variables={
|
||||||
|
'input': {
|
||||||
|
'note': {
|
||||||
|
'id': to_global_id('NoteNode', self.chapter_bookmark.note.pk),
|
||||||
|
'text': new_text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
self.assertIsNone(result.get('errors'))
|
||||||
|
|
||||||
|
bookmark = ChapterBookmark.objects.get(user=self.user, chapter=self.chapter)
|
||||||
|
self.assertEqual(bookmark.note.text, new_text)
|
||||||
|
|
||||||
|
self.assertEqual(Note.objects.count(), 2)
|
||||||
|
|
||||||
|
def test_update_wrong_user(self):
|
||||||
|
godis_note = Note.objects.create(text='Hello Godi')
|
||||||
|
godi = UserFactory(username='godi')
|
||||||
|
godis_bookmark = ModuleBookmarkFactory(module=self.module, user=godi)
|
||||||
|
|
||||||
|
godis_bookmark.note = godis_note
|
||||||
|
godis_bookmark.save()
|
||||||
|
|
||||||
|
result = self.client.execute(self.update_mutation, variables={
|
||||||
|
'input': {
|
||||||
|
'note': {
|
||||||
|
'id': to_global_id('NoteNode', godis_note.pk),
|
||||||
|
'text': 'Hello Aschi'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
print(result.get('errors'))
|
||||||
|
self.assertIsNotNone(result.get('errors'))
|
||||||
|
|
@ -47,6 +47,7 @@ objective_groups_1 = [
|
||||||
|
|
||||||
module_1_chapter_1 = {
|
module_1_chapter_1 = {
|
||||||
'title': '1.1 Lehrbeginn',
|
'title': '1.1 Lehrbeginn',
|
||||||
|
'description': 'Wie sieht Ihr Konsumverhalten aus?',
|
||||||
'content_blocks': [
|
'content_blocks': [
|
||||||
{
|
{
|
||||||
'type': 'task',
|
'type': 'task',
|
||||||
|
|
@ -186,6 +187,7 @@ module_1_chapter_1 = {
|
||||||
}
|
}
|
||||||
module_1_chapter_2 = {
|
module_1_chapter_2 = {
|
||||||
'title': '1.2 Die drei Lernorte',
|
'title': '1.2 Die drei Lernorte',
|
||||||
|
'description': 'Haben Sie sich beim Shoppen schon mal überlegt, aus welchem Beweggrund Sie ein bestimmtes Produkt eigentlich unbedingt haben wollten? Wir gehen im Folgenden anhand Ihres letzten Kleiderkaufs dieser Frage nach.',
|
||||||
'content_blocks': [
|
'content_blocks': [
|
||||||
{
|
{
|
||||||
'type': 'base_society',
|
'type': 'base_society',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
|
||||||
|
from notes.models import ContentBlockBookmark, ModuleBookmark, ChapterBookmark
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
self.stdout.write("Preparing bookmarks")
|
||||||
|
ContentBlockBookmark.objects.all().delete()
|
||||||
|
ModuleBookmark.objects.all().delete()
|
||||||
|
ChapterBookmark.objects.all().delete()
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
import factory
|
||||||
|
from notes.models import ChapterBookmark, ModuleBookmark
|
||||||
|
|
||||||
|
|
||||||
|
class ChapterBookmarkFactory(factory.DjangoModelFactory):
|
||||||
|
class Meta:
|
||||||
|
model = ChapterBookmark
|
||||||
|
|
||||||
|
class ModuleBookmarkFactory(factory.DjangoModelFactory):
|
||||||
|
class Meta:
|
||||||
|
model = ModuleBookmark
|
||||||
|
|
@ -3,8 +3,9 @@ from graphene import InputObjectType
|
||||||
|
|
||||||
|
|
||||||
class AddNoteArgument(InputObjectType):
|
class AddNoteArgument(InputObjectType):
|
||||||
content = graphene.UUID(required=True)
|
content = graphene.UUID()
|
||||||
content_block = graphene.ID(required=True)
|
content_block = graphene.ID()
|
||||||
|
parent = graphene.ID()
|
||||||
text = graphene.String(required=True)
|
text = graphene.String(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Generated by Django 2.0.6 on 2019-11-28 16:01
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('books', '0016_auto_20191128_1601'),
|
||||||
|
('notes', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ChapterBookmark',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('chapter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='books.Chapter')),
|
||||||
|
('note', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, to='notes.Note')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ModuleBookmark',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('module', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='books.Module')),
|
||||||
|
('note', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, to='notes.Note')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -10,7 +10,6 @@ class Note(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class Bookmark(models.Model):
|
class Bookmark(models.Model):
|
||||||
uuid = models.UUIDField(unique=True)
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
note = models.OneToOneField(Note, null=True, on_delete=models.SET_NULL)
|
note = models.OneToOneField(Note, null=True, on_delete=models.SET_NULL)
|
||||||
|
|
||||||
|
|
@ -19,4 +18,13 @@ class Bookmark(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class ContentBlockBookmark(Bookmark):
|
class ContentBlockBookmark(Bookmark):
|
||||||
|
uuid = models.UUIDField(unique=True)
|
||||||
content_block = models.ForeignKey('books.ContentBlock', on_delete=models.CASCADE)
|
content_block = models.ForeignKey('books.ContentBlock', on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleBookmark(Bookmark):
|
||||||
|
module = models.ForeignKey('books.Module', on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
|
||||||
|
class ChapterBookmark(Bookmark):
|
||||||
|
chapter = models.ForeignKey('books.Chapter', on_delete=models.CASCADE)
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,12 @@ from builtins import PermissionError
|
||||||
import graphene
|
import graphene
|
||||||
import json
|
import json
|
||||||
from graphene import relay
|
from graphene import relay
|
||||||
|
from graphql_relay import from_global_id
|
||||||
|
|
||||||
from api.utils import get_object
|
from api.utils import get_object
|
||||||
from books.models import ContentBlock
|
from books.models import ContentBlock, Chapter, Module
|
||||||
from notes.inputs import AddNoteArgument, UpdateNoteArgument
|
from notes.inputs import AddNoteArgument, UpdateNoteArgument
|
||||||
from notes.models import ContentBlockBookmark, Note
|
from notes.models import ContentBlockBookmark, Note, ChapterBookmark, ModuleBookmark
|
||||||
from notes.schema import NoteNode
|
from notes.schema import NoteNode
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -56,16 +57,31 @@ class AddNote(relay.ClientIDMutation):
|
||||||
user = info.context.user
|
user = info.context.user
|
||||||
|
|
||||||
note = kwargs.get('note')
|
note = kwargs.get('note')
|
||||||
content_uuid = note.get('content')
|
content_uuid = note.get('content', '')
|
||||||
content_block_id = note.get('content_block')
|
content_block_id = note.get('content_block', '')
|
||||||
content_block = get_object(ContentBlock, content_block_id)
|
parent = note.get('parent')
|
||||||
text = note.get('text')
|
text = note.get('text')
|
||||||
|
|
||||||
bookmark = ContentBlockBookmark.objects.get(
|
if content_uuid != '':
|
||||||
content_block=content_block,
|
content_block = get_object(ContentBlock, content_block_id)
|
||||||
uuid=content_uuid,
|
|
||||||
user=user
|
bookmark = ContentBlockBookmark.objects.get(
|
||||||
)
|
content_block=content_block,
|
||||||
|
uuid=content_uuid,
|
||||||
|
user=user
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
type, id = from_global_id(parent)
|
||||||
|
if type == 'ModuleNode':
|
||||||
|
bookmark = ModuleBookmark.objects.get(
|
||||||
|
module__id=id,
|
||||||
|
user=user
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
bookmark = ChapterBookmark.objects.get(
|
||||||
|
chapter__id=id,
|
||||||
|
user=user
|
||||||
|
)
|
||||||
|
|
||||||
bookmark.note = Note.objects.create(text=text)
|
bookmark.note = Note.objects.create(text=text)
|
||||||
bookmark.save()
|
bookmark.save()
|
||||||
|
|
@ -88,7 +104,9 @@ class UpdateNote(relay.ClientIDMutation):
|
||||||
text = note.get('text')
|
text = note.get('text')
|
||||||
note = get_object(Note, id)
|
note = get_object(Note, id)
|
||||||
|
|
||||||
if note.contentblockbookmark.user != user:
|
if hasattr(note, 'contentblockbookmark') and note.contentblockbookmark.user != user \
|
||||||
|
or hasattr(note, 'chapterbookmark') and note.chapterbookmark.user != user \
|
||||||
|
or hasattr(note, 'modulebookmark') and note.modulebookmark.user != user:
|
||||||
raise PermissionError
|
raise PermissionError
|
||||||
|
|
||||||
note.text = text
|
note.text = text
|
||||||
|
|
@ -96,7 +114,67 @@ class UpdateNote(relay.ClientIDMutation):
|
||||||
return cls(note=note)
|
return cls(note=note)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateChapterBookmark(relay.ClientIDMutation):
|
||||||
|
class Input:
|
||||||
|
chapter = graphene.ID(required=True)
|
||||||
|
bookmarked = graphene.Boolean(required=True)
|
||||||
|
|
||||||
|
success = graphene.Boolean()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def mutate_and_get_payload(cls, root, info, **kwargs):
|
||||||
|
user = info.context.user
|
||||||
|
chapter_id = kwargs.get('chapter')
|
||||||
|
bookmarked = kwargs.get('bookmarked')
|
||||||
|
|
||||||
|
chapter = get_object(Chapter, chapter_id)
|
||||||
|
|
||||||
|
if bookmarked:
|
||||||
|
ChapterBookmark.objects.create(
|
||||||
|
chapter=chapter,
|
||||||
|
user=user
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ChapterBookmark.objects.get(
|
||||||
|
chapter=chapter,
|
||||||
|
user=user
|
||||||
|
).delete()
|
||||||
|
|
||||||
|
return cls(success=True)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateModuleBookmark(relay.ClientIDMutation):
|
||||||
|
class Input:
|
||||||
|
module = graphene.ID(required=True)
|
||||||
|
bookmarked = graphene.Boolean(required=True)
|
||||||
|
|
||||||
|
success = graphene.Boolean()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def mutate_and_get_payload(cls, root, info, **kwargs):
|
||||||
|
user = info.context.user
|
||||||
|
module_id = kwargs.get('module')
|
||||||
|
bookmarked = kwargs.get('bookmarked')
|
||||||
|
|
||||||
|
module = get_object(Module, module_id)
|
||||||
|
|
||||||
|
if bookmarked:
|
||||||
|
ModuleBookmark.objects.create(
|
||||||
|
module=module,
|
||||||
|
user=user
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ModuleBookmark.objects.get(
|
||||||
|
module=module,
|
||||||
|
user=user
|
||||||
|
).delete()
|
||||||
|
|
||||||
|
return cls(success=True)
|
||||||
|
|
||||||
|
|
||||||
class NoteMutations:
|
class NoteMutations:
|
||||||
add_note = AddNote.Field()
|
add_note = AddNote.Field()
|
||||||
update_note = UpdateNote.Field()
|
update_note = UpdateNote.Field()
|
||||||
update_content_bookmark = UpdateContentBookmark.Field()
|
update_content_bookmark = UpdateContentBookmark.Field()
|
||||||
|
update_chapter_bookmark = UpdateChapterBookmark.Field()
|
||||||
|
update_module_bookmark = UpdateModuleBookmark.Field()
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import graphene
|
||||||
from graphene import relay
|
from graphene import relay
|
||||||
from graphene_django import DjangoObjectType
|
from graphene_django import DjangoObjectType
|
||||||
|
|
||||||
from notes.models import Note, ContentBlockBookmark
|
from notes.models import Note, ContentBlockBookmark, ModuleBookmark, ChapterBookmark
|
||||||
|
|
||||||
|
|
||||||
class NoteNode(DjangoObjectType):
|
class NoteNode(DjangoObjectType):
|
||||||
|
|
@ -23,3 +23,17 @@ class ContentBlockBookmarkNode(DjangoObjectType):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ContentBlockBookmark
|
model = ContentBlockBookmark
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleBookmarkNode(DjangoObjectType):
|
||||||
|
note = graphene.Field(NoteNode)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ModuleBookmark
|
||||||
|
|
||||||
|
|
||||||
|
class ChapterBookmarkNode(DjangoObjectType):
|
||||||
|
note = graphene.Field(NoteNode)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ChapterBookmark
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ class User(AbstractUser):
|
||||||
def get_teacher(self):
|
def get_teacher(self):
|
||||||
if self.user_roles.filter(role__key='teacher').exists():
|
if self.user_roles.filter(role__key='teacher').exists():
|
||||||
return self
|
return self
|
||||||
elif self.school_classes.count()>0:
|
elif self.school_classes.count() > 0:
|
||||||
return self.school_classes.first().get_teacher()
|
return self.school_classes.first().get_teacher()
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue