Use cache to propagate changes, add tests, style popover

This commit is contained in:
Christian Cueni 2019-07-25 11:12:23 +02:00
parent 0af01b4a48
commit 638bea0cd0
11 changed files with 133 additions and 53 deletions

View File

@ -1,15 +1,16 @@
<template> <template>
<div class="class-selection" v-if="isTeacher"> <div class="class-selection" v-if="isTeacher">
<div class="class-selection__selected-class" @click="showPopover = !showPopover">{{currentClassSelection}} <div class="class-selection__selected-class selected-class" @click="showPopover = !showPopover">
<p class="selected-class__text">Klasse: {{currentClassSelection.name}}</p>
</div> </div>
<widget-popover v-if="showPopover" <widget-popover v-if="showPopover"
@hide-me="showPopover = false" @hide-me="showPopover = false"
class="user-widget__popover"> class="class-selection__popover">
<li class="popover-links__link" v-for="schoolClass in schoolClasses" <li class="popover-links__link popover-links__link--large" v-for="schoolClass in schoolClasses"
:key="schoolClass.id" :key="schoolClass.id"
:label="schoolClass.name" :label="schoolClass.name"
:item="schoolClass" :item="schoolClass"
@click="updateFilter(schoolClass.id)"> @click="updateFilter(schoolClass)">
{{schoolClass.name}} {{schoolClass.name}}
</li> </li>
</widget-popover> </widget-popover>
@ -17,9 +18,9 @@
</template> </template>
<script> <script>
import {mapActions} from 'vuex';
import WidgetPopover from '@/components/WidgetPopover'; import WidgetPopover from '@/components/WidgetPopover';
import ME_QUERY from '@/graphql/gql/meQuery.gql'; import ME_QUERY from '@/graphql/gql/meQuery.gql';
import UPDATE_USER_SETTING from '@/graphql/gql/mutations/updateUserSetting.gql';
export default { export default {
components: { components: {
@ -35,18 +36,42 @@
data() { data() {
return { return {
me: { me: {
selectedClass: {
id: ''
},
permissions: [] permissions: []
}, },
showPopover: false 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: { computed: {
currentClassSelection() { currentClassSelection() {
let currentClass = this.schoolClasses.find(schoolClass => { let currentClass = this.schoolClasses.find(schoolClass => {
return schoolClass.id === this.$store.state.filterForSchoolClass; return schoolClass.id === this.me.selectedClass.id
}) })
return currentClass ? currentClass.name : 'Alle'; return currentClass || this.schoolClasses[0];
}, },
schoolClasses() { schoolClasses() {
return this.$getRidOfEdges(this.me.schoolClasses); return this.$getRidOfEdges(this.me.schoolClasses);
@ -56,29 +81,35 @@
} }
}, },
methods: {
...mapActions({
updateFilter: 'setfilterForSchoolClass'
})
}
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.filter-bar { .class-selection {
position: sticky;
top: -1px; position: relative;
z-index: 9; cursor: pointer;
padding: 0 24px; margin-right: $large-spacing;
height: 50px;
background-color: $color-silver-light; &__popover {
display: flex; white-space: nowrap;
align-items: center; top: 40px;
justify-items: left; }
border: 1px solid rgba(228, 228, 228, 0.9);
border-left: 0;
border-right: 0;
} }
.selected-class {
&__text {
line-height: $large-spacing;
@include regular-text;
color: $color-silver-dark;
}
}
.popover-links__link {
cursor: pointer;
}
</style> </style>

View File

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

View File

@ -3,22 +3,21 @@
<div class="user-widget__avatar" @click="toggleShowPopover()"> <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>
<span class="user-widget__date" v-if="date">{{date}}</span-->
<widget-popover v-if="showPopover" <widget-popover v-if="showPopover"
@hide-me="showPopover = false" @hide-me="showPopover = false"
class="user-widget__popover"> class="user-widget__popover ">
<li class="popover-links__link">{{firstName}} {{lastName}}</li> <li class="popover-links__link popover-links__link--large popover-links__link--emph">{{firstName}} {{lastName}}</li>
<li class="popover-links__link"> <li class="popover-links__link popover-links__link--large">
<router-link to="/me/activity">Aktivität</router-link> <router-link to="/me/activity" @click="toggleShowPopover()">Aktivität</router-link>
</li> </li>
<li class="popover-links__link"> <li class="popover-links__link popover-links__link--large" @click="toggleShowPopover()">
<router-link to="/me/profile">Profil</router-link> <router-link to="/me/profile">Profil</router-link>
</li> </li>
<li class="popover-links__link"> <li class="popover-links__link popover-links__link--large" @click="toggleShowPopover()">
<router-link to="/me/myclasses">Klassenliste</router-link> <router-link to="/me/myclasses">Klassenliste</router-link>
</li> </li>
<li class="popover-links__link" data-cy="logout" @click="logout()"><a>Logout</a> <li class="popover-links__link popover-links__link--large" data-cy="logout" @click="logout()">
<a>Logout</a>
</li> </li>
</widget-popover> </widget-popover>
</div> </div>
@ -64,6 +63,7 @@
<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;
@ -75,10 +75,11 @@
&__popover { &__popover {
top: 40px; 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;
} }

View File

@ -47,6 +47,17 @@
padding: 5px 0; padding: 5px 0;
cursor: pointer; cursor: pointer;
} }
&--large {
line-height: 40px;
& > a, & {
@include small-text;
}
}
&--emph {
@include regular-text;
font-weight: 600;
}
} }
} }
</style> </style>

View File

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

View File

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

View File

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

View File

@ -12,7 +12,6 @@ export default new Vuex.Store({
showMobileNavigation: false, showMobileNavigation: false,
contentBlockPosition: {}, contentBlockPosition: {},
scrollPosition: 0, scrollPosition: 0,
filterForSchoolClass: '',
currentContentBlock: '', currentContentBlock: '',
currentRoomEntry: '', currentRoomEntry: '',
parentRoom: null, parentRoom: null,
@ -108,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');
@ -174,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;
}, },

View File

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

View File

@ -5,7 +5,7 @@ from graphene import relay
from api.utils import get_object from api.utils import get_object
from users.inputs import PasswordUpdateInput from users.inputs import PasswordUpdateInput
from users.models import SchoolClass from users.models import SchoolClass, UserSetting
from users.serializers import PasswordSerialzer, AvatarUrlSerializer from users.serializers import PasswordSerialzer, AvatarUrlSerializer
@ -89,11 +89,13 @@ class UpdateSetting(relay.ClientIDMutation):
class_id = kwargs.get('id') class_id = kwargs.get('id')
school_class = get_object(SchoolClass, class_id) school_class = get_object(SchoolClass, class_id)
user = info.context.user user = info.context.user
if school_class not in user.school_classes.all():
if school_class and school_class not in user.school_classes.all():
raise PermissionDenied('Permission denied: Incorrect school class') raise PermissionDenied('Permission denied: Incorrect school class')
user.user_setting.selected_class = school_class user_settings, created = UserSetting.objects.get_or_create(user=user)
user.user_setting.save() user_settings.selected_class = school_class
user_settings.save()
return cls(success=True) return cls(success=True)
success = graphene.Boolean() success = graphene.Boolean()

View File

@ -94,3 +94,25 @@ class UserSettingTests(TestCase):
self.assertIsNone(query_result.get('errors')) self.assertIsNone(query_result.get('errors'))
self.assertEqual(query_result.get('data').get('me').get('selectedClass').get('name'), self.assertEqual(query_result.get('data').get('me').get('selectedClass').get('name'),
selected_class.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'))