Merged in feature/chapter-visibility (pull request #78)

Feature/chapter visibility

Approved-by: Christian Cueni
This commit is contained in:
Ramon Wenger 2021-02-23 10:13:16 +00:00
commit 3660a282a3
44 changed files with 621 additions and 482 deletions

View File

@ -23,7 +23,7 @@ python-dotenv = "==0.13.0"
dj-database-url = "==0.4.1" dj-database-url = "==0.4.1"
raven = "==6.9.0" raven = "==6.9.0"
django-extensions = "==1.9.8" django-extensions = "==1.9.8"
graphene-django = "==2.2.0" graphene-django = "==2.15.0"
django-filter = "==2.0.0" django-filter = "==2.0.0"
djangorestframework = "==3.8.2" djangorestframework = "==3.8.2"
pillow = "==5.0.0" pillow = "==5.0.0"

90
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "a5323ad6907e180d4901fe31c0edc344ee9890f7a95577b2d60e5a1ee19be2f3" "sha256": "58d8faf7e03679ac7b0053dd01e54288d3a719c8ee25c1edf20a74ebcbf87951"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -25,9 +25,10 @@
}, },
"autopep8": { "autopep8": {
"hashes": [ "hashes": [
"sha256:d21d3901cb0da6ebd1e83fc9b0dfbde8b46afc2ede4fe32fbda0c7c6118ca094" "sha256:9e136c472c475f4ee4978b51a88a494bfcd4e3ed17950a44a988d9e434837bea",
"sha256:cae4bc0fb616408191af41d062d7ec7ef8679c7f27b068875ca3a9e2878d5443"
], ],
"version": "==1.5.4" "version": "==1.5.5"
}, },
"backcall": { "backcall": {
"hashes": [ "hashes": [
@ -54,18 +55,18 @@
}, },
"boto3": { "boto3": {
"hashes": [ "hashes": [
"sha256:2a39bd5e5f2d50ce9267d682cc92750f8771399665021f47e80f9c8d2fb812a6", "sha256:1709ff5feb363fee7fcaa2330e659fcbc2b4c03a14f75a884ed682ee66011fc4",
"sha256:b4860f56bc585d3d1fde90d288da5eb4d1198401d72201dc3e25de8887b080e2" "sha256:80a761eff3b1cb0798d7e1a41b7c8e6d85c9647a8f7b6105335201a69404caa2"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.17.0" "version": "==1.17.10"
}, },
"botocore": { "botocore": {
"hashes": [ "hashes": [
"sha256:634b39ab0d55477cfbffb0e5dff31b7ab4bb171b04a0c69f8bcf65135f26ba94", "sha256:8c84eac6daf38890714e005623083106d68e9b2088e62132fdbf7d2b1228ecbd",
"sha256:a608d6d644b852f3c154fc433eaae52febbebc7c474fa8f4d666797d0931770a" "sha256:a601ee5a4ae66832f328ca362b5404d22b75f1c181f6cc0934f3cfca749eb27d"
], ],
"version": "==1.20.0" "version": "==1.20.10"
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
@ -189,10 +190,10 @@
}, },
"django-treebeard": { "django-treebeard": {
"hashes": [ "hashes": [
"sha256:214ae3ab331a7de11fb055a2015c201e34f3fa14255b667e1e07752231a7a398", "sha256:313fb61843e1ad025262014c382bc0c58fefc064cf5401421dcb5a2cfdacdc9d",
"sha256:f50e4eea146f7af6702decf7ef198ac1eee1fb9bb4af2c5dba276c3c48f76623" "sha256:8085928debdd187e9919afc522ea40069bb9f090fa804c7ae9a324b0f62843c6"
], ],
"version": "==4.4" "version": "==4.5"
}, },
"djangorestframework": { "djangorestframework": {
"hashes": [ "hashes": [
@ -219,10 +220,10 @@
}, },
"faker": { "faker": {
"hashes": [ "hashes": [
"sha256:0783729c61501d52efea2967aff6e6fcb8370f0f6b5a558f2a81233642ae529a", "sha256:3971803f32728314c54ba051139cd433fc93fde371e18d07a2cec960a7a2222a",
"sha256:6b2995ffff6c2b02bc5daad96f8c24c021e5bd491d9d53d31bcbd66f348181d4" "sha256:b27f9bc97490a11f14c1501cc25f1109cf68c75f11c6ef97714757a4298c33e5"
], ],
"version": "==5.8.0" "version": "==6.3.0"
}, },
"future": { "future": {
"hashes": [ "hashes": [
@ -245,11 +246,11 @@
}, },
"graphene-django": { "graphene-django": {
"hashes": [ "hashes": [
"sha256:3afd81d47c8b702650e05cc1179fac1cfceae991d241bb164d51f28bed9ec95c", "sha256:02671d195f0c09c8649acff2a8f4ad4f297d0f7d98ea6e6cdf034b81bab92880",
"sha256:760a18068feb5457e2ec00d2447c09b2fbac2a6b8c32cc8be2abce3752107ad3" "sha256:b78c9b05bc899016b9cc5bf13faa1f37fe1faa8c5407552c6ddd1a28f46fc31a"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.2.0" "version": "==2.15.0"
}, },
"graphql-core": { "graphql-core": {
"hashes": [ "hashes": [
@ -500,10 +501,10 @@
}, },
"prompt-toolkit": { "prompt-toolkit": {
"hashes": [ "hashes": [
"sha256:7e966747c18ececaec785699626b771c1ba8344c8d31759a1915d6b12fad6525", "sha256:0fa02fa80363844a4ab4b8d6891f62dd0645ba672723130423ca4037b80c1974",
"sha256:c96b30925025a7635471dc083ffb6af0cc67482a00611bd81aeaeeeb7e5a5e12" "sha256:62c811e46bd09130fb11ab759012a4ae385ce4fb2073442d1898867a824183bd"
], ],
"version": "==3.0.14" "version": "==3.0.16"
}, },
"psycopg2": { "psycopg2": {
"hashes": [ "hashes": [
@ -542,10 +543,10 @@
}, },
"pygments": { "pygments": {
"hashes": [ "hashes": [
"sha256:bc9591213a8f0e0ca1a5e68a479b4887fdc3e75d0774e5c71c31920c427de435", "sha256:37a13ba168a02ac54cc5891a42b1caec333e59b66addb7fa633ea8a6d73445c0",
"sha256:df49d09b498e83c1a73128295860250b0b7edd4c723a32e9bc0d295c7c2ec337" "sha256:b21b072d0ccdf29297a82a2363359d99623597b8a265b8081760e4d0f7153c88"
], ],
"version": "==2.7.4" "version": "==2.8.0"
}, },
"pyparsing": { "pyparsing": {
"hashes": [ "hashes": [
@ -641,10 +642,10 @@
}, },
"sendgrid": { "sendgrid": {
"hashes": [ "hashes": [
"sha256:499a4910623c03e73cb27bd9ef7cadd0968eb2c811afd5c83cfb517f76163f65", "sha256:2eb1dcb1f7d8656eed4db586e428c2c86f347590b8511d7f92993882d0e4fab9",
"sha256:5a87682dba540b706683d4b4a3a605e11fbe24f340ecff5fd502bfb17dfa7ef8" "sha256:e422c8263563ac7d664066d2f87b90bcb005b067eb7c33a9b1396442b2ed285b"
], ],
"version": "==6.5.0" "version": "==6.6.0"
}, },
"sentry-sdk": { "sentry-sdk": {
"hashes": [ "hashes": [
@ -711,10 +712,10 @@
}, },
"unidecode": { "unidecode": {
"hashes": [ "hashes": [
"sha256:4c9d15d2f73eb0d2649a151c566901f80a030da1ccb0a2043352e1dbf647586b", "sha256:12435ef2fc4cdfd9cf1035a1db7e98b6b047fe591892e81f34e94959591fad00",
"sha256:a039f89014245e0cad8858976293e23501accc9ff5a7bdbc739a14a2b7b85cdc" "sha256:8d73a97d387a956922344f6b74243c2c6771594659778744b2dbdaad8f6b727d"
], ],
"version": "==1.1.2" "version": "==1.2.0"
}, },
"unittest-xml-reporting": { "unittest-xml-reporting": {
"hashes": [ "hashes": [
@ -794,17 +795,18 @@
}, },
"autopep8": { "autopep8": {
"hashes": [ "hashes": [
"sha256:d21d3901cb0da6ebd1e83fc9b0dfbde8b46afc2ede4fe32fbda0c7c6118ca094" "sha256:9e136c472c475f4ee4978b51a88a494bfcd4e3ed17950a44a988d9e434837bea",
"sha256:cae4bc0fb616408191af41d062d7ec7ef8679c7f27b068875ca3a9e2878d5443"
], ],
"version": "==1.5.4" "version": "==1.5.5"
}, },
"awscli": { "awscli": {
"hashes": [ "hashes": [
"sha256:2bdc2ef330f9334dbeefb91c942061feb6a53646bd55c663e0056f8d0dc66fed", "sha256:299161d80c226fea405a69fda44fa90cec3d5cf2e484021e9c323d6454246d20",
"sha256:5a28b63869261c5c2f4ee83f7c43d8ec9622f790d0daf73f91643dcf7148bcf8" "sha256:f04edb9f34308a84541ba125387bb9d7f4ae03c066b03d46af306e995c4c5e42"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.19.0" "version": "==1.19.10"
}, },
"backcall": { "backcall": {
"hashes": [ "hashes": [
@ -815,10 +817,10 @@
}, },
"botocore": { "botocore": {
"hashes": [ "hashes": [
"sha256:634b39ab0d55477cfbffb0e5dff31b7ab4bb171b04a0c69f8bcf65135f26ba94", "sha256:8c84eac6daf38890714e005623083106d68e9b2088e62132fdbf7d2b1228ecbd",
"sha256:a608d6d644b852f3c154fc433eaae52febbebc7c474fa8f4d666797d0931770a" "sha256:a601ee5a4ae66832f328ca362b5404d22b75f1c181f6cc0934f3cfca749eb27d"
], ],
"version": "==1.20.0" "version": "==1.20.10"
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
@ -1064,10 +1066,10 @@
}, },
"prompt-toolkit": { "prompt-toolkit": {
"hashes": [ "hashes": [
"sha256:7e966747c18ececaec785699626b771c1ba8344c8d31759a1915d6b12fad6525", "sha256:0fa02fa80363844a4ab4b8d6891f62dd0645ba672723130423ca4037b80c1974",
"sha256:c96b30925025a7635471dc083ffb6af0cc67482a00611bd81aeaeeeb7e5a5e12" "sha256:62c811e46bd09130fb11ab759012a4ae385ce4fb2073442d1898867a824183bd"
], ],
"version": "==3.0.14" "version": "==3.0.16"
}, },
"ptyprocess": { "ptyprocess": {
"hashes": [ "hashes": [
@ -1103,10 +1105,10 @@
}, },
"pygments": { "pygments": {
"hashes": [ "hashes": [
"sha256:bc9591213a8f0e0ca1a5e68a479b4887fdc3e75d0774e5c71c31920c427de435", "sha256:37a13ba168a02ac54cc5891a42b1caec333e59b66addb7fa633ea8a6d73445c0",
"sha256:df49d09b498e83c1a73128295860250b0b7edd4c723a32e9bc0d295c7c2ec337" "sha256:b21b072d0ccdf29297a82a2363359d99623597b8a265b8081760e4d0f7153c88"
], ],
"version": "==2.7.4" "version": "==2.8.0"
}, },
"python-dateutil": { "python-dateutil": {
"hashes": [ "hashes": [

View File

@ -5,7 +5,7 @@
"targets": { "targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"] "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
} }
}], }]
], ],
"plugins": [ "plugins": [
"transform-vue-jsx", "transform-vue-jsx",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -17,12 +17,12 @@ const topic = {
return { return {
node: module, node: module,
__typename: 'ModuleNodeEdge' __typename: 'ModuleNodeEdge'
} };
}) })
] ]
} }
} }
} };
Cypress.Commands.add('checkHome', (n, skipHome) => { Cypress.Commands.add('checkHome', (n, skipHome) => {
if (!skipHome) { if (!skipHome) {
@ -71,7 +71,7 @@ describe('Current Module', () => {
'__typename': 'UserNode', '__typename': 'UserNode',
'permissions': [] 'permissions': []
} }
} };
}, },
AssignmentsQuery: { AssignmentsQuery: {
assignments assignments
@ -79,7 +79,7 @@ describe('Current Module', () => {
ModulesQuery: variables => { ModulesQuery: variables => {
return { return {
module: fullModules[variables.slug] module: fullModules[variables.slug]
} };
}, },
TopicsQuery: topics, TopicsQuery: topics,
Topic: topic, Topic: topic,
@ -95,7 +95,7 @@ describe('Current Module', () => {
lastModule: moduleTeasers[variables.input.id], lastModule: moduleTeasers[variables.input.id],
__typename: 'UpdateLastModulePayload' __typename: 'UpdateLastModulePayload'
} }
} };
} }
} }
}); });
@ -150,5 +150,5 @@ describe('Current Module', () => {
cy.get('[data-cy=start-module-teaser]').first().should('contain', 'Lerntipps'); cy.get('[data-cy=start-module-teaser]').first().should('contain', 'Lerntipps');
cy.get('[data-cy=start-module-teaser]').eq(1).should('contain', 'Random'); cy.get('[data-cy=start-module-teaser]').eq(1).should('contain', 'Random');
cy.get('[data-cy=start-module-teaser]').eq(2).should('contain', 'Geld'); cy.get('[data-cy=start-module-teaser]').eq(2).should('contain', 'Geld');
}) });
}); });

View File

@ -2,7 +2,20 @@
<div <div
:data-scrollto="chapter.id" :data-scrollto="chapter.id"
class="chapter"> class="chapter">
<h3 :id="'chapter-' + index">{{ chapter.title }}</h3> <div
:class="{'hideable-element--greyed-out': titleGreyedOut}"
class="hideable-element"
v-if="!titleHidden">
<h3
:id="'chapter-' + index"
>{{ chapter.title }}</h3>
</div>
<visibility-action
:block="chapter"
type="chapter-title"
v-if="editModule"
/>
<bookmark-actions <bookmark-actions
:bookmarked="chapter.bookmark" :bookmarked="chapter.bookmark"
@ -12,9 +25,22 @@
@edit-note="editNote" @edit-note="editNote"
@bookmark="bookmark(!chapter.bookmark)" @bookmark="bookmark(!chapter.bookmark)"
/> />
<p class="chapter__description intro"> <div
{{ chapter.description }} :class="{'hideable-element--greyed-out': descriptionGreyedOut}"
</p> class="chapter__intro intro hideable-element"
v-if="!descriptionHidden"
>
<visibility-action
:block="chapter"
:chapter="true"
type="chapter-description"
v-if="editModule"
/>
<p
class="chapter__description">
{{ chapter.description }}
</p>
</div>
<add-content-button <add-content-button
:parent="chapter" :parent="chapter"
@ -29,132 +55,154 @@
</template> </template>
<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 BookmarkActions from '@/components/notes/BookmarkActions';
import VisibilityAction from '@/components/visibility/VisibilityAction';
import {mapState} from 'vuex'; import {mapState} from 'vuex';
import {isHidden} from '@/helpers/content-block'; import {hidden} from '@/helpers/visibility';
import {meQuery} from '@/graphql/queries'; import {CONTENT_TYPE, CHAPTER_DESCRIPTION_TYPE, CHAPTER_TITLE_TYPE} from '@/consts/types';
import UPDATE_CHAPTER_BOOKMARK_MUTATION from '@/graphql/gql/mutations/updateChapterBookmark.gql'; import UPDATE_CHAPTER_BOOKMARK_MUTATION from '@/graphql/gql/mutations/updateChapterBookmark.gql';
import CHAPTER_QUERY from '@/graphql/gql/chapterQuery.gql'; import CHAPTER_QUERY from '@/graphql/gql/chapterQuery.gql';
export default { import me from '@/mixins/me';
props: ['chapter', 'index'],
components: { export default {
BookmarkActions, props: ['chapter', 'index'],
ContentBlock,
AddContentButton mixins: [me],
components: {
BookmarkActions,
VisibilityAction,
ContentBlock,
AddContentButton
},
computed: {
...mapState(['editModule']),
filteredContentBlocks() {
if (!(this.chapter && this.chapter.contentBlocks)) {
return [];
}
if (this.editModule) {
return this.chapter.contentBlocks;
}
return this.chapter.contentBlocks.filter(contentBlock => !hidden({
block: contentBlock,
schoolClass: this.schoolClass,
type: CONTENT_TYPE
}));
}, },
note() {
data() { if (this.chapter && this.chapter.bookmark) {
return {
me: {}
};
},
computed: {
...mapState(['editModule']),
filteredContentBlocks() {
if (!(this.chapter && this.chapter.contentBlocks)) {
return [];
}
if (this.editModule) {
return this.chapter.contentBlocks;
}
return this.chapter.contentBlocks.filter(contentBlock => !isHidden(contentBlock, this.schoolClass));
},
schoolClass() {
return this.me.selectedClass;
},
note() {
if (!(this.chapter && this.chapter.bookmark)) {
return;
}
return this.chapter.bookmark.note; return this.chapter.bookmark.note;
} }
}, },
titleGreyedOut() {
methods: { return this.textHidden(CHAPTER_TITLE_TYPE) && this.editModule;
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);
},
}, },
// never hidden when editing the module
apollo: { titleHidden() {
me: meQuery return this.textHidden(CHAPTER_TITLE_TYPE) && !this.editModule;
},
descriptionGreyedOut() {
return this.textHidden(CHAPTER_DESCRIPTION_TYPE) && this.editModule;
},
// never hidden when editing the module
descriptionHidden() {
return this.textHidden(CHAPTER_DESCRIPTION_TYPE) && !this.editModule;
} }
}; },
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);
},
textHidden(type) {
return hidden({
block: this.chapter,
schoolClass: this.schoolClass,
type
});
}
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_mixins.scss"; @import "@/styles/_mixins.scss";
.chapter { .chapter {
position: relative; position: relative;
&__bookmark-actions { &__bookmark-actions {
margin-top: 3px; margin-top: 3px;
}
&__description {
@include lead-paragraph;
margin-bottom: $large-spacing;
}
} }
&__intro {
position: relative;
}
&__description {
@include lead-paragraph;
margin-bottom: $large-spacing;
}
}
</style> </style>

