Enable teachers to add new objectives instead of objective groups

This commit is contained in:
Ramon Wenger 2019-08-22 19:01:26 +02:00
parent 76b3f70a87
commit 6d5fa1806d
18 changed files with 224 additions and 60 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,5 +1,5 @@
<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="content-block__actions" v-if="canEditContentBlock && editModule">
<user-widget v-bind="me" class="content-block__user-widget"></user-widget> <user-widget v-bind="me" class="content-block__user-widget"></user-widget>
@ -8,16 +8,10 @@
<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="!contentBlock.indent && editModule"
: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,8 @@
</div> </div>
<add-content-block-button :after="contentBlock.id" <add-content-button :after="contentBlock"
v-if="!contentBlock.indent && editModule"></add-content-block-button> v-if="!contentBlock.indent && editModule"></add-content-button>
</div> </div>
@ -54,7 +48,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 +93,7 @@
Solution, Solution,
Assignment, Assignment,
Task, Task,
AddContentBlockButton, AddContentButton,
VisibilityAction, VisibilityAction,
EyeIcon, EyeIcon,
PenIcon, PenIcon,
@ -249,19 +243,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,13 +254,6 @@
@include regular-text(); @include regular-text();
} }
&__visibility {
position: absolute;
left: -70px;
top: -4px;
display: grid;
}
&__actions { &__actions {
position: absolute; position: absolute;
top: 10px; top: 10px;

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

@ -1,21 +1,34 @@
<template> <template>
<li class="objective"> <li class="objective hideable-element" :class="{'hideable-element--hidden': hidden}">
<div class="objective__actions">
<visibility-action <visibility-action
:block="objective"></visibility-action> v-if="editModule"
:block="objective"></visibility-action>
<div>
{{objective.text}}
</div> </div>
{{objective.text}}
</li> </li>
</template> </template>
<script> <script>
import VisibilityAction from '@/components/visibility/VisibilityAction'; import VisibilityAction from '@/components/visibility/VisibilityAction';
import {mapGetters} from 'vuex';
import {isHidden} from '@/helpers/content-block';
export default { export default {
props: ['objective'], props: ['objective', 'schoolClass'],
components: { components: {
VisibilityAction VisibilityAction
},
computed: {
...mapGetters(['editModule']),
hidden() {
return isHidden(this.objective, this.schoolClass)
}
} }
} }
</script> </script>

View File

@ -5,9 +5,11 @@
<ul class="objective-group__objective-list"> <ul class="objective-group__objective-list">
<objective 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="objective"> :objective="objective" :school-class="currentFilter">
</objective> </objective>
</ul> </ul>
<add-content-button :parent="group">
</add-content-button>
</div> </div>
</template> </template>
@ -19,6 +21,7 @@
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';
export default { export default {
props: { props: {
@ -29,6 +32,7 @@
}, },
components: { components: {
AddContentButton,
VisibilityAction, VisibilityAction,
Objective, Objective,
EyeIcon, EyeIcon,
@ -44,7 +48,10 @@
computed: { computed: {
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

@ -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,9 +66,7 @@
} }
} }
} }
} } else {
// todo: refactor for single objectives when concept is clear
else {
mutation = UPDATE_OBJECTIVE_VISIBILITY_MUTATION; mutation = UPDATE_OBJECTIVE_VISIBILITY_MUTATION;
variables = { variables = {
input: { input: {
@ -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

@ -1,6 +1,7 @@
fragment ObjectiveParts on ObjectiveNode { fragment ObjectiveParts on ObjectiveNode {
id id
text text
userCreated
hiddenFor { hiddenFor {
edges { edges {
node { node {

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

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

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

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

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

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
@ -60,6 +60,30 @@ class UpdateObjectiveVisibility(relay.ClientIDMutation):
return cls(objective=objective) 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 AddObjectiveGroup(relay.ClientIDMutation): class AddObjectiveGroup(relay.ClientIDMutation):
class Input: class Input:
objective_group = graphene.Argument(AddObjectiveGroupArgument) objective_group = graphene.Argument(AddObjectiveGroupArgument)
@ -127,5 +151,6 @@ class UpdateObjectiveGroup(relay.ClientIDMutation):
class ObjectiveMutations: class ObjectiveMutations:
update_objective_progress = UpdateObjectiveProgress.Field() update_objective_progress = UpdateObjectiveProgress.Field()
update_objective_visibility = UpdateObjectiveVisibility.Field() update_objective_visibility = UpdateObjectiveVisibility.Field()
add_objective = AddObjective.Field()
add_objective_group = AddObjectiveGroup.Field() add_objective_group = AddObjectiveGroup.Field()
update_objective_group = UpdateObjectiveGroup.Field() update_objective_group = UpdateObjectiveGroup.Field()

View File

@ -44,6 +44,7 @@ class ObjectiveGroupNode(DjangoObjectType):
class ObjectiveNode(DjangoObjectType): class ObjectiveNode(DjangoObjectType):
pk = graphene.Int() pk = graphene.Int()
user_created = graphene.Boolean()
class Meta: class Meta:
model = Objective model = Objective
@ -53,6 +54,9 @@ 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
class ObjectiveProgressStatusNode(DjangoObjectType): class ObjectiveProgressStatusNode(DjangoObjectType):
pk = graphene.Int() pk = graphene.Int()