Merged in feature/objectives-by-class (pull request #32)

Feature/objectives by class
This commit is contained in:
Ramon Wenger 2019-08-26 13:12:34 +00:00 committed by Christian Cueni
commit 0bcdcf9941
28 changed files with 503 additions and 140 deletions

View File

@ -21,6 +21,7 @@
import EditObjectiveGroupWizard from '@/components/objective-groups/EditObjectiveGroupWizard'; import EditObjectiveGroupWizard from '@/components/objective-groups/EditObjectiveGroupWizard';
import NewProjectEntryWizard from '@/components/portfolio/NewProjectEntryWizard'; import NewProjectEntryWizard from '@/components/portfolio/NewProjectEntryWizard';
import EditProjectEntryWizard from '@/components/portfolio/EditProjectEntryWizard'; import EditProjectEntryWizard from '@/components/portfolio/EditProjectEntryWizard';
import NewObjectiveWizard from '@/components/objective-groups/NewObjectiveWizard';
import FullscreenImage from '@/components/FullscreenImage'; import FullscreenImage from '@/components/FullscreenImage';
import FullscreenInfographic from '@/components/FullscreenInfographic'; import FullscreenInfographic from '@/components/FullscreenInfographic';
import FullscreenVideo from '@/components/FullscreenVideo'; import FullscreenVideo from '@/components/FullscreenVideo';
@ -41,10 +42,12 @@
EditContentBlockWizard, EditContentBlockWizard,
NewRoomEntryWizard, NewRoomEntryWizard,
EditRoomEntryWizard, EditRoomEntryWizard,
// todo: remove
NewObjectiveGroupWizard, NewObjectiveGroupWizard,
EditObjectiveGroupWizard, EditObjectiveGroupWizard,
NewProjectEntryWizard, NewProjectEntryWizard,
EditProjectEntryWizard, EditProjectEntryWizard,
NewObjectiveWizard,
FullscreenImage, FullscreenImage,
FullscreenInfographic, FullscreenInfographic,
FullscreenVideo FullscreenVideo

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="add-content"> <div class="add-content">
<a class="add-content__button" v-on:click="addContentBlock"> <a class="add-content__button" v-on:click="addContent">
<add-pointer class="add-content__icon"></add-pointer> <add-pointer class="add-content__icon"></add-pointer>
</a> </a>
</div> </div>
@ -17,11 +17,15 @@
}, },
methods: { methods: {
addContentBlock() { addContent() {
this.$store.dispatch('addContentBlock', { if (this.parent && this.parent.__typename === 'ObjectiveGroupNode') {
after: this.after, this.$store.dispatch('addObjective', this.parent.id);
parent: this.parent } else {
}); this.$store.dispatch('addContentBlock', {
after: this.after ? this.after.id : undefined,
parent: this.parent ? this.parent.id : undefined
});
}
} }
} }
} }

View File

