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>
<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>
<widget-popover v-if="showPopover"
@hide-me="showPopover = false"
class="user-widget__popover">
<li class="popover-links__link" v-for="schoolClass in schoolClasses"
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.id)">
@click="updateFilter(schoolClass)">
{{schoolClass.name}}
</li>
</widget-popover>
@ -17,9 +18,9 @@
</template>
<script>
import {mapActions} from 'vuex';
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: {
@ -35,18 +36,42 @@
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.$store.state.filterForSchoolClass;
return schoolClass.id === this.me.selectedClass.id
})
return currentClass ? currentClass.name : 'Alle';
return currentClass || this.schoolClasses[0];
},
schoolClasses() {
return this.$getRidOfEdges(this.me.schoolClasses);
@ -56,29 +81,35 @@
}
},
methods: {
...mapActions({
updateFilter: 'setfilterForSchoolClass'
})
}
}
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.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;
.class-selection {
position: relative;
cursor: pointer;
margin-right: $large-spacing;
&__popover {
white-space: nowrap;
top: 40px;
}
}
.selected-class {
&__text {
line-height: $large-spacing;
@include regular-text;
color: $color-silver-dark;
}
}
.popover-links__link {
cursor: pointer;
}
</style>

View File

@ -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 />
<user-widget class="mobile-navigation__user-widget" v-bind="me"></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: {

View File

@ -3,22 +3,21 @@
<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"
class="user-widget__popover">
<li class="popover-links__link">{{firstName}} {{lastName}}</li>
<li class="popover-links__link">
<router-link to="/me/activity">Aktivität</router-link>
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">
<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">
<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" 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>
</widget-popover>
</div>
@ -64,6 +63,7 @@
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.user-widget {
color: $color-silver-dark;
@ -75,10 +75,11 @@
&__popover {
top: 40px;
white-space: nowrap;
}
&__name {
padding: 0px 10px;
padding: 0px $small-spacing;
color: $color-silver-dark;
font-family: $sans-serif-font-family;
}

View File

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

View File

@ -11,6 +11,9 @@ fragment UserParts on UserNode {
id
slug
}
selectedClass {
id
}
schoolClasses {
edges {
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));
},
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: []
}
}

View File

@ -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;
},

View File

@ -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')

View File

@ -5,7 +5,7 @@ from graphene import relay
from api.utils import get_object
from users.inputs import PasswordUpdateInput
from users.models import SchoolClass
from users.models import SchoolClass, UserSetting
from users.serializers import PasswordSerialzer, AvatarUrlSerializer
@ -89,11 +89,13 @@ class UpdateSetting(relay.ClientIDMutation):
class_id = kwargs.get('id')
school_class = get_object(SchoolClass, class_id)
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')
user.user_setting.selected_class = school_class
user.user_setting.save()
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()

View File

@ -94,3 +94,25 @@ class UserSettingTests(TestCase):
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'))