diff --git a/CHANGELOG.md b/CHANGELOG.md index 84bfed38..7e3ca6cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,43 @@ docker compose run --rm -it pgbackups /backup.sh (or `docker-compose` if your version of Docker does not support compose v2). +## [2.2.0] 2024-05-08 + +### Changed +- **Breaking change**: When exporting annotations as JSON, the "features" that the annotator entered are no longer nested under `label` ([#347](https://github.com/GateNLP/gate-teamware/issues/347)). Where previously the export would have been + ```json + { + "features": { + "label": { + "field1": "value1" + } + } + } + ``` + + it is now + ```json + { + "features": { + "field1": "value1" + } + } + ``` +- Include details of failed annotations in export formats ([#399](https://github.com/GateNLP/gate-teamware/pull/399)) + - When exporting annotation data from projects (both via the web UI and using the command line tool), + each document includes details of which users _rejected_, _timed out_ or _aborted_ annotation of + that document, as well as the annotation data from the users who completed the document successfully. + This can be useful for the project manager to identify documents that are particularly difficult + to annotate, perhaps suggesting that the annotation guidelines need to be extended or clarified. + +### Fixed +- Upgraded a number of third-party dependencies to close various vulnerabilities ([#397](https://github.com/GateNLP/gate-teamware/pull/397)) +- Fixed several issues relating to the export of annotated data ([#377](https://github.com/GateNLP/gate-teamware/pull/377)) + - "Anonymous" export was not properly anonymous ([#345](https://github.com/GateNLP/gate-teamware/issues/345)) + - Teamware now does a better job of preserving the GATE BDOC JSON structure when exporting documents that were originally uploaded in that format ([#346](https://github.com/GateNLP/gate-teamware/issues/346), [#348](https://github.com/GateNLP/gate-teamware/issues/348)) +- Added an explicit setting for "no email security", as an alternative to the implicit setting when the relevant environment variable is omitted. This is because the implicit setting was lost on upgrades, whereas an explicit "none" will be preserved ([#402](https://github.com/GateNLP/gate-teamware/pull/402)) + + ## [2.1.1] 2023-10-02 ### Added diff --git a/CITATION.cff b/CITATION.cff index 4d5aa1c2..262b417f 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,6 +1,6 @@ abstract: A web application for collaborative document annotation. GATE teamware provides a flexible web app platform for managing classification of documents by human annotators. -authors: +authors: - affiliation: The University of Sheffield email: t.karmakharm@sheffield.ac.uk family-names: Karmakharm @@ -33,13 +33,7 @@ keywords: - document annotation license: AGPL-3.0 message: If you use this software, please cite it using the metadata from this file. -repository-code: https://github.com/GateNLP/gate-teamware -title: GATE Teamware -type: software -url: https://gatenlp.github.io/gate-teamware/ -version: 2.1.1 preferred-citation: - type: conference-paper authors: - affiliation: The University of Sheffield email: d.wilby@sheffield.ac.uk @@ -66,14 +60,22 @@ preferred-citation: family-names: Bontcheva given-names: Kalina orcid: https://orcid.org/0000-0001-6152-9600 + collection-title: 'Proceedings of the 17th Conference of the European Chapter of + the Association for Computational Linguistics: System Demonstrations' doi: 10.18653/v1/2023.eacl-demo.17 - title: "GATE Teamware 2: An open-source tool for collaborative document classification annotation" - collection-title: "Proceedings of the 17th Conference of the European Chapter of the Association for Computational Linguistics: System Demonstrations" + end: 151 location: name: Dubrovnik, Croatia - year: 2023 month: 5 - start: 145 - end: 151 publisher: name: Association for Computational Linguistics + start: 145 + title: 'GATE Teamware 2: An open-source tool for collaborative document classification + annotation' + type: conference-paper + year: 2023 +repository-code: https://github.com/GateNLP/gate-teamware +title: GATE Teamware +type: software +url: https://gatenlp.github.io/gate-teamware/ +version: 2.2.0 diff --git a/VERSION b/VERSION index 7c327287..e3a4f193 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.1.1 \ No newline at end of file +2.2.0 \ No newline at end of file diff --git a/backend/models.py b/backend/models.py index 249638a9..24ffc0b6 100644 --- a/backend/models.py +++ b/backend/models.py @@ -978,25 +978,28 @@ def get_doc_annotation_dict(self, json_format="raw", anonymize=True): # Create dictionary for document doc_dict = None if json_format == "raw" or json_format == "csv": - doc_dict = self.data + doc_dict = self.data.copy() elif json_format == "gate": + # GATE json format are expected to have an existing "features" field + features_dict = dict(self.data["features"]) if "features" in self.data and isinstance(self.data["features"], dict) else {} - ignore_keys = {"text", self.project.document_id_field} - features_dict = {key: value for key, value in self.data.items() if key not in ignore_keys} + # Add any non-compliant top-level fields into the "features" field instead + ignore_keys = {"text", "features", "offset_type", "annotation_sets", self.project.document_id_field} + features_dict.update({key: value for key, value in self.data.items() if key not in ignore_keys}) doc_dict = { "text": self.data["text"], "features": features_dict, - "offset_type": "p", + "offset_type": self.data["offset_type"] if "offset_type" in self.data else "p", # Use original offset type "name": get_value_from_key_path(self.data, self.project.document_id_field) } - pass # Insert annotation sets into the doc dict annotations = self.annotations.filter(status=Annotation.COMPLETED) if json_format == "csv": + # Gets pre-existing annotations + annotation_sets = dict(self.data["annotations"]) if "annotations" in self.data else {} # Format annotations for CSV export - annotation_sets = {} for annotation in annotations: a_data = annotation.data annotation_dict = {} @@ -1009,19 +1012,21 @@ def get_doc_annotation_dict(self, json_format="raw", anonymize=True): annotation_dict["duration_seconds"] = annotation.time_to_complete if anonymize: - annotation_sets[str(annotation.user.id)] = annotation_dict + annotation_sets[f"{settings.ANONYMIZATION_PREFIX}{annotation.user.id}"] = annotation_dict else: annotation_sets[annotation.user.username] = annotation_dict doc_dict["annotations"] = annotation_sets else: + # Gets pre-existing annotations + annotation_sets = dict(self.data["annotation_sets"]) if "annotation_sets" in self.data else {} # Format for JSON in line with GATE formatting - annotation_sets = {} for annotation in annotations: a_data = annotation.data + anonymized_name = f"{settings.ANONYMIZATION_PREFIX}{annotation.user.id}" annotation_set = { - "name": annotation.user.id if anonymize else annotation.user.username, + "name": anonymized_name if anonymize else annotation.user.username, "annotations": [ { "type": "Document", @@ -1029,16 +1034,36 @@ def get_doc_annotation_dict(self, json_format="raw", anonymize=True): "end": 0, "id": 0, "duration_seconds": annotation.time_to_complete, - "features": { - "label": a_data - } + "features": a_data } ], "next_annid": 1, } - annotation_sets[annotation.user.username] = annotation_set + annotation_sets[anonymized_name if anonymize else annotation.user.username] = annotation_set + doc_dict["annotation_sets"] = annotation_sets + # Add to the export the lists (possibly empty) of users who rejected, + # timed out or aborted annotation of this document + teamware_status = {} + for key, status in [ + ("rejected_by", Annotation.REJECTED), + ("timed_out", Annotation.TIMED_OUT), + ("aborted", Annotation.ABORTED), + ]: + teamware_status[key] = [ + f"{settings.ANONYMIZATION_PREFIX}{annotation.user.id}" if anonymize else annotation.user.username + for annotation in self.annotations.filter(status=status) + ] + if json_format == "csv": + # Flatten list if exporting as CSV + teamware_status[key] = ",".join(str(val) for val in teamware_status[key]) + + if json_format == "gate": + doc_dict["features"]["teamware_status"] = teamware_status + else: + doc_dict["teamware_status"] = teamware_status + return doc_dict diff --git a/backend/rpc.py b/backend/rpc.py index 161fb364..32a48d19 100644 --- a/backend/rpc.py +++ b/backend/rpc.py @@ -510,7 +510,7 @@ def get_projects(request, current_page=1, page_size=None, filters=None): # Perform filtering if isinstance(filters, str): # Search project title if is filter is a string only - projects_query = Project.objects.filter(name__contains=filters.strip()) + projects_query = Project.objects.filter(name__icontains=filters.strip()) total_count = projects_query.count() else: projects_query = Project.objects.all() diff --git a/backend/tests/test_models.py b/backend/tests/test_models.py index 0dfa9bcb..2758df2c 100644 --- a/backend/tests/test_models.py +++ b/backend/tests/test_models.py @@ -1098,8 +1098,11 @@ def test_get_annotations_for_user_in_project(self): class TestDocumentAnnotationModelExport(TestCase): def setUp(self): + self.unanonymized_prefix = "namedperson" self.test_user = get_user_model().objects.create(username="project_creator") - self.annotators = [get_user_model().objects.create(username=f"anno{i}") for i in range(3)] + self.annotator_names = [f"{self.unanonymized_prefix}{i}" for i in range(3)] + self.annotators = [get_user_model().objects.create(username=u) for u in self.annotator_names] + self.anon_annotator_names = [f"{settings.ANONYMIZATION_PREFIX}{a.id}" for a in self.annotators] self.project = Project.objects.create(owner=self.test_user) for i in range(10): document = Document.objects.create( @@ -1110,6 +1113,55 @@ def setUp(self): "feature1": "Testvalue 1", "feature2": "Testvalue 1", "feature3": "Testvalue 1", + "features": { + "gate_format_feature1": "Gate feature test value", + "gate_format_feature2": "Gate feature test value", + "gate_format_feature3": "Gate feature test value", + }, + "offset_type": "x", + "annotations": { + "existing_annotator1": { + "sentiment": "positive" + }, + f"{settings.ANONYMIZATION_PREFIX}{self.annotators[0].pk}": { + "sentiment": "positive" + } + + }, + "annotation_sets": { + "existing_annotator1": { + "name": "existing_annotator1", + "annotations": [ + { + "type": "Document", + "start": 0, + "end": 10, + "id": 0, + "features": { + "sentiment": "positive" + } + } + ], + "next_annid": 1 + }, + f"{settings.ANONYMIZATION_PREFIX}{self.annotators[0].pk}": { + "name": f"{settings.ANONYMIZATION_PREFIX}{self.annotators[0].pk}", + "annotations": [ + { + "type": "Document", + "start": 0, + "end": 10, + "id": 0, + "features": { + "sentiment": "positive" + } + } + ], + "next_annid": 1 + } + + } + } ) @@ -1145,6 +1197,8 @@ def setUp(self): def test_export_raw(self): for document in self.project.documents.all(): + # Fields should remain exactly the same as what's been uploaded + # aside from annotation_sets doc_dict = document.get_doc_annotation_dict("raw") print(doc_dict) self.assertTrue("id" in doc_dict) @@ -1152,44 +1206,91 @@ def test_export_raw(self): self.assertTrue("feature1" in doc_dict) self.assertTrue("feature2" in doc_dict) self.assertTrue("feature3" in doc_dict) + self.assertTrue("features" in doc_dict) + self.assertTrue("offset_type" in doc_dict) + self.assertTrue("annotations" in doc_dict) + doc_features = doc_dict["features"] + self.assertTrue("gate_format_feature1" in doc_features) + self.assertTrue("gate_format_feature2" in doc_features) + self.assertTrue("gate_format_feature3" in doc_features) + self.check_raw_gate_annotation_formatting(doc_dict) + self.check_teamware_status(doc_dict, self.anon_annotator_names) def test_export_gate(self): for document in self.project.documents.all(): + # All top-level fields apart from name, text, features and annotation_sets should be + # nested inside the features field doc_dict = document.get_doc_annotation_dict("gate") print(doc_dict) self.assertTrue("text" in doc_dict) self.assertTrue("features" in doc_dict) + self.assertFalse("annotations" in doc_dict) + self.assertEqual(doc_dict["offset_type"], "x") doc_features = doc_dict["features"] self.assertTrue("id" in doc_features) self.assertTrue("feature1" in doc_features) self.assertTrue("feature2" in doc_features) self.assertTrue("feature3" in doc_features) + self.assertTrue("annotations" in doc_features) + self.assertFalse("features" in doc_features, "Double nesting of features field") + self.assertFalse("offset_type" in doc_features, "Double nesting of offset_type field") + self.assertTrue("gate_format_feature1" in doc_features) + self.assertTrue("gate_format_feature2" in doc_features) + self.assertTrue("gate_format_feature3" in doc_features) self.check_raw_gate_annotation_formatting(doc_dict) + self.check_teamware_status(doc_features, self.anon_annotator_names) + + def test_export_gate_with_no_offset_type(self): + + for document in self.project.documents.all(): + document.data.pop("offset_type") - def check_raw_gate_annotation_formatting(self, doc_dict): + doc_dict = document.get_doc_annotation_dict("gate") + self.assertEqual(doc_dict["offset_type"], "p", "offset_type should default to p") + + + def check_raw_gate_annotation_formatting(self, doc_dict: dict): self.assertTrue("annotation_sets" in doc_dict) - self.assertTrue(len(doc_dict["annotation_sets"]) == 3) + self.assertEqual(len(doc_dict["annotation_sets"]), 4, doc_dict) # Test annotation formatting for aset_key, aset_data in doc_dict["annotation_sets"].items(): - self.assertTrue("name" in aset_data) - self.assertTrue("annotations" in aset_data) - self.assertEqual(len(aset_data["annotations"]), 1) - anno_dict = aset_data["annotations"][0] - self.assertTrue("type" in anno_dict) - self.assertTrue("start" in anno_dict) - self.assertTrue("end" in anno_dict) - self.assertTrue("id" in anno_dict) - self.assertTrue("features" in anno_dict) - self.assertTrue("label" in anno_dict["features"]) - label_dict = anno_dict["features"]["label"] - self.assertTrue("text1" in label_dict) - self.assertTrue("checkbox1" in label_dict) + if aset_key != "existing_annotator1": + self.assertTrue("name" in aset_data) + self.assertTrue("annotations" in aset_data) + self.assertEqual(len(aset_data["annotations"]), 1) + anno_dict = aset_data["annotations"][0] + self.assertTrue("type" in anno_dict) + self.assertTrue("start" in anno_dict) + self.assertTrue("end" in anno_dict) + self.assertTrue("id" in anno_dict) + self.assertTrue("features" in anno_dict) + features_dict = anno_dict["features"] + self.assertTrue("text1" in features_dict) + self.assertTrue("checkbox1" in features_dict) + else: + # Check that existing annotation from document upload is carried over + self.assertEqual(aset_data["annotations"][0]["features"]["sentiment"], "positive") + + + + + def check_teamware_status(self, containing_dict, expected_value): + self.assertTrue("teamware_status" in containing_dict) + teamware_status = containing_dict["teamware_status"] + if isinstance(expected_value, str): + self.assertEqual(teamware_status["rejected_by"], expected_value) + self.assertEqual(teamware_status["aborted"], expected_value) + self.assertEqual(teamware_status["timed_out"], expected_value) + else: + self.assertSetEqual(set(teamware_status["rejected_by"]), set(expected_value)) + self.assertSetEqual(set(teamware_status["aborted"]), set(expected_value)) + self.assertSetEqual(set(teamware_status["timed_out"]), set(expected_value)) def test_export_csv(self): @@ -1203,40 +1304,77 @@ def test_export_csv(self): self.assertTrue("feature2" in doc_dict) self.assertTrue("feature3" in doc_dict) self.assertTrue("annotations" in doc_dict) - self.assertTrue(len(doc_dict["annotations"]) == 3) + self.assertEqual(len(doc_dict["annotations"]), 4, doc_dict) anno_set_dict = doc_dict["annotations"] for set_key in anno_set_dict: - self.assertTrue(isinstance(anno_set_dict[set_key]["text1"], str)) - self.assertTrue(isinstance(anno_set_dict[set_key]["checkbox1"], str)) + if set_key != "existing_annotator1": + self.assertTrue(isinstance(anno_set_dict[set_key]["text1"], str)) + self.assertTrue(isinstance(anno_set_dict[set_key]["checkbox1"], str)) + else: + self.assertEqual(anno_set_dict[set_key]["sentiment"], "positive") + + self.check_teamware_status(doc_dict, ",".join(str(i) for i in self.anon_annotator_names)) def test_export_raw_anonymized(self): for document in self.project.documents.all(): + # Mask any existing annotations that came with the document upload + document.data.pop("annotation_sets") + document.save() + doc_dict = document.get_doc_annotation_dict("raw", anonymize=True) for aset_key, aset_data in doc_dict["annotation_sets"].items(): - self.assertTrue(isinstance(aset_data.get("name", None), int)) + self.assertFalse(aset_key.startswith(self.unanonymized_prefix)) + self.assertFalse(aset_data.get("name", None).startswith(self.unanonymized_prefix)) + + self.check_teamware_status(doc_dict, self.anon_annotator_names) def test_export_raw_deanonymized(self): for document in self.project.documents.all(): + # Mask any existing annotations that came with the document upload + document.data.pop("annotation_sets") + document.save() + doc_dict = document.get_doc_annotation_dict("raw", anonymize=False) for aset_key, aset_data in doc_dict["annotation_sets"].items(): - self.assertTrue(isinstance(aset_data.get("name", None), str)) + self.assertTrue(aset_key.startswith(self.unanonymized_prefix)) + self.assertTrue(aset_data.get("name", None).startswith(self.unanonymized_prefix)) + + # for non-anonymized export the rejected/aborted/timed_out status + # uses names rather than ID numbers + self.check_teamware_status(doc_dict, self.annotator_names) def test_export_gate_anonymized(self): for document in self.project.documents.all(): + # Mask any existing annotations that came with the document upload + document.data.pop("annotation_sets") + document.save() + doc_dict = document.get_doc_annotation_dict("gate", anonymize=True) for aset_key, aset_data in doc_dict["annotation_sets"].items(): - self.assertTrue(isinstance(aset_data.get("name", None), int)) + self.assertFalse(aset_key.startswith(self.unanonymized_prefix)) + self.assertFalse(aset_data.get("name", None).startswith(self.unanonymized_prefix)) + + self.check_teamware_status(doc_dict["features"], self.anon_annotator_names) def test_export_gate_deanonymized(self): for document in self.project.documents.all(): + # Mask any existing annotations that came with the document upload + document.data.pop("annotation_sets") + document.save() + doc_dict = document.get_doc_annotation_dict("gate", anonymize=False) for aset_key, aset_data in doc_dict["annotation_sets"].items(): - self.assertTrue(isinstance(aset_data.get("name", None), str)) + self.assertTrue(aset_key.startswith(self.unanonymized_prefix)) + self.assertTrue(aset_data.get("name", None).startswith(self.unanonymized_prefix)) + + # for non-anonymized export the rejected/aborted/timed_out status + # uses names rather than ID numbers + self.check_teamware_status(doc_dict["features"], self.annotator_names) diff --git a/backend/tests/test_rpc_endpoints.py b/backend/tests/test_rpc_endpoints.py index 45db135e..bbf4788e 100644 --- a/backend/tests/test_rpc_endpoints.py +++ b/backend/tests/test_rpc_endpoints.py @@ -620,6 +620,10 @@ def test_get_projects(self): self.assertEqual(len(result["items"]), 1) self.assertEqual(result["total_count"], 1) + # Ensure filtering is case-insensitive + result = get_projects(self.get_loggedin_request(), 1, page_size, "pROJECT 1") + self.assertEqual(len(result["items"]), 1) + self.assertEqual(result["total_count"], 1) diff --git a/cypress/e2e/document-format-pref.spec.js b/cypress/e2e/document-format-pref.spec.js index bd1020e5..a11b7cbc 100644 --- a/cypress/e2e/document-format-pref.spec.js +++ b/cypress/e2e/document-format-pref.spec.js @@ -61,7 +61,9 @@ describe("Tests for document format preference changes", () => { cy.visit("/") cy.get(".navbar").contains(adminUsername).click() cy.contains("Account").click() + cy.wait(1000) cy.contains("CSV").click() + cy.wait(1000) cy.contains("Projects").click() cy.contains("Test project").click() diff --git a/docs/docs/.vuepress/versions.json b/docs/docs/.vuepress/versions.json index 89bd623d..0623e26d 100644 --- a/docs/docs/.vuepress/versions.json +++ b/docs/docs/.vuepress/versions.json @@ -18,6 +18,14 @@ "text": "2.1.0", "value": "/gate-teamware/2.1.0/" }, + { + "text": "2.1.1", + "value": "/gate-teamware/2.1.1/" + }, + { + "text": "2.2.0", + "value": "/gate-teamware/2.2.0/" + }, { "text": "development", "value": "/gate-teamware/development/" diff --git a/docs/docs/developerguide/README.md b/docs/docs/developerguide/README.md index ef38fc97..02e4494f 100644 --- a/docs/docs/developerguide/README.md +++ b/docs/docs/developerguide/README.md @@ -203,9 +203,13 @@ to the list of environment variables: ```bash DJANGO_EMAIL_BACKEND='django.core.mail.backends.smtp.EmailBackend' DJANGO_EMAIL_HOST='myserver.com' -DJANGO_EMAIL_PORT=22 +DJANGO_EMAIL_PORT=25 DJANGO_EMAIL_HOST_USER='username' DJANGO_EMAIL_HOST_PASSWORD='password' +DJANGO_EMAIL_SECURITY=tls +# tls = STARTTLS, typically on port 25 or 587 +# ssl = TLS-on-connect, typically on port 465 +# none (or omitted) = no encryption ``` #### E-mail using Google API diff --git a/docs/docs/manageradminguide/documents_annotations_management.md b/docs/docs/manageradminguide/documents_annotations_management.md index ac010c9f..7b852340 100644 --- a/docs/docs/manageradminguide/documents_annotations_management.md +++ b/docs/docs/manageradminguide/documents_annotations_management.md @@ -178,14 +178,20 @@ The above column headers will generate the following JSON: ## Exporting documents Documents and annotations can be exported using the **Export** button. A zip file is generated containing files with 500 -documents each. You can choose how documents are exported: +documents each. The option to "anonymize annotators" controls whether the individual annotators are identified with +their numeric ID or by their actual username - since usernames are often personally identifiable information (e.g. an +email address) the anonumous mode is recommended if you intend to share the annotation data with third parties. Note +that the anonymous IDs are consistent within a single installation of Teamware, so even in anonymous mode it is still +possible to determine which documents were annotated by _the same person_, just not who that person was. -* `.json` & `.jsonl` - JSON or JSON Lines files can be generated in the format of: - * `raw` - Exports unmodified JSON. If you've originally uploaded in GATE format then choose this option. +You can choose how documents are exported: - An additional field named `annotation_sets` is added for storing annotations. The annotations are laid out in the - same way as GATE JSON format. For example if a document has been annotated by `user1` with labels and values - `text`:`Annotation text`, `radio`:`val3`, and `checkbox`:`["val2", "val4"]`: +* `.json` & `.jsonl` - JSON or JSON Lines files can be generated in the format of: + * `raw` - Exports the original `JSON` combined with an additional field named `annotation_sets` for storing + annotations. The annotations are laid out in the same way as GATE + [bdocjs](https://gatenlp.github.io/gateplugin-Format_Bdoc/bdoc_document.html) format. For example if a document + has been annotated by `user1` with labels and values `text`:`Annotation text`, `radio`:`val3`, and + `checkbox`:`["val2", "val4"]`, the non-anonymous export might look like this: ```json { @@ -203,26 +209,38 @@ documents each. You can choose how documents are exported: "end":10, "id":0, "features":{ - "label":{ - "text":"Annotation text", - "radio":"val3", - "checkbox":[ - "val2", - "val4" - ] - } + "text":"Annotation text", + "radio":"val3", + "checkbox":[ + "val2", + "val4" + ] } } ], "next_annid":1 } + }, + "teamware_status": { + "rejected_by": ["user2"], + "timed_out": ["user3"], + "aborted": [] } } ``` - * `gate` - Convert documents to GATE JSON format and export. A `name` field is added that takes the ID value from the - ID field specified in the project configuration. Fields apart from `text` and the ID field specified in the project - config are placed in the `features` field. An `annotation_sets` field is added for storing annotations. + In anonymous mode the name `user1` would instead be derived from the user's opaque numeric identifier (e.g. + `annotator105`). + + The field `teamware_status` gives the usernames or anonymous IDs (depending on the "anonymize" setting) of those annotators + who rejected the document, "timed out" because they did not complete their annotation in the time allowed by the + project, or "aborted" for some other reason (e.g. they were removed from the project). + + * `gate` - Convert documents to GATE [bdocjs](https://gatenlp.github.io/gateplugin-Format_Bdoc/bdoc_document.html) + format and export. A `name` field is added that takes the `ID` value from the `ID field` specified in the + **project configuration**. Any top-level fields apart from `text`, `features`, `offset_type`, `annotation_sets`, + and the ID field specified in the project config are placed in the `features` field, as is the `teamware_status` + information. An `annotation_sets` field is added for storing annotations if it doesn't already exist. For example in the case of this uploaded JSON document: ```json @@ -233,21 +251,27 @@ documents each. You can choose how documents are exported: "feature1": "Feature text" } ``` - The generated output is as follows. The annotations are formatted same as the `raw` output above: + The generated output is as follows. The annotations and `teamware_status` are formatted same as the `raw` output + above: ```json { "name": 32, "text": "Document text", "features": { "text2": "Document text 2", - "feature1": "Feature text" + "feature1": "Feature text", + "teamware_status": {...} }, "offset_type":"p", "annotation_sets": {...} } ``` * `.csv` - The JSON documents will be flattened to csv's column based format. Annotations are added as additional - columns with the header of `annotations.username.label`. + columns with the header of `annotations.username.label` and the status information is in columns named + `teamware_status.rejected_by`, `teamware_status.timed_out` and `teamware_status.aborted`. + +**Note: Documents that contains existing annotations (i.e. the `annotation_sets` field for `JSON` or `annotations` for `CSV`) are merged with the new sets of annotations. Be aware that if the document has a new annotation from an annotator with the same +username, the previous annotation will be overwritten. Existing annotations are also not anonymized when exporting the document.** ## Deleting documents and annotations diff --git a/docs/package.json b/docs/package.json index a79d2b60..2e31e446 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,6 +1,6 @@ { "name": "gate-teamware-docs", - "version": "2.1.1", + "version": "2.2.0", "description": "Documentation for GATE Teamware.", "main": "index.js", "scripts": { diff --git a/docs/versioned/0.3.0/.vuepress/versions.json b/docs/versioned/0.3.0/.vuepress/versions.json index 51082447..135e3576 100644 --- a/docs/versioned/0.3.0/.vuepress/versions.json +++ b/docs/versioned/0.3.0/.vuepress/versions.json @@ -18,6 +18,14 @@ "text": "2.1.0", "value": "/gate-teamware/2.1.0/" }, + { + "text": "2.1.1", + "value": "/gate-teamware/2.1.1/" + }, + { + "text": "2.2.0", + "value": "/gate-teamware/2.2.0/" + }, { "text": "development", "value": "/gate-teamware/development/" diff --git a/docs/versioned/0.4.0/.vuepress/versions.json b/docs/versioned/0.4.0/.vuepress/versions.json index 506c50de..8854f71a 100644 --- a/docs/versioned/0.4.0/.vuepress/versions.json +++ b/docs/versioned/0.4.0/.vuepress/versions.json @@ -18,6 +18,14 @@ "text": "2.1.0", "value": "/gate-teamware/2.1.0/" }, + { + "text": "2.1.1", + "value": "/gate-teamware/2.1.1/" + }, + { + "text": "2.2.0", + "value": "/gate-teamware/2.2.0/" + }, { "text": "development", "value": "/gate-teamware/development/" diff --git a/docs/versioned/2.0.0/.vuepress/versions.json b/docs/versioned/2.0.0/.vuepress/versions.json index 5030337e..652ccdb8 100644 --- a/docs/versioned/2.0.0/.vuepress/versions.json +++ b/docs/versioned/2.0.0/.vuepress/versions.json @@ -18,6 +18,14 @@ "text": "2.1.0", "value": "/gate-teamware/2.1.0/" }, + { + "text": "2.1.1", + "value": "/gate-teamware/2.1.1/" + }, + { + "text": "2.2.0", + "value": "/gate-teamware/2.2.0/" + }, { "text": "development", "value": "/gate-teamware/development/" diff --git a/docs/versioned/2.1.0/.vuepress/versions.json b/docs/versioned/2.1.0/.vuepress/versions.json index 5fb15a42..52e6cd06 100644 --- a/docs/versioned/2.1.0/.vuepress/versions.json +++ b/docs/versioned/2.1.0/.vuepress/versions.json @@ -18,6 +18,14 @@ "text": "2.1.0", "value": "/gate-teamware/2.1.0/" }, + { + "text": "2.1.1", + "value": "/gate-teamware/2.1.1/" + }, + { + "text": "2.2.0", + "value": "/gate-teamware/2.2.0/" + }, { "text": "development", "value": "/gate-teamware/development/" diff --git a/docs/versioned/2.1.1/.vuepress/components/AnnotationRendererPreview.vue b/docs/versioned/2.1.1/.vuepress/components/AnnotationRendererPreview.vue new file mode 100644 index 00000000..98ecc1af --- /dev/null +++ b/docs/versioned/2.1.1/.vuepress/components/AnnotationRendererPreview.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/docs/versioned/2.1.1/.vuepress/components/DisplayVersion.vue b/docs/versioned/2.1.1/.vuepress/components/DisplayVersion.vue new file mode 100644 index 00000000..03ec07ed --- /dev/null +++ b/docs/versioned/2.1.1/.vuepress/components/DisplayVersion.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/docs/versioned/2.1.1/.vuepress/config.js b/docs/versioned/2.1.1/.vuepress/config.js new file mode 100644 index 00000000..b78cc868 --- /dev/null +++ b/docs/versioned/2.1.1/.vuepress/config.js @@ -0,0 +1,42 @@ +const versionData = require("./versions.json") +const path = require("path"); +module.exports = context => ({ + title: 'GATE Teamware Documentation', + description: 'Documentation for GATE Teamware', + base: versionData.base, + themeConfig: { + nav: [ + {text: 'Home', link: '/'}, + {text: 'Annotators', link: '/annotatorguide/'}, + {text: 'Managers & Admins', link: '/manageradminguide/'}, + {text: 'Developer', link: '/developerguide/'} + ], + sidebar: { + '/manageradminguide/': [ + "", + "project_management", + "project_config", + "documents_annotations_management", + "annotators_management" + ], + '/developerguide/': [ + '', + 'frontend', + 'testing', + 'releases', + 'documentation', + "api_docs", + + ], + }, + }, + configureWebpack: { + resolve: { + alias: { + '@': path.resolve(__dirname, versionData.frontendSource) + } + } + }, + + +}) diff --git a/docs/versioned/2.1.1/.vuepress/enhanceApp.js b/docs/versioned/2.1.1/.vuepress/enhanceApp.js new file mode 100644 index 00000000..e7aadadf --- /dev/null +++ b/docs/versioned/2.1.1/.vuepress/enhanceApp.js @@ -0,0 +1,17 @@ +import Vue from 'vue' +import {BootstrapVue, BootstrapVueIcons, IconsPlugin} from 'bootstrap-vue' + +import 'bootstrap/dist/css/bootstrap.css' +import 'bootstrap-vue/dist/bootstrap-vue.css' + +Vue.use(BootstrapVue) +Vue.use(BootstrapVueIcons) + +export default ({ + Vue, // the version of Vue being used in the VuePress app + options, // the options for the root Vue instance + router, // the router instance for the app + siteData // site metadata +}) => { + +} diff --git a/docs/versioned/2.1.1/.vuepress/theme/components/Navbar.vue b/docs/versioned/2.1.1/.vuepress/theme/components/Navbar.vue new file mode 100644 index 00000000..c3b966db --- /dev/null +++ b/docs/versioned/2.1.1/.vuepress/theme/components/Navbar.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/docs/versioned/2.1.1/.vuepress/theme/components/VersionSelector.vue b/docs/versioned/2.1.1/.vuepress/theme/components/VersionSelector.vue new file mode 100644 index 00000000..4cfb5eb9 --- /dev/null +++ b/docs/versioned/2.1.1/.vuepress/theme/components/VersionSelector.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/docs/versioned/2.1.1/.vuepress/theme/index.js b/docs/versioned/2.1.1/.vuepress/theme/index.js new file mode 100644 index 00000000..b91b8a57 --- /dev/null +++ b/docs/versioned/2.1.1/.vuepress/theme/index.js @@ -0,0 +1,3 @@ +module.exports = { + extend: '@vuepress/theme-default' +} diff --git a/docs/versioned/2.1.1/.vuepress/versions.json b/docs/versioned/2.1.1/.vuepress/versions.json new file mode 100644 index 00000000..bd5cf599 --- /dev/null +++ b/docs/versioned/2.1.1/.vuepress/versions.json @@ -0,0 +1,35 @@ +{ + "current": "2.1.1", + "base": "/gate-teamware/2.1.1/", + "versions": [ + { + "text": "0.3.0", + "value": "/gate-teamware/0.3.0/" + }, + { + "text": "0.4.0", + "value": "/gate-teamware/0.4.0/" + }, + { + "text": "2.0.0", + "value": "/gate-teamware/2.0.0/" + }, + { + "text": "2.1.0", + "value": "/gate-teamware/2.1.0/" + }, + { + "text": "2.1.1", + "value": "/gate-teamware/2.1.1/" + }, + { + "text": "2.2.0", + "value": "/gate-teamware/2.2.0/" + }, + { + "text": "development", + "value": "/gate-teamware/development/" + } + ], + "frontendSource": "../../../../frontend/src" +} \ No newline at end of file diff --git a/docs/versioned/2.1.1/README.md b/docs/versioned/2.1.1/README.md new file mode 100644 index 00000000..3586c291 --- /dev/null +++ b/docs/versioned/2.1.1/README.md @@ -0,0 +1,138 @@ +# GATE Teamware + +![GATE Teamware logo](./img/gate-teamware-logo.svg "GATE Teamware logo") + +A web application for collaborative document annotation. + +This is a documentation for Teamware version: + +## Key Features +* Free and open source software. +* Configure annotation options using a highly flexible JSON config. +* Set limits on proportions of a task that annotators can annotate. +* Import existing annotations as CSV or JSON. +* Export annotations as CSV or JSON. +* Annotation instructions and document rendering supports markdown and HTML. + +## Getting started +A quickstart guide for annotators is [available here](annotatorguide). + +To use an existing instance of GATE Teamware as a project manager or admin, find instructions in the [Managers and Admins guide](manageradminguide). + +Documentation on deploying your own instance can be found in the [Developer Guide](developerguide). + +## Installation Guide + +### Quick Start + +The simplest way to deploy your own copy of GATE Teamware is to use Docker Compose on Linux or Mac. Installation on Windows is possible but not officially supported - you need to be able to run `bash` shell scripts for the quick-start installer. + +1. Install Docker - [Docker Engine](https://docs.docker.com/engine/) for Linux servers or [Docker Desktop](https://docs.docker.com/desktop/) for Mac. +2. Install [Docker Compose](https://github.com/docker/compose), if your Docker does not already include it (Compose is included by default with Docker Desktop) +3. Download the [installation script](https://gate.ac.uk/get-teamware.sh) into an empty directory, run it and follow the instructions. + +``` +mkdir gate-teamware +cd gate-teamware +curl -LO https://gate.ac.uk/get-teamware.sh +bash ./get-teamware.sh +``` + +This will make the Teamware application available as `http://localhost:8076`, with the option to expose it as a public `https://` URL if your server is directly internet-accessible - for production use we recommend deploying Teamware with a suitable internet-facing reverse proxy, or use Kubernetes as described below. + +### Deployment using Kubernetes + +A Helm chart to deploy Teamware on Kubernetes is published to the GATE team public charts repository. The chart requires [Helm](https://helm.sh) version 3.7 or later, and is compatible with Kubernetes version 1.23 or later. Earlier Kubernetes versions back to 1.19 _may_ work provided autoscaling is not enabled, but these have not been tested. + +The following quick start instructions assume you have a compatible Kubernetes cluster and a working installation of `kubectl` and `helm` (3.7 or later) with permission to create all the necessary resource types in your target namespace. + +First generate a random "secret key" for the Django application. This must be at least 50 random characters, a quick way to do this is + +``` +# 42 random bytes base64 encoded becomes 56 random characters +kubectl create secret generic -n {namespace} django-secret \ + --from-literal="secret-key=$( openssl rand -base64 42 )" +``` + +Add the GATE charts repository to your Helm configuration: + +``` +helm repo add gate https://repo.gate.ac.uk/repository/charts +helm repo update +``` + +Create a `values.yaml` file with the key settings required for teamware. The following is a minimal set of values for a typical installation: + +```yaml +# Public-facing web hostname of the teamware application, the public +# URL will be https://{hostName} +hostName: teamware.example.com + +email: + # "From" address on emails sent by Teamware + adminAddress: admin@teamware.example.com + # Send email via an SMTP server - alternatively "gmail" to use GMail API + backend: "smtp" + smtp: + host: mail.example.com + # You will also need to set user and passwordSecret if your + # mail server requires authentication + +privacyPolicy: + # Contact details of the host and administrator of the teamware + # instance, if no admin defined, defaults to the host values. + host: + # Name of the host + name: "Service Host" + # Host's physical address + address: "123 Example Street, City. Country." + # A method of contacting the host, field supports HTML for e.g. linking to a form + contact: "Email" + admin: + name: "Dr. Service Admin" + address: "Department of Example Studies, University of Example, City. Country." + contact: "Email" + +backend: + # Name of the random secret you created above + djangoSecret: django-secret + +# Initial "super user" created on the first install. These are just +# the *initial* settings, you can (and should!) change the password +# once Teamware is up and running +superuser: + email: me@example.com + username: admin + password: changeme +``` + +Some of these may be omitted or others may be required depending on the setup of your specific cluster - see the [chart README](https://github.com/GateNLP/charts/blob/main/gate-teamware/README.md) and the chart's own values file (which you can retrieve with `helm show values gate/gate-teamware`) for full details. In particular these values assume: + +- your cluster has an ingress controller, with a default ingress class configured, and that controller has a default TLS certificate that is compatible with your chosen hostname (e.g. a `*.example.com` wildcard) +- your cluster has a default storageClass configured to provision PVCs, and at least 8 GB of available PV capacity +- you can send email via an SMTP server with no authentication +- the default GATE Teamware terms and privacy documents are suitable for your deployment and compliant with the laws of your location. If this is not the case you can supply your own custom policy documents in a ConfigMap +- you do not need to back up your PostgreSQL database - the chart does include the option to store backups in Amazon S3 or another compatible object store, see the full README for details + +Once you have created your values file, you can install the chart or upgrade an existing installation using + +``` +helm upgrade --install gate-teamware gate/gate-teamware \ + --namespace {namespace} --values teamware-values.yaml +``` + + +## Bug reports and feature requests +Please make bug reports and feature requests as Issues on the [GATE Teamware GitHub repo](https://github.com/GATENLP/gate-teamware). + +# Using Teamware +Teamware is developed by the [GATE](https://gate.ac.uk) team, an academic research group at The University of Sheffield. As a result, future funding relies on evidence of the impact that the software provides. If you use Teamware, please let us know using the contact form at [gate.ac.uk](https://gate.ac.uk/g8/contact). Please include details on grants, publications, commercial products etc. Any information that can help us to secure future funding for our work is greatly appreciated. + +## Citation +For published work that has used Teamware, please cite this repository. One way is to include a citation such as: + +> Karmakharm, T., Wilby, D., Roberts, I., & Bontcheva, K. (2022). GATE Teamware (Version 0.1.4) [Computer software]. https://github.com/GateNLP/gate-teamware + +Please use the `Cite this repository` button at the top of the [project's GitHub repository](https://github.com/GATENLP/gate-teamware) to get an up to date citation. + +The Teamware version can be found on the 'About' page of your Teamware instance. diff --git a/docs/versioned/2.1.1/annotatorguide/README.md b/docs/versioned/2.1.1/annotatorguide/README.md new file mode 100644 index 00000000..4a3ddae2 --- /dev/null +++ b/docs/versioned/2.1.1/annotatorguide/README.md @@ -0,0 +1,27 @@ +# Annotators Quickstart + +Annotating a project: + +* After signing up to the site, notify the owner of the annotation project you've been recruited of + your username. This will allow them to add you as an annotator to a project. +* After you've been recruited to a project, click on the `Annotate` link on the navigation bar at the + top of the page to start annotating. +* You will be shown the details about the project you're annotating along with a set of form(s) to capture + your annotation. Ensure you've read the Annotator guideline fully before starting the annotation process. +* You can then start annotating documents one at a time. Click on `Submit` to confirm the completion of + annotation, `Clear` to start again or `Reject` to skip the particular document. Be aware some projects + do not allow you to skip documents. +* Once you've finished annotating a certain number of documents in a project (specified by the project + manager) your task will be deemed complete, and you will be able to be recruited into another annotation + project. + +## Deleting your account + +At any time you can choose to stop participating and delete your account. You can do this by: + +* Click on your username in the top right corner and then `Account`. +* Click on `Delete my account`. +* When deleting your account, by default your personal information will be removed but your annotations will remain on the system. To completely remove all of your annotations, click on the checkbox next to `Also remove any annotations, projects and documents that I own:`. +* Click the `Unlock` button. +* Then click `Delete` to remove your account. + diff --git a/docs/versioned/2.1.1/developerguide/README.md b/docs/versioned/2.1.1/developerguide/README.md new file mode 100644 index 00000000..ef38fc97 --- /dev/null +++ b/docs/versioned/2.1.1/developerguide/README.md @@ -0,0 +1,282 @@ +# Developer guide + +## Architecture +``` +├── .github/workflows/ # github actions workflow files +├── teamware/ # Django project +│   └── settings/ +├── backend/ # Django app +├── cypress/ # integration test configurations +├── docs/ # documentation +├── examples/ # example data files +├── frontend/ # all frontend, in VueJS framework +├── nginx/ # Nginx configurations +| +# Top level directory contains scripts for management and deployment, +# main project package.json, python requirements, docker configs +├── build-images.sh +├── deploy.sh +├── create-django-db.sh +├── docker-compose.yml +├── Dockerfile +├── generate-docker-env.sh +├── manage.py +├── migrate-integration.sh +├── package.json +├── package-lock.json +├── pytest.ini +├── README.md +├── requirements-dev.txt +├── requirements.txt +└── run-server.sh + +``` + +## Installation for development + +The service depends on a combination of python and javascript libraries. We recommend developing inside a `conda` conda environment as it is able to install +python libraries and nodejs which is used to install javascript libraries. + +* Install anaconda/miniconda +* Create a blank virtual conda env + ```bash + $ conda create -n teamware python=3.9 + ``` +* Activate conda environment + ```bash + $ source activate teamware + # or + $ conda activate teamware + ``` +* Install python dependencies in conda environment using pip + ```bash + (teamware)$ pip install -r requirements.txt -r requirements-dev.txt + ``` +* Install nodejs, postgresql and openssl in the conda environment + ```bash + (teamware)$ conda install -y -c conda-forge postgresql=14.* + (teamware)$ conda install -y -c conda-forge nodejs=18.* + ``` +* Install nodejs dependencies + ```bash + (teamware)$ npm install + ``` + +Set up a new postgreSQL database and user for development: +``` +# Create a new directory for the db data and initialise +mkdir -p pgsql/data +initdb -D pgsql/data + +# Launch postgres in the background +postgres -p 5432 -D pgsql/data & + +# Create a DB user, you'll be prompted to input password, "password" is the default in teamware/settings/base.py for development +createuser -p 5432 -P user --createdb + +# Create a rumours_db with rumours as user +createdb -p 5432 -O user teamware_db + +# Migrate & create database tables +python manage.py migrate + +# create a new superuser - when prompted enter a username and password for the db superuser +python manage.py createsuperuser +``` + +## Updating packages +To update packages after a merge, run the following commands: + +```bash +# Activate the conda environment +source activate teamware +# Update any packages changed in the python requirements.txt and requirements-dev.txt files +pip install -r requirements.txt -r requirements-dev.txt +# Update any packages changed in package.json +npm install +``` + +## Development server +The application uses django's dev server to serve page contents and run the RPC API, it also uses Vue CLI's +development server to serve dynamic assets such as javascript or stylesheets allowing for hot-reloading +during development. + +To run both servers together: + + ```bash + npm run serve + ``` + +To run separately: + +* Django server + ```bash + npm run serve:backend + ``` +* Vue CLI dev server + ```bash + npm run serve:frontend + ``` + +## Deploying a development version using Docker +Deployment is via [docker-compose](https://docs.docker.com/compose/), using [NGINX](https://www.nginx.com/) to serve static content, a separate [postgreSQL](https://hub.docker.com/_/postgres) service containing the database and a database backup service (see `docker-compose.yml` for details). Pre-built images can be run using most versions of Docker but _building_ images requires `docker buildx`, which means either Docker Desktop or version 19.03 or later of Docker Engine. + +1. Run `./generate-docker-env.sh` to create a `.env` file containing randomly generated secrets which are mounted as environment variables into the container. See [below](#env-config) for details. + +2. Then build the images via: + ```bash + ./build-images.sh + ``` + +3. then deploy the stack with + + ```bash + ./deploy.sh production # (or prod) to deploy with production settings + ./deploy.sh staging # (or stag) to deploy with staging settings + ``` + +To bring the stack down, run `docker-compose down`, using the `-v` flag to destroy the database volume (be careful with this). + +### Configuration using environment variables (.env file) + +To allow the app to be easily configured between instances especially inside containers, many of the app's configuration can be done through environment variables. + +Run `./generate-docker-env.sh` to generate a `.env` file with all configurable environment parameters. + +To set values for your own deployment, add values to the variables in `.env`, most existing values will be kept after running `generate-docker-env.sh`, see comments in `.env` for specific details. Anything that is left blank will be filled with a default value. Passwords and keys are filled with auto-generated random values. + +Existing `.env` files are copied into a new file named `saved-env.` by `generate-docker-env.sh`. + +### Backups + +In a docker-compose based deployment, backups of the database are managed by the service `pgbackups` which uses the [`prodrigestivill/postgres-backup-local:12`](https://hub.docker.com/r/prodrigestivill/postgres-backup-local) image. +By default, backups are taken of the database daily, and the `docker-compose.yml` contains settings for the number of backups kept under the options for the `pgbackups` service. +Backups are stored as a gzipped SQL dump from the database. + +#### Taking a manual backup + +A shell script is provided for manually triggering a backup snapshot. +From the main project directory run + +```sh +$ ./backup_manual.sh +``` + +This uses the `pgbackups` service and all settings and envrionment variables it is configured with in `docker-compose.yml`, so backups will be taken to the same location as configured for the main backup schedule. + +#### Restoring from a backup +1. Locate the backup file (`*.sql.gz`) on your system that you would like to restore from. +2. Make sure that the stack is down, from the main project directory run `docker-commpose down`. +3. Run the backup restore shell script, passing in the path to your backup file as the only argument: + +```sh +$ ./backup_restore.sh path/to/my/backup.sql.gz +``` + +This will first launch the database container, then via Django's `dbshell` command, running in the `backend` service, execute a number of SQL commands before and after running all the SQL from the backup file. + +4. Redeploy the stack, via `./deploy.sh staging`, `./deploy.sh production`, or simply `docker compose up -d`, whichever is the case. +5. The database *should* be restored. + +## Configuration + +### Django settings files + +Django settings are located in `teamware/settings` folder. The app will use `base.py` setting by default +and this must be overridden depending on use. + +### Database +A SQLite3 database is used during development and during integration testing. + +For staging and production, postgreSQL is used, running from a `postgres-14` docker container. Settings are found in `teamware/settings/base.py` and `deployment.py` as well as being set as environment variables by `./generate-docker-env.sh` and passed to the container as configured in `docker-compose.yml`. + +In Kubernetes deployments the PostgreSQL database is installed using the Bitnami `postresql` public chart. + + +### Sending E-mail +It's recommended to specify e-mail configurations through environment variables (`.env`). As these settings will include username and passwords that should not be tracked by version control. + +#### E-mail using SMTP +SMTP is supported as standard in Django, add the following configurations with your own details +to the list of environment variables: + +```bash +DJANGO_EMAIL_BACKEND='django.core.mail.backends.smtp.EmailBackend' +DJANGO_EMAIL_HOST='myserver.com' +DJANGO_EMAIL_PORT=22 +DJANGO_EMAIL_HOST_USER='username' +DJANGO_EMAIL_HOST_PASSWORD='password' +``` + +#### E-mail using Google API +The [django-gmailapi-backend](https://github.com/dolfim/django-gmailapi-backend) library +has been added to allow sending of mail through Google's API as sending through SMTP is disabled as standard. + +Unlike with SMTP, Google's API requires OAuth authentication which means a project and a credential has to be +created through Google's cloud console. + +* More information on the Gmail API: [https://developers.google.com/gmail/api/guides/sending](https://developers.google.com/gmail/api/guides/sending) +* OAuth credentials for sending emails: [https://github.com/google/gmail-oauth2-tools/wiki/OAuth2DotPyRunThrough](https://github.com/google/gmail-oauth2-tools/wiki/OAuth2DotPyRunThrough) + +This package includes the script linked in the documentation above, which simplifies the setup of the API credentials. The following outlines the key steps: + +1. Create a project in the Google developer console, [https://console.cloud.google.com/](https://console.cloud.google.com/) +2. Enable the Gmail API +3. Create OAuth 2.0 credentials, you'll likely want to create a `Desktop` +4. Create a valid refresh_token using the helper script included in the package: + ```bash + gmail_oauth2 --generate_oauth2_token \ + --client_id="" \ + --client_secret="" \ + --scope="https://www.googleapis.com/auth/gmail.send" + ``` +5. Add the created credentials and tokens to the environment variable as shown below: + ```bash + DJANGO_EMAIL_BACKEND='gmailapi_backend.mail.GmailBackend' + DJANGO_GMAIL_API_CLIENT_ID='google_assigned_id' + DJANGO_GMAIL_API_CLIENT_SECRET='google_assigned_secret' + DJANGO_GMAIL_API_REFRESH_TOKEN='google_assigned_token' + ``` + + +#### Teamware Privacy Policy and Terms & Conditions + +Teamware includes a default privacy policy and terms & conditions, which are required for running the application. + +The default privacy policy is intended to be compliant with UK GDPR regulations, which may comply with the rights of users of your deployment, however it is your responsibility to ensure that this is the case. + +If the default privacy policy covers your use case, then you will need to include configuration for a few contact details. + +Contact details are required for the **host** and the **administrator**: the **host** is the organisation or individual responsible for managing the deployment of the teamware instance and the **administrator** is the organisation or individual responsible for managing users, projects and data on the instance. In many cases these roles will be filled by the same organisation or individual, so in this case specifying just the **host** details is sufficient. + +For deployment from source, set the following environment variables: + +* `PP_HOST_NAME` +* `PP_HOST_ADDRESS` +* `PP_HOST_CONTACT` +* `PP_ADMIN_NAME` +* `PP_ADMIN_ADDRESS` +* `PP_ADMIN_CONTACT` + +For deployment using docker-compose, set these values in `.env`. + +If the host and administrator are the same, you can just set the `PP_HOST_*` variables above which will be used for both. + +##### Including a custom Privacy Policy and/or Terms & Conditions + +If the default privacy policy or terms & conditions do not cover your use case, you can easily replace these with your own documents. + +If deploying from source, include markdown (`.md`) files in a `custom-policies` directory in the project root with the exact names `custom-policies/privacy-policy.md` and/or `custom-policies/terms-and-conditions.md` which will be rendered at the corresponding pages on the running web app. If you are not familiar with the Markdown language there are a number of free WYSIWYG-style editor tools available including [StackEdit](https://stackedit.io/app) (browser based) and [Zettlr](https://www.zettlr.com) (desktop app). + +If deploying with docker compose, place the `custom-policies` directory at the same location as the `docker-compose.yml` file before running `./deploy.sh` as above. + +An example custom privacy policy file contents might look like: + +```md +# Organisation X Teamware Privacy Policy +... +... +## Definitions of Roles and Terminology +... +... +``` diff --git a/docs/versioned/2.1.1/developerguide/api_docs.md b/docs/versioned/2.1.1/developerguide/api_docs.md new file mode 100644 index 00000000..03964b37 --- /dev/null +++ b/docs/versioned/2.1.1/developerguide/api_docs.md @@ -0,0 +1,1086 @@ +--- +sidebarDepth: 3 +--- + +# API Documentation + +## Using the JSONRPC endpoints + +::: tip +A single endpoint is used for all API requests, located at `/rpc` +::: + +The API used in the app complies to JSON-RPC 2.0 spec. Requests should always be sent with `POST` and +contain a JSON request object in the body. The response will also be in the form of a JSON object. + +For example, to call the method `subtract(a, b)`. Send `POST` a post request to `/rpc` with the following JSON +in the body: + +```json +{ + "jsonrpc":"2.0", + "method":"subtract", + "params":[ + 42, + 23 + ], + "id":1 +} +``` + +Variables are passed as a list to the `params` field, in this case `a=42` and `b=23`. The `id` field in the top +level of the request object refers to the message ID, this ID value will be matched in the response, +it does not affect the method that is being called. + +The response will be as follows: + +```json +{ + "jsonrpc":"2.0", + "result":19, + "id":1 +} +``` + +In the case of errors, the response will contain an `error` field with error `code` and error `message`: + +```json +{ + "jsonrpc":"2.0", + "error":{ + "code":-32601, + "message":"Method not found" + }, + "id":"1" +} +``` + +The following are error codes used in the app: + +```python +PARSE_ERROR = -32700 +INVALID_REQUEST = -32600 +METHOD_NOT_FOUND = -32601 +INVALID_PARAMS = -32602 +INTERNAL_ERROR = -32603 +AUTHENTICATION_ERROR = -32000 +UNAUTHORIZED_ERROR = -32001 +``` + +## API Listing + + + +### initialise() + + +::: tip Description +Provide the initial context information to initialise the Teamware app + + context_object: + user: + isAuthenticated: bool + isManager: bool + isAdmin: bool + configs: + docFormatPref: bool + global_configs: + allowUserDelete: bool +::: + + + + + + + + +### is_authenticated() + + +::: tip Description +Checks that the current user has logged in. +::: + + + + + + + + +### login(payload) + + + + +#### Parameters + +* payload + + + + + + + +### logout() + + + + + + + + + +### register(payload) + + + + +#### Parameters + +* payload + + + + + + + +### generate_user_activation(username) + + + + +#### Parameters + +* username + + + + + + + +### activate_account(username,token) + + + + +#### Parameters + +* username + +* token + + + + + + + +### generate_password_reset(username) + + + + +#### Parameters + +* username + + + + + + + +### reset_password(username,token,new_password) + + + + +#### Parameters + +* username + +* token + +* new_password + + + + + + + +### change_password(payload) + + + + +#### Parameters + +* payload + + + + + + + +### change_email(payload) + + + + +#### Parameters + +* payload + + + + + + + +### set_user_receive_mail_notifications(do_receive_notifications) + + + + +#### Parameters + +* do_receive_notifications + + + + + + + +### set_user_document_format_preference(doc_preference) + + + + +#### Parameters + +* doc_preference + + + + + + + +### get_user_details() + + + + + + + + + +### get_user_annotated_projects() + + +::: tip Description +Gets a list of projects that the user has annotated +::: + + + + + + + + +### get_user_annotations_in_project(project_id,current_page,page_size) + + +::: tip Description +Gets a list of documents in a project where the user has performed annotations in. + :param project_id: The id of the project to query + :param current_page: A 1-indexed page count + :param page_size: The maximum number of items to return per query + :returns: Dictionary of items and total count after filter is applied {"items": [], "total_count": int} +::: + + + +#### Parameters + +* project_id + +* current_page + +* page_size + + + + + + + +### user_delete_personal_information() + + + + + + + + + +### user_delete_account() + + + + + + + + + +### create_project() + + + + + + + + + +### delete_project(project_id) + + + + +#### Parameters + +* project_id + + + + + + + +### update_project(project_dict) + + + + +#### Parameters + +* project_dict + + + + + + + +### get_project(project_id) + + + + +#### Parameters + +* project_id + + + + + + + +### clone_project(project_id) + + + + +#### Parameters + +* project_id + + + + + + + +### import_project_config(pk,project_dict) + + + + +#### Parameters + +* pk + +* project_dict + + + + + + + +### export_project_config(pk) + + + + +#### Parameters + +* pk + + + + + + + +### get_projects(current_page,page_size,filters) + + +::: tip Description +Gets the list of projects. Query result can be limited by using current_page and page_size and sorted + by using filters. + + :param current_page: A 1-indexed page count + :param page_size: The maximum number of items to return per query + :param filters: Filter option used to search project, currently only string is used to search + for project title + :returns: Dictionary of items and total count after filter is applied {"items": [], "total_count": int} +::: + + + +#### Parameters + +* current_page + +* page_size + +* filters + + + + + + + +### get_project_documents(project_id,current_page,page_size,filters) + + +::: tip Description +Gets the list of documents and its annotations. Query result can be limited by using current_page and page_size + and sorted by using filters + + :param project_id: The id of the project that the documents belong to, is a required variable + :param current_page: A 1-indexed page count + :param page_size: The maximum number of items to return per query + :param filters: Filter currently only searches for ID of documents + for project title + :returns: Dictionary of items and total count after filter is applied {"items": [], "total_count": int} +::: + + + +#### Parameters + +* project_id + +* current_page + +* page_size + +* filters + + + + + + + +### get_project_test_documents(project_id,current_page,page_size,filters) + + +::: tip Description +Gets the list of documents and its annotations. Query result can be limited by using current_page and page_size + and sorted by using filters + + :param project_id: The id of the project that the documents belong to, is a required variable + :param current_page: A 1-indexed page count + :param page_size: The maximum number of items to return per query + :param filters: Filter currently only searches for ID of documents + for project title + :returns: Dictionary of items and total count after filter is applied {"items": [], "total_count": int} +::: + + + +#### Parameters + +* project_id + +* current_page + +* page_size + +* filters + + + + + + + +### get_project_training_documents(project_id,current_page,page_size,filters) + + +::: tip Description +Gets the list of documents and its annotations. Query result can be limited by using current_page and page_size + and sorted by using filters + + :param project_id: The id of the project that the documents belong to, is a required variable + :param current_page: A 1-indexed page count + :param page_size: The maximum number of items to return per query + :param filters: Filter currently only searches for ID of documents + for project title + :returns: Dictionary of items and total count after filter is applied {"items": [], "total_count": int} +::: + + + +#### Parameters + +* project_id + +* current_page + +* page_size + +* filters + + + + + + + +### add_project_document(project_id,document_data) + + + + +#### Parameters + +* project_id + +* document_data + + + + + + + +### add_project_test_document(project_id,document_data) + + + + +#### Parameters + +* project_id + +* document_data + + + + + + + +### add_project_training_document(project_id,document_data) + + + + +#### Parameters + +* project_id + +* document_data + + + + + + + +### add_document_annotation(doc_id,annotation_data) + + + + +#### Parameters + +* doc_id + +* annotation_data + + + + + + + +### get_annotations(project_id) + + +::: tip Description +Serialize project annotations as GATENLP format JSON using the python-gatenlp interface. +::: + + + +#### Parameters + +* project_id + + + + + + + +### delete_documents_and_annotations(doc_id_ary,anno_id_ary) + + + + +#### Parameters + +* doc_id_ary + +* anno_id_ary + + + + + + + +### get_possible_annotators(proj_id) + + + + +#### Parameters + +* proj_id + + + + + + + +### get_project_annotators(proj_id) + + + + +#### Parameters + +* proj_id + + + + + + + +### add_project_annotator(proj_id,username) + + + + +#### Parameters + +* proj_id + +* username + + + + + + + +### make_project_annotator_active(proj_id,username) + + + + +#### Parameters + +* proj_id + +* username + + + + + + + +### project_annotator_allow_annotation(proj_id,username) + + + + +#### Parameters + +* proj_id + +* username + + + + + + + +### remove_project_annotator(proj_id,username) + + + + +#### Parameters + +* proj_id + +* username + + + + + + + +### reject_project_annotator(proj_id,username) + + + + +#### Parameters + +* proj_id + +* username + + + + + + + +### get_annotation_timings(proj_id) + + + + +#### Parameters + +* proj_id + + + + + + + +### delete_annotation_change_history(annotation_change_history_id) + + + + +#### Parameters + +* annotation_change_history_id + + + + + + + +### get_annotation_task() + + +::: tip Description +Gets the annotator's current task, returns a dictionary about the annotation task that contains all the information + needed to render the Annotate view. +::: + + + + + + + + +### get_annotation_task_with_id(annotation_id) + + +::: tip Description +Get annotation task dictionary for a specific annotation_id, must belong to the annotator (or is a manager or above) +::: + + + +#### Parameters + +* annotation_id + + + + + + + +### complete_annotation_task(annotation_id,annotation_data,elapsed_time) + + +::: tip Description +Complete the annotator's current task +::: + + + +#### Parameters + +* annotation_id + +* annotation_data + +* elapsed_time + + + + + + + +### reject_annotation_task(annotation_id) + + +::: tip Description +Reject the annotator's current task +::: + + + +#### Parameters + +* annotation_id + + + + + + + +### change_annotation(annotation_id,new_data) + + +::: tip Description +Adds annotation data to history +::: + + + +#### Parameters + +* annotation_id + +* new_data + + + + + + + +### get_document(document_id) + + +::: tip Description +Obsolete: to be deleted +::: + + + +#### Parameters + +* document_id + + + + + + + +### get_annotation(annotation_id) + + +::: tip Description +Obsolete: to be deleted +::: + + + +#### Parameters + +* annotation_id + + + + + + + +### annotator_leave_project() + + +::: tip Description +Allow annotator to leave their currently associated project. +::: + + + + + + + + +### get_all_users() + + + + + + + + + +### get_user(username) + + + + +#### Parameters + +* username + + + + + + + +### admin_update_user(user_dict) + + + + +#### Parameters + +* user_dict + + + + + + + +### admin_update_user_password(username,password) + + + + +#### Parameters + +* username + +* password + + + + + + + +### admin_delete_user_personal_information(username) + + + + +#### Parameters + +* username + + + + + + + +### admin_delete_user(username) + + + + +#### Parameters + +* username + + + + + + + +### get_privacy_policy_details() + + + + + + + + + +### get_endpoint_listing() + + + + + + + + + + + diff --git a/docs/versioned/2.1.1/developerguide/documentation.md b/docs/versioned/2.1.1/developerguide/documentation.md new file mode 100644 index 00000000..3de09904 --- /dev/null +++ b/docs/versioned/2.1.1/developerguide/documentation.md @@ -0,0 +1,61 @@ +# Managing and versioning documentation + +Documentation versioning is managed by the custom node script located at `docs/manage_versions.js`. Versions of the documentation can be archived and the entire documentation site can be built using the script. + +Various configuration parameters used for management of documentation versioning can be found in `docs/docs.config.js`. + +## Installing dependencies required to serve the documentation site + +The documentation uses vuepress and other libraries which has to be installed separately running the following command from the root of the project: + +```bash +npm run install:docs +``` + +## Editing the documentation + +The latest version of the documentation is located at `/docs/docs`. The archived (versioned) documentation are located in `/docs/versioned/version_number`. + +Use the following command to live preview the latest version of the documentation: + +``` +npm run serve:docs +``` + +Note that this will not work with other versioned docs as they are managed as a separate site. To live preview versioned documentation use the command (replace version_num with the version you'd like to preview): + +``` +vuepress dev docs/versioned/version_num +``` + +## Creating a new documentation version + +To create a version of the documentation, run the command: + +``` +npm run docs:create_version +``` + +This creates a copy of the current set of documentation in `/docs/docs` and places it at `/docs/versioned/version_num`. The version number in `package.json` is used for the documentation version. + +Each set of documentation can be considered as a separate vuepress site. Each one has a `.vuepress/versions.json` file that contains the listing of all versions, allowing them to link to each other. + +Note: Versions can also be created manually by running the command: + +``` +# Replace version_num with the version you'd like to create +node docs/manage_versions.js create version_num +``` + + +## Building documentation site + +To build the documentation site, the previous documentation build command is used: + +``` +npm run build:docs +``` + +## Implementation of the version selector UI + +A partial override of the default Vuepress theme was needed to add a custom component the navigation bar. The modified version of the `NavBar` component can be found in `/docs/docs/.vuepress/theme/components/NavBar.vue`. The modified NavBar uses the `VersionSelector` (`/docs/docs/.vuepress/theme/components/VersionSelector.vue`) component which reads from the `.vuepress/versions.json` from each set of documentation. diff --git a/docs/versioned/2.1.1/developerguide/frontend.md b/docs/versioned/2.1.1/developerguide/frontend.md new file mode 100644 index 00000000..ae2aeff0 --- /dev/null +++ b/docs/versioned/2.1.1/developerguide/frontend.md @@ -0,0 +1,146 @@ +# Frontend + +Web GUI of Teamware is built with [vue.js](https://vuejs.org) version 2.7.x. + +[Bootstrap](https://getbootstrap.com/) (and [Bootstrap vue](https://bootstrap-vue.org/)) provides the visual styling. + +[Vite.js](https://vitejs.dev/) is used to bundle Vue code and other javascript dependencies for deployment and serve as a frontend dev server (which runs alongside django dev server) while testing or debugging. + +## Getting started + +### Installation +``` +npm install +``` + +### Compiles and hot-reloads for development +``` +npm run serve +``` + +### Compiles and minifies for production +``` +npm run build +``` + +### Testing + +**Tools used for testing:** +* [vitest](https://vitest.dev) - Used for unit testing (code without UI components) +* [cypress](https://docs.cypress.io) - Used for tests that contains (Vue) UI components +* [Vue test utils](https://vue-test-utils.vuejs.org) - Used for rendering vue component allows it to be mounted for unit testing. Officially recommended by Vue.js. + +* Tests for the frontend are all located in `/frontend/tests` folder. + * Unit test files should all be placed in `/frontend/tests/unit/` folder and have an extension `.spec.js`. + * Component test files should all be placed in `/frontend/tests/component` folder and have an extension `.cy.js` +* Test fixtures (data used in running the tests) are placed in `/examples` folder, this folder is shared with the integration test + +To run all frontend tests (unit and component tests): + +``` +npm run test +``` + +To run unit tests only: + +``` +npm run test:unit +``` + +To run component test only: + +``` +npm run test:component +``` + +## Notes when coming from the previous version <=2.0.0 + +- The `@` alias can still be used when doing module imports but file extensions should now be used when importing `.vue` files e.g. + - Before: `import DeleteModal from "@/components/DeleteModal" + - Now: `import DeleteModal from "@/components/DeleteModal.vue"` +- For code that is intended to run on the browser, e.g. in all `.vue` files, imports should use the ES 6 compliant `import` command and not node/commonjs's `require` + - **Exceptions for code that is run directly by node**, e.g. scripts used in the build chain, config files and test files used by build tools that run on node (e.g. vuepress or cypress) + + +## Explantion of the frontend + +### Vue and Vite + +Instead of separating html, css and javascript files, Vue has its own `single-file component` format normally with `.vue` extension ([reason why this file format is used](https://vuejs.org/guide/scaling-up/sfc.html)). Here is an example `.vue` file: + +```vue + + + + + +``` + +This means that `.vue` files cannot be directly imported into a standard html page. A tool has to be used for converting `.vue` file into standard javascript and/or css files, this is where [Vite.js](https://vitejs.dev/) comes in. + +[Vite.js](https://vitejs.dev/) is a tool that, amongst many other things, provides a dev server allowing hot module replacement (ability to immediately see changes in the UI during development) and bundling of javascript modules and other resources (css, images, etc.) i.e. not having to individually import each javascript and their dependencies from the main page. A [Vue plugin](https://github.com/vitejs/vite-plugin-vue2) is used to automatically convert `.vue` files into plain javascript as part of the bundling process. + +### App entrypoint (main.js) and routing + +The application's main entrypoint is `/frontend/src/main.js` which loads dependencies like Vue, Bootstrap Vue as well as loading the main component `AnnotationApp.vue` into a html page that contains a `
` tag. + +The `AnnotationApp.vue` component contains the special `` tag ([vue router](https://router.vuejs.org/)) which allows us to map url paths to specific vue components. The routing configuration can be found in `/frontend/src/router/index.js`, for example: + +```js +const routes = [ + { + path: '/', + name: 'Home', + component: Home, + meta: {guest: true}, + }, +... +``` + +The route shown above maps the root path e.g. `https://your-deployed-teamware-domain.com/` to the `Home.vue` component. Specifically, when pointing your browser to that path, the `Home.vue` component is inserted inside ``. + +### index.html, templates and bundling + +A html page is required to place our application in. Teamware uses Django to serve up the main html page which is located at `/frontend/templates/index.html` (see `MainView` class in `/backend/views.py`). This `index.html` page has to know where to load the generated javascript files. Where these files are differ depending on whether you're running the vite development server or using vite's statically built files. + +#### Using vite's development server (Django's `settings.FRONTEND_DEV_SERVER_USE` is `True`) +In during development we expect to be running the vite dev server alongside django server (when running `npm run serve` from the root of the project). In this case `index.html` imports javascript directly from the vite dev server: + +```html + + +``` + +This applies when running the `base`, `test` and `integration` django configurations. + +#### Using vite's statically built assets (Django's `settings.FRONTEND_DEV_SERVER_USE` is `false`) +When deploying the application, vite converts `.vue` files into plain javascript and bundles them to `/frontend/dist/static` directory. The `/frontend/src/main.js` becomes `/frontend/dist/static/assets/main-bb58d055.js`. The scripts are imported as static asset of going through the vite server, for example: + +```html + + +``` + +This applies when running the `deployment`, `docker-test` and `docker-integration` django configurations. + +#### index.html generation + +You may have noticed that a hash is added to the generated asset files (e.g. `main-bb58d055.js`) and this hash changes every time Vite builds the code. This means the `index.html` must also be re-generated after every Vite build as well. + +A simple build script which runs after every vite build `/frontend/build_template.js` performs this generation by taking the base template `/frontend/base_index.html`, merging it with Vite's generated manifest `/frontend/dist/manifest.json` and the output with the correct import path to `/frontend/templates/index.html`. + diff --git a/docs/versioned/2.1.1/developerguide/releases.md b/docs/versioned/2.1.1/developerguide/releases.md new file mode 100644 index 00000000..8a08d12b --- /dev/null +++ b/docs/versioned/2.1.1/developerguide/releases.md @@ -0,0 +1,18 @@ +# Managing Releases + +*These instructions are primarily intended for the maintainers of Teamware.* + +Note: Releases are always made from the `master` branch of the repository. + +## Steps to making a release + +1. **Update the changelog** - This has to be done manually, go through any pull requests to `dev` since the last release. + - In github pull requests page, use the search term `is:pr merged:>=yyyy-mm-dd` to find all merged PR from the date since the last version change. + - Include the changes in the `CHANGELOG.md` file; the changelog section _MUST_ begin with a level-two heading that starts with the relevant version number in square brackets (`## [N.M.P] Optional descriptive suffix`) as the GitHub workflow that creates a release from the eventual tag depends on this pattern to find the right release notes. Each main item within the changelog should have a link to the originating PR e.g. \[#123\](https://github.com/GateNLP/gate-teamware/pull/123). +1. **Update and check the version numbers** - from the teamware directory run `python version.py check` to check whether all version numbers are up to date. If not, update the master `VERSION` file and run `python version.py update` to update all other version numbers and commit the result. Alternatively, run `python version.py update ` where `` is the version number to update to, e.g. `python version.py update 2.1.0`. Note that `version.py` requires `pyyaml` for reading `CITATION.cff`, `pyyaml` is included in Teamware's dependencies. +1. **Create a version of the documentation** - Run `npm run docs:create_version`, this will archive the current version of the documentation using the version number in `package.json`. +1. **Create a pull request from `dev` to `master`** including any changes to `CHANGELOG.md`, `VERSION`. +1. **Create a tag** - Once the dev-to-master pull request has been merged, create a tag from the resulting `master` branch named `vN.M.P` (i.e. the new version number prefixed with the letter `v`). This will trigger two GitHub workflows: + - one that builds versioned Docker images for this release and pushes them to `ghcr.io`, updating the `latest` image tag to point to the new release + - one that creates a "release" on GitHub with the necessary artifacts to make the `https://gate.ac.uk/get-teamware.sh` installation mechanism work correctly. The release notes for this release will be generated by extracting the matching section from `CHANGELOG.md`. +1. **Update the Helm chart** - Create a new branch on [https://github.com/GateNLP/charts](https://github.com/GateNLP/charts) to update the `appVersion` of the `gate-teamware` Helm chart to match the version that was just created by the tag workflow. You must also update the chart `version`, bumping the major version number if the new chart is not backwards-compatible with the old. Submit a pull request to the `main` branch, which will publish the new chart when it is merged. diff --git a/docs/versioned/2.1.1/developerguide/testing.md b/docs/versioned/2.1.1/developerguide/testing.md new file mode 100644 index 00000000..578501b2 --- /dev/null +++ b/docs/versioned/2.1.1/developerguide/testing.md @@ -0,0 +1,182 @@ +# Testing +All the tests can be run using the following command: + +```bash +npm run test +``` + +## Backend Testing +Pytest is used for testing the backend. + +```bash +npm run test:backend +``` + +### Backend test files + +* Unit test files are located in `/backend/tests` + +## Frontend testing +[Jest](https://jestjs.io/) is used for frontend testing. +The [Vue testing-library](https://testing-library.com/docs/vue-testing-library/intro/) is used for testing +Vue components. + +```bash +npm run test:frontend +``` + +### Frontend test files + +* Frontend test files are located in `/fontend/tests/unit` and should the extension `.spec.js` + +### Testing JS functions + +```javascript +describe("Description of a group of tests to be run", () =>{ + + beforeAll(() =>{ + //The code here is run before each test + }) + + it("A single test's description", async () =>{ + + // Assertions are done with the expect() function e.g. + let funcOutput = 30 + 10 + expect(funcOutput).toBe(40) + + + }) +}) + +``` + +### Mocking JS classes + +This is an example of a mock harness for the JRPCClient class. + +A mock file is created inside a ``__mock__`` directory placed next to the file that's being mocked, e.g. +for our JRPCClient class at `/frontend/src/jrpc/index.js`, the mock file is `/frontend/src/jrpc/__mock__/index.js`. + + +Inside the mock file `/frontend/src/jrpc/__mock__/index.js`: +```javascript +// Mocking jrpc/index.js +//Mocking the JRPCClient class +//Replacing the call function with a custom mockCall function +export const mockCall = jest.fn(()=> 30); +const mock = jest.fn().mockImplementation(() => { + return {call: mockCall}; +}); + +export default mock; +``` + + +Inside the test file `*.spec.js`: +```javascript +import JRPCClient from "@/jrpc"; +jest.mock('@/jrpc') + +import store from '@/store' +//Example on how to mock the jrpc call + +describe("Vuex functions testing", () =>{ + + beforeAll(() =>{ + + //Re-implement custom mock call implementation if needed + JRPCClient.mockImplementation(()=>{ + return { + call(){ + return 50 + } + } + }) + + }) + + it("testfunc", async () =>{ + + const noutput = await store.dispatch("testnormal") + expect(noutput).toBe("Hello world") + + const aoutput = await store.dispatch("testasync") + expect(aoutput).toBe("Hello world") + + const rpc = new JRPCClient("/") + const result = await rpc.call("some param") + expect(result).toBe(50) + + }) +}) +``` + +### Testing Vue components + + +```javascript +//Example of how a component could be tested +import { render, fireEvent } from '@testing-library/vue' + + +import HelloWorld from '@/components/HelloWorld.vue' + +//Testing a component e.g. HelloWorld +describe('HelloWorld.vue', () => { + + it('renders props.msg when passed', () => { + const msg = 'new message' + const { getByText } = render(HelloWorld) + + getByText("Installed CLI Plugins") + }) +}) + +``` + + +## Integration testing +[Cypress](https://www.cypress.io/) is used for integration testing. + +The integration settings are located at `teamware/settings/integration.py` + +To run the integration test: +```bash +npm run test:integration +``` + +The test can also be run in **interactive mode** using: + +```bash +npm run serve:cypressintegration +``` + +### Integration test files +Files related to integration testing are located in `/cypress` + +* Test files are located in the `/cypress/integration` directory and should have the extension `.spec.js`. + +### Re-seeding the database + +The command `npm run migrate:integration` resets the database and performs migration, use with `beforeEach` to run it +before every test case in a suite: + +```js +describe('Example test suite', () => { + + beforeEach(() => { + // Resets the database every time before + // the test is run + cy.exec('npm run migrate:integration') + }) + + it('Test case 1', () => { + // Test something + }) + + it('Test case 2', () => { + // Test something + }) +}) +``` + diff --git a/docs/versioned/2.1.1/img/gate-teamware-logo.svg b/docs/versioned/2.1.1/img/gate-teamware-logo.svg new file mode 100644 index 00000000..12385947 --- /dev/null +++ b/docs/versioned/2.1.1/img/gate-teamware-logo.svg @@ -0,0 +1,79 @@ + + + + diff --git a/docs/versioned/2.1.1/manageradminguide/README.md b/docs/versioned/2.1.1/manageradminguide/README.md new file mode 100644 index 00000000..7a70a19f --- /dev/null +++ b/docs/versioned/2.1.1/manageradminguide/README.md @@ -0,0 +1,45 @@ +# GATE Teamware Overview + +## User roles + +There are three types of users in GATE Teamware, [annotators](#annotators), [managers](#managers) +and [admins](#admins). + +### Annotators + +Annotator is the default role when signing up to Teamware. An annotator can be recruited into +annotation projects and annotate documents. + + +### Managers + +Managers can create, view and modify annotation projects. They can also recruit annotators to a project. + +### Admins + +Admins, on top of what managers can do, they can also manage the users in the system and elevate them as +managers or admins. + +## Annotation Projects, Documents and Annotations + +Projects, documents and annotations form the core of the application. + +### Projects + +An annotation project contains a configuration of how annotations are to be captured, the documents and its +annotations and the recruited annotators. + + +### Documents + +A document in application refers to an individual set of arbitrary text that's to be annotated. A document +is stored as arbitrary JSON object and can represent various things such as, a single post (e.g. a tweet +or a post from reddit), a pair of source post and reply or a part of a HTML web page. + + +### Annotations + +An annotation represents a single annotation task against a single document. Like the document, +an annotation is stored as an arbitrary JSON object and can have any arbitrary structure. + + diff --git a/docs/versioned/2.1.1/manageradminguide/annotators_management.md b/docs/versioned/2.1.1/manageradminguide/annotators_management.md new file mode 100644 index 00000000..cb4c79b0 --- /dev/null +++ b/docs/versioned/2.1.1/manageradminguide/annotators_management.md @@ -0,0 +1,13 @@ +# Annotators management + +The **Annotators** tab in the **Project management** page allows the viewing and management of annotators in the project. + +Add annotators to the project by clicking on the list of names in the right column. Current annotators +can be removed by clicking on the names in the left column. Removing annotators does not delete their +completed annotations but will stop their current pending annotation task. + +An annotator can only be recruited into **one project at a time**. + +Once an annotator has annotated a proportion of documents in the project (specified in project configuration), they will +be deemed to have completed all their annotation tasks and automatically be removed the project. This frees them to be +recruited in another project. diff --git a/docs/versioned/2.1.1/manageradminguide/config_examples.js b/docs/versioned/2.1.1/manageradminguide/config_examples.js new file mode 100644 index 00000000..d6e40454 --- /dev/null +++ b/docs/versioned/2.1.1/manageradminguide/config_examples.js @@ -0,0 +1,332 @@ +export default { + config1: [ + { + "name": "htmldisplay", + "type": "html", + "text": "{{{text}}}" + }, + { + "name": "sentiment", + "type": "radio", + "title": "Sentiment", + "description": "Please select a sentiment of the text above.", + "options": [ + {"value": "negative", "label": "Negative"}, + {"value": "neutral", "label": "Neutral"}, + {"value": "positive", "label": "Positive"} + ] + } + ], + config2: [ + { + "name": "htmldisplay", + "type": "html", + "text": "{{{text}}}" + }, + { + "name": "sentiment", + "type": "radio", + "title": "Sentiment", + "description": "Please select a sentiment of the text above.", + "options": [ + {"value": "negative", "label": "Negative"}, + {"value": "neutral", "label": "Neutral"}, + {"value": "positive", "label": "Positive"} + ] + }, + { + "name": "opinion", + "type": "text", + "title": "What's your opinion of the above text?", + "optional": true + } + ], + configDisplay: [ + { + "name": "htmldisplay", + "type": "html", + "text": "{{{text}}}" + } + ], + configDisplayHtmlNoHtml: [ + { + "name": "htmldisplay", + "type": "html", + "text": "No HTML: {{text}}
HTML: {{{text}}}" + } + ], + configDisplayCustomFieldnames: [ + { + "name": "htmldisplay", + "type": "html", + "text": "Custom field: {{customField}}
Another custom field: {{{anotherCustomField}}}
Subfield: {{{subfield.subfieldContent}}}" + } + ], + configDisplayPreserveNewlines: [ + { + "name": "htmldisplay", + "type": "html", + "text": "
{{text}}
" + } + ], + configTextInput: [ + { + "name": "mylabel", + "type": "text", + "optional": true, //Optional - Set if validation is not required + "regex": "regex string", //Optional - When specified, the regex pattern will used to validate the text + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message then field is validated", //Optional + "valError": "Error message when field fails is validation" //Optional + } + ], + configTextarea: [ + { + "name": "mylabel", + "type": "textarea", + "optional": true, //Optional - Set if validation is not required + "regex": "regex string", //Optional - When specified, the regex pattern will used to validate the text + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message then field is validated", //Optional + "valError": "Error message when field fails is validation" //Optional + } + ], + configRadio: [ + { + "name": "mylabel", + "type": "radio", + "optional": true, //Optional - Set if validation is not required + "orientation": "vertical", //Optional - default is "horizontal" + "options": [ // The options that the user is able to select from + {"value": "value1", "label": "Text to show user 1"}, + {"value": "value2", "label": "Text to show user 2"}, + {"value": "value3", "label": "Text to show user 3"} + ], + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message then field is validated", //Optional + "valError": "Error message when field fails is validation" //Optional + } + ], + configRadioHelpText: [ + { + "name": "mylabel", + "type": "radio", + "optional": true, //Optional - Set if validation is not required + "orientation": "vertical", //Optional - default is "horizontal" + "options": [ // The options that the user is able to select from + {"value": "value1", "label": "Text to show user 1", "helptext": "Additional help text for option 1"}, + {"value": "value2", "label": "Text to show user 2", "helptext": "Additional help text for option 2"}, + {"value": "value3", "label": "Text to show user 3"} + ], + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message when the field is validated", //Optional + "valError": "Error message when the field fails validation" //Optional + } + ], + configCheckbox: [ + { + "name": "mylabel", + "type": "checkbox", + "optional": true, //Optional - Set if validation is not required + "orientation": "horizontal", //Optional - "horizontal" (default) or "vertical" + "options": [ // The options that the user is able to select from + {"value": "value1", "label": "Text to show user 1"}, + {"value": "value2", "label": "Text to show user 2"}, + {"value": "value3", "label": "Text to show user 3"} + ], + "minSelected": 1, //Optional - Specify the minimum number of options that must be selected + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message then field is validated", //Optional + "valError": "Error message when field fails is validation" //Optional + } + ], + configSelector: [ + { + "name": "mylabel", + "type": "selector", + "optional": true, //Optional - Set if validation is not required + "options": [ // The options that the user is able to select from + {"value": "value1", "label": "Text to show user 1"}, + {"value": "value2", "label": "Text to show user 2"}, + {"value": "value3", "label": "Text to show user 3"} + ], + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message then field is validated", //Optional + "valError": "Error message when field fails is validation" //Optional + } + ], + configRadioDict: [ + { + "name": "mylabel", + "type": "radio", + "optional": true, //Optional - Set if validation is not required + "options": { // The options can be specified as a dictionary, ordering is not guaranteed + "value1": "Text to show user 1", + "value2": "Text to show user 2", + "value3": "Text to show user 3", + }, + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message then field is validated", //Optional + "valError": "Error message when field fails is validation" //Optional + } + ], + + configDbpediaExample: [ + { + "name": "uri", + "type": "radio", + "title": "Select the most appropriate URI", + "options":[ + {"fromDocument": "candidates"}, + {"value": "none", "label": "None of the above"}, + {"value": "unknown", "label": "Cannot be determined without more context"} + ] + } + ], + docDbpediaExample: { + "text": "President Bush visited the air base yesterday...", + "candidates": [ + { + "value": "http://dbpedia.org/resource/George_W._Bush", + "label": "George W. Bush (Jnr)" + }, + { + "value": "http://dbpedia.org/resource/George_H._W._Bush", + "label": "George H. W. Bush (Snr)" + } + ] + }, + + configConditional1: [ + { + "name": "uri", + "type": "radio", + "title": "Select the most appropriate URI", + "options":[ + {"fromDocument": "candidates"}, + {"value": "other", "label": "Other"} + ] + }, + { + "name": "otherValue", + "type": "text", + "title": "Please specify another value", + "if": "annotation.uri == 'other'", + "regex": "^(https?|urn):", + "valError": "Please specify a URI (starting http:, https: or urn:)" + } + ], + configConditional2: [ + { + "name": "htmldisplay", + "type": "html", + "text": "{{{text}}}" + }, + { + "name": "sentiment", + "type": "radio", + "title": "Sentiment", + "description": "Please select a sentiment of the text above.", + "options": [ + {"value": "negative", "label": "Negative"}, + {"value": "neutral", "label": "Neutral"}, + {"value": "positive", "label": "Positive"} + ] + }, + { + "name": "reason", + "type": "text", + "title": "Why do you disagree with the suggested value?", + "if": "annotation.sentiment !== document.preanno.sentiment" + } + ], + docsConditional2: [ + { + "text": "I love the thing!", + "preanno": { + "sentiment": "positive" + } + }, + { + "text": "I hate the thing!", + "preanno": { + "sentiment": "negative" + } + }, + { + "text": "The thing is ok, I guess...", + "preanno": { + "sentiment": "neutral" + } + } + ], + + + doc1: {text: "Sometext with html"}, + doc2: { + customField: "Content of custom field.", + anotherCustomField: "Content of another custom field.", + subfield: { + subfieldContent: "Content of a subfield." + } + }, + docPlainText: { + "text": "This is some text\n\nIt has line breaks that we want to preserve." + }, + configPreAnnotation: [ + { + "name": "htmldisplay", + "type": "html", + "text": "{{{text}}}" + }, + { + "name": "radio", + "type": "radio", + "title": "Test radio input", + "options": [ + {"value": "val1", "label": "Value 1"}, + {"value": "val2", "label": "Value 2"}, + {"value": "val3", "label": "Value 4"}, + {"value": "val4", "label": "Value 5"} + ], + "description": "Test radio description" + }, + { + "name": "checkbox", + "type": "checkbox", + "title": "Test checkbox input", + "options": [ + {"value": "val1", "label": "Value 1"}, + {"value": "val2", "label": "Value 2"}, + {"value": "val3", "label": "Value 4"}, + {"value": "val4", "label": "Value 5"} + ], + "description": "Test checkbox description" + }, + { + "name": "text", + "type": "text", + "title": "Test text input", + "description": "Test text description" + } + + ], + docPreAnnotation: { + "id": 12345, + "text": "Example document text", + "preannotation": { + "radio": "val1", + "checkbox": ["val1", "val3"], + "text": "Pre-annotation text value" + } + } + + +} diff --git a/docs/versioned/2.1.1/manageradminguide/documents_annotations_management.md b/docs/versioned/2.1.1/manageradminguide/documents_annotations_management.md new file mode 100644 index 00000000..ac010c9f --- /dev/null +++ b/docs/versioned/2.1.1/manageradminguide/documents_annotations_management.md @@ -0,0 +1,272 @@ +# Documents & Annotations + +The **Documents & Annotations** tab in the **Project management** page allows the viewing and management of documents +and annotations related to the project. + +## Document & Annotation status + +### Annotation status + +Annotations can be in 1 of 5 states: + +* Annotation is completed - The annotator has completed this annotation task. +* Annotation is rejected - The annotator has chosen to not annotate the document. +* Annotation is timed out - The annotation task was not completed within the time specified in the project's configuration. The task is freed and can be assigned to another annotator. +* Annotation is aborted - The annotation task was aborted due to reasons other than timing out, such as when an annotator with a pending task is removed from a project. +* Annotation is pending - The annotator has started the annotation task but has not completed it. + +### Document status + +Documents also display a list of its current annotation status: + +* 1 - Number of completed annotations in the document. +* 1 - Number of rejected annotations in the document. +* 1 - Number of timed out annotations in the document. +* 1 - Number of aborted annotations in the document. +* 1 - Number of pending annotations in the document. + +## Importing documents + +Documents can be imported using the **Import** button. The supported file types are: + +* `.json` - The app expects a list of documents (represented as a dictionary object) + e.g. `[{"id": 1, "text": "Text1"}, ...]`. +* `.jsonl` - The app expects one document (represented as a dictionary object) per line. +* `.csv` - File must have a header row. It will be internally converted to JSON format. +* `.zip` - Can contain any number of `.json,.jsonl and .csv` files inside. + +### Importing documents with pre-annotation + +In the `Project Configurations` page, it is possible to set a field in which Teamware will look for pre-annotation. If +the field is found inside the document then the annotation form will be pre-filled with data provided in the document. + +The format for pre-annotation is exactly the same as the annotation output. You can see an example of generated +annotation by filling out the form in the `Annotation Preview` and observing the values in +the `Annotation Output Preview`. + + +For an example project configuration shown below, there are three captured labels named `radio`, `checkbox` and `text`: + +```json +[ + { + "name": "htmldisplay", + "type": "html", + "text": "{{{text}}}" + }, + { + "name": "radio", + "type": "radio", + "title": "Test radio input", + "options": [ + {"value": "val1", "label": "Value 1"}, + {"value": "val2", "label": "Value 2"}, + {"value": "val3", "label": "Value 4"}, + {"value": "val4", "label": "Value 5"} + ], + "description": "Test radio description" + }, + { + "name": "checkbox", + "type": "checkbox", + "title": "Test checkbox input", + "options": [ + {"value": "val1", "label": "Value 1"}, + {"value": "val2", "label": "Value 2"}, + {"value": "val3", "label": "Value 4"}, + {"value": "val4", "label": "Value 5"} + ], + "description": "Test checkbox description" + }, + { + "name": "text", + "type": "text", + "title": "Test text input", + "description": "Test text description" + } +] +``` + +On the `Project Configuration` page, if the `Pre-annotation` field is set to `preannotation`, the annotation form will be pre-filled with +the content provided in the `preannotation` field of the document e.g.: + +```json +{ + "id": 12345, + "text": "Example document text", + "preannotation": { + "radio": "val1", + "checkbox": [ + "val1", + "val3" + ], + "text": "Pre-annotation text value" + } +} +``` + +The example of the pre-filled form can be seen by clicking on the `Preview` tab above. + + + + + + +### Importing Training and Test documents + +When importing documents for the training and testing phase, Teamware expects a field/column (called `gold` by default) +that contains the correct annotation response for each label and, only for training documents, an explanation. + +For example, if we're expecting a multi-choice label for doing sentiment classification with a widget named `sentiment` +and choice of `postive`, `negative` and `neutrual`: + +```js +[ + { + "text": "What's my sentiment", + "gold": { + "sentiment": { + "value": "positive", // For this document, the correct value is postive + "explanation": "Because..." // Explanation is only given in the traiing phase and are optional in the test documents + } + } + } +] +``` + +in csv: + +| text | gold.sentiment.value | gold.sentiment.explanation | +| --- | --- | --- | +| What's my sentiment | positive | Because... | + +### Guidance on CSV column headings + +It is recommended that: + +* Spaces are not used in column headings, use dash (`-`), underscore (`_`) or camel case (e.g. fieldName) instead. +* The dot/full stop (`.`) is used to indicate hierarchical information so don't use it if that's not what's intended. + Explanation on this feature is given below. + +Documents imported from a CSV files are converted to JSON for use internally in Teamware, the reverse is true when +converting back to CSV. To allow a CSV to represent a hierarchical structure, a dot notation is used to indicate a +sub-field. + +In the following example, we can see that `gold` has a child field named `sentiment` which then has a child field +named `value`: + +| text | gold.sentiment.value | gold.sentiment.explanation | +| --- | --- | --- | +| What's my sentiment | positive | Because... | + +The above column headers will generate the following JSON: + +```js +[ + { + "text": "What's my sentiment", + "gold": { + "sentiment": { + "value": "positive", // For this document, the correct value is postive + "explanation": "Because..." // Explanation is only given in the traiing phase and are optional in the test documents + } + } + } +] +``` + +## Exporting documents + +Documents and annotations can be exported using the **Export** button. A zip file is generated containing files with 500 +documents each. You can choose how documents are exported: + +* `.json` & `.jsonl` - JSON or JSON Lines files can be generated in the format of: + * `raw` - Exports unmodified JSON. If you've originally uploaded in GATE format then choose this option. + + An additional field named `annotation_sets` is added for storing annotations. The annotations are laid out in the + same way as GATE JSON format. For example if a document has been annotated by `user1` with labels and values + `text`:`Annotation text`, `radio`:`val3`, and `checkbox`:`["val2", "val4"]`: + + ```json + { + "id": 32, + "text": "Document text", + "text2": "Document text 2", + "feature1": "Feature text", + "annotation_sets":{ + "user1":{ + "name":"user1", + "annotations":[ + { + "type":"Document", + "start":0, + "end":10, + "id":0, + "features":{ + "label":{ + "text":"Annotation text", + "radio":"val3", + "checkbox":[ + "val2", + "val4" + ] + } + } + } + ], + "next_annid":1 + } + } + } + ``` + + * `gate` - Convert documents to GATE JSON format and export. A `name` field is added that takes the ID value from the + ID field specified in the project configuration. Fields apart from `text` and the ID field specified in the project + config are placed in the `features` field. An `annotation_sets` field is added for storing annotations. + + For example in the case of this uploaded JSON document: + ```json + { + "id": 32, + "text": "Document text", + "text2": "Document text 2", + "feature1": "Feature text" + } + ``` + The generated output is as follows. The annotations are formatted same as the `raw` output above: + ```json + { + "name": 32, + "text": "Document text", + "features": { + "text2": "Document text 2", + "feature1": "Feature text" + }, + "offset_type":"p", + "annotation_sets": {...} + } + ``` +* `.csv` - The JSON documents will be flattened to csv's column based format. Annotations are added as additional + columns with the header of `annotations.username.label`. + +## Deleting documents and annotations + +It is possible to click on the top left of corner of documents and annotations to select it, then click on the +**Delete** button to delete them. + +::: tip + +Selecting a document also selects all its associated annotations. + +::: + + + diff --git a/docs/versioned/2.1.1/manageradminguide/project_config.md b/docs/versioned/2.1.1/manageradminguide/project_config.md new file mode 100644 index 00000000..d3d4ea3d --- /dev/null +++ b/docs/versioned/2.1.1/manageradminguide/project_config.md @@ -0,0 +1,684 @@ +--- +sidebarDepth: 3 +--- + +# Project configuration + +The **Configuration** tab in the **Project management** page allows you to change project settings including what +annotations are captured. + +Project configurations can be imported and exported in the format of a JSON file. + +The project can be also be cloned (have configurations copied to a new project). Note that cloning does not copy +documents, annotations or annotators to the new project. + +## Configuration fields + +* **Name** - The name of this annotation project. +* **Description** - The description of this annotation project that will be shown to annotators. Supports markdown and + HTML. +* **Annotator guideline** - The description of this annotation project that will be shown to annotators. Supports + markdown and HTML. +* **Annotations per document** - The project completes when each document in this annotation project have this many + number of valid annotations. When a project completes, all project annotators will be un-recruited and be allowed to + annotate other projects. +* **Maximum proportion of documents annotated per annotator (between 0 and 1)** - A single annotator cannot annotate + more than this proportion of documents. +* **Timeout for pending annotation tasks (minutes)** - Specify the number of minutes a user has to complete an + annotation task (i.e. annotating a single document). +* **Reject documents** - Switching this off will mean that annotators for this project will be unable to choose to reject documents. +* **Document ID field** - The field in your uploaded documents that is used as a unique identifier. GATE's json format + uses the name field. You can use a dot limited key path to access subfields e.g. enter features.name to get the id + from the object `{'features':{'name':'nameValue'}}` +* **Training stage enable/disable** - Enable or disable training stage, allows testing documents to be uploaded to the project. +* **Test stage enable/disable** - Enable or disable testing stage, allows test documents to be uploaded to the project. +* **Auto elevate to annotator** - The option works in combination with the training and test stage options, see table below for the behaviour: + + | Training stage | Testing stage | Auto elevate to annotator | Desciption | + | --- | --- | --- | --- | + | Disabled | Disabled | Enabled/Disabled | User allowed to annotate without manual approval. | + | Enabled | Disabled | Disabled | Manual approval required. | + | Disabled | Enabled | Disabled | " | + | Enabled | Disabled | Enabled | User always allowed to annotate after training phase completed | + | Disabled | Enabled | Enabled | User automatically allowed to annotate after passing test, if user fails test they have to be manually approved. | + | Enabled | Enabled | Enabled | " | + +* **Test pass proportion** - The proportion of correct test annotations to be automatically allowed to annotate documents. +* **Gold standard field** - The field in document's JSON/column that contains the ideal annotation values and explanation for the annotation. +* **Pre-annotation** - Pre-fill the form with annotation provided in the specified field. See [Importing Documents with pre-annotation](./documents_annotations_management.md#importing-documents-with-pre-annotation) section for more detail. + +## Annotation configuration + +The annotation configuration takes a `json` string for configuring how the document is displayed to the user and types +of annotation will be collected. Here's an example configuration and a preview of how it is shown to annotators: + + + + +```json +// Example configuration +[ + { + "name": "htmldisplay", + "type": "html", + "text": "{{{text}}}" + }, + { + "name": "sentiment", + "type": "radio", + "title": "Sentiment", + "description": "Please select a sentiment of the text above.", + "options": [ + {"value": "negative", "label": "Negative"}, + {"value": "neutral", "label": "Neutral"}, + {"value": "positive", "label": "Positive"} + ] + } +] +``` + + + +Within the configuration, it is possible to specify how your documents will be displayed. The **Document input preview** +box can be used to provide a sample of your document for rendering of the preview. + +```json +// Example contents for the Document input preview +{ + "text": "Sometext with html" +} +``` + + + +The above configuration displays the value from the `text` field from the document to be annotated. It then shows a set +of 3 radio inputs that allows the user to select a Negative, Neutral, or Positive sentiment with the label +name `sentiment`. + + + +All fields **require** the properties **name** and **type**, it is used to name our label and determine the type of +input/display to be shown to the user respectively. + +Another field can be added to collect more information, e.g. a text field for opinions: + + + +```json +[ + { + "name": "htmldisplay", + "type": "html", + "text": "{{{text}}}" + }, + { + "name": "sentiment", + "type": "radio", + "title": "Sentiment", + "description": "Please select a sentiment of the text above.", + "options": [ + {"value": "negative", "label": "Negative"}, + {"value": "neutral", "label": "Neutral"}, + {"value": "positive", "label": "Positive"} + ] + }, + { + "name": "opinion", + "type": "text", + "title": "What's your opinion of the above text?", + "optional": true + } + +] +``` + + + +Note that for the above case, the `optional` field is added ensure that allows user to not have to input any value. +This `optional` field can be used on all components. Any component may optionally have a field named `if`, containing an expression that is used to determine whether or not the component appears based on information in the document and/or the values entered in the other components. For example the user could be presented with a set of options that includes an "other" choice, and if the annotator chooses "other" then an additional free text field appears for them to fill in. The `if` option is described in more detail under the [conditional components](#conditional-components) section below. + +Some fields are available to configure which are specific to components, e.g. the `options` field are only available for +the `radio`, `checkbox` and `selector` components. See details below on the usage of each specific component. + +The captured annotation results in a JSON dictionary, an example can be seen in the **Annotation output preview** box. +The annotation is linked to a Document and is converted to a GATE JSON annotation format when exported. + +### Displaying text + + + +```json +[ + { + "name": "htmldisplay", + "type": "html", + "text": "{{{text}}}" // The text that will be displayed + } +] +``` + + + +The `htmldisplay` widget allows you to display the text you want annotated. It accepts almost full range of HTML +input which gives full styling flexibility. + +Any field/column from the document can be inserted by surrounding a field/column name with double or +triple curly brackets. Double curly brackets renders text as-is and triple curly brackets accepts HTML string: + + + +Input: + +```json +{ + "text": "Sometext with html" +} +``` + +Configuration, showing the same field/column in document as-is or as HTML: +```json +[ + { + "name": "htmldisplay", + "type": "html", + "text": "No HTML: {{text}}
HTML: {{{text}}}" + } +] +``` + +
+ +The widget makes no assumption about your document structure and any field/column names can be used, +even sub-fields by using the dot notation e.g. `parentField.childField`: + + + +JSON input: + +```json +{ + "customField": "Content of custom field.", + "anotherCustomField": "Content of another custom field.", + "subfield": { + "subfieldContent": "Content of a subfield." + } +} +``` + +or in csv + +| customField | anotherCustomField | subfield.subfieldContent | +| --- | --- | --- | +| Content of custom field. | Content of another custom field. | Content of a subfield. | + + +Configuration, showing the same field/column in document as-is or as HTML: +```json +[ + { + "name": "htmldisplay", + "type": "html", + "text": "Custom field: {{customField}}
Another custom field: {{{anotherCustomField}}}
Subfield: {{{subfield.subfieldContent}}}" + } +] +``` + +
+ +If your documents are plain text and include line breaks that need to be preserved when rendering, this can be achieved by using a special HTML wrapper which sets the [`white-space` CSS property](https://developer.mozilla.org/en-US/docs/Web/CSS/white-space). + + + +**Document** + +```json +{ + "text": "This is some text\n\nIt has line breaks that we want to preserve." +} +``` + +**Project configuration** + +```json +[ + { + "name": "htmldisplay", + "type": "html", + "text": "
{{text}}
" + } +] +``` + +
+ +`white-space: pre-line` preserves line breaks but collapses other whitespace down to a single space, `white-space: pre-wrap` would preserve all whitespace including indentation at the start of a line, but would still wrap lines that are too long for the available space. + +### Text input + + + +```json +[ + { + "name": "mylabel", + "type": "text", + "optional": true, //Optional - Set if validation is not required + "regex": "regex string", //Optional - When specified, the regex pattern will used to validate the text + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message when the field is validated", //Optional + "valError": "Error message when the field fails validation" //Optional + } +] +``` + + + +### Textarea input + + + +```json +[ + { + "name": "mylabel", + "type": "textarea", + "optional": true, //Optional - Set if validation is not required + "regex": "regex string", //Optional - When specified, the regex pattern will used to validate the text + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message when the field is validated", //Optional + "valError": "Error message when the field fails validation" //Optional + } +] +``` + + + +### Radio input + + + +```json +[ + { + "name": "mylabel", + "type": "radio", + "optional": true, //Optional - Set if validation is not required + "orientation": "vertical", //Optional - default is "horizontal" + "options": [ // The options that the user is able to select from + {"value": "value1", "label": "Text to show user 1"}, + {"value": "value2", "label": "Text to show user 2"}, + {"value": "value3", "label": "Text to show user 3"} + ], + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message when the field is validated", //Optional + "valError": "Error message when the field fails validation" //Optional + } +] +``` + + + +### Checkbox input + + + +```json +[ + { + "name": "mylabel", + "type": "checkbox", + "optional": true, //Optional - Set if validation is not required + "orientation": "horizontal", //Optional - "horizontal" (default) or "vertical" + "options": [ // The options that the user is able to select from + {"value": "value1", "label": "Text to show user 1"}, + {"value": "value2", "label": "Text to show user 2"}, + {"value": "value3", "label": "Text to show user 3"} + ], + "minSelected": 1, //Optional - Overrides optional field. Specify the minimum number of options that must be selected + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message when the field is validated", //Optional + "valError": "Error message when the field fails validation" //Optional + } +] +``` + + + +### Selector input + + + +```json +[ + { + "name": "mylabel", + "type": "selector", + "optional": true, //Optional - Set if validation is not required + "options": [ // The options that the user is able to select from + {"value": "value1", "label": "Text to show user 1"}, + {"value": "value2", "label": "Text to show user 2"}, + {"value": "value3", "label": "Text to show user 3"} + ], + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message when the field is validated", //Optional + "valError": "Error message when the field fails validation" //Optional + } +] +``` + + + +### Optional help text + +Optionally, radio buttons and checkboxes can be given help text to provide additional per-choice context or information to help annotators. + + + + +```json +[ + { + "name": "mylabel", + "type": "radio", + "optional": true, //Optional - Set if validation is not required + "orientation": "vertical", //Optional - default is "horizontal" + "options": [ // The options that the user is able to select from + {"value": "value1", "label": "Text to show user 1", "helptext": "Additional help text for option 1"}, + {"value": "value2", "label": "Text to show user 2", "helptext": "Additional help text for option 2"}, + {"value": "value3", "label": "Text to show user 3"} + ], + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message when the field is validated", //Optional + "valError": "Error message when the field fails validation" //Optional + } +] +``` + + + +### Alternative way to provide options for radio, checkbox and selector + +A dictionary (key value pairs) and also be provided to the `options` field of the radio, checkbox and selector widgets +but note that the ordering of the options are **not guaranteed** as javascript does not sort dictionaries by +the order in which keys are added. Note that additional help texts for radio buttons and checkboxes are not supported using this syntax. + + + +```json +[ + { + "name": "mylabel", + "type": "radio", + "optional": true, //Optional - Set if validation is not required + "options": { // The options can be specified as a dictionary, ordering is not guaranteed + "value1": "Text to show user 1", + "value2": "Text to show user 2", + "value3": "Text to show user 3" + }, + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message when the field is validated", //Optional + "valError": "Error message when the field fails validation" //Optional + } +] +``` + + + +### Dynamic options for radio, checkbox and selector + +All the examples above have a "static" list of available options for the radio, checkbox and selector widgets, where the complete options list is enumerated in the project configuration and every document offers the same set of options. However it is also possible to take some or all of the options from the _document_ data rather than the _configuration_ data. For example: + + + +**Project configuration** + +```json +[ + { + "name": "uri", + "type": "radio", + "title": "Select the most appropriate URI", + "options":[ + {"fromDocument": "candidates"}, + {"value": "none", "label": "None of the above"}, + {"value": "unknown", "label": "Cannot be determined without more context"} + ] + } +] +``` + +**Document** + +```json +{ + "text": "President Bush visited the air base yesterday...", + "candidates": [ + { + "value": "http://dbpedia.org/resource/George_W._Bush", + "label": "George W. Bush (Jnr)" + }, + { + "value": "http://dbpedia.org/resource/George_H._W._Bush", + "label": "George H. W. Bush (Snr)" + } + ] +} +``` + + + +`"fromDocument"` is a dot-separated property path leading to the location within each document where the additional options can be found, for example `"fromDocument":"candidates"` looks for a top-level property named `candidates` in each document, `"fromDocument": "options.custom"` would look for a property named `options` which is itself an object with a property named `custom`. The target property in the document may be in any of the following forms: + +- an array _of objects_, each with `value` and `label` (and optionally `helptext`) properties, exactly as in the static configuration format - this is the format used in the example above +- an array _of strings_, where the same string will be used as both the value and the label for that option +- an arbitrary ["dictionary"](#options-as-dict) object mapping values to labels +- a _single string_, which is parsed into a list of options + +The "single string" alternative is designed to be easier to use when [importing documents](documents_annotations_management.md#importing-documents) from CSV files. It allows you to provide any number of options in a _single_ CSV column value. Within the column the options are separated by semicolons, and each option is of the form `value=label`. Whitespace around the delimiters is ignored, both between options and between the value and label of a single option. For example given CSV document data of + +| text | options | +|-----------------|---------------------------------------------------| +| Favourite fruit | `apple=Apples; orange = Oranges; kiwi=Kiwi fruit` | + +a `{"fromDocument": "options"}` configuration would produce the equivalent of + +```json +[ + {"value": "apple", "label": "Apples"}, + {"value": "orange", "label": "Oranges"}, + {"value": "kiwi", "label": "Kiwi fruit"} +] +``` + +If your values or labels may need to contain the default separator characters `;` or `=` you can select different separators by adding extra properties to the configuration: + +```json +{"fromDocument": "options", "separator": "~~", "valueLabelSeparator": "::"} +``` + +| text | options | +|-----------------|------------------------------------------------------| +| Favourite fruit | `apple::Apples ~~ orange::Oranges ~~ kiwi::Kiwi fruit` | + +The separators can be more than one character, and you can set `"valueLabelSeparator":""` to disable label splitting altogether and just use the value as its own label. + +### Mixing static and dynamic options + +Static and `fromDocument` options may be freely interspersed in any order, so you can have a fully-dynamic set of options by specifying _only_ a `fromDocument` entry with no static options, or you can have static options that are listed first followed by dynamic options, or dynamic options first followed by static, etc. + +### Conditional components + +By default all components listed in the project configuration will be shown for all documents. However this is not always appropriate, for example you may have some components that are only relevant to certain documents, or only relevant for particular combinations of values in _other_ components. To allow for these kinds of scenarios any component can have a field named `if` specifying the conditions under which that component should be shown. + +The `if` field is an _expression_ that is able to refer to fields in both the current _document_ being annotated and the current state of the other annotation components. The expression language is largely based on a subset of the standard JavaScript expression syntax but with a few additional syntax elements to ease working with array data and regular expressions. + +The following simple example shows how you might implement an "Other (please specify)" pattern, where the user can select from a list of choices but also has the option to supply their own answer if none of the choices are appropriate. The free text field is only shown if the user selects the "other" choice. + + + +**Project configuration** + +```json +[ + { + "name": "uri", + "type": "radio", + "title": "Select the most appropriate URI", + "options":[ + {"fromDocument": "candidates"}, + {"value": "other", "label": "Other"} + ] + }, + { + "name": "otherValue", + "type": "text", + "title": "Please specify another value", + "if": "annotation.uri == 'other'", + "regex": "^(https?|urn):", + "valError": "Please specify a URI (starting http:, https: or urn:)" + } +] +``` + +**Document** + +```json +{ + "text": "President Bush visited the air base yesterday...", + "candidates": [ + { + "value": "http://dbpedia.org/resource/George_W._Bush", + "label": "George W. Bush (Jnr)" + }, + { + "value": "http://dbpedia.org/resource/George_H._W._Bush", + "label": "George H. W. Bush (Snr)" + } + ] +} +``` + + +Note that validation rules (such as `optional`, `minSelected` or `regex`) are not applied to components that are hidden by an `if` expression - hidden components will never be included in the annotation output, even if they would be considered "required" had they been visible. + +Components can also be made conditional on properties of the _document_, or a combination of the document and the annotation values, for example + + + +**Project configuration** + +```json +[ + { + "name": "htmldisplay", + "type": "html", + "text": "{{{text}}}" + }, + { + "name": "sentiment", + "type": "radio", + "title": "Sentiment", + "description": "Please select a sentiment of the text above.", + "options": [ + {"value": "negative", "label": "Negative"}, + {"value": "neutral", "label": "Neutral"}, + {"value": "positive", "label": "Positive"} + ] + }, + { + "name": "reason", + "type": "text", + "title": "Why do you disagree with the suggested value?", + "if": "annotation.sentiment !== document.preanno.sentiment" + } +] +``` + +**Documents** + +```json +[ + { + "text": "I love the thing!", + "preanno": { "sentiment": "positive" } + }, + { + "text": "I hate the thing!", + "preanno": { "sentiment": "negative" } + }, + { + "text": "The thing is ok, I guess...", + "preanno": { "sentiment": "neutral" } + } +] +``` + + + +The full list of supported constructions is as follows: + +- the `annotation` variable refers to the current state of the annotation components for this document + - the current value of a particular component can be accessed as `annotation.componentName` or `annotation['component name']` - the brackets version will always work, the dot version works if the component's `name` is a valid JavaScript identifier + - if a component has not been set since the form was last cleared the value may be `null` or `undefined` - the expression should be written to cope with both + - the value of a `text`, `textarea`, `radio` or `selector` component will be a single string (or null/undefined), the value of a `checkbox` component will be an _array_ of strings since more than one value may be selected. If no value is selected the array may be null, undefined or empty, the expression must be prepared to handle any of these +- the `document` variable refers to the current document that is being annotated + - again properties of the document can be accessed as `document.propertyName` or `document['property name']` + - continue the same pattern for nested properties e.g. `document.scores.label1` + - individual elements of array properties can be accessed by zero-based index (e.g. `document.options[0]`) +- various comparison operators are available: + - `==` and `!=` (equal and not-equal) + - `<`, `<=`, `>=`, `>` (less-than, less-or-equal, greater-or-equal, greater-than) + - these operators follow JavaScript rules, which are not always intuitive. Generally if both arguments are strings then they will be compared by lexicographic order, but if either argument is a number then the other one will also be converted to a number before comparing. So if the `score` component is set to the value "10" (a string of two digits) then `annotation.score < 5` would be _false_ (10 is converted to number and compared to 5) but `annotation.score < '5'` would be _true_ (the string "10" sorts before the string "5") + - `in` checks for the presence of an item in an array or a key in an object + - e.g. `'other' in annotation.someCheckbox` checks if the `other` option has been ticked in a checkbox component (whose value is an array) + - this is different from normal JavaScript rules, where `i in myArray` checks for the presence of an array _index_ rather than an array _item_ +- other operators + - `+` (concatenate strings, or add numbers) + - if either argument is a string then both sides are converted to strings and concatenated together + - otherwise both sides are treated as numbers and added + - `-`, `*`, `/`, `%` (subtraction, multiplication, division and remainder) + - `&&`, `||` (boolean AND and OR) + - `!` (prefix boolean NOT, e.g. `!annotation.selected` is true if `selected` is false/null/undefined and false otherwise) + - conditional operator `expr ? valueIfTrue : valueIfFalse` (exactly as in JavaScript, first evaluates the test `expr`, then either the `valueIfTrue` or `valueIfFalse` depending on the outcome of the test) +- `value =~ /regex/` tests whether the given string value contains any matches for the given [regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#writing_a_regular_expression_pattern) + - use `^` and/or `$` to anchor the match to the start and/or end of the value, for example `annotation.example =~ /^a/i` checks whether the `example` annotation value _starts with_ "a" or "A" (the `/i` flag makes the expression case-insensitive) + - since the project configuration is entered as JSON, any backslash characters within the regex must be doubled to escape them from the JSON parser, i.e. `"if": "annotation.option =~ /\\s/"` would check if `option` contains any space characters (for which the regular expression literal is `/\s/`) +- _Quantifier_ expressions let you check whether `any` or `all` of the items in an array or key/value pairs in an object match a predicate expression. The general form is `any(x in expr, predicate)` or `all(x in expr, predicate)`, where `expr` is an expression that resolves to an array or object value, `x` is a new identifier, and `predicate` is the expression to test each item against. The `predicate` expression can refer to the `x` identifier + - `any(option in annotation.someCheckbox, option > 3)` + - `all(e in document.scores, e.value < 0.7)` (assuming `scores` is an object mapping labels to scores, e.g. `{"scores": {"positive": 0.5, "negative": 0.3}}`) + - when testing a predicate against an _object_ each entry has `.key` and `.value` properties giving the key and value of the current entry + - on a null, undefined or empty array/object, `any` will return _false_ (since there are no items that pass the test) and `all` will return _true_ (since there are no items that _fail_ the test) + - the predicate is optional - `any(arrayExpression)` resolves to `true` if any item in the array has a value that JavaScript considers to be "truthy", i.e. anything other than the number 0, the empty string, null or undefined. So `any(annotation.myCheckbox)` is a convenient way to check whether _at least one_ option has been selected in a `checkbox` component. + +If the `if` expression for a particular component is _syntactically invalid_ (missing operands, mis-matched brackets, etc.) then the condition will be ignored and the component will always be displayed as though it did not have an `if` expression at all. Conversely, if the expression is valid but an error occurs while _evaluating_ it, this will be treated the same as if the expression returned `false`, and the associated component will not be displayed. The behaviour is this way around as the most common reason for errors during evaluation is attempting to refer to annotation components that have not yet been filled in - if this is not appropriate in your use case you must account for the possibility within your expression. For example, suppose `confidence` is a `radio` or `selector` component with values ranging from 1 to 5, then another component that declares + +``` +"if": "annotation.confidence && annotation.confidence < 4"` +``` + +will hide this component if `confidence` is unset, displaying it only if `confidence` is set to a value less than 4, whereas + +``` +"if": "!annotation.confidence || annotation.confidence < 4" +``` + +will hide this component only if `confidence` is actually _set_ to a value of 4 or greater - it will _show_ this component if `confidence` is unset. Either approach may be correct depending on your project's requirements. + +To assist managers in authoring project configurations with `if` conditions, the "preview" mode on the project configuration page will display details of any errors that occur when parsing the expressions, or when evaluating them against the **Document input preview** data. You are encouraged to test your expressions thoroughly against a variety of inputs to ensure they behave as intended, before opening your project to annotators. + + diff --git a/docs/versioned/2.1.1/manageradminguide/project_management.md b/docs/versioned/2.1.1/manageradminguide/project_management.md new file mode 100644 index 00000000..f1fd0cfe --- /dev/null +++ b/docs/versioned/2.1.1/manageradminguide/project_management.md @@ -0,0 +1,38 @@ +# Annotation Project Management + +## Project Listing +Clicking on the `Projects` link in the top navigation bar takes you to a contains a list of existing +projects. The project names are shown along with their summaries. Clicking on a project name will +take you to the project management page. + + +## Project Management Page + +The project management page contains all the functionalities to manage an annotation project. The page +is composed of three main tabs: + +* [Configuration](project_config.md) - Configure project settings including what annotations are captured. +* [Documents & Annotation](documents_annotations_management.md) - Manage documents and annotations. Upload documents, see contents of a document's annotations and import/export documents. +* [Annotators](annotators_management.md) - Manage the recruitment of annotators. + +::: warning + +Annotators can only be recruited to an annotation project after it has been configured and documents +are uploaded to the project. + +::: + + +## Project status icons +In the **Project listing** and **Project management page**, icon badges are used to provide a quick overview of the project's status: + +* 1 - Number of completed annotations in the project. +* 1 - Number of rejected annotations in the project. +* 1 - Number of timed out annotations in the project. +* 1 - Number of aborted annotations in the project. +* 1 - Number of pending annotations in the project. +* 2/60 - Number of occupied annotation tasks over number of total tasks in the project. +* 20/5/10 - Number of documents, training documents and test documents in the project. +* 1 - Number of annotators recruited in the project. Annotators are removed from the project when they have completed all annotation tasks in their quota. + + diff --git a/docs/versioned/2.2.0/.vuepress/components/AnnotationRendererPreview.vue b/docs/versioned/2.2.0/.vuepress/components/AnnotationRendererPreview.vue new file mode 100644 index 00000000..98ecc1af --- /dev/null +++ b/docs/versioned/2.2.0/.vuepress/components/AnnotationRendererPreview.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/docs/versioned/2.2.0/.vuepress/components/DisplayVersion.vue b/docs/versioned/2.2.0/.vuepress/components/DisplayVersion.vue new file mode 100644 index 00000000..03ec07ed --- /dev/null +++ b/docs/versioned/2.2.0/.vuepress/components/DisplayVersion.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/docs/versioned/2.2.0/.vuepress/config.js b/docs/versioned/2.2.0/.vuepress/config.js new file mode 100644 index 00000000..b78cc868 --- /dev/null +++ b/docs/versioned/2.2.0/.vuepress/config.js @@ -0,0 +1,42 @@ +const versionData = require("./versions.json") +const path = require("path"); +module.exports = context => ({ + title: 'GATE Teamware Documentation', + description: 'Documentation for GATE Teamware', + base: versionData.base, + themeConfig: { + nav: [ + {text: 'Home', link: '/'}, + {text: 'Annotators', link: '/annotatorguide/'}, + {text: 'Managers & Admins', link: '/manageradminguide/'}, + {text: 'Developer', link: '/developerguide/'} + ], + sidebar: { + '/manageradminguide/': [ + "", + "project_management", + "project_config", + "documents_annotations_management", + "annotators_management" + ], + '/developerguide/': [ + '', + 'frontend', + 'testing', + 'releases', + 'documentation', + "api_docs", + + ], + }, + }, + configureWebpack: { + resolve: { + alias: { + '@': path.resolve(__dirname, versionData.frontendSource) + } + } + }, + + +}) diff --git a/docs/versioned/2.2.0/.vuepress/enhanceApp.js b/docs/versioned/2.2.0/.vuepress/enhanceApp.js new file mode 100644 index 00000000..e7aadadf --- /dev/null +++ b/docs/versioned/2.2.0/.vuepress/enhanceApp.js @@ -0,0 +1,17 @@ +import Vue from 'vue' +import {BootstrapVue, BootstrapVueIcons, IconsPlugin} from 'bootstrap-vue' + +import 'bootstrap/dist/css/bootstrap.css' +import 'bootstrap-vue/dist/bootstrap-vue.css' + +Vue.use(BootstrapVue) +Vue.use(BootstrapVueIcons) + +export default ({ + Vue, // the version of Vue being used in the VuePress app + options, // the options for the root Vue instance + router, // the router instance for the app + siteData // site metadata +}) => { + +} diff --git a/docs/versioned/2.2.0/.vuepress/theme/components/Navbar.vue b/docs/versioned/2.2.0/.vuepress/theme/components/Navbar.vue new file mode 100644 index 00000000..c3b966db --- /dev/null +++ b/docs/versioned/2.2.0/.vuepress/theme/components/Navbar.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/docs/versioned/2.2.0/.vuepress/theme/components/VersionSelector.vue b/docs/versioned/2.2.0/.vuepress/theme/components/VersionSelector.vue new file mode 100644 index 00000000..4cfb5eb9 --- /dev/null +++ b/docs/versioned/2.2.0/.vuepress/theme/components/VersionSelector.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/docs/versioned/2.2.0/.vuepress/theme/index.js b/docs/versioned/2.2.0/.vuepress/theme/index.js new file mode 100644 index 00000000..b91b8a57 --- /dev/null +++ b/docs/versioned/2.2.0/.vuepress/theme/index.js @@ -0,0 +1,3 @@ +module.exports = { + extend: '@vuepress/theme-default' +} diff --git a/docs/versioned/2.2.0/.vuepress/versions.json b/docs/versioned/2.2.0/.vuepress/versions.json new file mode 100644 index 00000000..21ad6d41 --- /dev/null +++ b/docs/versioned/2.2.0/.vuepress/versions.json @@ -0,0 +1,35 @@ +{ + "current": "2.2.0", + "base": "/gate-teamware/2.2.0/", + "versions": [ + { + "text": "0.3.0", + "value": "/gate-teamware/0.3.0/" + }, + { + "text": "0.4.0", + "value": "/gate-teamware/0.4.0/" + }, + { + "text": "2.0.0", + "value": "/gate-teamware/2.0.0/" + }, + { + "text": "2.1.0", + "value": "/gate-teamware/2.1.0/" + }, + { + "text": "2.1.1", + "value": "/gate-teamware/2.1.1/" + }, + { + "text": "2.2.0", + "value": "/gate-teamware/2.2.0/" + }, + { + "text": "development", + "value": "/gate-teamware/development/" + } + ], + "frontendSource": "../../../../frontend/src" +} \ No newline at end of file diff --git a/docs/versioned/2.2.0/README.md b/docs/versioned/2.2.0/README.md new file mode 100644 index 00000000..d12b9b88 --- /dev/null +++ b/docs/versioned/2.2.0/README.md @@ -0,0 +1,138 @@ +# GATE Teamware + +![GATE Teamware logo](./img/gate-teamware-logo.svg "GATE Teamware logo") + +A web application for collaborative document annotation. + +This is a documentation for Teamware version: + +## Key Features +* Free and open source software. +* Configure annotation options using a highly flexible JSON config. +* Set limits on proportions of a task that annotators can annotate. +* Import existing annotations as CSV or JSON. +* Export annotations as CSV or JSON. +* Annotation instructions and document rendering supports markdown and HTML. + +## Getting started +A quickstart guide for annotators is [available here](annotatorguide). + +To use an existing instance of GATE Teamware as a project manager or admin, find instructions in the [Managers and Admins guide](manageradminguide). + +Documentation on deploying your own instance can be found in the [Developer Guide](developerguide). + +## Installation Guide + +### Quick Start + +The simplest way to deploy your own copy of GATE Teamware is to use Docker Compose on Linux or Mac. Installation on Windows is possible but not officially supported - you need to be able to run `bash` shell scripts for the quick-start installer. + +1. Install Docker - [Docker Engine](https://docs.docker.com/engine/) for Linux servers or [Docker Desktop](https://docs.docker.com/desktop/) for Mac. +2. Install [Docker Compose](https://github.com/docker/compose), if your Docker does not already include it (Compose is included by default with Docker Desktop) +3. Download the [installation script](https://gate.ac.uk/get-teamware.sh) into an empty directory, run it and follow the instructions. + +``` +mkdir gate-teamware +cd gate-teamware +curl -LO https://gate.ac.uk/get-teamware.sh +bash ./get-teamware.sh +``` + +This will make the Teamware application available as `http://localhost:8076`, with the option to expose it as a public `https://` URL if your server is directly internet-accessible - for production use we recommend deploying Teamware with a suitable internet-facing reverse proxy, or use Kubernetes as described below. + +### Deployment using Kubernetes + +A Helm chart to deploy Teamware on Kubernetes is published to the GATE team public charts repository. The chart requires [Helm](https://helm.sh) version 3.7 or later, and is compatible with Kubernetes version 1.23 or later. Earlier Kubernetes versions back to 1.19 _may_ work provided autoscaling is not enabled, but these have not been tested. + +The following quick start instructions assume you have a compatible Kubernetes cluster and a working installation of `kubectl` and `helm` (3.7 or later) with permission to create all the necessary resource types in your target namespace. + +First generate a random "secret key" for the Django application. This must be at least 50 random characters, a quick way to do this is + +``` +# 42 random bytes base64 encoded becomes 56 random characters +kubectl create secret generic -n {namespace} django-secret \ + --from-literal="secret-key=$( openssl rand -base64 42 )" +``` + +Add the GATE charts repository to your Helm configuration: + +``` +helm repo add gate https://repo.gate.ac.uk/repository/charts +helm repo update +``` + +Create a `values.yaml` file with the key settings required for teamware. The following is a minimal set of values for a typical installation: + +```yaml +# Public-facing web hostname of the teamware application, the public +# URL will be https://{hostName} +hostName: teamware.example.com + +email: + # "From" address on emails sent by Teamware + adminAddress: admin@teamware.example.com + # Send email via an SMTP server - alternatively "gmail" to use GMail API + backend: "smtp" + smtp: + host: mail.example.com + # You will also need to set user and passwordSecret if your + # mail server requires authentication + +privacyPolicy: + # Contact details of the host and administrator of the teamware + # instance, if no admin defined, defaults to the host values. + host: + # Name of the host + name: "Service Host" + # Host's physical address + address: "123 Example Street, City. Country." + # A method of contacting the host, field supports HTML for e.g. linking to a form + contact: "Email" + admin: + name: "Dr. Service Admin" + address: "Department of Example Studies, University of Example, City. Country." + contact: "Email" + +backend: + # Name of the random secret you created above + djangoSecret: django-secret + +# Initial "super user" created on the first install. These are just +# the *initial* settings, you can (and should!) change the password +# once Teamware is up and running +superuser: + email: me@example.com + username: admin + password: changeme +``` + +Some of these may be omitted or others may be required depending on the setup of your specific cluster - see the [chart README](https://github.com/GateNLP/charts/blob/main/gate-teamware/README.md) and the chart's own values file (which you can retrieve with `helm show values gate/gate-teamware`) for full details. In particular these values assume: + +- your cluster has an ingress controller, with a default ingress class configured, and that controller has a default TLS certificate that is compatible with your chosen hostname (e.g. a `*.example.com` wildcard) +- your cluster has a default storageClass configured to provision PVCs, and at least 8 GB of available PV capacity +- you can send email via an SMTP server with no authentication +- the default GATE Teamware terms and privacy documents are suitable for your deployment and compliant with the laws of your location. If this is not the case you can supply your own custom policy documents in a ConfigMap +- you do not need to back up your PostgreSQL database - the chart does include the option to store backups in Amazon S3 or another compatible object store, see the full README for details + +Once you have created your values file, you can install the chart or upgrade an existing installation using + +``` +helm upgrade --install gate-teamware gate/gate-teamware \ + --namespace {namespace} --values teamware-values.yaml +``` + + +## Bug reports and feature requests +Please make bug reports and feature requests as Issues on the [GATE Teamware GitHub repo](https://github.com/GATENLP/gate-teamware). + +# Using Teamware +Teamware is developed by the [GATE](https://gate.ac.uk) team, an academic research group at The University of Sheffield. As a result, future funding relies on evidence of the impact that the software provides. If you use Teamware, please let us know using the contact form at [gate.ac.uk](https://gate.ac.uk/g8/contact). Please include details on grants, publications, commercial products etc. Any information that can help us to secure future funding for our work is greatly appreciated. + +## Citation +For published work that has used Teamware, please cite the [EACL23 demo paper](https://aclanthology.org/2023.eacl-demo.17/). One way is to include a citation such as: + +> Wilby, D., Karmakharm, T., Roberts, I., Song, X. & Bontcheva, K. (2023). GATE Teamware 2: An open-source tool for collaborative document classification annotation. In Proceedings of the 17th Conference of the European Chapter of the Association for Computational Linguistics: System Demonstrations, pages 145–151, Dubrovnik, Croatia. Association for Computational Linguistics. https://aclanthology.org/2023.eacl-demo.17/ + +Please use the `Cite this repository` button at the top of the [project's GitHub repository](https://github.com/GATENLP/gate-teamware) to get an up to date citation. + +Permanent references to each version of the software are available from [Zenodo](https://doi.org/10.5281/zenodo.7899193). The Teamware version can be found on the 'About' page of your Teamware instance. diff --git a/docs/versioned/2.2.0/annotatorguide/README.md b/docs/versioned/2.2.0/annotatorguide/README.md new file mode 100644 index 00000000..4a3ddae2 --- /dev/null +++ b/docs/versioned/2.2.0/annotatorguide/README.md @@ -0,0 +1,27 @@ +# Annotators Quickstart + +Annotating a project: + +* After signing up to the site, notify the owner of the annotation project you've been recruited of + your username. This will allow them to add you as an annotator to a project. +* After you've been recruited to a project, click on the `Annotate` link on the navigation bar at the + top of the page to start annotating. +* You will be shown the details about the project you're annotating along with a set of form(s) to capture + your annotation. Ensure you've read the Annotator guideline fully before starting the annotation process. +* You can then start annotating documents one at a time. Click on `Submit` to confirm the completion of + annotation, `Clear` to start again or `Reject` to skip the particular document. Be aware some projects + do not allow you to skip documents. +* Once you've finished annotating a certain number of documents in a project (specified by the project + manager) your task will be deemed complete, and you will be able to be recruited into another annotation + project. + +## Deleting your account + +At any time you can choose to stop participating and delete your account. You can do this by: + +* Click on your username in the top right corner and then `Account`. +* Click on `Delete my account`. +* When deleting your account, by default your personal information will be removed but your annotations will remain on the system. To completely remove all of your annotations, click on the checkbox next to `Also remove any annotations, projects and documents that I own:`. +* Click the `Unlock` button. +* Then click `Delete` to remove your account. + diff --git a/docs/versioned/2.2.0/developerguide/README.md b/docs/versioned/2.2.0/developerguide/README.md new file mode 100644 index 00000000..02e4494f --- /dev/null +++ b/docs/versioned/2.2.0/developerguide/README.md @@ -0,0 +1,286 @@ +# Developer guide + +## Architecture +``` +├── .github/workflows/ # github actions workflow files +├── teamware/ # Django project +│   └── settings/ +├── backend/ # Django app +├── cypress/ # integration test configurations +├── docs/ # documentation +├── examples/ # example data files +├── frontend/ # all frontend, in VueJS framework +├── nginx/ # Nginx configurations +| +# Top level directory contains scripts for management and deployment, +# main project package.json, python requirements, docker configs +├── build-images.sh +├── deploy.sh +├── create-django-db.sh +├── docker-compose.yml +├── Dockerfile +├── generate-docker-env.sh +├── manage.py +├── migrate-integration.sh +├── package.json +├── package-lock.json +├── pytest.ini +├── README.md +├── requirements-dev.txt +├── requirements.txt +└── run-server.sh + +``` + +## Installation for development + +The service depends on a combination of python and javascript libraries. We recommend developing inside a `conda` conda environment as it is able to install +python libraries and nodejs which is used to install javascript libraries. + +* Install anaconda/miniconda +* Create a blank virtual conda env + ```bash + $ conda create -n teamware python=3.9 + ``` +* Activate conda environment + ```bash + $ source activate teamware + # or + $ conda activate teamware + ``` +* Install python dependencies in conda environment using pip + ```bash + (teamware)$ pip install -r requirements.txt -r requirements-dev.txt + ``` +* Install nodejs, postgresql and openssl in the conda environment + ```bash + (teamware)$ conda install -y -c conda-forge postgresql=14.* + (teamware)$ conda install -y -c conda-forge nodejs=18.* + ``` +* Install nodejs dependencies + ```bash + (teamware)$ npm install + ``` + +Set up a new postgreSQL database and user for development: +``` +# Create a new directory for the db data and initialise +mkdir -p pgsql/data +initdb -D pgsql/data + +# Launch postgres in the background +postgres -p 5432 -D pgsql/data & + +# Create a DB user, you'll be prompted to input password, "password" is the default in teamware/settings/base.py for development +createuser -p 5432 -P user --createdb + +# Create a rumours_db with rumours as user +createdb -p 5432 -O user teamware_db + +# Migrate & create database tables +python manage.py migrate + +# create a new superuser - when prompted enter a username and password for the db superuser +python manage.py createsuperuser +``` + +## Updating packages +To update packages after a merge, run the following commands: + +```bash +# Activate the conda environment +source activate teamware +# Update any packages changed in the python requirements.txt and requirements-dev.txt files +pip install -r requirements.txt -r requirements-dev.txt +# Update any packages changed in package.json +npm install +``` + +## Development server +The application uses django's dev server to serve page contents and run the RPC API, it also uses Vue CLI's +development server to serve dynamic assets such as javascript or stylesheets allowing for hot-reloading +during development. + +To run both servers together: + + ```bash + npm run serve + ``` + +To run separately: + +* Django server + ```bash + npm run serve:backend + ``` +* Vue CLI dev server + ```bash + npm run serve:frontend + ``` + +## Deploying a development version using Docker +Deployment is via [docker-compose](https://docs.docker.com/compose/), using [NGINX](https://www.nginx.com/) to serve static content, a separate [postgreSQL](https://hub.docker.com/_/postgres) service containing the database and a database backup service (see `docker-compose.yml` for details). Pre-built images can be run using most versions of Docker but _building_ images requires `docker buildx`, which means either Docker Desktop or version 19.03 or later of Docker Engine. + +1. Run `./generate-docker-env.sh` to create a `.env` file containing randomly generated secrets which are mounted as environment variables into the container. See [below](#env-config) for details. + +2. Then build the images via: + ```bash + ./build-images.sh + ``` + +3. then deploy the stack with + + ```bash + ./deploy.sh production # (or prod) to deploy with production settings + ./deploy.sh staging # (or stag) to deploy with staging settings + ``` + +To bring the stack down, run `docker-compose down`, using the `-v` flag to destroy the database volume (be careful with this). + +### Configuration using environment variables (.env file) + +To allow the app to be easily configured between instances especially inside containers, many of the app's configuration can be done through environment variables. + +Run `./generate-docker-env.sh` to generate a `.env` file with all configurable environment parameters. + +To set values for your own deployment, add values to the variables in `.env`, most existing values will be kept after running `generate-docker-env.sh`, see comments in `.env` for specific details. Anything that is left blank will be filled with a default value. Passwords and keys are filled with auto-generated random values. + +Existing `.env` files are copied into a new file named `saved-env.` by `generate-docker-env.sh`. + +### Backups + +In a docker-compose based deployment, backups of the database are managed by the service `pgbackups` which uses the [`prodrigestivill/postgres-backup-local:12`](https://hub.docker.com/r/prodrigestivill/postgres-backup-local) image. +By default, backups are taken of the database daily, and the `docker-compose.yml` contains settings for the number of backups kept under the options for the `pgbackups` service. +Backups are stored as a gzipped SQL dump from the database. + +#### Taking a manual backup + +A shell script is provided for manually triggering a backup snapshot. +From the main project directory run + +```sh +$ ./backup_manual.sh +``` + +This uses the `pgbackups` service and all settings and envrionment variables it is configured with in `docker-compose.yml`, so backups will be taken to the same location as configured for the main backup schedule. + +#### Restoring from a backup +1. Locate the backup file (`*.sql.gz`) on your system that you would like to restore from. +2. Make sure that the stack is down, from the main project directory run `docker-commpose down`. +3. Run the backup restore shell script, passing in the path to your backup file as the only argument: + +```sh +$ ./backup_restore.sh path/to/my/backup.sql.gz +``` + +This will first launch the database container, then via Django's `dbshell` command, running in the `backend` service, execute a number of SQL commands before and after running all the SQL from the backup file. + +4. Redeploy the stack, via `./deploy.sh staging`, `./deploy.sh production`, or simply `docker compose up -d`, whichever is the case. +5. The database *should* be restored. + +## Configuration + +### Django settings files + +Django settings are located in `teamware/settings` folder. The app will use `base.py` setting by default +and this must be overridden depending on use. + +### Database +A SQLite3 database is used during development and during integration testing. + +For staging and production, postgreSQL is used, running from a `postgres-14` docker container. Settings are found in `teamware/settings/base.py` and `deployment.py` as well as being set as environment variables by `./generate-docker-env.sh` and passed to the container as configured in `docker-compose.yml`. + +In Kubernetes deployments the PostgreSQL database is installed using the Bitnami `postresql` public chart. + + +### Sending E-mail +It's recommended to specify e-mail configurations through environment variables (`.env`). As these settings will include username and passwords that should not be tracked by version control. + +#### E-mail using SMTP +SMTP is supported as standard in Django, add the following configurations with your own details +to the list of environment variables: + +```bash +DJANGO_EMAIL_BACKEND='django.core.mail.backends.smtp.EmailBackend' +DJANGO_EMAIL_HOST='myserver.com' +DJANGO_EMAIL_PORT=25 +DJANGO_EMAIL_HOST_USER='username' +DJANGO_EMAIL_HOST_PASSWORD='password' +DJANGO_EMAIL_SECURITY=tls +# tls = STARTTLS, typically on port 25 or 587 +# ssl = TLS-on-connect, typically on port 465 +# none (or omitted) = no encryption +``` + +#### E-mail using Google API +The [django-gmailapi-backend](https://github.com/dolfim/django-gmailapi-backend) library +has been added to allow sending of mail through Google's API as sending through SMTP is disabled as standard. + +Unlike with SMTP, Google's API requires OAuth authentication which means a project and a credential has to be +created through Google's cloud console. + +* More information on the Gmail API: [https://developers.google.com/gmail/api/guides/sending](https://developers.google.com/gmail/api/guides/sending) +* OAuth credentials for sending emails: [https://github.com/google/gmail-oauth2-tools/wiki/OAuth2DotPyRunThrough](https://github.com/google/gmail-oauth2-tools/wiki/OAuth2DotPyRunThrough) + +This package includes the script linked in the documentation above, which simplifies the setup of the API credentials. The following outlines the key steps: + +1. Create a project in the Google developer console, [https://console.cloud.google.com/](https://console.cloud.google.com/) +2. Enable the Gmail API +3. Create OAuth 2.0 credentials, you'll likely want to create a `Desktop` +4. Create a valid refresh_token using the helper script included in the package: + ```bash + gmail_oauth2 --generate_oauth2_token \ + --client_id="" \ + --client_secret="" \ + --scope="https://www.googleapis.com/auth/gmail.send" + ``` +5. Add the created credentials and tokens to the environment variable as shown below: + ```bash + DJANGO_EMAIL_BACKEND='gmailapi_backend.mail.GmailBackend' + DJANGO_GMAIL_API_CLIENT_ID='google_assigned_id' + DJANGO_GMAIL_API_CLIENT_SECRET='google_assigned_secret' + DJANGO_GMAIL_API_REFRESH_TOKEN='google_assigned_token' + ``` + + +#### Teamware Privacy Policy and Terms & Conditions + +Teamware includes a default privacy policy and terms & conditions, which are required for running the application. + +The default privacy policy is intended to be compliant with UK GDPR regulations, which may comply with the rights of users of your deployment, however it is your responsibility to ensure that this is the case. + +If the default privacy policy covers your use case, then you will need to include configuration for a few contact details. + +Contact details are required for the **host** and the **administrator**: the **host** is the organisation or individual responsible for managing the deployment of the teamware instance and the **administrator** is the organisation or individual responsible for managing users, projects and data on the instance. In many cases these roles will be filled by the same organisation or individual, so in this case specifying just the **host** details is sufficient. + +For deployment from source, set the following environment variables: + +* `PP_HOST_NAME` +* `PP_HOST_ADDRESS` +* `PP_HOST_CONTACT` +* `PP_ADMIN_NAME` +* `PP_ADMIN_ADDRESS` +* `PP_ADMIN_CONTACT` + +For deployment using docker-compose, set these values in `.env`. + +If the host and administrator are the same, you can just set the `PP_HOST_*` variables above which will be used for both. + +##### Including a custom Privacy Policy and/or Terms & Conditions + +If the default privacy policy or terms & conditions do not cover your use case, you can easily replace these with your own documents. + +If deploying from source, include markdown (`.md`) files in a `custom-policies` directory in the project root with the exact names `custom-policies/privacy-policy.md` and/or `custom-policies/terms-and-conditions.md` which will be rendered at the corresponding pages on the running web app. If you are not familiar with the Markdown language there are a number of free WYSIWYG-style editor tools available including [StackEdit](https://stackedit.io/app) (browser based) and [Zettlr](https://www.zettlr.com) (desktop app). + +If deploying with docker compose, place the `custom-policies` directory at the same location as the `docker-compose.yml` file before running `./deploy.sh` as above. + +An example custom privacy policy file contents might look like: + +```md +# Organisation X Teamware Privacy Policy +... +... +## Definitions of Roles and Terminology +... +... +``` diff --git a/docs/versioned/2.2.0/developerguide/api_docs.md b/docs/versioned/2.2.0/developerguide/api_docs.md new file mode 100644 index 00000000..03964b37 --- /dev/null +++ b/docs/versioned/2.2.0/developerguide/api_docs.md @@ -0,0 +1,1086 @@ +--- +sidebarDepth: 3 +--- + +# API Documentation + +## Using the JSONRPC endpoints + +::: tip +A single endpoint is used for all API requests, located at `/rpc` +::: + +The API used in the app complies to JSON-RPC 2.0 spec. Requests should always be sent with `POST` and +contain a JSON request object in the body. The response will also be in the form of a JSON object. + +For example, to call the method `subtract(a, b)`. Send `POST` a post request to `/rpc` with the following JSON +in the body: + +```json +{ + "jsonrpc":"2.0", + "method":"subtract", + "params":[ + 42, + 23 + ], + "id":1 +} +``` + +Variables are passed as a list to the `params` field, in this case `a=42` and `b=23`. The `id` field in the top +level of the request object refers to the message ID, this ID value will be matched in the response, +it does not affect the method that is being called. + +The response will be as follows: + +```json +{ + "jsonrpc":"2.0", + "result":19, + "id":1 +} +``` + +In the case of errors, the response will contain an `error` field with error `code` and error `message`: + +```json +{ + "jsonrpc":"2.0", + "error":{ + "code":-32601, + "message":"Method not found" + }, + "id":"1" +} +``` + +The following are error codes used in the app: + +```python +PARSE_ERROR = -32700 +INVALID_REQUEST = -32600 +METHOD_NOT_FOUND = -32601 +INVALID_PARAMS = -32602 +INTERNAL_ERROR = -32603 +AUTHENTICATION_ERROR = -32000 +UNAUTHORIZED_ERROR = -32001 +``` + +## API Listing + + + +### initialise() + + +::: tip Description +Provide the initial context information to initialise the Teamware app + + context_object: + user: + isAuthenticated: bool + isManager: bool + isAdmin: bool + configs: + docFormatPref: bool + global_configs: + allowUserDelete: bool +::: + + + + + + + + +### is_authenticated() + + +::: tip Description +Checks that the current user has logged in. +::: + + + + + + + + +### login(payload) + + + + +#### Parameters + +* payload + + + + + + + +### logout() + + + + + + + + + +### register(payload) + + + + +#### Parameters + +* payload + + + + + + + +### generate_user_activation(username) + + + + +#### Parameters + +* username + + + + + + + +### activate_account(username,token) + + + + +#### Parameters + +* username + +* token + + + + + + + +### generate_password_reset(username) + + + + +#### Parameters + +* username + + + + + + + +### reset_password(username,token,new_password) + + + + +#### Parameters + +* username + +* token + +* new_password + + + + + + + +### change_password(payload) + + + + +#### Parameters + +* payload + + + + + + + +### change_email(payload) + + + + +#### Parameters + +* payload + + + + + + + +### set_user_receive_mail_notifications(do_receive_notifications) + + + + +#### Parameters + +* do_receive_notifications + + + + + + + +### set_user_document_format_preference(doc_preference) + + + + +#### Parameters + +* doc_preference + + + + + + + +### get_user_details() + + + + + + + + + +### get_user_annotated_projects() + + +::: tip Description +Gets a list of projects that the user has annotated +::: + + + + + + + + +### get_user_annotations_in_project(project_id,current_page,page_size) + + +::: tip Description +Gets a list of documents in a project where the user has performed annotations in. + :param project_id: The id of the project to query + :param current_page: A 1-indexed page count + :param page_size: The maximum number of items to return per query + :returns: Dictionary of items and total count after filter is applied {"items": [], "total_count": int} +::: + + + +#### Parameters + +* project_id + +* current_page + +* page_size + + + + + + + +### user_delete_personal_information() + + + + + + + + + +### user_delete_account() + + + + + + + + + +### create_project() + + + + + + + + + +### delete_project(project_id) + + + + +#### Parameters + +* project_id + + + + + + + +### update_project(project_dict) + + + + +#### Parameters + +* project_dict + + + + + + + +### get_project(project_id) + + + + +#### Parameters + +* project_id + + + + + + + +### clone_project(project_id) + + + + +#### Parameters + +* project_id + + + + + + + +### import_project_config(pk,project_dict) + + + + +#### Parameters + +* pk + +* project_dict + + + + + + + +### export_project_config(pk) + + + + +#### Parameters + +* pk + + + + + + + +### get_projects(current_page,page_size,filters) + + +::: tip Description +Gets the list of projects. Query result can be limited by using current_page and page_size and sorted + by using filters. + + :param current_page: A 1-indexed page count + :param page_size: The maximum number of items to return per query + :param filters: Filter option used to search project, currently only string is used to search + for project title + :returns: Dictionary of items and total count after filter is applied {"items": [], "total_count": int} +::: + + + +#### Parameters + +* current_page + +* page_size + +* filters + + + + + + + +### get_project_documents(project_id,current_page,page_size,filters) + + +::: tip Description +Gets the list of documents and its annotations. Query result can be limited by using current_page and page_size + and sorted by using filters + + :param project_id: The id of the project that the documents belong to, is a required variable + :param current_page: A 1-indexed page count + :param page_size: The maximum number of items to return per query + :param filters: Filter currently only searches for ID of documents + for project title + :returns: Dictionary of items and total count after filter is applied {"items": [], "total_count": int} +::: + + + +#### Parameters + +* project_id + +* current_page + +* page_size + +* filters + + + + + + + +### get_project_test_documents(project_id,current_page,page_size,filters) + + +::: tip Description +Gets the list of documents and its annotations. Query result can be limited by using current_page and page_size + and sorted by using filters + + :param project_id: The id of the project that the documents belong to, is a required variable + :param current_page: A 1-indexed page count + :param page_size: The maximum number of items to return per query + :param filters: Filter currently only searches for ID of documents + for project title + :returns: Dictionary of items and total count after filter is applied {"items": [], "total_count": int} +::: + + + +#### Parameters + +* project_id + +* current_page + +* page_size + +* filters + + + + + + + +### get_project_training_documents(project_id,current_page,page_size,filters) + + +::: tip Description +Gets the list of documents and its annotations. Query result can be limited by using current_page and page_size + and sorted by using filters + + :param project_id: The id of the project that the documents belong to, is a required variable + :param current_page: A 1-indexed page count + :param page_size: The maximum number of items to return per query + :param filters: Filter currently only searches for ID of documents + for project title + :returns: Dictionary of items and total count after filter is applied {"items": [], "total_count": int} +::: + + + +#### Parameters + +* project_id + +* current_page + +* page_size + +* filters + + + + + + + +### add_project_document(project_id,document_data) + + + + +#### Parameters + +* project_id + +* document_data + + + + + + + +### add_project_test_document(project_id,document_data) + + + + +#### Parameters + +* project_id + +* document_data + + + + + + + +### add_project_training_document(project_id,document_data) + + + + +#### Parameters + +* project_id + +* document_data + + + + + + + +### add_document_annotation(doc_id,annotation_data) + + + + +#### Parameters + +* doc_id + +* annotation_data + + + + + + + +### get_annotations(project_id) + + +::: tip Description +Serialize project annotations as GATENLP format JSON using the python-gatenlp interface. +::: + + + +#### Parameters + +* project_id + + + + + + + +### delete_documents_and_annotations(doc_id_ary,anno_id_ary) + + + + +#### Parameters + +* doc_id_ary + +* anno_id_ary + + + + + + + +### get_possible_annotators(proj_id) + + + + +#### Parameters + +* proj_id + + + + + + + +### get_project_annotators(proj_id) + + + + +#### Parameters + +* proj_id + + + + + + + +### add_project_annotator(proj_id,username) + + + + +#### Parameters + +* proj_id + +* username + + + + + + + +### make_project_annotator_active(proj_id,username) + + + + +#### Parameters + +* proj_id + +* username + + + + + + + +### project_annotator_allow_annotation(proj_id,username) + + + + +#### Parameters + +* proj_id + +* username + + + + + + + +### remove_project_annotator(proj_id,username) + + + + +#### Parameters + +* proj_id + +* username + + + + + + + +### reject_project_annotator(proj_id,username) + + + + +#### Parameters + +* proj_id + +* username + + + + + + + +### get_annotation_timings(proj_id) + + + + +#### Parameters + +* proj_id + + + + + + + +### delete_annotation_change_history(annotation_change_history_id) + + + + +#### Parameters + +* annotation_change_history_id + + + + + + + +### get_annotation_task() + + +::: tip Description +Gets the annotator's current task, returns a dictionary about the annotation task that contains all the information + needed to render the Annotate view. +::: + + + + + + + + +### get_annotation_task_with_id(annotation_id) + + +::: tip Description +Get annotation task dictionary for a specific annotation_id, must belong to the annotator (or is a manager or above) +::: + + + +#### Parameters + +* annotation_id + + + + + + + +### complete_annotation_task(annotation_id,annotation_data,elapsed_time) + + +::: tip Description +Complete the annotator's current task +::: + + + +#### Parameters + +* annotation_id + +* annotation_data + +* elapsed_time + + + + + + + +### reject_annotation_task(annotation_id) + + +::: tip Description +Reject the annotator's current task +::: + + + +#### Parameters + +* annotation_id + + + + + + + +### change_annotation(annotation_id,new_data) + + +::: tip Description +Adds annotation data to history +::: + + + +#### Parameters + +* annotation_id + +* new_data + + + + + + + +### get_document(document_id) + + +::: tip Description +Obsolete: to be deleted +::: + + + +#### Parameters + +* document_id + + + + + + + +### get_annotation(annotation_id) + + +::: tip Description +Obsolete: to be deleted +::: + + + +#### Parameters + +* annotation_id + + + + + + + +### annotator_leave_project() + + +::: tip Description +Allow annotator to leave their currently associated project. +::: + + + + + + + + +### get_all_users() + + + + + + + + + +### get_user(username) + + + + +#### Parameters + +* username + + + + + + + +### admin_update_user(user_dict) + + + + +#### Parameters + +* user_dict + + + + + + + +### admin_update_user_password(username,password) + + + + +#### Parameters + +* username + +* password + + + + + + + +### admin_delete_user_personal_information(username) + + + + +#### Parameters + +* username + + + + + + + +### admin_delete_user(username) + + + + +#### Parameters + +* username + + + + + + + +### get_privacy_policy_details() + + + + + + + + + +### get_endpoint_listing() + + + + + + + + + + + diff --git a/docs/versioned/2.2.0/developerguide/documentation.md b/docs/versioned/2.2.0/developerguide/documentation.md new file mode 100644 index 00000000..3de09904 --- /dev/null +++ b/docs/versioned/2.2.0/developerguide/documentation.md @@ -0,0 +1,61 @@ +# Managing and versioning documentation + +Documentation versioning is managed by the custom node script located at `docs/manage_versions.js`. Versions of the documentation can be archived and the entire documentation site can be built using the script. + +Various configuration parameters used for management of documentation versioning can be found in `docs/docs.config.js`. + +## Installing dependencies required to serve the documentation site + +The documentation uses vuepress and other libraries which has to be installed separately running the following command from the root of the project: + +```bash +npm run install:docs +``` + +## Editing the documentation + +The latest version of the documentation is located at `/docs/docs`. The archived (versioned) documentation are located in `/docs/versioned/version_number`. + +Use the following command to live preview the latest version of the documentation: + +``` +npm run serve:docs +``` + +Note that this will not work with other versioned docs as they are managed as a separate site. To live preview versioned documentation use the command (replace version_num with the version you'd like to preview): + +``` +vuepress dev docs/versioned/version_num +``` + +## Creating a new documentation version + +To create a version of the documentation, run the command: + +``` +npm run docs:create_version +``` + +This creates a copy of the current set of documentation in `/docs/docs` and places it at `/docs/versioned/version_num`. The version number in `package.json` is used for the documentation version. + +Each set of documentation can be considered as a separate vuepress site. Each one has a `.vuepress/versions.json` file that contains the listing of all versions, allowing them to link to each other. + +Note: Versions can also be created manually by running the command: + +``` +# Replace version_num with the version you'd like to create +node docs/manage_versions.js create version_num +``` + + +## Building documentation site + +To build the documentation site, the previous documentation build command is used: + +``` +npm run build:docs +``` + +## Implementation of the version selector UI + +A partial override of the default Vuepress theme was needed to add a custom component the navigation bar. The modified version of the `NavBar` component can be found in `/docs/docs/.vuepress/theme/components/NavBar.vue`. The modified NavBar uses the `VersionSelector` (`/docs/docs/.vuepress/theme/components/VersionSelector.vue`) component which reads from the `.vuepress/versions.json` from each set of documentation. diff --git a/docs/versioned/2.2.0/developerguide/frontend.md b/docs/versioned/2.2.0/developerguide/frontend.md new file mode 100644 index 00000000..ae2aeff0 --- /dev/null +++ b/docs/versioned/2.2.0/developerguide/frontend.md @@ -0,0 +1,146 @@ +# Frontend + +Web GUI of Teamware is built with [vue.js](https://vuejs.org) version 2.7.x. + +[Bootstrap](https://getbootstrap.com/) (and [Bootstrap vue](https://bootstrap-vue.org/)) provides the visual styling. + +[Vite.js](https://vitejs.dev/) is used to bundle Vue code and other javascript dependencies for deployment and serve as a frontend dev server (which runs alongside django dev server) while testing or debugging. + +## Getting started + +### Installation +``` +npm install +``` + +### Compiles and hot-reloads for development +``` +npm run serve +``` + +### Compiles and minifies for production +``` +npm run build +``` + +### Testing + +**Tools used for testing:** +* [vitest](https://vitest.dev) - Used for unit testing (code without UI components) +* [cypress](https://docs.cypress.io) - Used for tests that contains (Vue) UI components +* [Vue test utils](https://vue-test-utils.vuejs.org) - Used for rendering vue component allows it to be mounted for unit testing. Officially recommended by Vue.js. + +* Tests for the frontend are all located in `/frontend/tests` folder. + * Unit test files should all be placed in `/frontend/tests/unit/` folder and have an extension `.spec.js`. + * Component test files should all be placed in `/frontend/tests/component` folder and have an extension `.cy.js` +* Test fixtures (data used in running the tests) are placed in `/examples` folder, this folder is shared with the integration test + +To run all frontend tests (unit and component tests): + +``` +npm run test +``` + +To run unit tests only: + +``` +npm run test:unit +``` + +To run component test only: + +``` +npm run test:component +``` + +## Notes when coming from the previous version <=2.0.0 + +- The `@` alias can still be used when doing module imports but file extensions should now be used when importing `.vue` files e.g. + - Before: `import DeleteModal from "@/components/DeleteModal" + - Now: `import DeleteModal from "@/components/DeleteModal.vue"` +- For code that is intended to run on the browser, e.g. in all `.vue` files, imports should use the ES 6 compliant `import` command and not node/commonjs's `require` + - **Exceptions for code that is run directly by node**, e.g. scripts used in the build chain, config files and test files used by build tools that run on node (e.g. vuepress or cypress) + + +## Explantion of the frontend + +### Vue and Vite + +Instead of separating html, css and javascript files, Vue has its own `single-file component` format normally with `.vue` extension ([reason why this file format is used](https://vuejs.org/guide/scaling-up/sfc.html)). Here is an example `.vue` file: + +```vue + + + + + +``` + +This means that `.vue` files cannot be directly imported into a standard html page. A tool has to be used for converting `.vue` file into standard javascript and/or css files, this is where [Vite.js](https://vitejs.dev/) comes in. + +[Vite.js](https://vitejs.dev/) is a tool that, amongst many other things, provides a dev server allowing hot module replacement (ability to immediately see changes in the UI during development) and bundling of javascript modules and other resources (css, images, etc.) i.e. not having to individually import each javascript and their dependencies from the main page. A [Vue plugin](https://github.com/vitejs/vite-plugin-vue2) is used to automatically convert `.vue` files into plain javascript as part of the bundling process. + +### App entrypoint (main.js) and routing + +The application's main entrypoint is `/frontend/src/main.js` which loads dependencies like Vue, Bootstrap Vue as well as loading the main component `AnnotationApp.vue` into a html page that contains a `
` tag. + +The `AnnotationApp.vue` component contains the special `` tag ([vue router](https://router.vuejs.org/)) which allows us to map url paths to specific vue components. The routing configuration can be found in `/frontend/src/router/index.js`, for example: + +```js +const routes = [ + { + path: '/', + name: 'Home', + component: Home, + meta: {guest: true}, + }, +... +``` + +The route shown above maps the root path e.g. `https://your-deployed-teamware-domain.com/` to the `Home.vue` component. Specifically, when pointing your browser to that path, the `Home.vue` component is inserted inside ``. + +### index.html, templates and bundling + +A html page is required to place our application in. Teamware uses Django to serve up the main html page which is located at `/frontend/templates/index.html` (see `MainView` class in `/backend/views.py`). This `index.html` page has to know where to load the generated javascript files. Where these files are differ depending on whether you're running the vite development server or using vite's statically built files. + +#### Using vite's development server (Django's `settings.FRONTEND_DEV_SERVER_USE` is `True`) +In during development we expect to be running the vite dev server alongside django server (when running `npm run serve` from the root of the project). In this case `index.html` imports javascript directly from the vite dev server: + +```html + + +``` + +This applies when running the `base`, `test` and `integration` django configurations. + +#### Using vite's statically built assets (Django's `settings.FRONTEND_DEV_SERVER_USE` is `false`) +When deploying the application, vite converts `.vue` files into plain javascript and bundles them to `/frontend/dist/static` directory. The `/frontend/src/main.js` becomes `/frontend/dist/static/assets/main-bb58d055.js`. The scripts are imported as static asset of going through the vite server, for example: + +```html + + +``` + +This applies when running the `deployment`, `docker-test` and `docker-integration` django configurations. + +#### index.html generation + +You may have noticed that a hash is added to the generated asset files (e.g. `main-bb58d055.js`) and this hash changes every time Vite builds the code. This means the `index.html` must also be re-generated after every Vite build as well. + +A simple build script which runs after every vite build `/frontend/build_template.js` performs this generation by taking the base template `/frontend/base_index.html`, merging it with Vite's generated manifest `/frontend/dist/manifest.json` and the output with the correct import path to `/frontend/templates/index.html`. + diff --git a/docs/versioned/2.2.0/developerguide/releases.md b/docs/versioned/2.2.0/developerguide/releases.md new file mode 100644 index 00000000..8a08d12b --- /dev/null +++ b/docs/versioned/2.2.0/developerguide/releases.md @@ -0,0 +1,18 @@ +# Managing Releases + +*These instructions are primarily intended for the maintainers of Teamware.* + +Note: Releases are always made from the `master` branch of the repository. + +## Steps to making a release + +1. **Update the changelog** - This has to be done manually, go through any pull requests to `dev` since the last release. + - In github pull requests page, use the search term `is:pr merged:>=yyyy-mm-dd` to find all merged PR from the date since the last version change. + - Include the changes in the `CHANGELOG.md` file; the changelog section _MUST_ begin with a level-two heading that starts with the relevant version number in square brackets (`## [N.M.P] Optional descriptive suffix`) as the GitHub workflow that creates a release from the eventual tag depends on this pattern to find the right release notes. Each main item within the changelog should have a link to the originating PR e.g. \[#123\](https://github.com/GateNLP/gate-teamware/pull/123). +1. **Update and check the version numbers** - from the teamware directory run `python version.py check` to check whether all version numbers are up to date. If not, update the master `VERSION` file and run `python version.py update` to update all other version numbers and commit the result. Alternatively, run `python version.py update ` where `` is the version number to update to, e.g. `python version.py update 2.1.0`. Note that `version.py` requires `pyyaml` for reading `CITATION.cff`, `pyyaml` is included in Teamware's dependencies. +1. **Create a version of the documentation** - Run `npm run docs:create_version`, this will archive the current version of the documentation using the version number in `package.json`. +1. **Create a pull request from `dev` to `master`** including any changes to `CHANGELOG.md`, `VERSION`. +1. **Create a tag** - Once the dev-to-master pull request has been merged, create a tag from the resulting `master` branch named `vN.M.P` (i.e. the new version number prefixed with the letter `v`). This will trigger two GitHub workflows: + - one that builds versioned Docker images for this release and pushes them to `ghcr.io`, updating the `latest` image tag to point to the new release + - one that creates a "release" on GitHub with the necessary artifacts to make the `https://gate.ac.uk/get-teamware.sh` installation mechanism work correctly. The release notes for this release will be generated by extracting the matching section from `CHANGELOG.md`. +1. **Update the Helm chart** - Create a new branch on [https://github.com/GateNLP/charts](https://github.com/GateNLP/charts) to update the `appVersion` of the `gate-teamware` Helm chart to match the version that was just created by the tag workflow. You must also update the chart `version`, bumping the major version number if the new chart is not backwards-compatible with the old. Submit a pull request to the `main` branch, which will publish the new chart when it is merged. diff --git a/docs/versioned/2.2.0/developerguide/testing.md b/docs/versioned/2.2.0/developerguide/testing.md new file mode 100644 index 00000000..578501b2 --- /dev/null +++ b/docs/versioned/2.2.0/developerguide/testing.md @@ -0,0 +1,182 @@ +# Testing +All the tests can be run using the following command: + +```bash +npm run test +``` + +## Backend Testing +Pytest is used for testing the backend. + +```bash +npm run test:backend +``` + +### Backend test files + +* Unit test files are located in `/backend/tests` + +## Frontend testing +[Jest](https://jestjs.io/) is used for frontend testing. +The [Vue testing-library](https://testing-library.com/docs/vue-testing-library/intro/) is used for testing +Vue components. + +```bash +npm run test:frontend +``` + +### Frontend test files + +* Frontend test files are located in `/fontend/tests/unit` and should the extension `.spec.js` + +### Testing JS functions + +```javascript +describe("Description of a group of tests to be run", () =>{ + + beforeAll(() =>{ + //The code here is run before each test + }) + + it("A single test's description", async () =>{ + + // Assertions are done with the expect() function e.g. + let funcOutput = 30 + 10 + expect(funcOutput).toBe(40) + + + }) +}) + +``` + +### Mocking JS classes + +This is an example of a mock harness for the JRPCClient class. + +A mock file is created inside a ``__mock__`` directory placed next to the file that's being mocked, e.g. +for our JRPCClient class at `/frontend/src/jrpc/index.js`, the mock file is `/frontend/src/jrpc/__mock__/index.js`. + + +Inside the mock file `/frontend/src/jrpc/__mock__/index.js`: +```javascript +// Mocking jrpc/index.js +//Mocking the JRPCClient class +//Replacing the call function with a custom mockCall function +export const mockCall = jest.fn(()=> 30); +const mock = jest.fn().mockImplementation(() => { + return {call: mockCall}; +}); + +export default mock; +``` + + +Inside the test file `*.spec.js`: +```javascript +import JRPCClient from "@/jrpc"; +jest.mock('@/jrpc') + +import store from '@/store' +//Example on how to mock the jrpc call + +describe("Vuex functions testing", () =>{ + + beforeAll(() =>{ + + //Re-implement custom mock call implementation if needed + JRPCClient.mockImplementation(()=>{ + return { + call(){ + return 50 + } + } + }) + + }) + + it("testfunc", async () =>{ + + const noutput = await store.dispatch("testnormal") + expect(noutput).toBe("Hello world") + + const aoutput = await store.dispatch("testasync") + expect(aoutput).toBe("Hello world") + + const rpc = new JRPCClient("/") + const result = await rpc.call("some param") + expect(result).toBe(50) + + }) +}) +``` + +### Testing Vue components + + +```javascript +//Example of how a component could be tested +import { render, fireEvent } from '@testing-library/vue' + + +import HelloWorld from '@/components/HelloWorld.vue' + +//Testing a component e.g. HelloWorld +describe('HelloWorld.vue', () => { + + it('renders props.msg when passed', () => { + const msg = 'new message' + const { getByText } = render(HelloWorld) + + getByText("Installed CLI Plugins") + }) +}) + +``` + + +## Integration testing +[Cypress](https://www.cypress.io/) is used for integration testing. + +The integration settings are located at `teamware/settings/integration.py` + +To run the integration test: +```bash +npm run test:integration +``` + +The test can also be run in **interactive mode** using: + +```bash +npm run serve:cypressintegration +``` + +### Integration test files +Files related to integration testing are located in `/cypress` + +* Test files are located in the `/cypress/integration` directory and should have the extension `.spec.js`. + +### Re-seeding the database + +The command `npm run migrate:integration` resets the database and performs migration, use with `beforeEach` to run it +before every test case in a suite: + +```js +describe('Example test suite', () => { + + beforeEach(() => { + // Resets the database every time before + // the test is run + cy.exec('npm run migrate:integration') + }) + + it('Test case 1', () => { + // Test something + }) + + it('Test case 2', () => { + // Test something + }) +}) +``` + diff --git a/docs/versioned/2.2.0/img/gate-teamware-logo.svg b/docs/versioned/2.2.0/img/gate-teamware-logo.svg new file mode 100644 index 00000000..12385947 --- /dev/null +++ b/docs/versioned/2.2.0/img/gate-teamware-logo.svg @@ -0,0 +1,79 @@ + + + + diff --git a/docs/versioned/2.2.0/manageradminguide/README.md b/docs/versioned/2.2.0/manageradminguide/README.md new file mode 100644 index 00000000..7a70a19f --- /dev/null +++ b/docs/versioned/2.2.0/manageradminguide/README.md @@ -0,0 +1,45 @@ +# GATE Teamware Overview + +## User roles + +There are three types of users in GATE Teamware, [annotators](#annotators), [managers](#managers) +and [admins](#admins). + +### Annotators + +Annotator is the default role when signing up to Teamware. An annotator can be recruited into +annotation projects and annotate documents. + + +### Managers + +Managers can create, view and modify annotation projects. They can also recruit annotators to a project. + +### Admins + +Admins, on top of what managers can do, they can also manage the users in the system and elevate them as +managers or admins. + +## Annotation Projects, Documents and Annotations + +Projects, documents and annotations form the core of the application. + +### Projects + +An annotation project contains a configuration of how annotations are to be captured, the documents and its +annotations and the recruited annotators. + + +### Documents + +A document in application refers to an individual set of arbitrary text that's to be annotated. A document +is stored as arbitrary JSON object and can represent various things such as, a single post (e.g. a tweet +or a post from reddit), a pair of source post and reply or a part of a HTML web page. + + +### Annotations + +An annotation represents a single annotation task against a single document. Like the document, +an annotation is stored as an arbitrary JSON object and can have any arbitrary structure. + + diff --git a/docs/versioned/2.2.0/manageradminguide/annotators_management.md b/docs/versioned/2.2.0/manageradminguide/annotators_management.md new file mode 100644 index 00000000..cb4c79b0 --- /dev/null +++ b/docs/versioned/2.2.0/manageradminguide/annotators_management.md @@ -0,0 +1,13 @@ +# Annotators management + +The **Annotators** tab in the **Project management** page allows the viewing and management of annotators in the project. + +Add annotators to the project by clicking on the list of names in the right column. Current annotators +can be removed by clicking on the names in the left column. Removing annotators does not delete their +completed annotations but will stop their current pending annotation task. + +An annotator can only be recruited into **one project at a time**. + +Once an annotator has annotated a proportion of documents in the project (specified in project configuration), they will +be deemed to have completed all their annotation tasks and automatically be removed the project. This frees them to be +recruited in another project. diff --git a/docs/versioned/2.2.0/manageradminguide/config_examples.js b/docs/versioned/2.2.0/manageradminguide/config_examples.js new file mode 100644 index 00000000..d6e40454 --- /dev/null +++ b/docs/versioned/2.2.0/manageradminguide/config_examples.js @@ -0,0 +1,332 @@ +export default { + config1: [ + { + "name": "htmldisplay", + "type": "html", + "text": "{{{text}}}" + }, + { + "name": "sentiment", + "type": "radio", + "title": "Sentiment", + "description": "Please select a sentiment of the text above.", + "options": [ + {"value": "negative", "label": "Negative"}, + {"value": "neutral", "label": "Neutral"}, + {"value": "positive", "label": "Positive"} + ] + } + ], + config2: [ + { + "name": "htmldisplay", + "type": "html", + "text": "{{{text}}}" + }, + { + "name": "sentiment", + "type": "radio", + "title": "Sentiment", + "description": "Please select a sentiment of the text above.", + "options": [ + {"value": "negative", "label": "Negative"}, + {"value": "neutral", "label": "Neutral"}, + {"value": "positive", "label": "Positive"} + ] + }, + { + "name": "opinion", + "type": "text", + "title": "What's your opinion of the above text?", + "optional": true + } + ], + configDisplay: [ + { + "name": "htmldisplay", + "type": "html", + "text": "{{{text}}}" + } + ], + configDisplayHtmlNoHtml: [ + { + "name": "htmldisplay", + "type": "html", + "text": "No HTML: {{text}}
HTML: {{{text}}}" + } + ], + configDisplayCustomFieldnames: [ + { + "name": "htmldisplay", + "type": "html", + "text": "Custom field: {{customField}}
Another custom field: {{{anotherCustomField}}}
Subfield: {{{subfield.subfieldContent}}}" + } + ], + configDisplayPreserveNewlines: [ + { + "name": "htmldisplay", + "type": "html", + "text": "
{{text}}
" + } + ], + configTextInput: [ + { + "name": "mylabel", + "type": "text", + "optional": true, //Optional - Set if validation is not required + "regex": "regex string", //Optional - When specified, the regex pattern will used to validate the text + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message then field is validated", //Optional + "valError": "Error message when field fails is validation" //Optional + } + ], + configTextarea: [ + { + "name": "mylabel", + "type": "textarea", + "optional": true, //Optional - Set if validation is not required + "regex": "regex string", //Optional - When specified, the regex pattern will used to validate the text + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message then field is validated", //Optional + "valError": "Error message when field fails is validation" //Optional + } + ], + configRadio: [ + { + "name": "mylabel", + "type": "radio", + "optional": true, //Optional - Set if validation is not required + "orientation": "vertical", //Optional - default is "horizontal" + "options": [ // The options that the user is able to select from + {"value": "value1", "label": "Text to show user 1"}, + {"value": "value2", "label": "Text to show user 2"}, + {"value": "value3", "label": "Text to show user 3"} + ], + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message then field is validated", //Optional + "valError": "Error message when field fails is validation" //Optional + } + ], + configRadioHelpText: [ + { + "name": "mylabel", + "type": "radio", + "optional": true, //Optional - Set if validation is not required + "orientation": "vertical", //Optional - default is "horizontal" + "options": [ // The options that the user is able to select from + {"value": "value1", "label": "Text to show user 1", "helptext": "Additional help text for option 1"}, + {"value": "value2", "label": "Text to show user 2", "helptext": "Additional help text for option 2"}, + {"value": "value3", "label": "Text to show user 3"} + ], + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message when the field is validated", //Optional + "valError": "Error message when the field fails validation" //Optional + } + ], + configCheckbox: [ + { + "name": "mylabel", + "type": "checkbox", + "optional": true, //Optional - Set if validation is not required + "orientation": "horizontal", //Optional - "horizontal" (default) or "vertical" + "options": [ // The options that the user is able to select from + {"value": "value1", "label": "Text to show user 1"}, + {"value": "value2", "label": "Text to show user 2"}, + {"value": "value3", "label": "Text to show user 3"} + ], + "minSelected": 1, //Optional - Specify the minimum number of options that must be selected + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message then field is validated", //Optional + "valError": "Error message when field fails is validation" //Optional + } + ], + configSelector: [ + { + "name": "mylabel", + "type": "selector", + "optional": true, //Optional - Set if validation is not required + "options": [ // The options that the user is able to select from + {"value": "value1", "label": "Text to show user 1"}, + {"value": "value2", "label": "Text to show user 2"}, + {"value": "value3", "label": "Text to show user 3"} + ], + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message then field is validated", //Optional + "valError": "Error message when field fails is validation" //Optional + } + ], + configRadioDict: [ + { + "name": "mylabel", + "type": "radio", + "optional": true, //Optional - Set if validation is not required + "options": { // The options can be specified as a dictionary, ordering is not guaranteed + "value1": "Text to show user 1", + "value2": "Text to show user 2", + "value3": "Text to show user 3", + }, + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message then field is validated", //Optional + "valError": "Error message when field fails is validation" //Optional + } + ], + + configDbpediaExample: [ + { + "name": "uri", + "type": "radio", + "title": "Select the most appropriate URI", + "options":[ + {"fromDocument": "candidates"}, + {"value": "none", "label": "None of the above"}, + {"value": "unknown", "label": "Cannot be determined without more context"} + ] + } + ], + docDbpediaExample: { + "text": "President Bush visited the air base yesterday...", + "candidates": [ + { + "value": "http://dbpedia.org/resource/George_W._Bush", + "label": "George W. Bush (Jnr)" + }, + { + "value": "http://dbpedia.org/resource/George_H._W._Bush", + "label": "George H. W. Bush (Snr)" + } + ] + }, + + configConditional1: [ + { + "name": "uri", + "type": "radio", + "title": "Select the most appropriate URI", + "options":[ + {"fromDocument": "candidates"}, + {"value": "other", "label": "Other"} + ] + }, + { + "name": "otherValue", + "type": "text", + "title": "Please specify another value", + "if": "annotation.uri == 'other'", + "regex": "^(https?|urn):", + "valError": "Please specify a URI (starting http:, https: or urn:)" + } + ], + configConditional2: [ + { + "name": "htmldisplay", + "type": "html", + "text": "{{{text}}}" + }, + { + "name": "sentiment", + "type": "radio", + "title": "Sentiment", + "description": "Please select a sentiment of the text above.", + "options": [ + {"value": "negative", "label": "Negative"}, + {"value": "neutral", "label": "Neutral"}, + {"value": "positive", "label": "Positive"} + ] + }, + { + "name": "reason", + "type": "text", + "title": "Why do you disagree with the suggested value?", + "if": "annotation.sentiment !== document.preanno.sentiment" + } + ], + docsConditional2: [ + { + "text": "I love the thing!", + "preanno": { + "sentiment": "positive" + } + }, + { + "text": "I hate the thing!", + "preanno": { + "sentiment": "negative" + } + }, + { + "text": "The thing is ok, I guess...", + "preanno": { + "sentiment": "neutral" + } + } + ], + + + doc1: {text: "Sometext with html"}, + doc2: { + customField: "Content of custom field.", + anotherCustomField: "Content of another custom field.", + subfield: { + subfieldContent: "Content of a subfield." + } + }, + docPlainText: { + "text": "This is some text\n\nIt has line breaks that we want to preserve." + }, + configPreAnnotation: [ + { + "name": "htmldisplay", + "type": "html", + "text": "{{{text}}}" + }, + { + "name": "radio", + "type": "radio", + "title": "Test radio input", + "options": [ + {"value": "val1", "label": "Value 1"}, + {"value": "val2", "label": "Value 2"}, + {"value": "val3", "label": "Value 4"}, + {"value": "val4", "label": "Value 5"} + ], + "description": "Test radio description" + }, + { + "name": "checkbox", + "type": "checkbox", + "title": "Test checkbox input", + "options": [ + {"value": "val1", "label": "Value 1"}, + {"value": "val2", "label": "Value 2"}, + {"value": "val3", "label": "Value 4"}, + {"value": "val4", "label": "Value 5"} + ], + "description": "Test checkbox description" + }, + { + "name": "text", + "type": "text", + "title": "Test text input", + "description": "Test text description" + } + + ], + docPreAnnotation: { + "id": 12345, + "text": "Example document text", + "preannotation": { + "radio": "val1", + "checkbox": ["val1", "val3"], + "text": "Pre-annotation text value" + } + } + + +} diff --git a/docs/versioned/2.2.0/manageradminguide/documents_annotations_management.md b/docs/versioned/2.2.0/manageradminguide/documents_annotations_management.md new file mode 100644 index 00000000..7b852340 --- /dev/null +++ b/docs/versioned/2.2.0/manageradminguide/documents_annotations_management.md @@ -0,0 +1,296 @@ +# Documents & Annotations + +The **Documents & Annotations** tab in the **Project management** page allows the viewing and management of documents +and annotations related to the project. + +## Document & Annotation status + +### Annotation status + +Annotations can be in 1 of 5 states: + +* Annotation is completed - The annotator has completed this annotation task. +* Annotation is rejected - The annotator has chosen to not annotate the document. +* Annotation is timed out - The annotation task was not completed within the time specified in the project's configuration. The task is freed and can be assigned to another annotator. +* Annotation is aborted - The annotation task was aborted due to reasons other than timing out, such as when an annotator with a pending task is removed from a project. +* Annotation is pending - The annotator has started the annotation task but has not completed it. + +### Document status + +Documents also display a list of its current annotation status: + +* 1 - Number of completed annotations in the document. +* 1 - Number of rejected annotations in the document. +* 1 - Number of timed out annotations in the document. +* 1 - Number of aborted annotations in the document. +* 1 - Number of pending annotations in the document. + +## Importing documents + +Documents can be imported using the **Import** button. The supported file types are: + +* `.json` - The app expects a list of documents (represented as a dictionary object) + e.g. `[{"id": 1, "text": "Text1"}, ...]`. +* `.jsonl` - The app expects one document (represented as a dictionary object) per line. +* `.csv` - File must have a header row. It will be internally converted to JSON format. +* `.zip` - Can contain any number of `.json,.jsonl and .csv` files inside. + +### Importing documents with pre-annotation + +In the `Project Configurations` page, it is possible to set a field in which Teamware will look for pre-annotation. If +the field is found inside the document then the annotation form will be pre-filled with data provided in the document. + +The format for pre-annotation is exactly the same as the annotation output. You can see an example of generated +annotation by filling out the form in the `Annotation Preview` and observing the values in +the `Annotation Output Preview`. + + +For an example project configuration shown below, there are three captured labels named `radio`, `checkbox` and `text`: + +```json +[ + { + "name": "htmldisplay", + "type": "html", + "text": "{{{text}}}" + }, + { + "name": "radio", + "type": "radio", + "title": "Test radio input", + "options": [ + {"value": "val1", "label": "Value 1"}, + {"value": "val2", "label": "Value 2"}, + {"value": "val3", "label": "Value 4"}, + {"value": "val4", "label": "Value 5"} + ], + "description": "Test radio description" + }, + { + "name": "checkbox", + "type": "checkbox", + "title": "Test checkbox input", + "options": [ + {"value": "val1", "label": "Value 1"}, + {"value": "val2", "label": "Value 2"}, + {"value": "val3", "label": "Value 4"}, + {"value": "val4", "label": "Value 5"} + ], + "description": "Test checkbox description" + }, + { + "name": "text", + "type": "text", + "title": "Test text input", + "description": "Test text description" + } +] +``` + +On the `Project Configuration` page, if the `Pre-annotation` field is set to `preannotation`, the annotation form will be pre-filled with +the content provided in the `preannotation` field of the document e.g.: + +```json +{ + "id": 12345, + "text": "Example document text", + "preannotation": { + "radio": "val1", + "checkbox": [ + "val1", + "val3" + ], + "text": "Pre-annotation text value" + } +} +``` + +The example of the pre-filled form can be seen by clicking on the `Preview` tab above. + + + + + + +### Importing Training and Test documents + +When importing documents for the training and testing phase, Teamware expects a field/column (called `gold` by default) +that contains the correct annotation response for each label and, only for training documents, an explanation. + +For example, if we're expecting a multi-choice label for doing sentiment classification with a widget named `sentiment` +and choice of `postive`, `negative` and `neutrual`: + +```js +[ + { + "text": "What's my sentiment", + "gold": { + "sentiment": { + "value": "positive", // For this document, the correct value is postive + "explanation": "Because..." // Explanation is only given in the traiing phase and are optional in the test documents + } + } + } +] +``` + +in csv: + +| text | gold.sentiment.value | gold.sentiment.explanation | +| --- | --- | --- | +| What's my sentiment | positive | Because... | + +### Guidance on CSV column headings + +It is recommended that: + +* Spaces are not used in column headings, use dash (`-`), underscore (`_`) or camel case (e.g. fieldName) instead. +* The dot/full stop (`.`) is used to indicate hierarchical information so don't use it if that's not what's intended. + Explanation on this feature is given below. + +Documents imported from a CSV files are converted to JSON for use internally in Teamware, the reverse is true when +converting back to CSV. To allow a CSV to represent a hierarchical structure, a dot notation is used to indicate a +sub-field. + +In the following example, we can see that `gold` has a child field named `sentiment` which then has a child field +named `value`: + +| text | gold.sentiment.value | gold.sentiment.explanation | +| --- | --- | --- | +| What's my sentiment | positive | Because... | + +The above column headers will generate the following JSON: + +```js +[ + { + "text": "What's my sentiment", + "gold": { + "sentiment": { + "value": "positive", // For this document, the correct value is postive + "explanation": "Because..." // Explanation is only given in the traiing phase and are optional in the test documents + } + } + } +] +``` + +## Exporting documents + +Documents and annotations can be exported using the **Export** button. A zip file is generated containing files with 500 +documents each. The option to "anonymize annotators" controls whether the individual annotators are identified with +their numeric ID or by their actual username - since usernames are often personally identifiable information (e.g. an +email address) the anonumous mode is recommended if you intend to share the annotation data with third parties. Note +that the anonymous IDs are consistent within a single installation of Teamware, so even in anonymous mode it is still +possible to determine which documents were annotated by _the same person_, just not who that person was. + +You can choose how documents are exported: + +* `.json` & `.jsonl` - JSON or JSON Lines files can be generated in the format of: + * `raw` - Exports the original `JSON` combined with an additional field named `annotation_sets` for storing + annotations. The annotations are laid out in the same way as GATE + [bdocjs](https://gatenlp.github.io/gateplugin-Format_Bdoc/bdoc_document.html) format. For example if a document + has been annotated by `user1` with labels and values `text`:`Annotation text`, `radio`:`val3`, and + `checkbox`:`["val2", "val4"]`, the non-anonymous export might look like this: + + ```json + { + "id": 32, + "text": "Document text", + "text2": "Document text 2", + "feature1": "Feature text", + "annotation_sets":{ + "user1":{ + "name":"user1", + "annotations":[ + { + "type":"Document", + "start":0, + "end":10, + "id":0, + "features":{ + "text":"Annotation text", + "radio":"val3", + "checkbox":[ + "val2", + "val4" + ] + } + } + ], + "next_annid":1 + } + }, + "teamware_status": { + "rejected_by": ["user2"], + "timed_out": ["user3"], + "aborted": [] + } + } + ``` + + In anonymous mode the name `user1` would instead be derived from the user's opaque numeric identifier (e.g. + `annotator105`). + + The field `teamware_status` gives the usernames or anonymous IDs (depending on the "anonymize" setting) of those annotators + who rejected the document, "timed out" because they did not complete their annotation in the time allowed by the + project, or "aborted" for some other reason (e.g. they were removed from the project). + + * `gate` - Convert documents to GATE [bdocjs](https://gatenlp.github.io/gateplugin-Format_Bdoc/bdoc_document.html) + format and export. A `name` field is added that takes the `ID` value from the `ID field` specified in the + **project configuration**. Any top-level fields apart from `text`, `features`, `offset_type`, `annotation_sets`, + and the ID field specified in the project config are placed in the `features` field, as is the `teamware_status` + information. An `annotation_sets` field is added for storing annotations if it doesn't already exist. + + For example in the case of this uploaded JSON document: + ```json + { + "id": 32, + "text": "Document text", + "text2": "Document text 2", + "feature1": "Feature text" + } + ``` + The generated output is as follows. The annotations and `teamware_status` are formatted same as the `raw` output + above: + ```json + { + "name": 32, + "text": "Document text", + "features": { + "text2": "Document text 2", + "feature1": "Feature text", + "teamware_status": {...} + }, + "offset_type":"p", + "annotation_sets": {...} + } + ``` +* `.csv` - The JSON documents will be flattened to csv's column based format. Annotations are added as additional + columns with the header of `annotations.username.label` and the status information is in columns named + `teamware_status.rejected_by`, `teamware_status.timed_out` and `teamware_status.aborted`. + +**Note: Documents that contains existing annotations (i.e. the `annotation_sets` field for `JSON` or `annotations` for `CSV`) are merged with the new sets of annotations. Be aware that if the document has a new annotation from an annotator with the same +username, the previous annotation will be overwritten. Existing annotations are also not anonymized when exporting the document.** + +## Deleting documents and annotations + +It is possible to click on the top left of corner of documents and annotations to select it, then click on the +**Delete** button to delete them. + +::: tip + +Selecting a document also selects all its associated annotations. + +::: + + + diff --git a/docs/versioned/2.2.0/manageradminguide/project_config.md b/docs/versioned/2.2.0/manageradminguide/project_config.md new file mode 100644 index 00000000..d3d4ea3d --- /dev/null +++ b/docs/versioned/2.2.0/manageradminguide/project_config.md @@ -0,0 +1,684 @@ +--- +sidebarDepth: 3 +--- + +# Project configuration + +The **Configuration** tab in the **Project management** page allows you to change project settings including what +annotations are captured. + +Project configurations can be imported and exported in the format of a JSON file. + +The project can be also be cloned (have configurations copied to a new project). Note that cloning does not copy +documents, annotations or annotators to the new project. + +## Configuration fields + +* **Name** - The name of this annotation project. +* **Description** - The description of this annotation project that will be shown to annotators. Supports markdown and + HTML. +* **Annotator guideline** - The description of this annotation project that will be shown to annotators. Supports + markdown and HTML. +* **Annotations per document** - The project completes when each document in this annotation project have this many + number of valid annotations. When a project completes, all project annotators will be un-recruited and be allowed to + annotate other projects. +* **Maximum proportion of documents annotated per annotator (between 0 and 1)** - A single annotator cannot annotate + more than this proportion of documents. +* **Timeout for pending annotation tasks (minutes)** - Specify the number of minutes a user has to complete an + annotation task (i.e. annotating a single document). +* **Reject documents** - Switching this off will mean that annotators for this project will be unable to choose to reject documents. +* **Document ID field** - The field in your uploaded documents that is used as a unique identifier. GATE's json format + uses the name field. You can use a dot limited key path to access subfields e.g. enter features.name to get the id + from the object `{'features':{'name':'nameValue'}}` +* **Training stage enable/disable** - Enable or disable training stage, allows testing documents to be uploaded to the project. +* **Test stage enable/disable** - Enable or disable testing stage, allows test documents to be uploaded to the project. +* **Auto elevate to annotator** - The option works in combination with the training and test stage options, see table below for the behaviour: + + | Training stage | Testing stage | Auto elevate to annotator | Desciption | + | --- | --- | --- | --- | + | Disabled | Disabled | Enabled/Disabled | User allowed to annotate without manual approval. | + | Enabled | Disabled | Disabled | Manual approval required. | + | Disabled | Enabled | Disabled | " | + | Enabled | Disabled | Enabled | User always allowed to annotate after training phase completed | + | Disabled | Enabled | Enabled | User automatically allowed to annotate after passing test, if user fails test they have to be manually approved. | + | Enabled | Enabled | Enabled | " | + +* **Test pass proportion** - The proportion of correct test annotations to be automatically allowed to annotate documents. +* **Gold standard field** - The field in document's JSON/column that contains the ideal annotation values and explanation for the annotation. +* **Pre-annotation** - Pre-fill the form with annotation provided in the specified field. See [Importing Documents with pre-annotation](./documents_annotations_management.md#importing-documents-with-pre-annotation) section for more detail. + +## Annotation configuration + +The annotation configuration takes a `json` string for configuring how the document is displayed to the user and types +of annotation will be collected. Here's an example configuration and a preview of how it is shown to annotators: + + + + +```json +// Example configuration +[ + { + "name": "htmldisplay", + "type": "html", + "text": "{{{text}}}" + }, + { + "name": "sentiment", + "type": "radio", + "title": "Sentiment", + "description": "Please select a sentiment of the text above.", + "options": [ + {"value": "negative", "label": "Negative"}, + {"value": "neutral", "label": "Neutral"}, + {"value": "positive", "label": "Positive"} + ] + } +] +``` + + + +Within the configuration, it is possible to specify how your documents will be displayed. The **Document input preview** +box can be used to provide a sample of your document for rendering of the preview. + +```json +// Example contents for the Document input preview +{ + "text": "Sometext with html" +} +``` + + + +The above configuration displays the value from the `text` field from the document to be annotated. It then shows a set +of 3 radio inputs that allows the user to select a Negative, Neutral, or Positive sentiment with the label +name `sentiment`. + + + +All fields **require** the properties **name** and **type**, it is used to name our label and determine the type of +input/display to be shown to the user respectively. + +Another field can be added to collect more information, e.g. a text field for opinions: + + + +```json +[ + { + "name": "htmldisplay", + "type": "html", + "text": "{{{text}}}" + }, + { + "name": "sentiment", + "type": "radio", + "title": "Sentiment", + "description": "Please select a sentiment of the text above.", + "options": [ + {"value": "negative", "label": "Negative"}, + {"value": "neutral", "label": "Neutral"}, + {"value": "positive", "label": "Positive"} + ] + }, + { + "name": "opinion", + "type": "text", + "title": "What's your opinion of the above text?", + "optional": true + } + +] +``` + + + +Note that for the above case, the `optional` field is added ensure that allows user to not have to input any value. +This `optional` field can be used on all components. Any component may optionally have a field named `if`, containing an expression that is used to determine whether or not the component appears based on information in the document and/or the values entered in the other components. For example the user could be presented with a set of options that includes an "other" choice, and if the annotator chooses "other" then an additional free text field appears for them to fill in. The `if` option is described in more detail under the [conditional components](#conditional-components) section below. + +Some fields are available to configure which are specific to components, e.g. the `options` field are only available for +the `radio`, `checkbox` and `selector` components. See details below on the usage of each specific component. + +The captured annotation results in a JSON dictionary, an example can be seen in the **Annotation output preview** box. +The annotation is linked to a Document and is converted to a GATE JSON annotation format when exported. + +### Displaying text + + + +```json +[ + { + "name": "htmldisplay", + "type": "html", + "text": "{{{text}}}" // The text that will be displayed + } +] +``` + + + +The `htmldisplay` widget allows you to display the text you want annotated. It accepts almost full range of HTML +input which gives full styling flexibility. + +Any field/column from the document can be inserted by surrounding a field/column name with double or +triple curly brackets. Double curly brackets renders text as-is and triple curly brackets accepts HTML string: + + + +Input: + +```json +{ + "text": "Sometext with html" +} +``` + +Configuration, showing the same field/column in document as-is or as HTML: +```json +[ + { + "name": "htmldisplay", + "type": "html", + "text": "No HTML: {{text}}
HTML: {{{text}}}" + } +] +``` + +
+ +The widget makes no assumption about your document structure and any field/column names can be used, +even sub-fields by using the dot notation e.g. `parentField.childField`: + + + +JSON input: + +```json +{ + "customField": "Content of custom field.", + "anotherCustomField": "Content of another custom field.", + "subfield": { + "subfieldContent": "Content of a subfield." + } +} +``` + +or in csv + +| customField | anotherCustomField | subfield.subfieldContent | +| --- | --- | --- | +| Content of custom field. | Content of another custom field. | Content of a subfield. | + + +Configuration, showing the same field/column in document as-is or as HTML: +```json +[ + { + "name": "htmldisplay", + "type": "html", + "text": "Custom field: {{customField}}
Another custom field: {{{anotherCustomField}}}
Subfield: {{{subfield.subfieldContent}}}" + } +] +``` + +
+ +If your documents are plain text and include line breaks that need to be preserved when rendering, this can be achieved by using a special HTML wrapper which sets the [`white-space` CSS property](https://developer.mozilla.org/en-US/docs/Web/CSS/white-space). + + + +**Document** + +```json +{ + "text": "This is some text\n\nIt has line breaks that we want to preserve." +} +``` + +**Project configuration** + +```json +[ + { + "name": "htmldisplay", + "type": "html", + "text": "
{{text}}
" + } +] +``` + +
+ +`white-space: pre-line` preserves line breaks but collapses other whitespace down to a single space, `white-space: pre-wrap` would preserve all whitespace including indentation at the start of a line, but would still wrap lines that are too long for the available space. + +### Text input + + + +```json +[ + { + "name": "mylabel", + "type": "text", + "optional": true, //Optional - Set if validation is not required + "regex": "regex string", //Optional - When specified, the regex pattern will used to validate the text + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message when the field is validated", //Optional + "valError": "Error message when the field fails validation" //Optional + } +] +``` + + + +### Textarea input + + + +```json +[ + { + "name": "mylabel", + "type": "textarea", + "optional": true, //Optional - Set if validation is not required + "regex": "regex string", //Optional - When specified, the regex pattern will used to validate the text + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message when the field is validated", //Optional + "valError": "Error message when the field fails validation" //Optional + } +] +``` + + + +### Radio input + + + +```json +[ + { + "name": "mylabel", + "type": "radio", + "optional": true, //Optional - Set if validation is not required + "orientation": "vertical", //Optional - default is "horizontal" + "options": [ // The options that the user is able to select from + {"value": "value1", "label": "Text to show user 1"}, + {"value": "value2", "label": "Text to show user 2"}, + {"value": "value3", "label": "Text to show user 3"} + ], + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message when the field is validated", //Optional + "valError": "Error message when the field fails validation" //Optional + } +] +``` + + + +### Checkbox input + + + +```json +[ + { + "name": "mylabel", + "type": "checkbox", + "optional": true, //Optional - Set if validation is not required + "orientation": "horizontal", //Optional - "horizontal" (default) or "vertical" + "options": [ // The options that the user is able to select from + {"value": "value1", "label": "Text to show user 1"}, + {"value": "value2", "label": "Text to show user 2"}, + {"value": "value3", "label": "Text to show user 3"} + ], + "minSelected": 1, //Optional - Overrides optional field. Specify the minimum number of options that must be selected + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message when the field is validated", //Optional + "valError": "Error message when the field fails validation" //Optional + } +] +``` + + + +### Selector input + + + +```json +[ + { + "name": "mylabel", + "type": "selector", + "optional": true, //Optional - Set if validation is not required + "options": [ // The options that the user is able to select from + {"value": "value1", "label": "Text to show user 1"}, + {"value": "value2", "label": "Text to show user 2"}, + {"value": "value3", "label": "Text to show user 3"} + ], + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message when the field is validated", //Optional + "valError": "Error message when the field fails validation" //Optional + } +] +``` + + + +### Optional help text + +Optionally, radio buttons and checkboxes can be given help text to provide additional per-choice context or information to help annotators. + + + + +```json +[ + { + "name": "mylabel", + "type": "radio", + "optional": true, //Optional - Set if validation is not required + "orientation": "vertical", //Optional - default is "horizontal" + "options": [ // The options that the user is able to select from + {"value": "value1", "label": "Text to show user 1", "helptext": "Additional help text for option 1"}, + {"value": "value2", "label": "Text to show user 2", "helptext": "Additional help text for option 2"}, + {"value": "value3", "label": "Text to show user 3"} + ], + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message when the field is validated", //Optional + "valError": "Error message when the field fails validation" //Optional + } +] +``` + + + +### Alternative way to provide options for radio, checkbox and selector + +A dictionary (key value pairs) and also be provided to the `options` field of the radio, checkbox and selector widgets +but note that the ordering of the options are **not guaranteed** as javascript does not sort dictionaries by +the order in which keys are added. Note that additional help texts for radio buttons and checkboxes are not supported using this syntax. + + + +```json +[ + { + "name": "mylabel", + "type": "radio", + "optional": true, //Optional - Set if validation is not required + "options": { // The options can be specified as a dictionary, ordering is not guaranteed + "value1": "Text to show user 1", + "value2": "Text to show user 2", + "value3": "Text to show user 3" + }, + "title": "Title string", //Optional + "description": "Description string", //Optional + "valSuccess": "Success message when the field is validated", //Optional + "valError": "Error message when the field fails validation" //Optional + } +] +``` + + + +### Dynamic options for radio, checkbox and selector + +All the examples above have a "static" list of available options for the radio, checkbox and selector widgets, where the complete options list is enumerated in the project configuration and every document offers the same set of options. However it is also possible to take some or all of the options from the _document_ data rather than the _configuration_ data. For example: + + + +**Project configuration** + +```json +[ + { + "name": "uri", + "type": "radio", + "title": "Select the most appropriate URI", + "options":[ + {"fromDocument": "candidates"}, + {"value": "none", "label": "None of the above"}, + {"value": "unknown", "label": "Cannot be determined without more context"} + ] + } +] +``` + +**Document** + +```json +{ + "text": "President Bush visited the air base yesterday...", + "candidates": [ + { + "value": "http://dbpedia.org/resource/George_W._Bush", + "label": "George W. Bush (Jnr)" + }, + { + "value": "http://dbpedia.org/resource/George_H._W._Bush", + "label": "George H. W. Bush (Snr)" + } + ] +} +``` + + + +`"fromDocument"` is a dot-separated property path leading to the location within each document where the additional options can be found, for example `"fromDocument":"candidates"` looks for a top-level property named `candidates` in each document, `"fromDocument": "options.custom"` would look for a property named `options` which is itself an object with a property named `custom`. The target property in the document may be in any of the following forms: + +- an array _of objects_, each with `value` and `label` (and optionally `helptext`) properties, exactly as in the static configuration format - this is the format used in the example above +- an array _of strings_, where the same string will be used as both the value and the label for that option +- an arbitrary ["dictionary"](#options-as-dict) object mapping values to labels +- a _single string_, which is parsed into a list of options + +The "single string" alternative is designed to be easier to use when [importing documents](documents_annotations_management.md#importing-documents) from CSV files. It allows you to provide any number of options in a _single_ CSV column value. Within the column the options are separated by semicolons, and each option is of the form `value=label`. Whitespace around the delimiters is ignored, both between options and between the value and label of a single option. For example given CSV document data of + +| text | options | +|-----------------|---------------------------------------------------| +| Favourite fruit | `apple=Apples; orange = Oranges; kiwi=Kiwi fruit` | + +a `{"fromDocument": "options"}` configuration would produce the equivalent of + +```json +[ + {"value": "apple", "label": "Apples"}, + {"value": "orange", "label": "Oranges"}, + {"value": "kiwi", "label": "Kiwi fruit"} +] +``` + +If your values or labels may need to contain the default separator characters `;` or `=` you can select different separators by adding extra properties to the configuration: + +```json +{"fromDocument": "options", "separator": "~~", "valueLabelSeparator": "::"} +``` + +| text | options | +|-----------------|------------------------------------------------------| +| Favourite fruit | `apple::Apples ~~ orange::Oranges ~~ kiwi::Kiwi fruit` | + +The separators can be more than one character, and you can set `"valueLabelSeparator":""` to disable label splitting altogether and just use the value as its own label. + +### Mixing static and dynamic options + +Static and `fromDocument` options may be freely interspersed in any order, so you can have a fully-dynamic set of options by specifying _only_ a `fromDocument` entry with no static options, or you can have static options that are listed first followed by dynamic options, or dynamic options first followed by static, etc. + +### Conditional components + +By default all components listed in the project configuration will be shown for all documents. However this is not always appropriate, for example you may have some components that are only relevant to certain documents, or only relevant for particular combinations of values in _other_ components. To allow for these kinds of scenarios any component can have a field named `if` specifying the conditions under which that component should be shown. + +The `if` field is an _expression_ that is able to refer to fields in both the current _document_ being annotated and the current state of the other annotation components. The expression language is largely based on a subset of the standard JavaScript expression syntax but with a few additional syntax elements to ease working with array data and regular expressions. + +The following simple example shows how you might implement an "Other (please specify)" pattern, where the user can select from a list of choices but also has the option to supply their own answer if none of the choices are appropriate. The free text field is only shown if the user selects the "other" choice. + + + +**Project configuration** + +```json +[ + { + "name": "uri", + "type": "radio", + "title": "Select the most appropriate URI", + "options":[ + {"fromDocument": "candidates"}, + {"value": "other", "label": "Other"} + ] + }, + { + "name": "otherValue", + "type": "text", + "title": "Please specify another value", + "if": "annotation.uri == 'other'", + "regex": "^(https?|urn):", + "valError": "Please specify a URI (starting http:, https: or urn:)" + } +] +``` + +**Document** + +```json +{ + "text": "President Bush visited the air base yesterday...", + "candidates": [ + { + "value": "http://dbpedia.org/resource/George_W._Bush", + "label": "George W. Bush (Jnr)" + }, + { + "value": "http://dbpedia.org/resource/George_H._W._Bush", + "label": "George H. W. Bush (Snr)" + } + ] +} +``` + + +Note that validation rules (such as `optional`, `minSelected` or `regex`) are not applied to components that are hidden by an `if` expression - hidden components will never be included in the annotation output, even if they would be considered "required" had they been visible. + +Components can also be made conditional on properties of the _document_, or a combination of the document and the annotation values, for example + + + +**Project configuration** + +```json +[ + { + "name": "htmldisplay", + "type": "html", + "text": "{{{text}}}" + }, + { + "name": "sentiment", + "type": "radio", + "title": "Sentiment", + "description": "Please select a sentiment of the text above.", + "options": [ + {"value": "negative", "label": "Negative"}, + {"value": "neutral", "label": "Neutral"}, + {"value": "positive", "label": "Positive"} + ] + }, + { + "name": "reason", + "type": "text", + "title": "Why do you disagree with the suggested value?", + "if": "annotation.sentiment !== document.preanno.sentiment" + } +] +``` + +**Documents** + +```json +[ + { + "text": "I love the thing!", + "preanno": { "sentiment": "positive" } + }, + { + "text": "I hate the thing!", + "preanno": { "sentiment": "negative" } + }, + { + "text": "The thing is ok, I guess...", + "preanno": { "sentiment": "neutral" } + } +] +``` + + + +The full list of supported constructions is as follows: + +- the `annotation` variable refers to the current state of the annotation components for this document + - the current value of a particular component can be accessed as `annotation.componentName` or `annotation['component name']` - the brackets version will always work, the dot version works if the component's `name` is a valid JavaScript identifier + - if a component has not been set since the form was last cleared the value may be `null` or `undefined` - the expression should be written to cope with both + - the value of a `text`, `textarea`, `radio` or `selector` component will be a single string (or null/undefined), the value of a `checkbox` component will be an _array_ of strings since more than one value may be selected. If no value is selected the array may be null, undefined or empty, the expression must be prepared to handle any of these +- the `document` variable refers to the current document that is being annotated + - again properties of the document can be accessed as `document.propertyName` or `document['property name']` + - continue the same pattern for nested properties e.g. `document.scores.label1` + - individual elements of array properties can be accessed by zero-based index (e.g. `document.options[0]`) +- various comparison operators are available: + - `==` and `!=` (equal and not-equal) + - `<`, `<=`, `>=`, `>` (less-than, less-or-equal, greater-or-equal, greater-than) + - these operators follow JavaScript rules, which are not always intuitive. Generally if both arguments are strings then they will be compared by lexicographic order, but if either argument is a number then the other one will also be converted to a number before comparing. So if the `score` component is set to the value "10" (a string of two digits) then `annotation.score < 5` would be _false_ (10 is converted to number and compared to 5) but `annotation.score < '5'` would be _true_ (the string "10" sorts before the string "5") + - `in` checks for the presence of an item in an array or a key in an object + - e.g. `'other' in annotation.someCheckbox` checks if the `other` option has been ticked in a checkbox component (whose value is an array) + - this is different from normal JavaScript rules, where `i in myArray` checks for the presence of an array _index_ rather than an array _item_ +- other operators + - `+` (concatenate strings, or add numbers) + - if either argument is a string then both sides are converted to strings and concatenated together + - otherwise both sides are treated as numbers and added + - `-`, `*`, `/`, `%` (subtraction, multiplication, division and remainder) + - `&&`, `||` (boolean AND and OR) + - `!` (prefix boolean NOT, e.g. `!annotation.selected` is true if `selected` is false/null/undefined and false otherwise) + - conditional operator `expr ? valueIfTrue : valueIfFalse` (exactly as in JavaScript, first evaluates the test `expr`, then either the `valueIfTrue` or `valueIfFalse` depending on the outcome of the test) +- `value =~ /regex/` tests whether the given string value contains any matches for the given [regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#writing_a_regular_expression_pattern) + - use `^` and/or `$` to anchor the match to the start and/or end of the value, for example `annotation.example =~ /^a/i` checks whether the `example` annotation value _starts with_ "a" or "A" (the `/i` flag makes the expression case-insensitive) + - since the project configuration is entered as JSON, any backslash characters within the regex must be doubled to escape them from the JSON parser, i.e. `"if": "annotation.option =~ /\\s/"` would check if `option` contains any space characters (for which the regular expression literal is `/\s/`) +- _Quantifier_ expressions let you check whether `any` or `all` of the items in an array or key/value pairs in an object match a predicate expression. The general form is `any(x in expr, predicate)` or `all(x in expr, predicate)`, where `expr` is an expression that resolves to an array or object value, `x` is a new identifier, and `predicate` is the expression to test each item against. The `predicate` expression can refer to the `x` identifier + - `any(option in annotation.someCheckbox, option > 3)` + - `all(e in document.scores, e.value < 0.7)` (assuming `scores` is an object mapping labels to scores, e.g. `{"scores": {"positive": 0.5, "negative": 0.3}}`) + - when testing a predicate against an _object_ each entry has `.key` and `.value` properties giving the key and value of the current entry + - on a null, undefined or empty array/object, `any` will return _false_ (since there are no items that pass the test) and `all` will return _true_ (since there are no items that _fail_ the test) + - the predicate is optional - `any(arrayExpression)` resolves to `true` if any item in the array has a value that JavaScript considers to be "truthy", i.e. anything other than the number 0, the empty string, null or undefined. So `any(annotation.myCheckbox)` is a convenient way to check whether _at least one_ option has been selected in a `checkbox` component. + +If the `if` expression for a particular component is _syntactically invalid_ (missing operands, mis-matched brackets, etc.) then the condition will be ignored and the component will always be displayed as though it did not have an `if` expression at all. Conversely, if the expression is valid but an error occurs while _evaluating_ it, this will be treated the same as if the expression returned `false`, and the associated component will not be displayed. The behaviour is this way around as the most common reason for errors during evaluation is attempting to refer to annotation components that have not yet been filled in - if this is not appropriate in your use case you must account for the possibility within your expression. For example, suppose `confidence` is a `radio` or `selector` component with values ranging from 1 to 5, then another component that declares + +``` +"if": "annotation.confidence && annotation.confidence < 4"` +``` + +will hide this component if `confidence` is unset, displaying it only if `confidence` is set to a value less than 4, whereas + +``` +"if": "!annotation.confidence || annotation.confidence < 4" +``` + +will hide this component only if `confidence` is actually _set_ to a value of 4 or greater - it will _show_ this component if `confidence` is unset. Either approach may be correct depending on your project's requirements. + +To assist managers in authoring project configurations with `if` conditions, the "preview" mode on the project configuration page will display details of any errors that occur when parsing the expressions, or when evaluating them against the **Document input preview** data. You are encouraged to test your expressions thoroughly against a variety of inputs to ensure they behave as intended, before opening your project to annotators. + + diff --git a/docs/versioned/2.2.0/manageradminguide/project_management.md b/docs/versioned/2.2.0/manageradminguide/project_management.md new file mode 100644 index 00000000..f1fd0cfe --- /dev/null +++ b/docs/versioned/2.2.0/manageradminguide/project_management.md @@ -0,0 +1,38 @@ +# Annotation Project Management + +## Project Listing +Clicking on the `Projects` link in the top navigation bar takes you to a contains a list of existing +projects. The project names are shown along with their summaries. Clicking on a project name will +take you to the project management page. + + +## Project Management Page + +The project management page contains all the functionalities to manage an annotation project. The page +is composed of three main tabs: + +* [Configuration](project_config.md) - Configure project settings including what annotations are captured. +* [Documents & Annotation](documents_annotations_management.md) - Manage documents and annotations. Upload documents, see contents of a document's annotations and import/export documents. +* [Annotators](annotators_management.md) - Manage the recruitment of annotators. + +::: warning + +Annotators can only be recruited to an annotation project after it has been configured and documents +are uploaded to the project. + +::: + + +## Project status icons +In the **Project listing** and **Project management page**, icon badges are used to provide a quick overview of the project's status: + +* 1 - Number of completed annotations in the project. +* 1 - Number of rejected annotations in the project. +* 1 - Number of timed out annotations in the project. +* 1 - Number of aborted annotations in the project. +* 1 - Number of pending annotations in the project. +* 2/60 - Number of occupied annotation tasks over number of total tasks in the project. +* 20/5/10 - Number of documents, training documents and test documents in the project. +* 1 - Number of annotators recruited in the project. Annotators are removed from the project when they have completed all annotation tasks in their quota. + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 07f84b4f..0ebd3b25 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "frontend", - "version": "2.0.0", + "version": "2.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "frontend", - "version": "2.0.0", + "version": "2.1.1", "dependencies": { "@jsep-plugin/numbers": "^1.0.1", "@jsep-plugin/object": "^1.2.1", @@ -25,27 +25,40 @@ "jsonl-parse-stringify": "^1.0.1", "jszip": "^3.7.1", "lodash": "^4.17.21", - "markdown-it-vue": "^1.1.6", + "markdown-it-vue": "^1.1.7", "mustache": "^4.2.0", "sass": "^1.32.8", "sass-loader": "^10.1.1", "style-loader": "^2.0.0", "v-jsoneditor": "^1.4.2", - "vite": "^4.2.1", - "vue": "^2.7.14", + "vite": "^4.5.2", + "vue": "^2.7.16", "vue-chartjs": "^5.2.0", "vue-json-pretty": "^1.8.0", - "vue-router": "^3.2.0", - "vue-template-compiler": "^2.6.11", - "vuex": "^3.4.0" + "vue-router": "^3.6.5", + "vue-template-compiler": "^2.7.16", + "vuex": "^3.6.2" }, "devDependencies": { - "@testing-library/vue": "^5.6.2", - "@vitest/coverage-c8": "^0.30.1", - "@vue/test-utils": "^1.0.3", - "cypress": "^12.9.0", - "esbuild": "^0.17.18", - "vitest": "^0.30.1" + "@testing-library/vue": "^5.9.0", + "@vitest/coverage-v8": "^0.34.6", + "@vue/test-utils": "^1.3.6", + "cypress": "^12.17.4", + "esbuild": "^0.19.11", + "vitest": "^0.34.6" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/@babel/code-frame": { @@ -155,9 +168,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", - "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", "bin": { "parser": "bin/babel-parser.js" }, @@ -200,9 +213,9 @@ } }, "node_modules/@cypress/request": { - "version": "2.88.11", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.11.tgz", - "integrity": "sha512-M83/wfQ1EkspjkE2lNWNV5ui2Cv7UCv1swW1DqljahbzLVWltcsexQh8jYtuS/vzFXP+HySntGM83ZXA9fn17w==", + "version": "2.88.12", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.12.tgz", + "integrity": "sha512-tOn+0mDZxASFM+cuAP9szGUGPI1HwWVSvdzm7V4cCsPdFTx6qMj29CwaQmRAMIEhORIUBFBsYROYJcveK4uOjA==", "dev": true, "dependencies": { "aws-sign2": "~0.7.0", @@ -220,7 +233,7 @@ "performance-now": "^2.1.0", "qs": "~6.10.3", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", + "tough-cookie": "^4.1.3", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" }, @@ -242,19 +255,6 @@ "node": ">= 0.12" } }, - "node_modules/@cypress/request/node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/@cypress/xvfb": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", @@ -274,13 +274,30 @@ "ms": "^2.1.1" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz", + "integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/android-arm": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.18.tgz", - "integrity": "sha512-EmwL+vUBZJ7mhFCs5lA4ZimpUH3WMAoqvOIYhVQwdIgSpHC8ImHdsRyhHAVxpDYUSm0lWvd63z0XH1IlImS2Qw==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz", + "integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==", "cpu": [ "arm" ], + "dev": true, "optional": true, "os": [ "android" @@ -290,12 +307,13 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.18.tgz", - "integrity": "sha512-/iq0aK0eeHgSC3z55ucMAHO05OIqmQehiGay8eP5l/5l+iEr4EIbh4/MI8xD9qRFjqzgkc0JkX0LculNC9mXBw==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz", + "integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==", "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "android" @@ -305,12 +323,13 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.18.tgz", - "integrity": "sha512-x+0efYNBF3NPW2Xc5bFOSFW7tTXdAcpfEg2nXmxegm4mJuVeS+i109m/7HMiOQ6M12aVGGFlqJX3RhNdYM2lWg==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz", + "integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==", "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "android" @@ -320,12 +339,13 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.18.tgz", - "integrity": "sha512-6tY+djEAdF48M1ONWnQb1C+6LiXrKjmqjzPNPWXhu/GzOHTHX2nh8Mo2ZAmBFg0kIodHhciEgUBtcYCAIjGbjQ==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz", + "integrity": "sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==", "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "darwin" @@ -335,12 +355,13 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.18.tgz", - "integrity": "sha512-Qq84ykvLvya3dO49wVC9FFCNUfSrQJLbxhoQk/TE1r6MjHo3sFF2tlJCwMjhkBVq3/ahUisj7+EpRSz0/+8+9A==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz", + "integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==", "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "darwin" @@ -350,12 +371,13 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.18.tgz", - "integrity": "sha512-fw/ZfxfAzuHfaQeMDhbzxp9mc+mHn1Y94VDHFHjGvt2Uxl10mT4CDavHm+/L9KG441t1QdABqkVYwakMUeyLRA==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz", + "integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==", "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "freebsd" @@ -365,12 +387,13 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.18.tgz", - "integrity": "sha512-FQFbRtTaEi8ZBi/A6kxOC0V0E9B/97vPdYjY9NdawyLd4Qk5VD5g2pbWN2VR1c0xhzcJm74HWpObPszWC+qTew==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz", + "integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==", "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "freebsd" @@ -380,12 +403,13 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.18.tgz", - "integrity": "sha512-jW+UCM40LzHcouIaqv3e/oRs0JM76JfhHjCavPxMUti7VAPh8CaGSlS7cmyrdpzSk7A+8f0hiedHqr/LMnfijg==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz", + "integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==", "cpu": [ "arm" ], + "dev": true, "optional": true, "os": [ "linux" @@ -395,12 +419,13 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.18.tgz", - "integrity": "sha512-R7pZvQZFOY2sxUG8P6A21eq6q+eBv7JPQYIybHVf1XkQYC+lT7nDBdC7wWKTrbvMXKRaGudp/dzZCwL/863mZQ==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz", + "integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==", "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -410,12 +435,13 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.18.tgz", - "integrity": "sha512-ygIMc3I7wxgXIxk6j3V00VlABIjq260i967Cp9BNAk5pOOpIXmd1RFQJQX9Io7KRsthDrQYrtcx7QCof4o3ZoQ==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz", + "integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==", "cpu": [ "ia32" ], + "dev": true, "optional": true, "os": [ "linux" @@ -425,12 +451,13 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.18.tgz", - "integrity": "sha512-bvPG+MyFs5ZlwYclCG1D744oHk1Pv7j8psF5TfYx7otCVmcJsEXgFEhQkbhNW8otDHL1a2KDINW20cfCgnzgMQ==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz", + "integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==", "cpu": [ "loong64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -440,12 +467,13 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.18.tgz", - "integrity": "sha512-oVqckATOAGuiUOa6wr8TXaVPSa+6IwVJrGidmNZS1cZVx0HqkTMkqFGD2HIx9H1RvOwFeWYdaYbdY6B89KUMxA==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz", + "integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==", "cpu": [ "mips64el" ], + "dev": true, "optional": true, "os": [ "linux" @@ -455,12 +483,13 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.18.tgz", - "integrity": "sha512-3dLlQO+b/LnQNxgH4l9rqa2/IwRJVN9u/bK63FhOPB4xqiRqlQAU0qDU3JJuf0BmaH0yytTBdoSBHrb2jqc5qQ==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz", + "integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==", "cpu": [ "ppc64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -470,12 +499,13 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.18.tgz", - "integrity": "sha512-/x7leOyDPjZV3TcsdfrSI107zItVnsX1q2nho7hbbQoKnmoeUWjs+08rKKt4AUXju7+3aRZSsKrJtaRmsdL1xA==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz", + "integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==", "cpu": [ "riscv64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -485,12 +515,13 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.18.tgz", - "integrity": "sha512-cX0I8Q9xQkL/6F5zWdYmVf5JSQt+ZfZD2bJudZrWD+4mnUvoZ3TDDXtDX2mUaq6upMFv9FlfIh4Gfun0tbGzuw==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz", + "integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==", "cpu": [ "s390x" ], + "dev": true, "optional": true, "os": [ "linux" @@ -500,12 +531,13 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.18.tgz", - "integrity": "sha512-66RmRsPlYy4jFl0vG80GcNRdirx4nVWAzJmXkevgphP1qf4dsLQCpSKGM3DUQCojwU1hnepI63gNZdrr02wHUA==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz", + "integrity": "sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==", "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -515,12 +547,13 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.18.tgz", - "integrity": "sha512-95IRY7mI2yrkLlTLb1gpDxdC5WLC5mZDi+kA9dmM5XAGxCME0F8i4bYH4jZreaJ6lIZ0B8hTrweqG1fUyW7jbg==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz", + "integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==", "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "netbsd" @@ -530,12 +563,13 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.18.tgz", - "integrity": "sha512-WevVOgcng+8hSZ4Q3BKL3n1xTv5H6Nb53cBrtzzEjDbbnOmucEVcZeGCsCOi9bAOcDYEeBZbD2SJNBxlfP3qiA==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz", + "integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==", "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "openbsd" @@ -545,12 +579,13 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.18.tgz", - "integrity": "sha512-Rzf4QfQagnwhQXVBS3BYUlxmEbcV7MY+BH5vfDZekU5eYpcffHSyjU8T0xucKVuOcdCsMo+Ur5wmgQJH2GfNrg==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz", + "integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==", "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "sunos" @@ -560,12 +595,13 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.18.tgz", - "integrity": "sha512-Kb3Ko/KKaWhjeAm2YoT/cNZaHaD1Yk/pa3FTsmqo9uFh1D1Rfco7BBLIPdDOozrObj2sahslFuAQGvWbgWldAg==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz", + "integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==", "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "win32" @@ -575,12 +611,13 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.18.tgz", - "integrity": "sha512-0/xUMIdkVHwkvxfbd5+lfG7mHOf2FRrxNbPiKWg9C4fFrB8H0guClmaM3BFiRUYrznVoyxTIyC/Ou2B7QQSwmw==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz", + "integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==", "cpu": [ "ia32" ], + "dev": true, "optional": true, "os": [ "win32" @@ -590,12 +627,13 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.18.tgz", - "integrity": "sha512-qU25Ma1I3NqTSHJUOKi9sAH1/Mzuvlke0ioMJRthLXKm7JiSKVwFghlGbDLOO2sARECGhja4xYfRAZNPAkooYg==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz", + "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==", "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "win32" @@ -621,11 +659,22 @@ "node": ">=8" } }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "peer": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -647,7 +696,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "peer": true, "engines": { "node": ">=6.0.0" } @@ -757,6 +805,12 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, "node_modules/@sphinxxxx/color-conversion": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/@sphinxxxx/color-conversion/-/color-conversion-2.2.2.tgz", @@ -833,9 +887,9 @@ "dev": true }, "node_modules/@types/chai": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.4.tgz", - "integrity": "sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==", + "version": "4.3.11", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.11.tgz", + "integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==", "dev": true }, "node_modules/@types/chai-subset": { @@ -874,9 +928,9 @@ "peer": true }, "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "dev": true }, "node_modules/@types/json-schema": { @@ -923,44 +977,57 @@ "vue": "^2.7.0-0" } }, - "node_modules/@vitest/coverage-c8": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/coverage-c8/-/coverage-c8-0.30.1.tgz", - "integrity": "sha512-/Wa3dtSuckpdngAmiCwowaEXXgJkqPrtfvrs9HTB9QoEfNbZWPu4E4cjEn4lJZb4qcGf4fxFtUA2f9DnDNAzBA==", + "node_modules/@vitest/coverage-v8": { + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-0.34.6.tgz", + "integrity": "sha512-fivy/OK2d/EsJFoEoxHFEnNGTg+MmdZBAVK9Ka4qhXR2K3J0DS08vcGVwzDtXSuUMabLv4KtPcpSKkcMXFDViw==", "dev": true, "dependencies": { - "c8": "^7.13.0", + "@ampproject/remapping": "^2.2.1", + "@bcoe/v8-coverage": "^0.2.3", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.1.5", + "magic-string": "^0.30.1", "picocolors": "^1.0.0", - "std-env": "^3.3.2" + "std-env": "^3.3.3", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.1.0" }, "funding": { - "url": "https://github.com/sponsors/antfu" + "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": ">=0.30.0 <1" + "vitest": ">=0.32.0 <1" } }, "node_modules/@vitest/expect": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.30.1.tgz", - "integrity": "sha512-c3kbEtN8XXJSeN81iDGq29bUzSjQhjES2WR3aColsS4lPGbivwLtas4DNUe0jD9gg/FYGIteqOenfU95EFituw==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", + "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==", "dev": true, "dependencies": { - "@vitest/spy": "0.30.1", - "@vitest/utils": "0.30.1", - "chai": "^4.3.7" + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.30.1.tgz", - "integrity": "sha512-W62kT/8i0TF1UBCNMRtRMOBWJKRnNyv9RrjIgdUryEe0wNpGZvvwPDLuzYdxvgSckzjp54DSpv1xUbv4BQ0qVA==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz", + "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==", "dev": true, "dependencies": { - "@vitest/utils": "0.30.1", - "concordance": "^5.0.4", + "@vitest/utils": "0.34.6", "p-limit": "^4.0.0", - "pathe": "^1.1.0" + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner/node_modules/p-limit": { @@ -978,51 +1045,139 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@vitest/runner/node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@vitest/snapshot": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.30.1.tgz", - "integrity": "sha512-fJZqKrE99zo27uoZA/azgWyWbFvM1rw2APS05yB0JaLwUIg9aUtvvnBf4q7JWhEcAHmSwbrxKFgyBUga6tq9Tw==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz", + "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.1", + "pathe": "^1.1.1", + "pretty-format": "^29.5.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@vitest/snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "dependencies": { - "magic-string": "^0.30.0", - "pathe": "^1.1.0", - "pretty-format": "^27.5.1" + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@vitest/snapshot/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "node_modules/@vitest/spy": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.30.1.tgz", - "integrity": "sha512-YfJeIf37GvTZe04ZKxzJfnNNuNSmTEGnla2OdL60C8od16f3zOfv9q9K0nNii0NfjDJRt/CVN/POuY5/zTS+BA==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz", + "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==", "dev": true, "dependencies": { - "tinyspy": "^2.1.0" + "tinyspy": "^2.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.30.1.tgz", - "integrity": "sha512-/c8Xv2zUVc+rnNt84QF0Y0zkfxnaGhp87K2dYJMLtLOIckPzuxLVzAtFCicGFdB4NeBHNzTRr1tNn7rCtQcWFA==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", + "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", "dev": true, "dependencies": { - "concordance": "^5.0.4", + "diff-sequences": "^29.4.3", "loupe": "^2.3.6", - "pretty-format": "^27.5.1" + "pretty-format": "^29.5.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@vitest/utils/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@vitest/utils/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "node_modules/@vue/compiler-sfc": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.14.tgz", - "integrity": "sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==", + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.16.tgz", + "integrity": "sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==", "dependencies": { - "@babel/parser": "^7.18.4", + "@babel/parser": "^7.23.5", "postcss": "^8.4.14", "source-map": "^0.6.1" + }, + "optionalDependencies": { + "prettier": "^1.18.2 || ^2.0.0" } }, "node_modules/@vue/test-utils": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-1.3.4.tgz", - "integrity": "sha512-yh2sbosCxk5FfwjXYXdY9rUffaJqYEFjsod5sCD4oosRn2x8LfBLEzQH0scdo5n7z8VkBUThpYzbkI6DVAWimA==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-1.3.6.tgz", + "integrity": "sha512-udMmmF1ts3zwxUJEIAj5ziioR900reDrt6C9H3XpWPsLBx2lpHKoA4BTdd9HNIYbkGltWw+JjWJ+5O6QBwiyEw==", "dev": true, "dependencies": { "dom-event-types": "^1.0.0", @@ -1212,9 +1367,9 @@ "integrity": "sha512-EriMhoxdfhh0zKm7icSt8EXekODAOVsYh9fpnlru9ALwf0Iw7J7bpuqLjhi3QRxvVKR7P0teQdJwTvjVMcYHuw==" }, "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "bin": { "acorn": "bin/acorn" }, @@ -1556,12 +1711,6 @@ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, - "node_modules/blueimp-md5": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", - "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==", - "dev": true - }, "node_modules/bootstrap": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz", @@ -1690,119 +1839,6 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "peer": true }, - "node_modules/c8": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/c8/-/c8-7.13.0.tgz", - "integrity": "sha512-/NL4hQTv1gBL6J6ei80zu3IiTrmePDKXKXOTLpHvcIWZTVYQlDhVWjjWvkhICylE8EwwnMVzDZugCvdx0/DIIA==", - "dev": true, - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@istanbuljs/schema": "^0.1.3", - "find-up": "^5.0.0", - "foreground-child": "^2.0.0", - "istanbul-lib-coverage": "^3.2.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-reports": "^3.1.4", - "rimraf": "^3.0.2", - "test-exclude": "^6.0.0", - "v8-to-istanbul": "^9.0.0", - "yargs": "^16.2.0", - "yargs-parser": "^20.2.9" - }, - "bin": { - "c8": "bin/c8.js" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/c8/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/c8/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/c8/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/c8/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/c8/node_modules/v8-to-istanbul": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", - "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/c8/node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -1861,18 +1897,18 @@ "dev": true }, "node_modules/chai": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", - "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", + "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", "dev": true, "dependencies": { "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^4.1.2", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.5" + "type-detect": "^4.0.8" }, "engines": { "node": ">=4" @@ -1890,10 +1926,13 @@ } }, "node_modules/check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, "engines": { "node": "*" } @@ -2009,17 +2048,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, "node_modules/clone": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", @@ -2082,25 +2110,6 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, - "node_modules/concordance": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/concordance/-/concordance-5.0.4.tgz", - "integrity": "sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==", - "dev": true, - "dependencies": { - "date-time": "^3.1.0", - "esutils": "^2.0.3", - "fast-diff": "^1.2.0", - "js-string-escape": "^1.0.1", - "lodash": "^4.17.15", - "md5-hex": "^3.0.1", - "semver": "^7.3.2", - "well-known-symbols": "^2.0.0" - }, - "engines": { - "node": ">=10.18.0 <11 || >=12.14.0 <13 || >=14" - } - }, "node_modules/condense-newlines": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/condense-newlines/-/condense-newlines-0.2.1.tgz", @@ -2131,9 +2140,9 @@ "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==" }, "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, "node_modules/core-js": { @@ -2255,15 +2264,15 @@ } }, "node_modules/cypress": { - "version": "12.9.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.9.0.tgz", - "integrity": "sha512-Ofe09LbHKgSqX89Iy1xen2WvpgbvNxDzsWx3mgU1mfILouELeXYGwIib3ItCwoRrRifoQwcBFmY54Vs0zw7QCg==", + "version": "12.17.4", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.17.4.tgz", + "integrity": "sha512-gAN8Pmns9MA5eCDFSDJXWKUpaL3IDd89N9TtIupjYnzLSmlpVr+ZR+vb4U/qaMp+lB6tBvAmt7504c3Z4RU5KQ==", "dev": true, "hasInstallScript": true, "dependencies": { - "@cypress/request": "^2.88.10", + "@cypress/request": "2.88.12", "@cypress/xvfb": "^1.2.4", - "@types/node": "^14.14.31", + "@types/node": "^16.18.39", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", "arch": "^2.2.0", @@ -2275,7 +2284,7 @@ "check-more-types": "^2.24.0", "cli-cursor": "^3.1.0", "cli-table3": "~0.6.1", - "commander": "^5.1.0", + "commander": "^6.2.1", "common-tags": "^1.8.0", "dayjs": "^1.10.4", "debug": "^4.3.4", @@ -2293,12 +2302,13 @@ "listr2": "^3.8.3", "lodash": "^4.17.21", "log-symbols": "^4.0.0", - "minimist": "^1.2.6", + "minimist": "^1.2.8", "ospath": "^1.2.2", "pretty-bytes": "^5.6.0", + "process": "^0.11.10", "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", - "semver": "^7.3.2", + "semver": "^7.5.3", "supports-color": "^8.1.1", "tmp": "~0.2.1", "untildify": "^4.0.0", @@ -2312,9 +2322,9 @@ } }, "node_modules/cypress/node_modules/@types/node": { - "version": "14.18.42", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.42.tgz", - "integrity": "sha512-xefu+RBie4xWlK8hwAzGh3npDz/4VhF6icY/shU+zv/1fNn+ZVG7T7CRwe9LId9sAYRPxI+59QBPuKL3WpyGRg==", + "version": "16.18.73", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.73.tgz", + "integrity": "sha512-GaTgwUNzESSlX9uhTX2RQcwj2KBf/Wda+52TTtuMpgzR2Rvw7NNypQ8BJdc5Wk6osxZHcUZAKip5PtqWsUl31Q==", "dev": true }, "node_modules/cypress/node_modules/chalk": { @@ -2346,9 +2356,9 @@ } }, "node_modules/cypress/node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", "dev": true, "engines": { "node": ">= 6" @@ -3128,18 +3138,6 @@ "node": ">=10" } }, - "node_modules/date-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/date-time/-/date-time-3.1.0.tgz", - "integrity": "sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==", - "dev": true, - "dependencies": { - "time-zone": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/dayjs": { "version": "1.11.7", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", @@ -3257,6 +3255,15 @@ "node": ">=0.4.0" } }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -3447,9 +3454,10 @@ "peer": true }, "node_modules/esbuild": { - "version": "0.17.18", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.18.tgz", - "integrity": "sha512-z1lix43jBs6UKjcZVKOw2xx69ffE2aG0PygLL5qJ9OS/gy0Ewd1gW/PUQIOIQGXBHWNywSc0floSKoMFF8aK2w==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz", + "integrity": "sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==", + "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -3458,34 +3466,36 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.17.18", - "@esbuild/android-arm64": "0.17.18", - "@esbuild/android-x64": "0.17.18", - "@esbuild/darwin-arm64": "0.17.18", - "@esbuild/darwin-x64": "0.17.18", - "@esbuild/freebsd-arm64": "0.17.18", - "@esbuild/freebsd-x64": "0.17.18", - "@esbuild/linux-arm": "0.17.18", - "@esbuild/linux-arm64": "0.17.18", - "@esbuild/linux-ia32": "0.17.18", - "@esbuild/linux-loong64": "0.17.18", - "@esbuild/linux-mips64el": "0.17.18", - "@esbuild/linux-ppc64": "0.17.18", - "@esbuild/linux-riscv64": "0.17.18", - "@esbuild/linux-s390x": "0.17.18", - "@esbuild/linux-x64": "0.17.18", - "@esbuild/netbsd-x64": "0.17.18", - "@esbuild/openbsd-x64": "0.17.18", - "@esbuild/sunos-x64": "0.17.18", - "@esbuild/win32-arm64": "0.17.18", - "@esbuild/win32-ia32": "0.17.18", - "@esbuild/win32-x64": "0.17.18" + "@esbuild/aix-ppc64": "0.19.11", + "@esbuild/android-arm": "0.19.11", + "@esbuild/android-arm64": "0.19.11", + "@esbuild/android-x64": "0.19.11", + "@esbuild/darwin-arm64": "0.19.11", + "@esbuild/darwin-x64": "0.19.11", + "@esbuild/freebsd-arm64": "0.19.11", + "@esbuild/freebsd-x64": "0.19.11", + "@esbuild/linux-arm": "0.19.11", + "@esbuild/linux-arm64": "0.19.11", + "@esbuild/linux-ia32": "0.19.11", + "@esbuild/linux-loong64": "0.19.11", + "@esbuild/linux-mips64el": "0.19.11", + "@esbuild/linux-ppc64": "0.19.11", + "@esbuild/linux-riscv64": "0.19.11", + "@esbuild/linux-s390x": "0.19.11", + "@esbuild/linux-x64": "0.19.11", + "@esbuild/netbsd-x64": "0.19.11", + "@esbuild/openbsd-x64": "0.19.11", + "@esbuild/sunos-x64": "0.19.11", + "@esbuild/win32-arm64": "0.19.11", + "@esbuild/win32-ia32": "0.19.11", + "@esbuild/win32-x64": "0.19.11" } }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "peer": true, "engines": { "node": ">=6" } @@ -3588,6 +3598,8 @@ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -3691,12 +3703,6 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, - "node_modules/fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", - "dev": true - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3798,19 +3804,6 @@ "is-callable": "^1.1.3" } }, - "node_modules/foreground-child": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", - "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -3882,7 +3875,8 @@ "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true }, "node_modules/functions-have-names": { "version": "1.2.3", @@ -3893,19 +3887,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, "engines": { "node": "*" @@ -4041,6 +4026,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -4388,17 +4374,6 @@ "is-ci": "bin.js" } }, - "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-date-object": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", @@ -4687,32 +4662,46 @@ "dev": true }, "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "dependencies": { "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", + "make-dir": "^4.0.0", "supports-color": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" } }, "node_modules/istanbul-reports": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", - "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", "dev": true, "dependencies": { "html-escaper": "^2.0.0", @@ -4835,15 +4824,6 @@ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" }, - "node_modules/js-string-escape": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", - "integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5271,12 +5251,12 @@ } }, "node_modules/loupe": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", - "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", "dev": true, "dependencies": { - "get-func-name": "^2.0.0" + "get-func-name": "^2.0.1" } }, "node_modules/lz-string": { @@ -5289,41 +5269,38 @@ } }, "node_modules/magic-string": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", - "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", "dev": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.13" + "@jridgewell/sourcemap-codec": "^1.4.15" }, "engines": { "node": ">=12" } }, + "node_modules/magic-string/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "dependencies": { - "semver": "^6.0.0" + "semver": "^7.5.3" }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/markdown-it": { "version": "12.3.2", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", @@ -5477,18 +5454,6 @@ "resolved": "https://registry.npmjs.org/match-at/-/match-at-0.1.1.tgz", "integrity": "sha512-h4Yd392z9mST+dzc+yjuybOGFNOZjmXIPKWjxBd1Bb23r4SmDOsk2NYCU2BMUBGbSpZqwVsZYNq26QS3xfaT3Q==" }, - "node_modules/md5-hex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-3.0.1.tgz", - "integrity": "sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==", - "dev": true, - "dependencies": { - "blueimp-md5": "^2.10.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", @@ -5565,15 +5530,15 @@ } }, "node_modules/mlly": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.2.0.tgz", - "integrity": "sha512-+c7A3CV0KGdKcylsI6khWyts/CYrGTrRVo4R/I7u/cUsy0Conxa6LUhiEzVKIw14lc2L5aiO4+SeVe4TeGRKww==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.5.0.tgz", + "integrity": "sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==", "dev": true, "dependencies": { - "acorn": "^8.8.2", - "pathe": "^1.1.0", - "pkg-types": "^1.0.2", - "ufo": "^1.1.1" + "acorn": "^8.11.3", + "pathe": "^1.1.2", + "pkg-types": "^1.0.3", + "ufo": "^1.3.2" } }, "node_modules/mobius1-selectr": { @@ -5601,9 +5566,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "funding": [ { "type": "github", @@ -5838,15 +5803,6 @@ "optional": true, "peer": true }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -5865,15 +5821,10 @@ "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, "node_modules/pathe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.0.tgz", - "integrity": "sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", "dev": true }, "node_modules/pathval": { @@ -5928,13 +5879,13 @@ } }, "node_modules/pkg-types": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.2.tgz", - "integrity": "sha512-hM58GKXOcj8WTqUXnsQyJYXdeAPbythQgEF3nTcEo+nkD49chjQ9IKm/QJy9xf6JakXptz86h7ecP2024rrLaQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", "dev": true, "dependencies": { "jsonc-parser": "^3.2.0", - "mlly": "^1.1.1", + "mlly": "^1.2.0", "pathe": "^1.1.0" } }, @@ -5957,9 +5908,9 @@ } }, "node_modules/postcss": { - "version": "8.4.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", - "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "version": "8.4.33", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", + "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", "funding": [ { "type": "opencollective", @@ -5968,10 +5919,14 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -6062,6 +6017,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "optional": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pretty/-/pretty-2.0.0.tgz", @@ -6114,6 +6084,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -6180,9 +6159,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/randombytes": { "version": "2.1.0", @@ -6269,38 +6246,11 @@ "throttleit": "^1.0.0" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true, - "optional": true, - "peer": true - }, - "node_modules/resolve": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", - "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", - "dependencies": { - "is-core-module": "^2.11.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "dev": true }, "node_modules/restore-cursor": { "version": "3.1.0", @@ -6342,9 +6292,9 @@ "integrity": "sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==" }, "node_modules/rollup": { - "version": "3.20.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.20.2.tgz", - "integrity": "sha512-3zwkBQl7Ai7MFYQE0y1MeQ15+9jsi7XxfrqwTb/9EK8D9C9+//EBR4M+CuA1KODRaNbFez/lWxA5vhEGZp4MUg==", + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", "bin": { "rollup": "dist/bin/rollup" }, @@ -6464,9 +6414,9 @@ } }, "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -6601,9 +6551,9 @@ } }, "node_modules/sshpk": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", - "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", "dev": true, "dependencies": { "asn1": "~0.2.3", @@ -6632,9 +6582,9 @@ "dev": true }, "node_modules/std-env": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.2.tgz", - "integrity": "sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", "dev": true }, "node_modules/stop-iteration-iterator": { @@ -6750,17 +6700,6 @@ "node": ">=8" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -6856,34 +6795,25 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, - "node_modules/time-zone": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/time-zone/-/time-zone-1.0.0.tgz", - "integrity": "sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/tinybench": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.4.0.tgz", - "integrity": "sha512-iyziEiyFxX4kyxSp+MtY1oCH/lvjH3PxFN8PGCDeqcZWAJ/i+9y+nL85w99PxVzrIvew/GSkSbDYtiGVa85Afg==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.6.0.tgz", + "integrity": "sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==", "dev": true }, "node_modules/tinypool": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.4.0.tgz", - "integrity": "sha512-2ksntHOKf893wSAH4z/+JbPpi92esw8Gn9N2deXX+B0EO92hexAVI9GIZZPx7P5aYo5KULfeOSt3kMOmSOy6uA==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", + "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", "dev": true, "engines": { "node": ">=14.0.0" } }, "node_modules/tinyspy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.1.0.tgz", - "integrity": "sha512-7eORpyqImoOvkQJCSkL0d0mB4NHHIFAy4b1u8PHdDa7SjGS2njzl6/lyGoZLm+eyYEtlUmFGE0rFj66SWxZgQQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", + "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", "dev": true, "engines": { "node": ">=14.0.0" @@ -6913,12 +6843,10 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", - "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "dev": true, - "optional": true, - "peer": true, "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", @@ -7007,9 +6935,9 @@ "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" }, "node_modules/ufo": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.1.tgz", - "integrity": "sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz", + "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==", "dev": true }, "node_modules/universalify": { @@ -7017,8 +6945,6 @@ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", "dev": true, - "optional": true, - "peer": true, "engines": { "node": ">= 4.0.0" } @@ -7079,8 +7005,6 @@ "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", "dev": true, - "optional": true, - "peer": true, "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" @@ -7119,6 +7043,20 @@ "jsoneditor": "^9.5.3" } }, + "node_modules/v8-to-istanbul": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/vanilla-picker": { "version": "2.12.1", "resolved": "https://registry.npmjs.org/vanilla-picker/-/vanilla-picker-2.12.1.tgz", @@ -7148,14 +7086,13 @@ "dev": true }, "node_modules/vite": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.2.1.tgz", - "integrity": "sha512-7MKhqdy0ISo4wnvwtqZkjke6XN4taqQ2TBaTccLIpOKv7Vp2h4Y+NpmWCnGDeSvvn45KxvWgGyb0MkHvY1vgbg==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", + "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", "dependencies": { - "esbuild": "^0.17.5", - "postcss": "^8.4.21", - "resolve": "^1.22.1", - "rollup": "^3.18.0" + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" }, "bin": { "vite": "bin/vite.js" @@ -7163,12 +7100,16 @@ "engines": { "node": "^14.18.0 || >=16.0.0" }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@types/node": ">= 14", "less": "*", + "lightningcss": "^1.21.0", "sass": "*", "stylus": "*", "sugarss": "*", @@ -7181,6 +7122,9 @@ "less": { "optional": true }, + "lightningcss": { + "optional": true + }, "sass": { "optional": true }, @@ -7196,17 +7140,17 @@ } }, "node_modules/vite-node": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.30.1.tgz", - "integrity": "sha512-vTikpU/J7e6LU/8iM3dzBo8ZhEiKZEKRznEMm+mJh95XhWaPrJQraT/QsT2NWmuEf+zgAoMe64PKT7hfZ1Njmg==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz", + "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==", "dev": true, "dependencies": { "cac": "^6.7.14", "debug": "^4.3.4", - "mlly": "^1.2.0", - "pathe": "^1.1.0", + "mlly": "^1.4.0", + "pathe": "^1.1.1", "picocolors": "^1.0.0", - "vite": "^3.0.0 || ^4.0.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" @@ -7215,40 +7159,404 @@ "node": ">=v14.18.0" }, "funding": { - "url": "https://github.com/sponsors/antfu" + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" } }, "node_modules/vitest": { - "version": "0.30.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.30.1.tgz", - "integrity": "sha512-y35WTrSTlTxfMLttgQk4rHcaDkbHQwDP++SNwPb+7H8yb13Q3cu2EixrtHzF27iZ8v0XCciSsLg00RkPAzB/aA==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", + "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", "dev": true, "dependencies": { - "@types/chai": "^4.3.4", + "@types/chai": "^4.3.5", "@types/chai-subset": "^1.3.3", "@types/node": "*", - "@vitest/expect": "0.30.1", - "@vitest/runner": "0.30.1", - "@vitest/snapshot": "0.30.1", - "@vitest/spy": "0.30.1", - "@vitest/utils": "0.30.1", - "acorn": "^8.8.2", + "@vitest/expect": "0.34.6", + "@vitest/runner": "0.34.6", + "@vitest/snapshot": "0.34.6", + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "acorn": "^8.9.0", "acorn-walk": "^8.2.0", "cac": "^6.7.14", - "chai": "^4.3.7", - "concordance": "^5.0.4", + "chai": "^4.3.10", "debug": "^4.3.4", "local-pkg": "^0.4.3", - "magic-string": "^0.30.0", - "pathe": "^1.1.0", + "magic-string": "^0.30.1", + "pathe": "^1.1.1", "picocolors": "^1.0.0", - "source-map": "^0.6.1", - "std-env": "^3.3.2", + "std-env": "^3.3.3", "strip-literal": "^1.0.1", - "tinybench": "^2.4.0", - "tinypool": "^0.4.0", - "vite": "^3.0.0 || ^4.0.0", - "vite-node": "0.30.1", + "tinybench": "^2.5.0", + "tinypool": "^0.7.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", + "vite-node": "0.34.6", "why-is-node-running": "^2.2.2" }, "bin": { @@ -7258,7 +7566,7 @@ "node": ">=v14.18.0" }, "funding": { - "url": "https://github.com/sponsors/antfu" + "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", @@ -7307,11 +7615,12 @@ } }, "node_modules/vue": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.14.tgz", - "integrity": "sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ==", + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.16.tgz", + "integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==", + "deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.", "dependencies": { - "@vue/compiler-sfc": "2.7.14", + "@vue/compiler-sfc": "2.7.16", "csstype": "^3.1.0" } }, @@ -7344,9 +7653,9 @@ "integrity": "sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ==" }, "node_modules/vue-template-compiler": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz", - "integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==", + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", "dependencies": { "de-indent": "^1.0.2", "he": "^1.2.0" @@ -7475,15 +7784,6 @@ "acorn": "^8" } }, - "node_modules/well-known-symbols": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/well-known-symbols/-/well-known-symbols-2.0.0.tgz", - "integrity": "sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/whatwg-encoding": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", @@ -7688,42 +7988,6 @@ "optional": true, "peer": true }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", @@ -7734,18 +7998,6 @@ "fd-slicer": "~1.1.0" } }, - "node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/zrender": { "version": "5.4.3", "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.4.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 57cb86aa..f2e48fb3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "2.0.0", + "version": "2.1.1", "private": true, "scripts": { "serve": "vite", @@ -30,27 +30,27 @@ "jsonl-parse-stringify": "^1.0.1", "jszip": "^3.7.1", "lodash": "^4.17.21", - "markdown-it-vue": "^1.1.6", + "markdown-it-vue": "^1.1.7", "mustache": "^4.2.0", "sass": "^1.32.8", "sass-loader": "^10.1.1", "style-loader": "^2.0.0", "v-jsoneditor": "^1.4.2", - "vite": "^4.2.1", - "vue": "^2.7.14", + "vite": "^4.5.2", + "vue": "^2.7.16", "vue-chartjs": "^5.2.0", "vue-json-pretty": "^1.8.0", - "vue-router": "^3.2.0", - "vue-template-compiler": "^2.6.11", - "vuex": "^3.4.0" + "vue-router": "^3.6.5", + "vue-template-compiler": "^2.7.16", + "vuex": "^3.6.2" }, "devDependencies": { - "@testing-library/vue": "^5.6.2", - "@vitest/coverage-c8": "^0.30.1", - "@vue/test-utils": "^1.0.3", - "cypress": "^12.9.0", - "esbuild": "^0.17.18", - "vitest": "^0.30.1" + "@testing-library/vue": "^5.9.0", + "@vitest/coverage-v8": "^0.34.6", + "@vue/test-utils": "^1.3.6", + "cypress": "^12.17.4", + "esbuild": "^0.19.11", + "vitest": "^0.34.6" }, "browserslist": [ "> 1%", diff --git a/frontend/src/views/ManageUsers.vue b/frontend/src/views/ManageUsers.vue index 8802f203..fb7fba1e 100644 --- a/frontend/src/views/ManageUsers.vue +++ b/frontend/src/views/ManageUsers.vue @@ -168,7 +168,7 @@ export default { ...mapActions(["getAllUsers", "adminGetUser", "adminUpdateUser", "adminUpdateUserPassword", "generatePasswordReset", "generateUserActivation", "adminDeleteUser", "adminDeleteUserPersonalInformation"]), searchUsers(users, searchString) { - const regEx = new RegExp(searchString); + const regEx = new RegExp(searchString, "i"); const result = _.filter(users, ({username, email}) => !!username.match(regEx) || !!email.match(regEx)); return result }, diff --git a/install/get-teamware.sh b/install/get-teamware.sh index e7a5e30a..cacf2c82 100644 --- a/install/get-teamware.sh +++ b/install/get-teamware.sh @@ -175,7 +175,7 @@ else case "$EMAIL_SECURE" in [Nn]*) - DJANGO_EMAIL_SECURITY= + DJANGO_EMAIL_SECURITY=none ;; *) diff --git a/package-lock.json b/package-lock.json index 0ab14ea4..a46bd4ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gate-teamware", - "version": "2.1.0", + "version": "2.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gate-teamware", - "version": "2.1.0", + "version": "2.1.1", "hasInstallScript": true, "license": "ISC", "dependencies": { @@ -15,7 +15,7 @@ }, "devDependencies": { "cross-env": "^7.0.3", - "cypress": "^12.9.0", + "cypress": "^12.17.4", "cypress-file-upload": "^5.0.8" } }, @@ -30,9 +30,9 @@ } }, "node_modules/@cypress/request": { - "version": "2.88.11", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.11.tgz", - "integrity": "sha512-M83/wfQ1EkspjkE2lNWNV5ui2Cv7UCv1swW1DqljahbzLVWltcsexQh8jYtuS/vzFXP+HySntGM83ZXA9fn17w==", + "version": "2.88.12", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.12.tgz", + "integrity": "sha512-tOn+0mDZxASFM+cuAP9szGUGPI1HwWVSvdzm7V4cCsPdFTx6qMj29CwaQmRAMIEhORIUBFBsYROYJcveK4uOjA==", "dev": true, "dependencies": { "aws-sign2": "~0.7.0", @@ -50,7 +50,7 @@ "performance-now": "^2.1.0", "qs": "~6.10.3", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", + "tough-cookie": "^4.1.3", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" }, @@ -78,9 +78,9 @@ } }, "node_modules/@types/node": { - "version": "14.18.42", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.42.tgz", - "integrity": "sha512-xefu+RBie4xWlK8hwAzGh3npDz/4VhF6icY/shU+zv/1fNn+ZVG7T7CRwe9LId9sAYRPxI+59QBPuKL3WpyGRg==", + "version": "16.18.73", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.73.tgz", + "integrity": "sha512-GaTgwUNzESSlX9uhTX2RQcwj2KBf/Wda+52TTtuMpgzR2Rvw7NNypQ8BJdc5Wk6osxZHcUZAKip5PtqWsUl31Q==", "dev": true }, "node_modules/@types/sinonjs__fake-timers": { @@ -210,7 +210,7 @@ "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "dev": true, "engines": { "node": ">=0.8" @@ -234,7 +234,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, "node_modules/at-least-node": { @@ -260,7 +260,7 @@ "node_modules/aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", "dev": true, "engines": { "node": "*" @@ -300,7 +300,7 @@ "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "dev": true, "dependencies": { "tweetnacl": "^0.14.3" @@ -384,7 +384,7 @@ "node_modules/caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", "dev": true }, "node_modules/chalk": { @@ -528,9 +528,9 @@ } }, "node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", "dev": true, "engines": { "node": ">= 6" @@ -553,7 +553,7 @@ "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "dev": true }, "node_modules/cross-env": { @@ -589,15 +589,15 @@ } }, "node_modules/cypress": { - "version": "12.9.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.9.0.tgz", - "integrity": "sha512-Ofe09LbHKgSqX89Iy1xen2WvpgbvNxDzsWx3mgU1mfILouELeXYGwIib3ItCwoRrRifoQwcBFmY54Vs0zw7QCg==", + "version": "12.17.4", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.17.4.tgz", + "integrity": "sha512-gAN8Pmns9MA5eCDFSDJXWKUpaL3IDd89N9TtIupjYnzLSmlpVr+ZR+vb4U/qaMp+lB6tBvAmt7504c3Z4RU5KQ==", "dev": true, "hasInstallScript": true, "dependencies": { - "@cypress/request": "^2.88.10", + "@cypress/request": "2.88.12", "@cypress/xvfb": "^1.2.4", - "@types/node": "^14.14.31", + "@types/node": "^16.18.39", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", "arch": "^2.2.0", @@ -609,7 +609,7 @@ "check-more-types": "^2.24.0", "cli-cursor": "^3.1.0", "cli-table3": "~0.6.1", - "commander": "^5.1.0", + "commander": "^6.2.1", "common-tags": "^1.8.0", "dayjs": "^1.10.4", "debug": "^4.3.4", @@ -627,12 +627,13 @@ "listr2": "^3.8.3", "lodash": "^4.17.21", "log-symbols": "^4.0.0", - "minimist": "^1.2.6", + "minimist": "^1.2.8", "ospath": "^1.2.2", "pretty-bytes": "^5.6.0", + "process": "^0.11.10", "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", - "semver": "^7.3.2", + "semver": "^7.5.3", "supports-color": "^8.1.1", "tmp": "~0.2.1", "untildify": "^4.0.0", @@ -658,9 +659,9 @@ } }, "node_modules/cypress/node_modules/semver": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz", - "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -675,7 +676,7 @@ "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "dev": true, "dependencies": { "assert-plus": "^1.0.0" @@ -725,7 +726,7 @@ "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, "engines": { "node": ">=0.4.0" @@ -734,7 +735,7 @@ "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "dev": true, "dependencies": { "jsbn": "~0.1.0", @@ -930,7 +931,7 @@ "node_modules/extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", "dev": true, "engines": [ "node >=0.6.0" @@ -971,7 +972,7 @@ "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", "dev": true, "engines": { "node": "*" @@ -1097,7 +1098,7 @@ "node_modules/getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "dev": true, "dependencies": { "assert-plus": "^1.0.0" @@ -1576,7 +1577,7 @@ "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "dev": true }, "node_modules/is-unicode-supported": { @@ -1610,13 +1611,13 @@ "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "dev": true }, "node_modules/jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", "dev": true }, "node_modules/json-parse-better-errors": { @@ -1633,7 +1634,7 @@ "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "dev": true }, "node_modules/jsonfile": { @@ -2187,7 +2188,7 @@ "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "dev": true }, "node_modules/pidtree": { @@ -2222,6 +2223,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/proxy-from-env": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", @@ -2245,9 +2255,9 @@ } }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "engines": { "node": ">=6" @@ -2268,6 +2278,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "node_modules/read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", @@ -2306,6 +2322,12 @@ "throttleit": "^1.0.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, "node_modules/resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -2503,9 +2525,9 @@ "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==" }, "node_modules/sshpk": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", - "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", "dev": true, "dependencies": { "asn1": "~0.2.3", @@ -2679,16 +2701,27 @@ } }, "node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "dev": true, "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" }, "engines": { - "node": ">=0.8" + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" } }, "node_modules/tslib": { @@ -2700,7 +2733,7 @@ "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, "dependencies": { "safe-buffer": "^5.0.1" @@ -2712,7 +2745,7 @@ "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "dev": true }, "node_modules/type-fest": { @@ -2772,6 +2805,16 @@ "node": ">=8" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -2793,7 +2836,7 @@ "node_modules/verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", "dev": true, "engines": [ "node >=0.6.0" diff --git a/package.json b/package.json index 29668a06..940daf3b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gate-teamware", - "version": "2.1.1", + "version": "2.2.0", "description": "A service for collaborative document annotation.", "main": "index.js", "scripts": { @@ -51,7 +51,7 @@ "devDependencies": { "cross-env": "^7.0.3", "cypress-file-upload": "^5.0.8", - "cypress": "^12.9.0" + "cypress": "^12.17.4" }, "dependencies": { "npm-run-all": "^4.1.5", diff --git a/teamware/settings/base.py b/teamware/settings/base.py index fccf1e4f..c6a91b55 100644 --- a/teamware/settings/base.py +++ b/teamware/settings/base.py @@ -214,8 +214,10 @@ EMAIL_USE_SSL = True elif os.environ['DJANGO_EMAIL_SECURITY'].lower() == 'tls': EMAIL_USE_TLS = True + elif os.environ['DJANGO_EMAIL_SECURITY'].lower() in ['', 'none']: + pass else: - raise ValueError("DJANGO_EMAIL_SECURITY, if set, must be either SSL or TLS") + raise ValueError("DJANGO_EMAIL_SECURITY, if set, must be either SSL or TLS, or 'none' for no security") if 'DJANGO_EMAIL_CLIENT_CERTIFICATE' in os.environ: # If certificate is set then key must also, and we want to raise an @@ -265,6 +267,11 @@ DELETED_USER_LASTNAME = "Deleted" DELETED_USER_EMAIL_DOMAIN = "teamware-deleted.com" +""" +Anonymization settings +""" +ANONYMIZATION_PREFIX = "annotator" + """ Frontend dev server configuration """