From ba1e63a84b715065b28deb2b5039cdc46f29ceba Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Thu, 14 Mar 2019 12:12:31 +0100 Subject: [PATCH 01/20] restrict access to users and rooms, add tests --- server/rooms/models.py | 7 +- server/rooms/schema.py | 23 +++- .../rooms/tests/test_room_query_permission.py | 119 ++++++++++++++++++ server/users/models.py | 3 + server/users/schema.py | 6 + 5 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 server/rooms/tests/test_room_query_permission.py diff --git a/server/rooms/models.py b/server/rooms/models.py index 322183b8..228ea681 100644 --- a/server/rooms/models.py +++ b/server/rooms/models.py @@ -1,10 +1,10 @@ from django.contrib.auth import get_user_model from django.db import models -from django_extensions.db.models import TitleDescriptionModel, TitleSlugDescriptionModel +from django_extensions.db.models import TitleSlugDescriptionModel from wagtail.core.fields import StreamField from books.blocks import ImageUrlBlock, LinkBlock, VideoBlock -from books.models import ContentBlock, TextBlock +from books.models import TextBlock from users.models import SchoolClass @@ -37,3 +37,6 @@ class RoomEntry(TitleSlugDescriptionModel): def __str__(self): return 'RoomEntry {}-{}-{}'.format(self.id, self.title, self.author) + + def can_user_see_entry(self, user): + return user.is_superuser or self.room.school_class.is_user_in_schoolclass(user) diff --git a/server/rooms/schema.py b/server/rooms/schema.py index e224a5ec..74f6a217 100644 --- a/server/rooms/schema.py +++ b/server/rooms/schema.py @@ -55,14 +55,29 @@ class RoomsQuery(object): return Room.objects.filter(school_class__in=user.school_classes.all()) def resolve_room(self, info, **kwargs): - return get_by_id_or_slug(Room, **kwargs) + room = get_by_id_or_slug(Room, **kwargs) + if room.school_class.is_user_in_schoolclass(info.context.user): + return room + else: + return None def resolve_room_entry(self, info, **kwargs): slug = kwargs.get('slug') id = kwargs.get('id') + room_entry = None if id is not None: - return get_object(RoomEntry, id) + room_entry = get_object(RoomEntry, id) if slug is not None: - return RoomEntry.objects.get(slug=slug) - return None + room_entry = RoomEntry.objects.get(slug=slug) + + if room_entry and room_entry.can_user_see_entry(info.context.user): + return room_entry + else: + return None + + def resolve_all_room_entries(self, info, **kwargs): + if not info.context.user.is_superuser: + return RoomEntry.objects.none() + else: + return RoomEntry.objects.all() diff --git a/server/rooms/tests/test_room_query_permission.py b/server/rooms/tests/test_room_query_permission.py new file mode 100644 index 00000000..449f03f0 --- /dev/null +++ b/server/rooms/tests/test_room_query_permission.py @@ -0,0 +1,119 @@ +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 rooms.factories import RoomFactory, RoomEntryFactory +from rooms.models import Room +from users.factories import SchoolClassFactory + + +class RoomQueryPermission(TestCase): + + @staticmethod + def get_first_contents(result): + return result.get('data').get('rooms').get('edges')[0] + + def setUp(self): + self.user = UserFactory(username='aschi') + self.another_user = UserFactory(username='pesche') + sc1 = SchoolClassFactory(users=[self.user]) + sc2 = SchoolClassFactory(users=[self.another_user]) + self.room1 = RoomFactory(school_class=sc1) + self.room2 = RoomFactory(school_class=sc2) + + request = RequestFactory().get('/') + request.user = self.user + self.client = Client(schema=schema, context_value=request) + + def test_student_should_only_see_rooms_of_class(self): + + query = ''' + query { + rooms { + edges { + node { + title + } + } + } + } + ''' + + result = self.client.execute(query) + self.assertIsNone(result.get('errors')) + self.assertEqual(len(result.get('data').get('rooms').get('edges')), 1) + self.assertEqual(result.get('data').get('rooms').get('edges')[0].get('node').get('title'), self.room1.title) + + def test_student_should_not_be_able_to_query_rooms_of_other_classes(self): + + query = ''' + query RoomQuery($slug: String) { + room(slug: $slug) { + title + } + } + ''' + + result = self.client.execute(query, variables={ + 'slug': self.room2.slug + }) + + self.assertIsNone(result.get('errors')) + self.assertEqual(result.get('data').get('room'), None) + + +class RoomEntryQueryPermissions(TestCase): + + @staticmethod + def get_first_contents(result): + return result.get('data').get('rooms').get('edges')[0] + + def setUp(self): + self.user = UserFactory(username='aschi') + self.another_user = UserFactory(username='pesche') + sc1 = SchoolClassFactory(users=[self.user]) + sc2 = SchoolClassFactory(users=[self.another_user]) + room1 = RoomFactory(school_class=sc1) + room2 = RoomFactory(school_class=sc2) + self.roomEntry1 = RoomEntryFactory(room=room1, author=self.user) + self.roomEntry2 = RoomEntryFactory(room=room2, author=self.another_user) + + request = RequestFactory().get('/') + request.user = self.user + self.client = Client(schema=schema, context_value=request) + + def test_user_should_see_room_entries_from_own_class(self): + + query = ''' + query RoomEntryQuery($slug: String) { + roomEntry(slug: $slug) { + title + } + } + ''' + + result = self.client.execute(query, variables={ + 'slug': self.roomEntry1.slug + }) + + self.assertIsNone(result.get('errors')) + self.assertEqual(result.get('data').get('roomEntry').get('title'), self.roomEntry1.title) + + def test_user_should_not_see_room_entries_from_orther_class(self): + + query = ''' + query RoomEntryQuery($slug: String) { + roomEntry(slug: $slug) { + title + } + } + ''' + + result = self.client.execute(query, variables={ + 'slug': self.roomEntry2.slug + }) + + self.assertIsNone(result.get('errors')) + self.assertEqual(result.get('data').get('roomEntry'), None) diff --git a/server/users/models.py b/server/users/models.py index a163adb9..2e33c4ff 100644 --- a/server/users/models.py +++ b/server/users/models.py @@ -51,6 +51,9 @@ class SchoolClass(models.Model): def __str__(self): return 'SchoolClass {}-{}-{}'.format(self.id, self.name, self.year) + def is_user_in_schoolclass(self, user): + return user.is_superuser or user.school_classes.filter(pk=self.id).count() > 0 + class Role(models.Model): key = models.CharField(_('Key'), max_length=100, blank=False, null=False, unique=True) diff --git a/server/users/schema.py b/server/users/schema.py index 6779ae15..401b9adf 100644 --- a/server/users/schema.py +++ b/server/users/schema.py @@ -45,3 +45,9 @@ class UsersQuery(object): def resolve_me(self, info, **kwargs): return info.context.user + + def resolve_all_users(self, info, **kwargs): + if not info.context.user.is_superuser: + return User.objects.none() + else: + return User.objects.all() From 43f942ea2d0220eb0cdae05706a32d4e6f917e2e Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Thu, 28 Mar 2019 11:31:02 +0100 Subject: [PATCH 02/20] protect updates on room entries, add tests --- server/objectives/schema.py | 1 - server/rooms/mutations.py | 12 +++- .../rooms/tests/test_room_entry_mutations.py | 72 +++++++++++++++++++ 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/server/objectives/schema.py b/server/objectives/schema.py index 0c6f136a..6f13b67a 100644 --- a/server/objectives/schema.py +++ b/server/objectives/schema.py @@ -26,7 +26,6 @@ class ObjectiveGroupNode(DjangoObjectType): return self.owner is not None and self.owner.pk == info.context.user.pk - class ObjectiveNode(DjangoObjectType): pk = graphene.Int() diff --git a/server/rooms/mutations.py b/server/rooms/mutations.py index a1b84f53..7a12e028 100644 --- a/server/rooms/mutations.py +++ b/server/rooms/mutations.py @@ -84,14 +84,24 @@ class MutateRoomEntry(relay.ClientIDMutation): @classmethod def mutate_and_get_payload(cls, root, info, **kwargs): room_entry_data = kwargs.get('room_entry') + if room_entry_data.get('room') is not None: room_entry_data['room'] = get_object(Room, room_entry_data.get('room')).id - room_entry_data['author'] = info.context.user.pk if room_entry_data.get('id') is not None: + # update path instance = get_object(RoomEntry, room_entry_data.get('id')) + + if not instance.room.school_class.is_user_in_schoolclass(info.context.user): + raise Exception('You are in the wrong class') + + if instance.author.pk != info.context.user.pk: + raise Exception('You are not the author') + serializer = RoomEntrySerializer(instance, data=room_entry_data, partial=True) else: + # add path + room_entry_data['author'] = info.context.user.pk serializer = RoomEntrySerializer(data=room_entry_data) if serializer.is_valid(): diff --git a/server/rooms/tests/test_room_entry_mutations.py b/server/rooms/tests/test_room_entry_mutations.py index 1a0fc724..48ec640a 100644 --- a/server/rooms/tests/test_room_entry_mutations.py +++ b/server/rooms/tests/test_room_entry_mutations.py @@ -14,7 +14,9 @@ class RoomEntryMutationsTestCase(TestCase): def setUp(self): self.user = UserFactory(username='aschi') self.another_user = UserFactory(username='pesche') + self.yet_another_user = UserFactory(username='hansueli') s = SchoolClassFactory(users=[self.user, self.another_user]) + s2 = SchoolClassFactory(users=[self.yet_another_user]) self.room_entry = RoomEntryFactory(author=self.user, room=RoomFactory(school_class=s)) request = RequestFactory().get('/') @@ -64,3 +66,73 @@ class RoomEntryMutationsTestCase(TestCase): self.assertIsNotNone(result.get('errors')) self.assertEqual(RoomEntry.objects.count(), 1) + + def test_update_room_entry_not_owner_but_same_class(self): + self.assertEqual(RoomEntry.objects.count(), 1) + mutation = ''' + mutation UpdateRoomEntry($input: UpdateRoomEntryInput!){ + updateRoomEntry(input: $input) { + roomEntry { + title + author { + firstName + } + } + errors + } + } + ''' + + request = RequestFactory().get('/') + request.user = self.another_user + client = Client(schema=schema, context_value=request) + + new_title = 'new title, Alte!' + + result = client.execute(mutation, variables={ + 'input': { + 'roomEntry': { + 'id': to_global_id('RoomEntryNode', self.room_entry.pk), + 'title': new_title + } + } + }) + + entry = RoomEntry.objects.get(pk=self.room_entry.pk) + self.assertIsNotNone(result.get('errors')) + self.assertEqual(entry.title, self.room_entry.title) + + def test_update_room_entry_not_owner_from_other_class(self): + self.assertEqual(RoomEntry.objects.count(), 1) + mutation = ''' + mutation UpdateRoomEntry($input: UpdateRoomEntryInput!){ + updateRoomEntry(input: $input) { + roomEntry { + title + author { + firstName + } + } + errors + } + } + ''' + + request = RequestFactory().get('/') + request.user = self.yet_another_user + client = Client(schema=schema, context_value=request) + + new_title = 'new title, Alte!' + + result = client.execute(mutation, variables={ + 'input': { + 'roomEntry': { + 'id': to_global_id('RoomEntryNode', self.room_entry.pk), + 'title': new_title + } + } + }) + + entry = RoomEntry.objects.get(pk=self.room_entry.pk) + self.assertIsNotNone(result.get('errors')) + self.assertEqual(entry.title, self.room_entry.title) From aeded872272329fcbf549dc913a76d712e8f5e92 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Thu, 28 Mar 2019 14:55:32 +0100 Subject: [PATCH 03/20] Add permissions to objectivegroup mutations --- server/objectives/mutations.py | 14 +++++++++++++- server/objectives/tests.py | 1 - server/objectives/tests/__init__.py | 0 server/rooms/tests/test_room_query_permission.py | 2 -- 4 files changed, 13 insertions(+), 4 deletions(-) delete mode 100644 server/objectives/tests.py create mode 100644 server/objectives/tests/__init__.py diff --git a/server/objectives/mutations.py b/server/objectives/mutations.py index df9e50ff..ca615e21 100644 --- a/server/objectives/mutations.py +++ b/server/objectives/mutations.py @@ -1,6 +1,7 @@ import graphene from graphene import relay, InputObjectType from graphql_relay import from_global_id +from rest_framework.exceptions import PermissionDenied from api.utils import get_object from books.models import Module @@ -67,13 +68,18 @@ class AddObjectiveGroup(relay.ClientIDMutation): @classmethod def mutate_and_get_payload(cls, root, info, **kwargs): + + owner = info.context.user + if not owner.has_perm('users.can_manage_school_class_content'): + raise PermissionDenied('Missing permissions') + objective_group_data = kwargs.get('objective_group') title = objective_group_data.get('title') if title != 'society': title = 'language_communication' module_id = objective_group_data.get('module') module = get_object(Module, module_id) - owner = info.context.user + new_objective_group = ObjectiveGroup.objects.create(title=title, module=module, owner=owner) objectives = objective_group_data.get('objectives') for objective in objectives: @@ -89,9 +95,15 @@ class UpdateObjectiveGroup(relay.ClientIDMutation): @classmethod def mutate_and_get_payload(cls, root, info, **kwargs): + + user = info.context.user + if not user.has_perm('users.can_manage_school_class_content'): + raise PermissionDenied('Missing permissions') + objective_group_data = kwargs.get('objective_group') id = objective_group_data.get('id') objective_group = get_object(ObjectiveGroup, id) + objectives = objective_group_data.get('objectives') existing_objective_ids = list(objective_group.objectives.values_list('id', flat=True)) for objective in objectives: diff --git a/server/objectives/tests.py b/server/objectives/tests.py deleted file mode 100644 index a39b155a..00000000 --- a/server/objectives/tests.py +++ /dev/null @@ -1 +0,0 @@ -# Create your tests here. diff --git a/server/objectives/tests/__init__.py b/server/objectives/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/rooms/tests/test_room_query_permission.py b/server/rooms/tests/test_room_query_permission.py index 449f03f0..6a2cc583 100644 --- a/server/rooms/tests/test_room_query_permission.py +++ b/server/rooms/tests/test_room_query_permission.py @@ -1,11 +1,9 @@ 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 rooms.factories import RoomFactory, RoomEntryFactory -from rooms.models import Room from users.factories import SchoolClassFactory From afb21fa499eb74b68505dfc851c8c32a4dfb6acd Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Fri, 29 Mar 2019 16:31:08 +0100 Subject: [PATCH 04/20] Add moment to dependencies --- client/package-lock.json | 13 +++++++++---- client/package.json | 1 + 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 764af335..d6031d7f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -3042,6 +3042,12 @@ "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true }, + "moment": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", + "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=", + "dev": true + }, "supports-color": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.1.0.tgz", @@ -7191,10 +7197,9 @@ } }, "moment": { - "version": "2.22.2", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", - "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=", - "dev": true + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" }, "move-concurrently": { "version": "1.0.1", diff --git a/client/package.json b/client/package.json index 326d8fd5..ba340f11 100644 --- a/client/package.json +++ b/client/package.json @@ -51,6 +51,7 @@ "graphql-tag": "^2.9.2", "html-webpack-plugin": "^2.30.1", "lodash": "^4.17.10", + "moment": "^2.24.0", "node-notifier": "^5.1.2", "node-sass": "^4.9.2", "optimize-css-assets-webpack-plugin": "^3.2.0", From 5b83b04a0aff2d1f287f224c841df411d83300e5 Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Fri, 29 Mar 2019 16:36:04 +0100 Subject: [PATCH 05/20] Add coverage --- .gitignore | 2 + Pipfile | 1 + Pipfile.lock | 92 ++++++++++++++++++++++++++-------------------- server/.coveragerc | 3 ++ 4 files changed, 58 insertions(+), 40 deletions(-) create mode 100644 server/.coveragerc diff --git a/.gitignore b/.gitignore index 90ceb143..f11c4ce0 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,5 @@ server/media/ # pyenv .python-version + +.coverage diff --git a/Pipfile b/Pipfile index af02de55..55793104 100644 --- a/Pipfile +++ b/Pipfile @@ -36,3 +36,4 @@ bleach = "*" newrelic = "*" sentry-sdk = "==0.7.2" "django-sendgrid-v5" = "*" +coverage = "*" diff --git a/Pipfile.lock b/Pipfile.lock index c1724977..3938e957 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "c0e186a3fb465dc3cb6985204c534ea52652af00103c61601d0f859bacdee533" + "sha256": "97ff5ca56ac835d40353e34e32ec8333ccb23822bcf971644b7641429d9774e1" }, "pipfile-spec": 6, "requires": { @@ -41,19 +41,18 @@ }, "boto3": { "hashes": [ - "sha256:39e9b6516a72864f5a6b69f38edac087cc4ba623095f4528b916620e63de32b3", - "sha256:a3eb22bb975a200a69084ec2bb69819e483ad531bf05e7b73861fc1b333aad42" + "sha256:3927beac97e5467f869d63d60920b83c2d39964f69fbf944bc1db724116bfe1a", + "sha256:be88cae6f16bb9fe3b850b6c8259b297f60b46855175cadae57594c9a403c582" ], "index": "pypi", - "version": "==1.9.120" + "version": "==1.9.124" }, "botocore": { "hashes": [ - "sha256:2bf8768887bfb008406eec725eecf6f174307dac00f5fad244cdd5d39c6c2147", - "sha256:c0f9c57e7a8c65f17a62a0926c3e73686f6ae1c08abf3b5a63cc3a5bcc4c437b" + "sha256:bb756a8da2c6e3ccf42dccb0ac71c1df2e07844db339183da06f4e0285b251d0", + "sha256:fc7560a2676df2f0bab4ef0638277b86e5a00944c2ce1c3bb124b3066e6d3d2a" ], - "markers": "python_version != '3.0.*' and python_version >= '2.6' and python_version != '3.1.*'", - "version": "==1.12.120" + "version": "==1.12.124" }, "certifi": { "hashes": [ @@ -74,9 +73,45 @@ "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" ], - "markers": "python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.3.*'", "version": "==7.0" }, + "coverage": { + "hashes": [ + "sha256:3684fabf6b87a369017756b551cef29e505cb155ddb892a7a29277b978da88b9", + "sha256:39e088da9b284f1bd17c750ac672103779f7954ce6125fd4382134ac8d152d74", + "sha256:3c205bc11cc4fcc57b761c2da73b9b72a59f8d5ca89979afb0c1c6f9e53c7390", + "sha256:465ce53a8c0f3a7950dfb836438442f833cf6663d407f37d8c52fe7b6e56d7e8", + "sha256:48020e343fc40f72a442c8a1334284620f81295256a6b6ca6d8aa1350c763bbe", + "sha256:5296fc86ab612ec12394565c500b412a43b328b3907c0d14358950d06fd83baf", + "sha256:5f61bed2f7d9b6a9ab935150a6b23d7f84b8055524e7be7715b6513f3328138e", + "sha256:68a43a9f9f83693ce0414d17e019daee7ab3f7113a70c79a3dd4c2f704e4d741", + "sha256:6b8033d47fe22506856fe450470ccb1d8ba1ffb8463494a15cfc96392a288c09", + "sha256:7ad7536066b28863e5835e8cfeaa794b7fe352d99a8cded9f43d1161be8e9fbd", + "sha256:7bacb89ccf4bedb30b277e96e4cc68cd1369ca6841bde7b005191b54d3dd1034", + "sha256:839dc7c36501254e14331bcb98b27002aa415e4af7ea039d9009409b9d2d5420", + "sha256:8f9a95b66969cdea53ec992ecea5406c5bd99c9221f539bca1e8406b200ae98c", + "sha256:932c03d2d565f75961ba1d3cec41ddde00e162c5b46d03f7423edcb807734eab", + "sha256:988529edadc49039d205e0aa6ce049c5ccda4acb2d6c3c5c550c17e8c02c05ba", + "sha256:998d7e73548fe395eeb294495a04d38942edb66d1fa61eb70418871bc621227e", + "sha256:9de60893fb447d1e797f6bf08fdf0dbcda0c1e34c1b06c92bd3a363c0ea8c609", + "sha256:9e80d45d0c7fcee54e22771db7f1b0b126fb4a6c0a2e5afa72f66827207ff2f2", + "sha256:a545a3dfe5082dc8e8c3eb7f8a2cf4f2870902ff1860bd99b6198cfd1f9d1f49", + "sha256:a5d8f29e5ec661143621a8f4de51adfb300d7a476224156a39a392254f70687b", + "sha256:aca06bfba4759bbdb09bf52ebb15ae20268ee1f6747417837926fae990ebc41d", + "sha256:bb23b7a6fd666e551a3094ab896a57809e010059540ad20acbeec03a154224ce", + "sha256:bfd1d0ae7e292105f29d7deaa9d8f2916ed8553ab9d5f39ec65bcf5deadff3f9", + "sha256:c62ca0a38958f541a73cf86acdab020c2091631c137bd359c4f5bddde7b75fd4", + "sha256:c709d8bda72cf4cd348ccec2a4881f2c5848fd72903c185f363d361b2737f773", + "sha256:c968a6aa7e0b56ecbd28531ddf439c2ec103610d3e2bf3b75b813304f8cb7723", + "sha256:df785d8cb80539d0b55fd47183264b7002077859028dfe3070cf6359bf8b2d9c", + "sha256:f406628ca51e0ae90ae76ea8398677a921b36f0bd71aab2099dfed08abd0322f", + "sha256:f46087bbd95ebae244a0eda01a618aff11ec7a069b15a3ef8f6b520db523dcf1", + "sha256:f8019c5279eb32360ca03e9fac40a12667715546eed5c5eb59eb381f2f501260", + "sha256:fc5f4d209733750afd2714e9109816a29500718b32dd9a5db01c0cb3a019b96a" + ], + "index": "pypi", + "version": "==4.5.3" + }, "dj-database-url": { "hashes": [ "sha256:7f4c78d2a090df8dfaf56d5d3ff7bbee17360436e4879558317e2314424864cd" @@ -209,14 +244,12 @@ "sha256:00b7011757c4907546f17d0e47df098b542ea2b04c966ee0e80a493aae2c13c8", "sha256:745ac8b9c9526e338696e07b7f2e206e5e317e5744e22fdd7c2894bf19af41f1" ], - "markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*'", "version": "==1.0.4" }, "future": { "hashes": [ "sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8" ], - "markers": "python_version != '3.1.*' and python_version != '3.2.*' and python_version != '3.0.*' and python_version >= '2.6'", "version": "==0.17.1" }, "graphene": { @@ -239,7 +272,6 @@ "sha256:889e869be5574d02af77baf1f30b5db9ca2959f1c9f5be7b2863ead5a3ec6181", "sha256:9462e22e32c7f03b667373ec0a84d95fba10e8ce2ead08f29fbddc63b671b0c1" ], - "markers": "python_version >= '2.6' and python_version != '3.0.*' and python_version != '3.1.*'", "version": "==2.1" }, "graphql-relay": { @@ -261,7 +293,6 @@ "sha256:20b159aa3badc9d5ee8f5c647e5efd02ed2a66ab8d354930bd9ff139fc1dc0a3", "sha256:66cb0dcfdbbc4f9c3ba1a63fdb511ffdbd4f513b2b6d81b80cd26ce6b3fb3736" ], - "markers": "python_version >= '2.6' and python_version != '3.0.*' and python_version != '3.1.*'", "version": "==1.0.1" }, "idna": { @@ -269,7 +300,6 @@ "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" ], - "markers": "python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.3.*'", "version": "==2.8" }, "jmespath": { @@ -298,15 +328,14 @@ "sha256:e8941881063691d50f9cc8b8d6d8fd7bec86a8c461b2a4fc87188a5fc44d6ba4", "sha256:f4b29b0c70d753c754a58aaad7c31ad3309ca4a26f9aa64e695157251f6832ad" ], - "markers": "python_version >= '2.6' and python_version != '3.0.*' and python_version != '3.1.*'", "version": "==0.18.0" }, "newrelic": { "hashes": [ - "sha256:b0f2ef6c817d9b5389cb3ef0f06037abbd1c1ed1a4ae04f293dadeb0f78ea924" + "sha256:1d08248dee0d33116a145de723a2ae86e57a10674145ce4c8af3c316423bd140" ], "index": "pypi", - "version": "==4.14.0.115" + "version": "==4.16.0.116" }, "pillow": { "hashes": [ @@ -344,7 +373,6 @@ "sha256:2ebbfc10b7abf6354403ed785fe4f04b9dfd421eb1a474ac8d187022228332af", "sha256:348f5f6c3edd4fd47c9cd65aed03ac1b31136d375aa63871a57d3e444c85655c" ], - "markers": "python_version >= '2.6' and python_version != '3.0.*' and python_version != '3.1.*'", "version": "==2.2.1" }, "psycopg2": { @@ -432,7 +460,6 @@ "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" ], - "markers": "python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*' and python_version < '4' and python_version >= '2.7' and python_version != '3.3.*'", "version": "==2.21.0" }, "rjsmin": { @@ -453,7 +480,6 @@ "sha256:7b9ad3213bff7d357f888e0fab5101b56fa1a0548ee77d121c3a3dbfbef4cb2e", "sha256:f23d5cb7d862b104401d9021fc82e5fa0e0cf57b7660a1331425aab0c691d021" ], - "markers": "python_version != '3.0.*' and python_version >= '2.6' and python_version != '3.1.*'", "version": "==0.2.0" }, "sendgrid": { @@ -476,7 +502,6 @@ "sha256:5b06af87df13818d14f08a028e42f566640aef80805c3b50c5056b086e3c2b9c", "sha256:833b46966687b3de7f438c761ac475213e53b306740f1abfaa86e1d1aae56aa8" ], - "markers": "python_version >= '2.6' and python_version != '3.0.*' and python_version != '3.1.*'", "version": "==3.4.0.3" }, "six": { @@ -484,7 +509,6 @@ "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" ], - "markers": "python_version != '3.0.*' and python_version >= '2.6' and python_version != '3.1.*'", "version": "==1.12.0" }, "text-unidecode": { @@ -553,21 +577,13 @@ } }, "develop": { - "appnope": { - "hashes": [ - "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0", - "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71" - ], - "markers": "sys_platform == 'darwin'", - "version": "==0.1.0" - }, "awscli": { "hashes": [ - "sha256:9cbc48eff6f8c9ee2072be2caf569dd5f18734d61e020556b792ba509bbb03f0", - "sha256:e0b9afc24c591ecb04d02a34d9e8d3508a4bd25c713d10a358cdad09abbbcf8e" + "sha256:87258e4719978f51dae8c62e15cd0486a778ddcb530645f3bc035239b800f184", + "sha256:fbd9dc00ecd7060f36e5768122c9293672b82748fa224cb13e22e6322532d8db" ], "index": "pypi", - "version": "==1.16.130" + "version": "==1.16.134" }, "backcall": { "hashes": [ @@ -578,11 +594,10 @@ }, "botocore": { "hashes": [ - "sha256:2bf8768887bfb008406eec725eecf6f174307dac00f5fad244cdd5d39c6c2147", - "sha256:c0f9c57e7a8c65f17a62a0926c3e73686f6ae1c08abf3b5a63cc3a5bcc4c437b" + "sha256:bb756a8da2c6e3ccf42dccb0ac71c1df2e07844db339183da06f4e0285b251d0", + "sha256:fc7560a2676df2f0bab4ef0638277b86e5a00944c2ce1c3bb124b3066e6d3d2a" ], - "markers": "python_version != '3.0.*' and python_version >= '2.6' and python_version != '3.1.*'", - "version": "==1.12.120" + "version": "==1.12.124" }, "colorama": { "hashes": [ @@ -633,7 +648,6 @@ "sha256:86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de", "sha256:f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6" ], - "markers": "python_version != '3.0.*' and python_version >= '2.6' and python_version != '3.1.*'", "version": "==4.4.0" }, "docutils": { @@ -766,7 +780,6 @@ "sha256:7b9ad3213bff7d357f888e0fab5101b56fa1a0548ee77d121c3a3dbfbef4cb2e", "sha256:f23d5cb7d862b104401d9021fc82e5fa0e0cf57b7660a1331425aab0c691d021" ], - "markers": "python_version != '3.0.*' and python_version >= '2.6' and python_version != '3.1.*'", "version": "==0.2.0" }, "six": { @@ -774,7 +787,6 @@ "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" ], - "markers": "python_version != '3.0.*' and python_version >= '2.6' and python_version != '3.1.*'", "version": "==1.12.0" }, "traitlets": { diff --git a/server/.coveragerc b/server/.coveragerc new file mode 100644 index 00000000..838fce82 --- /dev/null +++ b/server/.coveragerc @@ -0,0 +1,3 @@ +[report] +omit = + */site-packages/* From 47572bb2125a9fc1837181a16dc856ade755c07a Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Fri, 29 Mar 2019 16:37:48 +0100 Subject: [PATCH 06/20] Add new tests for projects --- server/api/test_utils.py | 23 ++++++++++++++ .../tests/test_assignment_permissions.py | 28 ++++------------- server/portfolio/tests/__init__.py | 0 .../portfolio/tests/test_project_mutations.py | 30 +++++++++++++++++++ .../rooms/tests/test_room_entry_mutations.py | 2 -- 5 files changed, 59 insertions(+), 24 deletions(-) create mode 100644 server/api/test_utils.py create mode 100644 server/portfolio/tests/__init__.py create mode 100644 server/portfolio/tests/test_project_mutations.py diff --git a/server/api/test_utils.py b/server/api/test_utils.py new file mode 100644 index 00000000..967eead4 --- /dev/null +++ b/server/api/test_utils.py @@ -0,0 +1,23 @@ +from django.test import RequestFactory, TestCase +from graphene.test import Client +from api.schema import schema +from users.models import User +from users.services import create_users + + +def create_client(user): + request = RequestFactory().get('/') + request.user = user + return Client(schema=schema, context_value=request) + + +class DefaultUserTestCase(TestCase): + def setUp(self): + create_users() + + 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') + diff --git a/server/assignments/tests/test_assignment_permissions.py b/server/assignments/tests/test_assignment_permissions.py index f45fff12..8b9235ce 100644 --- a/server/assignments/tests/test_assignment_permissions.py +++ b/server/assignments/tests/test_assignment_permissions.py @@ -1,25 +1,13 @@ -from django.test import RequestFactory, TestCase -from graphene.test import Client from graphql_relay import to_global_id -from api.utils import get_graphql_mutation +from api.test_utils import create_client, DefaultUserTestCase from assignments.models import Assignment, StudentSubmission -from books.factories import ModuleFactory from ..factories import AssignmentFactory -from users.models import User -from users.services import create_users -from api.schema import schema -class AssignmentPermissionsTestCase(TestCase): +class AssignmentPermissionsTestCase(DefaultUserTestCase): def setUp(self): - create_users() - - 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') + super(AssignmentPermissionsTestCase, self).setUp() self.assignment = AssignmentFactory( owner=self.teacher ) @@ -27,10 +15,6 @@ class AssignmentPermissionsTestCase(TestCase): self.assignment_id = to_global_id('AssignmentNode', self.assignment.pk) self.module_id = to_global_id('ModuleNode', self.assignment.module.pk) - def _create_client(self, user): - request = RequestFactory().get('/') - request.user = user - return Client(schema=schema, context_value=request) def _submit_submission(self, user=None): mutation = ''' @@ -53,9 +37,9 @@ class AssignmentPermissionsTestCase(TestCase): ''' if user is None: - client = self._create_client(self.student1) + client = create_client(self.student1) else: - client = self._create_client(user) + client = create_client(user) return client.execute(mutation, variables={ 'input': { @@ -82,7 +66,7 @@ class AssignmentPermissionsTestCase(TestCase): self.assertEqual(StudentSubmission.objects.count(), 1) def _test_visibility(self, user, count): - client = self._create_client(user) + client = create_client(user) query = ''' query AssignmentWithSubmissions($id: ID!) { assignment(id: $id) { diff --git a/server/portfolio/tests/__init__.py b/server/portfolio/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/portfolio/tests/test_project_mutations.py b/server/portfolio/tests/test_project_mutations.py new file mode 100644 index 00000000..30e9e215 --- /dev/null +++ b/server/portfolio/tests/test_project_mutations.py @@ -0,0 +1,30 @@ +from api.test_utils import create_client, DefaultUserTestCase +from portfolio.models import Project + + +class ProjectMutationsTestCase(DefaultUserTestCase): + def test_add_project(self): + client = create_client(self.student1) + mutation = """ + mutation AddProjectMutation($input: AddProjectInput!){ + addProject(input: $input){ + project { + id + } + errors + } + } + """ + + result = client.execute(mutation, variables={ + 'input': { + "project": { + "title": "Rick Astley", + "description": "She wants to dance with me", + "objectives": "Dance with me", + "appearance": "green" + } + } + }) + self.assertIsNone(result.get('errors')) + self.assertEqual(Project.objects.count(), 1) diff --git a/server/rooms/tests/test_room_entry_mutations.py b/server/rooms/tests/test_room_entry_mutations.py index 1a0fc724..043dd826 100644 --- a/server/rooms/tests/test_room_entry_mutations.py +++ b/server/rooms/tests/test_room_entry_mutations.py @@ -7,8 +7,6 @@ from core.factories import UserFactory from rooms.factories import RoomEntryFactory, RoomFactory from rooms.models import RoomEntry from users.factories import SchoolClassFactory -from users.services import create_users - class RoomEntryMutationsTestCase(TestCase): def setUp(self): From ba436f8186ccb510dba7c2babed9dd2e3fc1a0c3 Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Fri, 29 Mar 2019 16:45:43 +0100 Subject: [PATCH 07/20] Fix os.environ call --- server/core/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/core/settings.py b/server/core/settings.py index dbf6ec8e..12964ab8 100644 --- a/server/core/settings.py +++ b/server/core/settings.py @@ -350,6 +350,6 @@ GRAPHQL_MUTATIONS_DIR = os.path.join(GRAPHQL_QUERIES_DIR, 'mutations') EMAIL_BACKEND = 'sendgrid_backend.SendgridBackend' -SENDGRID_API_KEY = os.environ["SENDGRID_API_KEY"] +SENDGRID_API_KEY = os.environ.get("SENDGRID_API_KEY") SENDGRID_SANDBOX_MODE_IN_DEBUG = False DEFAULT_FROM_EMAIL='noreply@myskillbox.ch' From 8ad5be305af04c74c551c10debd751198f3c70c7 Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Fri, 29 Mar 2019 17:03:15 +0100 Subject: [PATCH 08/20] Add cypress test to all pipelines --- bitbucket-pipelines.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml index 981aa2a1..561e7856 100644 --- a/bitbucket-pipelines.yml +++ b/bitbucket-pipelines.yml @@ -36,7 +36,7 @@ aliases: - echo "This pipeline rules!" - *setup-tests - npm install --prefix client -# - npm run "install:cypress" --prefix client + # - npm run "install:cypress" --prefix client - psql -U $DATABASE_USER -h $DATABASE_HOST -c "create database $DATABASE_NAME" - python server/manage.py dummy_data - python server/manage.py runserver & @@ -55,6 +55,7 @@ pipelines: branches: master: - step: *unittest-python + - step: *cypress-test develop: - step: *unittest-python @@ -67,4 +68,5 @@ pipelines: custom: prod: - step: *unittest-python + - step: *cypress-test - step: *deploy-prod From 08e66795b5509e9a0dd67d2f815d3250f9ade265 Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Mon, 1 Apr 2019 10:44:53 +0200 Subject: [PATCH 09/20] Add header to start page --- client/src/components/HeaderBar.vue | 120 +++++++++++++++++++++++++++ client/src/layouts/DefaultLayout.vue | 102 +---------------------- client/src/pages/start.vue | 13 ++- 3 files changed, 133 insertions(+), 102 deletions(-) create mode 100644 client/src/components/HeaderBar.vue diff --git a/client/src/components/HeaderBar.vue b/client/src/components/HeaderBar.vue new file mode 100644 index 00000000..71f5770f --- /dev/null +++ b/client/src/components/HeaderBar.vue @@ -0,0 +1,120 @@ + + + + + diff --git a/client/src/layouts/DefaultLayout.vue b/client/src/layouts/DefaultLayout.vue index 02201f0b..3ce6255e 100644 --- a/client/src/layouts/DefaultLayout.vue +++ b/client/src/layouts/DefaultLayout.vue @@ -1,15 +1,7 @@ @@ -93,67 +63,7 @@ &__header { grid-area: h; - display: -ms-grid; - @supports (display: grid) { - display: grid; - } - align-items: center; - justify-content: space-around; - background-color: $color-white; - grid-auto-rows: 50px; - width: 100%; - @include desktop { - grid-template-columns: 1fr 1fr 1fr; - } - - /* - * For IE10+ - */ - -ms-grid-columns: 1fr 1fr 1fr; - -ms-grid-rows: 60px 60px; - - /* - * For IE10+ - */ - & > :nth-child(1) { - -ms-grid-column: 1; - -ms-grid-row-align: center; - } - - /* - * For IE10+ - */ - & > :nth-child(3) { - -ms-grid-column: 3; - -ms-grid-row-align: center; - -ms-grid-column-align: end; - - justify-self: end; - } - - & > :nth-child(4) { - -ms-grid-row: 2; - -ms-grid-column: 1; - -ms-grid-column-span: 3; - } - - } - - &__header-logo { - color: #17A887; - font-size: 36px; - font-weight: 800; - font-family: $sans-serif-font-family; - display: flex; - justify-self: center; - - /* - * For IE10+ - */ - -ms-grid-column: 2; - -ms-grid-row-align: center; - -ms-grid-column-align: center; } &__footer { @@ -169,8 +79,4 @@ -ms-grid-column: 1; } } - - .user-header { - display: flex; - } diff --git a/client/src/pages/start.vue b/client/src/pages/start.vue index f6718d4a..79cabeed 100644 --- a/client/src/pages/start.vue +++ b/client/src/pages/start.vue @@ -1,6 +1,6 @@ - + diff --git a/client/src/layouts/DefaultLayout.vue b/client/src/layouts/DefaultLayout.vue index 3ce6255e..d5a9b46f 100644 --- a/client/src/layouts/DefaultLayout.vue +++ b/client/src/layouts/DefaultLayout.vue @@ -3,9 +3,9 @@ - + - +
Footer
@@ -39,7 +39,9 @@ .skillbox { margin: 0 auto; width: 100%; - display: grid; + @supports (display: grid) { + display: grid; + } grid-template-rows: auto 1fr; min-height: 100vh; grid-auto-rows: 1fr; @@ -49,21 +51,41 @@ &--show-filter { grid-template-rows: auto auto 1fr; + -ms-grid-rows: 50px 50px 30px 1fr; // 1 extra row for gap grid-template-areas: "h" "." "c"; } + /* + * For IE10+ + */ + + &--show-filter &__content { + -ms-grid-row: 4; + -ms-grid-column: 1; + } + + &--show-filter &__filter-bar { + -ms-grid-row: 2; + -ms-grid-column: 1; + } + /* * For IE10+ */ display: -ms-grid; - -ms-grid-rows: auto 32px 1fr; // 1 extra row for gap + -ms-grid-rows: 50px 30px 1fr; // 1 extra row for gap -ms-grid-columns: 1fr; @include skillbox-colors; &__header { grid-area: h; + -ms-grid-row: 1; + } + &__content { + -ms-grid-row: 3; + -ms-grid-column: 1; } &__footer { @@ -75,7 +97,11 @@ * For IE10+ */ & > :nth-child(2) { - -ms-grid-row: 3; + + } + + & > :nth-child(3) { + -ms-grid-row: 4; -ms-grid-column: 1; } } diff --git a/client/src/pages/rooms.vue b/client/src/pages/rooms.vue index 85a9ca26..f09537d3 100644 --- a/client/src/pages/rooms.vue +++ b/client/src/pages/rooms.vue @@ -63,7 +63,9 @@ @import "@/styles/_mixins.scss"; .rooms-page { - display: -ms-grid; + display: flex; + flex-wrap: wrap; + @supports (display: grid) { display: grid; } @@ -71,6 +73,7 @@ padding: 50px 15px; @include desktop { grid-template-columns: repeat(3, 1fr); + -ms-grid-columns: 1fr 30px 1fr 30px 1fr; padding: 50px 45px; } grid-column-gap: 30px; @@ -81,26 +84,14 @@ justify-self: center; box-sizing: border-box; - /* - * For IE10+ - */ - -ms-grid-columns: 1fr 1fr 1fr; - -ms-grid-rows: 260px; + & > div { + flex: 0 0 30%; + margin-bottom: $large-spacing; + margin-right: 1%; - /* - * SHAME SHAME SHAME - * this is very hacky, but we have a dynamic amount of elements. better to be safe than sorry - * SHAME SHAME SHAME - */ - @for $i from 1 to 101 { - & > :nth-child(#{$i}) { - @if ($i%3) == 0 { - -ms-grid-column: 3; - } @else { - -ms-grid-column: ($i%3); - } - - -ms-grid-row: floor(($i - 1)/3)+1; + @supports (display: grid) { + margin-bottom: inherit; + margin-right: inherit; } } diff --git a/client/src/pages/start.vue b/client/src/pages/start.vue index 79cabeed..8fe7d381 100644 --- a/client/src/pages/start.vue +++ b/client/src/pages/start.vue @@ -91,20 +91,26 @@ } - diff --git a/client/src/components/rooms/WidgetPopover.vue b/client/src/components/rooms/WidgetPopover.vue index 45d53c24..68f0963c 100644 --- a/client/src/components/rooms/WidgetPopover.vue +++ b/client/src/components/rooms/WidgetPopover.vue @@ -25,7 +25,8 @@ position: absolute; right: 0; bottom: -110px; - display: grid; + display: flex; + flex-direction: column; background-color: $color-white; padding: 20px; z-index: 10; diff --git a/client/src/layouts/SimpleLayout.vue b/client/src/layouts/SimpleLayout.vue index a38d3a2d..ff35ba0f 100644 --- a/client/src/layouts/SimpleLayout.vue +++ b/client/src/layouts/SimpleLayout.vue @@ -13,14 +13,19 @@ .layout { &--simple { display: -ms-grid; - display: grid; + @supports (display: grid) { + display: grid; + } padding: 20px; + width: 100%; @include desktop { grid-template-columns: 1fr 640px 1fr; + -ms-grid-columns: 1fr 640px 1fr; & > :nth-child(2) { grid-column: 2; + -ms-grid-column: 2; } } @@ -31,8 +36,12 @@ justify-self: end; cursor: pointer; + display:flex; + justify-content:end; + @include desktop { grid-column: 3; + -ms-grid-column: 3; } &__icon { diff --git a/client/src/pages/portfolio.vue b/client/src/pages/portfolio.vue index a66cf482..d5335a80 100644 --- a/client/src/pages/portfolio.vue +++ b/client/src/pages/portfolio.vue @@ -1,7 +1,7 @@