View File

@ -1,6 +1,6 @@
<template> <template>
<div <div
:class="{'hideable-element--hidden': hidden}" :class="{'hideable-element--greyed-out': hidden}"
class="content-block__container hideable-element"> class="content-block__container hideable-element">
<div <div
:class="specialClass" :class="specialClass"
@ -60,11 +60,11 @@
import CHAPTER_QUERY from '@/graphql/gql/chapterQuery.gql'; import CHAPTER_QUERY from '@/graphql/gql/chapterQuery.gql';
import DELETE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/deleteContentBlock.gql'; import DELETE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/deleteContentBlock.gql';
import {meQuery} from '@/graphql/queries';
import {mapState} from 'vuex'; 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 = { const instruments = {
base_communication: 'Sprache & Kommunikation', base_communication: 'Sprache & Kommunikation',
@ -77,6 +77,8 @@
name: 'ContentBlock', name: 'ContentBlock',
props: ['contentBlock', 'parent'], props: ['contentBlock', 'parent'],
mixins: [me],
components: { components: {
ContentComponent, ContentComponent,
AddContentButton, AddContentButton,
@ -85,13 +87,6 @@
UserWidget UserWidget
}, },
data() {
return {
showVisibility: false,
me: {}
};
},
computed: { computed: {
...mapState(['editModule']), ...mapState(['editModule']),
canEditModule() { canEditModule() {
@ -164,11 +159,12 @@
contents: this.removeSingleContentListItem(newContent, startingIndex) contents: this.removeSingleContentListItem(newContent, startingIndex)
}); });
}, },
schoolClass() {
return this.me.selectedClass;
},
hidden() { hidden() {
return isHidden(this.contentBlock, this.schoolClass); return hidden({
block: this.contentBlock,
schoolClass: this.schoolClass,
type: CONTENT_TYPE
});
}, },
root() { root() {
// we need the root content block id, not the generated content block if inside a content list block // 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)]; return [...content.slice(0, listIndex), ...content[listIndex].contents[0].value, ...content.slice(listIndex + 1)];
} }
},
apollo: {
me: meQuery
} }
}; };
</script> </script>

View File

@ -47,14 +47,11 @@
import ObjectiveGroups from '@/components/objective-groups/ObjectiveGroups.vue'; import ObjectiveGroups from '@/components/objective-groups/ObjectiveGroups.vue';
import Chapter from '@/components/Chapter.vue'; import Chapter from '@/components/Chapter.vue';
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 UPDATE_MODULE_BOOKMARK_MUTATION from '@/graphql/gql/mutations/updateModuleBookmark.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_FRAGMENT from '@/graphql/gql/fragments/moduleParts.gql'; import MODULE_FRAGMENT from '@/graphql/gql/fragments/moduleParts.gql';
import {withoutOwnerFirst} from '@/helpers/sorting';
import BookmarkActions from '@/components/notes/BookmarkActions'; import BookmarkActions from '@/components/notes/BookmarkActions';
import meMixin from '@/mixins/me'; import meMixin from '@/mixins/me';
@ -82,18 +79,15 @@
computed: { computed: {
languageCommunicationObjectiveGroups() { languageCommunicationObjectiveGroups() {
return this.module.objectiveGroups ? this.module.objectiveGroups return this.module.objectiveGroups ? this.module.objectiveGroups
.filter(group => group.title === 'LANGUAGE_COMMUNICATION') .filter(group => group.title === 'LANGUAGE_COMMUNICATION') : [];
.sort(withoutOwnerFirst) : [];
}, },
societyObjectiveGroups() { societyObjectiveGroups() {
return this.module.objectiveGroups ? this.module.objectiveGroups return this.module.objectiveGroups ? this.module.objectiveGroups
.filter(group => group.title === 'SOCIETY') .filter(group => group.title === 'SOCIETY') : [];
.sort(withoutOwnerFirst) : [];
}, },
interdisciplinaryObjectiveGroups() { interdisciplinaryObjectiveGroups() {
return this.module.objectiveGroups ? this.module.objectiveGroups return this.module.objectiveGroups ? this.module.objectiveGroups
.filter(group => group.title === 'INTERDISCIPLINARY') .filter(group => group.title === 'INTERDISCIPLINARY') : [];
.sort(withoutOwnerFirst) : [];
}, },
isStudent() { isStudent() {
return !this.me.permissions.includes('users.can_manage_school_class_content'); return !this.me.permissions.includes('users.can_manage_school_class_content');
@ -145,28 +139,6 @@
} }
}); });
}, },
updateObjectiveProgress(done, objectiveId) {
this.$apollo.mutate({
mutation: UPDATE_OBJECTIVE_PROGRESS_MUTATION,
variables: {
input: {
id: objectiveId,
done: done
}
},
update(store, {data: {updateObjectiveProgress: {objective}}}) {
if (objective) {
const variables = {id: objectiveId};
const query = OBJECTIVE_QUERY;
const data = store.readQuery({query, variables});
if (data && data.objective.objectiveProgress.edges.length > 0) {
data.objective.objectiveProgress.edges[0].node.done = done;
}
store.writeQuery({query: OBJECTIVE_QUERY, data, variables});
}
}
});
},
bookmark(bookmarked) { bookmark(bookmarked) {
const slug = this.module.slug; const slug = this.module.slug;
this.$apollo.mutate({ this.$apollo.mutate({

View File

@ -59,21 +59,20 @@
<script> <script>
import {moduleQuery} from '@/graphql/queries'; import {moduleQuery} from '@/graphql/queries';
import ME_QUERY from '@/graphql/gql/meQuery.gql';
import SubNavigationItem from '@/components/book-navigation/SubNavigationItem'; import SubNavigationItem from '@/components/book-navigation/SubNavigationItem';
import ToggleSolutionsForModule from '@/components/toggle-menu/ToggleSolutionsForModule'; import ToggleSolutionsForModule from '@/components/toggle-menu/ToggleSolutionsForModule';
import ToggleEditing from '@/components/toggle-menu/ToggleEditing'; import ToggleEditing from '@/components/toggle-menu/ToggleEditing';
import ChevronLeft from '@/components/icons/ChevronLeft'; import ChevronLeft from '@/components/icons/ChevronLeft';
import me from '@/mixins/me';
export default { export default {
apollo: { apollo: {
module: moduleQuery, module: moduleQuery,
me: {
query: ME_QUERY
}
}, },
mixins: [me],
components: { components: {
SubNavigationItem, SubNavigationItem,
ToggleSolutionsForModule, ToggleSolutionsForModule,
@ -87,9 +86,6 @@
assignments: [], assignments: [],
topic: {} topic: {}
}, },
me: {
permissions: []
}
}; };
}, },
@ -103,9 +99,6 @@
showResults() { showResults() {
return this.me.permissions.includes('users.can_manage_school_class_content'); return this.me.permissions.includes('users.can_manage_school_class_content');
}, },
canManageContent() {
return this.me.permissions.includes('users.can_manage_school_class_content');
},
assignments() { assignments() {
if (!this.module.chapters) { if (!this.module.chapters) {
return []; return [];

View File

@ -1,10 +1,11 @@
<template> <template>
<li <li
:class="{'hideable-element--hidden': hidden}" :class="{'hideable-element--greyed-out': hidden}"
class="objective hideable-element" class="objective hideable-element"
v-if="editModule || !hidden"> v-if="editModule || !hidden">
<visibility-action <visibility-action
:block="objective" :block="objective"
:type="type"
v-if="editModule"/> v-if="editModule"/>
<div <div
class="block-actions" class="block-actions"
@ -28,15 +29,19 @@
import UserWidget from '@/components/UserWidget'; import UserWidget from '@/components/UserWidget';
import MoreOptionsWidget from '@/components/MoreOptionsWidget'; import MoreOptionsWidget from '@/components/MoreOptionsWidget';
import {mapState} from 'vuex';
import {isHidden} from '@/helpers/content-block';
import {meQuery} from '@/graphql/queries';
import DELETE_OBJECTIVE_MUTATION from '@/graphql/gql/mutations/deleteObjective.gql'; import DELETE_OBJECTIVE_MUTATION from '@/graphql/gql/mutations/deleteObjective.gql';
import MODULE_DETAILS_QUERY from '@/graphql/gql/moduleDetailsQuery.gql'; import MODULE_DETAILS_QUERY from '@/graphql/gql/moduleDetailsQuery.gql';
import {hidden} from '@/helpers/visibility';
import {OBJECTIVE_TYPE} from '@/consts/types';
import editModule from '@/mixins/edit-module';
import me from '@/mixins/me';
export default { export default {
props: ['objective', 'schoolClass'], props: ['objective', 'schoolClass'],
mixins: [me, editModule],
components: { components: {
MoreOptionsWidget, MoreOptionsWidget,
VisibilityAction, VisibilityAction,
@ -45,14 +50,17 @@
data() { data() {
return { return {
me: {} type: OBJECTIVE_TYPE
}; };
}, },
computed: { computed: {
...mapState(['editModule']),
hidden() { hidden() {
return isHidden(this.objective, this.schoolClass); return hidden({
block: this.objective,
schoolClass: this.schoolClass,
type: this.type
});
}, },
canEdit() { canEdit() {
return this.objective.mine; return this.objective.mine;
@ -89,17 +97,14 @@
// } // }
}); });
} }
}, }
apollo: {
me: meQuery
},
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.objective { .objective {
min-height: 50px; min-height: 50px;
&__user-widget { &__user-widget {
margin-right: 0; margin-right: 0;
} }

View File

@ -1,5 +1,12 @@
<template> <template>
<div class="objective-group"> <div
:class="{'hideable-element--greyed-out': hidden}"
class="objective-group hideable-element"
v-if="editModule || !hidden">
<visibility-action
:block="group"
:type="type"
v-if="editModule"/>
<h4>{{ group.displayTitle }}</h4> <h4>{{ group.displayTitle }}</h4>
@ -7,7 +14,7 @@
<objective <objective
:key="objective.id" :key="objective.id"
:objective="objective" :objective="objective"
:school-class="currentFilter" :school-class="schoolClass"
class="objective-group__objective" class="objective-group__objective"
v-for="objective in group.objectives"/> v-for="objective in group.objectives"/>
</ul> </ul>
@ -20,11 +27,14 @@
<script> <script>
import Objective from '@/components/objective-groups/Objective'; import Objective from '@/components/objective-groups/Objective';
import VisibilityAction from '@/components/visibility/VisibilityAction';
import ME_QUERY from '@/graphql/gql/meQuery.gql';
import AddContentButton from '@/components/AddContentButton'; import AddContentButton from '@/components/AddContentButton';
import {mapState} from 'vuex'; import me from '@/mixins/me';
import editModule from '@/mixins/edit-module';
import {OBJECTIVE_GROUP_TYPE} from '@/consts/types';
import {hidden} from '@/helpers/visibility';
export default { export default {
props: { props: {
@ -34,24 +44,27 @@
} }
}, },
mixins: [me, editModule],
components: { components: {
AddContentButton, AddContentButton,
Objective Objective,
VisibilityAction
}, },
apollo: { data() {
me: { return {
query: ME_QUERY, type: OBJECTIVE_GROUP_TYPE
}, };
}, },
computed: { computed: {
...mapState(['editModule']), hidden() {
canManageContent() { return hidden({
return this.me.permissions.includes('users.can_manage_school_class_content'); block: this.group,
}, schoolClass: this.schoolClass,
currentFilter() { type: this.type
return this.me.selectedClass; });
}, },
}, },
}; };

View File

@ -10,7 +10,6 @@
<script> <script>
import ObjectiveGroup from '@/components/objective-groups/ObjectiveGroup'; import ObjectiveGroup from '@/components/objective-groups/ObjectiveGroup';
import {meQuery} from '@/graphql/queries'; import {meQuery} from '@/graphql/queries';
import {withoutOwnerFirst} from '@/helpers/sorting';
export default { export default {
props: { props: {
@ -47,19 +46,13 @@
return this.groups; return this.groups;
} else { } else {
// todo: maybe this can be done a bit more elegantly // todo: maybe this can be done a bit more elegantly
const groups = [...this.groups].sort(withoutOwnerFirst); let groups = this.groups;
const objectives = groups.map(g => g.objectives).flat(); // get all objectives in one array const objectives = groups.map(g => g.objectives).flat(); // get all objectives in one array
const firstGroup = Object.assign({}, groups.shift(), {objectives}); const firstGroup = Object.assign({}, groups.shift(), {objectives});
return [firstGroup]; return [firstGroup];
} }
} }
},
methods: {
updateObjectiveProgress(checked, id) {
this.$emit('updateObjectiveProgress', checked, id);
}
} }
}; };
</script> </script>

View File

@ -8,7 +8,7 @@
<script> <script>
import Checkbox from '@/components/Checkbox'; import Checkbox from '@/components/Checkbox';
import {mapGetters, mapActions} from 'vuex'; import {mapState, mapActions} from 'vuex';
export default { export default {
components: { components: {
@ -16,7 +16,7 @@
}, },
computed: { computed: {
...mapGetters({ ...mapState({
checked: 'editModule', checked: 'editModule',
}) })
}, },

View File

@ -18,79 +18,51 @@
import EyeIcon from '@/components/icons/EyeIcon'; import EyeIcon from '@/components/icons/EyeIcon';
import ClosedEyeIcon from '@/components/icons/ClosedEyeIcon'; import ClosedEyeIcon from '@/components/icons/ClosedEyeIcon';
import ME_QUERY from '@/graphql/gql/meQuery.gql'; import me from '@/mixins/me';
import CHANGE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/mutateContentBlock.gql';
import UPDATE_OBJECTIVE_VISIBILITY_MUTATION from '@/graphql/gql/mutations/updateObjectiveVisibility.gql'; import {TYPES, CONTENT_TYPE} from '@/consts/types';
import {createVisibilityMutation, hidden} from '@/helpers/visibility';
export default { export default {
props: ['block'], props: {
block: {
type: Object,
default: () => ({})
},
type: {
type: String,
default: CONTENT_TYPE,
validator: value => {
// value must be one of TYPES
return TYPES.indexOf(value) !== -1;
}
}
},
mixins: [me],
components: { components: {
EyeIcon, EyeIcon,
ClosedEyeIcon ClosedEyeIcon
}, },
data() {
return {
showVisibility: false,
me: {
permissions: []
}
};
},
computed: { 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() { hidden() {
// is this content block / objective group user created? return hidden({type: this.type, block: this.block, schoolClass: this.schoolClass});
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;
} }
}, },
methods: { methods: {
toggleVisibility() { toggleVisibility() {
let hidden = !this.hidden; const hidden = !this.hidden;
let schoolClassId = this.schoolClass.id; const schoolClassId = this.schoolClass.id;
const visibility = [{ const visibility = [{
schoolClassId, schoolClassId,
hidden hidden
}]; }];
let mutation, variables; const {mutation, variables} = createVisibilityMutation(this.type, this.block.id, visibility);
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
}
};
}
this.$apollo.mutate({ this.$apollo.mutate({
mutation, mutation,
@ -98,12 +70,6 @@
}); });
}, },
}, },
apollo: {
me: {
query: ME_QUERY,
},
},
}; };
</script> </script>

View File

@ -0,0 +1,12 @@
export const CONTENT_TYPE = 'content';
export const OBJECTIVE_TYPE = 'objective';
export const OBJECTIVE_GROUP_TYPE = 'objective-group';
export const CHAPTER_TITLE_TYPE = 'chapter-title';
export const CHAPTER_DESCRIPTION_TYPE = 'chapter-description';
export const TYPES = [
CONTENT_TYPE,
OBJECTIVE_TYPE,
OBJECTIVE_GROUP_TYPE,
CHAPTER_TITLE_TYPE,
CHAPTER_DESCRIPTION_TYPE
];

View File

@ -16,4 +16,20 @@ fragment ChapterParts on ChapterNode {
} }
} }
} }
titleHiddenFor {
edges {
node {
id
name
}
}
}
descriptionHiddenFor {
edges {
node {
id
name
}
}
}
} }

