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

View File

@ -1,6 +1,6 @@
<template>
<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>
</a>
</div>
@ -17,11 +17,15 @@
},
methods: {
addContentBlock() {
this.$store.dispatch('addContentBlock', {
after: this.after,
parent: this.parent
});
addContent() {
if (this.parent && this.parent.__typename === 'ObjectiveGroupNode') {
this.$store.dispatch('addObjective', this.parent.id);
} 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}}
</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"
:parent="chapter.id"
@ -17,7 +17,7 @@
<script>
import ContentBlock from '@/components/ContentBlock';
import AddContentBlockButton from '@/components/AddContentBlockButton';
import AddContentButton from '@/components/AddContentButton';
import {mapGetters} from 'vuex';
import {isHidden} from '@/helpers/content-block';
@ -28,7 +28,7 @@
components: {
ContentBlock,
AddContentBlockButton
AddContentButton
},
computed: {

View File

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

View File

@ -86,6 +86,7 @@
justify-content: space-between;
align-items: center;
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;
&__popover {

View File

@ -10,11 +10,8 @@
<h3 id="objectives">Lernziele</h3>
<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>
<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>

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

View File

@ -13,7 +13,7 @@
import ME_QUERY from '@/graphql/gql/meQuery.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 {
props: ['block'],
@ -35,7 +35,7 @@
},
hidden() {
// 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?
? this.block.visibleFor.findIndex(el => el.id === this.schoolClass.id) === -1
// 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({
mutation,
@ -106,6 +104,11 @@
.visibility-action {
margin-top: 9px;
position: absolute;
left: -70px;
top: 0px;
display: grid;
&__visibility-menu {
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/assignmentParts.gql"
#import "./fragments/objectiveGroupParts.gql"
#import "./fragments/objectiveParts.gql"
#import "./fragments/moduleParts.gql"
query ModulesQuery($slug: String!) {
module(slug: $slug) {
@ -19,16 +20,7 @@ query ModulesQuery($slug: String!) {
objectives {
edges {
node {
id
text
objectiveProgress {
edges {
node {
id
done
}
}
}
...ObjectiveParts
}
}
}

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

View File

@ -2,3 +2,16 @@
width: 30px;
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 "navigation";
@import "survey";
@import "visibility";

View File

@ -122,22 +122,6 @@ class ModuleNode(DjangoObjectType):
def resolve_solutions_enabled(self, info, **kwargs):
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):
pk = graphene.Int()
@ -146,7 +130,7 @@ class TopicNode(DjangoObjectType):
class Meta:
model = Topic
only_fields = [
'slug', 'title', 'meta_title', 'teaser', 'description', 'vimeo_id', 'order'
'slug', 'title', 'meta_title', 'teaser', 'description', 'vimeo_id', 'order'
]
filter_fields = {
'slug': ['exact', 'icontains', 'in'],

View File

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

View File

@ -7,6 +7,10 @@ class ObjectiveInput(InputObjectType):
id = graphene.ID()
class AddObjectiveArgument(InputObjectType):
text = graphene.String(required=True)
objective_group = graphene.ID(reuired=True)
class AddObjectiveGroupArgument(InputObjectType):
title = graphene.String(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)
def __str__(self):
return 'ObjectiveGroup {}-{}-{}'.format(self.id, self.module, self.title)
return '{} - {}'.format(self.module, self.title)
class Objective(models.Model):
@ -38,6 +38,9 @@ class Objective(models.Model):
text = models.CharField('text', blank=True, null=False, max_length=255)
group = models.ForeignKey(ObjectiveGroup, blank=False, null=False, on_delete=models.CASCADE,
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):
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.schema.inputs import UserGroupBlockVisibility
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.schema import ObjectiveNode, ObjectiveGroupNode
@ -36,28 +36,74 @@ class UpdateObjectiveProgress(relay.ClientIDMutation):
return cls(objective=objective)
class UpdateObjectiveGroupVisibility(relay.ClientIDMutation):
class UpdateObjectiveVisibility(relay.ClientIDMutation):
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)
objective_group = graphene.Field(ObjectiveGroupNode)
objective = graphene.Field(ObjectiveNode)
@classmethod
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')
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 objective_group.owner is not None:
set_visible_for(objective_group, visibility_list)
if objective.owner is not None:
set_visible_for(objective, visibility_list)
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):
@ -126,6 +172,8 @@ class UpdateObjectiveGroup(relay.ClientIDMutation):
class ObjectiveMutations:
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()
update_objective_group = UpdateObjectiveGroup.Field()

View File

@ -25,9 +25,27 @@ class ObjectiveGroupNode(DjangoObjectType):
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):
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):
pk = graphene.Int()
user_created = graphene.Boolean()
mine = graphene.Boolean()
class Meta:
model = Objective
@ -37,6 +55,11 @@ class ObjectiveNode(DjangoObjectType):
def resolve_objective_progress(self, info, **kwargs):
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):
pk = graphene.Int()