From 8d9761b3ef2fe1532a952b79db273021b235d38b Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Thu, 18 Feb 2021 17:54:02 +0100 Subject: [PATCH 01/14] Add new chapter visibility properties to model and schema --- server/api/schema.py | 2 +- .../migrations/0024_auto_20210218_1336.py | 24 +++++++++++ server/books/models/chapter.py | 3 ++ server/books/schema/mutations/__init__.py | 22 ++++++---- server/books/schema/mutations/chapter.py | 43 +++++++++++++++++++ server/books/schema/mutations/contentblock.py | 7 +-- server/books/schema/mutations/main.py | 11 ----- server/books/schema/queries.py | 2 +- 8 files changed, 87 insertions(+), 27 deletions(-) create mode 100644 server/books/migrations/0024_auto_20210218_1336.py create mode 100644 server/books/schema/mutations/chapter.py diff --git a/server/api/schema.py b/server/api/schema.py index afd53d37..453a1c45 100644 --- a/server/api/schema.py +++ b/server/api/schema.py @@ -8,7 +8,7 @@ from api import graphene_wagtail # Keep this import exactly here, it's necessar from assignments.schema.mutations import AssignmentMutations from assignments.schema.queries import AssignmentsQuery, StudentSubmissionQuery from basicknowledge.queries import BasicKnowledgeQuery -from books.schema.mutations.main import BookMutations +from books.schema.mutations import BookMutations from books.schema.queries import BookQuery from core.schema.mutations.coupon import CouponMutations from core.schema.mutations.main import CoreMutations diff --git a/server/books/migrations/0024_auto_20210218_1336.py b/server/books/migrations/0024_auto_20210218_1336.py new file mode 100644 index 00000000..fad99a2f --- /dev/null +++ b/server/books/migrations/0024_auto_20210218_1336.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.17 on 2021-02-18 13:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0025_auto_20210126_1343'), + ('books', '0023_auto_20200707_1501'), + ] + + operations = [ + migrations.AddField( + model_name='chapter', + name='description_hidden_for', + field=models.ManyToManyField(related_name='hidden_chapter_descriptions', to='users.SchoolClass'), + ), + migrations.AddField( + model_name='chapter', + name='title_hidden_for', + field=models.ManyToManyField(related_name='hidden_chapter_titles', to='users.SchoolClass'), + ), + ] diff --git a/server/books/models/chapter.py b/server/books/models/chapter.py index 7e340b4b..8f329601 100644 --- a/server/books/models/chapter.py +++ b/server/books/models/chapter.py @@ -4,6 +4,7 @@ from django.db import models from wagtail.admin.edit_handlers import FieldPanel, TabbedInterface, ObjectList from core.wagtail_utils import StrictHierarchyPage +from users.models import SchoolClass logger = logging.getLogger(__name__) @@ -34,3 +35,5 @@ class Chapter(StrictHierarchyPage): parent_page_types = ['books.Module'] subpage_types = ['books.ContentBlock'] + title_hidden_for = models.ManyToManyField(SchoolClass, related_name='hidden_chapter_titles') + description_hidden_for = models.ManyToManyField(SchoolClass, related_name='hidden_chapter_descriptions') diff --git a/server/books/schema/mutations/__init__.py b/server/books/schema/mutations/__init__.py index 3e491c29..fc85ed51 100644 --- a/server/books/schema/mutations/__init__.py +++ b/server/books/schema/mutations/__init__.py @@ -1,9 +1,13 @@ -# -*- coding: utf-8 -*- -# -# Iterativ GmbH -# http://www.iterativ.ch/ -# -# Copyright (c) 2018 Iterativ GmbH. All rights reserved. -# -# Created on 25.09.18 -# @author: Ramon Wenger +from books.schema.mutations.chapter import UpdateChapterVisibility +from books.schema.mutations.contentblock import MutateContentBlock, AddContentBlock, DeleteContentBlock +from books.schema.mutations.module import UpdateSolutionVisibility, UpdateLastModule, UpdateLastTopic + + +class BookMutations(object): + mutate_content_block = MutateContentBlock.Field() + add_content_block = AddContentBlock.Field() + delete_content_block = DeleteContentBlock.Field() + update_solution_visibility = UpdateSolutionVisibility.Field() + update_last_module = UpdateLastModule.Field() + update_last_topic = UpdateLastTopic.Field() + update_chapter_visibility = UpdateChapterVisibility.Field() diff --git a/server/books/schema/mutations/chapter.py b/server/books/schema/mutations/chapter.py new file mode 100644 index 00000000..48f62da1 --- /dev/null +++ b/server/books/schema/mutations/chapter.py @@ -0,0 +1,43 @@ +import graphene +from graphene import relay + +from api.utils import get_object +from books.models import Chapter +from books.schema.inputs import UserGroupBlockVisibility +from books.schema.queries import ChapterNode +from users.models import SchoolClass + + +class UpdateChapterVisibility(relay.ClientIDMutation): + class Input: + id = graphene.ID(required=True) + visibility = graphene.List(UserGroupBlockVisibility) + type = graphene.String(required=True) + + chapter = graphene.Field(ChapterNode) + + @classmethod + def mutate_and_get_payload(cls, root, info, **kwargs): + id_param = kwargs['id'] + visibility_list = kwargs.get('visibility', None) + mutation_type = kwargs['type'] + + chapter = get_object(Chapter, id_param) + + if visibility_list is not None: + for v in visibility_list: + school_class = get_object(SchoolClass, v.school_class_id) + if v.hidden: + if mutation_type == 'chapter-title': + chapter.title_hidden_for.add(school_class) + else: + chapter.description_hidden_for.add(school_class) + else: + if mutation_type == 'chapter-title': + chapter.title_hidden_for.remove(school_class) + else: + chapter.description_hidden_for.remove(school_class) + + chapter.save() + + return cls(chapter=chapter) diff --git a/server/books/schema/mutations/contentblock.py b/server/books/schema/mutations/contentblock.py index d7234c81..f20500b9 100644 --- a/server/books/schema/mutations/contentblock.py +++ b/server/books/schema/mutations/contentblock.py @@ -10,7 +10,6 @@ from books.models import ContentBlock, Chapter, SchoolClass from books.schema.inputs import ContentBlockInput from books.schema.queries import ContentBlockNode from core.utils import set_hidden_for, set_visible_for -from notes.models import ContentBlockBookmark from .utils import handle_content_block, set_user_defined_block_type @@ -19,7 +18,6 @@ class MutateContentBlock(relay.ClientIDMutation): id = graphene.ID(required=True) content_block = graphene.Argument(ContentBlockInput) - errors = graphene.List(graphene.String) content_block = graphene.Field(ContentBlockNode) @classmethod @@ -52,17 +50,16 @@ class MutateContentBlock(relay.ClientIDMutation): if contents is not None: content_block.contents = json.dumps([handle_content_block(c, info.context, module) for c in contents]) - content_block.save() return cls(content_block=content_block) except ValidationError as e: errors = get_errors(e) + raise errors except Exception as e: errors = ['Error: {}'.format(e)] - - return cls(content_block=None, errors=errors) + raise errors class AddContentBlock(relay.ClientIDMutation): diff --git a/server/books/schema/mutations/main.py b/server/books/schema/mutations/main.py index 075f8cf8..e69de29b 100644 --- a/server/books/schema/mutations/main.py +++ b/server/books/schema/mutations/main.py @@ -1,11 +0,0 @@ -from books.schema.mutations.contentblock import MutateContentBlock, AddContentBlock, DeleteContentBlock -from books.schema.mutations.module import UpdateSolutionVisibility, UpdateLastModule, UpdateLastTopic - - -class BookMutations(object): - mutate_content_block = MutateContentBlock.Field() - add_content_block = AddContentBlock.Field() - delete_content_block = DeleteContentBlock.Field() - update_solution_visibility = UpdateSolutionVisibility.Field() - update_last_module = UpdateLastModule.Field() - update_last_topic = UpdateLastTopic.Field() diff --git a/server/books/schema/queries.py b/server/books/schema/queries.py index 74d09723..5c3a1aea 100644 --- a/server/books/schema/queries.py +++ b/server/books/schema/queries.py @@ -77,7 +77,7 @@ class ChapterNode(DjangoObjectType): class Meta: model = Chapter only_fields = [ - 'slug', 'title', 'description', + 'slug', 'title', 'description', 'title_hidden_for', 'description_hidden_for' ] filter_fields = [ 'slug', 'title', From fa12fb21126d0fe44c58f1550c6cb96586eef614 Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Thu, 18 Feb 2021 17:58:08 +0100 Subject: [PATCH 02/14] Add new chapter visibility mutations to client --- .../visibility/VisibilityAction.vue | 82 ++++++------------- client/src/consts/types.js | 5 ++ .../graphql/gql/fragments/chapterParts.gql | 16 ++++ .../gql/mutations/mutateContentBlock.gql | 1 - .../gql/mutations/updateChapterVisibility.gql | 9 ++ client/src/helpers/visibility.js | 78 ++++++++++++++++++ client/src/mixins/me.js | 28 ++++--- 7 files changed, 149 insertions(+), 70 deletions(-) create mode 100644 client/src/consts/types.js create mode 100644 client/src/graphql/gql/mutations/updateChapterVisibility.gql create mode 100644 client/src/helpers/visibility.js diff --git a/client/src/components/visibility/VisibilityAction.vue b/client/src/components/visibility/VisibilityAction.vue index f0203e41..7234b06a 100644 --- a/client/src/components/visibility/VisibilityAction.vue +++ b/client/src/components/visibility/VisibilityAction.vue @@ -18,79 +18,51 @@ import EyeIcon from '@/components/icons/EyeIcon'; import ClosedEyeIcon from '@/components/icons/ClosedEyeIcon'; - import ME_QUERY from '@/graphql/gql/meQuery.gql'; - import CHANGE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/mutateContentBlock.gql'; - import UPDATE_OBJECTIVE_VISIBILITY_MUTATION from '@/graphql/gql/mutations/updateObjectiveVisibility.gql'; + import me from '@/mixins/me'; + + import {TYPES} from '@/consts/types'; + import {createVisibilityMutation, hidden} from '@/helpers/visibility'; export default { - props: ['block'], + props: { + block: { + type: Object, + default: () => ({}) + }, + type: { + type: String, + default: 'content', + validator: value => { + // value must be one of TYPES + return TYPES.indexOf(value) !== -1; + } + } + }, + + mixins: [me], components: { EyeIcon, ClosedEyeIcon }, - data() { - return { - showVisibility: false, - me: { - permissions: [] - } - }; - }, - computed: { - canManageContent() { - return this.me.permissions.includes('users.can_manage_school_class_content'); - }, - isContentBlock() { - return this.block.__typename === 'ContentBlockNode'; - }, - schoolClass() { - return this.me.selectedClass; - }, hidden() { - // is this content block / objective group user created? - return this.block.userCreated - // if so, is visibility not explicitly set for this school class? - ? this.block.visibleFor.findIndex(el => el.id === this.schoolClass.id) === -1 - // otherwise, is it explicitly hidden for this school class? - : this.block.hiddenFor.findIndex(el => el.id === this.schoolClass.id) > -1; + return hidden({type: this.type, block: this.block, schoolClass: this.schoolClass}); } }, methods: { toggleVisibility() { - let hidden = !this.hidden; - let schoolClassId = this.schoolClass.id; + const hidden = !this.hidden; + const schoolClassId = this.schoolClass.id; const visibility = [{ schoolClassId, hidden }]; - let mutation, variables; - const id = this.block.id; - - if (this.isContentBlock) { - mutation = CHANGE_CONTENT_BLOCK_MUTATION; - variables = { - input: { - id, - contentBlock: { - visibility - } - } - }; - } else { - mutation = UPDATE_OBJECTIVE_VISIBILITY_MUTATION; - variables = { - input: { - id, - visibility - } - }; - } + const {mutation, variables} = createVisibilityMutation(this.type, this.block.id, visibility); this.$apollo.mutate({ mutation, @@ -98,12 +70,6 @@ }); }, }, - - apollo: { - me: { - query: ME_QUERY, - }, - }, }; diff --git a/client/src/consts/types.js b/client/src/consts/types.js new file mode 100644 index 00000000..48625c62 --- /dev/null +++ b/client/src/consts/types.js @@ -0,0 +1,5 @@ +export const CONTENT_TYPE = 'content'; +export const OBJECTIVE_TYPE = 'objective'; +export const CHAPTER_TITLE_TYPE = 'chapter-title'; +export const CHAPTER_DESCRIPTION_TYPE = 'chapter-description'; +export const TYPES = [CONTENT_TYPE, OBJECTIVE_TYPE, CHAPTER_TITLE_TYPE, CHAPTER_DESCRIPTION_TYPE]; diff --git a/client/src/graphql/gql/fragments/chapterParts.gql b/client/src/graphql/gql/fragments/chapterParts.gql index b3d41af0..449fb617 100644 --- a/client/src/graphql/gql/fragments/chapterParts.gql +++ b/client/src/graphql/gql/fragments/chapterParts.gql @@ -16,4 +16,20 @@ fragment ChapterParts on ChapterNode { } } } + titleHiddenFor { + edges { + node { + id + name + } + } + } + descriptionHiddenFor { + edges { + node { + id + name + } + } + } } diff --git a/client/src/graphql/gql/mutations/mutateContentBlock.gql b/client/src/graphql/gql/mutations/mutateContentBlock.gql index ec53bffe..84761705 100644 --- a/client/src/graphql/gql/mutations/mutateContentBlock.gql +++ b/client/src/graphql/gql/mutations/mutateContentBlock.gql @@ -5,6 +5,5 @@ mutation MutateContentBlock($input: MutateContentBlockInput!) { contentBlock { ...ContentBlockParts } - errors } } diff --git a/client/src/graphql/gql/mutations/updateChapterVisibility.gql b/client/src/graphql/gql/mutations/updateChapterVisibility.gql new file mode 100644 index 00000000..8a72544a --- /dev/null +++ b/client/src/graphql/gql/mutations/updateChapterVisibility.gql @@ -0,0 +1,9 @@ +#import "../fragments/chapterParts.gql" +mutation UpdateChapterVisibility($input: UpdateChapterVisibilityInput!) { + updateChapterVisibility(input: $input) { + chapter { + ...ChapterParts + } + } +} + diff --git a/client/src/helpers/visibility.js b/client/src/helpers/visibility.js new file mode 100644 index 00000000..152e2eab --- /dev/null +++ b/client/src/helpers/visibility.js @@ -0,0 +1,78 @@ +import {CHAPTER_DESCRIPTION_TYPE, CHAPTER_TITLE_TYPE, CONTENT_TYPE, OBJECTIVE_TYPE} from '@/consts/types'; +import CHANGE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/mutateContentBlock'; +import UPDATE_OBJECTIVE_VISIBILITY_MUTATION from '@/graphql/gql/mutations/updateObjectiveVisibility'; +import UPDATE_CHAPTER_VISIBILITY_MUTATION from '@/graphql/gql/mutations/updateChapterVisibility.gql'; + +export const createVisibilityMutation = (type, id, visibility) => { + let mutation, variables; + switch (type) { + case CONTENT_TYPE: + mutation = CHANGE_CONTENT_BLOCK_MUTATION; + variables = { + input: { + id, + contentBlock: { + visibility + } + } + }; + break; + case OBJECTIVE_TYPE: + mutation = UPDATE_OBJECTIVE_VISIBILITY_MUTATION; + variables = { + input: { + id, + visibility + } + }; + break; + case CHAPTER_TITLE_TYPE: + case CHAPTER_DESCRIPTION_TYPE: + mutation = UPDATE_CHAPTER_VISIBILITY_MUTATION; + variables = { + input: { + id, + type, + visibility + } + }; + break; + } + + return { + mutation, + variables + }; +}; + +const containsClass = (arr, schoolClass) => arr.map(entry => entry.id).includes(schoolClass.id); + +export const hidden = ({ + type, + block: { + userCreated, + visibleFor, + hiddenFor, + titleHiddenFor, + descriptionHiddenFor + }, + schoolClass + }) => { + switch (type) { + case CONTENT_TYPE: + case OBJECTIVE_TYPE: + // is this content block / objective group user created? + return userCreated + // if so, is visibility not explicitly set for this school class? + ? !containsClass(visibleFor, schoolClass) + // otherwise, is it explicitly hidden for this school class? + : containsClass(hiddenFor, schoolClass); + case CHAPTER_TITLE_TYPE: + console.log(containsClass(titleHiddenFor, schoolClass)); + return containsClass(titleHiddenFor, schoolClass); + case CHAPTER_DESCRIPTION_TYPE: + return containsClass(descriptionHiddenFor, schoolClass); + default: + return false; + } +}; diff --git a/client/src/mixins/me.js b/client/src/mixins/me.js index 53e1fe03..aaa043d3 100644 --- a/client/src/mixins/me.js +++ b/client/src/mixins/me.js @@ -15,19 +15,25 @@ export default { }; }, - computed: { - topicRoute() { - if (this.me.lastTopic && this.me.lastTopic.slug) { - return { - name: 'topic', - params: { - topicSlug: this.me.lastTopic.slug - } - }; - } - return '/book/topic/berufliche-grundbildung'; + computed: { + topicRoute() { + if (this.me.lastTopic && this.me.lastTopic.slug) { + return { + name: 'topic', + params: { + topicSlug: this.me.lastTopic.slug + } + }; } + return '/book/topic/berufliche-grundbildung'; }, + schoolClass() { + return this.me.selectedClass; + }, + canManageContent() { + return this.me.permissions.includes('users.can_manage_school_class_content'); + }, + }, apollo: { me: { From d97ad231cc3932ba2fd0ad40e40aeb0a78ac0755 Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Thu, 18 Feb 2021 17:58:59 +0100 Subject: [PATCH 03/14] Add visibility menu to Chapter component --- client/src/components/Chapter.vue | 281 ++++++++++++++++++------------ 1 file changed, 165 insertions(+), 116 deletions(-) diff --git a/client/src/components/Chapter.vue b/client/src/components/Chapter.vue index 10311cf9..e69859f1 100644 --- a/client/src/components/Chapter.vue +++ b/client/src/components/Chapter.vue @@ -2,7 +2,20 @@
-

{{ chapter.title }}

+
+

{{ chapter.title }}

+
+ + -

- {{ chapter.description }} -

+
+ +

+ {{ chapter.description }} +

+
From 16baba9423da584911e414f182e379b94ef89cbd Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Thu, 18 Feb 2021 18:15:19 +0100 Subject: [PATCH 04/14] Refactor some code --- client/src/components/ContentBlock.vue | 28 +++++++------------ .../components/modules/ModuleNavigation.vue | 13 ++------- .../components/objective-groups/Objective.vue | 9 ++++-- .../objective-groups/ObjectiveGroup.vue | 14 +++------- .../components/toggle-menu/ToggleEditing.vue | 4 +-- client/src/helpers/content-block.js | 14 ---------- client/src/pages/module.vue | 4 +-- 7 files changed, 28 insertions(+), 58 deletions(-) diff --git a/client/src/components/ContentBlock.vue b/client/src/components/ContentBlock.vue index 92c85d53..67e32e7b 100644 --- a/client/src/components/ContentBlock.vue +++ b/client/src/components/ContentBlock.vue @@ -60,11 +60,11 @@ import CHAPTER_QUERY from '@/graphql/gql/chapterQuery.gql'; import DELETE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/deleteContentBlock.gql'; - import {meQuery} from '@/graphql/queries'; - import {mapState} from 'vuex'; - import {isHidden} from '@/helpers/content-block'; + import me from '@/mixins/me'; + import {hidden} from '@/helpers/visibility'; + import {CONTENT_TYPE} from '@/consts/types'; const instruments = { base_communication: 'Sprache & Kommunikation', @@ -77,6 +77,8 @@ name: 'ContentBlock', props: ['contentBlock', 'parent'], + mixins: [me], + components: { ContentComponent, AddContentButton, @@ -85,13 +87,6 @@ UserWidget }, - data() { - return { - showVisibility: false, - me: {} - }; - }, - computed: { ...mapState(['editModule']), canEditModule() { @@ -164,11 +159,12 @@ contents: this.removeSingleContentListItem(newContent, startingIndex) }); }, - schoolClass() { - return this.me.selectedClass; - }, hidden() { - return isHidden(this.contentBlock, this.schoolClass); + return hidden({ + block: this.contentBlock, + schoolClass: this.schoolClass, + type: CONTENT_TYPE + }); }, root() { // we need the root content block id, not the generated content block if inside a content list block @@ -235,10 +231,6 @@ } return [...content.slice(0, listIndex), ...content[listIndex].contents[0].value, ...content.slice(listIndex + 1)]; } - }, - - apollo: { - me: meQuery } }; diff --git a/client/src/components/modules/ModuleNavigation.vue b/client/src/components/modules/ModuleNavigation.vue index 85183a2b..e033a2e8 100644 --- a/client/src/components/modules/ModuleNavigation.vue +++ b/client/src/components/modules/ModuleNavigation.vue @@ -59,21 +59,20 @@