View File

@ -2,10 +2,6 @@ fragment ObjectiveGroupParts on ObjectiveGroupNode {
id id
title title
displayTitle displayTitle
mine
owner {
id
}
hiddenFor { hiddenFor {
edges { edges {
node { node {
@ -14,12 +10,4 @@ fragment ObjectiveGroupParts on ObjectiveGroupNode {
} }
} }
} }
visibleFor {
edges {
node {
id
name
}
}
}
} }

View File

@ -19,12 +19,4 @@ fragment ObjectiveParts on ObjectiveNode {
} }
} }
} }
objectiveProgress {
edges {
node {
id
done
}
}
}
} }

View File

@ -5,6 +5,5 @@ mutation MutateContentBlock($input: MutateContentBlockInput!) {
contentBlock { contentBlock {
...ContentBlockParts ...ContentBlockParts
} }
errors
} }
} }

View File

@ -0,0 +1,9 @@
#import "../fragments/chapterParts.gql"
mutation UpdateChapterVisibility($input: UpdateChapterVisibilityInput!) {
updateChapterVisibility(input: $input) {
chapter {
...ChapterParts
}
}
}

View File

@ -0,0 +1,9 @@
#import "../fragments/objectiveGroupParts.gql"
mutation UpdateObjectiveGroupVisibility($input: UpdateObjectiveGroupVisibilityInput!) {
updateObjectiveGroupVisibility(input: $input) {
objectiveGroup {
...ObjectiveGroupParts
}
}
}

View File

@ -1,17 +1,3 @@
export function setUserBlockType(isAssignment) { export function setUserBlockType(isAssignment) {
return isAssignment ? 'TASK' : 'NORMAL'; return isAssignment ? 'TASK' : 'NORMAL';
} }
export const isHidden = (contentBlock, schoolClass) => {
if (!contentBlock.id || !contentBlock.visibleFor || !contentBlock.hiddenFor) {
return false;
}
if (contentBlock.userCreated) {
if (schoolClass.id === '') {
return false;
}
return !contentBlock.visibleFor.map(entry => entry.id).includes(schoolClass.id);
} else {
return contentBlock.hiddenFor.map(entry => entry.id).includes(schoolClass.id);
}
};

View File

@ -0,0 +1,95 @@
import {
CHAPTER_DESCRIPTION_TYPE,
CHAPTER_TITLE_TYPE,
CONTENT_TYPE,
OBJECTIVE_GROUP_TYPE,
OBJECTIVE_TYPE
} from '@/consts/types';
import CHANGE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/mutateContentBlock.gql';
import UPDATE_OBJECTIVE_VISIBILITY_MUTATION from '@/graphql/gql/mutations/updateObjectiveVisibility.gql';
import UPDATE_OBJECTIVE_GROUP_VISIBILITY_MUTATION from '@/graphql/gql/mutations/updateObjectiveGroupVisibility.gql';
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 OBJECTIVE_GROUP_TYPE:
mutation = UPDATE_OBJECTIVE_GROUP_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 OBJECTIVE_GROUP_TYPE:
return containsClass(hiddenFor, schoolClass);
case CHAPTER_TITLE_TYPE:
return containsClass(titleHiddenFor, schoolClass);
case CHAPTER_DESCRIPTION_TYPE:
return containsClass(descriptionHiddenFor, schoolClass);
default:
return false;
}
};

