Merged in feature/rooms-read-only (pull request #88)
Feature/rooms read only
This commit is contained in:
commit
b50ba068a0
|
|
@ -2,6 +2,13 @@ import { GraphQLError } from 'graphql';
|
|||
|
||||
const schema = require('../../../fixtures/schema.json');
|
||||
|
||||
const redeemCoupon = coupon => {
|
||||
if (coupon !== '') {
|
||||
cy.get('[data-cy="coupon-input"]').type(coupon);
|
||||
}
|
||||
cy.get('[data-cy="coupon-button"]').click();
|
||||
};
|
||||
|
||||
describe('Email Verifcation', () => {
|
||||
beforeEach(() => {
|
||||
cy.server();
|
||||
|
|
@ -22,7 +29,7 @@ describe('Email Verifcation', () => {
|
|||
cy.apolloLogin('rahel.cueni', 'test');
|
||||
|
||||
cy.visit('/license-activation');
|
||||
cy.redeemCoupon('12345asfd');
|
||||
redeemCoupon('12345asfd');
|
||||
cy.assertStartPage();
|
||||
});
|
||||
|
||||
|
|
@ -31,7 +38,7 @@ describe('Email Verifcation', () => {
|
|||
cy.apolloLogin('rahel.cueni', 'test');
|
||||
|
||||
cy.visit('/license-activation');
|
||||
cy.redeemCoupon('');
|
||||
redeemCoupon('');
|
||||
cy.get('[data-cy="coupon-local-errors"]').contains('Coupon ist ein Pflichtfeld.');
|
||||
});
|
||||
|
||||
|
|
@ -46,7 +53,7 @@ describe('Email Verifcation', () => {
|
|||
cy.apolloLogin('rahel.cueni', 'test');
|
||||
|
||||
cy.visit('/license-activation');
|
||||
cy.redeemCoupon('12345asfd');
|
||||
redeemCoupon('12345asfd');
|
||||
cy.get('[data-cy="coupon-remote-errors"]').contains('Der angegebene Coupon-Code ist ungültig.');
|
||||
});
|
||||
|
||||
|
|
@ -61,7 +68,7 @@ describe('Email Verifcation', () => {
|
|||
cy.apolloLogin('rahel.cueni', 'test');
|
||||
|
||||
cy.visit('/license-activation');
|
||||
cy.redeemCoupon('12345asfd');
|
||||
redeemCoupon('12345asfd');
|
||||
cy.get('[data-cy="coupon-remote-errors"]').contains('Es ist ein Fehler aufgetreten. Bitte versuchen Sie es nochmals oder kontaktieren Sie den Administrator.');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ describe('Assignment feedback read-only - Teacher', () => {
|
|||
// cy.get('@textarea').should('have.attr', 'readonly');
|
||||
|
||||
cy.isSubmissionReadOnly(myText);
|
||||
cy.canReopen(false);
|
||||
cy.getByDataCy('final-submission-reopen').should('not.exist');
|
||||
});
|
||||
it('can edit', () => {
|
||||
cy.mockGraphqlOps({
|
||||
|
|
@ -63,6 +63,7 @@ describe('Assignment feedback read-only - Teacher', () => {
|
|||
});
|
||||
|
||||
cy.visit('submission/submission-id');
|
||||
cy.canReopen(false);
|
||||
|
||||
cy.getByDataCy('final-submission-reopen').should('exist');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ describe('Assignments read-only - Student', () => {
|
|||
cy.visit('module/module-with-assignment');
|
||||
|
||||
cy.isSubmissionReadOnly(myText);
|
||||
cy.canReopen(false);
|
||||
cy.getByDataCy('final-submission-reopen').should('not.exist');
|
||||
});
|
||||
|
||||
it('can revoke turn in', () => {
|
||||
|
|
@ -110,6 +110,6 @@ describe('Assignments read-only - Student', () => {
|
|||
|
||||
cy.visit('module/module-with-assignment');
|
||||
cy.getByDataCy('final-submission').should('exist');
|
||||
cy.canReopen(false);
|
||||
cy.getByDataCy('final-submission-reopen').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
import mocks from '../../../fixtures/mocks';
|
||||
|
||||
const SELECTED_CLASS_ID = 'selectedClassId';
|
||||
|
||||
const getOperations = ({readOnly, classReadOnly}) => ({
|
||||
MeQuery: {
|
||||
me: {
|
||||
onboardingVisited: true,
|
||||
readOnly,
|
||||
isTeacher: true,
|
||||
selectedClass: {
|
||||
id: SELECTED_CLASS_ID,
|
||||
readOnly: classReadOnly,
|
||||
},
|
||||
},
|
||||
},
|
||||
RoomEntriesQuery: {
|
||||
room: {
|
||||
id: 'roomId',
|
||||
slug: '',
|
||||
title: 'room title',
|
||||
entryCount: 3,
|
||||
appearance: 'blue',
|
||||
description: 'room description',
|
||||
schoolClass: {
|
||||
id: SELECTED_CLASS_ID,
|
||||
name: 'selected class',
|
||||
},
|
||||
roomEntries: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: 'entryId',
|
||||
slug: '',
|
||||
title: 'entry title',
|
||||
contents: [],
|
||||
author: {
|
||||
id: 'authorId',
|
||||
firstName: 'first',
|
||||
lastName: 'last',
|
||||
avatarUrl: ''
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const checkRoomReadOnly = ({editable, readOnly, classReadOnly = false}) => {
|
||||
const operations = getOperations({readOnly, classReadOnly});
|
||||
|
||||
cy.mockGraphqlOps({
|
||||
operations,
|
||||
});
|
||||
|
||||
const exist = editable ? 'exist' : 'not.exist';
|
||||
cy.visit('room/some-room');
|
||||
cy.get('.room-entry').should('exist');
|
||||
cy.getByDataCy('add-room-entry-button').should(exist);
|
||||
cy.getByDataCy('room-actions').should(exist);
|
||||
};
|
||||
|
||||
describe('Room Team Management - Read only', () => {
|
||||
beforeEach(() => {
|
||||
cy.setup();
|
||||
});
|
||||
|
||||
it('can edit room', () => {
|
||||
checkRoomReadOnly({editable: true, readOnly: false});
|
||||
});
|
||||
|
||||
it('can not edit room', () => {
|
||||
checkRoomReadOnly({editable: false, readOnly: true});
|
||||
});
|
||||
|
||||
it('can not edit room of inactive class', () => {
|
||||
checkRoomReadOnly({editable: false, readOnly: false, classReadOnly: true});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import mocks from '../../../fixtures/mocks';
|
||||
|
||||
const SELECTED_CLASS_ID = 'selectedClassId';
|
||||
|
||||
const getOperations = ({readOnly, classReadOnly}) => ({
|
||||
MeQuery: {
|
||||
me: {
|
||||
onboardingVisited: true,
|
||||
readOnly,
|
||||
isTeacher: true,
|
||||
selectedClass: {
|
||||
id: SELECTED_CLASS_ID,
|
||||
readOnly: classReadOnly,
|
||||
},
|
||||
},
|
||||
},
|
||||
RoomsQuery: {
|
||||
rooms: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: '',
|
||||
slug: '',
|
||||
title: 'some room',
|
||||
entryCount: 3,
|
||||
appearance: 'red',
|
||||
description: 'some description',
|
||||
schoolClass: {
|
||||
id: SELECTED_CLASS_ID,
|
||||
name: 'bla'
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const checkRoomsReadOnly = ({editable, readOnly, classReadOnly = false}) => {
|
||||
const operations = getOperations({readOnly, classReadOnly});
|
||||
|
||||
cy.mockGraphqlOps({
|
||||
operations,
|
||||
});
|
||||
|
||||
const exist = editable ? 'exist' : 'not.exist';
|
||||
cy.visit('rooms');
|
||||
cy.log('visit');
|
||||
cy.get('.room-widget').should('exist');
|
||||
cy.getByDataCy('add-room').should(exist);
|
||||
cy.getByDataCy('widget-footer').should(exist);
|
||||
};
|
||||
|
||||
describe('Room Team Management - Read only', () => {
|
||||
beforeEach(() => {
|
||||
cy.setup();
|
||||
});
|
||||
|
||||
it('can edit room', () => {
|
||||
checkRoomsReadOnly({editable: true, readOnly: false});
|
||||
});
|
||||
|
||||
it('can not edit room', () => {
|
||||
checkRoomsReadOnly({editable: false, readOnly: true});
|
||||
});
|
||||
|
||||
it('can not edit room of inactive class', () => {
|
||||
checkRoomsReadOnly({editable: false, readOnly: false, classReadOnly: true});
|
||||
});
|
||||
});
|
||||
|
|
@ -32,7 +32,7 @@ const getOperations = ({readOnly}) => ({
|
|||
},
|
||||
});
|
||||
|
||||
Cypress.Commands.add('checkSchoolClassTeamReadOnly', (readOnly) => {
|
||||
const checkSchoolClassTeamReadOnly = (readOnly) => {
|
||||
cy.mockGraphqlOps({
|
||||
operations: getOperations({readOnly}),
|
||||
});
|
||||
|
|
@ -47,7 +47,7 @@ Cypress.Commands.add('checkSchoolClassTeamReadOnly', (readOnly) => {
|
|||
cy.getByDataCy('current-class-name').should('exist');
|
||||
cy.getByDataCy('create-class-link').should(exist);
|
||||
cy.getByDataCy('my-team-link').should(exist);
|
||||
});
|
||||
};
|
||||
|
||||
describe('School Class and Team Management - Read only', () => {
|
||||
beforeEach(() => {
|
||||
|
|
@ -62,21 +62,10 @@ describe('School Class and Team Management - Read only', () => {
|
|||
});
|
||||
|
||||
it('can see menu items', () => {
|
||||
cy.checkSchoolClassTeamReadOnly(false);
|
||||
// cy.visit('me/class');
|
||||
// cy.getByDataCy('group-list-name').should('exist').should('contain', selectedClassName);
|
||||
// cy.getByDataCy('show-code-button').should('exist');
|
||||
// cy.getByDataCy('edit-group-name-link').should('exist');
|
||||
// cy.openSidebar();
|
||||
// cy.getByDataCy('class-selection').click();
|
||||
// cy.getByDataCy('current-class-name').should('exist');
|
||||
// cy.getByDataCy('create-class-link').should('exist');
|
||||
checkSchoolClassTeamReadOnly(false);
|
||||
});
|
||||
|
||||
it('can not see menu items', () => {
|
||||
// cy.mockGraphqlOps({
|
||||
// operations: getOperations({readOnly: true}),
|
||||
// });
|
||||
cy.checkSchoolClassTeamReadOnly(true);
|
||||
checkSchoolClassTeamReadOnly(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
// todo: once above issue is fixed, go back to the original repo -> npm install cypress-graphql-mock
|
||||
// import 'cypress-graphql-mock';
|
||||
import '@iam4x/cypress-graphql-mock';
|
||||
import mocks from '../fixtures/mocks';
|
||||
|
||||
Cypress.Commands.add('apolloLogin', (username, password) => {
|
||||
const payload = {
|
||||
|
|
@ -118,51 +119,6 @@ Cypress.Commands.add('enterPassword', (password) => {
|
|||
cy.get('[data-cy="login-button"]').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('register', (prefix, firstname, lastname, street, city, postcode, password, passwordConfirmation, acceptTerms) => {
|
||||
let selection = prefix === 1 ? 'Herr' : 'Frau';
|
||||
|
||||
cy.get('[data-cy="prefix-selection"]').select(selection);
|
||||
|
||||
if (firstname !== '') {
|
||||
cy.get('[data-cy="firstname-input"]').type(firstname);
|
||||
}
|
||||
if (lastname !== '') {
|
||||
cy.get('[data-cy="lastname-input"]').type(lastname);
|
||||
}
|
||||
|
||||
if (street !== '') {
|
||||
cy.get('[data-cy="street-input"]').type(street);
|
||||
}
|
||||
|
||||
if (city !== '') {
|
||||
cy.get('[data-cy="city-input"]').type(city);
|
||||
}
|
||||
|
||||
if (postcode !== '') {
|
||||
cy.get('[data-cy="postcode-input"]').type(postcode);
|
||||
}
|
||||
|
||||
if (password !== '') {
|
||||
cy.get('[data-cy="password-input"]').type(password);
|
||||
}
|
||||
|
||||
if (acceptTerms) {
|
||||
cy.get('[data-cy="acceptedTerms-input"] > input').first().check({force: true}).then(() => {
|
||||
cy.get('[data-cy="acceptedTerms-input"] > input:checkbox').should('be.checked');
|
||||
});
|
||||
}
|
||||
|
||||
cy.get('[data-cy="passwordConfirmation-input"]').type(passwordConfirmation);
|
||||
cy.get('[data-cy="register-button"]').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('redeemCoupon', coupon => {
|
||||
if (coupon !== '') {
|
||||
cy.get('[data-cy="coupon-input"]').type(coupon);
|
||||
}
|
||||
cy.get('[data-cy="coupon-button"]').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('assertStartPage', (onboarding) => {
|
||||
if (onboarding) {
|
||||
cy.get('[data-cy=onboarding-page]').should('exist');
|
||||
|
|
@ -190,11 +146,6 @@ Cypress.Commands.add('fakeLogin', () => {
|
|||
cy.setCookie('loginStatus', 'true');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('canReopen', (exists) => {
|
||||
let check = exists ? 'exist' : 'not.exist';
|
||||
cy.getByDataCy('final-submission-reopen').should(check);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('isSubmissionReadOnly', (myText) => {
|
||||
cy.get('.submission-form__textarea--readonly').as('textarea');
|
||||
|
||||
|
|
@ -206,3 +157,15 @@ Cypress.Commands.add('isSubmissionReadOnly', (myText) => {
|
|||
Cypress.Commands.add('openSidebar', () => {
|
||||
cy.getByDataCy('user-widget-avatar').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('setup', () => {
|
||||
cy.fakeLogin('nino.teacher', 'test');
|
||||
cy.server();
|
||||
cy.viewport('macbook-15');
|
||||
cy.task('getSchema').then(schema => {
|
||||
cy.mockGraphql({
|
||||
schema,
|
||||
mocks,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// todo: clean up this file
|
||||
|
||||
const getSchoolClassNode = (id, schoolClassName) => ({
|
||||
'id': btoa(`SchoolClassNode:${id}`),
|
||||
'name': schoolClassName,
|
||||
|
|
@ -52,43 +54,6 @@ export const getMe = ({schoolClasses, teacher}) => {
|
|||
};
|
||||
};
|
||||
|
||||
export const getAssignments = () => {
|
||||
return {
|
||||
'assignments': {
|
||||
'edges': [
|
||||
{
|
||||
'node': {
|
||||
'id': 'QXNzaWdubWVudE5vZGU6MQ==',
|
||||
'title': 'Ein Auftragstitel',
|
||||
'assignment': 'Ein Auftrag',
|
||||
'solution': null,
|
||||
'submission': {
|
||||
'id': 'U3R1ZGVudFN1Ym1pc3Npb25Ob2RlOjE=',
|
||||
'text': 'Hir ist ein Feler gewesen',
|
||||
'final': false,
|
||||
'document': '',
|
||||
'submissionFeedback': {
|
||||
'id': 'U3VibWlzc2lvbkZlZWRiYWNrTm9kZTox',
|
||||
'text': '\ud83d\ude42\ud83d\ude10\ud83e\udd2c\ud83d\udc4d\ud83e\udd22\ud83e\udd22\ud83e\udd22\ud83e\udd22\ud83d\ude2e\ud83e\udd17',
|
||||
'teacher': {
|
||||
'firstName': 'Nico',
|
||||
'lastName': 'Zickgraf',
|
||||
'__typename': 'UserNode',
|
||||
},
|
||||
'__typename': 'SubmissionFeedbackNode',
|
||||
},
|
||||
'__typename': 'StudentSubmissionNode',
|
||||
},
|
||||
'__typename': 'AssignmentNode',
|
||||
},
|
||||
'__typename': 'AssignmentNodeEdge',
|
||||
},
|
||||
],
|
||||
'__typename': 'AssignmentNodeConnection',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getModules = () => {
|
||||
return {
|
||||
'lohn-und-budget': {
|
||||
|
|
|
|||
|
|
@ -24,10 +24,10 @@ declare namespace Cypress {
|
|||
|
||||
fakeLogin(username: string, password: string): void
|
||||
|
||||
canReopen(exists: boolean): void
|
||||
|
||||
isSubmissionReadOnly(myText: string): void
|
||||
|
||||
openSidebar(): void
|
||||
|
||||
setup(): void
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -118,7 +118,7 @@
|
|||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-jest": "^24.8.0",
|
||||
"canvas": "^2.5.0",
|
||||
"cypress": "^6.2.1",
|
||||
"cypress": "^8.0.0",
|
||||
"graphql-config": "^3.2.0",
|
||||
"jest": "^24.8.0",
|
||||
"jest-serializer-vue": "^2.0.2",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<template>
|
||||
<div class="room-actions">
|
||||
<div
|
||||
class="room-actions"
|
||||
data-cy="room-actions">
|
||||
<a
|
||||
class="room-actions__more-link"
|
||||
@click="toggleMenu">
|
||||
|
|
@ -68,8 +70,7 @@
|
|||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/styles/_variables.scss";
|
||||
@import "@/styles/_mixins.scss";
|
||||
@import "~styles/helpers";
|
||||
|
||||
.room-actions {
|
||||
&__more-link {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@
|
|||
<room-group-widget v-bind="schoolClass"/>
|
||||
<entry-count-widget :entry-count="entryCount"/>
|
||||
</router-link>
|
||||
<widget-footer v-if="canEditRoom">
|
||||
<widget-footer
|
||||
data-cy="widget-footer"
|
||||
v-if="canEditRoom">
|
||||
<room-actions :id="id"/>
|
||||
</widget-footer>
|
||||
</div>
|
||||
|
|
@ -49,7 +51,7 @@
|
|||
return `room-widget--${this.appearance}`;
|
||||
},
|
||||
canEditRoom() {
|
||||
return this.me.permissions.includes('users.can_manage_school_class_content');
|
||||
return this.me.isTeacher && !this.me.readOnly && !this.me.selectedClass.readOnly;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ fragment UserParts on PrivateUserNode {
|
|||
}
|
||||
selectedClass {
|
||||
id
|
||||
readOnly
|
||||
}
|
||||
recentModules(orderBy: "-visited") {
|
||||
edges {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@
|
|||
{{ room.description }}
|
||||
</p>
|
||||
<div class="room__meta">
|
||||
<room-actions :id="room.id"/>
|
||||
<room-actions
|
||||
:id="room.id"
|
||||
v-if="canEdit"/>
|
||||
<room-group-widget v-bind="room.schoolClass"/>
|
||||
<entry-count-widget :entry-count="roomEntryCount"/>
|
||||
</div>
|
||||
|
|
@ -14,7 +16,7 @@
|
|||
<div class="room__content">
|
||||
<add-room-entry-button
|
||||
:parent="room"
|
||||
v-if="room.id">
|
||||
v-if="room.id && canEdit">
|
||||
<!--
|
||||
the v-if is there for the case where the room hasn't loaded yet, but there is already an attempt to create
|
||||
a new room entry. mainly happens during cypress testing, but could also happen on a very slow connection
|
||||
|
|
@ -30,11 +32,18 @@
|
|||
|
||||
<script>
|
||||
import ROOM_ENTRIES_QUERY from '@/graphql/gql/queries/roomEntriesQuery.gql';
|
||||
import roomMixin from '@/mixins/room';
|
||||
import room from '@/mixins/room';
|
||||
import me from '@/mixins/me';
|
||||
|
||||
export default {
|
||||
props: ['slug'],
|
||||
mixins: [roomMixin],
|
||||
mixins: [room, me],
|
||||
|
||||
computed: {
|
||||
canEdit() {
|
||||
return !this.me.readOnly && !this.me.selectedClass.readOnly;
|
||||
}
|
||||
},
|
||||
|
||||
apollo: {
|
||||
modules: {
|
||||
|
|
@ -59,5 +68,5 @@
|
|||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/styles/_room.scss";
|
||||
@import "~styles/room";
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
return this.me.selectedClass.id;
|
||||
},
|
||||
canAddRoom() {
|
||||
return this.me.permissions.includes('users.can_manage_school_class_content');
|
||||
return this.me.isTeacher && !this.me.readOnly && !this.me.selectedClass.readOnly;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -70,7 +70,7 @@
|
|||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/styles/_mixins.scss";
|
||||
@import "~styles/helpers";
|
||||
|
||||
.rooms-page {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class UpdateSolutionVisibility(relay.ClientIDMutation):
|
|||
slug = args.get('slug')
|
||||
enabled = args.get('enabled')
|
||||
user = info.context.user
|
||||
selected_class = user.selected_class()
|
||||
selected_class = user.selected_class
|
||||
if 'users.can_manage_school_class_content' not in user.get_role_permissions():
|
||||
raise PermissionError()
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ class ModuleNode(DjangoObjectType):
|
|||
return self.get_parent().specific
|
||||
|
||||
def resolve_solutions_enabled(self, info, **kwargs):
|
||||
school_class = info.context.user.selected_class()
|
||||
school_class = info.context.user.selected_class
|
||||
return self.solutions_enabled_for.filter(pk=school_class.pk).exists() if school_class is not None else False
|
||||
|
||||
def resolve_bookmark(self, info, **kwags):
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class ModuleSolutionVisibilityTest(TestCase):
|
|||
)
|
||||
|
||||
self.teacher = User.objects.get(username="teacher")
|
||||
self.selected_class = self.teacher.selected_class()
|
||||
self.selected_class = self.teacher.selected_class
|
||||
self.student = User.objects.get(username="student1")
|
||||
student_request = RequestFactory().get('/')
|
||||
student_request.user = self.student
|
||||
|
|
@ -54,7 +54,7 @@ class ModuleSolutionVisibilityTest(TestCase):
|
|||
"""
|
||||
|
||||
def test_hide_solutions_for_students_and_then_show_them(self):
|
||||
self.assertEqual(self.student.selected_class(), self.teacher.selected_class())
|
||||
self.assertEqual(self.student.selected_class, self.teacher.selected_class)
|
||||
|
||||
student_result = self.student_client.execute(self.query, variables={
|
||||
'id': self.content_block_id
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from users.models import User
|
|||
|
||||
|
||||
def are_solutions_enabled_for(user: User, module: Module):
|
||||
school_class = user.selected_class()
|
||||
school_class = user.selected_class
|
||||
return module.solutions_enabled_for.filter(pk=school_class.pk).exists()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -370,7 +370,6 @@ type CreateTeamPayload {
|
|||
}
|
||||
|
||||
type CustomMutation {
|
||||
redeemCoupon(input: CouponInput!): CouponPayload
|
||||
spellCheck(input: SpellCheckInput!): SpellCheckPayload
|
||||
addNote(input: AddNoteInput!): AddNotePayload
|
||||
updateNote(input: UpdateNoteInput!): UpdateNotePayload
|
||||
|
|
@ -920,6 +919,7 @@ type SchoolClassNode implements Node {
|
|||
rooms(offset: Int, before: String, after: String, first: Int, last: Int, slug: String, appearance: String): RoomNodeConnection!
|
||||
pk: Int
|
||||
members: [ClassMemberNode]
|
||||
readOnly: Boolean
|
||||
}
|
||||
|
||||
type SchoolClassNodeConnection {
|
||||
|
|
|
|||
|
|
@ -7,9 +7,11 @@ from django.contrib.auth import get_user_model
|
|||
from django.contrib.auth.models import AbstractUser, Permission
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import models
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import make_aware, is_aware
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils import timezone
|
||||
from typing import Union
|
||||
|
||||
from users.licenses import MYSKILLBOX_LICENSES
|
||||
from users.managers import RoleManager, UserRoleManager, UserManager, LicenseManager
|
||||
|
|
@ -75,7 +77,8 @@ class User(AbstractUser):
|
|||
def is_teacher(self):
|
||||
return self.user_roles.filter(role__key='teacher').exists()
|
||||
|
||||
def selected_class(self):
|
||||
@cached_property
|
||||
def selected_class(self) -> Union['SchoolClass', None]:
|
||||
try:
|
||||
settings = UserSetting.objects.get(user=self)
|
||||
return settings.selected_class
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import graphene
|
|||
from django.db.models import Q
|
||||
from django.utils.dateformat import format
|
||||
from django_filters import FilterSet, OrderingFilter
|
||||
from graphene import relay, ObjectType
|
||||
from graphene import ObjectType, relay
|
||||
from graphene_django import DjangoObjectType
|
||||
from graphene_django.filter import DjangoFilterConnectionField
|
||||
from graphql_relay import to_global_id
|
||||
|
|
@ -13,13 +13,14 @@ 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, SchoolClassMember, Team
|
||||
from users.models import SchoolClass, SchoolClassMember, Team, User
|
||||
|
||||
|
||||
class SchoolClassNode(DjangoObjectType):
|
||||
pk = graphene.Int()
|
||||
members = graphene.List('users.schema.ClassMemberNode')
|
||||
code = graphene.String()
|
||||
read_only = graphene.Boolean()
|
||||
|
||||
class Meta:
|
||||
model = SchoolClass
|
||||
|
|
@ -32,13 +33,19 @@ class SchoolClassNode(DjangoObjectType):
|
|||
def resolve_members(self, info, **kwargs):
|
||||
return SchoolClassMember.objects.filter(school_class=self)
|
||||
|
||||
def resolve_code(self, info, **kwargs):
|
||||
def resolve_code(self: SchoolClass, info, **kwargs):
|
||||
if not info.context.user.is_teacher():
|
||||
return None
|
||||
if self.code is None:
|
||||
self.generate_code()
|
||||
return self.code
|
||||
|
||||
@staticmethod
|
||||
def resolve_read_only(root: SchoolClass, info, **kwargs):
|
||||
user = info.context.user
|
||||
member = SchoolClassMember.objects.get(user=user, school_class=root)
|
||||
return not member.active
|
||||
|
||||
|
||||
class TeamNode(DjangoObjectType):
|
||||
class Meta:
|
||||
|
|
@ -107,25 +114,28 @@ class PrivateUserNode(DjangoObjectType):
|
|||
def resolve_permissions(self, info):
|
||||
return self.get_all_permissions()
|
||||
|
||||
def resolve_selected_class(self, info):
|
||||
return self.selected_class()
|
||||
@staticmethod
|
||||
def resolve_selected_class(root: User, info):
|
||||
return root.selected_class
|
||||
|
||||
def resolve_expiry_date(self, info):
|
||||
if not self.hep_id: # concerns users that already have an (old) account
|
||||
@staticmethod
|
||||
def resolve_expiry_date(root: User, info):
|
||||
if not root.hep_id: # concerns users that already have an (old) account
|
||||
return format(datetime(2020, 7, 31), 'U') # just set some expiry date
|
||||
else:
|
||||
return self.license_expiry_date
|
||||
return root.license_expiry_date
|
||||
|
||||
def resolve_is_teacher(self, info):
|
||||
return self.is_teacher()
|
||||
def resolve_is_teacher(root: User, info):
|
||||
return root.is_teacher()
|
||||
|
||||
def resolve_school_classes(self, info):
|
||||
if self.selected_class() is None: # then we don't have any class to return
|
||||
@staticmethod
|
||||
def resolve_school_classes(root: User, info):
|
||||
if root.selected_class is None: # then we don't have any class to return
|
||||
return SchoolClass.objects.none()
|
||||
return SchoolClass.objects.filter(
|
||||
Q(schoolclassmember__active=True, schoolclassmember__user=self) | Q(pk=self.selected_class().pk)).distinct()
|
||||
Q(schoolclassmember__active=True, schoolclassmember__user=root) | Q(pk=root.selected_class.pk)).distinct()
|
||||
|
||||
def resolve_old_classes(self, info):
|
||||
def resolve_old_classes(self: User, info):
|
||||
return SchoolClass.objects.filter(schoolclassmember__active=False, schoolclassmember__user=self)
|
||||
|
||||
def resolve_recent_modules(self, info, **kwargs):
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class JoinSchoolClassTest(TestCase):
|
|||
self.assertEqual(result['success'], True)
|
||||
self.assertEqual(result['schoolClass']['name'], 'Klasse 2B')
|
||||
self.assertEqual(self.user.school_classes.count(), 2)
|
||||
self.assertEqual(self.user.selected_class().code, 'YYYY')
|
||||
self.assertEqual(self.user.selected_class.code, 'YYYY')
|
||||
|
||||
def test_class_already_joined(self):
|
||||
code = 'YYYY'
|
||||
|
|
|
|||
|
|
@ -82,3 +82,5 @@ class MySchoolClasses(TestCase):
|
|||
self.assertEqual(len(old_classes), 1)
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from graphql_relay import to_global_id
|
|||
from api.schema import schema
|
||||
from api.utils import get_graphql_mutation, get_object
|
||||
from core.factories import UserFactory, TeacherFactory
|
||||
from core.tests.base_test import SkillboxTestCase
|
||||
from users.models import SchoolClass, User
|
||||
from users.services import create_users
|
||||
|
||||
|
|
@ -38,18 +39,14 @@ class SchoolClassesTest(TestCase):
|
|||
self.assertEqual(f'{self.prefix} {user.pk}', class_name)
|
||||
|
||||
|
||||
class ModifySchoolClassTest(TestCase):
|
||||
class ModifySchoolClassTest(SkillboxTestCase):
|
||||
def setUp(self):
|
||||
create_users()
|
||||
self.teacher = User.objects.get(username='teacher')
|
||||
self.student = User.objects.get(username='student1')
|
||||
|
||||
request = RequestFactory().get('/')
|
||||
request.user = self.teacher
|
||||
student_request = RequestFactory().get('/')
|
||||
student_request.user = self.student
|
||||
self.client = Client(schema=schema, context_value=request)
|
||||
self.student_client = Client(schema=schema, context_value=student_request)
|
||||
self.client = self.get_client(user=self.teacher)
|
||||
self.student_client = self.get_client(user=self.student)
|
||||
|
||||
def test_update_school_class(self):
|
||||
class_name = 'The Colbert Show'
|
||||
|
|
@ -115,7 +112,7 @@ class ModifySchoolClassTest(TestCase):
|
|||
school_class = get_object(SchoolClass, id)
|
||||
self.assertEqual(school_class.name, class_name)
|
||||
self.assertEqual(school_class.get_teacher(), self.teacher)
|
||||
self.assertEqual(self.teacher.selected_class().name, class_name)
|
||||
self.assertEqual(self.teacher.selected_class.name, class_name)
|
||||
|
||||
def test_create_school_class_fail(self):
|
||||
self.assertEqual(SchoolClass.objects.count(), 2)
|
||||
|
|
|
|||
|
|
@ -1,29 +1,18 @@
|
|||
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 core.tests.base_test import SkillboxTestCase
|
||||
from users.factories import SchoolClassFactory
|
||||
from users.models import UserSetting
|
||||
from users.models import SchoolClassMember, UserSetting
|
||||
|
||||
|
||||
class UserSettingTests(TestCase):
|
||||
class UserSettingTests(SkillboxTestCase):
|
||||
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 = '''
|
||||
|
|
@ -37,7 +26,7 @@ class UserSettingTests(TestCase):
|
|||
}
|
||||
'''
|
||||
|
||||
return self.client.execute(mutation, variables={
|
||||
return self.get_client(user=self.user).execute(mutation, variables={
|
||||
'input': {
|
||||
'id': to_global_id('SchoolClassNode', class_id)
|
||||
}
|
||||
|
|
@ -50,18 +39,20 @@ class UserSettingTests(TestCase):
|
|||
selectedClass {
|
||||
name
|
||||
id
|
||||
readOnly
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
return self.client.execute(query)
|
||||
return self.get_client(user=self.user).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)
|
||||
self.assertFalse(result.get('data').get('me').get('selectedClass').get('readOnly'))
|
||||
|
||||
def test_returns_selected_class(self):
|
||||
selected_class = self.user.school_classes.all()[1]
|
||||
|
|
@ -73,7 +64,6 @@ class UserSettingTests(TestCase):
|
|||
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()
|
||||
|
|
@ -87,7 +77,6 @@ class UserSettingTests(TestCase):
|
|||
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'))
|
||||
|
|
@ -106,4 +95,17 @@ class UserSettingTests(TestCase):
|
|||
mutation_result = self.make_mutation(self.class3.pk)
|
||||
self.assertIsNotNone(mutation_result.get('errors'))
|
||||
|
||||
def test_inactive_class_as_selected_class(self):
|
||||
selected_class = self.class2
|
||||
membership = SchoolClassMember.objects.get(user=self.user, school_class=selected_class)
|
||||
self.assertTrue(membership.active)
|
||||
membership.active = False
|
||||
membership.save()
|
||||
|
||||
setting = UserSetting.objects.create(user=self.user, selected_class=selected_class)
|
||||
setting.save()
|
||||
|
||||
result = self.make_query()
|
||||
self.assertIsNone(result.get('errors'))
|
||||
self.assertTrue(result.get('data').get('me').get('selectedClass').get('readOnly'))
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue