Merged in feature/active-and-inactive-users-in-class (pull request #49)
Feature/active and inactive users in class Approved-by: Christian Cueni
This commit is contained in:
commit
1da6a00d40
|
|
@ -7,6 +7,7 @@
|
|||
"firstName": "Rahel",
|
||||
"lastName": "Cueni",
|
||||
"avatarUrl": "",
|
||||
"isTeacher": false,
|
||||
"lastModule": {
|
||||
"id": "TW9kdWxlTm9kZToxNw==",
|
||||
"slug": "lohn-und-budget",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"me": {
|
||||
"id": "VXNlck5vZGU6Mg==",
|
||||
"isTeacher": false,
|
||||
"selectedClass": {
|
||||
"id": "U2Nob29sQ2xhc3NOb2RlOjE=",
|
||||
"name": "Moordale",
|
||||
"members": [
|
||||
{
|
||||
"id": "VXNlck5vZGU6Mw==",
|
||||
"firstName": "Otis",
|
||||
"lastName": "Milburn",
|
||||
"isTeacher": false,
|
||||
"active": true,
|
||||
"__typename": "ClassMemberNode"
|
||||
},
|
||||
{
|
||||
"id": "VXNlck5vZGU6NA==",
|
||||
"firstName": "Maeve",
|
||||
"lastName": "Wiley",
|
||||
"isTeacher": false,
|
||||
"active": true,
|
||||
"__typename": "ClassMemberNode"
|
||||
}
|
||||
],
|
||||
"__typename": "SchoolClassNode"
|
||||
},
|
||||
"__typename": "UserNode"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
const schema = require('../fixtures/schema.json');
|
||||
const me = require('../fixtures/me.join-class.json');
|
||||
const selectedClass = require('../fixtures/selected-school-class.json');
|
||||
|
||||
describe('Join Class', () => {
|
||||
describe('Class Management', () => {
|
||||
beforeEach(() => {
|
||||
cy.server();
|
||||
|
||||
|
|
@ -31,14 +32,15 @@ describe('Join Class', () => {
|
|||
}
|
||||
});
|
||||
|
||||
|
||||
cy.visit('/me/profile');
|
||||
|
||||
cy.get('[data-cy=header-user-widget]').within(() => {
|
||||
cy.get('[data-cy=user-widget-avatar]').click();
|
||||
});
|
||||
|
||||
cy.get('[data-cy=class-selection]').click();
|
||||
cy.get('[data-cy=class-selection-entry]').should('have.length', 1);
|
||||
cy.get('[data-cy=class-selection]').click();
|
||||
|
||||
cy.get('[data-cy=join-class-link]').click();
|
||||
|
||||
|
|
@ -51,5 +53,94 @@ describe('Join Class', () => {
|
|||
|
||||
cy.get('[data-cy=class-selection]').click();
|
||||
cy.get('[data-cy=class-selection-entry]').should('have.length', 2);
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
it('should not be able to leave class', () => {
|
||||
cy.mockGraphqlOps({
|
||||
operations: {
|
||||
MeQuery: me,
|
||||
MySchoolClassQuery: selectedClass
|
||||
}
|
||||
});
|
||||
|
||||
cy.visit('/me/my-class');
|
||||
cy.get('[data-cy=school-class-member]').should('have.length', 2);
|
||||
cy.get('[data-cy=remove-from-class]').should('have.length', 0);
|
||||
cy.get('[data-cy=add-to-class]').should('have.length', 0);
|
||||
});
|
||||
|
||||
it('should leave and re-join class', () => {
|
||||
const teacher = {
|
||||
me: {
|
||||
...me.me,
|
||||
isTeacher: true
|
||||
}
|
||||
};
|
||||
const teacherSelectedClass = {
|
||||
me: {
|
||||
...selectedClass.me,
|
||||
isTeacher: true
|
||||
}
|
||||
};
|
||||
cy.mockGraphqlOps({
|
||||
operations: {
|
||||
MeQuery: teacher,
|
||||
AddRemoveMember: {
|
||||
addRemoveMember: {
|
||||
success: true
|
||||
}
|
||||
},
|
||||
MySchoolClassQuery: teacherSelectedClass
|
||||
}
|
||||
});
|
||||
|
||||
cy.visit('/me/my-class');
|
||||
cy.get('[data-cy=active-class-members-list]').within(() => {
|
||||
cy.get('[data-cy=school-class-member]').should('have.length', 2)
|
||||
});
|
||||
cy.get('[data-cy=inactive-class-members-list]').within(() => {
|
||||
cy.get('[data-cy=school-class-member]').should('have.length', 0)
|
||||
});
|
||||
|
||||
cy.get('[data-cy=remove-from-class]').first().click();
|
||||
|
||||
cy.get('[data-cy=active-class-members-list]').within(() => {
|
||||
cy.get('[data-cy=school-class-member]').should('have.length', 1)
|
||||
});
|
||||
cy.get('[data-cy=inactive-class-members-list]').within(() => {
|
||||
cy.get('[data-cy=school-class-member]').should('have.length', 1)
|
||||
});
|
||||
|
||||
cy.get('[data-cy=add-to-class]').first().click();
|
||||
|
||||
cy.get('[data-cy=active-class-members-list]').within(() => {
|
||||
cy.get('[data-cy=school-class-member]').should('have.length', 2)
|
||||
});
|
||||
cy.get('[data-cy=inactive-class-members-list]').within(() => {
|
||||
cy.get('[data-cy=school-class-member]').should('have.length', 0)
|
||||
});
|
||||
});
|
||||
|
||||
it.only('should display old classes', () => {
|
||||
let oldClasses = me.me.schoolClasses;
|
||||
let OldClassesQuery = {
|
||||
me: {
|
||||
...me.me,
|
||||
oldClasses
|
||||
},
|
||||
};
|
||||
|
||||
debugger;
|
||||
cy.mockGraphqlOps({
|
||||
operations: {
|
||||
MeQuery: me,
|
||||
OldClassesQuery
|
||||
}
|
||||
});
|
||||
|
||||
cy.visit('/me/old-classes');
|
||||
|
||||
cy.get('[data-cy=old-class-item]').should('have.length', 1);
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: $color-white;
|
||||
padding: 20px;
|
||||
padding: 0;
|
||||
z-index: 100;
|
||||
@include widget-shadow;
|
||||
|
||||
|
|
@ -43,6 +43,9 @@
|
|||
display: grid;
|
||||
|
||||
&__link {
|
||||
cursor: pointer;
|
||||
padding: 0 $medium-spacing;
|
||||
|
||||
& > a {
|
||||
display: inline-block;
|
||||
color: $color-silver-dark;
|
||||
|
|
@ -55,6 +58,7 @@
|
|||
|
||||
&--large {
|
||||
line-height: 40px;
|
||||
padding: $small-spacing $medium-spacing;
|
||||
& > a, & {
|
||||
@include small-text;
|
||||
}
|
||||
|
|
@ -64,5 +68,9 @@
|
|||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&__divider {
|
||||
border-top: 1px solid $color-silver-dark;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
<template>
|
||||
<div class="school-class">
|
||||
<h2 class="school-class__name">{{name}}</h2>
|
||||
<div class="school-class__members school-class-members">
|
||||
<ul class="school-class-members__list simple-list simple-list--active" data-cy="active-class-members-list">
|
||||
<li
|
||||
class="simple-list__item member-item"
|
||||
data-cy="school-class-member"
|
||||
v-for="member in activeMembers"
|
||||
:key="member.id">
|
||||
<span class="member-item__name">{{fullName(member)}}</span>
|
||||
<span class="member-item__role">{{role(member)}}</span>
|
||||
<a
|
||||
class="member-item__action simple-list__action"
|
||||
data-cy="remove-from-class"
|
||||
v-if="teacher"
|
||||
@click="$emit('remove', member)">Deaktivieren</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h3 class="school-class__inactive-heading">Deaktivierte Benutzer</h3>
|
||||
<ul data-cy="inactive-class-members-list" class="simple-list simple-list--inactive">
|
||||
<li
|
||||
class="simple-list__item member-item"
|
||||
data-cy="school-class-member"
|
||||
v-for="member in inactiveMembers"
|
||||
:key="member.id">
|
||||
<span class="member-item__name">{{fullName(member)}}</span>
|
||||
<span class="member-item__role">{{role(member)}}</span>
|
||||
<a
|
||||
class="member-item__action simple-list__action"
|
||||
data-cy="add-to-class"
|
||||
v-if="teacher"
|
||||
@click="$emit('add', member)">Aktivieren</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['members', 'name', 'teacher'],
|
||||
methods: {
|
||||
fullName(member) {
|
||||
return `${member.firstName} ${member.lastName}`;
|
||||
},
|
||||
role({isTeacher}) {
|
||||
return isTeacher ? 'Lehrperson' : 'Schüler';
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
activeMembers() {
|
||||
return this.members.filter(member => member.active)
|
||||
},
|
||||
inactiveMembers() {
|
||||
return this.members.filter(member => !member.active)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/styles/_variables.scss";
|
||||
@import "@/styles/_mixins.scss";
|
||||
|
||||
.school-class {
|
||||
&__inactive-heading {
|
||||
@include heading-4;
|
||||
margin-bottom: $small-spacing;
|
||||
}
|
||||
}
|
||||
|
||||
.member-item {
|
||||
&__name {
|
||||
font-family: $sans-serif-font-family;
|
||||
font-weight: $font-weight-bold;
|
||||
flex: 2 1 auto;
|
||||
}
|
||||
|
||||
&__role {
|
||||
flex: 0 1 110px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&__action {
|
||||
flex: 0 1 110px;
|
||||
padding-left: $large-spacing;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
<template>
|
||||
<div class="schoolclass">
|
||||
<h2 class="schoolclass__name">{{name}}</h2>
|
||||
<div class="schoolclass__members schoolclass-members">
|
||||
<ul class="schoolclass-members__list members-list">
|
||||
<li v-for="user in users" :key="user.id" class="members-list__item">
|
||||
<p class="member-item"><span class="member-item__name">{{fullName(user)}}</span> <span class="member-item__role">{{role(user)}}</span></p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['users', 'name'],
|
||||
methods: {
|
||||
fullName (user) {
|
||||
return `${user.firstName} ${user.lastName}`;
|
||||
},
|
||||
role ({permissions}) {
|
||||
return permissions.indexOf('users.can_manage_school_class_content') > -1 ? 'Lehrperson' : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/styles/_variables.scss";
|
||||
|
||||
$height: 52px;
|
||||
|
||||
.members-list {
|
||||
&__item {
|
||||
line-height: $height;
|
||||
height: $height;
|
||||
border-bottom: 1px solid $color-silver-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.member-item {
|
||||
|
||||
line-height: $height;
|
||||
height: $height;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
&__name {
|
||||
font-family: $sans-serif-font-family;
|
||||
font-weight: $font-weight-bold;
|
||||
}
|
||||
|
||||
&__role {
|
||||
padding-right: $medium-spacing;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -2,7 +2,8 @@
|
|||
<div class="class-selection" v-if="currentClassSelection">
|
||||
<div data-cy="class-selection" class="class-selection__selected-class selected-class"
|
||||
@click="showPopover = !showPopover">
|
||||
<current-class class="selected-class__text"></current-class> <chevron-down class="selected-class__dropdown-icon"></chevron-down>
|
||||
<current-class class="selected-class__text"></current-class>
|
||||
<chevron-down class="selected-class__dropdown-icon"></chevron-down>
|
||||
</div>
|
||||
<widget-popover v-if="showPopover"
|
||||
@hide-me="showPopover = false"
|
||||
|
|
@ -13,9 +14,13 @@
|
|||
:key="schoolClass.id"
|
||||
:label="schoolClass.name"
|
||||
:item="schoolClass"
|
||||
@click="updateFilter(schoolClass)">
|
||||
@click="updateSelectedClass(schoolClass)">
|
||||
{{schoolClass.name}}
|
||||
</li>
|
||||
<li class="popover-links__link popover-links__link--large popover-links__divider">Klasse erfassen</li>
|
||||
<li class="popover-links__link popover-links__link--large popover-links__divider">
|
||||
<router-link :to="{name: 'old-classes'}">Alte Klassen anzeigen</router-link>
|
||||
</li>
|
||||
</widget-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -24,8 +29,10 @@
|
|||
import WidgetPopover from '@/components/WidgetPopover';
|
||||
import ChevronDown from '@/components/icons/ChevronDown';
|
||||
import CurrentClass from '@/components/school-class/CurrentClass';
|
||||
|
||||
import ME_QUERY from '@/graphql/gql/meQuery.gql';
|
||||
import UPDATE_USER_SETTING from '@/graphql/gql/mutations/updateUserSetting.gql';
|
||||
|
||||
import updateSelectedClassMixin from '@/mixins/updateSelectedClass';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
@ -41,6 +48,8 @@
|
|||
}
|
||||
},
|
||||
|
||||
mixins: [updateSelectedClassMixin],
|
||||
|
||||
apollo: {
|
||||
me: {
|
||||
query: ME_QUERY,
|
||||
|
|
@ -67,22 +76,8 @@
|
|||
},
|
||||
|
||||
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)
|
||||
});
|
||||
updateSelectedClassAndHidePopover(selectedClass) {
|
||||
this.updateSelectedClass(selectedClass);
|
||||
this.showPopover = false;
|
||||
}
|
||||
},
|
||||
|
|
@ -139,9 +134,4 @@
|
|||
fill: $color-brand;
|
||||
}
|
||||
}
|
||||
|
||||
.popover-links__link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
mutation AddRemoveMember($input: AddRemoveMemberInput!) {
|
||||
addRemoveMember(input: $input) {
|
||||
success
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +1,16 @@
|
|||
query {
|
||||
query MySchoolClassQuery {
|
||||
me {
|
||||
id
|
||||
isTeacher
|
||||
selectedClass {
|
||||
id
|
||||
name
|
||||
users {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
permissions
|
||||
}
|
||||
}
|
||||
members {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
isTeacher
|
||||
active
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
query OldClassesQuery {
|
||||
me {
|
||||
id
|
||||
oldClasses {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import ME_QUERY from '@/graphql/gql/meQuery.gql';
|
||||
import UPDATE_USER_SETTING from '@/graphql/gql/mutations/updateUserSetting.gql';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
updateSelectedClass(selectedClass) {
|
||||
return 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.warn('failed to update selected class', error)
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
@ -1,33 +1,79 @@
|
|||
<template>
|
||||
<div class="my-class">
|
||||
<h1 class="my-class__header" data-cy="class-list-title">Klassenliste</h1>
|
||||
<classlist v-bind="selectedClass" class="my-class__class"></classlist>
|
||||
<class-list
|
||||
class="my-class__class"
|
||||
:name="me.selectedClass.name"
|
||||
:members="me.selectedClass.members"
|
||||
:teacher="me.isTeacher"
|
||||
@remove="remove"
|
||||
@add="add"
|
||||
></class-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClassList from '@/components/profile/ClassList';
|
||||
|
||||
import MY_SCHOOL_CLASS_QUERY from '@/graphql/gql/mySchoolClass.gql';
|
||||
import Classlist from '@/components/profile/Classlist';
|
||||
import ADD_REMOVE_MEMBER_MUTATION from '@/graphql/gql/mutations/addRemoveMember.gql';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Classlist
|
||||
ClassList
|
||||
},
|
||||
|
||||
methods: {
|
||||
changeMember(member, active) {
|
||||
this.$apollo.mutate({
|
||||
mutation: ADD_REMOVE_MEMBER_MUTATION,
|
||||
variables: {
|
||||
input: {
|
||||
member: member.id,
|
||||
schoolClass: this.me.selectedClass.id,
|
||||
active
|
||||
}
|
||||
},
|
||||
update(store, {data: {addRemoveMember: {success}}}) {
|
||||
if (success) {
|
||||
const query = MY_SCHOOL_CLASS_QUERY;
|
||||
const data = store.readQuery({query});
|
||||
let memberIndex = data.me.selectedClass.members.findIndex(m => m.id === member.id);
|
||||
data.me.selectedClass.members = [
|
||||
...data.me.selectedClass.members.slice(0, memberIndex),
|
||||
{...member, active},
|
||||
...data.me.selectedClass.members.slice(memberIndex),
|
||||
];
|
||||
store.writeQuery({query, data});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
add(member) {
|
||||
this.changeMember(member, true);
|
||||
},
|
||||
remove(member) {
|
||||
this.changeMember(member, false);
|
||||
}
|
||||
},
|
||||
|
||||
apollo: {
|
||||
selectedClass: {
|
||||
me: {
|
||||
query: MY_SCHOOL_CLASS_QUERY,
|
||||
update(data) {
|
||||
return this.$getRidOfEdges(data).me.selectedClass
|
||||
return this.$getRidOfEdges(data).me
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
selectedClass: {
|
||||
name: ''
|
||||
me: {
|
||||
isTeacher: false,
|
||||
selectedClass: {
|
||||
name: '',
|
||||
members: []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
<template>
|
||||
<div class="old-classes">
|
||||
<h1 class="old-classes__title">Alte Klassen</h1>
|
||||
<ul class="old-classes__list simple-list">
|
||||
<li class="simple-list__item" v-for="schoolClass in me.oldClasses" :key="schoolClass.id" data-cy="old-class-item"><span
|
||||
class="old-classes__class-name">{{schoolClass.name}}</span> <a
|
||||
class="simple-list__action" @click="updateSelectedClassAndGoToClassList(schoolClass)">Anzeigen</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import OLD_CLASSES_QUERY from '@/graphql/gql/oldClasses.gql';
|
||||
|
||||
import updateSelectedClassMixin from '@/mixins/updateSelectedClass';
|
||||
|
||||
export default {
|
||||
mixins: [updateSelectedClassMixin],
|
||||
|
||||
methods: {
|
||||
updateSelectedClassAndGoToClassList(selectedClass) {
|
||||
this.updateSelectedClass(selectedClass).then(() => {
|
||||
this.$router.push({name: 'my-class'});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// todo: test this in front- and backend
|
||||
apollo: {
|
||||
me: {
|
||||
query: OLD_CLASSES_QUERY,
|
||||
update(data) {
|
||||
return this.$getRidOfEdges(data).me;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
me: {
|
||||
oldClasses: []
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/styles/_variables.scss";
|
||||
|
||||
.old-classes {
|
||||
&__class-name {
|
||||
font-family: $sans-serif-font-family;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -31,6 +31,7 @@ import login from '@/pages/login'
|
|||
import registration from '@/pages/registration'
|
||||
import waitForClass from '@/pages/waitForClass'
|
||||
import joinClass from '@/pages/joinClass'
|
||||
import oldClasses from '@/pages/oldClasses';
|
||||
|
||||
import store from '@/store/index';
|
||||
|
||||
|
|
@ -111,6 +112,13 @@ const routes = [
|
|||
{path: 'my-class', name: 'my-class', component: myClass, meta: {isProfile: true}},
|
||||
{path: 'activity', name: 'activity', component: activity, meta: {isProfile: true}},
|
||||
{path: '', name: 'profile-activity', component: activity, meta: {isProfile: true}},
|
||||
{
|
||||
path: 'old-classes',
|
||||
name: 'old-classes',
|
||||
component: oldClasses,
|
||||
meta: {isProfile: true}
|
||||
},
|
||||
|
||||
]
|
||||
},
|
||||
{path: 'join-class', name: 'join-class', component: joinClass, meta: {layout: 'simple'}},
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@
|
|||
font-family: $sans-serif-font-family;
|
||||
font-weight: $font-weight-regular;
|
||||
color: $color-silver-dark;
|
||||
cursor: pointer;
|
||||
|
||||
&--active {
|
||||
color: $color-brand;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
@import "variables";
|
||||
|
||||
.simple-list {
|
||||
border-top: 1px solid $color-silver-dark;
|
||||
|
||||
&--active {
|
||||
margin-bottom: 2*$large-spacing;
|
||||
}
|
||||
|
||||
&__item {
|
||||
line-height: $list-height;
|
||||
height: $list-height;
|
||||
border-bottom: 1px solid $color-silver-dark;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__action {
|
||||
@include default-link;
|
||||
color: $color-brand;
|
||||
}
|
||||
}
|
||||
|
|
@ -56,6 +56,9 @@ $red: #FA5F5F;
|
|||
$green: #6DD79A;
|
||||
$brown: #EB9E77;
|
||||
|
||||
$list-height: 52px;
|
||||
|
||||
|
||||
|
||||
$default-border-radius: 13px;
|
||||
$input-border-radius: 3px;
|
||||
|
|
|
|||
|
|
@ -23,3 +23,4 @@
|
|||
@import "student-submission";
|
||||
@import "module-activity";
|
||||
@import "book-subnavigation";
|
||||
@import "simple-list";
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from graphql_relay import to_global_id
|
|||
from api.test_utils import create_client, DefaultUserTestCase
|
||||
from assignments.models import Assignment, StudentSubmission
|
||||
from users.factories import SchoolClassFactory
|
||||
from users.models import SchoolClassMember
|
||||
from ..factories import AssignmentFactory, StudentSubmissionFactory, SubmissionFeedbackFactory
|
||||
|
||||
|
||||
|
|
@ -25,13 +26,16 @@ class SubmissionFeedbackTestCase(DefaultUserTestCase):
|
|||
)
|
||||
self.assignment_id = to_global_id('AssignmentNode', self.assignment.pk)
|
||||
|
||||
self.student_submission = StudentSubmissionFactory(assignment=self.assignment, student=self.student1, final=False)
|
||||
self.student_submission = StudentSubmissionFactory(assignment=self.assignment, student=self.student1,
|
||||
final=False)
|
||||
self.student_submission_id = to_global_id('StudentSubmissionNode', self.student_submission.pk)
|
||||
|
||||
school_class = SchoolClassFactory()
|
||||
school_class.users.add(self.student1)
|
||||
school_class.users.add(self.teacher)
|
||||
school_class.users.add(self.teacher2)
|
||||
for user in [self.student1, self.teacher, self.teacher2]:
|
||||
SchoolClassMember.objects.create(
|
||||
user=user,
|
||||
school_class=school_class
|
||||
)
|
||||
|
||||
def _create_submission_feedback(self, user, final, text, student_submission_id):
|
||||
mutation = '''
|
||||
|
|
@ -122,19 +126,17 @@ class SubmissionFeedbackTestCase(DefaultUserTestCase):
|
|||
})
|
||||
|
||||
def test_teacher_can_create_feedback(self):
|
||||
|
||||
result = self._create_submission_feedback(self.teacher, False, 'Balalal', self.student_submission_id)
|
||||
|
||||
self.assertIsNone(result.get('errors'))
|
||||
self.assertIsNotNone(result.get('data').get('updateSubmissionFeedback').get('updatedSubmissionFeedback').get('id'))
|
||||
self.assertIsNotNone(
|
||||
result.get('data').get('updateSubmissionFeedback').get('updatedSubmissionFeedback').get('id'))
|
||||
|
||||
def test_student_cannot_create_feedback(self):
|
||||
|
||||
result = self._create_submission_feedback(self.student1, False, 'Balalal', self.student_submission_id)
|
||||
self.assertIsNotNone(result.get('errors'))
|
||||
|
||||
def test_teacher_can_update_feedback(self):
|
||||
|
||||
assignment = AssignmentFactory(
|
||||
owner=self.teacher
|
||||
)
|
||||
|
|
@ -148,13 +150,13 @@ class SubmissionFeedbackTestCase(DefaultUserTestCase):
|
|||
|
||||
self.assertIsNone(result.get('errors'))
|
||||
|
||||
submission_feedback_response = result.get('data').get('updateSubmissionFeedback').get('updatedSubmissionFeedback')
|
||||
submission_feedback_response = result.get('data').get('updateSubmissionFeedback').get(
|
||||
'updatedSubmissionFeedback')
|
||||
|
||||
self.assertTrue(submission_feedback_response.get('final'))
|
||||
self.assertEqual(submission_feedback_response.get('text'), 'Some')
|
||||
|
||||
def test_rogue_teacher_cannot_update_feedback(self):
|
||||
|
||||
assignment = AssignmentFactory(
|
||||
owner=self.teacher
|
||||
)
|
||||
|
|
@ -169,14 +171,12 @@ class SubmissionFeedbackTestCase(DefaultUserTestCase):
|
|||
self.assertIsNotNone(result.get('errors'))
|
||||
|
||||
def test_student_does_not_see_non_final_feedback(self):
|
||||
|
||||
SubmissionFeedbackFactory(teacher=self.teacher, final=False, student_submission=self.student_submission)
|
||||
result = self._fetch_assignment_student(self.student1)
|
||||
|
||||
self.assertIsNone(result.get('data').get('submissionFeedback'))
|
||||
|
||||
def test_student_does_see_final_feedback(self):
|
||||
|
||||
submission_feedback = SubmissionFeedbackFactory(teacher=self.teacher, final=True,
|
||||
student_submission=self.student_submission)
|
||||
result = self._fetch_assignment_student(self.student1)
|
||||
|
|
@ -195,7 +195,7 @@ class SubmissionFeedbackTestCase(DefaultUserTestCase):
|
|||
|
||||
def test_rogue_teacher_cannot_see_feedback(self):
|
||||
SubmissionFeedbackFactory(teacher=self.teacher, final=False,
|
||||
student_submission=self.student_submission)
|
||||
student_submission=self.student_submission)
|
||||
self.student_submission.final = True
|
||||
self.student_submission.save()
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import os
|
|||
import requests
|
||||
from django.conf import settings
|
||||
|
||||
from users.models import User, SchoolClass, Role, UserRole
|
||||
from users.models import User, SchoolClass, Role, UserRole, SchoolClassMember
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
|
@ -40,7 +40,10 @@ class Command(BaseCommand):
|
|||
self.stdout.write("Adding to class(es) {}".format(', '.join(school_class_names)))
|
||||
for school_class_name in school_class_names:
|
||||
school, _ = SchoolClass.objects.get_or_create(name=school_class_name)
|
||||
user.school_classes.add(school)
|
||||
SchoolClassMember.objects.create(
|
||||
school_class=school,
|
||||
user=user
|
||||
)
|
||||
|
||||
self.stdout.write("")
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from graphene import relay
|
|||
from core.views import SetPasswordView
|
||||
from registration.models import License
|
||||
from registration.serializers import RegistrationSerializer
|
||||
from users.models import User, Role, UserRole, SchoolClass
|
||||
from users.models import User, Role, UserRole, SchoolClass, SchoolClassMember
|
||||
|
||||
|
||||
class PublicFieldError(graphene.ObjectType):
|
||||
|
|
@ -61,7 +61,10 @@ class Registration(relay.ClientIDMutation):
|
|||
UserRole.objects.get_or_create(user=user, role=teacher_role)
|
||||
default_class_name = SchoolClass.generate_default_group_name()
|
||||
default_class = SchoolClass.objects.create(name=default_class_name)
|
||||
user.school_classes.add(default_class)
|
||||
SchoolClassMember.objects.create(
|
||||
user=user,
|
||||
school_class=default_class
|
||||
)
|
||||
else:
|
||||
student_role = Role.objects.get(key=Role.objects.STUDENT_KEY)
|
||||
UserRole.objects.get_or_create(user=user, role=student_role)
|
||||
|
|
|
|||
|
|
@ -7,15 +7,24 @@ from .models import User, SchoolClass, Role, UserRole, UserSetting
|
|||
|
||||
class SchoolClassInline(admin.TabularInline):
|
||||
model = SchoolClass.users.through
|
||||
extra = 1
|
||||
|
||||
|
||||
class RoleInline(admin.TabularInline):
|
||||
model = UserRole
|
||||
extra = 1
|
||||
|
||||
|
||||
@admin.register(SchoolClass)
|
||||
class SchoolClassAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'code', 'is_deleted')
|
||||
list_display = ('name', 'code', 'user_list', 'is_deleted')
|
||||
|
||||
inlines = [
|
||||
SchoolClassInline
|
||||
]
|
||||
|
||||
def user_list(self, obj):
|
||||
return ', '.join([s.username for s in obj.users.all()])
|
||||
|
||||
|
||||
@admin.register(Role)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import random
|
|||
|
||||
import factory
|
||||
|
||||
from users.models import SchoolClass
|
||||
from users.models import SchoolClass, SchoolClassMember
|
||||
|
||||
class_types = ['DA', 'KV', 'INF', 'EE']
|
||||
class_suffix = ['A', 'B', 'C', 'D', 'E']
|
||||
|
|
@ -16,7 +16,8 @@ class SchoolClassFactory(factory.django.DjangoModelFactory):
|
|||
class Meta:
|
||||
model = SchoolClass
|
||||
|
||||
name = factory.Sequence(lambda n: '{}{}{}'.format(random.choice(class_types), '18', class_suffix[n % len(class_suffix)]))
|
||||
name = factory.Sequence(
|
||||
lambda n: '{}{}{}'.format(random.choice(class_types), '18', class_suffix[n % len(class_suffix)]))
|
||||
is_deleted = False
|
||||
|
||||
@factory.post_generation
|
||||
|
|
@ -28,4 +29,4 @@ class SchoolClassFactory(factory.django.DjangoModelFactory):
|
|||
if extracted:
|
||||
# A list of groups were passed in, use them
|
||||
for user in extracted:
|
||||
self.users.add(user)
|
||||
SchoolClassMember.objects.create(user=user, school_class=self, active=True)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
# Generated by Django 2.1.15 on 2020-03-03 12:58
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0011_auto_20200302_1613'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserSchoolClassConnection',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('active', models.BooleanField(default=True)),
|
||||
('school_class', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.SchoolClass')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='schoolclass',
|
||||
name='users_with_active',
|
||||
field=models.ManyToManyField(blank=True, related_name='school_classes_with_active', through='users.UserSchoolClassConnection', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 2.1.15 on 2020-03-03 12:59
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
def forwards(apps, schema_editor):
|
||||
SchoolClass = apps.get_model('users', 'SchoolClass')
|
||||
UserSchoolClassConnection = apps.get_model('users', 'UserSchoolClassConnection')
|
||||
|
||||
for school_class in SchoolClass.objects.all():
|
||||
for user in school_class.users.all():
|
||||
UserSchoolClassConnection.objects.create(user=user, school_class=school_class, active=True)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('users', '0012_auto_20200303_1258'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(forwards)
|
||||
]
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 2.1.15 on 2020-03-03 13:06
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0013_auto_20200303_1259'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='schoolclass',
|
||||
name='users',
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 2.1.15 on 2020-03-03 13:06
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0014_remove_schoolclass_users'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='schoolclass',
|
||||
name='users_with_active',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='schoolclass',
|
||||
name='users',
|
||||
field=models.ManyToManyField(blank=True, related_name='school_classes', through='users.UserSchoolClassConnection', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 2.1.15 on 2020-03-04 12:50
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0015_auto_20200303_1306'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='UserSchoolClassConnection',
|
||||
new_name='SchoolClassMember',
|
||||
),
|
||||
]
|
||||
|
|
@ -42,13 +42,16 @@ class User(AbstractUser):
|
|||
return User.objects.filter(school_classes__users=self.pk)
|
||||
|
||||
def get_teacher(self):
|
||||
if self.user_roles.filter(role__key='teacher').exists():
|
||||
if self.is_teacher():
|
||||
return self
|
||||
elif self.school_classes.count() > 0:
|
||||
return self.school_classes.first().get_teacher()
|
||||
else:
|
||||
return None
|
||||
|
||||
def is_teacher(self):
|
||||
return self.user_roles.filter(role__key='teacher').exists()
|
||||
|
||||
def selected_class(self):
|
||||
try:
|
||||
settings = UserSetting.objects.get(user=self)
|
||||
|
|
@ -69,7 +72,8 @@ class User(AbstractUser):
|
|||
class SchoolClass(models.Model):
|
||||
name = models.CharField(max_length=100, blank=False, null=False, unique=True)
|
||||
is_deleted = models.BooleanField(blank=False, null=False, default=False)
|
||||
users = models.ManyToManyField(get_user_model(), related_name='school_classes', blank=True)
|
||||
users = models.ManyToManyField(get_user_model(), related_name='school_classes', blank=True,
|
||||
through='users.SchoolClassMember')
|
||||
code = models.CharField('Code zum Beitreten', blank=True, null=True, max_length=10, unique=True, default=None)
|
||||
|
||||
class Meta:
|
||||
|
|
@ -175,3 +179,9 @@ class UserRole(models.Model):
|
|||
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)
|
||||
|
||||
|
||||
class SchoolClassMember(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
school_class = models.ForeignKey(SchoolClass, on_delete=models.CASCADE)
|
||||
active = models.BooleanField(default=True)
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ from django.contrib.auth import update_session_auth_hash
|
|||
from django.core.exceptions import PermissionDenied
|
||||
from django.db.models import Q
|
||||
from graphene import relay
|
||||
from graphql_relay import from_global_id
|
||||
|
||||
from api.utils import get_object
|
||||
from users.inputs import PasswordUpdateInput
|
||||
from users.models import SchoolClass, UserSetting
|
||||
from users.models import SchoolClass, UserSetting, User, SchoolClassMember
|
||||
from users.schema import SchoolClassNode
|
||||
from users.serializers import PasswordSerialzer, AvatarUrlSerializer
|
||||
|
||||
|
|
@ -122,15 +123,45 @@ class JoinClass(relay.ClientIDMutation):
|
|||
try:
|
||||
school_class = SchoolClass.objects.get(Q(code__iexact=code))
|
||||
|
||||
|
||||
if user not in list(school_class.users.all()):
|
||||
school_class.users.add(user)
|
||||
SchoolClassMember.objects.create(
|
||||
user=user,
|
||||
school_class=school_class
|
||||
)
|
||||
else:
|
||||
raise CodeNotFoundException('[CAJ] Schüler ist bereits in Klasse') # CAJ = Class Already Joined
|
||||
raise CodeNotFoundException('[CAJ] Schüler ist bereits in Klasse') # CAJ = Class Already Joined
|
||||
|
||||
return cls(success=True, school_class=school_class)
|
||||
except SchoolClass.DoesNotExist:
|
||||
raise CodeNotFoundException('[CNV] Code ist nicht gültig') # CAV = Code Not Valid
|
||||
raise CodeNotFoundException('[CNV] Code ist nicht gültig') # CAV = Code Not Valid
|
||||
|
||||
|
||||
class AddRemoveMember(relay.ClientIDMutation):
|
||||
class Input:
|
||||
member = graphene.ID(required=True)
|
||||
school_class = graphene.ID(required=True)
|
||||
active = graphene.Boolean(required=True)
|
||||
|
||||
success = graphene.Boolean()
|
||||
|
||||
@classmethod
|
||||
def mutate_and_get_payload(cls, root, info, **kwargs):
|
||||
member_id = kwargs.get('member')
|
||||
school_class_id = kwargs.get('school_class')
|
||||
active = kwargs.get('active')
|
||||
user = info.context.user
|
||||
|
||||
member_pk = from_global_id(member_id)[1]
|
||||
school_class = get_object(SchoolClass, school_class_id)
|
||||
|
||||
if not user.is_teacher() or not school_class.users.filter(pk=user.pk).exists():
|
||||
raise PermissionError('Fehlende Berechtigung')
|
||||
|
||||
school_class_member = SchoolClassMember.objects.get(user__pk=member_pk, school_class=school_class)
|
||||
school_class_member.active = active
|
||||
school_class_member.save()
|
||||
|
||||
return cls(success=True)
|
||||
|
||||
|
||||
class ProfileMutations:
|
||||
|
|
@ -138,3 +169,4 @@ class ProfileMutations:
|
|||
update_avatar = UpdateAvatar.Field()
|
||||
update_setting = UpdateSetting.Field()
|
||||
join_class = JoinClass.Field()
|
||||
add_remove_member = AddRemoveMember.Field()
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
import graphene
|
||||
from graphene import relay
|
||||
from django.db.models import Prefetch, Value, CharField, Q
|
||||
from django.db.models.functions import Concat
|
||||
from graphene import relay, ObjectType
|
||||
from graphene_django import DjangoObjectType
|
||||
from graphene_django.filter import DjangoFilterConnectionField
|
||||
from graphql_relay import to_global_id
|
||||
|
||||
from assignments.models import StudentSubmission
|
||||
from assignments.schema.types import StudentSubmissionNode
|
||||
from basicknowledge.models import BasicKnowledge
|
||||
from basicknowledge.queries import InstrumentNode
|
||||
from books.models import Module
|
||||
from books.schema.queries import ModuleNode
|
||||
from users.models import User, SchoolClass
|
||||
from users.models import User, SchoolClass, SchoolClassMember
|
||||
|
||||
|
||||
class SchoolClassNode(DjangoObjectType):
|
||||
pk = graphene.Int()
|
||||
members = graphene.List('users.schema.ClassMemberNode')
|
||||
|
||||
class Meta:
|
||||
model = SchoolClass
|
||||
|
|
@ -23,11 +25,16 @@ class SchoolClassNode(DjangoObjectType):
|
|||
def resolve_pk(self, *args, **kwargs):
|
||||
return self.id
|
||||
|
||||
def resolve_members(self, info, **kwargs):
|
||||
return SchoolClassMember.objects.filter(school_class=self)
|
||||
|
||||
|
||||
class UserNode(DjangoObjectType):
|
||||
pk = graphene.Int()
|
||||
permissions = graphene.List(graphene.String)
|
||||
selected_class = graphene.Field(SchoolClassNode)
|
||||
is_teacher = graphene.Boolean()
|
||||
old_classes = DjangoFilterConnectionField(SchoolClassNode)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
|
|
@ -45,6 +52,49 @@ class UserNode(DjangoObjectType):
|
|||
def resolve_selected_class(self, info):
|
||||
return self.selected_class()
|
||||
|
||||
def resolve_is_teacher(self, info):
|
||||
return self.is_teacher()
|
||||
|
||||
def resolve_school_classes(self, info):
|
||||
if self.selected_class() is None: # then we don't have any class to return
|
||||
return []
|
||||
return SchoolClass.objects.filter(
|
||||
Q(schoolclassmember__active=True, schoolclassmember__user=self) | Q(pk=self.selected_class().pk)).distinct()
|
||||
|
||||
def resolve_old_classes(self, info):
|
||||
return SchoolClass.objects.filter(schoolclassmember__active=False, schoolclassmember__user=self)
|
||||
|
||||
|
||||
class ClassMemberNode(ObjectType):
|
||||
"""
|
||||
We need to build this ourselves, because we want the active property on the node, because providing it on the
|
||||
Connection or Edge for a UserNodeConnection is difficult.
|
||||
"""
|
||||
user = graphene.Field(UserNode)
|
||||
active = graphene.Boolean()
|
||||
first_name = graphene.String()
|
||||
last_name = graphene.String()
|
||||
is_teacher = graphene.Boolean()
|
||||
id = graphene.ID()
|
||||
|
||||
def resolve_id(self, *args):
|
||||
return to_global_id('UserNode', self.user.pk)
|
||||
|
||||
def resolve_active(self, *args):
|
||||
return self.active
|
||||
|
||||
def resolve_first_name(self, *args):
|
||||
return self.user.first_name
|
||||
|
||||
def resolve_last_name(self, *args):
|
||||
return self.user.last_name
|
||||
|
||||
def resolve_user(self, info, **kwargs):
|
||||
return self.user
|
||||
|
||||
def resolve_is_teacher(self, *args):
|
||||
return self.user.is_teacher()
|
||||
|
||||
|
||||
class UsersQuery(object):
|
||||
me = graphene.Field(UserNode)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from graphene.test import Client
|
|||
|
||||
from core.factories import UserFactory
|
||||
from users.factories import SchoolClassFactory
|
||||
from users.models import SchoolClass
|
||||
from users.models import SchoolClass, SchoolClassMember
|
||||
from api.schema import schema
|
||||
|
||||
|
||||
|
|
@ -45,7 +45,10 @@ class JoinSchoolClassTest(TestCase):
|
|||
def test_class_already_joined(self):
|
||||
code = 'YYYY'
|
||||
school_class = SchoolClass.objects.get(code=code)
|
||||
school_class.users.add(self.user)
|
||||
SchoolClassMember.objects.create(
|
||||
user=self.user,
|
||||
school_class=school_class
|
||||
)
|
||||
|
||||
self.assertEqual(self.user.school_classes.count(), 2)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,126 @@
|
|||
from django.test import TestCase
|
||||
from graphene import Context
|
||||
from graphene.test import Client
|
||||
from graphql_relay import to_global_id
|
||||
|
||||
from api.utils import get_graphql_mutation
|
||||
from core.factories import UserFactory
|
||||
from users.factories import SchoolClassFactory
|
||||
from users.models import SchoolClass, User, SchoolClassMember
|
||||
from api.schema import schema
|
||||
from users.services import create_users
|
||||
|
||||
|
||||
class JoinSchoolClassTest(TestCase):
|
||||
def setUp(self):
|
||||
self.client = Client(schema=schema)
|
||||
self.school_class_name = 'Moordale'
|
||||
|
||||
user_data = [
|
||||
{
|
||||
'teacher': ('Emily', 'Sands',),
|
||||
'class': self.school_class_name,
|
||||
'code': 'SEXED',
|
||||
'students': [
|
||||
('Otis', 'Milburn'),
|
||||
('Maeve', 'Wiley'),
|
||||
('Adam', 'Groff'),
|
||||
('Eric', 'Effiong'),
|
||||
('Jackson', 'Marchetti'),
|
||||
]
|
||||
},
|
||||
{
|
||||
'teacher': ('Colin', 'Hendricks'),
|
||||
'class': 'Swing Band',
|
||||
'students': [
|
||||
('Ola', 'Nyman'),
|
||||
]
|
||||
}
|
||||
]
|
||||
create_users(user_data)
|
||||
teacher = User.objects.get(username='emily.sands')
|
||||
self.teacher_id = to_global_id('UserNode', teacher.pk)
|
||||
student = User.objects.get(username='adam.groff')
|
||||
self.student_id = to_global_id('UserNode', student.pk)
|
||||
other_student = User.objects.get(username='eric.effiong')
|
||||
self.other_student_id = to_global_id('UserNode', other_student.pk)
|
||||
|
||||
school_class = SchoolClass.objects.get(name=self.school_class_name)
|
||||
self.school_class_id = to_global_id('SchoolClassNode', school_class.pk)
|
||||
self.teacher_context = Context(user=teacher)
|
||||
self.student_context = Context(user=student)
|
||||
|
||||
self.mutation = get_graphql_mutation('addRemoveMember.gql')
|
||||
|
||||
def test_leave_and_join_class(self):
|
||||
self.assertEqual(
|
||||
SchoolClassMember.objects.filter(school_class__name=self.school_class_name, active=True).count(), 6)
|
||||
self.assertEqual(
|
||||
SchoolClassMember.objects.filter(school_class__name=self.school_class_name, active=False).count(),
|
||||
0)
|
||||
result = self.client.execute(self.mutation, variables={
|
||||
'input': {
|
||||
'schoolClass': self.school_class_id,
|
||||
'member': self.student_id,
|
||||
'active': False
|
||||
}
|
||||
}, context=self.teacher_context)
|
||||
self.assertIsNone(result.get('errors'))
|
||||
self.assertEqual(
|
||||
SchoolClassMember.objects.filter(school_class__name=self.school_class_name, active=True).count(), 5)
|
||||
self.assertEqual(
|
||||
SchoolClassMember.objects.filter(school_class__name=self.school_class_name, active=False).count(),
|
||||
1)
|
||||
|
||||
result = self.client.execute(self.mutation, variables={
|
||||
'input': {
|
||||
'schoolClass': self.school_class_id,
|
||||
'member': self.student_id,
|
||||
'active': True
|
||||
}
|
||||
}, context=self.teacher_context)
|
||||
|
||||
self.assertIsNone(result.get('errors'))
|
||||
self.assertEqual(
|
||||
SchoolClassMember.objects.filter(school_class__name=self.school_class_name, active=True).count(), 6)
|
||||
self.assertEqual(
|
||||
SchoolClassMember.objects.filter(school_class__name=self.school_class_name, active=False).count(),
|
||||
0)
|
||||
|
||||
def test_leave_class_student_raises_error(self):
|
||||
self.assertEqual(
|
||||
SchoolClassMember.objects.filter(school_class__name=self.school_class_name, active=True).count(), 6)
|
||||
self.assertEqual(
|
||||
SchoolClassMember.objects.filter(school_class__name=self.school_class_name, active=False).count(), 0)
|
||||
result = self.client.execute(self.mutation, variables={
|
||||
'input': {
|
||||
'schoolClass': self.school_class_id,
|
||||
'member': self.other_student_id,
|
||||
'active': False
|
||||
}
|
||||
}, context=self.student_context)
|
||||
self.assertIsNotNone(result['errors'])
|
||||
self.assertEqual(
|
||||
SchoolClassMember.objects.filter(school_class__name=self.school_class_name, active=True).count(), 6)
|
||||
self.assertEqual(
|
||||
SchoolClassMember.objects.filter(school_class__name=self.school_class_name, active=False).count(), 0)
|
||||
|
||||
def test_leave_class_other_school_class_raises_error(self):
|
||||
self.assertEqual(
|
||||
SchoolClassMember.objects.filter(school_class__name=self.school_class_name, active=True).count(), 6)
|
||||
self.assertEqual(
|
||||
SchoolClassMember.objects.filter(school_class__name=self.school_class_name, active=False).count(), 0)
|
||||
student = User.objects.get(username='ola.nyman')
|
||||
school_class = SchoolClass.objects.get(name='Swing Band')
|
||||
result = self.client.execute(self.mutation, variables={
|
||||
'input': {
|
||||
'schoolClass': to_global_id('SchoolClassNode', school_class.id),
|
||||
'member': to_global_id('UserNode', student.id),
|
||||
'active': False
|
||||
}
|
||||
}, context=self.teacher_context)
|
||||
self.assertIsNotNone(result['errors'])
|
||||
self.assertEqual(
|
||||
SchoolClassMember.objects.filter(school_class__name=self.school_class_name, active=True).count(), 6)
|
||||
self.assertEqual(
|
||||
SchoolClassMember.objects.filter(school_class__name=self.school_class_name, active=False).count(), 0)
|
||||
|
|
@ -11,10 +11,12 @@ from django.conf import settings
|
|||
from django.test import TestCase, RequestFactory
|
||||
from graphene.test import Client
|
||||
from api.schema import schema
|
||||
from api.utils import get_graphql_query
|
||||
from core.factories import UserFactory
|
||||
from django.contrib.auth import authenticate
|
||||
|
||||
from users.factories import SchoolClassFactory
|
||||
from users.models import SchoolClassMember
|
||||
|
||||
|
||||
class MySchoolClasses(TestCase):
|
||||
|
|
@ -24,13 +26,17 @@ class MySchoolClasses(TestCase):
|
|||
self.class1 = SchoolClassFactory(users=[self.user, self.another_user])
|
||||
self.class2 = SchoolClassFactory(users=[self.user])
|
||||
self.class3 = SchoolClassFactory(users=[self.another_user])
|
||||
self.inactive_class = SchoolClassFactory(users=[self.user])
|
||||
|
||||
inactive_member = SchoolClassMember.objects.get(school_class=self.inactive_class)
|
||||
inactive_member.active = False
|
||||
inactive_member.save()
|
||||
|
||||
request = RequestFactory().get('/')
|
||||
request.user = self.user
|
||||
self.client = Client(schema=schema, context_value=request)
|
||||
|
||||
def make_query(self):
|
||||
|
||||
def test_user_sees_her_classes(self):
|
||||
query = '''
|
||||
query {
|
||||
me {
|
||||
|
|
@ -55,11 +61,8 @@ class MySchoolClasses(TestCase):
|
|||
}
|
||||
}
|
||||
'''
|
||||
return self.client.execute(query)
|
||||
|
||||
def test_user_sees_her_classes(self):
|
||||
|
||||
result = self.make_query()
|
||||
result = self.client.execute(query)
|
||||
|
||||
classes = result.get('data').get('me').get('schoolClasses').get('edges')
|
||||
self.assertEqual(len(classes), 2)
|
||||
|
|
@ -72,5 +75,12 @@ class MySchoolClasses(TestCase):
|
|||
elif school_class.get('name') == self.class2:
|
||||
self.fail('MySchoolClassTest:test_user_sees_her_classes: Class should not be in response')
|
||||
|
||||
def test_old_classes(self):
|
||||
query = get_graphql_query('oldClasses.gql')
|
||||
|
||||
result = self.client.execute(query)
|
||||
|
||||
old_classes = result.get('data').get('me').get('oldClasses').get('edges')
|
||||
self.assertEqual(len(old_classes), 1)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue