Merged in feature/objectives-by-user (pull request #10)

Feature/objectives by user
This commit is contained in:
Ramon Wenger 2018-11-01 13:28:15 +00:00
commit 58d42e2472
27 changed files with 489 additions and 121 deletions

View File

@ -2,20 +2,13 @@
<div class="content-block__container"> <div class="content-block__container">
<div class="content-block" :class="specialClass"> <div class="content-block" :class="specialClass">
<div class="content-block__actions"> <div class="content-block__actions">
<a @click="toggleVisibility()" v-if="canManageContent" class="content-block__action-button"> <visibility-action
<eye-icon class="content-block__action-icon"></eye-icon> :block="contentBlock"></visibility-action>
</a>
<visibility-popover
@hide-me="showVisibility = false"
:show="showVisibility"
:content-block="contentBlock"
class="content-block__visibility-menu"
></visibility-popover>
<a @click="editContentBlock()" v-if="contentBlock.mine" class="content-block__action-button"> <a @click="editContentBlock()" v-if="contentBlock.mine" class="content-block__action-button">
<pen-icon class="content-block__action-icon"></pen-icon> <pen-icon class="content-block__action-icon action-icon"></pen-icon>
</a> </a>
<a @click="deleteContentBlock(contentBlock.id)" v-if="contentBlock.mine" class="content-block__action-button"> <a @click="deleteContentBlock(contentBlock.id)" v-if="contentBlock.mine" class="content-block__action-button">
<trash-icon class="content-block__action-icon"></trash-icon> <trash-icon class="content-block__action-icon action-icon"></trash-icon>
</a> </a>
</div> </div>
@ -46,11 +39,11 @@
import DocumentBlock from '@/components/content-blocks/DocumentBlock'; import DocumentBlock from '@/components/content-blocks/DocumentBlock';
import Assignment from '@/components/content-blocks/assignment/Assignment'; import Assignment from '@/components/content-blocks/assignment/Assignment';
import AddContentBlockButton from '@/components/AddContentBlockButton'; import AddContentBlockButton from '@/components/AddContentBlockButton';
import VisibilityPopover from '@/components/VisibilityPopover'; import VisibilityAction from '@/components/visibility/VisibilityAction';
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 TrashIcon from '@/components/icons/TrashIcon'; import TrashIcon from '@/components/icons/TrashIcon';
import ME_QUERY from '@/graphql/gql/meQuery.gql';
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';
@ -68,7 +61,7 @@
Assignment, Assignment,
Task, Task,
AddContentBlockButton, AddContentBlockButton,
VisibilityPopover, VisibilityAction,
EyeIcon, EyeIcon,
PenIcon, PenIcon,
TrashIcon TrashIcon
@ -77,16 +70,10 @@
computed: { computed: {
specialClass() { specialClass() {
return `content-block--${this.contentBlock.type.toLowerCase()}` return `content-block--${this.contentBlock.type.toLowerCase()}`
},
canManageContent() {
return this.me.permissions.includes('users.can_manage_school_class_content');
} }
}, },
methods: { methods: {
toggleVisibility() {
this.showVisibility = !this.showVisibility;
},
editContentBlock() { editContentBlock() {
this.$store.dispatch('editContentBlock', this.contentBlock.id); this.$store.dispatch('editContentBlock', this.contentBlock.id);
}, },
@ -118,18 +105,9 @@
} }
}, },
apollo: {
me: {
query: ME_QUERY,
},
},
data() { data() {
return { return {
showVisibility: false, showVisibility: false
me: {
permissions: []
}
} }
} }
} }
@ -158,16 +136,6 @@
cursor: pointer; cursor: pointer;
} }
&__action-icon {
width: 40px;
height: 40px;
fill: $color-grey;
}
&__visibility-menu {
top: 40px;
}
&--base_communication { &--base_communication {
@include content-box($color-accent-1); @include content-box($color-accent-1);
} }

View File

@ -8,28 +8,30 @@
<div class="module__intro" v-html="module.intro"></div> <div class="module__intro" v-html="module.intro"></div>
<h3 id="objectives">Lernziele</h3> <h3 id="objectives">Lernziele</h3>
<objective-group v-for="group in module.objectiveGroups" :key="group.id" :group="group"></objective-group>
<objective-groups :groups="languageCommunicationObjectiveGroups"></objective-groups>
<objective-groups :groups="societyObjectiveGroups"></objective-groups>
<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>
<h3 id="objectives-confirm">Alles klar?</h3> <h3 id="objectives-confirm">Alles klar?</h3>
<objective-group-control
v-for="(group, index) in module.objectiveGroups" <objective-groups @updateObjectiveProgress="updateObjectiveProgress" :groups="languageCommunicationObjectiveGroups" :control="true"></objective-groups>
:key="`${group.id}${index}`" <objective-groups @updateObjectiveProgress="updateObjectiveProgress" :groups="societyObjectiveGroups" :control="true"></objective-groups>
:group="group"
@updateObjectiveProgress="updateObjectiveProgress"></objective-group-control>
</div> </div>
</template> </template>
<script> <script>
import ObjectiveGroup from '@/components/modules/ObjectiveGroup.vue'; import ObjectiveGroups from '@/components/modules/ObjectiveGroups.vue';
import ObjectiveGroupControl from '@/components/modules/ObjectiveGroupControl.vue'; import ObjectiveGroupControl from '@/components/modules/ObjectiveGroupControl.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_OBJECTIVE_PROGRESS_MUTATION from '@/graphql/gql/mutations/updateObjectiveProgress.gql';
import OBJECTIVE_QUERY from '@/graphql/gql/objectiveQuery.gql'; import OBJECTIVE_QUERY from '@/graphql/gql/objectiveQuery.gql';
const withoutOwnerFirst = (a, b) => a.owner ? 1 : 0;
export default { export default {
components: { components: {
ObjectiveGroup, ObjectiveGroups,
ObjectiveGroupControl, ObjectiveGroupControl,
Chapter Chapter
}, },
@ -41,15 +43,20 @@
} }
}, },
data() { created() {
return {
chapter: {
title: '1.1 Lehrbeginn'
}
}
}, },
created() { computed: {
languageCommunicationObjectiveGroups() {
return this.module.objectiveGroups ? this.module.objectiveGroups
.filter(group => group.title === 'LANGUAGE_COMMUNICATION')
.sort(withoutOwnerFirst) : [];
},
societyObjectiveGroups() {
return this.module.objectiveGroups ? this.module.objectiveGroups
.filter(group => group.title === 'SOCIETY')
.sort(withoutOwnerFirst) : [];
}
}, },
methods: { methods: {

View File

@ -1,6 +1,11 @@
<template> <template>
<div class="objective-group"> <div class="objective-group">
<h4>{{group.title}}</h4> <div class="objective-group__actions">
<visibility-action :block="group">
</visibility-action>
</div>
<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"> <li class="objective-group__objective" v-for="objective in group.objectives" :key="objective.id">
@ -12,9 +17,12 @@
</template> </template>
<script> <script>
export default { import VisibilityAction from '@/components/visibility/VisibilityAction';
name: 'objective-group', import EyeIcon from '@/components/icons/EyeIcon';
import ME_QUERY from '@/graphql/gql/meQuery.gql';
export default {
props: { props: {
group: { group: {
required: true, required: true,
@ -22,7 +30,35 @@
} }
}, },
components: {
VisibilityAction,
EyeIcon
},
apollo: {
me: {
query: ME_QUERY,
},
},
computed: { computed: {
canManageContent() {
return this.me.permissions.includes('users.can_manage_school_class_content');
}
} }
} }
</script> </script>
<style scoped lang="scss">
.objective-group {
position: relative;
&__actions {
position: absolute;
left: -70px;
top: -4px;
display: grid;
}
}
</style>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="objective-group objective-group-control"> <div class="objective-group objective-group-control">
<h4>{{group.title}}</h4> <h4>{{group.displayTitle}}</h4>
<ul class="objective-group__objective-list objective-group__objective-list--no-list"> <ul class="objective-group__objective-list objective-group__objective-list--no-list">
<li v-if="objectives" class="objective-group__objective" v-for="objective in objectives" :key="objective.id"> <li v-if="objectives" class="objective-group__objective" v-for="objective in objectives" :key="objective.id">

View File

@ -0,0 +1,73 @@
<template>
<div>
<template v-if="control">
<objective-group-control
v-for="(group, index) in objectiveGroups"
:key="`${group.id}${index}`"
:group="group"
@updateObjectiveProgress="updateObjectiveProgress"></objective-group-control>
</template>
<template v-if="!control">
<objective-group v-for="group in objectiveGroups" :key="group.id" :group="group"></objective-group>
</template>
</div>
</template>
<script>
import ObjectiveGroup from '@/components/modules/ObjectiveGroup';
import ObjectiveGroupControl from '@/components/modules/ObjectiveGroupControl';
import {meQuery} from '@/graphql/queries';
export default {
props: {
groups: Array,
control: {
type: Boolean,
default: false
}
},
components: {
ObjectiveGroup,
ObjectiveGroupControl
},
apollo: {
me: meQuery
},
data() {
return {
me: {
permissions: []
}
}
},
computed: {
objectiveGroups() {
/*
a teacher should get multiple blocks, so he can manage the visibility for his students.
students don't care about the blocks, so they should get just one block that contains all the objectives
*/
if (this.me.permissions.includes('users.can_manage_school_class_content') || !this.groups.length) {
return this.groups;
} else {
// todo: maybe this can be done a bit more elegantly
const groups = [...this.groups];
const objectives = groups.map(g => g.objectives).flat(); // get all objectives in one array
const firstGroup = Object.assign({}, groups.shift(), {objectives});
return [firstGroup];
}
}
},
methods: {
updateObjectiveProgress(checked, id) {
this.$emit('updateObjectiveProgress', checked, id);
}
}
}
</script>

View File

@ -0,0 +1,65 @@
<template>
<div class="visibility-action">
<a @click="toggleVisibility()" v-if="canManageContent" class="visibility-action__action-button">
<eye-icon class="visibility-action__action-icon action-icon"></eye-icon>
</a>
<visibility-popover
@hide-me="showVisibility = false"
:show="showVisibility"
:block="block"
class="visibility-action__visibility-menu"
></visibility-popover>
</div>
</template>
<script>
import EyeIcon from '@/components/icons/EyeIcon';
import VisibilityPopover from '@/components/visibility/VisibilityPopover';
import ME_QUERY from '@/graphql/gql/meQuery.gql';
export default {
props: ['block'],
components: {
VisibilityPopover,
EyeIcon
},
computed: {
canManageContent() {
return this.me.permissions.includes('users.can_manage_school_class_content');
}
},
methods: {
toggleVisibility() {
this.showVisibility = !this.showVisibility;
},
},
apollo: {
me: {
query: ME_QUERY,
},
},
data() {
return {
showVisibility: false,
me: {
permissions: []
}
}
}
}
</script>
<style scoped lang="scss">
.visibility-action {
&__visibility-menu {
top: 40px;
}
}
</style>

View File

@ -13,14 +13,12 @@
<script> <script>
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 Checkbox from '@/components/Checkbox'; import Checkbox from '@/components/Checkbox';
import ME_QUERY from '@/graphql/gql/meQuery.gql' import ME_QUERY from '@/graphql/gql/meQuery.gql'
// import MODULE_DETAILS_QUERY from '@/graphql/gql/moduleDetailsQuery.gql';
// import store from '@/store/index';
export default { export default {
props: ['show', 'content-block'], props: ['show', 'block'],
components: { components: {
Checkbox Checkbox
@ -42,30 +40,48 @@
updateVisibility(checked, item) { updateVisibility(checked, item) {
item.hidden = !checked; item.hidden = !checked;
this.$apollo.mutate({ const visibility = this.schoolClassVisibility.map(g => {
mutation: CHANGE_CONTENT_BLOCK_MUTATION, return {
variables: { schoolClassId: g.id,
hidden: g.hidden || false
}
});
let mutation, variables;
const id = this.block.id;
if (this.isContentBlock) {
mutation = CHANGE_CONTENT_BLOCK_MUTATION;
variables = {
input: { input: {
id: this.contentBlock.id, id,
contentBlock: { contentBlock: {
visibility: this.schoolClassVisibility.map(g => { visibility
return {
schoolClassId: g.id,
hidden: g.hidden || false
}
})
} }
} }
} }
} else {
mutation = UPDATE_OBJECTIVE_GROUP_VISIBILITY_MUTATION;
variables = {
input: {
id,
visibility
}
}
}
this.$apollo.mutate({
mutation,
variables
}); });
}, },
hidePopover() { hidePopover() {
this.$emit('hide-me'); this.$emit('hide-me');
}, },
isSchoolClassHidden(schoolClass) { isSchoolClassHidden(schoolClass) {
return this.contentBlock.userCreated return (this.isContentBlock ? this.block.userCreated : !!this.block.owner)
? this.contentBlock.visibleFor.findIndex(el => el.id === schoolClass.id) === -1 ? this.block.visibleFor.findIndex(el => el.id === schoolClass.id) === -1
: this.contentBlock.hiddenFor.findIndex(el => el.id === schoolClass.id) > -1; : this.block.hiddenFor.findIndex(el => el.id === schoolClass.id) > -1;
} }
}, },
@ -74,6 +90,10 @@
return this.$getRidOfEdges(this.me.schoolClasses); return this.$getRidOfEdges(this.me.schoolClasses);
}, },
isContentBlock() {
return this.block.__typename === 'ContentBlockNode';
},
schoolClassVisibility() { schoolClassVisibility() {
return this.schoolClasses.map(schoolClass => { return this.schoolClasses.map(schoolClass => {
return { return {

View File

@ -0,0 +1,24 @@
fragment ObjectiveGroupParts on ObjectiveGroupNode {
id
title
displayTitle
owner {
id
}
hiddenFor {
edges {
node {
id
name
}
}
}
visibleFor {
edges {
node {
id
name
}
}
}
}

View File

@ -1,5 +1,6 @@
#import "./fragments/chapterParts.gql" #import "./fragments/chapterParts.gql"
#import "./fragments/assignmentParts.gql" #import "./fragments/assignmentParts.gql"
#import "./fragments/objectiveGroupParts.gql"
query ModulesQuery($slug: String!) { query ModulesQuery($slug: String!) {
module(slug: $slug) { module(slug: $slug) {
id id
@ -19,8 +20,7 @@ query ModulesQuery($slug: String!) {
objectiveGroups { objectiveGroups {
edges { edges {
node { node {
id ...ObjectiveGroupParts
title
objectives { objectives {
edges { edges {
node { node {

View File

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

View File

@ -1,4 +1,5 @@
import MODULE_DETAILS_QUERY from './gql/moduleDetailsQuery.gql'; import MODULE_DETAILS_QUERY from './gql/moduleDetailsQuery.gql';
import ME_QUERY from './gql/meQuery.gql';
export function moduleQuery() { export function moduleQuery() {
return { return {
@ -11,3 +12,9 @@ export function moduleQuery() {
} }
} }
} }
export function meQuery() {
return {
query: ME_QUERY
}
}

View File

@ -0,0 +1,5 @@
.action-icon {
width: 40px;
height: 40px;
fill: $color-grey;
}

View File

@ -12,3 +12,4 @@
@import "help-text"; @import "help-text";
@import "objective-group"; @import "objective-group";
@import "article"; @import "article";
@import "actions";

View File

@ -17,7 +17,7 @@ class ContentElementValueInput(InputObjectType):
# we'll handle this with a single input, even tho it would be nice to have a type for every different possibility # we'll handle this with a single input, even tho it would be nice to have a type for every different possibility
# see discussion at https://github.com/graphql/graphql-js/issues/207 # see discussion at https://github.com/graphql/graphql-js/issues/207
text = graphene.String(description='To be used for link_block, text_block types') text = graphene.String(description='To be used for link_block, text_block types')
url = graphene.String(description='To be used for link, basic_knowledge, image_block types') url = graphene.String(description='To be used for link, image_block types')
description = graphene.String(description='To be used for basic_knowledge type') description = graphene.String(description='To be used for basic_knowledge type')
title = graphene.String(description='To be used for image_block, assignment type') title = graphene.String(description='To be used for image_block, assignment type')
assignment = graphene.String(description='To be used for assignment type') assignment = graphene.String(description='To be used for assignment type')
@ -29,7 +29,7 @@ class ContentElementInput(InputObjectType):
value = ContentElementValueInput() value = ContentElementValueInput()
class UserGroupContentBlockVisibility(InputObjectType): class UserGroupBlockVisibility(InputObjectType):
school_class_id = graphene.ID(required=True) school_class_id = graphene.ID(required=True)
hidden = graphene.Boolean(required=True) hidden = graphene.Boolean(required=True)
@ -38,4 +38,4 @@ class ContentBlockInput(InputObjectType):
title = graphene.String() title = graphene.String()
type = graphene.String() type = graphene.String()
contents = graphene.List(ContentElementInput) contents = graphene.List(ContentElementInput)
visibility = graphene.List(UserGroupContentBlockVisibility) visibility = graphene.List(UserGroupBlockVisibility)

View File

@ -9,6 +9,7 @@ from api.utils import get_object, get_errors
from books.models import ContentBlock, Chapter, SchoolClass 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 .utils import handle_content_block, set_user_defined_block_type from .utils import handle_content_block, set_user_defined_block_type
@ -37,9 +38,9 @@ class MutateContentBlock(relay.ClientIDMutation):
if visibility_list is not None: if visibility_list is not None:
if content_block.user_created: if content_block.user_created:
cls.set_visible_for(content_block, visibility_list) set_visible_for(content_block, visibility_list)
else: else:
cls.set_hidden_for(content_block, visibility_list) set_hidden_for(content_block, visibility_list)
if title is not None: if title is not None:
content_block.title = title content_block.title = title
@ -59,24 +60,6 @@ class MutateContentBlock(relay.ClientIDMutation):
return cls(content_block=None, errors=errors) return cls(content_block=None, errors=errors)
@classmethod
def set_hidden_for(cls, content_block, visibility_list):
for v in visibility_list:
school_class = get_object(SchoolClass, v.school_class_id)
if v.hidden:
content_block.hidden_for.add(school_class)
else:
content_block.hidden_for.remove(school_class)
@classmethod
def set_visible_for(cls, content_block, visibility_list):
for v in visibility_list:
school_class = get_object(SchoolClass, v.school_class_id)
if v.hidden:
content_block.visible_for.remove(school_class)
else:
content_block.visible_for.add(school_class)
class AddContentBlock(relay.ClientIDMutation): class AddContentBlock(relay.ClientIDMutation):
class Input: class Input:

View File

@ -56,11 +56,6 @@ class ChapterNode(DjangoObjectType):
return publisher_content_blocks.union(user_created_content_blocks) return publisher_content_blocks.union(user_created_content_blocks)
# if user.has_perm('users.can_manage_school_class_content'):
# return ContentBlock.get_by_parent(self)
# else:
# return ContentBlock.get_by_parent(self)
class ModuleNode(DjangoObjectType): class ModuleNode(DjangoObjectType):
pk = graphene.Int() pk = graphene.Int()
@ -88,6 +83,22 @@ class ModuleNode(DjangoObjectType):
def resolve_chapters(self, info, **kwargs): def resolve_chapters(self, info, **kwargs):
return Chapter.get_by_parent(self) return Chapter.get_by_parent(self)
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()

View File

@ -16,7 +16,7 @@ from users.services import create_users
data = [ data = [
{ {
'title': 'Gesellschaft', 'title': 'society',
'topics': [ 'topics': [
{ {
'order': 1, 'order': 1,
@ -42,7 +42,7 @@ data = [
""", """,
'objective_groups': [ 'objective_groups': [
{ {
'title': 'Gesellschaft', 'title': 'society',
'objectives': [ 'objectives': [
{ {
'text': 'Ich kann wichtige personelle und organisatorische Strukturen der Berufsfachschule beschreiben und deren Angebote, Konzepte erklären und Weisungen nennen.' 'text': 'Ich kann wichtige personelle und organisatorische Strukturen der Berufsfachschule beschreiben und deren Angebote, Konzepte erklären und Weisungen nennen.'
@ -56,7 +56,7 @@ data = [
] ]
}, },
{ {
'title': 'Sprache und Kommunikation', 'title': 'language_communication',
'objectives': [ 'objectives': [
{ {
'text': 'Ich kenne verschiedene Arten von Fragen.' 'text': 'Ich kenne verschiedene Arten von Fragen.'
@ -348,7 +348,7 @@ data = [
""", """,
'objective_groups': [ 'objective_groups': [
{ {
'title': 'Gesellschaft', 'title': 'society',
'objectives': [ 'objectives': [
{ {
'text': 'Ich kann Anlageformen für mich so auswählen, dass sie meiner wirtschaftlichen Situation und meiner Risikofreude entsprechen.' 'text': 'Ich kann Anlageformen für mich so auswählen, dass sie meiner wirtschaftlichen Situation und meiner Risikofreude entsprechen.'
@ -359,7 +359,7 @@ data = [
] ]
}, },
{ {
'title': 'Sprache und Kommunikation', 'title': 'language_communication',
'objectives': [ 'objectives': [
{ {
'text': 'Ich kann zu wichtigen Aussagen aus einem Erklärvideo Notizen machen.' 'text': 'Ich kann zu wichtigen Aussagen aus einem Erklärvideo Notizen machen.'
@ -755,7 +755,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,
user=None, owner=None,
**factory_params) **factory_params)
default_objectives = [{} for i in range(0, 4)] default_objectives = [{} for i in range(0, 4)]

20
server/core/utils.py Normal file
View File

@ -0,0 +1,20 @@
from api.utils import get_object
from users.models import SchoolClass
def set_hidden_for(block, visibility_list):
for v in visibility_list:
school_class = get_object(SchoolClass, v.school_class_id)
if v.hidden:
block.hidden_for.add(school_class)
else:
block.hidden_for.remove(school_class)
def set_visible_for(block, visibility_list):
for v in visibility_list:
school_class = get_object(SchoolClass, v.school_class_id)
if v.hidden:
block.visible_for.remove(school_class)
else:
block.visible_for.add(school_class)

View File

@ -5,8 +5,8 @@ from objectives.models import ObjectiveGroup, Objective, ObjectiveProgressStatus
@admin.register(ObjectiveGroup) @admin.register(ObjectiveGroup)
class ObjectiveGroupAdmin(admin.ModelAdmin): class ObjectiveGroupAdmin(admin.ModelAdmin):
list_display = ('title', 'module', 'user') list_display = ('title', 'module', 'owner')
list_filter = ('title', 'module', 'user') list_filter = ('title', 'module', 'owner')
@admin.register(Objective) @admin.register(Objective)

View File

@ -12,10 +12,10 @@ class ObjectiveGroupFactory(factory.django.DjangoModelFactory):
class Meta: class Meta:
model = ObjectiveGroup model = ObjectiveGroup
title = factory.Iterator(['Gesellschaft', 'Sprache und Kommunikation']) title = factory.Iterator([ObjectiveGroup.LANGUAGE_COMMUNICATION, ObjectiveGroup.SOCIETY])
module = factory.SubFactory(ModuleFactory) module = factory.SubFactory(ModuleFactory)
user = factory.Iterator(get_user_model().objects.all()) owner = factory.Iterator(get_user_model().objects.all())
class ObjectiveFactory(factory.django.DjangoModelFactory): class ObjectiveFactory(factory.django.DjangoModelFactory):

View File

@ -0,0 +1,18 @@
# Generated by Django 2.0.6 on 2018-10-30 17:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('objectives', '0003_auto_20181022_1646'),
]
operations = [
migrations.AlterField(
model_name='objectivegroup',
name='title',
field=models.CharField(blank=True, choices=[('language_communication', 'Sprache & Kommunikation'), ('society', 'Gesellschaft')], default='language_communication', max_length=255, verbose_name='title'),
),
]

View File

@ -0,0 +1,20 @@
from django.db import migrations
def migrate_titles(apps, schema_editor):
ObjectiveGroup = apps.get_model('objectives', 'ObjectiveGroup')
for og in ObjectiveGroup.objects.filter(title='Sprache und Kommunikation'):
og.title = 'language_communication'
og.save()
for og in ObjectiveGroup.objects.filter(title='Gesellschaft'):
og.title = 'society'
og.save()
class Migration(migrations.Migration):
dependencies = [
('objectives', '0004_auto_20181030_1726')
]
operations = [
migrations.RunPython(migrate_titles)
]

View File

@ -0,0 +1,29 @@
# Generated by Django 2.0.6 on 2018-10-31 13:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0002_auto_20181017_1340'),
('objectives', '0005_migrate_titles'),
]
operations = [
migrations.RenameField(
model_name='objectivegroup',
old_name='user',
new_name='owner',
),
migrations.AddField(
model_name='objectivegroup',
name='hidden_for',
field=models.ManyToManyField(related_name='hidden_objective_groups', to='users.SchoolClass'),
),
migrations.AddField(
model_name='objectivegroup',
name='visible_for',
field=models.ManyToManyField(related_name='visible_objective_groups', to='users.SchoolClass'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 2.0.6 on 2018-10-31 13:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('objectives', '0006_auto_20181031_1323'),
]
operations = [
migrations.AlterField(
model_name='objectivegroup',
name='hidden_for',
field=models.ManyToManyField(blank=True, related_name='hidden_objective_groups', to='users.SchoolClass'),
),
migrations.AlterField(
model_name='objectivegroup',
name='visible_for',
field=models.ManyToManyField(blank=True, related_name='visible_objective_groups', to='users.SchoolClass'),
),
]

View File

@ -2,6 +2,7 @@ from django.contrib.auth import get_user_model
from django.db import models from django.db import models
from books.models import Module from books.models import Module
from users.models import SchoolClass
class ObjectiveGroup(models.Model): class ObjectiveGroup(models.Model):
@ -9,10 +10,21 @@ class ObjectiveGroup(models.Model):
verbose_name = 'Lernzielgruppe' verbose_name = 'Lernzielgruppe'
verbose_name_plural = 'Lernzielgruppen' verbose_name_plural = 'Lernzielgruppen'
title = models.CharField('title', blank=True, null=False, max_length=255) LANGUAGE_COMMUNICATION = 'language_communication'
SOCIETY = 'society'
TITLE_CHOICES = (
(LANGUAGE_COMMUNICATION, 'Sprache & Kommunikation'),
(SOCIETY, 'Gesellschaft'),
)
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 # a user can define her own objectives, hence this optional param
user = models.ForeignKey(get_user_model(), blank=True, null=True, on_delete=models.CASCADE) owner = models.ForeignKey(get_user_model(), blank=True, null=True, on_delete=models.CASCADE)
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 'ObjectiveGroup {}-{}-{}'.format(self.id, self.module, self.title) return 'ObjectiveGroup {}-{}-{}'.format(self.id, self.module, self.title)

View File

@ -1,8 +1,10 @@
import graphene import graphene
from graphene import relay from graphene import relay, InputObjectType
from api.utils import get_object from api.utils import get_object
from objectives.models import ObjectiveProgressStatus, Objective from books.schema.inputs import UserGroupBlockVisibility
from objectives.schema import ObjectiveNode from core.utils import set_visible_for, set_hidden_for
from objectives.models import ObjectiveProgressStatus, Objective, ObjectiveGroup
from objectives.schema import ObjectiveNode, ObjectiveGroupNode
class UpdateObjectiveProgress(relay.ClientIDMutation): class UpdateObjectiveProgress(relay.ClientIDMutation):
@ -29,5 +31,31 @@ class UpdateObjectiveProgress(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:
if objective_group.owner is not None:
set_visible_for(objective_group, visibility_list)
else:
set_hidden_for(objective_group, visibility_list)
objective_group.save()
return cls(objective_group=objective_group)
class ObjectiveMutations: class ObjectiveMutations:
update_objective_progress = UpdateObjectiveProgress.Field() update_objective_progress = UpdateObjectiveProgress.Field()
update_objective_group_visibility = UpdateObjectiveGroupVisibility.Field()

View File

@ -8,6 +8,7 @@ from objectives.models import ObjectiveGroup, Objective, ObjectiveProgressStatus
class ObjectiveGroupNode(DjangoObjectType): class ObjectiveGroupNode(DjangoObjectType):
pk = graphene.Int() pk = graphene.Int()
display_title = graphene.String()
class Meta: class Meta:
model = ObjectiveGroup model = ObjectiveGroup
@ -17,6 +18,10 @@ class ObjectiveGroupNode(DjangoObjectType):
def resolve_pk(self, *args, **kwargs): def resolve_pk(self, *args, **kwargs):
return self.id return self.id
def resolve_display_title(self, *args, **kwargs):
return self.get_title_display()
class ObjectiveNode(DjangoObjectType): class ObjectiveNode(DjangoObjectType):
pk = graphene.Int() pk = graphene.Int()
@ -26,6 +31,9 @@ class ObjectiveNode(DjangoObjectType):
filter_fields = ['text'] filter_fields = ['text']
interfaces = (relay.Node,) interfaces = (relay.Node,)
def resolve_objective_progress(self, info, **kwargs):
return self.objective_progress.filter(user=info.context.user)
class ObjectiveProgressStatusNode(DjangoObjectType): class ObjectiveProgressStatusNode(DjangoObjectType):
pk = graphene.Int() pk = graphene.Int()
@ -40,4 +48,5 @@ class ObjectiveProgressStatusNode(DjangoObjectType):
class ObjectivesQuery(object): class ObjectivesQuery(object):
objective_group = relay.Node.Field(ObjectiveGroupNode)
objective_groups = DjangoFilterConnectionField(ObjectiveGroupNode) objective_groups = DjangoFilterConnectionField(ObjectiveGroupNode)