Merged in feature/bookmarks-for-modules-and-chapters (pull request #42)

Feature/bookmarks for modules and chapters
This commit is contained in:
Ramon Wenger 2019-12-10 14:09:13 +00:00
commit 388c1f63d7
26 changed files with 737 additions and 86 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,12 @@ fragment ChapterParts on ChapterNode {
id id
title title
description description
bookmark {
note {
id
text
}
}
contentBlocks { contentBlocks {
edges { edges {
node { node {

View File

@ -7,4 +7,10 @@ fragment ModuleParts on ModuleNode {
slug slug
heroImage heroImage
solutionsEnabled solutionsEnabled
bookmark {
note {
id
text
}
}
} }

View File

@ -0,0 +1,5 @@
mutation UpdateChapterBookmark($input: UpdateChapterBookmarkInput!) {
updateChapterBookmark(input: $input) {
success
}
}

View File

@ -0,0 +1,5 @@
mutation UpdateModuleBookmark($input: UpdateModuleBookmarkInput!) {
updateModuleBookmark(input: $input) {
success
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

12
server/notes/factories.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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