From e4bb4cc9ee40a244717a6114029be98a80aa2e2d Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Thu, 1 Jul 2021 16:58:56 +0200 Subject: [PATCH 01/11] Add tests for read only assignments --- client/cypress/fixtures/mocks.js | 26 +++++- ...ignment-feedback-read-only.teacher.spec.js | 8 ++ .../assignment-read-only.student.spec.js | 82 +++++++++++++++++++ .../assignment/SubmissionForm.vue | 1 + .../src/graphql/gql/fragments/userParts.gql | 1 + server/assignments/tests/test_read_only.py | 15 ++++ server/schema.graphql | 1 + server/users/models.py | 5 ++ server/users/schema.py | 3 +- 9 files changed, 137 insertions(+), 5 deletions(-) create mode 100644 client/cypress/integration/frontend/assignment-feedback-read-only.teacher.spec.js create mode 100644 client/cypress/integration/frontend/assignment-read-only.student.spec.js create mode 100644 server/assignments/tests/test_read_only.py diff --git a/client/cypress/fixtures/mocks.js b/client/cypress/fixtures/mocks.js index d54c15ce..bd2e669d 100644 --- a/client/cypress/fixtures/mocks.js +++ b/client/cypress/fixtures/mocks.js @@ -1,11 +1,11 @@ export default { UUID: () => '123-456-789', - GenericStreamFieldType: () => 'GenericStreamFieldType', + GenericStreamFieldType: () => ({type: 'text_block', value: 'Generic Stream Field Type'}), DateTime: () => '2021-01-01Z10:01:23', SnapshotNode: () => ({ // id: ID! // module: ModuleNode! - chapters: [], + chapters: [], // chapters: [SnapshotChapterNode] // hiddenContentBlocks(offset: Int, before: String, after: String, first: Int, last: Int, slug: String, title: String): ContentBlockNodeConnection! // created: DateTime! @@ -13,13 +13,31 @@ export default { // shared: Boolean! // objectiveGroups: [SnapshotObjectiveGroupNode] // hiddenObjectives(offset: Int, before: String, after: String, first: Int, last: Int, text: String): ObjectiveNodeConnection! - title: 'MockSnapshotTitle', - metaTitle: 'MockSnapshotMetaTitle' + title: 'MockSnapshotTitle', + metaTitle: 'MockSnapshotMetaTitle', // heroImage: String // changes: SnapshotChangesNode // mine: Boolean }), + ChapterNode: () => ({ + slug: 'chapter-slug', + id: 'chapter-id', + title: 'chapter-title', + description: 'chapter-description', + + }), ContentBlockNode: () => ({ 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', }), }; diff --git a/client/cypress/integration/frontend/assignment-feedback-read-only.teacher.spec.js b/client/cypress/integration/frontend/assignment-feedback-read-only.teacher.spec.js new file mode 100644 index 00000000..bbfcaf07 --- /dev/null +++ b/client/cypress/integration/frontend/assignment-feedback-read-only.teacher.spec.js @@ -0,0 +1,8 @@ +describe('Assignment feedback read-only - Teacher', () => { + it('can not edit', () => { + cy.get('.not-implemented'); + }); + it('can not share', () => { + cy.get('.not-implemented'); + }); +}); diff --git a/client/cypress/integration/frontend/assignment-read-only.student.spec.js b/client/cypress/integration/frontend/assignment-read-only.student.spec.js new file mode 100644 index 00000000..ec436ecf --- /dev/null +++ b/client/cypress/integration/frontend/assignment-read-only.student.spec.js @@ -0,0 +1,82 @@ +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 operations = { + MeQuery: { + me: { + onboardingVisited: true, + }, + }, + ModuleDetailsQuery: { + module, + }, + AssignmentQuery: { + assignment: { + submission: { + text: myText, + final: false, + 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, + operations, + }); + }); + }); + + it('can not edit', () => { + cy.visit('module/module-with-assignment'); + + cy.get('.submission-form__textarea').invoke('val').should('contain', myText); + cy.getByDataCy('submission-form-submit').should('not.exist'); + }); + + it('can not turn in', () => { + cy.get('.not-implemented'); + }); +}); diff --git a/client/src/components/content-blocks/assignment/SubmissionForm.vue b/client/src/components/content-blocks/assignment/SubmissionForm.vue index 46afbfac..cb94c2eb 100644 --- a/client/src/components/content-blocks/assignment/SubmissionForm.vue +++ b/client/src/components/content-blocks/assignment/SubmissionForm.vue @@ -16,6 +16,7 @@ v-if="!final"> diff --git a/client/src/graphql/gql/fragments/userParts.gql b/client/src/graphql/gql/fragments/userParts.gql index 6d6b949e..ef1f47e5 100644 --- a/client/src/graphql/gql/fragments/userParts.gql +++ b/client/src/graphql/gql/fragments/userParts.gql @@ -9,6 +9,7 @@ fragment UserParts on PrivateUserNode { lastName avatarUrl expiryDate + readOnly lastModule { id slug diff --git a/server/assignments/tests/test_read_only.py b/server/assignments/tests/test_read_only.py new file mode 100644 index 00000000..09f1bfc0 --- /dev/null +++ b/server/assignments/tests/test_read_only.py @@ -0,0 +1,15 @@ +from core.tests.base_test import SkillboxTestCase + + +class AssignmentReadOnlyTestCase(SkillboxTestCase): + def test_edit_assignment_fails(self): + raise NotImplementedError() + + def test_share_assignment_fails(self): + raise NotImplementedError() + + def test_edit_feedback_fails(self): + raise NotImplementedError() + + def test_share_feedback_fails(self): + raise NotImplementedError() diff --git a/server/schema.graphql b/server/schema.graphql index 07ddbaaf..266904a2 100644 --- a/server/schema.graphql +++ b/server/schema.graphql @@ -788,6 +788,7 @@ type PrivateUserNode implements Node { isTeacher: Boolean 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 + readOnly: Boolean } type PrivateUserNodeConnection { diff --git a/server/users/models.py b/server/users/models.py index 63f90291..67cf299c 100644 --- a/server/users/models.py +++ b/server/users/models.py @@ -7,6 +7,7 @@ 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.translation import ugettext_lazy as _ from core.hep_client import HepClient, MYSKILLBOX_LICENSES @@ -114,6 +115,10 @@ class User(AbstractUser): def full_name(self): return self.get_full_name() + @cached_property + def read_only(self): + return True + class Meta: ordering = ['pk', ] diff --git a/server/users/schema.py b/server/users/schema.py index 1857efcb..328fda70 100644 --- a/server/users/schema.py +++ b/server/users/schema.py @@ -88,7 +88,7 @@ class PrivateUserNode(DjangoObjectType): filter_fields = ['username', 'email'] only_fields = ['username', 'email', 'first_name', 'last_name', 'school_classes', 'last_module', 'last_topic', 'avatar_url', - 'selected_class', 'expiry_date', 'onboarding_visited', 'team'] + 'selected_class', 'expiry_date', 'onboarding_visited', 'team', 'read_only'] interfaces = (relay.Node,) pk = graphene.Int() @@ -99,6 +99,7 @@ class PrivateUserNode(DjangoObjectType): old_classes = DjangoFilterConnectionField(SchoolClassNode) recent_modules = DjangoFilterConnectionField(ModuleNode, filterset_class=RecentModuleFilter) team = graphene.Field(TeamNode) + read_only = graphene.Boolean() def resolve_pk(self, info, **kwargs): return self.id From 949f65607938a0d3baaedaa9d4263e6b9eb9d939 Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Mon, 5 Jul 2021 13:14:03 +0200 Subject: [PATCH 02/11] Make assignments read only in client --- .../assignment-read-only.student.spec.js | 64 +++++++++++++++---- .../content-blocks/assignment/Assignment.vue | 1 + .../assignment/FinalSubmission.vue | 28 ++++++-- .../assignment/SubmissionForm.vue | 17 +++-- .../assignment/SubmissionInput.vue | 5 +- 5 files changed, 92 insertions(+), 23 deletions(-) diff --git a/client/cypress/integration/frontend/assignment-read-only.student.spec.js b/client/cypress/integration/frontend/assignment-read-only.student.spec.js index ec436ecf..35bb2f79 100644 --- a/client/cypress/integration/frontend/assignment-read-only.student.spec.js +++ b/client/cypress/integration/frontend/assignment-read-only.student.spec.js @@ -16,7 +16,6 @@ const module = { }, id: 'bla-123', }, - { type: 'assignment', value: { @@ -35,10 +34,11 @@ const module = { const myText = 'submission text'; -const operations = { +const getOperations = ({final, readOnly}) => ({ MeQuery: { me: { onboardingVisited: true, + readOnly }, }, ModuleDetailsQuery: { @@ -48,13 +48,17 @@ const operations = { assignment: { submission: { text: myText, - final: false, + final, document: '', submissionFeedback: null }, }, }, -}; +}); + +Cypress.Commands.add('canNotReopen', () => { + cy.getByDataCy('final-submission-reopen').should('not.exist'); +}); describe('Assignments read-only - Student', () => { beforeEach(() => { @@ -63,20 +67,58 @@ describe('Assignments read-only - Student', () => { cy.task('getSchema').then(schema => { cy.mockGraphql({ schema, - mocks, - operations, + mocks }); }); }); - it('can not edit', () => { + 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').invoke('val').should('contain', myText); - cy.getByDataCy('submission-form-submit').should('not.exist'); + 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 turn in', () => { - cy.get('.not-implemented'); + it('can not edit or turn in', () => { + cy.mockGraphqlOps({ + operations: getOperations({final: false, readOnly: true}) + }); + cy.visit('module/module-with-assignment'); + + 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'); + cy.canNotReopen(); + }); + + 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.canNotReopen(); }); }); diff --git a/client/src/components/content-blocks/assignment/Assignment.vue b/client/src/components/content-blocks/assignment/Assignment.vue index 87772616..7607b32d 100644 --- a/client/src/components/content-blocks/assignment/Assignment.vue +++ b/client/src/components/content-blocks/assignment/Assignment.vue @@ -16,6 +16,7 @@ :spellcheck-loading="spellcheckLoading" :saved="!unsaved" :spellcheck="true" + :read-only="me.readOnly" placeholder="Ergebnis erfassen" action="Ergebnis mit Lehrperson teilen" shared-msg="Das Ergebnis wurde mit der Lehrperson geteilt." diff --git a/client/src/components/content-blocks/assignment/FinalSubmission.vue b/client/src/components/content-blocks/assignment/FinalSubmission.vue index d2d8d55e..107a25a7 100644 --- a/client/src/components/content-blocks/assignment/FinalSubmission.vue +++ b/client/src/components/content-blocks/assignment/FinalSubmission.vue @@ -1,5 +1,7 @@