@ -6,7 +6,7 @@
{{chapter.description}} {{chapter.description}}
</p> </p>
<add-content-block-button :parent="chapter.id" v-if="editModule"></add-content-block-button> <add-content-button :parent="chapter" v-if="editModule"></add-content-button>
<content-block :contentBlock="contentBlock" <content-block :contentBlock="contentBlock"
:parent="chapter.id" :parent="chapter.id"
@ -17,7 +17,7 @@
<script> <script>
import ContentBlock from '@/components/ContentBlock'; import ContentBlock from '@/components/ContentBlock';
import AddContentBlockButton from '@/components/AddContentBlockButton'; import AddContentButton from '@/components/AddContentButton';
import {mapGetters} from 'vuex'; import {mapGetters} from 'vuex';
import {isHidden} from '@/helpers/content-block'; import {isHidden} from '@/helpers/content-block';
@ -28,7 +28,7 @@
components: { components: {
ContentBlock, ContentBlock,
AddContentBlockButton AddContentButton
}, },
computed: { computed: {

View File

@ -1,23 +1,17 @@
<template> <template>
<div class="content-block__container" :class="{'content-block__container--hidden': hidden}"> <div class="content-block__container hideable-element" :class="{'hideable-element--hidden': hidden}">
<div class="content-block" :class="specialClass"> <div class="content-block" :class="specialClass">
<div class="content-block__actions" v-if="canEditContentBlock && editModule"> <div class="block-actions" v-if="canEditContentBlock && editModule">
<user-widget v-bind="me" class="content-block__user-widget"></user-widget> <user-widget v-bind="me" class="block-actions__user-widget content-block__user-widget"></user-widget>
<more-options-widget> <more-options-widget>
<li class="popover-links__link"><a @click="deleteContentBlock(contentBlock)">Löschen</a></li> <li class="popover-links__link"><a @click="deleteContentBlock(contentBlock)">Löschen</a></li>
<li class="popover-links__link"><a @click="editContentBlock(contentBlock)">Bearbeiten</a></li> <li class="popover-links__link"><a @click="editContentBlock(contentBlock)">Bearbeiten</a></li>
</more-options-widget> </more-options-widget>
</div> </div>
<div class="content-block__visibility" v-if="editModule"> <div class="content-block__visibility">
<visibility-action <visibility-action
v-if="!contentBlock.indent" v-if="canEditModule"
:block="contentBlock"></visibility-action> :block="contentBlock"></visibility-action>
<!--<a @click="editContentBlock()" v-if="canEditContentBlock" class="content-block__action-button">-->
<!--<pen-icon class="content-block__action-icon action-icon"></pen-icon>-->
<!--</a>-->
<!--<a @click="deleteContentBlock(contentBlock.id)" v-if="canEditContentBlock" class="content-block__action-button">-->
<!--<trash-icon class="content-block__action-icon action-icon"></trash-icon>-->
<!--</a>-->
</div> </div>
<h3 v-if="instrumentLabel !== ''" class="content-block__instrument-label">{{instrumentLabel}}</h3> <h3 v-if="instrumentLabel !== ''" class="content-block__instrument-label">{{instrumentLabel}}</h3>
@ -31,8 +25,7 @@
</div> </div>
<add-content-block-button :after="contentBlock.id" <add-content-button :after="contentBlock" v-if="canEditModule"></add-content-button>
v-if="!contentBlock.indent && editModule"></add-content-block-button>
</div> </div>
@ -54,7 +47,7 @@
import Assignment from '@/components/content-blocks/assignment/Assignment'; import Assignment from '@/components/content-blocks/assignment/Assignment';
import Survey from '@/components/content-blocks/SurveyBlock'; import Survey from '@/components/content-blocks/SurveyBlock';
import Solution from '@/components/content-blocks/Solution'; import Solution from '@/components/content-blocks/Solution';
import AddContentBlockButton from '@/components/AddContentBlockButton'; import AddContentButton from '@/components/AddContentButton';
import MoreOptionsWidget from '@/components/MoreOptionsWidget'; import MoreOptionsWidget from '@/components/MoreOptionsWidget';
import UserWidget from '@/components/UserWidget'; import UserWidget from '@/components/UserWidget';
import VisibilityAction from '@/components/visibility/VisibilityAction'; import VisibilityAction from '@/components/visibility/VisibilityAction';
@ -99,7 +92,7 @@
Solution, Solution,
Assignment, Assignment,
Task, Task,
AddContentBlockButton, AddContentButton,
VisibilityAction, VisibilityAction,
EyeIcon, EyeIcon,
PenIcon, PenIcon,
@ -110,16 +103,19 @@
computed: { computed: {
...mapGetters(['editModule']), ...mapGetters(['editModule']),
canEditModule() {
return !this.contentBlock.indent && this.editModule;
},
specialClass() { specialClass() {
return `content-block--${this.contentBlock.type.toLowerCase()}` return `content-block--${this.contentBlock.type.toLowerCase()}`;
}, },
instrumentLabel() { instrumentLabel() {
const contentType = this.contentBlock.type.toLowerCase() const contentType = this.contentBlock.type.toLowerCase();
if (!(contentType in instruments)) { if (!(contentType in instruments)) {
return '' return '';
} }
return `Instrument - ${instruments[contentType]}` return `Instrument - ${instruments[contentType]}`;
}, },
canEditContentBlock() { canEditContentBlock() {
return this.contentBlock.mine && !this.contentBlock.indent; return this.contentBlock.mine && !this.contentBlock.indent;
@ -165,7 +161,7 @@
contentList = []; contentList = [];
return newContents; return newContents;
} else { } else {
return [...newContents, content] return [...newContents, content];
} }
} }
}, []); }, []);
@ -249,19 +245,6 @@
&__container { &__container {
position: relative; position: relative;
&--hidden {
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: rgba(255, 255, 255, 0.5);
z-index: 10;
}
}
} }
&__title { &__title {
@ -273,29 +256,12 @@
@include regular-text(); @include regular-text();
} }
&__visibility { &__action-button {
position: absolute; cursor: pointer;
left: -70px;
top: -4px;
display: grid;
}
&__actions {
position: absolute;
top: 10px;
right: -85px;
display: flex;
flex-direction: column;
align-items: center;
} }
&__user-widget { &__user-widget {
margin-right: 0; margin-right: 0;
margin-bottom: $small-spacing;
}
&__action-button {
cursor: pointer;
} }
&--base_communication { &--base_communication {

View File

@ -86,6 +86,7 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
position: relative; position: relative;
// todo: do we need the margin right always? just do it where needed --> content block actions and objecives override this
margin-right: $medium-spacing; margin-right: $medium-spacing;
&__popover { &__popover {

View File

@ -10,11 +10,8 @@
<h3 id="objectives">Lernziele</h3> <h3 id="objectives">Lernziele</h3>
<objective-groups :groups="languageCommunicationObjectiveGroups"></objective-groups> <objective-groups :groups="languageCommunicationObjectiveGroups"></objective-groups>
<add-objective-group-button v-if="!isStudent" type="languageCommunication"
:module="module.id"></add-objective-group-button>
<objective-groups :groups="societyObjectiveGroups"></objective-groups> <objective-groups :groups="societyObjectiveGroups"></objective-groups>
<add-objective-group-button v-if="!isStudent" type="society" :module="module.id"></add-objective-group-button>
<chapter :chapter="chapter" :index="index" v-for="(chapter, index) in module.chapters" :key="chapter.id"></chapter> <chapter :chapter="chapter" :index="index" v-for="(chapter, index) in module.chapters" :key="chapter.id"></chapter>

View File

@ -0,0 +1,87 @@
<template>
<modal :hide-header="true">
<modal-input
:placeholder="'Lernziel'"
:value="text"
@input="text = $event"
></modal-input>
<div slot="footer">
<a class="button button--primary" data-cy="modal-save-button" :class="{'button--disabled': disableSave}"
v-on:click="save(text)">Speichern</a>
<a class="button" v-on:click="hide()">Abbrechen</a>
</div>
</modal>
</template>
<script>
import Modal from '@/components/Modal';
import ModalInput from '@/components/ModalInput';
import NEW_OBJECTIVE_MUTATION from '@/graphql/gql/mutations/addObjective.gql';
import OBJECTIVE_GROUP_QUERY from '@/graphql/gql/objectiveGroupQuery.gql';
import {mapGetters} from 'vuex';
export default {
components: {
Modal,
ModalInput
},
computed: {
...mapGetters({
objectiveGroup: 'currentObjectiveGroup'
}),
disableSave() {
return this.saving;
}
},
data() {
return {
text: '',
saving: false
}
},
methods: {
save(entry) {
this.saving = true;
this.$apollo.mutate({
mutation: NEW_OBJECTIVE_MUTATION,
variables: {
input: {
objective: Object.assign({}, {
objectiveGroup: this.objectiveGroup,
text: entry
})
}
},
update: (store, {data: {addObjective: {objective}}}) => {
try {
const query = OBJECTIVE_GROUP_QUERY;
const variables = {id: this.objectiveGroup};
const data = store.readQuery({query, variables});
if (data.objectiveGroup && data.objectiveGroup.objectives) {
data.objectiveGroup.objectives.edges.push({
node: objective,
__typename: 'ObjectiveNode'
});
store.writeQuery({query, variables, data});
}
} catch (e) {
// Query did not exist in the cache, and apollo throws a generic Error. Do nothing
}
}
}).then(() => {
this.saving = false;
this.hide();
});
},
hide() {
this.$store.dispatch('hideModal');
}
}
}
</script>

View File

@ -0,0 +1,100 @@
<template>
<li class="objective hideable-element" :class="{'hideable-element--hidden': hidden}" v-if="editModule || !hidden">
<visibility-action
v-if="editModule"
:block="objective"></visibility-action>
<div class="block-actions" v-if="editModule && canEdit">
<user-widget class="block-actions__user-widget objective__user-widget" v-bind="me"></user-widget>
<more-options-widget>
<div class="popover-links__link"><a @click="deleteObjective(objective)">Löschen</a></div>
</more-options-widget>
</div>
<div>
{{objective.text}}
</div>
</li>
</template>
<script>
import VisibilityAction from '@/components/visibility/VisibilityAction';
import UserWidget from '@/components/UserWidget';
import MoreOptionsWidget from '@/components/MoreOptionsWidget';
import {mapGetters} from 'vuex';
import {isHidden} from '@/helpers/content-block';
import {meQuery} from '@/graphql/queries';
import DELETE_OBJECTIVE_MUTATION from '@/graphql/gql/mutations/deleteObjective.gql';
import MODULE_DETAILS_QUERY from '@/graphql/gql/moduleDetailsQuery.gql';
export default {
props: ['objective', 'schoolClass'],
components: {
MoreOptionsWidget,
VisibilityAction,
UserWidget
},
computed: {
...mapGetters(['editModule']),
hidden() {
return isHidden(this.objective, this.schoolClass)
},
canEdit() {
return this.objective.mine;
}
},
methods: {
deleteObjective(objective) {
this.$apollo.mutate({
mutation: DELETE_OBJECTIVE_MUTATION,
variables: {
input: {
id: objective.id
}
},
refetchQueries: [{
query: MODULE_DETAILS_QUERY,
variables: {
slug: this.$route.params.slug
}
}]
// todo: make update work here also
// update(store, {data: {deleteObjective: {success}}}) {
// if (success) {
// const query = MODULE_DETAILS_QUERY;
// const variables = {slug: this.$route.params.slug};
// const data = store.readQuery({query, variables});
// if (data) {
// data.module.objectiveGroups.edges.
// data.rooms.edges.splice(data.rooms.edges.findIndex(edge => edge.node.id === theId), 1);
// store.writeQuery({query, variables, data});
// }
// }
// }
});
}
},
apollo: {
me: meQuery
},
data() {
return {
me: {}
}
}
}
</script>
<style scoped lang="scss">
.objective {
min-height: 50px;
&__user-widget {
margin-right: 0;
}
}
</style>

View File

@ -1,31 +1,29 @@
<template> <template>
<div class="objective-group"> <div class="objective-group">
<div class="objective-group__actions">
<!--visibility-action :block="group">
</visibility-action-->
<a @click="editObjectiveGroup()" v-if="group.mine" class="objective-group__action-button">
<pen-icon class="objective-group__action-icon action-icon"></pen-icon>
</a>
</div>
<h4>{{group.displayTitle}}</h4> <h4>{{group.displayTitle}}</h4>
<ul class="objective-group__objective-list"> <ul class="objective-group__objective-list">
<li class="objective-group__objective" v-for="objective in group.objectives" :key="objective.id"> <objective class="objective-group__objective" v-for="objective in group.objectives" :key="objective.id"
{{objective.text}} :objective="objective" :school-class="currentFilter">
</li> </objective>
</ul> </ul>
<add-content-button :parent="group" v-if="editModule">
</add-content-button>
</div> </div>
</template> </template>
<script> <script>
import VisibilityAction from '@/components/visibility/VisibilityAction'; import VisibilityAction from '@/components/visibility/VisibilityAction';
import Objective from '@/components/objective-groups/Objective';
import EyeIcon from '@/components/icons/EyeIcon'; import EyeIcon from '@/components/icons/EyeIcon';
import PenIcon from '@/components/icons/PenIcon'; import PenIcon from '@/components/icons/PenIcon';
import ME_QUERY from '@/graphql/gql/meQuery.gql'; import ME_QUERY from '@/graphql/gql/meQuery.gql';
import AddContentButton from '@/components/AddContentButton';
import {mapGetters} from 'vuex';
export default { export default {
props: { props: {
@ -36,7 +34,9 @@
}, },
components: { components: {
AddContentButton,
VisibilityAction, VisibilityAction,
Objective,
EyeIcon, EyeIcon,
PenIcon PenIcon
}, },
@ -48,9 +48,13 @@
}, },
computed: { computed: {
...mapGetters(['editModule']),
canManageContent() { canManageContent() {
return this.me.permissions.includes('users.can_manage_school_class_content'); return this.me.permissions.includes('users.can_manage_school_class_content');
} },
currentFilter() {
return this.me.selectedClass;
},
}, },
methods: { methods: {

View File

@ -13,7 +13,7 @@
import ME_QUERY from '@/graphql/gql/meQuery.gql'; import ME_QUERY from '@/graphql/gql/meQuery.gql';
import CHANGE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/mutateContentBlock.gql'; import CHANGE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/mutateContentBlock.gql';
// import UPDATE_OBJECTIVE_GROUP_VISIBILITY_MUTATION from '@/graphql/gql/mutations/updateObjectiveGroupVisibility.gql'; import UPDATE_OBJECTIVE_VISIBILITY_MUTATION from '@/graphql/gql/mutations/updateObjectiveVisibility.gql';
export default { export default {
props: ['block'], props: ['block'],
@ -35,7 +35,7 @@
}, },
hidden() { hidden() {
// is this content block / objective group user created? // is this content block / objective group user created?
return (this.isContentBlock ? this.block.userCreated : !!this.block.owner) return this.block.userCreated
// if so, is visibility not explicitly set for this school class? // if so, is visibility not explicitly set for this school class?
? this.block.visibleFor.findIndex(el => el.id === this.schoolClass.id) === -1 ? this.block.visibleFor.findIndex(el => el.id === this.schoolClass.id) === -1
// otherwise, is it explicitly hidden for this school class? // otherwise, is it explicitly hidden for this school class?
@ -66,17 +66,15 @@
} }
} }
} }
} else {
mutation = UPDATE_OBJECTIVE_VISIBILITY_MUTATION;
variables = {
input: {
id,
visibility
}
}
} }
// todo: refactor for single objectives when concept is clear
// else {
// mutation = UPDATE_OBJECTIVE_GROUP_VISIBILITY_MUTATION;
// variables = {
// input: {
// id,
// visibility
// }
// }
// }
this.$apollo.mutate({ this.$apollo.mutate({
mutation, mutation,
@ -106,6 +104,11 @@
.visibility-action { .visibility-action {
margin-top: 9px; margin-top: 9px;
position: absolute;
left: -70px;
top: 0px;
display: grid;
&__visibility-menu { &__visibility-menu {
top: 40px; top: 40px;
} }

View File

@ -0,0 +1,30 @@
fragment ObjectiveParts on ObjectiveNode {
id
text
mine
userCreated
hiddenFor {
edges {
node {
id
name
}
}
}
visibleFor {
edges {
node {
id
name
}
}
}
objectiveProgress {
edges {
node {
id
done
}
}
}
}

View File

@ -1,6 +1,7 @@
#import "./fragments/chapterParts.gql" #import "./fragments/chapterParts.gql"
#import "./fragments/assignmentParts.gql" #import "./fragments/assignmentParts.gql"
#import "./fragments/objectiveGroupParts.gql" #import "./fragments/objectiveGroupParts.gql"
#import "./fragments/objectiveParts.gql"
#import "./fragments/moduleParts.gql" #import "./fragments/moduleParts.gql"
query ModulesQuery($slug: String!) { query ModulesQuery($slug: String!) {
module(slug: $slug) { module(slug: $slug) {
@ -19,16 +20,7 @@ query ModulesQuery($slug: String!) {
objectives { objectives {
edges { edges {
node { node {
id ...ObjectiveParts
text
objectiveProgress {
edges {
node {
id
done
}
}
}
} }
} }
} }

View File

@ -0,0 +1,16 @@
#import "../fragments/objectiveParts.gql"
mutation AddObjective($input: AddObjectiveInput!){
addObjective(input: $input) {
objective {
...ObjectiveParts
}
}
}
#{"input": {
# "objective": {
# "objectiveGroup": "asdas",
# "text": "Lern etwas"
# }}
#}

View File

@ -0,0 +1,11 @@
mutation DeleteObjective($input: DeleteObjectiveInput!) {
deleteObjective(input: $input) {
success
}
}
#{
# "input": {
# "id": "Um9vbU5vZGU6MjY="
# }
#}

View File

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

View File

@ -0,0 +1,9 @@
#import "../fragments/objectiveParts.gql"
mutation UpdateObjectiveVisibility($input: UpdateObjectiveVisibilityInput!) {
updateObjectiveVisibility(input: $input) {
objective {
...ObjectiveParts
}
}
}

View File

@ -7,6 +7,22 @@ query ObjectiveGroupQuery($id: ID!) {
node { node {
id id
text text
hiddenFor {
edges {
node {
id
name
}
}
}
visibleFor {
edges {
node {
id
name
}
}
}
} }
} }
} }

View File

@ -43,7 +43,8 @@ export default new Vuex.Store({
scrollToAssignmentReady: state => state.scrollToAssignmentReady, scrollToAssignmentReady: state => state.scrollToAssignmentReady,
scrollingToAssignment: state => state.scrollingToAssignment, scrollingToAssignment: state => state.scrollingToAssignment,
currentProjectEntry: state => state.currentProjectEntry, currentProjectEntry: state => state.currentProjectEntry,
editModule: state => state.editModule editModule: state => state.editModule,
currentObjectiveGroup: state => state.currentObjectiveGroup
}, },
actions: { actions: {
@ -61,6 +62,7 @@ export default new Vuex.Store({
commit('setContentBlockPosition', {}); commit('setContentBlockPosition', {});
commit('setParentRoom', null); commit('setParentRoom', null);
commit('setParentModule', ''); commit('setParentModule', '');
// todo: remove
commit('setObjectiveGroupType', ''); commit('setObjectiveGroupType', '');
commit('setCurrentObjectiveGroup', ''); commit('setCurrentObjectiveGroup', '');
commit('setParentProject', null); commit('setParentProject', null);
@ -86,6 +88,10 @@ export default new Vuex.Store({
commit('setContentBlockPosition', payload); commit('setContentBlockPosition', payload);
dispatch('showModal', 'new-content-block-wizard'); dispatch('showModal', 'new-content-block-wizard');
}, },
addObjective({commit, dispatch}, payload) {
commit('setCurrentObjectiveGroup', payload);
dispatch('showModal', 'new-objective-wizard');
},
addRoomEntry({commit, dispatch}, payload) { addRoomEntry({commit, dispatch}, payload) {
commit('setParentRoom', payload); commit('setParentRoom', payload);
dispatch('showModal', 'new-room-entry-wizard'); dispatch('showModal', 'new-room-entry-wizard');
@ -94,6 +100,7 @@ export default new Vuex.Store({
commit('setCurrentRoomEntry', payload); commit('setCurrentRoomEntry', payload);
dispatch('showModal', 'edit-room-entry-wizard'); dispatch('showModal', 'edit-room-entry-wizard');
}, },
// todo: remove
addObjectiveGroup({commit, dispatch}, {module, type}) { addObjectiveGroup({commit, dispatch}, {module, type}) {
commit('setParentModule', module); commit('setParentModule', module);
commit('setObjectiveGroupType', type); commit('setObjectiveGroupType', type);
@ -179,6 +186,7 @@ export default new Vuex.Store({
setParentModule(state, payload) { setParentModule(state, payload) {
state.parentModule = payload; state.parentModule = payload;
}, },
// todo: remove
setObjectiveGroupType(state, payload) { setObjectiveGroupType(state, payload) {
state.objectiveGroupType = payload; state.objectiveGroupType = payload;
}, },

View File

@ -2,3 +2,16 @@
width: 30px; width: 30px;
fill: $color-silver-dark; fill: $color-silver-dark;
} }
.block-actions {
position: absolute;
top: 10px;
right: -85px;
display: flex;
flex-direction: column;
align-items: center;
&__user-widget {
margin-bottom: $small-spacing;
}
}

View File

@ -0,0 +1,17 @@
.hideable-element {
position: relative;
&--hidden {
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: rgba(255, 255, 255, 0.5);
z-index: 10;
}
}
}

View File

@ -16,3 +16,4 @@
@import "actions"; @import "actions";
@import "navigation"; @import "navigation";
@import "survey"; @import "survey";
@import "visibility";

View File

@ -122,22 +122,6 @@ class ModuleNode(DjangoObjectType):
def resolve_solutions_enabled(self, info, **kwargs): def resolve_solutions_enabled(self, info, **kwargs):
return self.solutions_enabled_by.filter(pk=info.context.user.pk).exists() return self.solutions_enabled_by.filter(pk=info.context.user.pk).exists()
def resolve_objective_groups(self, info, **kwargs):
user = info.context.user
school_classes = user.school_classes.values_list('pk')
if user.has_perm('users.can_manage_school_class_content'): # teacher
publisher_objective_groups = self.objective_groups.filter(owner=None)
user_created_objective_groups = self.objective_groups.filter(owner=user)
else: # student
publisher_objective_groups = self.objective_groups.filter(owner=None).exclude(
hidden_for__in=school_classes)
user_created_objective_groups = self.objective_groups.filter(owner__isnull=False,
visible_for__in=school_classes)
return publisher_objective_groups.union(user_created_objective_groups)
class TopicNode(DjangoObjectType): class TopicNode(DjangoObjectType):
pk = graphene.Int() pk = graphene.Int()
@ -146,7 +130,7 @@ class TopicNode(DjangoObjectType):
class Meta: class Meta:
model = Topic model = Topic
only_fields = [ only_fields = [
'slug', 'title', 'meta_title', 'teaser', 'description', 'vimeo_id', 'order' 'slug', 'title', 'meta_title', 'teaser', 'description', 'vimeo_id', 'order'
] ]
filter_fields = { filter_fields = {
'slug': ['exact', 'icontains', 'in'], 'slug': ['exact', 'icontains', 'in'],

View File

@ -11,8 +11,8 @@ class ObjectiveGroupAdmin(admin.ModelAdmin):
@admin.register(Objective) @admin.register(Objective)
class ObjectiveAdmin(admin.ModelAdmin): class ObjectiveAdmin(admin.ModelAdmin):
list_display = ('text', 'group') list_display = ('text', 'group', 'owner')
list_filter = ('group',) list_filter = ('group', 'owner')
@admin.register(ObjectiveProgressStatus) @admin.register(ObjectiveProgressStatus)

View File

@ -7,6 +7,10 @@ class ObjectiveInput(InputObjectType):
id = graphene.ID() id = graphene.ID()
class AddObjectiveArgument(InputObjectType):
text = graphene.String(required=True)
objective_group = graphene.ID(reuired=True)
class AddObjectiveGroupArgument(InputObjectType): class AddObjectiveGroupArgument(InputObjectType):
title = graphene.String(required=True) title = graphene.String(required=True)
module = graphene.ID(required=True) module = graphene.ID(required=True)

View File

@ -0,0 +1,32 @@
# Generated by Django 2.0.6 on 2019-08-21 12:52
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('users', '0007_usersetting'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('objectives', '0007_auto_20181031_1347'),
]
operations = [
migrations.AddField(
model_name='objective',
name='hidden_for',
field=models.ManyToManyField(blank=True, related_name='hidden_objectives', to='users.SchoolClass'),
),
migrations.AddField(
model_name='objective',
name='owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='objective',
name='visible_for',
field=models.ManyToManyField(blank=True, related_name='visible_objectives', to='users.SchoolClass'),
),
]

View File

@ -27,7 +27,7 @@ class ObjectiveGroup(models.Model):
visible_for = models.ManyToManyField(SchoolClass, related_name='visible_objective_groups', blank=True) visible_for = models.ManyToManyField(SchoolClass, related_name='visible_objective_groups', blank=True)
def __str__(self): def __str__(self):
return 'ObjectiveGroup {}-{}-{}'.format(self.id, self.module, self.title) return '{} - {}'.format(self.module, self.title)
class Objective(models.Model): class Objective(models.Model):
@ -38,6 +38,9 @@ class Objective(models.Model):
text = models.CharField('text', blank=True, null=False, max_length=255) text = models.CharField('text', blank=True, null=False, max_length=255)
group = models.ForeignKey(ObjectiveGroup, blank=False, null=False, on_delete=models.CASCADE, group = models.ForeignKey(ObjectiveGroup, blank=False, null=False, on_delete=models.CASCADE,
related_name='objectives') related_name='objectives')
owner = models.ForeignKey(get_user_model(), blank=True, null=True, on_delete=models.CASCADE)
hidden_for = models.ManyToManyField(SchoolClass, related_name='hidden_objectives', blank=True)
visible_for = models.ManyToManyField(SchoolClass, related_name='visible_objectives', blank=True)
def __str__(self): def __str__(self):
return 'Objective {}-{}'.format(self.id, self.text) return 'Objective {}-{}'.format(self.id, self.text)

View File

@ -7,7 +7,7 @@ from api.utils import get_object
from books.models import Module from books.models import Module
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 AddObjectiveGroupArgument, UpdateObjectiveGroupArgument from objectives.inputs import AddObjectiveGroupArgument, UpdateObjectiveGroupArgument, AddObjectiveArgument
from objectives.models import ObjectiveProgressStatus, Objective, ObjectiveGroup from objectives.models import ObjectiveProgressStatus, Objective, ObjectiveGroup
from objectives.schema import ObjectiveNode, ObjectiveGroupNode from objectives.schema import ObjectiveNode, ObjectiveGroupNode
@ -36,28 +36,74 @@ class UpdateObjectiveProgress(relay.ClientIDMutation):
return cls(objective=objective) return cls(objective=objective)
class UpdateObjectiveGroupVisibility(relay.ClientIDMutation): class UpdateObjectiveVisibility(relay.ClientIDMutation):
class Input: class Input:
id = graphene.ID(required=True, description='The ID of the objective group') id = graphene.ID(required=True, description='The ID of the objective')
visibility = graphene.List(UserGroupBlockVisibility) visibility = graphene.List(UserGroupBlockVisibility)
objective_group = graphene.Field(ObjectiveGroupNode) objective = graphene.Field(ObjectiveNode)
@classmethod @classmethod
def mutate_and_get_payload(cls, root, info, **kwargs): def mutate_and_get_payload(cls, root, info, **kwargs):
objective_group_id = kwargs.get('id') objective_id = kwargs.get('id')
visibility_list = kwargs.get('visibility') visibility_list = kwargs.get('visibility')
objective_group = get_object(ObjectiveGroup, objective_group_id) # info.context.user = django user objective = get_object(Objective, objective_id) # info.context.user = django user
if visibility_list is not None: if visibility_list is not None:
if objective_group.owner is not None: if objective.owner is not None:
set_visible_for(objective_group, visibility_list) set_visible_for(objective, visibility_list)
else: else:
set_hidden_for(objective_group, visibility_list) set_hidden_for(objective, visibility_list)
objective_group.save() objective.save()
return cls(objective=objective)
class AddObjective(relay.ClientIDMutation):
class Input:
objective = graphene.Argument(AddObjectiveArgument)
objective = graphene.Field(ObjectiveNode)
@classmethod
def mutate_and_get_payload(cls, root, info, **kwargs):
owner = info.context.user
if not owner.has_perm('users.can_manage_school_class_content'):
raise PermissionDenied('Missing permissions')
objective_data = kwargs.get('objective')
objective_group_id = objective_data.get('objective_group')
text = objective_data.get('text')
objective_group = get_object(ObjectiveGroup, objective_group_id)
objective = Objective.objects.create(text=text, owner=owner, group=objective_group)
return cls(objective=objective)
class DeleteObjective(relay.ClientIDMutation):
class Input:
id = graphene.ID(required=True)
success = graphene.Boolean()
@classmethod
def mutate_and_get_payload(cls, root, info, **kwargs):
id = kwargs.get('id')
objective = get_object(Objective, id)
user = info.context.user
if not user.has_perm('users.can_manage_school_class_content'):
raise PermissionDenied('Missing permissions')
if objective.owner.pk != user.pk:
raise PermissionDenied('Permission denied: Not owner')
objective.delete()
return cls(success=True)
return cls(objective_group=objective_group)
class AddObjectiveGroup(relay.ClientIDMutation): class AddObjectiveGroup(relay.ClientIDMutation):
@ -126,6 +172,8 @@ class UpdateObjectiveGroup(relay.ClientIDMutation):
class ObjectiveMutations: class ObjectiveMutations:
update_objective_progress = UpdateObjectiveProgress.Field() update_objective_progress = UpdateObjectiveProgress.Field()
update_objective_group_visibility = UpdateObjectiveGroupVisibility.Field() update_objective_visibility = UpdateObjectiveVisibility.Field()
add_objective = AddObjective.Field()
delete_objective = DeleteObjective.Field()
add_objective_group = AddObjectiveGroup.Field() add_objective_group = AddObjectiveGroup.Field()
update_objective_group = UpdateObjectiveGroup.Field() update_objective_group = UpdateObjectiveGroup.Field()

View File

@ -25,9 +25,27 @@ class ObjectiveGroupNode(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
def resolve_objectives(self, info, **kwargs):
user = info.context.user
school_classes = user.school_classes.values_list('pk')
if user.has_perm('users.can_manage_school_class_content'): # teacher
publisher_objectives = self.objectives.filter(owner=None)
user_created_objectives = self.objectives.filter(owner=user)
else: # student
publisher_objectives = self.objectives.filter(owner=None).exclude(
hidden_for__in=school_classes)
user_created_objectives = self.objectives.filter(owner__isnull=False,
visible_for__in=school_classes)
return publisher_objectives.union(user_created_objectives)
class ObjectiveNode(DjangoObjectType): class ObjectiveNode(DjangoObjectType):
pk = graphene.Int() pk = graphene.Int()
user_created = graphene.Boolean()
mine = graphene.Boolean()
class Meta: class Meta:
model = Objective model = Objective
@ -37,6 +55,11 @@ class ObjectiveNode(DjangoObjectType):
def resolve_objective_progress(self, info, **kwargs): def resolve_objective_progress(self, info, **kwargs):
return self.objective_progress.filter(user=info.context.user) return self.objective_progress.filter(user=info.context.user)
def resolve_user_created(self, info, **kwargs):
return self.owner is not None
def resolve_mine(self, info, **kwargs):
return self.owner is not None and self.owner.pk == info.context.user.pk
class ObjectiveProgressStatusNode(DjangoObjectType): class ObjectiveProgressStatusNode(DjangoObjectType):
pk = graphene.Int() pk = graphene.Int()