From 0d438babbfb8a3d60d22b9710bb29fdd5c76244d Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Mon, 16 Dec 2019 14:21:53 +0100 Subject: [PATCH 01/22] Add requests dependency --- Pipfile | 1 + Pipfile.lock | 144 ++++++++++++++++++++++----------------------------- 2 files changed, 64 insertions(+), 81 deletions(-) diff --git a/Pipfile b/Pipfile index d56dd984..0297b614 100644 --- a/Pipfile +++ b/Pipfile @@ -40,3 +40,4 @@ python-http-client = "==3.2.1" coverage = "*" graphql-relay = "*" ipython = "*" +requests = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 7cb566c6..3c5dfd20 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "725dd26226fe58559f67b30a09dec72086dc4681a69f2f2672f5bd87c9ac74b8" + "sha256": "73e8e28f265be35eef420da6c66d15089102a8ca943070fe462b8385b0f18f8f" }, "pipfile-spec": 6, "requires": { @@ -23,14 +23,6 @@ ], "version": "==7.0.0" }, - "appnope": { - "hashes": [ - "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0", - "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71" - ], - "markers": "sys_platform == 'darwin'", - "version": "==0.1.0" - }, "backcall": { "hashes": [ "sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", @@ -56,25 +48,25 @@ }, "boto3": { "hashes": [ - "sha256:08d949fede71c14db8b9b638edaa13831d79daed84e2da27750629fd606bdb57", - "sha256:4d7c2cc266917cd0ff7e5e039158de80991e21696a2e8bf85201de2d06d7ceea" + "sha256:210b119965782feacadbdf46944b2ca83d52f7ac7c625ec2e5188c60621a6153", + "sha256:7dc118fafbc81e32d753ee3c00600f04fd032042cec9136fc8fdce3e6f58191d" ], "index": "pypi", - "version": "==1.10.9" + "version": "==1.10.37" }, "botocore": { "hashes": [ - "sha256:61ad58e737e7a5d61308599606cd5a435cc3b97eb7fa693f99663c91bf1706d1", - "sha256:8cb038c110822681925a1f5d9005dc2bbc4259fff89d4abfaaf803a3489d0ee3" + "sha256:a5061854d117e4afb82c2f2da7ff1fb7492045e1968efdca6df80b9a9a1f31d5", + "sha256:cdecb25b58823784d6118d8f991bbd6a36ee220039ea7bf2001029d1bbff0cb0" ], - "version": "==1.13.9" + "version": "==1.13.37" }, "certifi": { "hashes": [ - "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", - "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" + "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", + "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" ], - "version": "==2019.9.11" + "version": "==2019.11.28" }, "chardet": { "hashes": [ @@ -213,11 +205,11 @@ }, "django-storages": { "hashes": [ - "sha256:87287b7ad2e789cd603373439994e1ac6f94d9dc2e5f8173d2a87aa3ed458bd9", - "sha256:f3b3def96493d3ccde37b864cea376472baf6e8a596504b209278801c510b807" + "sha256:0a9b7e620e969fb0797523695329ed223bf540bbfdf6cd163b061fc11dab2d1c", + "sha256:9322ab74ba6371e2e0fccc350c741686ade829e43085597b26b07ae8955a0a00" ], "index": "pypi", - "version": "==1.7.2" + "version": "==1.8" }, "django-taggit": { "hashes": [ @@ -264,10 +256,10 @@ }, "faker": { "hashes": [ - "sha256:5902379d8df308a204fc11c4f621590ee83975805a6c7b2228203b9defa45250", - "sha256:5e8c755c619f332d5ec28b7586389665f136bcf528e165eb925e87c06a63eda7" + "sha256:202ad3b2ec16ae7c51c02904fb838831f8d2899e61bf18db1e91a5a582feab11", + "sha256:92c84a10bec81217d9cb554ee12b3838c8986ce0b5d45f72f769da22e4bb5432" ], - "version": "==2.0.3" + "version": "==3.0.0" }, "future": { "hashes": [ @@ -299,12 +291,11 @@ }, "graphql-relay": { "hashes": [ - "sha256:0e94201af4089e1f81f07d7bd8f84799768e39d70fa1ea16d1df505b46cc6335", - "sha256:75aa0758971e252964cb94068a4decd472d2a8295229f02189e3cbca1f10dbb5", - "sha256:7fa74661246e826ef939ee92e768f698df167a7617361ab399901eaebf80dce6" + "sha256:870b6b5304123a38a0b215a79eace021acce5a466bf40cd39fa18cb8528afabb", + "sha256:ac514cb86db9a43014d7e73511d521137ac12cf0101b2eaa5f0a3da2e10d913d" ], "index": "pypi", - "version": "==2.0.0" + "version": "==2.0.1" }, "gunicorn": { "hashes": [ @@ -330,11 +321,11 @@ }, "ipython": { "hashes": [ - "sha256:dfd303b270b7b5232b3d08bd30ec6fd685d8a58cabd54055e3d69d8f029f7280", - "sha256:ed7ebe1cba899c1c3ccad6f7f1c2d2369464cc77dba8eebc65e2043e19cda995" + "sha256:c66c7e27239855828a764b1e8fc72c24a6f4498a2637572094a78c5551fb9d51", + "sha256:f186b01b36609e0c5d0de27c7ef8e80c990c70478f8c880863004b3489a9030e" ], "index": "pypi", - "version": "==7.9.0" + "version": "==7.10.1" }, "ipython-genutils": { "hashes": [ @@ -380,10 +371,10 @@ }, "newrelic": { "hashes": [ - "sha256:da9adab674d9fe7aa8fbabb6691b33fb7be94a32b6a80548ec7018be9df8ef65" + "sha256:d0e69703792f5abb7ea2440bbbcaa560892c364a09df60bdf36db77f95124f7b" ], "index": "pypi", - "version": "==5.2.1.129" + "version": "==5.4.0.132" }, "parso": { "hashes": [ @@ -447,11 +438,10 @@ }, "prompt-toolkit": { "hashes": [ - "sha256:46642344ce457641f28fc9d1c9ca939b63dadf8df128b86f1b9860e59c73a5e4", - "sha256:e7f8af9e3d70f514373bf41aa51bc33af12a6db3f71461ea47fea985defb2c31", - "sha256:f15af68f66e664eaa559d4ac8a928111eebd5feda0c11738b5998045224829db" + "sha256:0278d2f51b5ceba6ea8da39f76d15684e84c996b325475f6e5720edc584326a7", + "sha256:63daee79aa8366c8f1c637f1a4876b890da5fc92a19ebd2f7080ebacb901e990" ], - "version": "==2.0.10" + "version": "==3.0.2" }, "psycopg2": { "hashes": [ @@ -499,10 +489,10 @@ }, "pygments": { "hashes": [ - "sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127", - "sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297" + "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b", + "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe" ], - "version": "==2.4.2" + "version": "==2.5.2" }, "python-dateutil": { "hashes": [ @@ -555,6 +545,7 @@ "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" ], + "index": "pypi", "version": "==2.22.0" }, "rjsmin": { @@ -614,10 +605,10 @@ }, "six": { "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", + "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" ], - "version": "==1.12.0" + "version": "==1.13.0" }, "text-unidecode": { "hashes": [ @@ -650,11 +641,11 @@ }, "urllib3": { "hashes": [ - "sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398", - "sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86" + "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293", + "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745" ], "markers": "python_version >= '3.4'", - "version": "==1.25.6" + "version": "==1.25.7" }, "wagtail": { "hashes": [ @@ -699,21 +690,13 @@ } }, "develop": { - "appnope": { - "hashes": [ - "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0", - "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71" - ], - "markers": "sys_platform == 'darwin'", - "version": "==0.1.0" - }, "awscli": { "hashes": [ - "sha256:4a75cb44dc3dab14bc9bdb0d2731c37a5026bf0afa4acb620fec72c74e62915a", - "sha256:90b0b3e91a900e4569bc47f29769522337c46ff50e35f9e4a41830fdb425f000" + "sha256:26118aec806fb68e091bff0ec2255d73a7b8b1d8576457874bf2264b857bf2ae", + "sha256:2b89d290dcab2278ea11f627ff03f5fa18264f049b18940fe4d58d7f9e6c4cfb" ], "index": "pypi", - "version": "==1.16.273" + "version": "==1.16.301" }, "backcall": { "hashes": [ @@ -724,10 +707,10 @@ }, "botocore": { "hashes": [ - "sha256:61ad58e737e7a5d61308599606cd5a435cc3b97eb7fa693f99663c91bf1706d1", - "sha256:8cb038c110822681925a1f5d9005dc2bbc4259fff89d4abfaaf803a3489d0ee3" + "sha256:a5061854d117e4afb82c2f2da7ff1fb7492045e1968efdca6df80b9a9a1f31d5", + "sha256:cdecb25b58823784d6118d8f991bbd6a36ee220039ea7bf2001029d1bbff0cb0" ], - "version": "==1.13.9" + "version": "==1.13.37" }, "colorama": { "hashes": [ @@ -792,18 +775,18 @@ }, "ipdb": { "hashes": [ - "sha256:473fdd798a099765f093231a8b1fabfa95b0b682fce12de0c74b61a4b4d8ee57" + "sha256:5d9a4a0e3b7027a158fc6f2929934341045b9c3b0b86ed5d7e84e409653f72fd" ], "index": "pypi", - "version": "==0.12.2" + "version": "==0.12.3" }, "ipython": { "hashes": [ - "sha256:dfd303b270b7b5232b3d08bd30ec6fd685d8a58cabd54055e3d69d8f029f7280", - "sha256:ed7ebe1cba899c1c3ccad6f7f1c2d2369464cc77dba8eebc65e2043e19cda995" + "sha256:c66c7e27239855828a764b1e8fc72c24a6f4498a2637572094a78c5551fb9d51", + "sha256:f186b01b36609e0c5d0de27c7ef8e80c990c70478f8c880863004b3489a9030e" ], "index": "pypi", - "version": "==7.9.0" + "version": "==7.10.1" }, "ipython-genutils": { "hashes": [ @@ -850,11 +833,10 @@ }, "prompt-toolkit": { "hashes": [ - "sha256:46642344ce457641f28fc9d1c9ca939b63dadf8df128b86f1b9860e59c73a5e4", - "sha256:e7f8af9e3d70f514373bf41aa51bc33af12a6db3f71461ea47fea985defb2c31", - "sha256:f15af68f66e664eaa559d4ac8a928111eebd5feda0c11738b5998045224829db" + "sha256:0278d2f51b5ceba6ea8da39f76d15684e84c996b325475f6e5720edc584326a7", + "sha256:63daee79aa8366c8f1c637f1a4876b890da5fc92a19ebd2f7080ebacb901e990" ], - "version": "==2.0.10" + "version": "==3.0.2" }, "ptyprocess": { "hashes": [ @@ -865,17 +847,17 @@ }, "pyasn1": { "hashes": [ - "sha256:62cdade8b5530f0b185e09855dd422bc05c0bbff6b72ff61381c09dac7befd8c", - "sha256:a9495356ca1d66ed197a0f72b41eb1823cf7ea8b5bd07191673e8147aecf8604" + "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", + "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba" ], - "version": "==0.4.7" + "version": "==0.4.8" }, "pygments": { "hashes": [ - "sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127", - "sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297" + "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b", + "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe" ], - "version": "==2.4.2" + "version": "==2.5.2" }, "python-dateutil": { "hashes": [ @@ -920,10 +902,10 @@ }, "six": { "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", + "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" ], - "version": "==1.12.0" + "version": "==1.13.0" }, "traitlets": { "hashes": [ @@ -934,11 +916,11 @@ }, "urllib3": { "hashes": [ - "sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398", - "sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86" + "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293", + "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745" ], "markers": "python_version >= '3.4'", - "version": "==1.25.6" + "version": "==1.25.7" }, "wcwidth": { "hashes": [ From cb1a6fda1922b706c41784ce43251df08194952b Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Mon, 16 Dec 2019 14:23:18 +0100 Subject: [PATCH 02/22] Add taskbase id field to assignments --- .../migrations/0011_assignment_taskbase_id.py | 18 ++++++++++++++++++ server/assignments/models.py | 1 + 2 files changed, 19 insertions(+) create mode 100644 server/assignments/migrations/0011_assignment_taskbase_id.py diff --git a/server/assignments/migrations/0011_assignment_taskbase_id.py b/server/assignments/migrations/0011_assignment_taskbase_id.py new file mode 100644 index 00000000..617d5511 --- /dev/null +++ b/server/assignments/migrations/0011_assignment_taskbase_id.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.6 on 2019-12-11 10:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assignments', '0010_auto_20191210_1427'), + ] + + operations = [ + migrations.AddField( + model_name='assignment', + name='taskbase_id', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/server/assignments/models.py b/server/assignments/models.py index 5ab7486f..f2455f3c 100644 --- a/server/assignments/models.py +++ b/server/assignments/models.py @@ -15,6 +15,7 @@ class Assignment(TimeStampedModel): on_delete=models.PROTECT) # probably don't want to delete all assignments if a user gets deleted module = models.ForeignKey('books.Module', related_name='assignments', on_delete=models.CASCADE) user_created = models.BooleanField(default=False) + taskbase_id = models.CharField(max_length=255, null=True, blank=True) panels = [ FieldPanel('title'), From 17e061892a183c1a7dcb7ed57d378ac361754035 Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Mon, 16 Dec 2019 14:24:08 +0100 Subject: [PATCH 03/22] Fix assignment creation in dummy data --- server/books/factories.py | 14 +++++++++++++- .../core/management/commands/data/module_data.py | 13 +++++++++++++ server/rooms/schema.py | 1 - 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/server/books/factories.py b/server/books/factories.py index 00e3951b..09c22649 100644 --- a/server/books/factories.py +++ b/server/books/factories.py @@ -4,6 +4,7 @@ import factory import wagtail_factories from django.contrib.auth import get_user_model from factory import CREATE_STRATEGY +from wagtail.core import blocks from wagtail.core.models import Page from wagtail.core.rich_text import RichText @@ -96,6 +97,17 @@ class AssignmentBlockFactory(wagtail_factories.StructBlockFactory): class Meta: model = AssignmentBlock + @classmethod + def _build(cls, model_class, *args, **kwargs): + block = model_class() + return blocks.StructValue( + block, + # todo: build in a more generic fashion + [ + (name, kwargs['assignment']) for name, child_block in block.child_blocks.items() + ], + ) + class VideoBlockFactory(wagtail_factories.StructBlockFactory): url = factory.LazyAttribute(lambda x: 'https://www.youtube.com/watch?v=lO9d-AJai8Q') @@ -139,7 +151,7 @@ class ContentBlockFactory(BasePageFactory): owner=user, module=module ) - kwargs['{}__{}__{}__{}'.format(stream_field_name, idx, block_type, 'assignment_id')] = assignment.id + kwargs['{}__{}__{}__{}'.format(stream_field_name, idx, block_type, 'assignment')] = assignment else: diff --git a/server/core/management/commands/data/module_data.py b/server/core/management/commands/data/module_data.py index 76e364b1..44ff137d 100644 --- a/server/core/management/commands/data/module_data.py +++ b/server/core/management/commands/data/module_data.py @@ -49,6 +49,19 @@ module_1_chapter_1 = { 'title': '1.1 Lehrbeginn', 'description': 'Wie sieht Ihr Konsumverhalten aus?', 'content_blocks': [ + { + 'type': 'normal', + 'title': 'Assignment', + 'contents': [ + { + 'type': 'assignment', + 'value': { + 'assignment': 'Ein Auftrag', + 'title': 'Ein Auftragstitel' + } + }, + ] + }, { 'type': 'task', 'title': 'Auftrag 1', diff --git a/server/rooms/schema.py b/server/rooms/schema.py index e9bc1306..4ea816ad 100644 --- a/server/rooms/schema.py +++ b/server/rooms/schema.py @@ -86,7 +86,6 @@ class RoomsQuery(object): class ModuleRoomsQuery(object): - module_room = graphene.Field(RoomNode, slug=graphene.String(), class_id=graphene.ID()) def resolve_module_room(self, info, **kwargs): From bdda817533f195373f2ff4d5a7bfa5188d8cff4c Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Mon, 16 Dec 2019 14:24:50 +0100 Subject: [PATCH 04/22] Add spell check module to backend --- server/api/schema.py | 8 +-- server/spellcheck/__init__.py | 0 server/spellcheck/client.py | 51 ++++++++++++++ server/spellcheck/mutations.py | 121 +++++++++++++++++++++++++++++++++ server/spellcheck/schema.py | 0 5 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 server/spellcheck/__init__.py create mode 100644 server/spellcheck/client.py create mode 100644 server/spellcheck/mutations.py create mode 100644 server/spellcheck/schema.py diff --git a/server/api/schema.py b/server/api/schema.py index f265e893..77de5fb6 100644 --- a/server/api/schema.py +++ b/server/api/schema.py @@ -16,6 +16,7 @@ from objectives.mutations import ObjectiveMutations from objectives.schema import ObjectivesQuery from portfolio.mutations import PortfolioMutations from portfolio.schema import PortfolioQuery +from spellcheck.mutations import SpellCheckMutations from surveys.schema import SurveysQuery from surveys.mutations import SurveyMutations from rooms.mutations import RoomMutations @@ -26,8 +27,7 @@ from registration.mutations_public import RegistrationMutations class Query(UsersQuery, AllUsersQuery, ModuleRoomsQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery, - StudentSubmissionQuery, BasicKnowledgeQuery, PortfolioQuery, SurveysQuery, - graphene.ObjectType): + StudentSubmissionQuery, BasicKnowledgeQuery, PortfolioQuery, SurveysQuery, graphene.ObjectType): node = relay.Node.Field() if settings.DEBUG: @@ -35,8 +35,8 @@ class Query(UsersQuery, AllUsersQuery, ModuleRoomsQuery, RoomsQuery, ObjectivesQ class Mutation(BookMutations, RoomMutations, AssignmentMutations, ObjectiveMutations, CoreMutations, PortfolioMutations, - ProfileMutations, SurveyMutations, NoteMutations, RegistrationMutations, graphene.ObjectType): - + ProfileMutations, SurveyMutations, NoteMutations, RegistrationMutations, SpellCheckMutations, + graphene.ObjectType): if settings.DEBUG: debug = graphene.Field(DjangoDebug, name='__debug') diff --git a/server/spellcheck/__init__.py b/server/spellcheck/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/spellcheck/client.py b/server/spellcheck/client.py new file mode 100644 index 00000000..3479e70e --- /dev/null +++ b/server/spellcheck/client.py @@ -0,0 +1,51 @@ +import re +import requests + +# from spellcheck.client import TaskbaseClient +# client = TaskbaseClient('info@iterativ.ch', 'myverysafepassword1234', 'https://dev-iterativ.taskbase.com') +# client.spellcheck('aOciP9H7tNu7pLsR4ohllk', 'Dies ist ein Sats mit filen Felern') + +class TaskbaseClient: + # def __init__(self, resource_url): + # # resource url should be in the form https://username:password@baseurl + # pattern = re.compile(r'(\w+)://(\w+):(\w+)@([a-zA-Z0-9.]+)') + # scheme, username, password, url = pattern.match(resource_url).groups() + # self.username = username + # self.password = password + # self.token = None + # self.base_url = '{}://{}'.format(scheme, url) + def __init__(self, username, password, base_url): + self.username = username + self.password = password + self.base_url = base_url + + self.token = None + + def login(self): + payload = { + 'username': self.username, + 'password': self.password + } + response = requests.post('{}/api/login'.format(self.base_url), json=payload) + data = response.json() + self.token = data['accessToken'] + + def spellcheck(self, task, text): + if self.token is None: + self.login() + + payload = { + "taskId": task, + "input": { + "text": text, + "type": "SPELL_CHECK" + } + } + + headers = {'Authorization': 'Bearer {}'.format(self.token), 'Content-Type': 'application/json'} + + response = requests.post('{}/api/grade'.format(self.base_url), json=payload, headers=headers) + if response.status_code == 200: + return response.content + else: + raise Exception('Something went wrong') diff --git a/server/spellcheck/mutations.py b/server/spellcheck/mutations.py new file mode 100644 index 00000000..25281944 --- /dev/null +++ b/server/spellcheck/mutations.py @@ -0,0 +1,121 @@ +import json + +import graphene +from django.conf import settings +from graphene import relay + +# class SpellCheckPartNode(graphene.ObjectType): +# sentence = graphene.String() +# offset = graphene.Int() +# length = graphene.Int() +# affected = graphene.String() +# corrected = graphene.String() +from spellcheck.client import TaskbaseClient + + +class SpellCheckStepNode(graphene.ObjectType): + # id = graphene.String() + # part = graphene.Field(SpellCheckPartNode) + sentence = graphene.String() + offset = graphene.Int() + length = graphene.Int() + affected = graphene.String() + corrected = graphene.String() + + # def resolve_sentence(self, *args, **kwargs): + # print(args) + # print(kwargs) + # print(self) + # return self.part.sentence + + # def resolve_offset(self): + # return self.part['offset'] + # + # def resolve_length(self): + # return self.part['length'] + # + # def resolve_affected(self): + # return self.part['affected'] + # + # def resolve_corrected(self): + # return self.part['corrected'] + + +class SpellCheck(relay.ClientIDMutation): + class Input: + text = graphene.String(required=True) + assignment = graphene.ID(required=True) + + results = graphene.List(SpellCheckStepNode) + correct = graphene.Boolean() + + @classmethod + def mutate_and_get_payload(cls, root, info, **kwargs): + user = info.context.user + text = kwargs.get('text') + assignment = kwargs.get('assignment') + + client = TaskbaseClient(settings.TASKBASE_USER, settings.TASKBASE_PASSWORD, settings.TASKBASE_BASEURL) + + data = json.loads(client.spellcheck('aOciP9H7tNu7pLsR4ohllk', text)) + + # data = { + # "correct": "WRONG", + # "solution": "", + # "steps": [ + # { + # "id": 755, + # "part": { + # "sentence": "Das ist ein text mit Felern", + # "offset": 12, + # "length": 4, + # "affected": "text", + # "corrected": "Text", + # "mistakeType": "SPELLING", + # "mistakeSubtype": "CAPITALIZATION", + # "partType": "com.taskbase.lap.server.services.step.tokenizer.SpellCheckPart" + # }, + # "partHash": "802479c1bf90c4ad0f998d550d34c18d502c948b3d72ae2d571808fb24823055", + # "count": 2 + # }, + # { + # "id": 759, + # "part": { + # "sentence": "Das ist ein text mit Felern", + # "offset": 21, + # "length": 6, + # "affected": "Felern", + # "corrected": "Feiern", + # "mistakeType": "SPELLING", + # "mistakeSubtype": "TYPO", + # "partType": "com.taskbase.lap.server.services.step.tokenizer.SpellCheckPart" + # }, + # "partHash": "b6128347caec0cd0d81afd00f03e26bd71785ff12f35e892796d584adfbb18bf", + # "count": 2 + # } + # ] + # } + + return cls(correct=data['correct'] == 'CORRECT', results=map(lambda x: x['part'], data['steps'])) + # user = info.context.user + # module_id = kwargs.get('module') + # bookmarked = kwargs.get('bookmarked') + # + # module = get_object(Module, module_id) + # + # if bookmarked: + # ModuleBookmark.objects.create( + # module=module, + # user=user + # ) + # else: + # ModuleBookmark.objects.get( + # module=module, + # user=user + # ).delete() + # + # return cls(success=True) + + +class SpellCheckMutations: + spell_check = SpellCheck.Field() diff --git a/server/spellcheck/schema.py b/server/spellcheck/schema.py new file mode 100644 index 00000000..e69de29b From 7f719775237bc808bb555bebbfb1bed5866fd0d2 Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Mon, 16 Dec 2019 14:25:55 +0100 Subject: [PATCH 05/22] Add initial frontend implementation for spell checks --- .../content-blocks/assignment/Assignment.vue | 39 +++++- .../assignment/SubmissionForm.vue | 129 ++++++++++-------- .../src/graphql/gql/mutations/spellCheck.gql | 13 ++ client/src/styles/_taskbase.scss | 5 + client/src/styles/main.scss | 1 + 5 files changed, 124 insertions(+), 63 deletions(-) create mode 100644 client/src/graphql/gql/mutations/spellCheck.gql create mode 100644 client/src/styles/_taskbase.scss diff --git a/client/src/components/content-blocks/assignment/Assignment.vue b/client/src/components/content-blocks/assignment/Assignment.vue index cad57597..6e2e5623 100644 --- a/client/src/components/content-blocks/assignment/Assignment.vue +++ b/client/src/components/content-blocks/assignment/Assignment.vue @@ -13,20 +13,25 @@ @saveInput="saveInput" @reopen="reopen" @changeDocumentUrl="changeDocumentUrl" + @spellcheck="spellcheck" :user-input="submission" placeholder="Ergebnis erfassen" action="Ergebnis mit Lehrperson teilen" shared-msg="Das Ergebnis wurde mit der Lehrperson geteilt." :saved="!unsaved" - > + :spellcheck="true" + > +
+ @@ -38,6 +43,7 @@ import ME_QUERY from '@/graphql/gql/meQuery.gql'; import UPDATE_ASSIGNMENT_MUTATION from '@/graphql/gql/mutations/updateAssignmentMutation.gql'; import UPDATE_ASSIGNMENT_MUTATION_WITH_SUCCESS from '@/graphql/gql/mutations/updateAssignmentMutationWithSuccess.gql'; + import SPELL_CHECK_MUTATION from '@/graphql/gql/mutations/spellCheck.gql'; import debounce from 'lodash/debounce'; import cloneDeep from 'lodash/cloneDeep' @@ -178,6 +184,31 @@ final: false, } }, + spellcheck() { + let self = this; + this.$apollo.mutate({ + mutation: SPELL_CHECK_MUTATION, + variables: { + input: { + assignment: 'Hallo', + text: this.assignment.submission.text + } + }, + update(store, {data: {spellCheck: {correct, results}}}) { + console.log(results); + console.log(correct); + let corrections = results.map(result => { + let first, middle, last; + first = result.sentence.substring(0, result.offset); + middle = result.sentence.substring(result.offset, result.offset + result.length); + last = result.sentence.substring(result.offset + result.length); + + return `

${first}${middle}${last}

`; + }); + self.corrections = corrections.join(''); + } + }); + } }, apollo: { @@ -212,7 +243,8 @@ }, inputType: 'text', unsaved: false, - saving: 0 + saving: 0, + corrections: '' } } } @@ -258,6 +290,7 @@ &__feedback { margin-top: $medium-spacing; } + } diff --git a/client/src/components/content-blocks/assignment/SubmissionForm.vue b/client/src/components/content-blocks/assignment/SubmissionForm.vue index 137ab45a..b0d42600 100644 --- a/client/src/components/content-blocks/assignment/SubmissionForm.vue +++ b/client/src/components/content-blocks/assignment/SubmissionForm.vue @@ -1,44 +1,49 @@ diff --git a/client/src/graphql/gql/mutations/spellCheck.gql b/client/src/graphql/gql/mutations/spellCheck.gql new file mode 100644 index 00000000..db54804b --- /dev/null +++ b/client/src/graphql/gql/mutations/spellCheck.gql @@ -0,0 +1,13 @@ +mutation SpellCheck($input: SpellCheckInput!) { + spellCheck(input: $input) { + correct + results { + sentence + offset + length + affected + corrected + } + } +} + diff --git a/client/src/styles/_taskbase.scss b/client/src/styles/_taskbase.scss new file mode 100644 index 00000000..64f13107 --- /dev/null +++ b/client/src/styles/_taskbase.scss @@ -0,0 +1,5 @@ +.taskbase { + &__correction { + background: yellow; + } +} diff --git a/client/src/styles/main.scss b/client/src/styles/main.scss index 62d4cf26..00e4bd68 100644 --- a/client/src/styles/main.scss +++ b/client/src/styles/main.scss @@ -22,3 +22,4 @@ @import "public-page"; @import "student-submission"; @import "module-activity"; +@import "taskbase"; From 53ce39c887b6fd7f716e0611a71992efe09288ee Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Mon, 16 Dec 2019 14:26:25 +0100 Subject: [PATCH 06/22] Add taskbase config variables to settings --- server/core/settings.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server/core/settings.py b/server/core/settings.py index 8a13634b..245f60f3 100644 --- a/server/core/settings.py +++ b/server/core/settings.py @@ -252,7 +252,8 @@ AWS_STORAGE_BUCKET_NAME = os.environ.get('AWS_STORAGE_BUCKET_NAME') AWS_S3_FILE_OVERWRITE = False # use with cloudfront -AWS_S3_CUSTOM_DOMAIN = '{}.s3-{}.amazonaws.com'.format(AWS_STORAGE_BUCKET_NAME, os.environ.get('AWS_REGION', 'eu-west-1')) +AWS_S3_CUSTOM_DOMAIN = '{}.s3-{}.amazonaws.com'.format(AWS_STORAGE_BUCKET_NAME, + os.environ.get('AWS_REGION', 'eu-west-1')) if USE_AWS: DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" # use with cloudfront @@ -316,7 +317,6 @@ if not DEBUG and os.environ.get('SENTRY_DSN'): integrations=[DjangoIntegration()] ) - # LOGGING['handlers'] = { # 'sentry': { # 'level': 'ERROR', # ERROR, WARNING, INFO @@ -360,3 +360,7 @@ EMAIL_BACKEND = 'sendgrid_backend.SendgridBackend' SENDGRID_API_KEY = os.environ.get("SENDGRID_API_KEY") SENDGRID_SANDBOX_MODE_IN_DEBUG = False DEFAULT_FROM_EMAIL = 'myskillbox ' + +TASKBASE_USER = os.environ.get("TASKBASE_USER") +TASKBASE_PASSWORD = os.environ.get("TASKBASE_PASSWORD") +TASKBASE_BASEURL = os.environ.get("TASKBASE_BASEURL") From 9ee3dc48b56750253fb3f4f29f14c4adafbb03e0 Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Tue, 7 Jan 2020 10:25:22 +0100 Subject: [PATCH 07/22] Add login via GraphQL to cypress commands --- client/cypress/support/commands.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/client/cypress/support/commands.js b/client/cypress/support/commands.js index b454f50c..e0eaddad 100644 --- a/client/cypress/support/commands.js +++ b/client/cypress/support/commands.js @@ -24,6 +24,27 @@ // -- This is will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) +Cypress.Commands.add('apolloLogin', (username, password) => { + const payload = { + 'operationName': 'Login', + 'variables': { + 'input': { + 'usernameInput': username, + 'passwordInput': password + } + }, + 'query': 'mutation Login($input: LoginInput!) {\n login(input: $input) {\n success\n errors {\n field\n __typename\n }\n __typename\n }\n}\n' + }; + + cy.request({ + method: 'POST', + url: '/api/graphql-public/', + body: payload + }); + + +}); + // todo: replace with apollo call Cypress.Commands.add("login", (username, password, visitLogin=false) => { if (visitLogin) { From ff7a6b93b38f97bd7f1b6f912aab2abd5c66a7fc Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Tue, 7 Jan 2020 10:26:50 +0100 Subject: [PATCH 08/22] Add first implementation of a test with mocked GraphQL calls --- client/cypress/integration/spellcheck.spec.js | 153 ++++++++++++++++++ client/cypress/support/commands.js | 6 +- client/cypress/support/index.js | 8 +- client/package-lock.json | 69 ++++++++ client/package.json | 1 + 5 files changed, 231 insertions(+), 6 deletions(-) create mode 100644 client/cypress/integration/spellcheck.spec.js diff --git a/client/cypress/integration/spellcheck.spec.js b/client/cypress/integration/spellcheck.spec.js new file mode 100644 index 00000000..d65b8de1 --- /dev/null +++ b/client/cypress/integration/spellcheck.spec.js @@ -0,0 +1,153 @@ +const schema = require('../../../schema_formatted.json'); + +describe('Spellcheck', () => { + beforeEach(() => { + cy.server(); + cy.mockGraphql({ + schema: schema, + // endpoint: '/api/graphql' + }); + }); + + it('should highlight three errors', () => { + let module = { + id: 'TW9kdWxlTm9kZToxNw==', + title: 'Whatevs', + metaTitle: 'Modul 1', + teaser: 'Die Berufsbildung ist ein neuer Lebensabschnit', + intro: '\n

Sie stehen am Anfang eines neuen Lebensabschnitts. In Ihrer Rolle als Berufslernende oder Berufslernender haben Sie Verantwortung \u00fcbernommen.

\n

Wie erging es Ihnen am ersten Arbeits- und Schultag?

\n ', + slug: 'lohn-und-budget', + heroImage: 'https://hep-skillbox-files-prod.s3-eu-central-1.amazonaws.com/original_images/dummy_7bzqodY.jpg', + solutionsEnabled: false, + bookmark: {note: null, '__typename': 'ModuleBookmarkNode'}, + __typename: 'ModuleNode', + assignments: { + 'edges': [{ + 'node': { + 'id': 'QXNzaWdubWVudE5vZGU6MQ==', + 'title': 'Ein Auftragstitel', + 'assignment': 'Ein Auftrag', + 'solution': null, + 'submission': { + 'id': 'U3R1ZGVudFN1Ym1pc3Npb25Ob2RlOjE=', + 'text': 'Hir ist ein Feler gewesen', + 'final': false, + 'document': '', + 'submissionFeedback': { + 'id': 'U3VibWlzc2lvbkZlZWRiYWNrTm9kZTox', + 'text': '\ud83d\ude42\ud83d\ude10\ud83e\udd2c\ud83d\udc4d\ud83e\udd22\ud83e\udd22\ud83e\udd22\ud83e\udd22\ud83d\ude2e\ud83e\udd17', + 'teacher': {'firstName': 'Nico', 'lastName': 'Zickgraf', '__typename': 'UserNode'}, + '__typename': 'SubmissionFeedbackNode' + }, + '__typename': 'StudentSubmissionNode' + }, + '__typename': 'AssignmentNode' + }, + '__typename': 'AssignmentNodeEdge' + }], + '__typename': 'AssignmentNodeConnection' + }, + 'objectiveGroups': { + 'edges': [], '__typename': 'ObjectiveGroupNodeConnection' + }, + 'chapters': { + 'edges': [{ + 'node': { + 'id': 'Q2hhcHRlck5vZGU6MTg=', + 'title': '1.1 Lehrbeginn', + 'description': 'Wie sieht Ihr Konsumverhalten aus?', + 'bookmark': { + 'note': {'id': 'Tm90ZU5vZGU6Mg==', 'text': 'Chapter Chapter', '__typename': 'NoteNode'}, + '__typename': 'ChapterBookmarkNode' + }, + 'contentBlocks': { + 'edges': [{ + 'node': { + 'id': 'Q29udGVudEJsb2NrTm9kZToxOQ==', + 'slug': 'assignment', + 'title': 'Assignment', + 'type': 'NORMAL', + 'contents': [{ + 'type': 'assignment', + 'value': { + 'title': 'Ein Auftragstitel', + 'assignment': 'Ein Auftrag', + 'id': 'QXNzaWdubWVudE5vZGU6MQ==' + }, + 'id': 'df8212ee-3e82-49fa-977e-c4b60789163e' + }], + 'userCreated': false, + 'mine': false, + 'bookmarks': [{ + 'uuid': 'df8212ee-3e82-49fa-977e-c4b60789163e', + 'note': {'id': 'Tm90ZU5vZGU6Mw==', 'text': 'Noch eine Notiz', '__typename': 'NoteNode'}, + '__typename': 'ContentBlockBookmarkNode' + }], + 'hiddenFor': {'edges': [], '__typename': 'SchoolClassNodeConnection'}, + 'visibleFor': {'edges': [], '__typename': 'SchoolClassNodeConnection'}, + '__typename': 'ContentBlockNode' + }, + '__typename': 'ContentBlockNodeEdge' + }], + '__typename': 'ContentBlockNodeConnection' + }, + '__typename': 'ChapterNode' + }, + '__typename': 'ChapterNodeEdge' + }], + '__typename': 'ChapterNodeConnection' + } + }; + + cy.mockGraphqlOps({ + // endpoint: '/api/graphql', + operations: { + MeQuery: { + me: { + permissions: [] + } + }, + ModulesQuery: { + module + }, + SpellCheck: { + spellCheck: { + correct: false, + results: [{ + sentence: 'Hir ist ein Feler gewesen', + offset: 0, + length: 3, + affected: 'Hir', + corrected: 'Dir', + __typename: 'SpellCheckStepNode' + }, { + sentence: 'Hir ist ein Feler gewesen', + offset: 12, + length: 5, + affected: 'Feler', + corrected: 'Fehler', + __typename: 'SpellCheckStepNode' + }, { + sentence: 'Hir ist ein Feler gewesen', + offset: 18, + length: 7, + affected: 'gewesen', + corrected: 'gewesen.', + __typename: 'SpellCheckStepNode' + }], + __typename: 'SpellCheckPayload' + } + } + } + }); + + cy.apolloLogin('rahel.cueni', 'test'); + cy.visit('/module/lohn-und-budget/'); + + cy.get('.spellcheck__correction').should('have.length', 0); + + cy.get('.submission-form-container__spellcheck').click(); + + cy.get('.spellcheck__correction').should('have.length', 3); + }); +}); diff --git a/client/cypress/support/commands.js b/client/cypress/support/commands.js index e0eaddad..c75b0efd 100644 --- a/client/cypress/support/commands.js +++ b/client/cypress/support/commands.js @@ -24,6 +24,8 @@ // -- This is will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) +import 'cypress-graphql-mock'; + Cypress.Commands.add('apolloLogin', (username, password) => { const payload = { 'operationName': 'Login', @@ -46,7 +48,7 @@ Cypress.Commands.add('apolloLogin', (username, password) => { }); // todo: replace with apollo call -Cypress.Commands.add("login", (username, password, visitLogin=false) => { +Cypress.Commands.add("login", (username, password, visitLogin = false) => { if (visitLogin) { cy.visit('/login'); } @@ -88,7 +90,7 @@ Cypress.Commands.add('startGraphQLCapture', () => { // from https://stackoverflow.com/questions/53814647/how-can-i-alias-specific-graphql-requests-in-cypress Cypress.Commands.add('waitFor', operationName => { cy.wait('@graphQL').then(({request}) => { - if(request.body.operationName !== operationName) { + if (request.body.operationName !== operationName) { return cy.waitFor(operationName); } }); diff --git a/client/cypress/support/index.js b/client/cypress/support/index.js index ac5a4f7f..c8f6ff9e 100644 --- a/client/cypress/support/index.js +++ b/client/cypress/support/index.js @@ -20,7 +20,7 @@ import './commands' // require('./commands') // from https://stackoverflow.com/questions/49079005/how-to-stub-a-call-to-graphql-using-cypress#49088084 -Cypress.on('window:before:load', win => { - win.fetch = null; - win.Blob = null; -}); +// Cypress.on('window:before:load', win => { +// win.fetch = null; +// win.Blob = null; +// }); diff --git a/client/package-lock.json b/client/package-lock.json index e994281d..24e4ae86 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -7244,6 +7244,15 @@ } } }, + "cypress-graphql-mock": { + "version": "0.5.0-alpha.4", + "resolved": "https://registry.npmjs.org/cypress-graphql-mock/-/cypress-graphql-mock-0.5.0-alpha.4.tgz", + "integrity": "sha512-dgsczorpXRyG0Jak0N8RdNcyJv+9FPE1cS9UlKEx8g+JBABZF3mVDjpzksnWDvSAUHGrhD+nHFgtgqMXojQVAw==", + "requires": { + "graphql-tools": "^4.0.3", + "tslib": "^1.9.3" + } + }, "d": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", @@ -7463,6 +7472,11 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" }, + "deprecated-decorator": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/deprecated-decorator/-/deprecated-decorator-0.1.6.tgz", + "integrity": "sha1-AJZjF7ehL+kvPMgx91g68ym4bDc=" + }, "des.js": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", @@ -9742,6 +9756,60 @@ "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.10.1.tgz", "integrity": "sha512-jApXqWBzNXQ8jYa/HLkZJaVw9jgwNqZkywa2zfFn16Iv1Zb7ELNHkJaXHR7Quvd5SIGsy6Ny7SUKATgnu05uEg==" }, + "graphql-tools": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/graphql-tools/-/graphql-tools-4.0.6.tgz", + "integrity": "sha512-jHLQw8x3xmSNRBCsaZqelXXsFfUSUSktSCUP8KYHiX1Z9qEuwcMpAf+FkdBzk8aTAFqOlPdNZ3OI4DKKqGKUqg==", + "requires": { + "apollo-link": "^1.2.3", + "apollo-utilities": "^1.0.1", + "deprecated-decorator": "^0.1.6", + "iterall": "^1.1.3", + "uuid": "^3.1.0" + }, + "dependencies": { + "apollo-link": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.13.tgz", + "integrity": "sha512-+iBMcYeevMm1JpYgwDEIDt/y0BB7VWyvlm/7x+TIPNLHCTCMgcEgDuW5kH86iQZWo0I7mNwQiTOz+/3ShPFmBw==", + "requires": { + "apollo-utilities": "^1.3.0", + "ts-invariant": "^0.4.0", + "tslib": "^1.9.3", + "zen-observable-ts": "^0.8.20" + }, + "dependencies": { + "apollo-utilities": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.3.3.tgz", + "integrity": "sha512-F14aX2R/fKNYMvhuP2t9GD9fggID7zp5I96MF5QeKYWDWTrkRdHRp4+SVfXUVN+cXOaB/IebfvRtzPf25CM0zw==", + "requires": { + "@wry/equality": "^0.1.2", + "fast-json-stable-stringify": "^2.0.0", + "ts-invariant": "^0.4.0", + "tslib": "^1.10.0" + }, + "dependencies": { + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" + } + } + } + } + }, + "zen-observable-ts": { + "version": "0.8.20", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.20.tgz", + "integrity": "sha512-2rkjiPALhOtRaDX6pWyNqK1fnP5KkJJybYebopNSn6wDG1lxBoFs2+nwwXKoA6glHIrtwrfBBy6da0stkKtTAA==", + "requires": { + "tslib": "^1.9.3", + "zen-observable": "^0.8.0" + } + } + } + }, "growl": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", @@ -11669,6 +11737,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } diff --git a/client/package.json b/client/package.json index 0660bb1e..ec20a10b 100644 --- a/client/package.json +++ b/client/package.json @@ -37,6 +37,7 @@ "chalk": "^2.0.1", "copy-webpack-plugin": "^4.0.1", "css-loader": "^0.28.0", + "cypress-graphql-mock": "^0.5.0-alpha.4", "debounce": "^1.2.0", "eslint": "^4.15.0", "eslint-config-standard": "^10.2.1", From cfde20c67c997c1d32e848a50a24741951fe824a Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Tue, 7 Jan 2020 10:30:41 +0100 Subject: [PATCH 09/22] Add spell check component --- .../content-blocks/assignment/Assignment.vue | 22 +++----- .../content-blocks/assignment/SpellCheck.vue | 54 +++++++++++++++++++ .../assignment/SubmissionForm.vue | 2 +- 3 files changed, 62 insertions(+), 16 deletions(-) create mode 100644 client/src/components/content-blocks/assignment/SpellCheck.vue diff --git a/client/src/components/content-blocks/assignment/Assignment.vue b/client/src/components/content-blocks/assignment/Assignment.vue index 6e2e5623..0d6d8c0e 100644 --- a/client/src/components/content-blocks/assignment/Assignment.vue +++ b/client/src/components/content-blocks/assignment/Assignment.vue @@ -23,7 +23,7 @@ > -
+