Merged in feature/assignment-read-only (pull request #87)
Feature/assignment read only
This commit is contained in:
commit
fe3d8eb00c
|
|
@ -1,11 +1,11 @@
|
||||||
export default {
|
export default {
|
||||||
UUID: () => '123-456-789',
|
UUID: () => '123-456-789',
|
||||||
GenericStreamFieldType: () => 'GenericStreamFieldType',
|
GenericStreamFieldType: () => ({type: 'text_block', value: 'Generic Stream Field Type'}),
|
||||||
DateTime: () => '2021-01-01Z10:01:23',
|
DateTime: () => '2021-01-01Z10:01:23',
|
||||||
SnapshotNode: () => ({
|
SnapshotNode: () => ({
|
||||||
// id: ID!
|
// id: ID!
|
||||||
// module: ModuleNode!
|
// module: ModuleNode!
|
||||||
chapters: [],
|
chapters: [],
|
||||||
// chapters: [SnapshotChapterNode]
|
// chapters: [SnapshotChapterNode]
|
||||||
// hiddenContentBlocks(offset: Int, before: String, after: String, first: Int, last: Int, slug: String, title: String): ContentBlockNodeConnection!
|
// hiddenContentBlocks(offset: Int, before: String, after: String, first: Int, last: Int, slug: String, title: String): ContentBlockNodeConnection!
|
||||||
// created: DateTime!
|
// created: DateTime!
|
||||||
|
|
@ -13,13 +13,31 @@ export default {
|
||||||
// shared: Boolean!
|
// shared: Boolean!
|
||||||
// objectiveGroups: [SnapshotObjectiveGroupNode]
|
// objectiveGroups: [SnapshotObjectiveGroupNode]
|
||||||
// hiddenObjectives(offset: Int, before: String, after: String, first: Int, last: Int, text: String): ObjectiveNodeConnection!
|
// hiddenObjectives(offset: Int, before: String, after: String, first: Int, last: Int, text: String): ObjectiveNodeConnection!
|
||||||
title: 'MockSnapshotTitle',
|
title: 'MockSnapshotTitle',
|
||||||
metaTitle: 'MockSnapshotMetaTitle'
|
metaTitle: 'MockSnapshotMetaTitle',
|
||||||
// heroImage: String
|
// heroImage: String
|
||||||
// changes: SnapshotChangesNode
|
// changes: SnapshotChangesNode
|
||||||
// mine: Boolean
|
// mine: Boolean
|
||||||
}),
|
}),
|
||||||
|
ChapterNode: () => ({
|
||||||
|
slug: 'chapter-slug',
|
||||||
|
id: 'chapter-id',
|
||||||
|
title: 'chapter-title',
|
||||||
|
description: 'chapter-description',
|
||||||
|
|
||||||
|
}),
|
||||||
ContentBlockNode: () => ({
|
ContentBlockNode: () => ({
|
||||||
contents: [],
|
contents: [],
|
||||||
|
title: 'content-block-title',
|
||||||
|
slug: 'content-block-slug',
|
||||||
|
userCreated: false,
|
||||||
|
type: '',
|
||||||
|
id: 'content-block-id',
|
||||||
|
}),
|
||||||
|
AssignmentNode: () => ({
|
||||||
|
id: 'assignment-id',
|
||||||
|
title: 'Assignment Title',
|
||||||
|
assignment: 'Assignment Text',
|
||||||
|
solution: 'Assignment Solution',
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import mocks from '../../fixtures/mocks';
|
||||||
|
|
||||||
|
const myText = 'Mein Feedback';
|
||||||
|
|
||||||
|
const getOperations = ({readOnly}) => ({
|
||||||
|
MeQuery: {
|
||||||
|
me: {
|
||||||
|
onboardingVisited: true,
|
||||||
|
readOnly,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
StudentSubmissions: {
|
||||||
|
studentSubmission: {
|
||||||
|
id: 'id',
|
||||||
|
text: 'Submission Text',
|
||||||
|
document: '',
|
||||||
|
student: {
|
||||||
|
firstName: 'Peter',
|
||||||
|
lastName: 'Student',
|
||||||
|
},
|
||||||
|
assignment: {
|
||||||
|
title: 'Assignment Title',
|
||||||
|
assignment: 'Assignment Text',
|
||||||
|
},
|
||||||
|
submissionFeedback: {
|
||||||
|
id: 'feedback-id',
|
||||||
|
text: myText,
|
||||||
|
final: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Assignment feedback read-only - Teacher', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.fakeLogin('nico.teacher', 'test');
|
||||||
|
cy.server();
|
||||||
|
cy.task('getSchema').then(schema => {
|
||||||
|
cy.mockGraphql({
|
||||||
|
schema,
|
||||||
|
mocks,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can not edit', () => {
|
||||||
|
cy.mockGraphqlOps({
|
||||||
|
operations: getOperations({readOnly: true}),
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.visit('submission/submission-id');
|
||||||
|
// cy.get('.submission-form__textarea--readonly').as('textarea');
|
||||||
|
//
|
||||||
|
// cy.get('@textarea').invoke('val').should('contain', myText);
|
||||||
|
// cy.get('@textarea').should('have.attr', 'readonly');
|
||||||
|
|
||||||
|
cy.isSubmissionReadOnly(myText);
|
||||||
|
cy.canReopen(false);
|
||||||
|
});
|
||||||
|
it('can edit', () => {
|
||||||
|
cy.mockGraphqlOps({
|
||||||
|
operations: getOperations({readOnly: false}),
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.visit('submission/submission-id');
|
||||||
|
cy.canReopen(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
import mocks from '../../fixtures/mocks';
|
||||||
|
import minimalModule from '../../fixtures/module.minimal';
|
||||||
|
|
||||||
|
const module = {
|
||||||
|
...minimalModule,
|
||||||
|
chapters: [
|
||||||
|
{
|
||||||
|
contentBlocks: [
|
||||||
|
{
|
||||||
|
type: 'task',
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
type: 'text_block',
|
||||||
|
value: {
|
||||||
|
text: 'hallo velo',
|
||||||
|
},
|
||||||
|
id: 'bla-123',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'assignment',
|
||||||
|
value: {
|
||||||
|
title: 'assignment-title',
|
||||||
|
assignment: 'assignment-assignment',
|
||||||
|
id: 'some-assignment',
|
||||||
|
},
|
||||||
|
id: '123-456',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const myText = 'submission text';
|
||||||
|
|
||||||
|
const getOperations = ({final, readOnly}) => ({
|
||||||
|
MeQuery: {
|
||||||
|
me: {
|
||||||
|
onboardingVisited: true,
|
||||||
|
readOnly
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ModuleDetailsQuery: {
|
||||||
|
module,
|
||||||
|
},
|
||||||
|
AssignmentQuery: {
|
||||||
|
assignment: {
|
||||||
|
submission: {
|
||||||
|
text: myText,
|
||||||
|
final,
|
||||||
|
document: '',
|
||||||
|
submissionFeedback: null
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Assignments read-only - Student', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.fakeLogin('rahel.cueni', 'test');
|
||||||
|
cy.server();
|
||||||
|
cy.task('getSchema').then(schema => {
|
||||||
|
cy.mockGraphql({
|
||||||
|
schema,
|
||||||
|
mocks
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can edit and turn in', () => {
|
||||||
|
cy.mockGraphqlOps({
|
||||||
|
operations: getOperations({final: false, readOnly: false})
|
||||||
|
});
|
||||||
|
cy.visit('module/module-with-assignment');
|
||||||
|
|
||||||
|
cy.get('.submission-form__textarea').as('textarea');
|
||||||
|
|
||||||
|
cy.get('@textarea').invoke('val').should('contain', myText);
|
||||||
|
cy.get('@textarea').clear().type('Hello');
|
||||||
|
cy.get('@textarea').should('not.have.attr', 'readonly');
|
||||||
|
|
||||||
|
cy.getByDataCy('submission-form-submit').should('exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can not edit or turn in', () => {
|
||||||
|
cy.mockGraphqlOps({
|
||||||
|
operations: getOperations({final: false, readOnly: true})
|
||||||
|
});
|
||||||
|
cy.visit('module/module-with-assignment');
|
||||||
|
|
||||||
|
cy.isSubmissionReadOnly(myText);
|
||||||
|
cy.canReopen(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can revoke turn in', () => {
|
||||||
|
cy.mockGraphqlOps({
|
||||||
|
operations: getOperations({final: true, readOnly: false})
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.visit('module/module-with-assignment');
|
||||||
|
cy.getByDataCy('final-submission').should('exist');
|
||||||
|
cy.getByDataCy('final-submission-reopen').should('exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can not revoke turn in', () => {
|
||||||
|
cy.mockGraphqlOps({
|
||||||
|
operations: getOperations({final: true, readOnly: true})
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.visit('module/module-with-assignment');
|
||||||
|
cy.getByDataCy('final-submission').should('exist');
|
||||||
|
cy.canReopen(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -35,16 +35,16 @@ Cypress.Commands.add('apolloLogin', (username, password) => {
|
||||||
'variables': {
|
'variables': {
|
||||||
'input': {
|
'input': {
|
||||||
'usernameInput': username,
|
'usernameInput': username,
|
||||||
'passwordInput': password
|
'passwordInput': password,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
'query': 'mutation BetaLogin($input: BetaLoginInput!) {\n betaLogin(input: $input) {\n success\n __typename\n }\n}\n'
|
'query': 'mutation BetaLogin($input: BetaLoginInput!) {\n betaLogin(input: $input) {\n success\n __typename\n }\n}\n',
|
||||||
};
|
};
|
||||||
|
|
||||||
cy.request({
|
cy.request({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/api/graphql-public/',
|
url: '/api/graphql-public/',
|
||||||
body: payload
|
body: payload,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -78,8 +78,8 @@ Cypress.Commands.add('loginByCsrf', (username, password, csrftoken) => {
|
||||||
body: {
|
body: {
|
||||||
username: username,
|
username: username,
|
||||||
password: password,
|
password: password,
|
||||||
csrfmiddlewaretoken: csrftoken
|
csrfmiddlewaretoken: csrftoken,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -189,3 +189,16 @@ Cypress.Commands.add('fakeLogin', () => {
|
||||||
cy.log('Logging in (fake)');
|
cy.log('Logging in (fake)');
|
||||||
cy.setCookie('loginStatus', 'true');
|
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');
|
||||||
|
|
||||||
|
cy.get('@textarea').invoke('val').should('contain', myText);
|
||||||
|
cy.get('@textarea').should('have.attr', 'readonly');
|
||||||
|
cy.getByDataCy('submission-form-submit').should('not.exist');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -23,5 +23,9 @@ declare namespace Cypress {
|
||||||
login(username: string, password: string, visitLogin?: boolean): void
|
login(username: string, password: string, visitLogin?: boolean): void
|
||||||
|
|
||||||
fakeLogin(username: string, password: string): void
|
fakeLogin(username: string, password: string): void
|
||||||
|
|
||||||
|
canReopen(exists: boolean): void
|
||||||
|
|
||||||
|
isSubmissionReadOnly(myText: string): void
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
:spellcheck-loading="spellcheckLoading"
|
:spellcheck-loading="spellcheckLoading"
|
||||||
:saved="!unsaved"
|
:saved="!unsaved"
|
||||||
:spellcheck="true"
|
:spellcheck="true"
|
||||||
|
:read-only="me.readOnly"
|
||||||
placeholder="Ergebnis erfassen"
|
placeholder="Ergebnis erfassen"
|
||||||
action="Ergebnis mit Lehrperson teilen"
|
action="Ergebnis mit Lehrperson teilen"
|
||||||
shared-msg="Das Ergebnis wurde mit der Lehrperson geteilt."
|
shared-msg="Das Ergebnis wurde mit der Lehrperson geteilt."
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="final-submission">
|
<div
|
||||||
|
class="final-submission"
|
||||||
|
data-cy="final-submission">
|
||||||
<document-block
|
<document-block
|
||||||
:value="{url: userInput.document}"
|
:value="{url: userInput.document}"
|
||||||
class="final-submission__document"
|
class="final-submission__document"
|
||||||
|
|
@ -10,6 +12,8 @@
|
||||||
<span class="final-submission__explanation-text">{{ sharedMsg }}</span>
|
<span class="final-submission__explanation-text">{{ sharedMsg }}</span>
|
||||||
<a
|
<a
|
||||||
class="final-submission__reopen"
|
class="final-submission__reopen"
|
||||||
|
data-cy="final-submission-reopen"
|
||||||
|
v-if="showReopen"
|
||||||
@click="$emit('reopen')">Bearbeiten</a>
|
@click="$emit('reopen')">Bearbeiten</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -21,7 +25,20 @@
|
||||||
import {newLineToParagraph} from '@/helpers/text';
|
import {newLineToParagraph} from '@/helpers/text';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['userInput', 'sharedMsg'],
|
props: {
|
||||||
|
userInput: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
showReopen: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
sharedMsg: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
|
|
@ -37,9 +54,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import "@/styles/_variables.scss";
|
@import "~styles/helpers";
|
||||||
@import "@/styles/_mixins.scss";
|
|
||||||
@import "@/styles/_functions.scss";
|
|
||||||
|
|
||||||
.final-submission {
|
.final-submission {
|
||||||
&__text {
|
&__text {
|
||||||
|
|
@ -66,18 +81,21 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__explanation-icon {
|
&__explanation-icon {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
fill: $color-brand;
|
fill: $color-brand;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__explanation-text {
|
&__explanation-text {
|
||||||
color: $color-brand;
|
color: $color-brand;
|
||||||
font-family: $sans-serif-font-family;
|
font-family: $sans-serif-font-family;
|
||||||
font-weight: $font-weight-regular;
|
font-weight: $font-weight-regular;
|
||||||
margin-right: $medium-spacing;
|
margin-right: $medium-spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__reopen {
|
&__reopen {
|
||||||
@include small-text;
|
@include small-text;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,18 @@
|
||||||
<submission-input
|
<submission-input
|
||||||
:input-text="userInput.text"
|
:input-text="userInput.text"
|
||||||
:saved="saved"
|
:saved="saved"
|
||||||
:final="final"
|
:readonly="isReadOnly"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:reopen="reopenSubmission"
|
|
||||||
@input="saveInput"
|
@input="saveInput"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="submission-form-container__actions"
|
class="submission-form-container__actions"
|
||||||
v-if="!final">
|
v-if="!isReadOnly">
|
||||||
<button
|
<button
|
||||||
class="submission-form-container__submit button button--primary button--white-bg"
|
class="submission-form-container__submit button button--primary button--white-bg"
|
||||||
|
data-cy="submission-form-submit"
|
||||||
@click="$emit('turnIn')"
|
@click="$emit('turnIn')"
|
||||||
>{{ action }}
|
>{{ action }}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -45,7 +45,8 @@
|
||||||
<final-submission
|
<final-submission
|
||||||
:user-input="userInput"
|
:user-input="userInput"
|
||||||
:shared-msg="sharedMsg"
|
:shared-msg="sharedMsg"
|
||||||
v-if="final"
|
:show-reopen="!readOnly"
|
||||||
|
v-if="isFinalOrReadOnly"
|
||||||
@reopen="$emit('reopen')"/>
|
@reopen="$emit('reopen')"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -64,6 +65,10 @@
|
||||||
action: String,
|
action: String,
|
||||||
reopen: Function,
|
reopen: Function,
|
||||||
document: String,
|
document: String,
|
||||||
|
readOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
spellcheck: {
|
spellcheck: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
|
@ -86,6 +91,9 @@
|
||||||
final() {
|
final() {
|
||||||
return !!this.userInput && this.userInput.final;
|
return !!this.userInput && this.userInput.final;
|
||||||
},
|
},
|
||||||
|
isFinalOrReadOnly() {
|
||||||
|
return this.final || this.readOnly;
|
||||||
|
},
|
||||||
allowsDocuments() {
|
allowsDocuments() {
|
||||||
return 'document' in this.userInput;
|
return 'document' in this.userInput;
|
||||||
},
|
},
|
||||||
|
|
@ -117,7 +125,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import '@/styles/_mixins.scss';
|
@import '~styles/helpers';
|
||||||
|
|
||||||
.submission-form-container {
|
.submission-form-container {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,9 @@
|
||||||
<textarea
|
<textarea
|
||||||
v-auto-grow
|
v-auto-grow
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:readonly="final"
|
:readonly="readonly"
|
||||||
:value="inputText"
|
:value="inputText"
|
||||||
|
:class="{'submission-form__textarea--readonly': readonly}"
|
||||||
rows="1"
|
rows="1"
|
||||||
class="submission-form__textarea"
|
class="submission-form__textarea"
|
||||||
@input="$emit('input', $event.target.value)"
|
@input="$emit('input', $event.target.value)"
|
||||||
|
|
@ -30,7 +31,7 @@
|
||||||
props: {
|
props: {
|
||||||
inputText: String,
|
inputText: String,
|
||||||
saved: Boolean,
|
saved: Boolean,
|
||||||
final: Boolean,
|
readonly: Boolean,
|
||||||
placeholder: {
|
placeholder: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'Ergebnis erfassen'
|
default: 'Ergebnis erfassen'
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import "@/styles/_variables.scss";
|
@import "~styles/helpers";
|
||||||
|
|
||||||
.assignment-form {
|
.assignment-form {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ fragment UserParts on PrivateUserNode {
|
||||||
lastName
|
lastName
|
||||||
avatarUrl
|
avatarUrl
|
||||||
expiryDate
|
expiryDate
|
||||||
|
readOnly
|
||||||
lastModule {
|
lastModule {
|
||||||
id
|
id
|
||||||
slug
|
slug
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
<submission-form
|
<submission-form
|
||||||
:user-input="feedback"
|
:user-input="feedback"
|
||||||
:saved="!unsaved"
|
:saved="!unsaved"
|
||||||
|
:read-only="readOnly"
|
||||||
placeholder="Feedback erfassen"
|
placeholder="Feedback erfassen"
|
||||||
action="Feedback teilen"
|
action="Feedback teilen"
|
||||||
shared-msg="Dieses Feedback wurde geteilt."
|
shared-msg="Dieses Feedback wurde geteilt."
|
||||||
|
|
@ -55,32 +56,36 @@
|
||||||
import UPDATE_FEEDBACK_WITH_TEXT_MUTATION from '@/graphql/gql/mutations/updateFeedbackWithText.gql';
|
import UPDATE_FEEDBACK_WITH_TEXT_MUTATION from '@/graphql/gql/mutations/updateFeedbackWithText.gql';
|
||||||
import SubmissionForm from '@/components/content-blocks/assignment/SubmissionForm';
|
import SubmissionForm from '@/components/content-blocks/assignment/SubmissionForm';
|
||||||
|
|
||||||
|
import me from '@/mixins/me';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
||||||
|
mixins: [me],
|
||||||
components: {
|
components: {
|
||||||
StudentSubmissionDocument,
|
StudentSubmissionDocument,
|
||||||
SubmissionForm
|
SubmissionForm,
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
studentSubmission: {
|
studentSubmission: {
|
||||||
assignment: {
|
assignment: {
|
||||||
title: ''
|
title: '',
|
||||||
},
|
},
|
||||||
student: {
|
student: {
|
||||||
firstName: '',
|
firstName: '',
|
||||||
lastName: ''
|
lastName: '',
|
||||||
},
|
},
|
||||||
text: '',
|
text: '',
|
||||||
document: '',
|
document: '',
|
||||||
submissionFeedback: {
|
submissionFeedback: {
|
||||||
text: '',
|
text: '',
|
||||||
final: false
|
final: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
unsaved: false,
|
unsaved: false,
|
||||||
saving: 0,
|
saving: 0,
|
||||||
emojis: ['👍', '👎', '🙂', '😐', '😕', '🙁', '😮', '😉', '🙄', '❕', '❔', '🧐', '🤩', '🤗', '🤬', '🤢']
|
emojis: ['👍', '👎', '🙂', '😐', '😕', '🙁', '😮', '😉', '🙄', '❕', '❔', '🧐', '🤩', '🤗', '🤬', '🤢'],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -97,8 +102,11 @@
|
||||||
feedback() {
|
feedback() {
|
||||||
return this.studentSubmission.submissionFeedback ? this.studentSubmission.submissionFeedback : {
|
return this.studentSubmission.submissionFeedback ? this.studentSubmission.submissionFeedback : {
|
||||||
text: '',
|
text: '',
|
||||||
final: false
|
final: false,
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
readOnly() {
|
||||||
|
return this.me.readOnly;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -108,7 +116,7 @@
|
||||||
query: STUDENT_SUBMISSIONS_QUERY,
|
query: STUDENT_SUBMISSIONS_QUERY,
|
||||||
variables() {
|
variables() {
|
||||||
return {
|
return {
|
||||||
id: this.$route.params.id
|
id: this.$route.params.id,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
result({data: {studentSubmission}}) {
|
result({data: {studentSubmission}}) {
|
||||||
|
|
@ -116,7 +124,7 @@
|
||||||
this.create();
|
this.create();
|
||||||
}
|
}
|
||||||
this.studentSubmission = cloneDeep(studentSubmission); // we don't want to update the value when the server updates
|
this.studentSubmission = cloneDeep(studentSubmission); // we don't want to update the value when the server updates
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -135,10 +143,10 @@
|
||||||
submissionFeedback: {
|
submissionFeedback: {
|
||||||
studentSubmission: this.studentSubmission.id,
|
studentSubmission: this.studentSubmission.id,
|
||||||
text: this.studentSubmission.submissionFeedback.text,
|
text: this.studentSubmission.submissionFeedback.text,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
update: this.updateCache
|
update: this.updateCache,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.saving--;
|
this.saving--;
|
||||||
if (this.saving === 0) {
|
if (this.saving === 0) {
|
||||||
|
|
@ -155,53 +163,52 @@
|
||||||
this.updateFeedbackText(feedbackText);
|
this.updateFeedbackText(feedbackText);
|
||||||
this._save();
|
this._save();
|
||||||
},
|
},
|
||||||
create() {
|
update({withText, text, final}) {
|
||||||
|
if (this.readOnly) {
|
||||||
|
this.$log.debug('read-only');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mutation = withText ? UPDATE_FEEDBACK_WITH_TEXT_MUTATION : UPDATE_FEEDBACK_MUTATION;
|
||||||
this.$apollo.mutate({
|
this.$apollo.mutate({
|
||||||
mutation: UPDATE_FEEDBACK_WITH_TEXT_MUTATION,
|
mutation,
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
submissionFeedback: {
|
submissionFeedback: {
|
||||||
studentSubmission: this.studentSubmission.id,
|
studentSubmission: this.studentSubmission.id,
|
||||||
text: '',
|
text: text,
|
||||||
final: false
|
final: final,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
update: this.updateCache
|
update: this.updateCache,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
create() {
|
||||||
|
this.$log.debug('create');
|
||||||
|
this.update({
|
||||||
|
withText: true,
|
||||||
|
text: '',
|
||||||
|
final: false,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
turnIn() {
|
turnIn() {
|
||||||
this.$apollo.mutate({
|
this.$log.debug('turnIn');
|
||||||
mutation: UPDATE_FEEDBACK_WITH_TEXT_MUTATION,
|
this.update({
|
||||||
variables: {
|
withText: true,
|
||||||
input: {
|
text: this.studentSubmission.submissionFeedback.text,
|
||||||
submissionFeedback: {
|
final: true,
|
||||||
studentSubmission: this.studentSubmission.id,
|
|
||||||
text: this.studentSubmission.submissionFeedback.text,
|
|
||||||
final: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
update: this.updateCache
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
reopen() {
|
reopen() {
|
||||||
|
this.$log.debug('reopen');
|
||||||
if (!this.studentSubmission.id) {
|
if (!this.studentSubmission.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$apollo.mutate({
|
this.update({
|
||||||
mutation: UPDATE_FEEDBACK_MUTATION,
|
withText: false,
|
||||||
variables: {
|
text: this.studentSubmission.submissionFeedback.text,
|
||||||
input: {
|
final: false,
|
||||||
submissionFeedback: {
|
|
||||||
studentSubmission: this.studentSubmission.id,
|
|
||||||
text: this.studentSubmission.submissionFeedback.text,
|
|
||||||
final: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
update: this.updateCache
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
updateCache(store, {data: {updateSubmissionFeedback: {successful, updatedSubmissionFeedback}}}) {
|
updateCache(store, {data: {updateSubmissionFeedback: {successful, updatedSubmissionFeedback}}}) {
|
||||||
|
|
@ -209,20 +216,20 @@
|
||||||
if (successful) {
|
if (successful) {
|
||||||
const query = STUDENT_SUBMISSIONS_QUERY;
|
const query = STUDENT_SUBMISSIONS_QUERY;
|
||||||
const variables = {
|
const variables = {
|
||||||
id: this.studentSubmission.id
|
id: this.studentSubmission.id,
|
||||||
};
|
};
|
||||||
const data = store.readQuery({query, variables});
|
const data = store.readQuery({query, variables});
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
if (!data.studentSubmission.submissionFeedback) {
|
if (!data.studentSubmission.submissionFeedback) {
|
||||||
data.studentSubmission.submissionFeedback = {
|
data.studentSubmission.submissionFeedback = {
|
||||||
'__typename': 'SubmissionFeedbackNode'
|
'__typename': 'SubmissionFeedbackNode',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
data.studentSubmission.submissionFeedback = Object.assign({}, data.studentSubmission.submissionFeedback, {
|
data.studentSubmission.submissionFeedback = Object.assign({}, data.studentSubmission.submissionFeedback, {
|
||||||
id: updatedSubmissionFeedback.id,
|
id: updatedSubmissionFeedback.id,
|
||||||
final: updatedSubmissionFeedback.final
|
final: updatedSubmissionFeedback.final,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (updatedSubmissionFeedback.text !== undefined) {
|
if (updatedSubmissionFeedback.text !== undefined) {
|
||||||
|
|
@ -242,16 +249,15 @@
|
||||||
},
|
},
|
||||||
updateFeedbackText(text) {
|
updateFeedbackText(text) {
|
||||||
this.studentSubmission = Object.assign({}, this.studentSubmission, {
|
this.studentSubmission = Object.assign({}, this.studentSubmission, {
|
||||||
submissionFeedback: Object.assign({}, this.studentSubmission.submissionFeedback, {text: text})
|
submissionFeedback: Object.assign({}, this.studentSubmission.submissionFeedback, {text: text}),
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import "@/styles/_variables.scss";
|
@import "~styles/helpers";
|
||||||
@import "@/styles/_functions.scss";
|
|
||||||
|
|
||||||
.submission-page {
|
.submission-page {
|
||||||
&__content {
|
&__content {
|
||||||
|
|
|
||||||
|
|
@ -23,13 +23,17 @@ class UpdateAssignment(relay.ClientIDMutation):
|
||||||
def mutate_and_get_payload(cls, root, info, **kwargs):
|
def mutate_and_get_payload(cls, root, info, **kwargs):
|
||||||
assignment_data = kwargs.get('assignment')
|
assignment_data = kwargs.get('assignment')
|
||||||
assignment = get_object(Assignment, assignment_data.get('id'))
|
assignment = get_object(Assignment, assignment_data.get('id'))
|
||||||
|
student = info.context.user
|
||||||
|
|
||||||
|
if student.read_only:
|
||||||
|
raise PermissionError('No valid license')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
(submission, _created) = assignment.submissions.get_or_create(student=info.context.user)
|
(submission, _created) = assignment.submissions.get_or_create(student=student)
|
||||||
except MultipleObjectsReturned:
|
except MultipleObjectsReturned:
|
||||||
for submission in assignment.submissions.filter(student=info.context.user):
|
for submission in assignment.submissions.filter(student=student):
|
||||||
submission.delete()
|
submission.delete()
|
||||||
submission = assignment.submissions.create(student=info.context.user)
|
submission = assignment.submissions.create(student=student)
|
||||||
submission.text = assignment_data.get('answer', '')
|
submission.text = assignment_data.get('answer', '')
|
||||||
submission.document = assignment_data.get('document', '')
|
submission.document = assignment_data.get('document', '')
|
||||||
final = assignment_data.get('final')
|
final = assignment_data.get('final')
|
||||||
|
|
@ -56,6 +60,9 @@ class UpdateSubmissionFeedback(relay.ClientIDMutation):
|
||||||
if not user.has_perm('users.can_manage_school_class_content'):
|
if not user.has_perm('users.can_manage_school_class_content'):
|
||||||
raise PermissionDenied('Missing permissions')
|
raise PermissionDenied('Missing permissions')
|
||||||
|
|
||||||
|
if user.read_only:
|
||||||
|
raise PermissionError('No valid license')
|
||||||
|
|
||||||
(submission_feedback, created) = SubmissionFeedback.objects.get_or_create(teacher=user,
|
(submission_feedback, created) = SubmissionFeedback.objects.get_or_create(teacher=user,
|
||||||
student_submission_id=student_submission_id)
|
student_submission_id=student_submission_id)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
UPDATE_ASSIGNMENT_MUTATION = '''
|
||||||
|
mutation UpdateAssignment($input: UpdateAssignmentInput!) {
|
||||||
|
updateAssignment(input: $input){
|
||||||
|
updatedAssignment {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
assignment
|
||||||
|
submission {
|
||||||
|
id
|
||||||
|
text
|
||||||
|
final
|
||||||
|
document
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
UPDATE_SUBMISSION_FEEDBACK_MUTATION = '''
|
||||||
|
mutation UpdateSubmissionFeedback($input: UpdateSubmissionFeedbackInput!) {
|
||||||
|
updateSubmissionFeedback(input: $input){
|
||||||
|
updatedSubmissionFeedback {
|
||||||
|
id
|
||||||
|
text
|
||||||
|
final
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
@ -2,6 +2,7 @@ from graphql_relay import to_global_id
|
||||||
|
|
||||||
from api.test_utils import create_client, DefaultUserTestCase
|
from api.test_utils import create_client, DefaultUserTestCase
|
||||||
from assignments.models import Assignment, StudentSubmission
|
from assignments.models import Assignment, StudentSubmission
|
||||||
|
from .queries.mutations import UPDATE_ASSIGNMENT_MUTATION
|
||||||
from ..factories import AssignmentFactory
|
from ..factories import AssignmentFactory
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -16,31 +17,14 @@ class AssignmentPermissionsTestCase(DefaultUserTestCase):
|
||||||
self.module_id = to_global_id('ModuleNode', self.assignment.module.pk)
|
self.module_id = to_global_id('ModuleNode', self.assignment.module.pk)
|
||||||
|
|
||||||
def _submit_submission(self, user=None):
|
def _submit_submission(self, user=None):
|
||||||
mutation = '''
|
|
||||||
mutation UpdateAssignment($input: UpdateAssignmentInput!) {
|
|
||||||
updateAssignment(input: $input){
|
|
||||||
updatedAssignment {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
assignment
|
|
||||||
submission {
|
|
||||||
id
|
|
||||||
text
|
|
||||||
final
|
|
||||||
document
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
if user is None:
|
if user is None:
|
||||||
client = create_client(self.student1)
|
client = create_client(self.student1)
|
||||||
else:
|
else:
|
||||||
client = create_client(user)
|
client = create_client(user)
|
||||||
|
|
||||||
return client.execute(mutation, variables={
|
return client.execute(UPDATE_ASSIGNMENT_MUTATION, variables={
|
||||||
'input': {
|
'input': {
|
||||||
"assignment": {
|
"assignment": {
|
||||||
"id": self.assignment_id,
|
"id": self.assignment_id,
|
||||||
|
|
|
||||||
|
|
@ -5,24 +5,10 @@ from graphql_relay import to_global_id
|
||||||
from api.schema import schema
|
from api.schema import schema
|
||||||
from assignments.factories import AssignmentFactory
|
from assignments.factories import AssignmentFactory
|
||||||
from assignments.models import StudentSubmission
|
from assignments.models import StudentSubmission
|
||||||
|
from assignments.tests.queries.mutations import UPDATE_ASSIGNMENT_MUTATION
|
||||||
from users.models import User
|
from users.models import User
|
||||||
from users.services import create_users
|
from users.services import create_users
|
||||||
|
|
||||||
UPDATE_ASSIGNMENT_MUTATION = """
|
|
||||||
mutation UpdateAssignment($input: UpdateAssignmentInput!) {
|
|
||||||
updateAssignment(input: $input) {
|
|
||||||
updatedAssignment {
|
|
||||||
id
|
|
||||||
submission {
|
|
||||||
id
|
|
||||||
text
|
|
||||||
final
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class DuplicateStudentSubmissionsTestCase(TestCase):
|
class DuplicateStudentSubmissionsTestCase(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,16 @@ from graphql_relay import to_global_id
|
||||||
|
|
||||||
from api.test_utils import create_client, DefaultUserTestCase
|
from api.test_utils import create_client, DefaultUserTestCase
|
||||||
from assignments.models import Assignment, StudentSubmission
|
from assignments.models import Assignment, StudentSubmission
|
||||||
|
from core.tests.base_test import SkillboxTestCase
|
||||||
from users.factories import SchoolClassFactory
|
from users.factories import SchoolClassFactory
|
||||||
from users.models import SchoolClassMember
|
from users.models import SchoolClassMember
|
||||||
|
from .queries.mutations import UPDATE_SUBMISSION_FEEDBACK_MUTATION
|
||||||
from ..factories import AssignmentFactory, StudentSubmissionFactory, SubmissionFeedbackFactory
|
from ..factories import AssignmentFactory, StudentSubmissionFactory, SubmissionFeedbackFactory
|
||||||
|
|
||||||
|
|
||||||
class SubmissionFeedbackTestCase(DefaultUserTestCase):
|
class SubmissionFeedbackTestCase(SkillboxTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(SubmissionFeedbackTestCase, self).setUp()
|
self.createDefault()
|
||||||
|
|
||||||
self.assignment = AssignmentFactory(
|
self.assignment = AssignmentFactory(
|
||||||
owner=self.teacher
|
owner=self.teacher
|
||||||
|
|
@ -38,21 +40,7 @@ class SubmissionFeedbackTestCase(DefaultUserTestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
def _create_submission_feedback(self, user, final, text, student_submission_id):
|
def _create_submission_feedback(self, user, final, text, student_submission_id):
|
||||||
mutation = '''
|
return self.get_client(user).execute(UPDATE_SUBMISSION_FEEDBACK_MUTATION, variables={
|
||||||
mutation UpdateSubmissionFeedback($input: UpdateSubmissionFeedbackInput!) {
|
|
||||||
updateSubmissionFeedback(input: $input){
|
|
||||||
updatedSubmissionFeedback {
|
|
||||||
id
|
|
||||||
text
|
|
||||||
final
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'''
|
|
||||||
|
|
||||||
client = create_client(user)
|
|
||||||
|
|
||||||
return client.execute(mutation, variables={
|
|
||||||
'input': {
|
'input': {
|
||||||
"submissionFeedback": {
|
"submissionFeedback": {
|
||||||
"studentSubmission": student_submission_id,
|
"studentSubmission": student_submission_id,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
from datetime import timedelta, date
|
||||||
|
|
||||||
|
from graphql_relay import to_global_id
|
||||||
|
|
||||||
|
from assignments.factories import AssignmentFactory, StudentSubmissionFactory
|
||||||
|
from assignments.tests.queries.mutations import UPDATE_ASSIGNMENT_MUTATION, UPDATE_SUBMISSION_FEEDBACK_MUTATION
|
||||||
|
from core.tests.base_test import SkillboxTestCase
|
||||||
|
from users.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class AssignmentReadOnlyTestCase(SkillboxTestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.createDefault()
|
||||||
|
yesterday = date.today() - timedelta(days=1)
|
||||||
|
|
||||||
|
self.student1.license_expiry_date = yesterday
|
||||||
|
self.student1.save()
|
||||||
|
self.teacher.license_expiry_date = yesterday
|
||||||
|
self.teacher.save()
|
||||||
|
self.assignment = AssignmentFactory()
|
||||||
|
self.assignment_id = to_global_id('AssignmentNode', self.assignment.id)
|
||||||
|
|
||||||
|
def test_edit_assignment_fails(self):
|
||||||
|
variables = {
|
||||||
|
"input": {
|
||||||
|
"assignment": {
|
||||||
|
"answer": "bla",
|
||||||
|
"document": "",
|
||||||
|
"final": False,
|
||||||
|
"id": self.assignment_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = self.get_client(self.student1).execute(UPDATE_ASSIGNMENT_MUTATION, variables=variables)
|
||||||
|
self.assertIsNotNone(result.get('errors'))
|
||||||
|
|
||||||
|
def test_share_assignment_fails(self):
|
||||||
|
variables = {
|
||||||
|
"input": {
|
||||||
|
"assignment": {
|
||||||
|
"answer": "bla",
|
||||||
|
"document": "",
|
||||||
|
"final": True,
|
||||||
|
"id": self.assignment_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = self.get_client(self.student1).execute(UPDATE_ASSIGNMENT_MUTATION, variables=variables)
|
||||||
|
self.assertIsNotNone(result.get('errors'))
|
||||||
|
|
||||||
|
def test_edit_feedback_fails(self):
|
||||||
|
student_submission = StudentSubmissionFactory(assignment=self.assignment, student=self.student1,
|
||||||
|
final=True)
|
||||||
|
student_submission_id = to_global_id('StudentSubmissionNode', student_submission.id)
|
||||||
|
result = self.get_client(self.teacher).execute(UPDATE_SUBMISSION_FEEDBACK_MUTATION, variables={
|
||||||
|
'input': {
|
||||||
|
"submissionFeedback": {
|
||||||
|
"studentSubmission": student_submission_id,
|
||||||
|
"text": "Feedback",
|
||||||
|
"final": False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertIsNotNone(result.get('errors'))
|
||||||
|
|
||||||
|
|
||||||
|
def test_share_feedback_fails(self):
|
||||||
|
student_submission = StudentSubmissionFactory(assignment=self.assignment, student=self.student1,
|
||||||
|
final=True)
|
||||||
|
student_submission_id = to_global_id('StudentSubmissionNode', student_submission.id)
|
||||||
|
result = self.get_client(self.teacher).execute(UPDATE_SUBMISSION_FEEDBACK_MUTATION, variables={
|
||||||
|
'input': {
|
||||||
|
"submissionFeedback": {
|
||||||
|
"studentSubmission": student_submission_id,
|
||||||
|
"text": "Feedback",
|
||||||
|
"final": True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertIsNotNone(result.get('errors'))
|
||||||
|
|
@ -10,6 +10,10 @@ class SkillboxTestCase(TestCase):
|
||||||
def createDefault(self) -> None:
|
def createDefault(self) -> None:
|
||||||
create_users()
|
create_users()
|
||||||
self.teacher = User.objects.get(username='teacher')
|
self.teacher = User.objects.get(username='teacher')
|
||||||
|
self.teacher2 = User.objects.get(username='teacher2')
|
||||||
|
self.student1 = User.objects.get(username='student1')
|
||||||
|
self.student2 = User.objects.get(username='student2')
|
||||||
|
self.student_second_class = User.objects.get(username='student_second_class')
|
||||||
|
|
||||||
def get_client(self, user=None) -> Client:
|
def get_client(self, user=None) -> Client:
|
||||||
request = RequestFactory().get('/')
|
request = RequestFactory().get('/')
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ class MiddlewareTestCase(TestCase):
|
||||||
|
|
||||||
self.assertTrue(is_private_api_call_allowed(user, body))
|
self.assertTrue(is_private_api_call_allowed(user, body))
|
||||||
|
|
||||||
def test_user_without_valid_license_cannot_see_private_api(self):
|
def test_user_with_expired_license_can_see_private_api(self):
|
||||||
|
|
||||||
yesterday = timezone.now() - timedelta(1)
|
yesterday = timezone.now() - timedelta(1)
|
||||||
user = UserFactory(username='aschiman@ch.ch', hep_id=23)
|
user = UserFactory(username='aschiman@ch.ch', hep_id=23)
|
||||||
|
|
@ -26,6 +26,14 @@ class MiddlewareTestCase(TestCase):
|
||||||
|
|
||||||
body = b'"{mutation {\\n addRoom}"'
|
body = b'"{mutation {\\n addRoom}"'
|
||||||
|
|
||||||
|
self.assertTrue(is_private_api_call_allowed(user, body))
|
||||||
|
|
||||||
|
def test_user_without_valid_license_cannot_see_private_api(self):
|
||||||
|
user = UserFactory(username='aschiman@ch.ch', hep_id=23)
|
||||||
|
user.license_expiry_date = None
|
||||||
|
|
||||||
|
body = b'"{mutation {\\n addRoom}"'
|
||||||
|
|
||||||
self.assertFalse(is_private_api_call_allowed(user, body))
|
self.assertFalse(is_private_api_call_allowed(user, body))
|
||||||
|
|
||||||
def test_logout_is_allowed_without_valid_license(self):
|
def test_logout_is_allowed_without_valid_license(self):
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ def is_private_api_call_allowed(user, body):
|
||||||
license_expiry = user.license_expiry_date
|
license_expiry = user.license_expiry_date
|
||||||
|
|
||||||
# all other resources are denied if the license is not valid
|
# all other resources are denied if the license is not valid
|
||||||
if license_expiry is None or license_expiry < timezone.now().date():
|
if license_expiry is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
|
||||||
|
|
@ -788,6 +788,7 @@ type PrivateUserNode implements Node {
|
||||||
isTeacher: Boolean
|
isTeacher: Boolean
|
||||||
oldClasses(offset: Int, before: String, after: String, first: Int, last: Int, name: String): SchoolClassNodeConnection
|
oldClasses(offset: Int, before: String, after: String, first: Int, last: Int, name: String): SchoolClassNodeConnection
|
||||||
recentModules(offset: Int, before: String, after: String, first: Int, last: Int, recentModules: [ID], orderBy: String): ModuleNodeConnection
|
recentModules(offset: Int, before: String, after: String, first: Int, last: Int, recentModules: [ID], orderBy: String): ModuleNodeConnection
|
||||||
|
readOnly: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type PrivateUserNodeConnection {
|
type PrivateUserNodeConnection {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import string
|
import string
|
||||||
from datetime import datetime
|
from datetime import date, datetime
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.models import AbstractUser, Permission
|
from django.contrib.auth.models import AbstractUser, Permission
|
||||||
|
|
@ -16,6 +16,10 @@ DEFAULT_SCHOOL_ID = 1
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractUser):
|
class User(AbstractUser):
|
||||||
|
LICENSE_NONE = 'no-license'
|
||||||
|
LICENSE_VALID = 'valid-license'
|
||||||
|
LICENSE_EXPIRED = 'expired-license'
|
||||||
|
|
||||||
last_module = models.ForeignKey('books.Module', related_name='+', on_delete=models.SET_NULL, null=True)
|
last_module = models.ForeignKey('books.Module', related_name='+', on_delete=models.SET_NULL, null=True)
|
||||||
recent_modules = models.ManyToManyField('books.Module', related_name='+', through='books.RecentModule')
|
recent_modules = models.ManyToManyField('books.Module', related_name='+', through='books.RecentModule')
|
||||||
last_topic = models.ForeignKey('books.Topic', related_name='+', on_delete=models.SET_NULL, null=True)
|
last_topic = models.ForeignKey('books.Topic', related_name='+', on_delete=models.SET_NULL, null=True)
|
||||||
|
|
@ -114,6 +118,17 @@ class User(AbstractUser):
|
||||||
def full_name(self):
|
def full_name(self):
|
||||||
return self.get_full_name()
|
return self.get_full_name()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def read_only(self):
|
||||||
|
return self.get_license_status() == User.LICENSE_EXPIRED
|
||||||
|
|
||||||
|
def get_license_status(self):
|
||||||
|
if self.license_expiry_date is None:
|
||||||
|
return User.LICENSE_NONE
|
||||||
|
if self.license_expiry_date >= date.today():
|
||||||
|
return User.LICENSE_VALID
|
||||||
|
return User.LICENSE_EXPIRED
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['pk', ]
|
ordering = ['pk', ]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ class PrivateUserNode(DjangoObjectType):
|
||||||
filter_fields = ['username', 'email']
|
filter_fields = ['username', 'email']
|
||||||
only_fields = ['username', 'email', 'first_name', 'last_name', 'school_classes', 'last_module',
|
only_fields = ['username', 'email', 'first_name', 'last_name', 'school_classes', 'last_module',
|
||||||
'last_topic', 'avatar_url',
|
'last_topic', 'avatar_url',
|
||||||
'selected_class', 'expiry_date', 'onboarding_visited', 'team']
|
'selected_class', 'expiry_date', 'onboarding_visited', 'team', 'read_only']
|
||||||
interfaces = (relay.Node,)
|
interfaces = (relay.Node,)
|
||||||
|
|
||||||
pk = graphene.Int()
|
pk = graphene.Int()
|
||||||
|
|
@ -99,6 +99,7 @@ class PrivateUserNode(DjangoObjectType):
|
||||||
old_classes = DjangoFilterConnectionField(SchoolClassNode)
|
old_classes = DjangoFilterConnectionField(SchoolClassNode)
|
||||||
recent_modules = DjangoFilterConnectionField(ModuleNode, filterset_class=RecentModuleFilter)
|
recent_modules = DjangoFilterConnectionField(ModuleNode, filterset_class=RecentModuleFilter)
|
||||||
team = graphene.Field(TeamNode)
|
team = graphene.Field(TeamNode)
|
||||||
|
read_only = graphene.Boolean()
|
||||||
|
|
||||||
def resolve_pk(self, info, **kwargs):
|
def resolve_pk(self, info, **kwargs):
|
||||||
return self.id
|
return self.id
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
from datetime import timedelta, date
|
||||||
|
|
||||||
|
from core.factories import UserFactory
|
||||||
|
from core.tests.base_test import SkillboxTestCase
|
||||||
|
from users.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class LicenseExpiredTestCase(SkillboxTestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.user = UserFactory(username='some_user')
|
||||||
|
|
||||||
|
def test_license_expired(self):
|
||||||
|
self.user.license_expiry_date = date.today() - timedelta(days=1)
|
||||||
|
self.user.save()
|
||||||
|
self.assertTrue(self.user.get_license_status(), User.LICENSE_EXPIRED)
|
||||||
|
|
||||||
|
def test_license_valid(self):
|
||||||
|
self.user.license_expiry_date = date.today() + timedelta(days=1)
|
||||||
|
self.user.save()
|
||||||
|
self.assertTrue(self.user.get_license_status(), User.LICENSE_VALID)
|
||||||
|
|
||||||
|
def test_license_none(self):
|
||||||
|
self.assertTrue(self.user.get_license_status(), User.LICENSE_NONE)
|
||||||
|
|
@ -1,14 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# ITerativ GmbH
|
|
||||||
# http://www.iterativ.ch/
|
|
||||||
#
|
|
||||||
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
|
|
||||||
#
|
|
||||||
# Created on 2019-10-02
|
|
||||||
# @author: chrigu <christian.cueni@iterativ.ch>
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue