Merged in feature/class-selection (pull request #30)
Feature/class selection
This commit is contained in:
commit
10a492f515
|
|
@ -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,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>
|
||||
</router-link>
|
||||
<div class="user-header">
|
||||
<router-link to="/me/activity">
|
||||
<user-widget v-bind="me"></user-widget>
|
||||
</router-link>
|
||||
<logout-widget></logout-widget>
|
||||
<class-selection-widget />
|
||||
<user-widget v-bind="me"></user-widget>
|
||||
</div>
|
||||
<book-navigation v-if="showSubnavigation">
|
||||
</book-navigation>
|
||||
|
|
@ -21,6 +19,7 @@
|
|||
import UserWidget from '@/components/UserWidget.vue';
|
||||
import LogoutWidget from '@/components/LogoutWidget.vue';
|
||||
import Logo from '@/components/icons/Logo';
|
||||
import ClassSelectionWidget from '@/components/ClassSelectionWidget';
|
||||
|
||||
import ME_QUERY from '@/graphql/gql/meQuery.gql';
|
||||
|
||||
|
|
@ -30,7 +29,8 @@
|
|||
UserWidget,
|
||||
LogoutWidget,
|
||||
BookNavigation,
|
||||
Logo
|
||||
Logo,
|
||||
ClassSelectionWidget
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
|
|
|||
|
|
@ -6,10 +6,8 @@
|
|||
</div>
|
||||
<div class="mobile-navigation__subnavigation"></div>
|
||||
<div class="mobile-navigation__secondary">
|
||||
<router-link to="/me/activity">
|
||||
<user-widget class="mobile-navigation__user-widget" v-bind="me"></user-widget>
|
||||
</router-link>
|
||||
<logout-widget class="mobile-navigation__logout-widget"></logout-widget>
|
||||
<class-selection-widget :mobile="true" />
|
||||
<user-widget class="mobile-navigation__user-widget" v-bind="me" :mobile="true"></user-widget>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -19,6 +17,7 @@
|
|||
import UserWidget from '@/components/UserWidget';
|
||||
import LogoutWidget from '@/components/LogoutWidget';
|
||||
import TopNavigation from '@/components/TopNavigation';
|
||||
import ClassSelectionWidget from '@/components/ClassSelectionWidget';
|
||||
|
||||
import {meQuery} from '@/graphql/queries';
|
||||
|
||||
|
|
@ -27,7 +26,8 @@
|
|||
TopNavigation,
|
||||
Cross,
|
||||
UserWidget,
|
||||
LogoutWidget
|
||||
LogoutWidget,
|
||||
ClassSelectionWidget
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import WidgetPopover from '@/components/rooms/WidgetPopover';
|
||||
import WidgetPopover from '@/components/WidgetPopover';
|
||||
import Ellipses from '@/components/icons/Ellipses.vue';
|
||||
|
||||
export default {
|
||||
|
|
@ -31,6 +31,7 @@
|
|||
|
||||
<style scoped lang="scss">
|
||||
@import "@/styles/_variables.scss";
|
||||
@import "@/styles/_mixins.scss";
|
||||
|
||||
.more-options {
|
||||
display: flex;
|
||||
|
|
@ -54,6 +55,7 @@
|
|||
|
||||
&__popover {
|
||||
width: 180px;
|
||||
@include popover-defaults();
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<div class="user-widget" :class="{'user-widget--is-profile': isProfile}">
|
||||
<div class="user-widget__avatar">
|
||||
<avatar :avatar-url="avatarUrl" :icon-highlighted="isProfile" />
|
||||
<div class="user-widget__avatar" @click="toggleShowPopover()">
|
||||
<avatar :avatar-url="avatarUrl" :icon-highlighted="isProfile"/>
|
||||
</div>
|
||||
<span class="user-widget__name">{{firstName}} {{lastName}}</span>
|
||||
<span class="user-widget__date" v-if="date">{{date}}</span>
|
||||
<widget-popover v-if="showPopover"
|
||||
@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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LOGOUT_MUTATION from '@/graphql/gql/mutations/logoutUser.gql';
|
||||
import Avatar from '@/components/profile/Avatar';
|
||||
import WidgetPopover from '@/components/WidgetPopover';
|
||||
|
||||
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: {
|
||||
Avatar
|
||||
Avatar, WidgetPopover
|
||||
},
|
||||
computed: {
|
||||
isProfile() {
|
||||
|
|
@ -27,14 +78,23 @@
|
|||
|
||||
<style scoped lang="scss">
|
||||
@import "@/styles/_variables.scss";
|
||||
@import "@/styles/_mixins.scss";
|
||||
|
||||
.user-widget {
|
||||
color: $color-silver-dark;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
margin-right: $medium-spacing;
|
||||
|
||||
&__popover {
|
||||
top: 40px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__name {
|
||||
padding: 0px 10px;
|
||||
padding: 0px $small-spacing;
|
||||
color: $color-silver-dark;
|
||||
font-family: $sans-serif-font-family;
|
||||
}
|
||||
|
|
@ -47,6 +107,7 @@
|
|||
width: 30px;
|
||||
height: 30px;
|
||||
fill: $color-silver-dark;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&--is-profile {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<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">
|
||||
<slot></slot>
|
||||
</ul>
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
props: ['mobile'],
|
||||
methods: {
|
||||
hidePopover() {
|
||||
this.$emit('hide-me');
|
||||
|
|
@ -23,13 +24,17 @@
|
|||
.widget-popover {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: -110px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: $color-white;
|
||||
padding: 20px;
|
||||
z-index: 10;
|
||||
z-index: 100;
|
||||
@include widget-shadow;
|
||||
|
||||
&--mobile {
|
||||
left: 0;
|
||||
right: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.popover-links {
|
||||
|
|
@ -47,6 +52,17 @@
|
|||
padding: 5px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&--large {
|
||||
line-height: 40px;
|
||||
& > a, & {
|
||||
@include small-text;
|
||||
}
|
||||
}
|
||||
&--emph {
|
||||
@include regular-text;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -5,7 +5,8 @@
|
|||
<ellipses></ellipses>
|
||||
</a>
|
||||
<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="editProject(id)">Projekt bearbeiten</a></li>
|
||||
<li v-if="!final" class="popover-links__link"><a @click="updateShareState(id, true)">Projekt teilen</a></li>
|
||||
|
|
@ -18,7 +19,7 @@
|
|||
|
||||
<script>
|
||||
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 PROJECT_QUERY from '@/graphql/gql/projectQuery.gql';
|
||||
|
|
@ -100,12 +101,17 @@
|
|||
|
||||
<style scoped lang="scss">
|
||||
@import "@/styles/_variables.scss";
|
||||
@import "@/styles/_mixins.scss";
|
||||
|
||||
.project-actions {
|
||||
&__more-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__popover {
|
||||
@include popover-defaults();
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 30px;
|
||||
fill: $color-charcoal-dark;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
<ellipses></ellipses>
|
||||
</a>
|
||||
<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="editRoom()">Raum bearbeiten</a></li>
|
||||
</widget-popover>
|
||||
|
|
@ -14,7 +15,7 @@
|
|||
|
||||
<script>
|
||||
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 ROOMS_QUERY from '@/graphql/gql/roomsQuery.gql';
|
||||
|
|
@ -66,12 +67,17 @@
|
|||
|
||||
<style scoped lang="scss">
|
||||
@import "@/styles/_variables.scss";
|
||||
@import "@/styles/_mixins.scss";
|
||||
|
||||
.room-actions {
|
||||
&__more-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__popover {
|
||||
@include popover-defaults();
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 30px;
|
||||
fill: $color-charcoal-dark;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
<p class="room-entry__teaser" v-html="teaser">
|
||||
|
||||
</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>
|
||||
</router-link>
|
||||
</div>
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
import ROOM_ENTRIES_QUERY from '@/graphql/gql/roomEntriesQuery.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 teaser from '@/helpers/teaser';
|
||||
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
|
||||
components: {
|
||||
MoreOptionsWidget,
|
||||
UserWidget
|
||||
UserMetaWidget
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ fragment UserParts on UserNode {
|
|||
id
|
||||
slug
|
||||
}
|
||||
selectedClass {
|
||||
id
|
||||
}
|
||||
schoolClasses {
|
||||
edges {
|
||||
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>
|
||||
|
||||
<!--filter-bar v-if="showFilter" class="skillbox__filter-bar"></filter-bar-->
|
||||
|
||||
<router-view class="skillbox__content"></router-view>
|
||||
<footer class="skillbox__footer">Footer</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FilterBar from '@/components/FilterBar';
|
||||
import HeaderBar from '@/components/HeaderBar';
|
||||
import MobileHeader from '@/components/MobileHeader';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FilterBar,
|
||||
HeaderBar,
|
||||
MobileHeader
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div class="article">
|
||||
<div class="article__header">
|
||||
<div class="article__meta">
|
||||
<user-widget v-bind="roomEntry.author"></user-widget>
|
||||
<user-meta-widget v-bind="roomEntry.author"></user-meta-widget>
|
||||
</div>
|
||||
<h1 class="article__title">{{roomEntry.title}}</h1>
|
||||
</div>
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
import VideoBlock from '@/components/content-blocks/VideoBlock';
|
||||
import LinkBlock from '@/components/content-blocks/LinkBlock';
|
||||
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';
|
||||
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
'video_block': VideoBlock,
|
||||
'link_block': LinkBlock,
|
||||
'document_block': DocumentBlock,
|
||||
UserWidget
|
||||
UserMetaWidget
|
||||
},
|
||||
|
||||
apollo: {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
return this.rooms.filter(room => this.visibleFor(room, this.currentFilter));
|
||||
},
|
||||
currentFilter() {
|
||||
return this.$store.state.filterForSchoolClass;
|
||||
return this.me.selectedClass.id;
|
||||
},
|
||||
canAddRoom() {
|
||||
return this.me.permissions.includes('users.can_manage_school_class_content')
|
||||
|
|
@ -53,6 +53,9 @@
|
|||
return {
|
||||
rooms: [],
|
||||
me: {
|
||||
selectedClass: {
|
||||
id: ''
|
||||
},
|
||||
permissions: []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ export default new Vuex.Store({
|
|||
showMobileNavigation: false,
|
||||
contentBlockPosition: {},
|
||||
scrollPosition: 0,
|
||||
filterForSchoolClass: '',
|
||||
currentContentBlock: '',
|
||||
currentRoomEntry: '',
|
||||
parentRoom: null,
|
||||
|
|
@ -108,9 +107,6 @@ export default new Vuex.Store({
|
|||
document.body.classList.add('no-scroll'); // won't get at the body any other way
|
||||
commit('setModal', payload);
|
||||
},
|
||||
setfilterForSchoolClass({commit}, payload) {
|
||||
commit('setfilterForSchoolClass', payload);
|
||||
},
|
||||
addProjectEntry({commit, dispatch}, payload) {
|
||||
commit('setParentProject', payload);
|
||||
dispatch('showModal', 'new-project-entry-wizard');
|
||||
|
|
@ -174,9 +170,6 @@ export default new Vuex.Store({
|
|||
setCurrentContentBlock(state, payload) {
|
||||
state.currentContentBlock = payload;
|
||||
},
|
||||
setfilterForSchoolClass(state, payload) {
|
||||
state.filterForSchoolClass = payload;
|
||||
},
|
||||
setParentRoom(state, payload) {
|
||||
state.parentRoom = payload;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -142,3 +142,7 @@
|
|||
@mixin light-border($border-position) {
|
||||
border-#{$border-position}: 1px solid $color-silver;
|
||||
}
|
||||
|
||||
@mixin popover-defaults() {
|
||||
bottom: $popover-default-bottom;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,3 +77,6 @@ $font-weight-semibold: 600;
|
|||
$font-weight-regular: 400;
|
||||
|
||||
$default-line-height: 1.5;
|
||||
|
||||
// popover
|
||||
$popover-default-bottom: -110px;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import io
|
||||
import os
|
||||
|
||||
from django.apps import apps
|
||||
from graphene_django.filter import DjangoFilterConnectionField
|
||||
from graphql_relay.node.node import from_global_id
|
||||
|
||||
"""Script defined to create helper functions for graphql schema."""
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import graphene
|
||||
from django.core.exceptions import PermissionDenied
|
||||
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 rooms.inputs import UpdateRoomArgument, AddRoomArgument, AddRoomEntryArgument, UpdateRoomEntryArgument
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from django.contrib import admin
|
|||
from django.contrib.auth.admin import UserAdmin
|
||||
|
||||
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):
|
||||
|
|
@ -53,3 +53,9 @@ class CustomUserAdmin(UserAdmin):
|
|||
|
||||
|
||||
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.models import AbstractUser, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
|
@ -38,6 +37,18 @@ class User(AbstractUser):
|
|||
def users_in_same_school_class(self):
|
||||
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
|
||||
def full_name(self):
|
||||
return self.get_full_name()
|
||||
|
|
@ -115,3 +126,9 @@ class UserRole(models.Model):
|
|||
|
||||
def __str__(self):
|
||||
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
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from graphene import relay
|
||||
|
||||
from api.utils import get_object
|
||||
from users.inputs import PasswordUpdateInput
|
||||
from users.models import SchoolClass, UserSetting
|
||||
from users.serializers import PasswordSerialzer, AvatarUrlSerializer
|
||||
|
||||
|
||||
|
|
@ -76,7 +80,30 @@ class UpdateAvatar(relay.ClientIDMutation):
|
|||
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:
|
||||
update_password = UpdatePassword.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
|
||||
|
||||
|
||||
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):
|
||||
pk = graphene.Int()
|
||||
|
||||
|
|
@ -35,6 +18,28 @@ class SchoolClassNode(DjangoObjectType):
|
|||
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):
|
||||
me = graphene.Field(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