View File

@ -0,0 +1,7 @@
import {mapState} from 'vuex';
export default {
computed: {
...mapState(['editModule']),
}
};

View File

@ -15,19 +15,25 @@ export default {
}; };
}, },
computed: { computed: {
topicRoute() { topicRoute() {
if (this.me.lastTopic && this.me.lastTopic.slug) { if (this.me.lastTopic && this.me.lastTopic.slug) {
return { return {
name: 'topic', name: 'topic',
params: { params: {
topicSlug: this.me.lastTopic.slug topicSlug: this.me.lastTopic.slug
} }
}; };
}
return '/book/topic/berufliche-grundbildung';
} }
return '/book/topic/berufliche-grundbildung';
}, },
schoolClass() {
return this.me.selectedClass;
},
canManageContent() {
return this.me.permissions.includes('users.can_manage_school_class_content');
},
},
apollo: { apollo: {
me: { me: {

View File

@ -6,7 +6,7 @@
</template> </template>
<script> <script>
import {mapGetters, mapActions} from 'vuex'; import {mapState, mapActions} from 'vuex';
import MODULE_DETAILS_QUERY from '@/graphql/gql/moduleDetailsQuery.gql'; import MODULE_DETAILS_QUERY from '@/graphql/gql/moduleDetailsQuery.gql';
import SCROLL_POSITION from '@/graphql/gql/local/scrollPosition.gql'; import SCROLL_POSITION from '@/graphql/gql/local/scrollPosition.gql';
@ -26,7 +26,7 @@
}, },
computed: { computed: {
...mapGetters({ ...mapState({
editModule: 'editModule' editModule: 'editModule'
}), }),
}, },

View File

@ -1,7 +1,7 @@
.hideable-element { .hideable-element {
position: relative; position: relative;
&--hidden { &--greyed-out {
&::before { &::before {
content: ''; content: '';
position: absolute; position: absolute;

View File

@ -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.mutations import AssignmentMutations
from assignments.schema.queries import AssignmentsQuery, StudentSubmissionQuery from assignments.schema.queries import AssignmentsQuery, StudentSubmissionQuery
from basicknowledge.queries import BasicKnowledgeQuery 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 books.schema.queries import BookQuery
from core.schema.mutations.coupon import CouponMutations from core.schema.mutations.coupon import CouponMutations
from core.schema.mutations.main import CoreMutations from core.schema.mutations.main import CoreMutations

View File

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

View File

@ -4,6 +4,7 @@ from django.db import models
from wagtail.admin.edit_handlers import FieldPanel, TabbedInterface, ObjectList from wagtail.admin.edit_handlers import FieldPanel, TabbedInterface, ObjectList
from core.wagtail_utils import StrictHierarchyPage from core.wagtail_utils import StrictHierarchyPage
from users.models import SchoolClass
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -34,3 +35,5 @@ class Chapter(StrictHierarchyPage):
parent_page_types = ['books.Module'] parent_page_types = ['books.Module']
subpage_types = ['books.ContentBlock'] 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')

View File

@ -1,9 +1,13 @@
# -*- coding: utf-8 -*- from books.schema.mutations.chapter import UpdateChapterVisibility
# from books.schema.mutations.contentblock import MutateContentBlock, AddContentBlock, DeleteContentBlock
# Iterativ GmbH from books.schema.mutations.module import UpdateSolutionVisibility, UpdateLastModule, UpdateLastTopic
# http://www.iterativ.ch/
#
# Copyright (c) 2018 Iterativ GmbH. All rights reserved. class BookMutations(object):
# mutate_content_block = MutateContentBlock.Field()
# Created on 25.09.18 add_content_block = AddContentBlock.Field()
# @author: Ramon Wenger <ramon.wenger@iterativ.ch> 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()

View File

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

View File

@ -10,7 +10,6 @@ from books.models import ContentBlock, Chapter, SchoolClass
from books.schema.inputs import ContentBlockInput from books.schema.inputs import ContentBlockInput
from books.schema.queries import ContentBlockNode from books.schema.queries import ContentBlockNode
from core.utils import set_hidden_for, set_visible_for 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 from .utils import handle_content_block, set_user_defined_block_type
@ -19,7 +18,6 @@ class MutateContentBlock(relay.ClientIDMutation):
id = graphene.ID(required=True) id = graphene.ID(required=True)
content_block = graphene.Argument(ContentBlockInput) content_block = graphene.Argument(ContentBlockInput)
errors = graphene.List(graphene.String)
content_block = graphene.Field(ContentBlockNode) content_block = graphene.Field(ContentBlockNode)
@classmethod @classmethod
@ -52,17 +50,16 @@ class MutateContentBlock(relay.ClientIDMutation):
if contents is not None: if contents is not None:
content_block.contents = json.dumps([handle_content_block(c, info.context, module) for c in contents]) content_block.contents = json.dumps([handle_content_block(c, info.context, module) for c in contents])
content_block.save() content_block.save()
return cls(content_block=content_block) return cls(content_block=content_block)
except ValidationError as e: except ValidationError as e:
errors = get_errors(e) errors = get_errors(e)
raise errors
except Exception as e: except Exception as e:
errors = ['Error: {}'.format(e)] errors = ['Error: {}'.format(e)]
raise errors
return cls(content_block=None, errors=errors)
class AddContentBlock(relay.ClientIDMutation): class AddContentBlock(relay.ClientIDMutation):

View File

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

View File

@ -77,7 +77,7 @@ class ChapterNode(DjangoObjectType):
class Meta: class Meta:
model = Chapter model = Chapter
only_fields = [ only_fields = [
'slug', 'title', 'description', 'slug', 'title', 'description', 'title_hidden_for', 'description_hidden_for'
] ]
filter_fields = [ filter_fields = [
'slug', 'title', 'slug', 'title',
@ -183,9 +183,7 @@ class ModuleNode(DjangoObjectType):
def resolve_objective_groups(self, root, **kwargs): def resolve_objective_groups(self, root, **kwargs):
return self.objective_groups.all() \ return self.objective_groups.all() \
.prefetch_related('hidden_for') \ .prefetch_related('hidden_for')
.prefetch_related('visible_for') \
.prefetch_related('objectives__objective_progress')
class RecentModuleNode(DjangoObjectType): class RecentModuleNode(DjangoObjectType):

View File

@ -88,7 +88,7 @@ class Command(BaseCommand):
for objective_group_idx, objective_group_entry in enumerate(objective_group_data): for objective_group_idx, objective_group_entry in enumerate(objective_group_data):
factory_params = self.filter_data(objective_group_entry, 'objectives') factory_params = self.filter_data(objective_group_entry, 'objectives')
objective_group = ObjectiveGroupFactory.create(module=module, objective_group = ObjectiveGroupFactory.create(module=module,
owner=None,
**factory_params) **factory_params)
default_objectives = [{} for i in range(0, 4)] default_objectives = [{} for i in range(0, 4)]

View File

@ -1,18 +1,19 @@
from django.contrib import admin from django.contrib import admin
from wagtail.core.models import Page from wagtail.core.models import Page
from objectives.models import ObjectiveGroup, Objective, ObjectiveProgressStatus from objectives.models import ObjectiveGroup, Objective
from books.models import Topic from books.models import Topic
@admin.register(ObjectiveGroup) @admin.register(ObjectiveGroup)
class ObjectiveGroupAdmin(admin.ModelAdmin): class ObjectiveGroupAdmin(admin.ModelAdmin):
list_display = ('title', 'module', 'owner') list_display = ('title', 'module')
list_filter = ('title', 'module', 'owner') list_filter = ('title', 'module')
@admin.register(Objective) @admin.register(Objective)
class ObjectiveAdmin(admin.ModelAdmin): class ObjectiveAdmin(admin.ModelAdmin):
list_display = ('text', 'get_topic', 'group', 'order', 'owner') list_display = ('text', 'get_topic', 'group', 'order', 'owner')
list_filter = ('group', 'owner') list_filter = ('group', 'owner')
def get_topic(self, obj): def get_topic(self, obj):
@ -36,9 +37,3 @@ class ObjectiveAdmin(admin.ModelAdmin):
return topic.title return topic.title
get_topic.short_description = 'Thema' get_topic.short_description = 'Thema'
@admin.register(ObjectiveProgressStatus)
class ObjectiveProgressStatusAdmin(admin.ModelAdmin):
list_display = ('objective', 'user', 'done')
list_filter = ('objective', 'user', 'done')

View File

@ -15,7 +15,6 @@ class ObjectiveGroupFactory(factory.django.DjangoModelFactory):
title = factory.Iterator([ObjectiveGroup.LANGUAGE_COMMUNICATION, ObjectiveGroup.SOCIETY]) title = factory.Iterator([ObjectiveGroup.LANGUAGE_COMMUNICATION, ObjectiveGroup.SOCIETY])
module = factory.SubFactory(ModuleFactory) module = factory.SubFactory(ModuleFactory)
owner = factory.Iterator(get_user_model().objects.all())
class ObjectiveFactory(factory.django.DjangoModelFactory): class ObjectiveFactory(factory.django.DjangoModelFactory):

View File

@ -0,0 +1,21 @@
# Generated by Django 2.2.18 on 2021-02-22 15:08
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('objectives', '0012_auto_20210210_1109'),
]
operations = [
migrations.RemoveField(
model_name='objectivegroup',
name='owner',
),
migrations.RemoveField(
model_name='objectivegroup',
name='visible_for',
),
]

View File

@ -23,11 +23,8 @@ class ObjectiveGroup(models.Model):
title = models.CharField('title', blank=True, null=False, max_length=255, choices=TITLE_CHOICES, default=LANGUAGE_COMMUNICATION) title = models.CharField('title', blank=True, null=False, max_length=255, choices=TITLE_CHOICES, default=LANGUAGE_COMMUNICATION)
module = models.ForeignKey(Module, blank=False, null=False, on_delete=models.CASCADE, related_name='objective_groups') module = models.ForeignKey(Module, blank=False, null=False, on_delete=models.CASCADE, related_name='objective_groups')
# a user can define her own objectives, hence this optional param
owner = models.ForeignKey(get_user_model(), blank=True, null=True, on_delete=models.PROTECT)
hidden_for = models.ManyToManyField(SchoolClass, related_name='hidden_objective_groups', blank=True) hidden_for = models.ManyToManyField(SchoolClass, related_name='hidden_objective_groups', blank=True)
visible_for = models.ManyToManyField(SchoolClass, related_name='visible_objective_groups', blank=True)
def __str__(self): def __str__(self):
return '{} - {}'.format(self.module, self.title) return '{} - {}'.format(self.module, self.title)
@ -49,18 +46,3 @@ class Objective(models.Model):
def __str__(self): def __str__(self):
return 'Objective {}-{}'.format(self.id, self.text) return 'Objective {}-{}'.format(self.id, self.text)
# todo: delete
class ObjectiveProgressStatus(models.Model):
class Meta:
verbose_name = 'Lernzielstatus'
verbose_name_plural = 'Lernzielstatus'
done = models.BooleanField('Lernziel erledigt?', default=False)
objective = models.ForeignKey(Objective, blank=False, null=False, on_delete=models.CASCADE,
related_name='objective_progress')
user = models.ForeignKey(get_user_model(), blank=True, null=True, on_delete=models.CASCADE)
def __str__(self):
return 'Lernzielstatus {}-{}'.format(self.objective, self.done)

View File

@ -6,34 +6,10 @@ from api.utils import get_object
from books.schema.inputs import UserGroupBlockVisibility from books.schema.inputs import UserGroupBlockVisibility
from core.utils import set_visible_for, set_hidden_for from core.utils import set_visible_for, set_hidden_for
from objectives.inputs import AddObjectiveArgument from objectives.inputs import AddObjectiveArgument
from objectives.models import ObjectiveProgressStatus, Objective, ObjectiveGroup from objectives.models import Objective, ObjectiveGroup
from objectives.schema import ObjectiveNode, ObjectiveGroupNode from objectives.schema import ObjectiveNode, ObjectiveGroupNode
class UpdateObjectiveProgress(relay.ClientIDMutation):
class Input:
id = graphene.ID(required=True, description="The ID of the objective")
done = graphene.Boolean(required=True)
errors = graphene.List(graphene.String)
objective = graphene.Field(ObjectiveNode)
@classmethod
def mutate_and_get_payload(cls, root, info, **kwargs):
objective_id = kwargs.get('id')
done = kwargs.get('done')
objective = get_object(Objective, objective_id) # info.context.user = django user
objective_progress, created = ObjectiveProgressStatus.objects.get_or_create(
objective=objective,
user=info.context.user
)
objective_progress.done = done
objective_progress.save()
return cls(objective=objective)
class UpdateObjectiveVisibility(relay.ClientIDMutation): class UpdateObjectiveVisibility(relay.ClientIDMutation):
class Input: class Input:
id = graphene.ID(required=True, description='The ID of the objective') id = graphene.ID(required=True, description='The ID of the objective')
@ -58,6 +34,26 @@ class UpdateObjectiveVisibility(relay.ClientIDMutation):
return cls(objective=objective) return cls(objective=objective)
class UpdateObjectiveGroupVisibility(relay.ClientIDMutation):
class Input:
id = graphene.ID(required=True, description='The ID of the objective group')
visibility = graphene.List(UserGroupBlockVisibility)
objective_group = graphene.Field(ObjectiveGroupNode)
@classmethod
def mutate_and_get_payload(cls, root, info, **kwargs):
objective_group_id = kwargs.get('id')
visibility_list = kwargs.get('visibility')
objective_group = get_object(ObjectiveGroup, objective_group_id) # info.context.user = django user
if visibility_list is not None:
set_hidden_for(objective_group, visibility_list)
objective_group.save()
return cls(objective_group=objective_group)
class AddObjective(relay.ClientIDMutation): class AddObjective(relay.ClientIDMutation):
class Input: class Input:
objective = graphene.Argument(AddObjectiveArgument) objective = graphene.Argument(AddObjectiveArgument)
@ -104,7 +100,7 @@ class DeleteObjective(relay.ClientIDMutation):
class ObjectiveMutations: class ObjectiveMutations:
update_objective_progress = UpdateObjectiveProgress.Field()
update_objective_visibility = UpdateObjectiveVisibility.Field() update_objective_visibility = UpdateObjectiveVisibility.Field()
update_objective_group_visibility = UpdateObjectiveGroupVisibility.Field()
add_objective = AddObjective.Field() add_objective = AddObjective.Field()
delete_objective = DeleteObjective.Field() delete_objective = DeleteObjective.Field()

View File

@ -4,13 +4,12 @@ from graphene import relay
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
from graphene_django.filter import DjangoFilterConnectionField from graphene_django.filter import DjangoFilterConnectionField
from objectives.models import ObjectiveGroup, Objective, ObjectiveProgressStatus from objectives.models import ObjectiveGroup, Objective
class ObjectiveGroupNode(DjangoObjectType): class ObjectiveGroupNode(DjangoObjectType):
pk = graphene.Int() pk = graphene.Int()
display_title = graphene.String() display_title = graphene.String()
mine = graphene.Boolean()
class Meta: class Meta:
model = ObjectiveGroup model = ObjectiveGroup
@ -23,9 +22,6 @@ class ObjectiveGroupNode(DjangoObjectType):
def resolve_display_title(self, *args, **kwargs): def resolve_display_title(self, *args, **kwargs):
return self.get_title_display() return self.get_title_display()
def resolve_mine(self, info, **kwargs):
return self.owner is not None and self.owner.pk == info.context.user.pk
def resolve_objectives(self, info, **kwargs): def resolve_objectives(self, info, **kwargs):
user = info.context.user user = info.context.user
school_classes = user.school_classes.values_list('pk') school_classes = user.school_classes.values_list('pk')
@ -59,17 +55,6 @@ class ObjectiveNode(DjangoObjectType):
def resolve_mine(self, info, **kwargs): def resolve_mine(self, info, **kwargs):
return self.owner is not None and self.owner.pk == info.context.user.pk return self.owner is not None and self.owner.pk == info.context.user.pk
class ObjectiveProgressStatusNode(DjangoObjectType):
pk = graphene.Int()
class Meta:
model = ObjectiveProgressStatus
filter_fields = ['objective__text', 'user__username', 'done']
interfaces = (relay.Node,)
def resolve_pk(self, *args, **kwargs):
return self.id
class ObjectivesQuery(object): class ObjectivesQuery(object):
objective_group = relay.Node.Field(ObjectiveGroupNode) objective_group = relay.Node.Field(ObjectiveGroupNode)

View File

@ -21,7 +21,7 @@ class ObjectiveOrderTestCase(TestCase):
self.user = user = User.objects.get(username='teacher') self.user = user = User.objects.get(username='teacher')
self.objective_group = ObjectiveGroupFactory(owner=None) self.objective_group = ObjectiveGroupFactory()
request = RequestFactory().get('/') request = RequestFactory().get('/')
@ -29,10 +29,10 @@ class ObjectiveOrderTestCase(TestCase):
self.client = Client(schema=schema, context_value=request) self.client = Client(schema=schema, context_value=request)
Objective.objects.create(owner=None, text='first', group=self.objective_group, order=0) Objective.objects.create(text='first', group=self.objective_group, order=0)
Objective.objects.create(owner=None, text='second', group=self.objective_group, order=1) Objective.objects.create(text='second', group=self.objective_group, order=1)
Objective.objects.create(owner=None, text='third', group=self.objective_group) Objective.objects.create(text='third', group=self.objective_group)
Objective.objects.create(owner=user, text='fourth', group=self.objective_group) Objective.objects.create(text='fourth', group=self.objective_group)
def test_objective_order(self): def test_objective_order(self):
query = """ query = """