Merged develop into master

This commit is contained in:
Ramon Wenger 2019-07-25 14:33:52 +02:00
commit 334cbca980
42 changed files with 687 additions and 318 deletions

View File

@ -46,7 +46,7 @@
} }
&__icon { &__icon {
height: 57px; width: 40px;
fill: $color-silver-dark; fill: $color-silver-dark;
} }
} }

View File

@ -6,7 +6,7 @@
{{chapter.description}} {{chapter.description}}
</p> </p>
<add-content-block-button :parent="chapter.id"></add-content-block-button> <add-content-block-button :parent="chapter.id" v-if="editModule"></add-content-block-button>
<content-block :contentBlock="contentBlock" <content-block :contentBlock="contentBlock"
:parent="chapter.id" :parent="chapter.id"
@ -19,6 +19,8 @@
import ContentBlock from '@/components/ContentBlock'; import ContentBlock from '@/components/ContentBlock';
import AddContentBlockButton from '@/components/AddContentBlockButton'; import AddContentBlockButton from '@/components/AddContentBlockButton';
import {mapGetters} from 'vuex';
export default { export default {
props: ['chapter', 'index'], props: ['chapter', 'index'],
@ -36,6 +38,7 @@
currentFilter() { currentFilter() {
return this.$store.state.filterForSchoolClass; return this.$store.state.filterForSchoolClass;
}, },
...mapGetters(['editModule'])
}, },
methods: { methods: {

View File

@ -0,0 +1,123 @@
<template>
<div class="class-selection" v-if="isTeacher">
<div class="class-selection__selected-class selected-class" @click="showPopover = !showPopover">
<p class="selected-class__text">Klasse: {{currentClassSelection.name}}</p>
</div>
<widget-popover v-if="showPopover"
@hide-me="showPopover = false"
:mobile="mobile"
class="class-selection__popover">
<li class="popover-links__link popover-links__link--large" v-for="schoolClass in schoolClasses"
:key="schoolClass.id"
:label="schoolClass.name"
:item="schoolClass"
@click="updateFilter(schoolClass)">
{{schoolClass.name}}
</li>
</widget-popover>
</div>
</template>
<script>
import WidgetPopover from '@/components/WidgetPopover';
import ME_QUERY from '@/graphql/gql/meQuery.gql';
import UPDATE_USER_SETTING from '@/graphql/gql/mutations/updateUserSetting.gql';
export default {
components: {
WidgetPopover
},
props: {
mobile: {
type: Boolean,
default: false
}
},
apollo: {
me: {
query: ME_QUERY
}
},
data() {
return {
me: {
selectedClass: {
id: ''
},
permissions: []
},
showPopover: false
}
},
methods: {
updateFilter(selectedClass) {
this.$apollo.mutate({
mutation: UPDATE_USER_SETTING,
variables: {
input: {
id: selectedClass.id
}
},
update(store, data) {
let meData = store.readQuery({query: ME_QUERY});
meData.me.selectedClass = selectedClass
store.writeQuery({query: ME_QUERY, data: meData});
}
}).catch((error) => {
console.log('fail', error)
});
this.showPopover = false;
}
},
computed: {
currentClassSelection() {
let currentClass = this.schoolClasses.find(schoolClass => {
return schoolClass.id === this.me.selectedClass.id
})
return currentClass || this.schoolClasses[0];
},
schoolClasses() {
return this.$getRidOfEdges(this.me.schoolClasses);
},
isTeacher() {
return this.me.permissions.includes('users.can_manage_school_class_content');
}
},
}
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.class-selection {
position: relative;
cursor: pointer;
margin-right: $large-spacing;
&__popover {
white-space: nowrap;
top: 40px;
}
}
.selected-class {
&__text {
line-height: $large-spacing;
@include regular-text;
color: $color-silver-dark;
}
}
.popover-links__link {
cursor: pointer;
}
</style>

View File

@ -1,17 +1,17 @@
<template> <template>
<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">
<visibility-action <visibility-action
v-if="!contentBlock.indent" v-if="!contentBlock.indent"
:block="contentBlock"></visibility-action> :block="contentBlock"></visibility-action>
<a @click="editContentBlock()" v-if="canEditContentBlock" class="content-block__action-button"> <!--<a @click="editContentBlock()" v-if="canEditContentBlock" class="content-block__action-button">-->
<pen-icon class="content-block__action-icon action-icon"></pen-icon> <!--<pen-icon class="content-block__action-icon action-icon"></pen-icon>-->
</a> <!--</a>-->
<a @click="deleteContentBlock(contentBlock.id)" v-if="canEditContentBlock" class="content-block__action-button"> <!--<a @click="deleteContentBlock(contentBlock.id)" v-if="canEditContentBlock" class="content-block__action-button">-->
<trash-icon class="content-block__action-icon action-icon"></trash-icon> <!--<trash-icon class="content-block__action-icon action-icon"></trash-icon>-->
</a> <!--</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>
<h4 class="content-block__title" v-if="!contentBlock.indent">{{contentBlock.title}}</h4> <h4 class="content-block__title" v-if="!contentBlock.indent">{{contentBlock.title}}</h4>
@ -24,7 +24,7 @@
</div> </div>
<add-content-block-button :after="contentBlock.id" v-if="!contentBlock.indent"></add-content-block-button> <add-content-block-button :after="contentBlock.id" v-if="!contentBlock.indent && editModule"></add-content-block-button>
</div> </div>
@ -55,6 +55,8 @@
import CHAPTER_QUERY from '@/graphql/gql/chapterQuery.gql'; import CHAPTER_QUERY from '@/graphql/gql/chapterQuery.gql';
import DELETE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/deleteContentBlock.gql'; import DELETE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/deleteContentBlock.gql';
import {mapGetters} from 'vuex';
const instruments = { const instruments = {
base_communication: 'Sprache & Kommunikation', base_communication: 'Sprache & Kommunikation',
base_society: 'Gesellschaft' base_society: 'Gesellschaft'
@ -89,6 +91,7 @@
}, },
computed: { computed: {
...mapGetters(['editModule']),
specialClass() { specialClass() {
return `content-block--${this.contentBlock.type.toLowerCase()}` return `content-block--${this.contentBlock.type.toLowerCase()}`
}, },

View File

@ -1,76 +0,0 @@
<template>
<div class="filter-bar" v-if="isTeacher">
<radiobutton label="Alles" :checked="!currentFilter" v-on:input="updateFilter('')"></radiobutton>
<radiobutton
v-for="schoolClass in schoolClasses"
:key="schoolClass.id"
:label="schoolClass.name"
:item="schoolClass"
:checked="schoolClass.id === currentFilter"
v-on:input="updateFilter(schoolClass.id)"
></radiobutton>
</div>
</template>
<script>
import {mapActions} from 'vuex';
import Radiobutton from '@/components/Radiobutton';
import ME_QUERY from '@/graphql/gql/meQuery.gql';
export default {
components: {
Radiobutton
},
apollo: {
me: {
query: ME_QUERY
}
},
data() {
return {
me: {
permissions: []
}
}
},
computed: {
currentFilter() {
return this.$store.state.filterForSchoolClass;
},
schoolClasses() {
return this.$getRidOfEdges(this.me.schoolClasses);
},
isTeacher() {
return this.me.permissions.includes('users.can_manage_school_class_content');
}
},
methods: {
...mapActions({
updateFilter: 'setfilterForSchoolClass'
})
}
}
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
.filter-bar {
position: sticky;
top: -1px;
z-index: 9;
padding: 0 24px;
height: 50px;
background-color: $color-silver-light;
display: flex;
align-items: center;
justify-items: left;
border: 1px solid rgba(228, 228, 228, 0.9);
border-left: 0;
border-right: 0;
}
</style>

View File

@ -5,10 +5,8 @@
<logo></logo> <logo></logo>
</router-link> </router-link>
<div class="user-header"> <div class="user-header">
<router-link to="/me/activity"> <class-selection-widget />
<user-widget v-bind="me"></user-widget> <user-widget v-bind="me"></user-widget>
</router-link>
<logout-widget></logout-widget>
</div> </div>
<book-navigation v-if="showSubnavigation"> <book-navigation v-if="showSubnavigation">
</book-navigation> </book-navigation>
@ -21,6 +19,7 @@
import UserWidget from '@/components/UserWidget.vue'; import UserWidget from '@/components/UserWidget.vue';
import LogoutWidget from '@/components/LogoutWidget.vue'; import LogoutWidget from '@/components/LogoutWidget.vue';
import Logo from '@/components/icons/Logo'; import Logo from '@/components/icons/Logo';
import ClassSelectionWidget from '@/components/ClassSelectionWidget';
import ME_QUERY from '@/graphql/gql/meQuery.gql'; import ME_QUERY from '@/graphql/gql/meQuery.gql';
@ -30,7 +29,8 @@
UserWidget, UserWidget,
LogoutWidget, LogoutWidget,
BookNavigation, BookNavigation,
Logo Logo,
ClassSelectionWidget
}, },
computed: { computed: {

View File

@ -6,10 +6,8 @@
</div> </div>
<div class="mobile-navigation__subnavigation"></div> <div class="mobile-navigation__subnavigation"></div>
<div class="mobile-navigation__secondary"> <div class="mobile-navigation__secondary">
<router-link to="/me/activity"> <class-selection-widget :mobile="true" />
<user-widget class="mobile-navigation__user-widget" v-bind="me"></user-widget> <user-widget class="mobile-navigation__user-widget" v-bind="me" :mobile="true"></user-widget>
</router-link>
<logout-widget class="mobile-navigation__logout-widget"></logout-widget>
</div> </div>
</div> </div>
</template> </template>
@ -19,6 +17,7 @@
import UserWidget from '@/components/UserWidget'; import UserWidget from '@/components/UserWidget';
import LogoutWidget from '@/components/LogoutWidget'; import LogoutWidget from '@/components/LogoutWidget';
import TopNavigation from '@/components/TopNavigation'; import TopNavigation from '@/components/TopNavigation';
import ClassSelectionWidget from '@/components/ClassSelectionWidget';
import {meQuery} from '@/graphql/queries'; import {meQuery} from '@/graphql/queries';
@ -27,7 +26,8 @@
TopNavigation, TopNavigation,
Cross, Cross,
UserWidget, UserWidget,
LogoutWidget LogoutWidget,
ClassSelectionWidget
}, },
methods: { methods: {

View File

@ -12,7 +12,7 @@
</template> </template>
<script> <script>
import WidgetPopover from '@/components/rooms/WidgetPopover'; import WidgetPopover from '@/components/WidgetPopover';
import Ellipses from '@/components/icons/Ellipses.vue'; import Ellipses from '@/components/icons/Ellipses.vue';
export default { export default {
@ -31,6 +31,7 @@
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.more-options { .more-options {
display: flex; display: flex;
@ -54,6 +55,7 @@
&__popover { &__popover {
width: 180px; width: 180px;
@include popover-defaults();
} }
} }
</style> </style>

View File

@ -0,0 +1,53 @@
<template>
<div class="user-widget">
<div class="user-widget__avatar">
<avatar :avatar-url="avatarUrl" />
</div>
<span class="user-widget__name">{{firstName}} {{lastName}}</span>
<span class="user-widget__date" v-if="date">{{date}}</span>
</div>
</template>
<script>
import Avatar from '@/components/profile/Avatar';
export default {
props: ['firstName', 'lastName', 'avatarUrl', 'date'],
components: {
Avatar
}
}
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
.user-widget {
color: $color-silver-dark;
display: flex;
align-items: center;
&__name {
padding: 0px 10px;
color: $color-silver-dark;
font-family: $sans-serif-font-family;
}
&__date {
font-family: $sans-serif-font-family;
}
&__avatar {
width: 30px;
height: 30px;
fill: $color-silver-dark;
}
&--is-profile {
& > span {
color: $color-brand;
}
}
}
</style>

View File

@ -1,21 +1,72 @@
<template> <template>
<div class="user-widget" :class="{'user-widget--is-profile': isProfile}"> <div class="user-widget" :class="{'user-widget--is-profile': isProfile}">
<div class="user-widget__avatar"> <div class="user-widget__avatar" @click="toggleShowPopover()">
<avatar :avatar-url="avatarUrl" :icon-highlighted="isProfile" /> <avatar :avatar-url="avatarUrl" :icon-highlighted="isProfile"/>
</div> </div>
<span class="user-widget__name">{{firstName}} {{lastName}}</span> <widget-popover v-if="showPopover"
<span class="user-widget__date" v-if="date">{{date}}</span> @hide-me="showPopover = false"
:mobile="mobile"
class="user-widget__popover ">
<li class="popover-links__link popover-links__link--large popover-links__link--emph">{{firstName}} {{lastName}}</li>
<li class="popover-links__link popover-links__link--large">
<router-link to="/me/activity" @click="toggleShowPopover()">Aktivität</router-link>
</li>
<li class="popover-links__link popover-links__link--large" @click="toggleShowPopover()">
<router-link to="/me/profile">Profil</router-link>
</li>
<li class="popover-links__link popover-links__link--large" @click="toggleShowPopover()">
<router-link to="/me/myclasses">Klassenliste</router-link>
</li>
<li class="popover-links__link popover-links__link--large" data-cy="logout" @click="logout()">
<a>Logout</a>
</li>
</widget-popover>
</div> </div>
</template> </template>
<script> <script>
import LOGOUT_MUTATION from '@/graphql/gql/mutations/logoutUser.gql';
import Avatar from '@/components/profile/Avatar'; import Avatar from '@/components/profile/Avatar';
import WidgetPopover from '@/components/WidgetPopover';
export default { export default {
props: ['firstName', 'lastName', 'avatarUrl', 'date'], props: {
firstName: {
type: String
},
lastName: {
type: String
},
avatarUrl: {
type: String
},
mobile: {
type: Boolean,
default: false
}
},
data() {
return {
showPopover: false
}
},
methods: {
toggleShowPopover() {
this.showPopover = !this.showPopover;
},
logout() {
this.$apollo.mutate({
mutation: LOGOUT_MUTATION,
}).then(({data}) => {
if (data.logout.success) { location.replace('/') }
});
}
},
components: { components: {
Avatar Avatar, WidgetPopover
}, },
computed: { computed: {
isProfile() { isProfile() {
@ -27,14 +78,23 @@
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.user-widget { .user-widget {
color: $color-silver-dark; color: $color-silver-dark;
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
position: relative;
margin-right: $medium-spacing;
&__popover {
top: 40px;
white-space: nowrap;
}
&__name { &__name {
padding: 0px 10px; padding: 0px $small-spacing;
color: $color-silver-dark; color: $color-silver-dark;
font-family: $sans-serif-font-family; font-family: $sans-serif-font-family;
} }
@ -47,6 +107,7 @@
width: 30px; width: 30px;
height: 30px; height: 30px;
fill: $color-silver-dark; fill: $color-silver-dark;
cursor: pointer;
} }
&--is-profile { &--is-profile {

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="widget-popover" v-click-outside="hidePopover"> <div class="widget-popover" v-click-outside="hidePopover" :class="{'widget-popover--mobile': mobile}">
<ul class="widget-popover__links popover-links"> <ul class="widget-popover__links popover-links">
<slot></slot> <slot></slot>
</ul> </ul>
@ -8,6 +8,7 @@
<script> <script>
export default { export default {
props: ['mobile'],
methods: { methods: {
hidePopover() { hidePopover() {
this.$emit('hide-me'); this.$emit('hide-me');
@ -23,13 +24,17 @@
.widget-popover { .widget-popover {
position: absolute; position: absolute;
right: 0; right: 0;
bottom: -110px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: $color-white; background-color: $color-white;
padding: 20px; padding: 20px;
z-index: 10; z-index: 100;
@include widget-shadow; @include widget-shadow;
&--mobile {
left: 0;
right: inherit;
}
} }
.popover-links { .popover-links {
@ -47,6 +52,17 @@
padding: 5px 0; padding: 5px 0;
cursor: pointer; cursor: pointer;
} }
&--large {
line-height: 40px;
& > a, & {
@include small-text;
}
}
&--emph {
@include regular-text;
font-weight: 600;
}
} }
} }
</style> </style>

View File

@ -1,7 +1,8 @@
<template> <template>
<svg id="shape" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> <svg id="shape" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<path <path
d="M70.26 20.26A29.77 29.77 0 0 0 40.59 48H2a2 2 0 1 0 0 4h38.6a29.74 29.74 0 1 0 29.66-31.74zm0 55.5A25.76 25.76 0 1 1 96 50a25.78 25.78 0 0 1-25.74 25.76z"/> d="M61.74,13.24a36.43,36.43,0,0,0-22.06,7.35L2.52,48a2.5,2.5,0,0,0,0,4L39.66,79.39A36.76,36.76,0,1,0,61.74,13.24Zm0,68.52a31.5,31.5,0,0,1-19.09-6.38L8.21,50,42.66,24.6A31.76,31.76,0,1,1,61.74,81.76Z"/>
<path d="M82.24 48h-10V38a2 2 0 0 0-4 0v10h-10a2 2 0 1 0 0 4h10v10a2 2 0 0 0 4 0V52h10a2 2 0 1 0 0-4z"/> <path
d="M79.64,47.5H64.51V32.38a2.5,2.5,0,0,0-5,0V47.5H44.39a2.5,2.5,0,0,0,0,5H59.51V67.63a2.5,2.5,0,0,0,5,0V52.5H79.64a2.5,2.5,0,0,0,0-5Z"/>
</svg> </svg>
</template> </template>

View File

@ -0,0 +1,6 @@
<template>
<svg id="shape" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<path
d="M99.5,48.11a56.91,56.91,0,0,0-21-21.19L91.18,14.26a3.85,3.85,0,1,0-5.44-5.44L71.21,23.36A56.73,56.73,0,0,0,.5,48.11a3.84,3.84,0,0,0,0,3.79,56.92,56.92,0,0,0,21,21.19L8.82,85.74a3.85,3.85,0,1,0,5.44,5.44L28.79,76.64A56.66,56.66,0,0,0,50,80.79,57,57,0,0,0,99.5,51.89,3.84,3.84,0,0,0,99.5,48.11ZM8.32,50a49.09,49.09,0,0,1,56.9-20.66L27.15,67.42A49.25,49.25,0,0,1,8.32,50ZM50,73.09a49,49,0,0,1-15.22-2.43L72.85,32.59A49.25,49.25,0,0,1,91.68,50,49.27,49.27,0,0,1,50,73.09Z"/>
</svg>
</template>

View File

@ -1,8 +1,6 @@
<template> <template>
<svg id="shape" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> <svg id="shape" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<path <path
d="M98.31,45.1a61.77,61.77,0,0,0-21-17.19A60.61,60.61,0,0,0,50.78,21.5H49.23a60.59,60.59,0,0,0-26.55,6.41,61.77,61.77,0,0,0-21,17.19,8,8,0,0,0,0,9.81,61.75,61.75,0,0,0,21,17.19A60.55,60.55,0,0,0,49.22,78.5h1.55a60.58,60.58,0,0,0,26.55-6.41,61.82,61.82,0,0,0,21-17.19A8,8,0,0,0,98.31,45.1ZM78.51,50a28.54,28.54,0,0,0-8.16-19.94c1.64.64,3.25,1.35,4.83,2.14A57,57,0,0,1,94.53,48a3.19,3.19,0,0,1,0,3.92A57,57,0,0,1,75.18,67.81c-1.56.78-3.18,1.49-4.82,2.13A28.56,28.56,0,0,0,78.51,50Zm-4.79,0A23.72,23.72,0,1,1,50,26.28,23.74,23.74,0,0,1,73.72,50ZM29.65,69.94c-1.64-.64-3.26-1.35-4.83-2.14A57,57,0,0,1,5.47,52a3.18,3.18,0,0,1,0-3.92A57,57,0,0,1,24.82,32.19c1.57-.78,3.19-1.5,4.83-2.14a28.46,28.46,0,0,0,0,39.88Z"/> d="M99.5,48.11a56.85,56.85,0,0,0-99,0,3.84,3.84,0,0,0,0,3.79,56.85,56.85,0,0,0,99,0A3.84,3.84,0,0,0,99.5,48.11ZM50,30.75A12.31,12.31,0,1,1,37.69,43.06,12.32,12.32,0,0,1,50,30.75Zm0,42.34A49.28,49.28,0,0,1,8.32,50,49.3,49.3,0,0,1,35.75,29a20,20,0,1,0,28.5,0A49.3,49.3,0,0,1,91.68,50,49.28,49.28,0,0,1,50,73.09Z"/>
<path
d="M50,39.31A10.69,10.69,0,1,0,60.69,50,10.7,10.7,0,0,0,50,39.31ZM55.9,50A5.9,5.9,0,1,1,50,44.1,5.9,5.9,0,0,1,55.9,50Z"/>
</svg> </svg>
</template> </template>

View File

@ -46,6 +46,10 @@
module: { module: {
required: true, required: true,
type: Object type: Object
},
edit: {
type: Boolean,
default: false
} }
}, },

View File

@ -32,6 +32,10 @@
</div> </div>
</div> </div>
<div class="module-navigation__toggle-menu" v-if="canManageContent">
<toggle-editing></toggle-editing>
</div>
<!--toggle-solutions-for-module <!--toggle-solutions-for-module
v-if="onModulePage && module.id" v-if="onModulePage && module.id"
:module="module.id" :module="module.id"
@ -47,7 +51,8 @@
import ME_QUERY from '@/graphql/gql/meQuery.gql'; import ME_QUERY from '@/graphql/gql/meQuery.gql';
import SubNavigationItem from '@/components/book-navigation/SubNavigationItem'; import SubNavigationItem from '@/components/book-navigation/SubNavigationItem';
import ToggleSolutionsForModule from '@/components/ToggleSolutionsForModule'; import ToggleSolutionsForModule from '@/components/toggle-menu/ToggleSolutionsForModule';
import ToggleEditing from '@/components/toggle-menu/ToggleEditing';
export default { export default {
apollo: { apollo: {
@ -59,7 +64,8 @@
components: { components: {
SubNavigationItem, SubNavigationItem,
ToggleSolutionsForModule ToggleSolutionsForModule,
ToggleEditing
}, },
computed: { computed: {
@ -76,6 +82,9 @@
return [...this.module.assignments].sort((a, b) => { return [...this.module.assignments].sort((a, b) => {
return a.title.toLowerCase() > b.title.toLowerCase() ? 1 : -1; return a.title.toLowerCase() > b.title.toLowerCase() ? 1 : -1;
}) })
},
canManageContent() {
return this.me.permissions.includes('users.can_manage_school_class_content');
} }
}, },
@ -152,7 +161,7 @@
} }
} }
&__solution-toggle { &__toggle-menu {
margin-left: auto; margin-left: auto;
} }
} }

View File

@ -5,7 +5,8 @@
<ellipses></ellipses> <ellipses></ellipses>
</a> </a>
<widget-popover v-if="showMenu" <widget-popover v-if="showMenu"
@hide-me="showMenu = false"> @hide-me="showMenu = false"
class="project-actions__popover">
<li class="popover-links__link"><a @click="deleteProject(id)">Projekt löschen</a></li> <li class="popover-links__link"><a @click="deleteProject(id)">Projekt löschen</a></li>
<li class="popover-links__link"><a @click="editProject(id)">Projekt bearbeiten</a></li> <li class="popover-links__link"><a @click="editProject(id)">Projekt bearbeiten</a></li>
<li v-if="!final" class="popover-links__link"><a @click="updateShareState(id, true)">Projekt teilen</a></li> <li v-if="!final" class="popover-links__link"><a @click="updateShareState(id, true)">Projekt teilen</a></li>
@ -18,7 +19,7 @@
<script> <script>
import Ellipses from '@/components/icons/Ellipses.vue'; import Ellipses from '@/components/icons/Ellipses.vue';
import WidgetPopover from '@/components/rooms/WidgetPopover'; import WidgetPopover from '@/components/WidgetPopover';
import DELETE_PROJECT_MUTATION from '@/graphql/gql/mutations/deleteProject.gql'; import DELETE_PROJECT_MUTATION from '@/graphql/gql/mutations/deleteProject.gql';
import PROJECT_QUERY from '@/graphql/gql/projectQuery.gql'; import PROJECT_QUERY from '@/graphql/gql/projectQuery.gql';
@ -100,12 +101,17 @@
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.project-actions { .project-actions {
&__more-link { &__more-link {
cursor: pointer; cursor: pointer;
} }
&__popover {
@include popover-defaults();
}
svg { svg {
width: 30px; width: 30px;
fill: $color-charcoal-dark; fill: $color-charcoal-dark;

View File

@ -5,7 +5,8 @@
<ellipses></ellipses> <ellipses></ellipses>
</a> </a>
<widget-popover v-if="showMenu" <widget-popover v-if="showMenu"
@hide-me="showMenu = false"> @hide-me="showMenu = false"
class="room-actions__popover">
<li class="popover-links__link"><a @click="deleteRoom()">Raum löschen</a></li> <li class="popover-links__link"><a @click="deleteRoom()">Raum löschen</a></li>
<li class="popover-links__link"><a @click="editRoom()">Raum bearbeiten</a></li> <li class="popover-links__link"><a @click="editRoom()">Raum bearbeiten</a></li>
</widget-popover> </widget-popover>
@ -14,7 +15,7 @@
<script> <script>
import Ellipses from '@/components/icons/Ellipses.vue'; import Ellipses from '@/components/icons/Ellipses.vue';
import WidgetPopover from '@/components/rooms/WidgetPopover'; import WidgetPopover from '@/components/WidgetPopover';
import DELETE_ROOM_MUTATION from '@/graphql/gql/mutations/deleteRoom.gql'; import DELETE_ROOM_MUTATION from '@/graphql/gql/mutations/deleteRoom.gql';
import ROOMS_QUERY from '@/graphql/gql/roomsQuery.gql'; import ROOMS_QUERY from '@/graphql/gql/roomsQuery.gql';
@ -66,12 +67,17 @@
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.room-actions { .room-actions {
&__more-link { &__more-link {
cursor: pointer; cursor: pointer;
} }
&__popover {
@include popover-defaults();
}
svg { svg {
width: 30px; width: 30px;
fill: $color-charcoal-dark; fill: $color-charcoal-dark;

View File

@ -13,7 +13,7 @@
<p class="room-entry__teaser" v-html="teaser"> <p class="room-entry__teaser" v-html="teaser">
</p> </p>
<user-widget class="room-entry__author" v-bind="author"></user-widget> <user-meta-widget class="room-entry__author" v-bind="author"></user-meta-widget>
</div> </div>
</router-link> </router-link>
</div> </div>
@ -25,7 +25,7 @@
import ROOM_ENTRIES_QUERY from '@/graphql/gql/roomEntriesQuery.gql'; import ROOM_ENTRIES_QUERY from '@/graphql/gql/roomEntriesQuery.gql';
import ME_QUERY from '@/graphql/gql/meQuery.gql'; import ME_QUERY from '@/graphql/gql/meQuery.gql';
import UserWidget from '@/components/UserWidget'; import UserMetaWidget from '@/components/UserMetaWidget';
import MoreOptionsWidget from '@/components/MoreOptionsWidget'; import MoreOptionsWidget from '@/components/MoreOptionsWidget';
import teaser from '@/helpers/teaser'; import teaser from '@/helpers/teaser';
@ -34,7 +34,7 @@
components: { components: {
MoreOptionsWidget, MoreOptionsWidget,
UserWidget UserMetaWidget
}, },
methods: { methods: {

View File

@ -0,0 +1,28 @@
<template>
<div class="toggle-editing">
<checkbox label="Modul anpassen" :checked="checked" @input="toggle"></checkbox>
</div>
</template>
<script>
import Checkbox from '@/components/Checkbox';
import { mapGetters, mapActions } from 'vuex';
export default {
components: {
Checkbox,
},
computed: {
...mapGetters({
checked: 'editModule',
})
},
methods: {
...mapActions({
toggle: 'editModule'
})
}
}
</script>

View File

@ -1,40 +1,87 @@
<template> <template>
<div class="visibility-action"> <div class="visibility-action">
<a @click="toggleVisibility()" v-if="canManageContent" class="visibility-action__action-button"> <a @click="toggleVisibility()" v-if="canManageContent" class="visibility-action__action-button">
<eye-icon class="visibility-action__action-icon action-icon"></eye-icon> <closed-eye-icon v-if="hidden" class="visibility-action__action-icon action-icon"></closed-eye-icon>
<eye-icon v-else class="visibility-action__action-icon action-icon"></eye-icon>
</a> </a>
<visibility-popover
@hide-me="showVisibility = false"
:show="showVisibility"
:block="block"
class="visibility-action__visibility-menu"
></visibility-popover>
</div> </div>
</template> </template>
<script> <script>
import EyeIcon from '@/components/icons/EyeIcon'; import EyeIcon from '@/components/icons/EyeIcon';
import VisibilityPopover from '@/components/visibility/VisibilityPopover'; import ClosedEyeIcon from '@/components/icons/ClosedEyeIcon';
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 UPDATE_OBJECTIVE_GROUP_VISIBILITY_MUTATION from '@/graphql/gql/mutations/updateObjectiveGroupVisibility.gql';
export default { export default {
props: ['block'], props: ['block'],
components: { components: {
VisibilityPopover, EyeIcon,
EyeIcon ClosedEyeIcon
}, },
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');
},
isContentBlock() {
return this.block.__typename === 'ContentBlockNode';
},
schoolClass() {
return this.me.selectedClass || {id: 'U2Nob29sQ2xhc3NOb2RlOjE='}; // todo: remove after merge with select class feature
},
hidden() {
// is this content block / objective group user created?
return (this.isContentBlock ? this.block.userCreated : !!this.block.owner)
// if so, is visibility not explicitly set for this school class?
? this.block.visibleFor.findIndex(el => el.id === this.schoolClass.id) === -1
// otherwise, is it explicitly hidden for this school class?
: this.block.hiddenFor.findIndex(el => el.id === this.schoolClass.id) > -1;
} }
}, },
methods: { methods: {
toggleVisibility() { toggleVisibility() {
this.showVisibility = !this.showVisibility; let hidden = !this.hidden;
let schoolClassId = this.schoolClass.id;
const visibility = [{
schoolClassId,
hidden
}];
let mutation, variables;
const id = this.block.id;
if (this.isContentBlock) {
mutation = CHANGE_CONTENT_BLOCK_MUTATION;
variables = {
input: {
id,
contentBlock: {
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,
variables
});
}, },
}, },
@ -57,6 +104,7 @@
<style scoped lang="scss"> <style scoped lang="scss">
.visibility-action { .visibility-action {
margin-top: 9px;
&__visibility-menu { &__visibility-menu {
top: 40px; top: 40px;

View File

@ -1,138 +0,0 @@
<template>
<div class="visibility-menu" v-if="show" v-click-outside="hidePopover">
<h3 class="visibility-menu__title">Sichtbarkeit</h3>
<div v-for="schoolClass in schoolClassVisibility" :key="schoolClass.id" class="visibility-menu__item">
<checkbox :checked="!schoolClass.hidden"
:item="schoolClass"
:label="schoolClass.name"
v-on:input="updateVisibility"
></checkbox>
</div>
</div>
</template>
<script>
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 ME_QUERY from '@/graphql/gql/meQuery.gql'
export default {
props: ['show', 'block'],
components: {
Checkbox
},
apollo: {
me: {
query: ME_QUERY,
},
},
data() {
return {
me: {}
}
},
methods: {
updateVisibility(checked, item) {
item.hidden = !checked;
const visibility = this.schoolClassVisibility.map(g => {
return {
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: {
id,
contentBlock: {
visibility
}
}
}
} else {
mutation = UPDATE_OBJECTIVE_GROUP_VISIBILITY_MUTATION;
variables = {
input: {
id,
visibility
}
}
}
this.$apollo.mutate({
mutation,
variables
});
},
hidePopover() {
this.$emit('hide-me');
},
isSchoolClassHidden(schoolClass) {
return (this.isContentBlock ? this.block.userCreated : !!this.block.owner)
? this.block.visibleFor.findIndex(el => el.id === schoolClass.id) === -1
: this.block.hiddenFor.findIndex(el => el.id === schoolClass.id) > -1;
}
},
computed: {
schoolClasses() {
return this.$getRidOfEdges(this.me.schoolClasses);
},
isContentBlock() {
return this.block.__typename === 'ContentBlockNode';
},
schoolClassVisibility() {
return this.schoolClasses.map(schoolClass => {
return {
...schoolClass,
hidden: this.isSchoolClassHidden(schoolClass)
}
});
}
}
}
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_functions.scss";
.visibility-menu {
border-radius: 13px;
border: 1px solid $color-silver-light;
box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.15);
width: 180px;
box-sizing: border-box;
padding: 19px;
left: -130px;
position: absolute;
z-index: 9;
background-color: $color-white;
&__title {
font-size: toRem(19px);
margin-bottom: 10px;
}
&__item {
margin-bottom: 18px;
&:last-of-type {
margin-bottom: 0;
}
}
}
</style>

View File

@ -11,6 +11,9 @@ fragment UserParts on UserNode {
id id
slug slug
} }
selectedClass {
id
}
schoolClasses { schoolClasses {
edges { edges {
node { node {

View File

@ -0,0 +1,8 @@
mutation UpdateSettings($input: UpdateSettingInput!) {
updateSetting(input: $input) {
success
errors {
field
}
}
}

View File

@ -5,21 +5,17 @@
<mobile-header class="header skillbox__header skillbox__header--mobile"></mobile-header> <mobile-header class="header skillbox__header skillbox__header--mobile"></mobile-header>
<!--filter-bar v-if="showFilter" class="skillbox__filter-bar"></filter-bar-->
<router-view class="skillbox__content"></router-view> <router-view class="skillbox__content"></router-view>
<footer class="skillbox__footer">Footer</footer> <footer class="skillbox__footer">Footer</footer>
</div> </div>
</template> </template>
<script> <script>
import FilterBar from '@/components/FilterBar';
import HeaderBar from '@/components/HeaderBar'; import HeaderBar from '@/components/HeaderBar';
import MobileHeader from '@/components/MobileHeader'; import MobileHeader from '@/components/MobileHeader';
export default { export default {
components: { components: {
FilterBar,
HeaderBar, HeaderBar,
MobileHeader MobileHeader
}, },

View File

@ -2,7 +2,7 @@
<div class="article"> <div class="article">
<div class="article__header"> <div class="article__header">
<div class="article__meta"> <div class="article__meta">
<user-widget v-bind="roomEntry.author"></user-widget> <user-meta-widget v-bind="roomEntry.author"></user-meta-widget>
</div> </div>
<h1 class="article__title">{{roomEntry.title}}</h1> <h1 class="article__title">{{roomEntry.title}}</h1>
</div> </div>
@ -24,7 +24,7 @@
import VideoBlock from '@/components/content-blocks/VideoBlock'; import VideoBlock from '@/components/content-blocks/VideoBlock';
import LinkBlock from '@/components/content-blocks/LinkBlock'; import LinkBlock from '@/components/content-blocks/LinkBlock';
import DocumentBlock from '@/components/content-blocks/DocumentBlock'; import DocumentBlock from '@/components/content-blocks/DocumentBlock';
import UserWidget from '@/components/UserWidget'; import UserMetaWidget from '@/components/UserMetaWidget';
import ROOM_ENTRY_QUERY from '@/graphql/gql/roomEntryQuery.gql'; import ROOM_ENTRY_QUERY from '@/graphql/gql/roomEntryQuery.gql';
@ -36,7 +36,7 @@
'video_block': VideoBlock, 'video_block': VideoBlock,
'link_block': LinkBlock, 'link_block': LinkBlock,
'document_block': DocumentBlock, 'document_block': DocumentBlock,
UserWidget UserMetaWidget
}, },
apollo: { apollo: {

View File

@ -1,5 +1,5 @@
<template> <template>
<module :module="module" v-if="module.id"></module> <module :module="module" v-if="module.id" :edit="editModule"></module>
</template> </template>
<script> <script>
@ -21,7 +21,8 @@
computed: { computed: {
...mapGetters({ ...mapGetters({
scrollToAssignmentId: 'scrollToAssignmentId', scrollToAssignmentId: 'scrollToAssignmentId',
isScrollingToAssignment: 'scrollingToAssignment' isScrollingToAssignment: 'scrollingToAssignment',
editModule: 'editModule'
}), }),
}, },

View File

@ -23,7 +23,7 @@
return this.rooms.filter(room => this.visibleFor(room, this.currentFilter)); return this.rooms.filter(room => this.visibleFor(room, this.currentFilter));
}, },
currentFilter() { currentFilter() {
return this.$store.state.filterForSchoolClass; return this.me.selectedClass.id;
}, },
canAddRoom() { canAddRoom() {
return this.me.permissions.includes('users.can_manage_school_class_content') return this.me.permissions.includes('users.can_manage_school_class_content')
@ -53,6 +53,9 @@
return { return {
rooms: [], rooms: [],
me: { me: {
selectedClass: {
id: ''
},
permissions: [] permissions: []
} }
} }

View File

@ -12,7 +12,6 @@ export default new Vuex.Store({
showMobileNavigation: false, showMobileNavigation: false,
contentBlockPosition: {}, contentBlockPosition: {},
scrollPosition: 0, scrollPosition: 0,
filterForSchoolClass: '',
currentContentBlock: '', currentContentBlock: '',
currentRoomEntry: '', currentRoomEntry: '',
parentRoom: null, parentRoom: null,
@ -29,7 +28,8 @@ export default new Vuex.Store({
vimeoId: null, vimeoId: null,
scrollToAssignmentId: '', scrollToAssignmentId: '',
scrollToAssignmentReady: false, scrollToAssignmentReady: false,
scrollingToAssignment: false scrollingToAssignment: false,
editModule: false
}, },
getters: { getters: {
@ -43,6 +43,7 @@ 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
}, },
actions: { actions: {
@ -106,9 +107,6 @@ export default new Vuex.Store({
document.body.classList.add('no-scroll'); // won't get at the body any other way document.body.classList.add('no-scroll'); // won't get at the body any other way
commit('setModal', payload); commit('setModal', payload);
}, },
setfilterForSchoolClass({commit}, payload) {
commit('setfilterForSchoolClass', payload);
},
addProjectEntry({commit, dispatch}, payload) { addProjectEntry({commit, dispatch}, payload) {
commit('setParentProject', payload); commit('setParentProject', payload);
dispatch('showModal', 'new-project-entry-wizard'); dispatch('showModal', 'new-project-entry-wizard');
@ -147,6 +145,9 @@ export default new Vuex.Store({
commit('setScrollingToAssignment', false); commit('setScrollingToAssignment', false);
dispatch('scrollToAssignmentId', ''); dispatch('scrollToAssignmentId', '');
} }
},
editModule({commit}, payload) {
commit('setEditModule', payload)
} }
}, },
@ -169,9 +170,6 @@ export default new Vuex.Store({
setCurrentContentBlock(state, payload) { setCurrentContentBlock(state, payload) {
state.currentContentBlock = payload; state.currentContentBlock = payload;
}, },
setfilterForSchoolClass(state, payload) {
state.filterForSchoolClass = payload;
},
setParentRoom(state, payload) { setParentRoom(state, payload) {
state.parentRoom = payload; state.parentRoom = payload;
}, },
@ -213,6 +211,9 @@ export default new Vuex.Store({
}, },
setScrollingToAssignment(state, payload) { setScrollingToAssignment(state, payload) {
state.scrollingToAssignment = payload; state.scrollingToAssignment = payload;
},
setEditModule(state, payload) {
state.editModule = payload;
} }
} }
}) })

View File

@ -1,5 +1,4 @@
.action-icon { .action-icon {
width: 40px; width: 30px;
height: 40px;
fill: $color-silver-dark; fill: $color-silver-dark;
} }

View File

@ -142,3 +142,7 @@
@mixin light-border($border-position) { @mixin light-border($border-position) {
border-#{$border-position}: 1px solid $color-silver; border-#{$border-position}: 1px solid $color-silver;
} }
@mixin popover-defaults() {
bottom: $popover-default-bottom;
}

View File

@ -1,5 +1,4 @@
.survey { .survey {
&__panel-title,
&__page-title { &__page-title {
@include main-title; @include main-title;
margin-bottom: $large-spacing*2; margin-bottom: $large-spacing*2;
@ -8,17 +7,22 @@
} }
} }
&__panel-title,
&__panel-description, &__panel-description,
&__page-description { &__page-description {
@include regular-paragraph;
line-height: 1.5; line-height: 1.5;
margin-bottom: $large-spacing; margin-bottom: $large-spacing;
font-size: 1rem;
> span > span { // weird survey.js html structure
@include heading-4;
}
} }
&__question-title { &__question-title {
@include heading-4; @include heading-4;
margin-bottom: $medium-spacing; margin-bottom: $medium-spacing;
span { > span > span { // weird survey.js html structure
@include heading-4; @include heading-4;
} }
} }

View File

@ -77,3 +77,6 @@ $font-weight-semibold: 600;
$font-weight-regular: 400; $font-weight-regular: 400;
$default-line-height: 1.5; $default-line-height: 1.5;
// popover
$popover-default-bottom: -110px;

View File

@ -1,8 +1,6 @@
import io import io
import os import os
from django.apps import apps
from graphene_django.filter import DjangoFilterConnectionField
from graphql_relay.node.node import from_global_id from graphql_relay.node.node import from_global_id
"""Script defined to create helper functions for graphql schema.""" """Script defined to create helper functions for graphql schema."""

View File

@ -1,7 +1,7 @@
import graphene import graphene
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from graphene import relay from graphene import relay
from graphql_relay import to_global_id, from_global_id from graphql_relay import to_global_id
from api.utils import get_object from api.utils import get_object
from rooms.inputs import UpdateRoomArgument, AddRoomArgument, AddRoomEntryArgument, UpdateRoomEntryArgument from rooms.inputs import UpdateRoomArgument, AddRoomArgument, AddRoomEntryArgument, UpdateRoomEntryArgument

View File

@ -2,7 +2,7 @@ from django.contrib import admin
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from users.forms import CustomUserCreationForm, CustomUserChangeForm from users.forms import CustomUserCreationForm, CustomUserChangeForm
from .models import User, SchoolClass, Role, UserRole from .models import User, SchoolClass, Role, UserRole, UserSetting
class SchoolClassInline(admin.TabularInline): class SchoolClassInline(admin.TabularInline):
@ -53,3 +53,9 @@ class CustomUserAdmin(UserAdmin):
admin.site.register(User, CustomUserAdmin) admin.site.register(User, CustomUserAdmin)
@admin.register(UserSetting)
class UserSettingAdmin(admin.ModelAdmin):
list_display = ('user', 'selected_class')
raw_id_fields = ('user', 'selected_class')

View File

@ -0,0 +1,23 @@
# Generated by Django 2.0.6 on 2019-07-24 20:25
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('users', '0006_auto_20190703_0959'),
]
operations = [
migrations.CreateModel(
name='UserSetting',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('selected_class', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='users.SchoolClass')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='user_setting', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -1,7 +1,6 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import AbstractUser, Permission from django.contrib.auth.models import AbstractUser, Permission
from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist
from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -38,6 +37,18 @@ class User(AbstractUser):
def users_in_same_school_class(self): def users_in_same_school_class(self):
return User.objects.filter(school_classes__users=self.pk) return User.objects.filter(school_classes__users=self.pk)
def selected_class(self):
try:
settings = UserSetting.objects.get(user=self)
return settings.selected_class
except ObjectDoesNotExist:
if self.school_classes.count() > 0:
default_selected_class = self.school_classes.first()
UserSetting.objects.create(selected_class=default_selected_class, user=self)
return default_selected_class
else:
return None
@property @property
def full_name(self): def full_name(self):
return self.get_full_name() return self.get_full_name()
@ -115,3 +126,9 @@ class UserRole(models.Model):
def __str__(self): def __str__(self):
return '%s: %s' % (self.role, self.user) return '%s: %s' % (self.role, self.user)
class UserSetting(models.Model):
user = models.OneToOneField(get_user_model(), on_delete=models.CASCADE, related_name='user_setting')
selected_class = models.ForeignKey(SchoolClass, blank=True, null=True, on_delete=models.CASCADE)

View File

@ -1,7 +1,11 @@
import graphene import graphene
from django.contrib.auth import update_session_auth_hash from django.contrib.auth import update_session_auth_hash
from django.core.exceptions import PermissionDenied
from graphene import relay from graphene import relay
from api.utils import get_object
from users.inputs import PasswordUpdateInput from users.inputs import PasswordUpdateInput
from users.models import SchoolClass, UserSetting
from users.serializers import PasswordSerialzer, AvatarUrlSerializer from users.serializers import PasswordSerialzer, AvatarUrlSerializer
@ -76,7 +80,30 @@ class UpdateAvatar(relay.ClientIDMutation):
return cls(success=False, errors=errors) return cls(success=False, errors=errors)
class UpdateSetting(relay.ClientIDMutation):
class Input:
id = graphene.ID(required=True)
@classmethod
def mutate_and_get_payload(cls, root, info, **kwargs):
class_id = kwargs.get('id')
school_class = get_object(SchoolClass, class_id)
user = info.context.user
if school_class and school_class not in user.school_classes.all():
raise PermissionDenied('Permission denied: Incorrect school class')
user_settings, created = UserSetting.objects.get_or_create(user=user)
user_settings.selected_class = school_class
user_settings.save()
return cls(success=True)
success = graphene.Boolean()
errors = graphene.List(UpdateError)
class ProfileMutations: class ProfileMutations:
update_password = UpdatePassword.Field() update_password = UpdatePassword.Field()
update_avatar = UpdateAvatar.Field() update_avatar = UpdateAvatar.Field()
update_setting = UpdateSetting.Field()

View File

@ -6,23 +6,6 @@ from graphene_django.filter import DjangoFilterConnectionField
from users.models import SchoolClass, User from users.models import SchoolClass, User
class UserNode(DjangoObjectType):
pk = graphene.Int()
permissions = graphene.List(graphene.String)
class Meta:
model = User
filter_fields = ['username', 'email']
only_fields = ['username', 'email', 'first_name', 'last_name', 'school_classes', 'last_module', 'avatar_url']
interfaces = (relay.Node,)
def resolve_pk(self, info, **kwargs):
return self.id
def resolve_permissions(self, info):
return self.get_all_permissions()
class SchoolClassNode(DjangoObjectType): class SchoolClassNode(DjangoObjectType):
pk = graphene.Int() pk = graphene.Int()
@ -35,6 +18,28 @@ class SchoolClassNode(DjangoObjectType):
return self.id return self.id
class UserNode(DjangoObjectType):
pk = graphene.Int()
permissions = graphene.List(graphene.String)
selected_class = graphene.Field(SchoolClassNode)
class Meta:
model = User
filter_fields = ['username', 'email']
only_fields = ['username', 'email', 'first_name', 'last_name', 'school_classes', 'last_module', 'avatar_url',
'selected_class']
interfaces = (relay.Node,)
def resolve_pk(self, info, **kwargs):
return self.id
def resolve_permissions(self, info):
return self.get_all_permissions()
def resolve_selected_class(self, info):
return self.selected_class()
class UsersQuery(object): class UsersQuery(object):
me = graphene.Field(UserNode) me = graphene.Field(UserNode)
all_users = DjangoFilterConnectionField(UserNode) all_users = DjangoFilterConnectionField(UserNode)

View File

@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
#
# Created on 2019-07-24
# @author: chrigu <christian.cueni@iterativ.ch>
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase, RequestFactory
from graphene.test import Client
from graphql_relay import to_global_id
from api.schema import schema
from core.factories import UserFactory
from users.factories import SchoolClassFactory
from users.models import UserSetting
class UserSettingTests(TestCase):
def setUp(self):
self.user = UserFactory(username='aschi')
self.class1 = SchoolClassFactory(users=[self.user])
self.class2 = SchoolClassFactory(users=[self.user])
self.class3 = SchoolClassFactory(users=[])
request = RequestFactory().get('/')
request.user = self.user
# adding session
middleware = SessionMiddleware()
middleware.process_request(request)
request.session.save()
self.client = Client(schema=schema, context_value=request)
def make_mutation(self, class_id):
mutation = '''
mutation UpdateSettings($input: UpdateSettingInput!) {
updateSetting(input: $input) {
success
errors {
field
}
}
}
'''
return self.client.execute(mutation, variables={
'input': {
'id': to_global_id('SchoolClassNode', class_id)
}
})
def make_query(self):
query = '''
query MeQuery {
me {
selectedClass {
name
id
}
}
}
'''
return self.client.execute(query)
def test_selects_first_class_on_first_call(self):
result = self.make_query()
first_class = self.user.school_classes.first()
self.assertIsNone(result.get('errors'))
self.assertEqual(result.get('data').get('me').get('selectedClass').get('name'), first_class.name)
def test_returns_selected_class(self):
selected_class = self.user.school_classes.all()[1]
setting = UserSetting.objects.create(user=self.user, selected_class=selected_class)
setting.save()
result = self.make_query()
self.assertIsNone(result.get('errors'))
self.assertEqual(result.get('data').get('me').get('selectedClass').get('name'),
selected_class.name)
def test_user_can_select_class(self):
selected_class = self.user.school_classes.first()
setting = UserSetting.objects.create(user=self.user, selected_class=selected_class)
setting.save()
selected_class = self.user.school_classes.all()[1]
mutation_result = self.make_mutation(selected_class.pk)
self.assertIsNone(mutation_result.get('errors'))
query_result = self.make_query()
self.assertIsNone(query_result.get('errors'))
self.assertEqual(query_result.get('data').get('me').get('selectedClass').get('name'),
selected_class.name)
def test_user_can_select_class_even_no_settings_exist(self):
selected_class = self.user.school_classes.all()[1]
mutation_result = self.make_mutation(selected_class.pk)
self.assertIsNone(mutation_result.get('errors'))
query_result = self.make_query()
self.assertIsNone(query_result.get('errors'))
self.assertEqual(query_result.get('data').get('me').get('selectedClass').get('name'),
selected_class.name)
def test_user_cannot_select_class_shes_not_part_of(self):
default_class = self.user.school_classes.first()
setting = UserSetting.objects.create(user=self.user, selected_class=default_class)
setting.save()
mutation_result = self.make_mutation(self.class3.pk)
self.assertIsNotNone(mutation_result.get('errors'))