Merged develop into master
This commit is contained in:
commit
334cbca980
|
|
@ -46,7 +46,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&__icon {
|
&__icon {
|
||||||
height: 57px;
|
width: 40px;
|
||||||
fill: $color-silver-dark;
|
fill: $color-silver-dark;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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()}`
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,10 @@
|
||||||
module: {
|
module: {
|
||||||
required: true,
|
required: true,
|
||||||
type: Object
|
type: Object
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -11,6 +11,9 @@ fragment UserParts on UserNode {
|
||||||
id
|
id
|
||||||
slug
|
slug
|
||||||
}
|
}
|
||||||
|
selectedClass {
|
||||||
|
id
|
||||||
|
}
|
||||||
schoolClasses {
|
schoolClasses {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
mutation UpdateSettings($input: UpdateSettingInput!) {
|
||||||
|
updateSetting(input: $input) {
|
||||||
|
success
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
.action-icon {
|
.action-icon {
|
||||||
width: 40px;
|
width: 30px;
|
||||||
height: 40px;
|
|
||||||
fill: $color-silver-dark;
|
fill: $color-silver-dark;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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."""
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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'))
|
||||||
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue