From 2fa1b878e82c3b039727c46d77a450e1cb3d1137 Mon Sep 17 00:00:00 2001 From: Lennard Strohmeyer <lennard.strohmeyer@digitallearning.gmbh> Date: Fri, 14 Feb 2025 10:37:28 +0100 Subject: [PATCH] rework to use groups as main model for consents, implemented control center --- ...e_userconsents_provider_schema_and_more.py | 46 ++ src/consents/models.py | 8 +- src/consents/serializers.py | 116 ++- .../tests/tests_consent_operations.py | 726 ++++++++--------- .../tests/tests_create_provider_schema.py | 159 ++-- .../tests/tests_paused_data_recording.py | 29 +- src/consents/tests/tests_third_party.py | 206 ++--- src/consents/urls.py | 2 + src/consents/views.py | 300 ++++--- src/frontend/package-lock.json | 748 +++++++++++++++++- src/frontend/package.json | 1 + .../analytics-engine.component.html | 42 +- .../analytics-engine.component.scss | 6 +- .../analytics-engine.component.ts | 18 +- src/frontend/src/app/app-routing.module.ts | 2 + src/frontend/src/app/app.component.html | 4 +- src/frontend/src/app/app.module.ts | 84 +- .../analytics-tokens.component.html | 4 +- .../consent-history.component.html | 80 +- .../consent-management/consentDeclaration.ts | 50 +- .../provider/provider.component.html | 139 ++-- .../provider/provider.component.scss | 3 - .../provider/provider.component.ts | 17 +- .../schema-change/schema-change.component.ts | 736 ++++++++--------- .../verb-groups/verb-groups.component.html | 41 + .../verb-groups/verb-groups.component.scss | 3 + .../verb-groups/verb-groups.component.spec.ts | 23 + .../verb-groups/verb-groups.component.ts | 43 + .../control-center.component.html | 51 ++ .../control-center.component.scss | 3 + .../control-center.component.spec.ts | 23 + .../control-center.component.ts | 74 ++ .../create-verb-group-dialog.html | 86 ++ .../create-verb-group-dialog.scss | 0 .../create-verb-group-dialog.ts | 102 +++ src/frontend/src/app/home/home.component.html | 2 +- src/frontend/src/app/home/home.component.scss | 4 +- .../app/merge-data/merge-data.component.html | 54 +- .../navigation/header/header.component.html | 9 + .../page-not-found.component.html | 2 +- src/frontend/src/app/services/api.service.ts | 61 +- .../src/environments/environment.prod.ts | 2 +- src/frontend/src/locale/messages.de.xlf | 139 +++- src/frontend/src/locale/messages.xlf | 67 +- src/frontend/src/theme.less | 54 +- ...providerschema_essential_verbs_and_more.py | 156 ++++ src/providers/models.py | 57 +- src/providers/serializers.py | 8 +- src/providers/views.py | 25 +- src/static/provider_schema.schema.json | 81 +- .../provider_schema_h5p_v1.example.json | 144 ++-- .../provider_schema_moodle_v1.example.json | 140 ++-- src/users/models.py | 2 +- src/users/serializers.py | 19 +- src/users/urls.py | 5 +- src/users/views.py | 39 +- src/xapi/models.py | 1 - src/xapi/tests/tests.py | 402 ++++++---- src/xapi/tests/tests_verb_id_validation.py | 127 +-- src/xapi/views.py | 32 +- 60 files changed, 3715 insertions(+), 1892 deletions(-) create mode 100644 src/consents/migrations/0005_remove_userconsents_provider_schema_and_more.py create mode 100644 src/frontend/src/app/consent-management/verb-groups/verb-groups.component.html create mode 100644 src/frontend/src/app/consent-management/verb-groups/verb-groups.component.scss create mode 100644 src/frontend/src/app/consent-management/verb-groups/verb-groups.component.spec.ts create mode 100644 src/frontend/src/app/consent-management/verb-groups/verb-groups.component.ts create mode 100644 src/frontend/src/app/control-center/control-center.component.html create mode 100644 src/frontend/src/app/control-center/control-center.component.scss create mode 100644 src/frontend/src/app/control-center/control-center.component.spec.ts create mode 100644 src/frontend/src/app/control-center/control-center.component.ts create mode 100644 src/frontend/src/app/dialogs/create-verb-group-dialog/create-verb-group-dialog.html create mode 100644 src/frontend/src/app/dialogs/create-verb-group-dialog/create-verb-group-dialog.scss create mode 100644 src/frontend/src/app/dialogs/create-verb-group-dialog/create-verb-group-dialog.ts create mode 100644 src/providers/migrations/0008_verb_remove_providerschema_essential_verbs_and_more.py diff --git a/src/consents/migrations/0005_remove_userconsents_provider_schema_and_more.py b/src/consents/migrations/0005_remove_userconsents_provider_schema_and_more.py new file mode 100644 index 0000000..2dffac1 --- /dev/null +++ b/src/consents/migrations/0005_remove_userconsents_provider_schema_and_more.py @@ -0,0 +1,46 @@ +from django.db import migrations, models +import django.db.models.deletion + +def migrate_user_consent_to_verb(apps, schema_editor): + UserConsents = apps.get_model('consents', 'UserConsents') + Verb = apps.get_model('providers', 'Verb') + for consent in UserConsents.objects.all(): + schema = consent.provider_schema + verb_id = consent.verb + verb = Verb.objects.get(verb_id=verb_id, provider_schema=schema) + consent.verb = verb.id + group = verb.providerverbgroup_set.first() + consent.verb_group_id = group.id + consent.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('providers', '0008_verb_remove_providerschema_essential_verbs_and_more'), + ('consents', '0004_userconsents_active'), + ] + + operations = [ + migrations.AddField( + model_name='userconsents', + name='verb_group', + field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, + to='providers.providerverbgroup'), + ), + migrations.RunPython(migrate_user_consent_to_verb, reverse_code=migrations.RunPython.noop), + migrations.AlterField( + model_name='userconsents', + name='verb', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='providers.verb'), + ), + migrations.RemoveField( + model_name='userconsents', + name='provider_schema', + ), + migrations.AlterField( # now we can remove the null default + model_name='userconsents', + name='verb_group', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, + to='providers.providerverbgroup'), + ) + ] diff --git a/src/consents/models.py b/src/consents/models.py index 37761ed..442e3f6 100644 --- a/src/consents/models.py +++ b/src/consents/models.py @@ -3,19 +3,19 @@ from email.policy import default from django.conf import settings from django.db import models -from providers.models import Provider, ProviderSchema +from providers.models import Provider, ProviderVerbGroup, Verb class UserConsents(models.Model): user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) consented = models.BooleanField() - provider_schema = models.ForeignKey(ProviderSchema, on_delete=models.CASCADE) provider = models.ForeignKey(Provider, on_delete=models.CASCADE) - verb = models.CharField(db_index=True, max_length=500) + verb_group = models.ForeignKey(ProviderVerbGroup, default=None, on_delete=models.CASCADE) + verb = models.ForeignKey(Verb, on_delete=models.CASCADE) object = models.JSONField() updated = models.DateTimeField(auto_now=True) created = models.DateTimeField(auto_now_add=True) active = models.BooleanField(default=True) def __str__(self): - return "User consent: " + self.verb + return "User consent for verb " + str(self.verb) diff --git a/src/consents/serializers.py b/src/consents/serializers.py index 59886d5..7dd3c72 100644 --- a/src/consents/serializers.py +++ b/src/consents/serializers.py @@ -1,9 +1,10 @@ import json from typing import Dict +from django.core.exceptions import ObjectDoesNotExist from rest_framework import serializers -from providers.models import AnalyticsToken, Provider, ProviderSchema +from providers.models import AnalyticsToken, Provider, ProviderSchema, Verb, VerbObject, ProviderVerbGroup class ProviderSchemaSerializer(serializers.ModelSerializer): @@ -14,8 +15,8 @@ class ProviderSchemaSerializer(serializers.ModelSerializer): "id": obj.provider.definition_id, "name": obj.provider.name, "description": obj.provider.description, - "groups": obj.groups, - "essential_verbs": obj.essential_verbs + "verbs": VerbSerializer(obj.verbs(), many=True).data, + "essential_verbs": VerbSerializer(obj.essential_verbs(), many=True).data } class Meta: @@ -23,21 +24,94 @@ class ProviderSchemaSerializer(serializers.ModelSerializer): fields = "__all__" +class VerbSerializer(serializers.ModelSerializer): + id = serializers.SerializerMethodField("_id") + label = serializers.SerializerMethodField("_label") + description = serializers.SerializerMethodField("_description") + defaultConsent = serializers.SerializerMethodField("_defaultConsent") + objects = serializers.SerializerMethodField("_objects") + + def _id(self, obj: Verb): + return obj.verb_id + + def _label(self, obj: Verb): + return obj.label + + def _description(self, obj: Verb): + return obj.description + + def _defaultConsent(self, obj: Verb): + return obj.default_consent + + def _objects(self, obj: Verb): + return VerbObjectSerializer(VerbObject.objects.filter(verb=obj), many=True).data + + class Meta: + model = Verb + exclude = ["verb_id", "default_consent", "provider", "provider_schema", "active", + "essential", "allow_anonymized_collection"] + + +class VerbObjectSerializer(serializers.ModelSerializer): + id = serializers.SerializerMethodField("_id") + objectType = serializers.SerializerMethodField("_objectType") + defaultConsent = serializers.SerializerMethodField("_defaultConsent") + definition = serializers.SerializerMethodField("_definition") + + def _id(self, obj: VerbObject): + return obj.object_id + + def _objectType(self, obj: VerbObject): + return obj.object_type + + def _definition(self, obj: VerbObject): + return obj.definition + + def _defaultConsent(self, obj: VerbObject): + return obj.verb.default_consent + + class Meta: + model = VerbObject + exclude = ["object_id", "verb"] + +class ProviderVerbGroupSerializer(serializers.ModelSerializer): + verbs = serializers.SerializerMethodField("_verbs") + purposeOfCollection = serializers.SerializerMethodField("_purposeOfCollection") + + def _verbs(self, obj): + return VerbSerializer(obj.verbs.all(), many=True).data + + def _purposeOfCollection(self, obj): + return obj.purpose_of_collection + + class Meta: + model = ProviderVerbGroup + exclude = ["purpose_of_collection"] + class ProvidersSerializer(serializers.ModelSerializer): versions = serializers.SerializerMethodField("_versions") + groups = serializers.SerializerMethodField("_groups") def _versions(self, obj): schemas = ProviderSchema.objects.filter(provider=obj).order_by("-created") serializer = ProviderSchemaSerializer(schemas, many=True) return serializer.data + def _groups(self, obj): + groups = ProviderVerbGroup.objects.filter(provider=obj).order_by("-created") + serializer = ProviderVerbGroupSerializer(groups, many=True) + return serializer.data + class Meta: model = Provider - fields = ["id", "name", "versions"] + fields = ["id", "name", "groups", "versions"] class UserVerbSerializer(serializers.Serializer): id = serializers.CharField() + group_id = serializers.PrimaryKeyRelatedField( + queryset=ProviderVerbGroup.objects.all() + ) consented = serializers.BooleanField() objects = serializers.CharField() @@ -89,6 +163,40 @@ class SaveUserConsentSerializer(serializers.Serializer): verbs = serializers.ListSerializer(child=UserVerbSerializer()) +class GroupVerbSerializer(serializers.Serializer): + id = serializers.CharField() + + +class CreateProviderVerbGroupSerializer(serializers.Serializer): + def __init__(self, provider, **kwargs): + super().__init__(**kwargs) + self.provider = provider + + def validate(self, data): + """ + Validate the existence of verbs associated with the new / updated group + and check if the chosen group ID is available. + """ + # new or updated groups have to use the newest schema + provider_schema = ProviderSchema.objects.get(provider=self.provider, superseded_by__isnull=True) + for verb_data in data["verbs"]: + try: + # we just validate verb IDs here since verbs are retrieved via the verb ID later + verb = Verb.objects.filter(verb_id=verb_data["id"], active=True, + provider=self.provider, + provider_schema=provider_schema) + except ObjectDoesNotExist: + raise serializers.ValidationError( + f"Verb {verb['id']} not found in newest schema." + ) + return data + id = serializers.CharField(allow_blank=True) + label = serializers.CharField() + description = serializers.CharField() + purposeOfCollection = serializers.CharField() + requiresConsent = serializers.BooleanField() + verbs = serializers.ListSerializer(child=GroupVerbSerializer()) + class CreateUserSerializer(serializers.Serializer): email = serializers.CharField() first_name = serializers.CharField() diff --git a/src/consents/tests/tests_consent_operations.py b/src/consents/tests/tests_consent_operations.py index e9be69b..c433730 100644 --- a/src/consents/tests/tests_consent_operations.py +++ b/src/consents/tests/tests_consent_operations.py @@ -6,7 +6,7 @@ from django.test import TransactionTestCase from rest_framework.test import APIClient from rolepermissions.roles import assign_role -from providers.models import Provider, ProviderAuthorization, ProviderSchema +from providers.models import Provider, ProviderAuthorization, ProviderSchema, ProviderVerbGroup from users.models import CustomUser from ..models import UserConsents @@ -27,100 +27,80 @@ class BaseTestCase(TransactionTestCase): "id": "h5p-0", "name": "H5P", "description": "Open-source content collaboration framework", - "groups": [ + "verbs": [ { - "id": "default_group", - "label": "Default group", - "description": "default", - "showVerbDetails": True, - "purposeOfCollection": "Lorem Ipsum", - "verbs": [ + "id": "http://h5p.example.com/expapi/verbs/experienced", + "label": "Experienced", + "description": "Experienced", + "defaultConsent": True, + "objects": [ { - "id": "http://h5p.example.com/expapi/verbs/experienced", - "label": "Experienced", - "description": "Experienced", + "id": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", + "label": "1.1.1 Funktionen", "defaultConsent": True, - "objects": [ - { - "id": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", - "label": "1.1.1 Funktionen", - "defaultConsent": True, - "matching": "definitionType", - "definition": { - "type": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", - "name": {"enUS": "1.1.1 Funktionen"}, - }, - } - ], - }, + "matching": "definitionType", + "definition": { + "type": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", + "name": {"enUS": "1.1.1 Funktionen"}, + }, + } + ], + }, + { + "id": "http://h5p.example.com/expapi/verbs/attempted", + "label": "Attempted", + "description": "Attempted", + "defaultConsent": True, + "objects": [ { - "id": "http://h5p.example.com/expapi/verbs/attempted", - "label": "Attempted", - "description": "Attempted", + "id": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", + "label": "2.3.1 Funktion Zirkulationsleitung", "defaultConsent": True, - "objects": [ - { - "id": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", - "label": "2.3.1 Funktion Zirkulationsleitung", - "defaultConsent": True, - "matching": "definitionType", - "definition": { - "type": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", - "name": { - "enUS": "2.3.1 Funktion Zirkulationsleitung" - }, - }, - } - ], - }, + "matching": "definitionType", + "definition": { + "type": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", + "name": { + "enUS": "2.3.1 Funktion Zirkulationsleitung" + }, + }, + } ], - "isDefault": True, }, { - "id": "group_2", - "label": "Group 2", + "id": "http://h5p.example.com/expapi/verbs/interacted", + "label": "Interacted", "description": "Lorem ipsum", - "showVerbDetails": True, - "purposeOfCollection": "Lorem Ipsum", - "verbs": [ + "defaultConsent": True, + "objects": [ { - "id": "http://h5p.example.com/expapi/verbs/interacted", - "label": "Interacted", - "description": "Lorem ipsum", + "id": "http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46", + "label": "1.2.3 Kappenventil", "defaultConsent": True, - "objects": [ - { - "id": "http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46", - "label": "1.2.3 Kappenventil", - "defaultConsent": True, - "matching": "definitionType", - "definition": { - "type": "http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46", - "name": {"enUS": "1.2.3 Kappenventil"}, - }, - } - ], - }, + "matching": "definitionType", + "definition": { + "type": "http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46", + "name": {"enUS": "1.2.3 Kappenventil"}, + }, + } + ], + }, + { + "id": "http://h5p.example.com/expapi/verbs/answered", + "label": "Answered", + "description": "lorem ipsum", + "defaultConsent": False, + "objects": [ { - "id": "http://h5p.example.com/expapi/verbs/answered", - "label": "Answered", - "description": "lorem ipsum", + "id": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", + "label": "7.2.1 Ventil Basics", "defaultConsent": False, - "objects": [ - { - "id": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", - "label": "7.2.1 Ventil Basics", - "defaultConsent": False, - "matching": "definitionType", - "definition": { - "type": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", - "name": {"enUS": "7.2.1 Ventil Basics"}, - }, - } - ], - }, + "matching": "definitionType", + "definition": { + "type": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", + "name": {"enUS": "7.2.1 Ventil Basics"}, + }, + } ], - "isDefault": False, }, ], "essentialVerbs": [ @@ -138,54 +118,44 @@ class BaseTestCase(TransactionTestCase): "id": "h5p-0", "name": "H5P", "description": "Open-source content collaboration framework", - "groups": [ + "verbs": [ { - "id": "default_group", - "label": "Default group", - "description": "default", - "showVerbDetails": True, - "purposeOfCollection": "Lorem Ipsum", - "verbs": [ + "id": "http://h5p.example.com/expapi/verbs/experienced", + "label": "Experienced", + "description": "Experienced", + "defaultConsent": True, + "objects": [ { - "id": "http://h5p.example.com/expapi/verbs/experienced", - "label": "Experienced", - "description": "Experienced", + "id": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", + "label": "1.1.1 Funktionen", "defaultConsent": True, - "objects": [ - { - "id": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", - "label": "1.1.1 Funktionen", - "defaultConsent": True, - "matching": "definitionType", - "definition": { - "type": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", - "name": {"enUS": "1.1.1 Funktionen"}, - }, - } - ], - }, + "matching": "definitionType", + "definition": { + "type": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", + "name": {"enUS": "1.1.1 Funktionen"}, + }, + } + ], + }, + { + "id": "http://h5p.example.com/expapi/verbs/attempted", + "label": "Attempted", + "description": "Attempted", + "defaultConsent": True, + "objects": [ { - "id": "http://h5p.example.com/expapi/verbs/attempted", - "label": "Attempted", - "description": "Attempted", + "id": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", + "label": "2.3.1 Funktion Zirkulationsleitung", "defaultConsent": True, - "objects": [ - { - "id": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", - "label": "2.3.1 Funktion Zirkulationsleitung", - "defaultConsent": True, - "matching": "definitionType", - "definition": { - "type": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", - "name": { - "enUS": "2.3.1 Funktion Zirkulationsleitung" - }, - }, - } - ], - }, + "matching": "definitionType", + "definition": { + "type": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", + "name": { + "enUS": "2.3.1 Funktion Zirkulationsleitung" + }, + }, + } ], - "isDefault": True, }, ], "essentialVerbs": [], @@ -195,14 +165,7 @@ class BaseTestCase(TransactionTestCase): "id": "moodle-0", "name": "Moodle", "description": "Open-source learning management system", - "groups": [ - { - "id": "default_group", - "label": "Default group", - "description": "default", - "showVerbDetails": True, - "purposeOfCollection": "Lorem Ipsum", - "verbs": [ + "verbs": [ { "id": "http://moodle.example.com/expapi/verbs/completed", "label": "Completed", @@ -274,9 +237,6 @@ class BaseTestCase(TransactionTestCase): ], }, ], - "isDefault": True, - } - ], "essentialVerbs": [], } @@ -358,47 +318,40 @@ class TestProviderSchemaCreation(BaseTestCase): "id": "h5p-0", "name": "H5P", "description": "Open-source content collaboration framework", - "group": [ + "verb": [ { - "label": "Default group", - "description": "default", - "verbs": [ + "id": "http://h5p.example.com/expapi/verbs/experienced", + "label": "Experienced", + "description": "Experienced", + "defaultConsent": True, + "objects": [ { - "id": "http://h5p.example.com/expapi/verbs/experienced", - "label": "Experienced", - "description": "Experienced", + "id": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", + "label": "1.1.1 Funktionen", "defaultConsent": True, - "objects": [ - { - "id": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", - "label": "1.1.1 Funktionen", - "defaultConsent": True, - "definition": { - "name": {"enUS": "1.1.1 Funktionen"} - }, - } - ], - }, + "definition": { + "name": {"enUS": "1.1.1 Funktionen"} + }, + } + ], + }, + { + "id": "http://h5p.example.com/expapi/verbs/attempted", + "label": "Attempted", + "description": "Attempted", + "defaultConsent": True, + "objects": [ { - "id": "http://h5p.example.com/expapi/verbs/attempted", - "label": "Attempted", - "description": "Attempted", + "id": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", + "label": "2.3.1 Funktion Zirkulationsleitung", "defaultConsent": True, - "objects": [ - { - "id": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", - "label": "2.3.1 Funktion Zirkulationsleitung", - "defaultConsent": True, - "definition": { - "name": { - "enUS": "2.3.1 Funktion Zirkulationsleitung" - } - }, + "definition": { + "name": { + "enUS": "2.3.1 Funktion Zirkulationsleitung" } - ], - }, + }, + } ], - "isDefault": True, }, ], "essentialVerbs": [], @@ -461,25 +414,25 @@ class TestUserConsentInvalidProviderIdFails(BaseTestCase): "providerSchemaId": 1, "verbs": [ { - "provider": 1, + "group_id": 1, "id": "http://h5p.example.com/expapi/verbs/experienced", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF","label":"1.1.1 Funktionen","defaultConsent":True,"definition":{"name":{"enUS":"1.1.1 Funktionen"}},"consented":true}]', }, { - "provider": 1, + "group_id": 1, "id": "http://h5p.example.com/expapi/verbs/attempted", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm","label":"2.3.1 Funktion Zirkulationsleitung","defaultConsent":True,"definition":{"name":{"enUS":"2.3.1 Funktion Zirkulationsleitung"}},"consented":true}]', }, { - "provider": 1, + "group_id": 1, "id": "http://h5p.example.com/expapi/verbs/interacted", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46","label":"1.2.3 Kappenventil","defaultConsent":True,"definition":{"name":{"enUS":"1.2.3 Kappenventil"}},"consented":true}]', }, { - "provider": 1, + "group_id": 1, "id": "http://h5p.example.com/expapi/verbs/answered", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b","label":"7.2.1 Ventil Basics","defaultConsent":False,"definition":{"name":{"enUS":"7.2.1 Ventil Basics"}},"consented":True}]', @@ -498,32 +451,55 @@ class TestUserConsentCreate(BaseTestCase): """ Ensure initial user consent gets created. """ + with StringIO(json.dumps(self.h5p_provider_schema)) as fp: + response = self.provider_client.put( + "/api/v1/consents/provider/create", + {"provider-schema": fp}, + format="multipart", + ) + self.assertEqual(response.status_code, 201) + # create a verb group + group = {"provider_id": Provider.objects.latest('id').id, "id": "default_group", "label": "Group 2", + "description": "Lorem ipsum", "showVerbDetails": True, "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/experienced"}, + {"id": "http://h5p.example.com/expapi/verbs/attempted"}, + {"id": "http://h5p.example.com/expapi/verbs/interacted"}, + {"id": "http://h5p.example.com/expapi/verbs/answered"}, + ]} + response = self.provider_client.post( + "/api/v1/consents/provider/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + group_id = ProviderVerbGroup.objects.latest('id').id payload = [ { - "providerId": 1, - "providerSchemaId": 1, + "providerId": Provider.objects.latest('id').id, + "providerSchemaId": ProviderSchema.objects.latest('id').id, "verbs": [ { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/experienced", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF","label":"1.1.1 Funktionen","defaultConsent":true,"definition":{"name":{"enUS":"1.1.1 Funktionen"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/attempted", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm","label":"2.3.1 Funktion Zirkulationsleitung","defaultConsent":true,"definition":{"name":{"enUS":"2.3.1 Funktion Zirkulationsleitung"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/interacted", "consented": False, "objects": '[{"id":"http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46","label":"1.2.3 Kappenventil","defaultConsent":true,"definition":{"name":{"enUS":"1.2.3 Kappenventil"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/answered", "consented": False, "objects": '[{"id":"http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b","label":"7.2.1 Ventil Basics","defaultConsent":false,"definition":{"name":{"enUS":"7.2.1 Ventil Basics"}},"consented":true}]', @@ -532,13 +508,6 @@ class TestUserConsentCreate(BaseTestCase): } ] - with StringIO(json.dumps(self.h5p_provider_schema)) as fp: - response = self.provider_client.put( - "/api/v1/consents/provider/create", - {"provider-schema": fp}, - format="multipart", - ) - self.assertEqual(response.status_code, 201) response = self.user_client.post( "/api/v1/consents/user/save", payload, format="json" @@ -549,22 +518,26 @@ class TestUserConsentCreate(BaseTestCase): self.assertEqual(len(user_consents), 4) self.assertTrue( UserConsents.objects.get( - verb="http://h5p.example.com/expapi/verbs/experienced" + verb__verb_id="http://h5p.example.com/expapi/verbs/experienced", + active=True ).consented ) self.assertTrue( UserConsents.objects.get( - verb="http://h5p.example.com/expapi/verbs/attempted" + verb__verb_id="http://h5p.example.com/expapi/verbs/attempted", + active=True ).consented ) self.assertFalse( UserConsents.objects.get( - verb="http://h5p.example.com/expapi/verbs/interacted" + verb__verb_id="http://h5p.example.com/expapi/verbs/interacted", + active=True ).consented ) self.assertFalse( UserConsents.objects.get( - verb="http://h5p.example.com/expapi/verbs/answered" + verb__verb_id="http://h5p.example.com/expapi/verbs/answered", + active=True ).consented ) @@ -574,31 +547,57 @@ class TestUserConsentOutdatedProviderSchemaId(BaseTestCase): """ Ensure user consent gets updated. """ + + with StringIO(json.dumps(self.h5p_provider_schema)) as fp: + response = self.provider_client.put( + "/api/v1/consents/provider/create", + {"provider-schema": fp}, + format="multipart", + ) + self.assertEqual(response.status_code, 201) + + # create a verb group + group = {"provider_id": Provider.objects.latest('id').id, "id": "default_group", "label": "Group 2", + "description": "Lorem ipsum", "showVerbDetails": True, "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/experienced"}, + {"id": "http://h5p.example.com/expapi/verbs/attempted"}, + {"id": "http://h5p.example.com/expapi/verbs/interacted"}, + {"id": "http://h5p.example.com/expapi/verbs/answered"}, + ]} + response = self.provider_client.post( + "/api/v1/consents/provider/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + group_id = ProviderVerbGroup.objects.latest('id').id + payload_second_consent = [ { - "providerId": 1, - "providerSchemaId": 2, + "providerId": Provider.objects.latest('id').id, + "providerSchemaId": ProviderSchema.objects.latest('id').id, "verbs": [ { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/experienced", "consented": False, "objects": '[{"id":"http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF","label":"1.1.1 Funktionen","defaultConsent":True,"definition":{"name":{"enUS":"1.1.1 Funktionen"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/attempted", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm","label":"2.3.1 Funktion Zirkulationsleitung","defaultConsent":True,"definition":{"name":{"enUS":"2.3.1 Funktion Zirkulationsleitung"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/interacted", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46","label":"1.2.3 Kappenventil","defaultConsent":True,"definition":{"name":{"enUS":"1.2.3 Kappenventil"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/answered", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b","label":"7.2.1 Ventil Basics","defaultConsent":False,"definition":{"name":{"enUS":"7.2.1 Ventil Basics"}},"consented":True}]', @@ -606,13 +605,6 @@ class TestUserConsentOutdatedProviderSchemaId(BaseTestCase): ], } ] - with StringIO(json.dumps(self.h5p_provider_schema)) as fp: - response = self.provider_client.put( - "/api/v1/consents/provider/create", - {"provider-schema": fp}, - format="multipart", - ) - self.assertEqual(response.status_code, 201) response = self.user_client.post( "/api/v1/consents/user/save", payload_second_consent, format="json" @@ -623,50 +615,54 @@ class TestUserConsentOutdatedProviderSchemaId(BaseTestCase): self.assertEqual(len(user_consents), 4) self.assertFalse( UserConsents.objects.get( - verb="http://h5p.example.com/expapi/verbs/experienced" + verb__verb_id="http://h5p.example.com/expapi/verbs/experienced", + active=True, ).consented ) self.assertTrue( UserConsents.objects.get( - verb="http://h5p.example.com/expapi/verbs/attempted" + verb__verb_id="http://h5p.example.com/expapi/verbs/attempted", + active=True, ).consented ) self.assertTrue( UserConsents.objects.get( - verb="http://h5p.example.com/expapi/verbs/interacted" + verb__verb_id="http://h5p.example.com/expapi/verbs/interacted", + active=True, ).consented ) self.assertTrue( UserConsents.objects.get( - verb="http://h5p.example.com/expapi/verbs/answered" + verb__verb_id="http://h5p.example.com/expapi/verbs/answered", + active=True, ).consented ) payload_second_consent = [ { - "providerId": 1, - "providerSchemaId": 2, + "providerId": Provider.objects.latest('id').id, + "providerSchemaId": ProviderSchema.objects.latest('id').id, "verbs": [ { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/experienced", "consented": False, "objects": '[{"id":"http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF","label":"1.1.1 Funktionen","defaultConsent":True,"definition":{"name":{"enUS":"1.1.1 Funktionen"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/attempted", "consented": False, "objects": '[{"id":"http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm","label":"2.3.1 Funktion Zirkulationsleitung","defaultConsent":True,"definition":{"name":{"enUS":"2.3.1 Funktion Zirkulationsleitung"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/interacted", "consented": False, "objects": '[{"id":"http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46","label":"1.2.3 Kappenventil","defaultConsent":True,"definition":{"name":{"enUS":"1.2.3 Kappenventil"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/answered", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b","label":"7.2.1 Ventil Basics","defaultConsent":False,"definition":{"name":{"enUS":"7.2.1 Ventil Basics"}},"consented":True}]', @@ -681,25 +677,29 @@ class TestUserConsentOutdatedProviderSchemaId(BaseTestCase): self.assertEqual(response.status_code, 200) user_consents = UserConsents.objects.filter(user__email=self.test_user_email) - self.assertEqual(len(user_consents), 4) + self.assertEqual(len(user_consents), 8) self.assertFalse( UserConsents.objects.get( - verb="http://h5p.example.com/expapi/verbs/experienced" + verb="http://h5p.example.com/expapi/verbs/experienced", + active=True, ).consented ) self.assertFalse( UserConsents.objects.get( - verb="http://h5p.example.com/expapi/verbs/attempted" + verb="http://h5p.example.com/expapi/verbs/attempted", + active=True, ).consented ) self.assertFalse( UserConsents.objects.get( - verb="http://h5p.example.com/expapi/verbs/interacted" + verb="http://h5p.example.com/expapi/verbs/interacted", + active=True, ).consented ) self.assertTrue( UserConsents.objects.get( - verb="http://h5p.example.com/expapi/verbs/answered" + verb="http://h5p.example.com/expapi/verbs/answered", + active=True, ).consented ) @@ -709,31 +709,56 @@ class TestUserConsentOutdatedProviderSchemaId(BaseTestCase): """ Ensure user consent save is rejected, if referenced provider schema id is outdated. """ + with StringIO(json.dumps(self.h5p_provider_schema)) as fp: + response = self.provider_client.put( + "/api/v1/consents/provider/create", + {"provider-schema": fp}, + format="multipart", + ) + self.assertEqual(response.status_code, 201) + + # create a verb group + group = {"provider_id": Provider.objects.latest('id').id, "id": "default_group", "label": "Group 2", + "description": "Lorem ipsum", "showVerbDetails": True, "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/experienced"}, + {"id": "http://h5p.example.com/expapi/verbs/attempted"}, + {"id": "http://h5p.example.com/expapi/verbs/interacted"}, + {"id": "http://h5p.example.com/expapi/verbs/answered"}, + ]} + response = self.provider_client.post( + "/api/v1/consents/provider/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + group_id = ProviderVerbGroup.objects.latest('id').id + payload = [ { - "providerId": 1, - "providerSchemaId": 1, + "providerId": Provider.objects.latest('id').id, + "providerSchemaId": ProviderSchema.objects.latest('id').id, "verbs": [ { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/experienced", "consented": False, "objects": '[{"id":"http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF","label":"1.1.1 Funktionen","defaultConsent":true,"definition":{"name":{"enUS":"1.1.1 Funktionen"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/attempted", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm","label":"2.3.1 Funktion Zirkulationsleitung","defaultConsent":true,"definition":{"name":{"enUS":"2.3.1 Funktion Zirkulationsleitung"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/interacted", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46","label":"1.2.3 Kappenventil","defaultConsent":true,"definition":{"name":{"enUS":"1.2.3 Kappenventil"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/answered", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b","label":"7.2.1 Ventil Basics","defaultConsent":false,"definition":{"name":{"enUS":"7.2.1 Ventil Basics"}},"consented":true}]', @@ -742,14 +767,6 @@ class TestUserConsentOutdatedProviderSchemaId(BaseTestCase): } ] - with StringIO(json.dumps(self.h5p_provider_schema)) as fp: - response = self.provider_client.put( - "/api/v1/consents/provider/create", - {"provider-schema": fp}, - format="multipart", - ) - self.assertEqual(response.status_code, 201) - # Add newer provider schema for H5P with StringIO(json.dumps(self.h5p_provider_schema_v2)) as fp: response = self.provider_client.put( @@ -762,6 +779,7 @@ class TestUserConsentOutdatedProviderSchemaId(BaseTestCase): response = self.user_client.post( "/api/v1/consents/user/save", payload, format="json" ) + self.assertEqual(response.status_code, 400) self.assertEqual( response.json()["message"], @@ -782,33 +800,48 @@ class TestUserConsentSaveUpdatedProviderSchema(BaseTestCase): format="multipart", ) self.assertEqual(response.status_code, 201) - + # create a verb group + group = {"provider_id": Provider.objects.latest('id').id, "id": "default_group", "label": "Group 2", + "description": "Lorem ipsum", "showVerbDetails": True, "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/experienced"}, + {"id": "http://h5p.example.com/expapi/verbs/attempted"}, + {"id": "http://h5p.example.com/expapi/verbs/interacted"}, + {"id": "http://h5p.example.com/expapi/verbs/answered"}, + ]} + response = self.provider_client.post( + "/api/v1/consents/provider/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + group_id = ProviderVerbGroup.objects.latest('id').id # Create user consent for first provider schema version first_provider_schema_consent = [ { - "providerId": 1, - "providerSchemaId": 1, + "providerId": Provider.objects.latest('id').id, + "providerSchemaId": ProviderSchema.objects.latest('id').id, "verbs": [ { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/experienced", "consented": False, "objects": '[{"id":"http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF","label":"1.1.1 Funktionen","defaultConsent":true,"definition":{"name":{"enUS":"1.1.1 Funktionen"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/attempted", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm","label":"2.3.1 Funktion Zirkulationsleitung","defaultConsent":true,"definition":{"name":{"enUS":"2.3.1 Funktion Zirkulationsleitung"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/interacted", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46","label":"1.2.3 Kappenventil","defaultConsent":true,"definition":{"name":{"enUS":"1.2.3 Kappenventil"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/answered", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b","label":"7.2.1 Ventil Basics","defaultConsent":false,"definition":{"name":{"enUS":"7.2.1 Ventil Basics"}},"consented":true}]', @@ -825,22 +858,26 @@ class TestUserConsentSaveUpdatedProviderSchema(BaseTestCase): self.assertEqual(len(user_consents), 4) self.assertFalse( UserConsents.objects.get( - verb="http://h5p.example.com/expapi/verbs/experienced" + verb__verb_id="http://h5p.example.com/expapi/verbs/experienced", + active=True ).consented ) self.assertTrue( UserConsents.objects.get( - verb="http://h5p.example.com/expapi/verbs/attempted" + verb__verb_id="http://h5p.example.com/expapi/verbs/attempted", + active=True ).consented ) self.assertTrue( UserConsents.objects.get( - verb="http://h5p.example.com/expapi/verbs/interacted" + verb__verb_id="http://h5p.example.com/expapi/verbs/interacted", + active=True ).consented ) self.assertTrue( UserConsents.objects.get( - verb="http://h5p.example.com/expapi/verbs/answered" + verb__verb_id="http://h5p.example.com/expapi/verbs/answered", + active=True ).consented ) @@ -853,19 +890,34 @@ class TestUserConsentSaveUpdatedProviderSchema(BaseTestCase): ) self.assertEqual(response.status_code, 201) + # create a verb group + group = {"provider_id": Provider.objects.latest('id').id, "id": "default_group", "label": "Group 2", + "description": "Lorem ipsum", "showVerbDetails": True, "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/experienced"}, + {"id": "http://h5p.example.com/expapi/verbs/attempted"}, + ]} + response = self.provider_client.post( + "/api/v1/consents/provider/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + group_id = ProviderVerbGroup.objects.latest('id').id + second_provider_schema_consent = [ { - "providerId": 1, - "providerSchemaId": 2, + "providerId": Provider.objects.latest('id').id, + "providerSchemaId": ProviderSchema.objects.latest('id').id, "verbs": [ { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/experienced", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF","label":"1.1.1 Funktionen","defaultConsent":true,"definition":{"name":{"enUS":"1.1.1 Funktionen"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/attempted", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm","label":"2.3.1 Funktion Zirkulationsleitung","defaultConsent":true,"definition":{"name":{"enUS":"2.3.1 Funktion Zirkulationsleitung"}},"consented":true}]', @@ -882,161 +934,41 @@ class TestUserConsentSaveUpdatedProviderSchema(BaseTestCase): self.assertEqual(len(user_consents), 2) self.assertTrue( UserConsents.objects.get( - verb="http://h5p.example.com/expapi/verbs/experienced", + verb__verb_id="http://h5p.example.com/expapi/verbs/experienced", active=True ).consented ) self.assertTrue( UserConsents.objects.get( - verb="http://h5p.example.com/expapi/verbs/attempted", + verb__verb_id="http://h5p.example.com/expapi/verbs/attempted", active=True ).consented ) -class TestUserConsentSave(BaseTestCase): - def test_update_user_consent_fails_incomplete_verb_consents(self): - """ - Ensure user consent gets rejected with incomplete verbs. - """ - # Create first provider schema version - with StringIO(json.dumps(self.h5p_provider_schema)) as fp: - response = self.provider_client.put( - "/api/v1/consents/provider/create", - {"provider-schema": fp}, - format="multipart", - ) - self.assertEqual(response.status_code, 201) - provider = ProviderAuthorization.objects.get(provider__name="H5P") - - # Create consent for one verb instead of 4 verbs - payload = [ - { - "providerId": 1, - "providerSchemaId": 1, - "verbs": [ - { - "provider": 1, - "id": "http://h5p.example.com/expapi/verbs/experienced", - "consented": False, - "objects": '[{"id":"http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF","label":"1.1.1 Funktionen","defaultConsent":true, "matching": "definitionType" ,"definition":{"type": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", "name":{"enUS":"1.1.1 Funktionen"}},"consented":true}]', - }, - { - "provider": 1, - "id": "http://h5p.example.com/expapi/verbs/attempted", - "consented": True, - "objects": '[{"id":"http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm","label":"2.3.1 Funktion Zirkulationsleitung","defaultConsent":true, "matching": "definitionType","definition":{"type": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", "name":{"enUS":"2.3.1 Funktion Zirkulationsleitung"}},"consented":true}]', - }, - { - "provider": 1, - "id": "http://h5p.example.com/expapi/verbs/answered", - "consented": True, - "objects": '[{"id":"http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b","label":"7.2.1 Ventil Basics","defaultConsent":false, "matching": "definitionType","definition":{"type": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", "name":{"enUS":"7.2.1 Ventil Basics"}},"consented":true}]', - }, - ], - } - ] - - response = self.user_client.post( - "/api/v1/consents/user/save", data=payload, format="json" - ) - self.assertEqual(response.status_code, 400) - self.assertJSONEqual( - str(response.content, encoding="utf8"), - {"message": "user consent contains missing provider schema verb consents"}, - ) - - def test_update_user_consent_fails_incomplete_object_consents(self): - """ - Ensure user consent gets rejected with incomplete verb objects. - """ - # Create first provider schema version - with StringIO(json.dumps(self.h5p_provider_schema)) as fp: - response = self.provider_client.put( - "/api/v1/consents/provider/create", - {"provider-schema": fp}, - format="multipart", - ) - self.assertEqual(response.status_code, 201) - provider = ProviderAuthorization.objects.get(provider__name="H5P") - - # Create consent for one verb instead of 4 verbs - payload = [ - { - "providerId": 1, - "providerSchemaId": 1, - "verbs": [ - { - "provider": 1, - "id": "http://h5p.example.com/expapi/verbs/experienced", - "consented": False, - "objects": '[{"id":"http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF","label":"1.1.1 Funktionen","defaultConsent":true,"definition":{"name":{"enUS":"1.1.1 Funktionen"}},"consented":true}]', - }, - { - "provider": 1, - "id": "http://h5p.example.com/expapi/verbs/attempted", - "consented": True, - "objects": '[{"id":"http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm","label":"2.3.1 Funktion Zirkulationsleitung","defaultConsent":true,"definition":{"name":{"enUS":"2.3.1 Funktion Zirkulationsleitung"}},"consented":true}]', - }, - { - "provider": 1, - "id": "http://h5p.example.com/expapi/verbs/interacted", - "consented": True, - "objects": "[]", - }, - { - "provider": 1, - "id": "http://h5p.example.com/expapi/verbs/answered", - "consented": True, - "objects": '[{"id":"http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b","label":"7.2.1 Ventil Basics","defaultConsent":false,"definition":{"name":{"enUS":"7.2.1 Ventil Basics"}},"consented":true}]', - }, - ], - } - ] - - response = self.user_client.post( - "/api/v1/consents/user/save", data=payload, format="json" - ) - self.assertEqual(response.status_code, 400) - self.assertJSONEqual( - str(response.content, encoding="utf8"), - {"message": "user consent contains missing provider schema verb consents"}, - ) - - class TestMultiUserConsent(BaseTestCase): provider_schema = { "id": "h5p-0", "name": "H5P", "description": "Open-source content collaboration framework", - "groups": [ + "verbs": [ { - "id": "default_group", - "label": "Default group", - "description": "default", - "showVerbDetails": True, - "purposeOfCollection": "Lorem Ipsum", - "verbs": [ + "id": "http://h5p.example.com/expapi/verbs/experienced", + "label": "Experienced", + "description": "Experienced", + "defaultConsent": True, + "objects": [ { - "id": "http://h5p.example.com/expapi/verbs/experienced", - "label": "Experienced", - "description": "Experienced", + "id": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", + "label": "1.1.1 Funktionen", "defaultConsent": True, - "objects": [ - { - "id": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", - "label": "1.1.1 Funktionen", - "defaultConsent": True, - "matching": "definitionType", - "definition": { - "type": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", - "name": {"enUS": "1.1.1 Funktionen"}, - }, - } - ], - }, + "matching": "definitionType", + "definition": { + "type": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", + "name": {"enUS": "1.1.1 Funktionen"}, + }, + } ], - "isDefault": True, }, ], "essentialVerbs": [], @@ -1101,13 +1033,27 @@ class TestMultiUserConsent(BaseTestCase): ) self.assertEqual(response.status_code, 201) + # create a verb group + group = {"provider_id": Provider.objects.latest('id').id, "id": "default_group", "label": "Group 2", + "description": "Lorem ipsum", "showVerbDetails": True, "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/experienced"}, + ]} + response = self.provider_client.post( + "/api/v1/consents/provider/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + group_id = ProviderVerbGroup.objects.latest('id').id + user_consent_1 = [ { - "providerId": 1, - "providerSchemaId": 1, + "providerId": Provider.objects.latest('id').id, + "providerSchemaId": ProviderSchema.objects.latest('id').id, "verbs": [ { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/experienced", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF","label":"1.1.1 Funktionen","defaultConsent":true, "matching": "definitionType" ,"definition":{"type": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", "name":{"enUS":"1.1.1 Funktionen"}},"consented":true}]', @@ -1117,11 +1063,11 @@ class TestMultiUserConsent(BaseTestCase): ] user_consent_2 = [ { - "providerId": 1, - "providerSchemaId": 1, + "providerId": Provider.objects.latest('id').id, + "providerSchemaId": ProviderSchema.objects.latest('id').id, "verbs": [ { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/experienced", "consented": False, "objects": '[{"id":"http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF","label":"1.1.1 Funktionen","defaultConsent":true, "matching": "definitionType" ,"definition":{"type": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", "name":{"enUS":"1.1.1 Funktionen"}},"consented":true}]', @@ -1145,12 +1091,12 @@ class TestMultiUserConsent(BaseTestCase): self.assertTrue( UserConsents.objects.get( user__email=self.user_email_1, - verb="http://h5p.example.com/expapi/verbs/experienced", + verb__verb_id="http://h5p.example.com/expapi/verbs/experienced", ).consented ) self.assertFalse( UserConsents.objects.get( user__email=self.user_email_2, - verb="http://h5p.example.com/expapi/verbs/experienced", + verb__verb_id="http://h5p.example.com/expapi/verbs/experienced", ).consented ) diff --git a/src/consents/tests/tests_create_provider_schema.py b/src/consents/tests/tests_create_provider_schema.py index 0a9d0d5..22e583a 100644 --- a/src/consents/tests/tests_create_provider_schema.py +++ b/src/consents/tests/tests_create_provider_schema.py @@ -6,95 +6,101 @@ from providers.models import Provider class TestProviderSchemaCreation(BaseTestCase): - def test_create_provider_schema_fails(self): + def test_create_provider_verb_groups(self): """ - Ensure provider schema with duplicate group ids fails. + Test verb group creation and update. """ provider_schema = { "id": "h5p-0", "name": "H5P", "description": "Open-source content collaboration framework", - "groups": [ + "verbs": [ { - "id": "default_group", - "label": "Default group", - "description": "default", - "showVerbDetails": True, - "purposeOfCollection": "Lorem Ipsum", - "verbs": [ + "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked", + "label": "Unlocked", + "description": "Actor unlocked an object", + "defaultConsent": True, + "objects": [ { - "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked", - "label": "Unlocked", - "description": "Actor unlocked an object", + "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/my-random-object-id", + "label": "Course", "defaultConsent": True, - "objects": [ - { - "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/my-random-object-id", - "label": "Course", - "defaultConsent": True, - "matching": "id", - "definition": { - "type": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/course", - "name": { - "enUS": "A course within an LMS. Contains learning materials and activities" - }, - }, + "matching": "id", + "definition": { + "type": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/course", + "name": { + "enUS": "A course within an LMS. Contains learning materials and activities" }, - ], - } + }, + }, ], - "isDefault": True, }, { - "id": "default_group", - "label": "Group 2", + "id": "http://h5p.example.com/expapi/verbs/interacted", + "label": "Interacted", "description": "Lorem ipsum", - "showVerbDetails": True, - "purposeOfCollection": "Lorem Ipsum", - "verbs": [ + "defaultConsent": True, + "objects": [ { - "id": "http://h5p.example.com/expapi/verbs/interacted", - "label": "Interacted", - "description": "Lorem ipsum", + "id": "http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46", + "label": "1.2.3 Kappenventil", "defaultConsent": True, - "objects": [ - { - "id": "http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46", - "label": "1.2.3 Kappenventil", - "defaultConsent": True, - "matching": "definitionType", - "definition": { - "type": "http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46", - "name": {"enUS": "1.2.3 Kappenventil"}, - }, - } - ], - }, + "matching": "definitionType", + "definition": { + "type": "http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46", + "name": {"enUS": "1.2.3 Kappenventil"}, + }, + } + ], + }, + { + "id": "http://h5p.example.com/expapi/verbs/answered", + "label": "Answered", + "description": "lorem ipsum", + "defaultConsent": False, + "objects": [ { - "id": "http://h5p.example.com/expapi/verbs/answered", - "label": "Answered", - "description": "lorem ipsum", + "id": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", + "label": "7.2.1 Ventil Basics", "defaultConsent": False, - "objects": [ - { - "id": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", - "label": "7.2.1 Ventil Basics", - "defaultConsent": False, - "matching": "definitionType", - "definition": { - "type": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", - "name": {"enUS": "7.2.1 Ventil Basics"}, - }, - } - ], - }, + "matching": "definitionType", + "definition": { + "type": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", + "name": {"enUS": "7.2.1 Ventil Basics"}, + }, + } ], - "isDefault": False, - }, + } ], "essentialVerbs": [], } + verb_groups = [ + { + "id": "default_group", + "label": "Default group", + "description": "default", + "showVerbDetails": True, + "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, + "verbs": [ + {"id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked"} + ] + }, + { + "id": "default_group", + "label": "Group 2", + "description": "Lorem ipsum", + "showVerbDetails": True, + "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, + "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/interacted"}, + {"id": "http://h5p.example.com/expapi/verbs/answered"}, + ] + } + ] + # Upload provider schema with StringIO(json.dumps(provider_schema)) as fp: response = self.provider_client.put( @@ -102,7 +108,22 @@ class TestProviderSchemaCreation(BaseTestCase): {"provider-schema": fp}, format="multipart", ) - self.assertEqual(response.status_code, 400) - self.assertEqual( - response.json()["message"], "Provider schema contains ambiguous group ids." + self.assertEqual(response.status_code, 201) + # create some verb groups + for group in verb_groups: + group["provider_id"] = Provider.objects.latest('id').id + response = self.provider_client.post( + "/api/v1/consents/provider/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + # one more time to simulate update + for group in verb_groups: + group["provider_id"] = Provider.objects.latest('id').id + response = self.provider_client.post( + "/api/v1/consents/provider/create-verb-group", + group, + format="json", ) + self.assertEqual(response.status_code, 200) \ No newline at end of file diff --git a/src/consents/tests/tests_paused_data_recording.py b/src/consents/tests/tests_paused_data_recording.py index fe451a4..c68ef55 100644 --- a/src/consents/tests/tests_paused_data_recording.py +++ b/src/consents/tests/tests_paused_data_recording.py @@ -2,7 +2,7 @@ import json from io import StringIO from consents.tests.tests_consent_operations import BaseTestCase -from providers.models import Provider +from providers.models import Provider, ProviderVerbGroup class TestPauseDataRecording(BaseTestCase): @@ -32,7 +32,7 @@ class TestPauseDataRecording(BaseTestCase): def test_pause_data_recording(self): """ - Ensure data recording can be turn on and off. + Ensure data recording can be turned on and off. """ # Create provider H5P with StringIO(json.dumps(self.h5p_provider_schema)) as fp: @@ -97,31 +97,48 @@ class TestPauseDataRecording(BaseTestCase): {"consent": None, "paused_data_recording": True} ) + # create a verb group + group = {"provider_id": Provider.objects.latest('id').id, "id": "default_group", "label": "Group 2", + "description": "Lorem ipsum", "showVerbDetails": True, "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/experienced"}, + {"id": "http://h5p.example.com/expapi/verbs/attempted"}, + {"id": "http://h5p.example.com/expapi/verbs/interacted"}, + {"id": "http://h5p.example.com/expapi/verbs/answered"}, + ]} + response = self.provider_client.post( + "/api/v1/consents/provider/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + group_id = ProviderVerbGroup.objects.latest('id').id + user_consent = [ { "providerId": 1, "providerSchemaId": 1, "verbs": [ { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/experienced", "consented": False, "objects": '[{"id":"http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF","label":"1.1.1 Funktionen","defaultConsent":true,"definition":{"name":{"enUS":"1.1.1 Funktionen"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/attempted", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm","label":"2.3.1 Funktion Zirkulationsleitung","defaultConsent":true,"definition":{"name":{"enUS":"2.3.1 Funktion Zirkulationsleitung"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/interacted", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46","label":"1.2.3 Kappenventil","defaultConsent":true,"definition":{"name":{"enUS":"1.2.3 Kappenventil"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/answered", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b","label":"7.2.1 Ventil Basics","defaultConsent":false,"definition":{"name":{"enUS":"7.2.1 Ventil Basics"}},"consented":true}]', diff --git a/src/consents/tests/tests_third_party.py b/src/consents/tests/tests_third_party.py index 6c704f5..fcbf91e 100644 --- a/src/consents/tests/tests_third_party.py +++ b/src/consents/tests/tests_third_party.py @@ -6,13 +6,30 @@ from rest_framework.test import APIClient from rolepermissions.roles import assign_role from consents.tests.tests_consent_operations import BaseTestCase -from providers.models import ProviderAuthorization +from providers.models import ProviderAuthorization, Provider, ProviderVerbGroup, ProviderSchema from users.models import CustomUser PROJECT_PATH = os.path.abspath(os.path.dirname(__name__)) class TestThirdPartyGetUserStatus(BaseTestCase): + verb_groups = [ + { + "provider_id": 1, + "id": "default_group", + "label": "Default group", + "description": "default", + "showVerbDetails": True, + "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, + "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/experienced"}, + {"id": "http://h5p.example.com/expapi/verbs/attempted"}, + {"id": "http://h5p.example.com/expapi/verbs/interacted"}, + {"id": "http://h5p.example.com/expapi/verbs/answered"} + ] + }, + ] def setUp(self): normal_user = CustomUser.objects.create_user( self.test_user_email, self.test_user_password @@ -69,6 +86,16 @@ class TestThirdPartyGetUserStatus(BaseTestCase): HTTP_AUTHORIZATION="Basic " + self.application_token ) + # create some verb groups + for group in self.verb_groups: + group["provider_id"] = Provider.objects.latest('id').id # database id is 2 on second go bc autoincrement + response = self.provider_client.post( + "/api/v1/consents/provider/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + def test_user_status_401(self): """ Ensure requests including an invalid application token are rejected. @@ -101,6 +128,7 @@ class TestThirdPartyGetUserStatus(BaseTestCase): ) def test_user_status_none_empty(self): + group_id = ProviderVerbGroup.objects.latest('id').id # Create user consent for first provider schema version first_provider_schema_consent = [ { @@ -108,25 +136,25 @@ class TestThirdPartyGetUserStatus(BaseTestCase): "providerSchemaId": 1, "verbs": [ { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/experienced", "consented": False, "objects": '[{"id":"http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF","label":"1.1.1 Funktionen","defaultConsent":true,"definition":{"name":{"enUS":"1.1.1 Funktionen"}},"consented":false}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/attempted", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm","label":"2.3.1 Funktion Zirkulationsleitung","defaultConsent":true,"definition":{"name":{"enUS":"2.3.1 Funktion Zirkulationsleitung"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/interacted", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46","label":"1.2.3 Kappenventil","defaultConsent":true,"definition":{"name":{"enUS":"1.2.3 Kappenventil"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/answered", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b","label":"7.2.1 Ventil Basics","defaultConsent":false,"definition":{"name":{"enUS":"7.2.1 Ventil Basics"}},"consented":true}]', @@ -157,6 +185,23 @@ class TestThirdPartyGetUserStatus(BaseTestCase): class TestThirdPartyUserConsentUpdate(BaseTestCase): + verb_groups = [ + { + "provider_id": 1, + "id": "default_group", + "label": "Default group", + "description": "default", + "showVerbDetails": True, + "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, + "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/experienced"}, + {"id": "http://h5p.example.com/expapi/verbs/attempted"}, + {"id": "http://h5p.example.com/expapi/verbs/interacted"}, + {"id": "http://h5p.example.com/expapi/verbs/answered"} + ] + }, + ] def setUp(self): normal_user = CustomUser.objects.create_user( self.test_user_email, self.test_user_password @@ -212,37 +257,46 @@ class TestThirdPartyUserConsentUpdate(BaseTestCase): self.third_party_client.credentials( HTTP_AUTHORIZATION="Basic " + self.application_token ) + # create some verb groups + for group in self.verb_groups: + group["provider_id"] = Provider.objects.latest('id').id # database id is 2 on second go bc autoincrement + response = self.provider_client.post( + "/api/v1/consents/provider/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) def test_update_user_consent(self): """ Ensure user consent gets updated via third party endpoint. """ provider = ProviderAuthorization.objects.get(provider__name="H5P") - + group_id = ProviderVerbGroup.objects.latest('id').id payload = { "user_id": self.test_user_email, "provider_schema_id": provider.id, "verbs": [ { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/experienced", "consented": False, "objects": '[{"id":"http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF","label":"1.1.1 Funktionen","defaultConsent":true,"definition":{"name":{"enUS":"1.1.1 Funktionen"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/attempted", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm","label":"2.3.1 Funktion Zirkulationsleitung","defaultConsent":true,"definition":{"name":{"enUS":"2.3.1 Funktion Zirkulationsleitung"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/interacted", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46","label":"1.2.3 Kappenventil","defaultConsent":true,"definition":{"name":{"enUS":"1.2.3 Kappenventil"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/answered", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b","label":"7.2.1 Ventil Basics","defaultConsent":false,"definition":{"name":{"enUS":"7.2.1 Ventil Basics"}},"consented":true}]', @@ -271,22 +325,23 @@ class TestThirdPartyUserConsentUpdate(BaseTestCase): responseDict["consent"]["groups"][0]["verbs"][1]["consented"], True ) self.assertEqual( - responseDict["consent"]["groups"][1]["verbs"][0]["consented"], True + responseDict["consent"]["groups"][0]["verbs"][2]["consented"], True ) self.assertEqual( - responseDict["consent"]["groups"][1]["verbs"][0]["consented"], True + responseDict["consent"]["groups"][0]["verbs"][3]["consented"], True ) def test_update_user_consent_fails_without_access_token(self): """ Ensure unauthenticated request gets rejected. """ + group_id = ProviderVerbGroup.objects.latest('id').id payload = { "user_id": self.test_user_email, "provider_schema_id": 1, "verbs": [ { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/experienced", "consented": False, "objects": '[{"id":"http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF","label":"1.1.1 Funktionen","defaultConsent":true,"definition":{"name":{"enUS":"1.1.1 Funktionen"}},"consented":true}]"},{"provider":1,"id":"http://h5p.example.com/expapi/verbs/attempted","consented":true,"objects":"[{"id":"http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm","label":"2.3.1 Funktion Zirkulationsleitung","defaultConsent":true,"definition":{"name":{"enUS":"2.3.1 Funktion Zirkulationsleitung"}},"consented":true}]"},{"provider":1,"id":"http://h5p.example.com/expapi/verbs/interacted","consented":true,"objects":"[{"id":"http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46","label":"1.2.3 Kappenventil","defaultConsent":true,"definition":{"name":{"enUS":"1.2.3 Kappenventil"}},"consented":true}]"},{"provider":1,"id":"http://h5p.example.com/expapi/verbs/answered","consented":false,"objects":"[{"id":"http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b","label":"7.2.1 Ventil Basics","defaultConsent":false,"definition":{"name":{"enUS":"7.2.1 Ventil Basics"}},"consented":false}]', @@ -306,14 +361,15 @@ class TestThirdPartyUserConsentUpdate(BaseTestCase): def test_update_on_none_existing_provider_fails(self): """ - Ensure attempt to update verb on none existing provider fails. + Ensure attempt to update verb on non-existing provider fails. """ + group_id = ProviderVerbGroup.objects.latest('id').id payload = { "user_id": self.test_user_email, - "provider_schema_id": 1, + "provider_schema_id": 2, "verbs": [ { - "provider": 2, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/experienced", "consented": False, "objects": '[{"id":"http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF","label":"1.1.1 Funktionen","defaultConsent":true,"definition":{"name":{"enUS":"1.1.1 Funktionen"}},"consented":true}]"},{"provider":1,"id":"http://h5p.example.com/expapi/verbs/attempted","consented":true,"objects":"[{"id":"http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm","label":"2.3.1 Funktion Zirkulationsleitung","defaultConsent":true,"definition":{"name":{"enUS":"2.3.1 Funktion Zirkulationsleitung"}},"consented":true}]"},{"provider":1,"id":"http://h5p.example.com/expapi/verbs/interacted","consented":true,"objects":"[{"id":"http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46","label":"1.2.3 Kappenventil","defaultConsent":true,"definition":{"name":{"enUS":"1.2.3 Kappenventil"}},"consented":true}]"},{"provider":1,"id":"http://h5p.example.com/expapi/verbs/answered","consented":false,"objects":"[{"id":"http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b","label":"7.2.1 Ventil Basics","defaultConsent":false,"definition":{"name":{"enUS":"7.2.1 Ventil Basics"}},"consented":false}]', @@ -325,121 +381,3 @@ class TestThirdPartyUserConsentUpdate(BaseTestCase): "/api/v1/consents/user/save/third-party", data=payload, format="json" ) self.assertEqual(response.status_code, 400) - - def test_update_other_provider_fails(self): - """ - Ensure you are not authorized to access providers other than your own. - """ - - # Create provider Moodle - with StringIO(json.dumps(self.moodle_schema)) as fp: - response = self.provider_client.put( - "/api/v1/consents/provider/create", - {"provider-schema": fp}, - format="multipart", - ) - self.assertEqual(response.status_code, 201) - - moodle_provider_id = ProviderAuthorization.objects.get( - provider__name="Moodle" - ).provider.id - - payload = { - "user_id": self.test_user_email, - "provider_schema_id": 1, - "verbs": [ - { - "provider": moodle_provider_id, - "id": "http://h5p.example.com/expapi/verbs/experienced", - "consented": False, - "objects": '[{"id":"http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF","label":"1.1.1 Funktionen","defaultConsent":true,"definition":{"name":{"enUS":"1.1.1 Funktionen"}},"consented":true}]"},{"provider":1,"id":"http://h5p.example.com/expapi/verbs/attempted","consented":true,"objects":"[{"id":"http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm","label":"2.3.1 Funktion Zirkulationsleitung","defaultConsent":true,"definition":{"name":{"enUS":"2.3.1 Funktion Zirkulationsleitung"}},"consented":true}]"},{"provider":1,"id":"http://h5p.example.com/expapi/verbs/interacted","consented":true,"objects":"[{"id":"http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46","label":"1.2.3 Kappenventil","defaultConsent":true,"definition":{"name":{"enUS":"1.2.3 Kappenventil"}},"consented":true}]"},{"provider":1,"id":"http://h5p.example.com/expapi/verbs/answered","consented":false,"objects":"[{"id":"http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b","label":"7.2.1 Ventil Basics","defaultConsent":false,"definition":{"name":{"enUS":"7.2.1 Ventil Basics"}},"consented":false}]', - } - ], - } - - response = self.third_party_client.post( - "/api/v1/consents/user/save/third-party", data=payload, format="json" - ) - self.assertEqual(response.status_code, 403) - self.assertJSONEqual( - str(response.content, encoding="utf8"), - { - "message": "you are not authorized to access providers other than your own" - }, - ) - - def test_update_user_consent_fails_incomplete_verb_consents(self): - """ - Ensure user consent gets with incomplete verbs. - """ - provider = ProviderAuthorization.objects.get(provider__name="H5P") - - # Create consent for one verb instead of 4 verbs - payload = { - "user_id": self.test_user_email, - "provider_schema_id": provider.id, - "verbs": [ - { - "provider": 1, - "id": "http://h5p.example.com/expapi/verbs/experienced", - "consented": False, - "objects": '[{"id":"http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF","label":"1.1.1 Funktionen","defaultConsent":true,"definition":{"name":{"enUS":"1.1.1 Funktionen"}},"consented":true}]', - } - ], - } - - response = self.third_party_client.post( - "/api/v1/consents/user/save/third-party", data=payload, format="json" - ) - self.assertEqual(response.status_code, 400) - self.assertJSONEqual( - str(response.content, encoding="utf8"), - {"message": "user consent contains missing provider schema verb consents"}, - ) - - def test_update_user_consent_fails_incomplete_object_consents(self): - """ - Ensure user consent gets with incomplete verb objects. - """ - provider = ProviderAuthorization.objects.get(provider__name="H5P") - - # Create consent for one verb instead of 4 verbs - payload = { - "user_id": self.test_user_email, - "provider_schema_id": provider.id, - "verbs": [ - { - "provider": 1, - "id": "http://h5p.example.com/expapi/verbs/experienced", - "consented": False, - "objects": '[{"id":"http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF","label":"1.1.1 Funktionen","defaultConsent":true,"definition":{"name":{"enUS":"1.1.1 Funktionen"}},"consented":true}]', - }, - { - "provider": 1, - "id": "http://h5p.example.com/expapi/verbs/attempted", - "consented": True, - "objects": "[]", - }, - { - "provider": 1, - "id": "http://h5p.example.com/expapi/verbs/interacted", - "consented": True, - "objects": '[{"id":"http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46","label":"1.2.3 Kappenventil","defaultConsent":true,"definition":{"name":{"enUS":"1.2.3 Kappenventil"}},"consented":true}]', - }, - { - "provider": 1, - "id": "http://h5p.example.com/expapi/verbs/answered", - "consented": True, - "objects": '[{"id":"http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b","label":"7.2.1 Ventil Basics","defaultConsent":false,"definition":{"name":{"enUS":"7.2.1 Ventil Basics"}},"consented":true}]', - }, - ], - } - - response = self.third_party_client.post( - "/api/v1/consents/user/save/third-party", data=payload, format="json" - ) - self.assertEqual(response.status_code, 400) - self.assertJSONEqual( - str(response.content, encoding="utf8"), - {"message": "user consent contains missing provider schema verb consents"}, - ) diff --git a/src/consents/urls.py b/src/consents/urls.py index 54c1ed5..647a378 100644 --- a/src/consents/urls.py +++ b/src/consents/urls.py @@ -6,6 +6,8 @@ urlpatterns = [ path('provider', views.GetProviderSchemasView.as_view()), path('provider-status/third-party', views.GetProviderStatusThirdPartyView.as_view()), path('provider/create', views.CreateProviderConsentView.as_view()), + path('provider/<provider_id>/create-verb-group', views.CreateProviderVerbGroupView.as_view()), # TODO + path('provider/<provider_id>/verb-groups', views.GetProviderVerbGroupsView.as_view()), # TODO path('user/save', views.SaveUserConsentView.as_view()), path('user/create', views.CreateUserConsentView.as_view()), path('user/create-via-connect-service', views.CreateUserConsentViaConnectServiceView.as_view()), diff --git a/src/consents/views.py b/src/consents/views.py index 51b1807..005a548 100644 --- a/src/consents/views.py +++ b/src/consents/views.py @@ -1,3 +1,4 @@ +import hashlib import json import string import random @@ -5,6 +6,7 @@ import os import secrets import time from datetime import timedelta, datetime +from pydoc import describe from django.core.cache import cache from django.conf import settings @@ -20,8 +22,11 @@ from rest_framework.views import APIView from backend.role_permission import IsProvider from consents.serializers import (ProviderSchemaSerializer, ProvidersSerializer, - SaveUserConsentSerializer, CreateUserSerializer, CreateUserShibbolethSerializer) -from providers.models import AnalyticsToken, AnalyticsTokenVerb, Provider, ProviderAuthorization, ProviderSchema + SaveUserConsentSerializer, CreateUserSerializer, CreateUserShibbolethSerializer, + ProviderVerbGroupSerializer, VerbObjectSerializer, VerbSerializer, + CreateProviderVerbGroupSerializer) +from providers.models import (AnalyticsToken, AnalyticsTokenVerb, Provider, ProviderAuthorization, ProviderSchema, Verb, + VerbObject, ProviderVerbGroup) from providers.serializers import (AnalyticsTokenSerializer, ConsentUserVerbThirdPartySerializer, GetUsersConsentsThirdPartySerializer) from users.models import CustomUser @@ -51,7 +56,7 @@ def render_provider_consent(provider_schema, supersedes=None): def find_user_verb(verb_id, user_verbs): for user_verb in user_verbs: - if user_verb.get("verb") == verb_id: + if user_verb.verb.verb_id == verb_id: return user_verb return None @@ -61,19 +66,24 @@ def merge_consents(provider_schema: ProviderSchema, user_verbs): """ Merges provider schema with actual user consent settings """ + #print(user_verbs) user_consent = { "id": provider_schema.provider.id, "name": provider_schema.provider.name, "description": provider_schema.provider.description, - "groups": provider_schema.groups, - "essential_verbs": provider_schema.essential_verbs, + "groups": ProviderVerbGroupSerializer(provider_schema.groups(), many=True).data, # all available groups + "essential_verbs": VerbSerializer(provider_schema.essential_verbs(), many=True).data, } for group in user_consent.get("groups"): - for verb in group.get("verbs", []): - user_verb = find_user_verb(verb.get("id"), user_verbs) + all_consented = True + for verb in group.get('verbs'): + user_verb = find_user_verb(verb["id"], user_verbs) if user_verb: - verb.update({"consented": user_verb.get("consented")}) - verb.update({"objects": json.loads(user_verb.get("object"))}) + verb.update({"consented": user_verb.consented}) + verb.update({"objects": json.loads(user_verb.object)}) + all_consented = all_consented and user_verb.consented + else: all_consented = False + group["consented"] = all_consented return user_consent @@ -84,9 +94,9 @@ def get_user_consent(user, provider_id): user_consents = UserConsents.objects.filter(user=user, provider__pk=provider_id, active=True) if user_consents.first(): - provider_schema = user_consents.first().provider_schema + provider_schema = user_consents.first().verb.provider_schema user_consent_definition = merge_consents( - provider_schema, user_consents.values() + provider_schema, user_consents ) return user_consent_definition return None @@ -94,72 +104,29 @@ def get_user_consent(user, provider_id): def save_user_consent(user, provider_schema, verbs): """ - Inactivates existing user consents for user and provider. Afterwards, new user consents are created. + Inactivates existing user consents for user and provider. + Afterward, new user consents are created. """ old_user_consents = UserConsents.objects.filter( user=user, provider=provider_schema.provider.id ).update(active=False) - for verb in verbs: + for verb_data in verbs: + verb = Verb.objects.get(verb_id=verb_data["id"], provider_schema=provider_schema, active=True) try: - UserConsents.objects.get(user=user, verb=verb["id"], provider=provider_schema.provider.id, active=True) + UserConsents.objects.get(user=user, verb=verb, provider=provider_schema.provider, active=True) # user consent for verb already exists, skip except UserConsents.DoesNotExist: - provider = Provider.objects.get(pk=provider_schema.provider.id) UserConsents.objects.create( user=user, - consented=verb.get("consented"), - provider_schema=provider_schema, - provider=provider, - verb=verb.get("id"), - object=verb.get("objects"), + consented=verb_data.get("consented"), + provider=provider_schema.provider, + verb_group=verb_data["group_id"], + verb=verb, + object=verb_data.get("objects"), ) -def is_provider_verb_or_object_missing( - provider_schema: ProviderSchema, user_consent: dict -) -> bool: - """ - Validate all provider schema verbs and objects are present in user consent - """ - provider_schema_verbs = {} - - for group in provider_schema.groups: - for verb in group["verbs"]: - provider_schema_verbs[verb["id"]] = [ - object["id"] for object in verb["objects"] - ] - - for provider_verb_id, provider_object_ids in provider_schema_verbs.items(): - user_verb_consent = next( - (verb for verb in user_consent["verbs"] if verb["id"] == provider_verb_id), - None, - ) - - if user_verb_consent is None: - # Missing provider schema verb id in user consent - return True - - user_verb_consent_objects_id = [ - object["id"] - for object in json.loads(user_verb_consent.get("objects", "[]")) - ] - - if any( - object_id not in user_verb_consent_objects_id - for object_id in provider_object_ids - ): - # Missing provider schema object id in user consent - return True - - return False - - -def validate_distinct_group_ids(provider_schema): - group_ids = [group["id"] for group in provider_schema["groups"]] - if len(group_ids) != len(set(group_ids)): - raise ValueError("Provider schema contains ambiguous group ids.") - def group_consents_by_date(consents): """ @@ -209,6 +176,24 @@ class GetProviderSchemasView(generics.ListAPIView): permission_classes = [IsAuthenticated] +class GetProviderVerbGroupsView(generics.ListAPIView): + """ + Lists all verb groups for the current provider. + """ + def get(self, request): + application_token = request.headers.get("Authorization", "").split("Basic ")[-1] + provider = ProviderAuthorization.objects.filter(key=application_token).first() + if provider is None: + return JsonResponse( + {"message": "invalid access token"}, + safe=False, + status=status.HTTP_401_UNAUTHORIZED, + ) + verb_groups = ProviderVerbGroup.objects.filter(provider=provider.provider).all() + serializer = ProviderVerbGroupSerializer(verb_groups, many=True) + return JsonResponse(serializer.data, safe=False, status=status.HTTP_200_OK) + + class GetProviderStatusThirdPartyView(APIView): """ Allows a third party to query the details of one's provider. @@ -229,7 +214,8 @@ class GetProviderStatusThirdPartyView(APIView): class CreateProviderConsentView(APIView): """ - Providers can upload provider schemas. In case a provider schema already exists, the existing provider schema references the new provider schema. + Providers can upload provider schemas. In case a provider schema already exists, + the existing provider schema will reference the new provider schema. The latest provider schema has no superseding provider schema associated. """ @@ -252,8 +238,6 @@ class CreateProviderConsentView(APIView): provider_schema = json.load(file.file) validate(instance=provider_schema, schema=json.load(f)) - validate_distinct_group_ids(provider_schema) - providerModel = Provider.objects.get( definition_id=provider_schema["id"] ) # maybe TODO: match via name, not definition_id, iff definition_id can change each definition @@ -291,44 +275,132 @@ class CreateProviderConsentView(APIView): ProviderAuthorization.objects.create(provider=providerModel, key=auth_key) - # Create new provider schema and link older schema version to new one (if old one exists) + # update provider + providerModel.description = provider_schema["description"] + providerModel.name = provider_schema["name"] + providerModel.save() + try: precedingProviderSchema = ProviderSchema.objects.get( provider=providerModel, superseded_by__isnull=True ) - - if precedingProviderSchema: - providerSchemaModel = ProviderSchema.objects.create( - provider=providerModel, - groups=provider_schema["groups"], - essential_verbs=provider_schema["essentialVerbs"], - additional_lrs=provider_schema["additionalLrs"] if "additionalLrs" in provider_schema.keys() else [] - ) - precedingProviderSchema.superseded_by = providerSchemaModel - precedingProviderSchema.save() - - provider = precedingProviderSchema.provider - provider.description = provider_schema["description"] - provider.name=provider_schema["name"] - provider.save() - except ObjectDoesNotExist: - providerSchemaModel = ProviderSchema.objects.create( + precedingProviderSchema = None + # Create new provider schema and link older schema version to new one (if old one exists) + providerSchemaModel = ProviderSchema.objects.create( + provider=providerModel, + additional_lrs=provider_schema["additionalLrs"] if "additionalLrs" in provider_schema.keys() else [] + ) + # create regular verbs + for verb_schema in provider_schema["verbs"]: + verb = Verb.objects.create( + provider=providerModel, + provider_schema=providerSchemaModel, + verb_id=verb_schema["id"], + label=verb_schema["label"], + description=verb_schema["description"], + default_consent=verb_schema["defaultConsent"], + essential=False + ) + for verb_object_schema in verb_schema["objects"]: + obj = VerbObject.objects.create( + verb=verb, + object_id=verb_object_schema["id"], + object_type=verb_object_schema["objectType"] if "objectType" in verb_object_schema.keys() else "Activity", + matching=verb_object_schema["matching"] if "matching" in verb_object_schema.keys() else "definitionType", + definition=verb_object_schema["definition"]) + # create essential verbs + for verb_schema in provider_schema["essentialVerbs"]: + verb = Verb.objects.create( provider=providerModel, - groups=provider_schema["groups"], - essential_verbs=provider_schema["essentialVerbs"], - additional_lrs=provider_schema["additionalLrs"] if "additionalLrs" in provider_schema.keys() else [] + provider_schema=providerSchemaModel, + verb_id=verb_schema["id"], + label=verb_schema["label"], + description=verb_schema["description"], + default_consent=verb_schema["defaultConsent"], + essential=True, ) + for verb_object_schema in verb_schema["objects"]: + obj = VerbObject.objects.create( + verb=verb, + object_id=verb_object_schema["id"], + object_type=verb_object_schema["objectType"] if "objectType" in verb_object_schema.keys() else "Activity", + matching=verb_object_schema["matching"] if "matching" in verb_object_schema.keys() else "definitionType", + definition=verb_object_schema["definition"] + ) + # update old schema to reference the new one + if precedingProviderSchema: + Verb.objects.filter(provider=providerModel, provider_schema=precedingProviderSchema).update( + active=False) + precedingProviderSchema.superseded_by = providerSchemaModel + precedingProviderSchema.save() + Verb.objects.filter(provider_schema=precedingProviderSchema).update(active=False) + return JsonResponse( {"message": "provider schema processed"}, status=status.HTTP_201_CREATED ) +class CreateProviderVerbGroupView(APIView): + permission_classes = [IsAuthenticated, IsProvider] + + def post(self, request, provider_id): + provider = Provider.objects.get(id=provider_id) + serializer = CreateProviderVerbGroupSerializer(data=request.data, provider=provider) + serializer.is_valid(raise_exception=True) + + provider_schema = ProviderSchema.objects.get(provider=provider, superseded_by__isnull=True) + + verbs = Verb.objects.filter(verb_id__in=[verb["id"] for verb in serializer.validated_data["verbs"]], + active=True, provider=provider, provider_schema=provider_schema).all() + if len(verbs) != len(serializer.validated_data["verbs"]): + return JsonResponse( + {"message": "at least one of the given verbs could not be found"}, + safe=False, + status=status.HTTP_400_BAD_REQUEST + ) + + # update existing group + if ProviderVerbGroup.objects.filter(provider=provider, provider_schema=provider_schema, + group_id=serializer.validated_data["id"]).first() is not None: + group = ProviderVerbGroup.objects.get(group_id=serializer.validated_data["id"], + provider=provider, provider_schema=provider_schema) + group.group_id = serializer.validated_data["id"] + group.label = serializer.validated_data["label"] + group.description = serializer.validated_data["description"] + group.purpose_of_collection = serializer.validated_data["purposeOfCollection"] + group.requires_consent = serializer.validated_data["requiresConsent"] + group.save() + else: + # create new group + group = ProviderVerbGroup.objects.create( + provider = provider, + provider_schema = provider_schema, + group_id = hashlib.sha3_256(serializer.validated_data["label"].encode("utf-8")).hexdigest(), + label = serializer.validated_data["label"], + description = serializer.validated_data["description"], + purpose_of_collection = serializer.validated_data["purposeOfCollection"], + requires_consent = serializer.validated_data["requiresConsent"], + ) + # sync verbs + group.verbs.set(verbs) + + return JsonResponse( + { + "message": "group has been saved", + "group": ProviderVerbGroupSerializer(group).data, + }, + safe=False, + status=status.HTTP_200_OK, + ) + + class GetUserConsentView(APIView): """ - Users are able to make a consent decision for a specific provider schema (always the latest version at time). - This endpoint returns the user consent along with the associated provider schema, which might not be the latest version anymore. + Users are able to make a consent decision for verb groups. + This endpoint returns the user consents along with the associated verb groups for a given provider, + which may not always belong to the latest version of the corresponding provider schema. """ permission_classes = (IsAuthenticated,) @@ -410,21 +482,21 @@ class GetUserConsentHistoryView(APIView): "consents": [] } for provider_id, consents in consent_group["consents"].items(): - provider_schema = consents[0].provider_schema + provider_schema = consents[0].verb.provider_schema user_consent = { "id": provider_schema.provider.id, "name": provider_schema.provider.name, "description": provider_schema.provider.description, - "groups": provider_schema.groups, - "essential_verbs": provider_schema.essential_verbs, + "groups": ProviderVerbGroupSerializer(provider_schema.groups(), many=True).data, + "essential_verbs": VerbSerializer(provider_schema.essential_verbs(), many=True).data, "created": consent_group["created"], } for group in user_consent.get("groups"): - for verb in group.get("verbs", []): - user_verb = find_user_verb(verb.get("id"), [consent.__dict__ for consent in consents]) + for verb in group.get("verbs"): + user_verb = find_user_verb(verb["id"], consents) if user_verb: - verb.update({"consented": user_verb.get("consented")}) - verb.update({"objects": json.loads(user_verb.get("object"))}) + verb.update({"consented": user_verb.consented}) + verb.update({"objects": json.loads(user_verb.object)}) consent_group_mapped["consents"].append(user_consent) consents_mapped.append(consent_group_mapped) @@ -562,17 +634,6 @@ class SaveUserConsentView(APIView): status=status.HTTP_400_BAD_REQUEST, ) - if is_provider_verb_or_object_missing( - latest_provider_schema, user_provider_setting - ): - return JsonResponse( - { - "message": "user consent contains missing provider schema verb consents" - }, - safe=False, - status=status.HTTP_400_BAD_REQUEST, - ) - for user_provider_setting in serializer.validated_data: user.accepted_provider_schemas.add( user_provider_setting["providerSchemaId"] @@ -741,7 +802,8 @@ class GetUsersConsentsThirdPartyView(APIView): class SaveUserConsentThirdPartyView(APIView): """ Saves a user consent, with authentication taking place via application tokens. - The provider schema id for each provider must always refer to the latest schema version of the respective provider, otherwise the request will be rejected + The provider schema id for each provider must always refer to the latest schema version of the respective provider, + otherwise the request will be rejected. """ def post(self, request): @@ -761,15 +823,6 @@ class SaveUserConsentThirdPartyView(APIView): email = shib_connector_resolver(email=email, provider=provider) user = CustomUser.objects.filter(email=email).first() - for verb in serializer.validated_data["verbs"]: - if verb["provider"].id != provider.provider.id: - return JsonResponse( - { - "message": "you are not authorized to access providers other than your own" - }, - safe=False, - status=status.HTTP_403_FORBIDDEN, - ) # Validate user provider setting refers to the latest provider schema id latest_provider_schema = ProviderSchema.objects.get( @@ -785,17 +838,6 @@ class SaveUserConsentThirdPartyView(APIView): status=status.HTTP_400_BAD_REQUEST, ) - if is_provider_verb_or_object_missing( - latest_provider_schema, serializer.validated_data - ): - return JsonResponse( - { - "message": "user consent contains missing provider schema verb consents" - }, - safe=False, - status=status.HTTP_400_BAD_REQUEST, - ) - save_user_consent( user, serializer.validated_data["provider_schema_id"], diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index 4fb8d74..130b29f 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -25,6 +25,7 @@ "@fortawesome/free-solid-svg-icons": "^5.15.4", "jwt-decode": "^3.1.2", "ng-zorro-antd": "^14.0.0", + "ngx-markdown": "^14.0.1", "rxjs": "~7.5.0", "tslib": "^2.3.0", "zone.js": "~0.11.4" @@ -2397,6 +2398,12 @@ "node": ">=6.9.0" } }, + "node_modules/@braintree/sanitize-url": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz", + "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==", + "license": "MIT" + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -3212,6 +3219,12 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, + "node_modules/@types/marked": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.3.2.tgz", + "integrity": "sha512-a79Yc3TOk6dGdituy8hmTTJXjOkZ7zsFYV10L337ttq/rec8lRMDBpV7fL3uLx6TgbFCa5DU/h8FmIBQPSbU0w==", + "license": "MIT" + }, "node_modules/@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -4322,6 +4335,17 @@ "node": ">= 10" } }, + "node_modules/clipboard": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", + "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", + "license": "MIT", + "dependencies": { + "good-listener": "^1.2.2", + "select": "^1.1.2", + "tiny-emitter": "^2.0.0" + } + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -4702,6 +4726,15 @@ "node": ">= 0.10" } }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/cosmiconfig": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", @@ -4949,6 +4982,486 @@ "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", "dev": true }, + "node_modules/cytoscape": { + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.31.0.tgz", + "integrity": "sha512-zDGn1K/tfZwEnoGOcHc0H4XazqAAXAuDpcYw9mUnUjATjqljyCNGJv8uEvbvxGaGHaVshxMecyl6oc6uKzRfbw==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.9.tgz", + "integrity": "sha512-rYR4QfVmy+sR44IBDvVtcAmOReGBvRCWDpO2QjYwqgh9yijw6eSHBqaPG/LIOEy7aBsniLvtMW6pg19qJhq60w==", + "license": "MIT", + "dependencies": { + "d3": "^7.8.2", + "lodash-es": "^4.17.21" + } + }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -4992,6 +5505,12 @@ "node": ">=4.0" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -5058,6 +5577,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==", + "license": "MIT" + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -5187,6 +5721,12 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz", + "integrity": "sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ==", + "license": "(MPL-2.0 OR Apache-2.0)" + }, "node_modules/domutils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", @@ -5213,12 +5753,24 @@ "integrity": "sha512-g6RQ9zCOV+U5QVHW9OpFR7rdk/V7xfopNXnyAamdpFgCHgZ1sjI8VuR1+zG2YG/TZk+tQ8mpNkug4P8FU0fuOA==", "dev": true }, + "node_modules/elkjs": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.8.2.tgz", + "integrity": "sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==", + "license": "EPL-2.0" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/emoji-toolkit": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/emoji-toolkit/-/emoji-toolkit-6.6.0.tgz", + "integrity": "sha512-pEu0kow2p1N8zCKnn/L6H0F3rWUBB3P3hVjr/O5yl1fK7N9jU4vO4G7EFapC5Y3XwZLUCY0FZbOPyTkH+4V2eQ==", + "license": "MIT" + }, "node_modules/emojis-list": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", @@ -6351,6 +6903,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==", + "license": "MIT", + "dependencies": { + "delegate": "^3.1.2" + } + }, "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -6868,6 +7429,15 @@ "node": ">=8" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/ip": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", @@ -7541,6 +8111,36 @@ "node": ">=10" } }, + "node_modules/katex": { + "version": "0.16.21", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz", + "integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -7559,6 +8159,12 @@ "node": ">= 8" } }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, "node_modules/less": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz", @@ -7698,6 +8304,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -7878,6 +8490,18 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -7920,6 +8544,43 @@ "node": ">= 8" } }, + "node_modules/mermaid": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-9.4.3.tgz", + "integrity": "sha512-TLkQEtqhRSuEHSE34lh5bCa94KATCyluAXmFnNI2PRZwOpXFeqiJWwZl+d2CcemE1RS6QbbueSSq9QIg8Uxcyw==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^6.0.0", + "cytoscape": "^3.23.0", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.1.0", + "d3": "^7.4.0", + "dagre-d3-es": "7.0.9", + "dayjs": "^1.11.7", + "dompurify": "2.4.3", + "elkjs": "^0.8.2", + "khroma": "^2.0.0", + "lodash-es": "^4.17.21", + "non-layered-tidy-tree-layout": "^2.0.2", + "stylis": "^4.1.2", + "ts-dedent": "^2.2.0", + "uuid": "^9.0.0", + "web-worker": "^1.2.0" + } + }, + "node_modules/mermaid/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -8208,6 +8869,7 @@ "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "dev": true, "engines": { "node": "*" } @@ -8325,6 +8987,29 @@ "@angular/router": "^14.1.0" } }, + "node_modules/ngx-markdown": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/ngx-markdown/-/ngx-markdown-14.0.1.tgz", + "integrity": "sha512-y5CY4e0QM0uR6+MvU1rnh1Ks+rku14309kVVojyXLcWl4zlrt8VAYCcf/+A+8z/IDOaz38yTrxNBnvYDJzNzYA==", + "license": "MIT", + "dependencies": { + "@types/marked": "^4.0.3", + "clipboard": "^2.0.11", + "emoji-toolkit": "^6.6.0", + "katex": "^0.16.0", + "marked": "^4.0.17", + "mermaid": "^9.1.2", + "prismjs": "^1.28.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^14.0.0", + "@angular/core": "^14.0.0", + "@angular/platform-browser": "^14.0.0", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "^0.11.4" + } + }, "node_modules/nice-napi": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", @@ -8430,6 +9115,12 @@ "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", "dev": true }, + "node_modules/non-layered-tidy-tree-layout": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", + "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==", + "license": "MIT" + }, "node_modules/nopt": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", @@ -9817,6 +10508,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/proc-log": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-2.0.1.tgz", @@ -10328,6 +11028,12 @@ "node": "*" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -10360,6 +11066,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/rxjs": { "version": "7.5.7", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz", @@ -10391,8 +11103,7 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { "version": "1.54.4", @@ -10473,6 +11184,12 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==", + "license": "MIT" + }, "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -11051,6 +11768,12 @@ "node": ">=6" } }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, "node_modules/stylus": { "version": "0.59.0", "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.59.0.tgz", @@ -11320,6 +12043,12 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "license": "MIT" + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -11371,6 +12100,15 @@ "tree-kill": "cli.js" } }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/tslib": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", @@ -11651,6 +12389,12 @@ "defaults": "^1.0.3" } }, + "node_modules/web-worker": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", + "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==", + "license": "Apache-2.0" + }, "node_modules/webpack": { "version": "5.74.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.74.0.tgz", diff --git a/src/frontend/package.json b/src/frontend/package.json index 5753332..0bf9f9e 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -27,6 +27,7 @@ "@fortawesome/free-solid-svg-icons": "^5.15.4", "jwt-decode": "^3.1.2", "ng-zorro-antd": "^14.0.0", + "ngx-markdown": "^14.0.1", "rxjs": "~7.5.0", "tslib": "^2.3.0", "zone.js": "~0.11.4" diff --git a/src/frontend/src/app/analytics-engine/analytics-engine.component.html b/src/frontend/src/app/analytics-engine/analytics-engine.component.html index 7a99fbc..d803b1c 100644 --- a/src/frontend/src/app/analytics-engine/analytics-engine.component.html +++ b/src/frontend/src/app/analytics-engine/analytics-engine.component.html @@ -1,24 +1,22 @@ -<div class="analytics-engines"> - <h1 i18n="Analyses | @@analysis"> - Analyses - </h1> - <nz-tabset> - <nz-tab i18n-nzTitle="Aktive Analysen | @@activeAnalyses" nzTitle="Active analyses"> - <div *ngIf="!activeAnalyses.length"> - <h1 i18n="no analyses | @@noAnalyses">No analyses</h1> +<h1 i18n="Analyses | @@analysis"> + Analyses +</h1> +<nz-tabset> + <nz-tab i18n-nzTitle="Aktive Analysen | @@activeAnalyses" nzTitle="Active analyses"> + <div *ngIf="!activeAnalyses.length"> + <h1 i18n="no analyses | @@noAnalyses">No analyses</h1> + </div> + <div nz-row [nzGutter]="[16, 24]"> + <div nz-col [nzSpan]="6" *ngFor="let analysis of activeAnalyses"> + <app-analysis-card [providers]="providers" class="card" [analysis]="analysis" [reloadList]="reloadList"></app-analysis-card> </div> - <div nz-row [nzGutter]="[16, 24]"> - <div nz-col [nzSpan]="6" *ngFor="let analysis of activeAnalyses"> - <app-analysis-card [providers]="providers" class="card" [analysis]="analysis" [reloadList]="reloadList"></app-analysis-card> - </div> + </div> + </nz-tab> + <nz-tab i18n-nzTitle="Inaktive Analysen | @@inactiveAnalyses" nzTitle="Inactive analyses"> + <div nz-row [nzGutter]="[16, 24]"> + <div nz-col [nzSpan]="6" *ngFor="let analysis of inactiveAnalyses"> + <app-analysis-card [providers]="providers" class="card" [analysis]="analysis" [reloadList]="reloadList"></app-analysis-card> </div> - </nz-tab> - <nz-tab i18n-nzTitle="Inaktive Analysen | @@inactiveAnalyses" nzTitle="Inactive analyses"> - <div nz-row [nzGutter]="[16, 24]"> - <div nz-col [nzSpan]="6" *ngFor="let analysis of inactiveAnalyses"> - <app-analysis-card [providers]="providers" class="card" [analysis]="analysis" [reloadList]="reloadList"></app-analysis-card> - </div> - </div> - </nz-tab> - </nz-tabset> -</div> + </div> + </nz-tab> +</nz-tabset> diff --git a/src/frontend/src/app/analytics-engine/analytics-engine.component.scss b/src/frontend/src/app/analytics-engine/analytics-engine.component.scss index 1c5898f..f04d96d 100644 --- a/src/frontend/src/app/analytics-engine/analytics-engine.component.scss +++ b/src/frontend/src/app/analytics-engine/analytics-engine.component.scss @@ -1,7 +1,3 @@ -.analytics-engines { - margin: 20px; -} - .card{ width : 100% -} \ No newline at end of file +} diff --git a/src/frontend/src/app/analytics-engine/analytics-engine.component.ts b/src/frontend/src/app/analytics-engine/analytics-engine.component.ts index bb7e458..6b9c5f9 100644 --- a/src/frontend/src/app/analytics-engine/analytics-engine.component.ts +++ b/src/frontend/src/app/analytics-engine/analytics-engine.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { AnalyticsToken, AnalyticsTokenVerb, ApiService, Provider, UserConsentResponse, Verb } from '../services/api.service'; +import { AnalyticsToken, AnalyticsTokenVerb, ApiService, Provider, UserConsentResponse } from '../services/api.service'; import { ProviderSchema, UserConsent, XApiVerbConsented } from '../consent-management/consentDeclaration'; @@ -43,12 +43,12 @@ export class AnalyticsEngineComponent implements OnInit { userConsentProviderPairs : UserConsentProviderPair[] = []; providerVerbConsentPairs : ProviderVerbConsentPair[] = [] - constructor(private _apiService: ApiService) { + constructor(private _apiService: ApiService) { this._apiService = _apiService this.reloadList = this.reloadList.bind(this) } - + ngOnInit(): void { this.loadAnalyticsTokens() } @@ -61,7 +61,7 @@ export class AnalyticsEngineComponent implements OnInit { this.providers = []; this.userConsentProviderPairs = []; this.providerVerbConsentPairs = [] - + this.loadAnalyticsTokens() } @@ -72,7 +72,7 @@ export class AnalyticsEngineComponent implements OnInit { }) .add(() => this.loadProviders()) } - + loadProviders() { this._apiService.getProvidersUsers().subscribe((providers) => { @@ -80,7 +80,7 @@ export class AnalyticsEngineComponent implements OnInit { for(let provider of providers) this.loadConsent(provider) }) - + } loadConsent(provider : Provider) @@ -111,7 +111,7 @@ export class AnalyticsEngineComponent implements OnInit { for(let group of consentGroups) for(let verb of group.verbs) allVerbs.push({...verb, providerId : provider.id}); - + this.providerVerbConsentPairs.push({provider : provider, providerSchema : providerSchema!, allVerbs : allVerbs}) } @@ -125,7 +125,7 @@ export class AnalyticsEngineComponent implements OnInit { for(let analysis of this.analysisTokens){ let consentedVerbs : XApiVerbConsentedWithProvider[] = [] let notConsentedVerbs : XApiVerbConsentedWithProvider[]= [] - + const verbs = analysis.analyticTokenVerbs for(let verb of verbs) { @@ -145,5 +145,5 @@ export class AnalyticsEngineComponent implements OnInit { this.activeAnalyses = active this.inactiveAnalyses = inactive } - + } diff --git a/src/frontend/src/app/app-routing.module.ts b/src/frontend/src/app/app-routing.module.ts index 3d57b7b..2a6ceb3 100644 --- a/src/frontend/src/app/app-routing.module.ts +++ b/src/frontend/src/app/app-routing.module.ts @@ -15,11 +15,13 @@ import { ApplicationTokensComponent } from './consent-management/application-tok import { AnalyticsTokensComponent } from './consent-management/analytics-tokens/analytics-tokens.component' import { AnalyticsEngineComponent } from './analytics-engine/analytics-engine.component' import { ConsentHistoryComponent } from './consent-management/consent-history/consent-history.component' +import { ControlCenterComponent } from './control-center/control-center.component' const routes: Routes = [ { path: 'home', component: HomeComponent }, { path: 'consent-management', component: ConsentManagementComponent, canActivate: [AuthGuard] }, { path: 'consent-history', component: ConsentHistoryComponent, canActivate: [AuthGuard] }, + { path: 'control-center', component: ControlCenterComponent, canActivate: [AuthGuard]}, { path: 'merge-actors', component: MergeDataComponent, canActivate: [AuthGuard] }, { path: 'analytics-engines', component: AnalyticsEngineComponent, canActivate: [AuthGuard] }, { path: 'login', component: LoginPageComponent }, diff --git a/src/frontend/src/app/app.component.html b/src/frontend/src/app/app.component.html index ea94cee..4cb2704 100644 --- a/src/frontend/src/app/app.component.html +++ b/src/frontend/src/app/app.component.html @@ -4,7 +4,9 @@ <div class="loading-spinner" *ngIf="loading"> <img ngSrc="assets/spinner_small.gif" alt="loading" height="100" width="100" /> </div> - <router-outlet></router-outlet> + <div class="content"> + <router-outlet></router-outlet> + </div> </nz-content> <app-footer></app-footer> </nz-layout> diff --git a/src/frontend/src/app/app.module.ts b/src/frontend/src/app/app.module.ts index fab3a63..f20d7f0 100644 --- a/src/frontend/src/app/app.module.ts +++ b/src/frontend/src/app/app.module.ts @@ -72,6 +72,11 @@ import { NzMessageService } from 'ng-zorro-antd/message' import { NzSpinModule } from 'ng-zorro-antd/spin' import { NzDatePickerModule } from 'ng-zorro-antd/date-picker' import { ConsentHistoryComponent } from './consent-management/consent-history/consent-history.component' +import { ControlCenterComponent } from './control-center/control-center.component' +import { VerbGroupsComponent } from './consent-management/verb-groups/verb-groups.component' +import { MarkdownModule, MarkdownService } from 'ngx-markdown' +import { CreateVerbGroupDialog } from './dialogs/create-verb-group-dialog/create-verb-group-dialog' +import { NzRadioModule } from 'ng-zorro-antd/radio' registerLocaleData(de) @@ -81,6 +86,7 @@ registerLocaleData(de) HeaderComponent, ConsentManagementComponent, ConsentHistoryComponent, + ControlCenterComponent, PageNotFoundComponent, LoginPageComponent, WizardDialog, @@ -98,47 +104,51 @@ registerLocaleData(de) SchemaChangeComponent, ObjectChangesComponent, CreateTokenDialog, + CreateVerbGroupDialog, OrderByPipe, MergeDataComponent, AnalysisCard, - AnalyticsEngineComponent - ], - imports: [ - BrowserModule, - AppRoutingModule, - LayoutModule, - FormsModule, - ReactiveFormsModule, - HttpClientModule, - BrowserAnimationsModule, - NzTabsModule, - NzGridModule, - NzCardModule, - NzButtonModule, - FontAwesomeModule, - NzLayoutModule, - NgOptimizedImage, - NzMenuModule, - NzAffixModule, - NzSwitchModule, - NzDropDownModule, - NzCollapseModule, - NzListModule, - NzToolTipModule, - NzSelectModule, - NzInputModule, - NzSpaceModule, - NzFormModule, - NzBadgeModule, - NzTagModule, - NzTableModule, - NzCheckboxModule, - NzProgressModule, - NzAlertModule, - NzModalModule, - NzSpinModule, - NzDatePickerModule + AnalyticsEngineComponent, + VerbGroupsComponent ], + imports: [ + BrowserModule, + AppRoutingModule, + LayoutModule, + FormsModule, + ReactiveFormsModule, + HttpClientModule, + BrowserAnimationsModule, + NzTabsModule, + NzGridModule, + NzCardModule, + NzButtonModule, + FontAwesomeModule, + NzLayoutModule, + NgOptimizedImage, + NzMenuModule, + NzAffixModule, + NzSwitchModule, + NzDropDownModule, + NzCollapseModule, + NzListModule, + NzToolTipModule, + NzSelectModule, + NzInputModule, + NzSpaceModule, + NzFormModule, + NzBadgeModule, + NzTagModule, + NzTableModule, + NzCheckboxModule, + NzProgressModule, + NzAlertModule, + NzModalModule, + NzSpinModule, + NzDatePickerModule, + MarkdownModule.forRoot(), + NzRadioModule + ], providers: [ { provide: APP_INITIALIZER, diff --git a/src/frontend/src/app/consent-management/analytics-tokens/analytics-tokens.component.html b/src/frontend/src/app/consent-management/analytics-tokens/analytics-tokens.component.html index 3584fc0..891e70f 100644 --- a/src/frontend/src/app/consent-management/analytics-tokens/analytics-tokens.component.html +++ b/src/frontend/src/app/consent-management/analytics-tokens/analytics-tokens.component.html @@ -18,8 +18,8 @@ <th></th> <th>Name</th> <th>Token</th> - <th>Created At</th> - <th>Expires</th> + <th i18n="Created At | Table column header @@createdAt">Created At</th> + <th i18n="Expires | Table column header @@expires">Expires</th> </tr> </thead> <tbody> diff --git a/src/frontend/src/app/consent-management/consent-history/consent-history.component.html b/src/frontend/src/app/consent-management/consent-history/consent-history.component.html index 52fc28a..4869b3d 100644 --- a/src/frontend/src/app/consent-management/consent-history/consent-history.component.html +++ b/src/frontend/src/app/consent-management/consent-history/consent-history.component.html @@ -1,23 +1,59 @@ -<nz-card style='margin: 8px;' [nzTitle]="cardTitle"> - <ng-template #cardTitle> - <h2 i18n="Consent History | Header entry @@consentHistoryHeader">Consent History</h2> - </ng-template> - <nz-collapse> - <nz-collapse-panel *ngFor="let consent of consentHistory" [nzHeader]="dateHeader"> - <ng-template #dateHeader> - {{ consent.created | date }} - </ng-template> - <nz-card - *ngFor="let userConsent of consent.consents" - [nzTitle]="userConsent.description" - > - <app-provider-setting - [preview]="true" - [consentDeclaration]="userConsent" - [previousUserConsent]="null" - ></app-provider-setting> - </nz-card> +<h1 i18n="Consent History | Header entry @@consentHistoryHeader">Consent Management</h1> + +<p> + POLARIS verarbeitet deine persönlichen Lerndaten nur mit deiner vorherigen Zustimmung. In dieser Übersicht kannst du deine Einwilligungen zu verschiedenen Datenbündeln ansehen und bearbeiten. Du kannst deine Einwilligung widerrufen oder die Datenübertragung vorübergehend pausieren. +</p> +<p> </p> + +<nz-collapse> + <nz-collapse-panel [nzHeader]="faqPauseHeader"> + <ng-template #faqPauseHeader> + Was passiert, wenn ich die Datenübertragung pausiere? + </ng-template> + <p> + Du kannst die Übertragung deiner Lerndaten an POLARIS jederzeit pausieren. Ab diesem Zeitpunkt werden auf unbestimmte Zeit keine weiteren Lerndaten mehr an POLARIS geschickt. Anders als beim Widerruf bleiben die in POLARIS gespeicherten Daten erhalten. + </p> + <p> + Die persönlichen Statistiken, die dir auf Basis der Analyse dieser Lerndaten angezeigt wurden, werden ausgeblendet. + </p> + <p> + Die Datenübertragung wird fortgesetzt, wenn du die Pausierung wieder aufhebst. Dies kannst du unten unter „Meine Einwilligungen“ tun oder indem du auf den Button „Statistiken aktivieren“ im jeweiligen Quellsystem, z.B. Moodle oder Dynexite, klickst. + </p> + </nz-collapse-panel> + <nz-collapse-panel [nzHeader]="faqRevokeHeader"> + <ng-template #faqRevokeHeader> + Was passiert, wenn ich meine Einwilligung widerrufe? + </ng-template> + <p> + Eine Einwilligung in die Verarbeitung personenbezogener Daten kannst du jederzeit widerrufen – dieses Recht ist durch die Datenschutz-Grundverordnung gesichert. Du musst dazu keine Gründe angeben und deine Entscheidung darf keine Nachteile für dich zur Folge haben. In diesem Fall können jedoch die entsprechenden Analysen nicht für Dich durchgeführt werden. + </p> + <p> + Wenn du die Zustimmung zur Verarbeitung bestimmter Lerndaten in POLARIS widerrufst, werden ab diesem Zeitpunkt keine weiteren Daten mehr an POLARIS geschickt. Außerdem werden die bis zu diesem Zeitpunkt über dich gespeicherten Lerndaten innerhalb der gesetzlichen Frist von vier Wochen unwiderruflich anonymisiert. Anonyme Daten können dir nie wieder zugeordnet werden. + </p> + <p> + Die persönlichen Statistiken, die dir auf Basis der Analyse dieser Lerndaten angezeigt wurden, werden ausgeblendet. + </p> + </nz-collapse-panel> +</nz-collapse> +<p style="padding-bottom: 88px"> </p> + +<h2>Deine Einwilligungen</h2> + +<nz-collapse> + <nz-collapse-panel *ngFor="let consent of consentHistory" [nzHeader]="dateHeader"> + <ng-template #dateHeader> + {{ consent.created | date }} + </ng-template> + <nz-card + *ngFor="let userConsent of consent.consents" + [nzTitle]="userConsent.description" + > + <app-provider-setting + [preview]="true" + [consentDeclaration]="userConsent" + [previousUserConsent]="null" + ></app-provider-setting> + </nz-card> + </nz-collapse-panel> +</nz-collapse> - </nz-collapse-panel> - </nz-collapse> -</nz-card> diff --git a/src/frontend/src/app/consent-management/consentDeclaration.ts b/src/frontend/src/app/consent-management/consentDeclaration.ts index 51e06e7..013133f 100644 --- a/src/frontend/src/app/consent-management/consentDeclaration.ts +++ b/src/frontend/src/app/consent-management/consentDeclaration.ts @@ -2,6 +2,7 @@ export interface XApiObjectSchema { definition: Object consented?: boolean defaultConsent: boolean + objectType: string label: string id: string } @@ -10,18 +11,17 @@ export interface XApiObjectConsented { definition: Object consented: boolean defaultConsent: boolean + objectType: string label: string id: string } -interface XApiVerb { -} - export interface XApiVerbSchema { id: string label: string description: string defaultConsent: boolean + essential: boolean objects: XApiObjectSchema[] } @@ -30,18 +30,24 @@ export interface XApiVerbConsented extends XApiVerbSchema { objects: XApiObjectConsented[] } -export interface ConsentGroupSchema { +export interface XApiVerbGroupSchema { id: string label: string description: string purposeOfCollection: string - showVerbDetails: boolean + requiresConsent: boolean + provider_schema: number verbs: XApiVerbSchema[] - isDefault: boolean } -export const instanceOfXApiVerbConsented = (object: any): object is XApiObjectConsented => { - return 'consented' in object +export interface XApiVerbGroupConsented extends XApiVerbGroupSchema { + id: string + label: string + description: string + purposeOfCollection: string + showVerbDetails: boolean + verbs: XApiVerbSchema[] + isDefault: boolean } export interface ConsentGroupConsented { @@ -55,37 +61,30 @@ export interface ConsentGroupConsented { } interface ConsentDeclaration { - id: string + id: number description: string } -export interface ProviderConsent { - id: string - name: string - superseded_by: number | null - createdAt: string - definition: ConsentDeclaration[] - supersedes?: ProviderConsent -} - export interface ProviderSchemaDefinition { id: string name: string description: string - groups: ConsentGroupSchema[] + verbs: XApiVerbSchema[] essential_verbs: XApiVerbSchema[] // verbs which can be collected without the user consent } -export interface ProviderSchema extends ConsentDeclaration { +export interface ProviderSchema { + id: number + description: string superseded_by: number | null createdAt: string - groups: ConsentGroupSchema[] + groups: XApiVerbGroupConsented[] essential_verbs: XApiVerbSchema[] // verbs which can be collected without the user consent definition: ProviderSchemaDefinition } export interface UserConsent extends ConsentDeclaration { - id: string + id: number groups: ConsentGroupConsented[] essential_verbs: XApiVerbSchema[] // verbs which can be collected without the user consent created?: Date @@ -93,11 +92,12 @@ export interface UserConsent extends ConsentDeclaration { export interface UserConsentVerbs { providerId: ProviderId - providerSchemaId: string + providerSchemaId: ProviderSchemaId verbs: { id: string; consented?: boolean; objects: string }[] } export type ProviderId = number +export type ProviderSchemaId = number /** * Iterates over each verb and object of a provider schema and sets the consented field to the default consent @@ -108,7 +108,7 @@ export const providerSchemaToUserConsent = (providerSchema: ProviderSchema): Use return { id: providerSchema.id, description: providerSchema.description, - groups: providerSchema.groups.map((group) => ({ + groups: providerSchema.groups? providerSchema.groups.map((group) => ({ ...group, verbs: group.verbs.map((verb) => ({ ...verb, @@ -118,7 +118,7 @@ export const providerSchemaToUserConsent = (providerSchema: ProviderSchema): Use consented: object.defaultConsent })) : [] })) - })), + })) : [], essential_verbs: providerSchema.essential_verbs } } diff --git a/src/frontend/src/app/consent-management/provider/provider.component.html b/src/frontend/src/app/consent-management/provider/provider.component.html index bb8eb67..843d5b1 100644 --- a/src/frontend/src/app/consent-management/provider/provider.component.html +++ b/src/frontend/src/app/consent-management/provider/provider.component.html @@ -1,80 +1,89 @@ -<div class="provider"> - <h1 i18n="Provider Schemas | Provider Schemas header @@providerSchemas">Provider Schemas</h1> +<h1 i18n="Provider Schemas | Provider Schemas header @@providerSchemas">Provider Schemas</h1> - <nz-tabset> - <nz-tab *ngFor="let provider of providers" [nzTitle]="provider.name"> - <div class="provider-details"> - <div - *ngFor=" - let schemaVersion of provider.versions; - let cnt = count; - let idx = index - "> - <nz-card class="existing-consents"> - <nz-card-meta [nzTitle]="cardTitle" [nzDescription]="schemaVersion.createdAt | date : 'medium'"></nz-card-meta> - <ng-template #cardTitle> - <div class="schema-version-title"> - <div>Version: {{ cnt - idx }}</div> - <div> - <nz-tag *ngIf="idx === 0" nzColor="default">latest</nz-tag> - </div> +<nz-tabset> + <nz-tab *ngFor="let provider of providers" [nzTitle]="provider.name"> + <div class="provider-details"> + <div + *ngFor=" + let schemaVersion of provider.versions; + let cnt = count; + let idx = index + "> + <nz-card class="existing-consents"> + <nz-card-meta [nzTitle]="cardTitle" [nzDescription]="schemaVersion.createdAt | date : 'medium'"></nz-card-meta> + <ng-template #cardTitle> + <div class="schema-version-title"> + <div>Version: {{ cnt - idx }}</div> + <div> + <nz-tag *ngIf="idx === 0" nzColor="default" i18n="Latest Tag @@latestTag">latest</nz-tag> </div> - </ng-template> - - <div - *ngIf="schemaVersion.superseded_by" - i18n="Superseded By | Provider schema information @@supersededBy"> - Superseded by: {{ cnt - idx + 1 }} - </div> - <nz-switch [(ngModel)]="definitionVisible[provider.id][schemaVersion.id]"></nz-switch> - <span i18n="Toggle Definition | Slide Toggle Description @@toggleDefinition">Toggle Definition</span> - <div - class="definiton-view" - *ngIf="definitionVisible[provider.id][schemaVersion.id]"> - <pre>{{ schemaVersion.definition | json }}</pre> </div> - </nz-card> + </ng-template> + + <div + *ngIf="schemaVersion.superseded_by" + i18n="Superseded By | Provider schema information @@supersededBy"> + Superseded by: {{ cnt - idx + 1 }} + </div> + <nz-switch [(ngModel)]="definitionVisible[provider.id][schemaVersion.id]"></nz-switch> + <span i18n="Toggle Definition | Slide Toggle Description @@toggleDefinition">Toggle Definition</span> + <div + class="definiton-view" + *ngIf="definitionVisible[provider.id][schemaVersion.id]"> + <pre>{{ schemaVersion.definition | json }}</pre> + </div> + + <p></p> + + <app-verb-groups + [provider_id]="provider.id" + [allVerbs]="schemaVersion.definition.verbs" + [verbGroups]="getGroupsForProviderSchema(schemaVersion)" + > + </app-verb-groups> <app-schema-change *ngIf="idx < provider.versions.length - 1" [newSchema]="provider.versions[idx].definition" [oldSchema]="provider.versions[idx + 1].definition"> </app-schema-change> - </div> + + </nz-card> </div> - </nz-tab> - </nz-tabset> + </div> + </nz-tab> +</nz-tabset> - <nz-card [nzTitle]="createCardTitle"> - <ng-template #createCardTitle i18n="Create Provider Schema @@createProviderSchema"> - Create/Update Provider Schema - </ng-template> - <input - type="file" - class="file-input" - accept="application/json" - (change)="onFileSelected($event)" - #fileUpload /> +<nz-card [nzTitle]="createCardTitle"> + <ng-template #createCardTitle i18n="Create Provider Schema @@createProviderSchema"> + Create/Update Provider Schema + </ng-template> + <input + type="file" + class="file-input" + accept="application/json" + (change)="onFileSelected($event)" + #fileUpload /> - <div class="file-upload"> - {{ fileName }} + <div class="file-upload"> + {{ fileName }} - <button nz-button nzShape="circle" nzSize="small" nzType="primary" class="upload-btn" (click)="fileUpload.click()"> - <fa-icon [icon]="faPaperclip"></fa-icon> - </button> - </div> + <button nz-button nzShape="circle" nzSize="small" nzType="primary" class="upload-btn" (click)="fileUpload.click()"> + <fa-icon [icon]="faPaperclip"></fa-icon> + </button> + </div> - <div class="progress"> - <nz-progress - class="progress-bar" - [nzPercent]="uploadProgress" - *ngIf="uploadProgress"> - </nz-progress> + <div class="progress"> + <nz-progress + class="progress-bar" + [nzPercent]="uploadProgress" + *ngIf="uploadProgress"> + </nz-progress> + + <fa-icon [icon]="faTrash" *ngIf="uploadProgress" (click)="cancelUpload()"></fa-icon> + </div> + <button nz-button nzType="primary" (click)="onSubmit()" i18n="Submit @@submit"> + Submit + </button> +</nz-card> - <fa-icon [icon]="faTrash" *ngIf="uploadProgress" (click)="cancelUpload()"></fa-icon> - </div> - <button nz-button nzType="primary" (click)="onSubmit()" i18n="Submit @@submit"> - Submit - </button> - </nz-card> -</div> diff --git a/src/frontend/src/app/consent-management/provider/provider.component.scss b/src/frontend/src/app/consent-management/provider/provider.component.scss index 994467b..e7c0f10 100644 --- a/src/frontend/src/app/consent-management/provider/provider.component.scss +++ b/src/frontend/src/app/consent-management/provider/provider.component.scss @@ -1,6 +1,3 @@ -.provider { - padding: 20px; -} .file-input { display: none; } diff --git a/src/frontend/src/app/consent-management/provider/provider.component.ts b/src/frontend/src/app/consent-management/provider/provider.component.ts index 8c4fcde..fcb03da 100644 --- a/src/frontend/src/app/consent-management/provider/provider.component.ts +++ b/src/frontend/src/app/consent-management/provider/provider.component.ts @@ -6,7 +6,7 @@ import { ApiService, Provider } from 'src/app/services/api.service' import { ProviderSchema, providerSchemaToUserConsent, - UserConsent + UserConsent, XApiVerbGroupSchema } from '../consentDeclaration' import { faPaperclip, faTrash } from '@fortawesome/free-solid-svg-icons' import { NzMessageService } from 'ng-zorro-antd/message' @@ -54,6 +54,21 @@ export class ProviderComponent implements OnInit { }) } + getGroupsForProviderSchema(provider_schema: ProviderSchema): XApiVerbGroupSchema[] { + let groups: XApiVerbGroupSchema[] = []; + this.providers.forEach((provider) => { + provider.versions.forEach((version) => { + if(version.id === provider_schema.id) { + let temp_groups = provider.groups?.filter((group) => group.provider_schema == provider_schema.id); + if(temp_groups !== undefined) { + groups = temp_groups; + } + } + }) + }) + return groups; + } + onFileSelected(event: any) { const file: File = event.target.files[0] diff --git a/src/frontend/src/app/consent-management/schema-change/schema-change.component.ts b/src/frontend/src/app/consent-management/schema-change/schema-change.component.ts index b797e8a..07ed555 100644 --- a/src/frontend/src/app/consent-management/schema-change/schema-change.component.ts +++ b/src/frontend/src/app/consent-management/schema-change/schema-change.component.ts @@ -1,9 +1,9 @@ import { Component, OnInit, Input } from '@angular/core' import { - ConsentGroupSchema, - ProviderSchemaDefinition, - XApiObjectSchema, - XApiVerbSchema + XApiVerbGroupConsented, + ProviderSchemaDefinition, + XApiObjectSchema, + XApiVerbSchema } from '../consentDeclaration' import { faPlus, faTrash, faWrench } from '@fortawesome/free-solid-svg-icons' @@ -12,448 +12,370 @@ type VerbChangeType = 'new' | 'removed' | 'changed' | 'onlyObjectChanged' | 'mov type ObjectChangeType = 'new' | 'removed' | 'changed' export interface ObjectChange { - type: ObjectChangeType - old?: XApiObjectSchema - new?: XApiObjectSchema - description: string + type: ObjectChangeType + old?: XApiObjectSchema + new?: XApiObjectSchema + description: string } interface VerbChange { - type: VerbChangeType - old?: XApiVerbSchema - new?: XApiVerbSchema - description: string - isEssentialVerb: boolean - objectChanges: ObjectChange[] + type: VerbChangeType + old?: XApiVerbSchema + new?: XApiVerbSchema + description: string + isEssentialVerb: boolean + objectChanges: ObjectChange[] } interface SchemaChange { - id: string - name: string - type: 'new' | 'removed' | 'changed' | 'moved' - changedVerbs?: VerbChange[] - newVerbs?: VerbChange[] - removedVerbs?: VerbChange[] - verbsWithChangedObjects?: VerbChange[] + id: string + name: string + type: 'new' | 'removed' | 'changed' | 'moved' + changedVerbs?: VerbChange[] + newVerbs?: VerbChange[] + removedVerbs?: VerbChange[] + verbsWithChangedObjects?: VerbChange[] } @Component({ - selector: 'app-schema-change', - templateUrl: './schema-change.component.html', - styleUrls: ['./schema-change.component.scss'] + selector: 'app-schema-change', + templateUrl: './schema-change.component.html', + styleUrls: ['./schema-change.component.scss'] }) export class SchemaChangeComponent implements OnInit { - @Input() oldSchema?: ProviderSchemaDefinition - @Input() newSchema?: ProviderSchemaDefinition + @Input() oldSchema?: ProviderSchemaDefinition + @Input() newSchema?: ProviderSchemaDefinition - schemaChanges: SchemaChange[] = [] + schemaChanges: SchemaChange[] = [] - constructor() {} + constructor() { + } - ngOnInit(): void { - this.detectChanges() - } + ngOnInit(): void { + this.detectChanges() + } - detectObjectChanges(oldVerb: XApiVerbSchema, newVerb: XApiVerbSchema): ObjectChange[] { - const objectChanges: ObjectChange[] = [] + detectObjectChanges(oldVerb: XApiVerbSchema, newVerb: XApiVerbSchema): ObjectChange[] { + const objectChanges: ObjectChange[] = [] - for (const oldObject of oldVerb.objects) { - const matchingNewObject = newVerb.objects.find((e) => e.id === oldObject.id) - if (matchingNewObject) { - if (oldObject.defaultConsent && !matchingNewObject.defaultConsent) - objectChanges.push({ - description: `Object ${matchingNewObject.label} changed from Opt Out to Opt In.`, - type: 'changed', - old: oldObject, - new: matchingNewObject - }) - if (!oldObject.defaultConsent && matchingNewObject.defaultConsent) - objectChanges.push({ - description: `Object ${matchingNewObject.label} changed from Opt In to Opt Out.`, - type: 'changed', - old: oldObject, - new: matchingNewObject - }) - } else { - objectChanges.push({ - description: `Object ${oldObject.label} removed.`, - type: 'removed', - old: oldObject - }) - } - } - for (const newObject of newVerb.objects) { - const matchingOldObject = oldVerb.objects.find((e) => e.id === newObject.id) - if (!matchingOldObject) { - objectChanges.push({ - description: `Object ${newObject.label} added.`, - type: 'new', - new: newObject - }) - } - } - return objectChanges + for (const oldObject of oldVerb.objects) { + const matchingNewObject = newVerb.objects.find((e) => e.id === oldObject.id) + if (matchingNewObject) { + if (oldObject.defaultConsent && !matchingNewObject.defaultConsent) + objectChanges.push({ + description: `Object ${matchingNewObject.label} changed from Opt Out to Opt In.`, + type: 'changed', + old: oldObject, + new: matchingNewObject + }) + if (!oldObject.defaultConsent && matchingNewObject.defaultConsent) + objectChanges.push({ + description: `Object ${matchingNewObject.label} changed from Opt In to Opt Out.`, + type: 'changed', + old: oldObject, + new: matchingNewObject + }) + } else { + objectChanges.push({ + description: `Object ${oldObject.label} removed.`, + type: 'removed', + old: oldObject + }) + } + } + for (const newObject of newVerb.objects) { + const matchingOldObject = oldVerb.objects.find((e) => e.id === newObject.id) + if (!matchingOldObject) { + objectChanges.push({ + description: `Object ${newObject.label} added.`, + type: 'new', + new: newObject + }) + } } + return objectChanges + } - /** - * Compares default consent between two xAPI Verbs. - * @param oldVerb - * @param newVerb - * @returns - */ - detectVerbChange( - oldVerb: XApiVerbSchema, - newVerb: XApiVerbSchema, - isEssentialVerb: boolean - ): VerbChange | null { - if ( - (oldVerb.defaultConsent && newVerb.defaultConsent) || - (!oldVerb.defaultConsent && !newVerb.defaultConsent) - ) { - const objectChanges = this.detectObjectChanges(oldVerb, newVerb) - if (objectChanges.length > 0) { - return { - description: $localize`:@@schemaChangeVerbObjChanges:Verb ${newVerb.label} includes object changes`, - type: 'onlyObjectChanged', - isEssentialVerb: isEssentialVerb, - objectChanges: objectChanges - } - } - return null + /** + * Compares default consent between two xAPI Verbs. + * @param oldVerb + * @param newVerb + * @returns + */ + detectVerbChange( + oldVerb: XApiVerbSchema, + newVerb: XApiVerbSchema + ): VerbChange | null { + const isEssentialVerb = oldVerb.essential + if ( + (oldVerb.defaultConsent && newVerb.defaultConsent) || + (!oldVerb.defaultConsent && !newVerb.defaultConsent) + ) { + const objectChanges = this.detectObjectChanges(oldVerb, newVerb) + if (objectChanges.length > 0) { + return { + description: $localize`:@@schemaChangeVerbObjChanges:Verb ${newVerb.label} includes object changes`, + type: 'onlyObjectChanged', + isEssentialVerb: isEssentialVerb, + objectChanges: objectChanges } - if (!oldVerb.defaultConsent && newVerb.defaultConsent) - return { - description: $localize`:@@schemaChangeVerbOptInToOptOut:Verb ${newVerb.label} changed from Opt In to Opt Out`, - old: oldVerb, - new: newVerb, - type: 'changed', - isEssentialVerb: isEssentialVerb, - objectChanges: this.detectObjectChanges(oldVerb, newVerb) - } - if (oldVerb.defaultConsent && !newVerb.defaultConsent) - return { - description: $localize`:@@schemaChangeVerbOptOutToOptIn:Verb ${newVerb.label} changed from Opt Out to Opt In`, - old: oldVerb, - new: newVerb, - type: 'changed', - isEssentialVerb: isEssentialVerb, - objectChanges: this.detectObjectChanges(oldVerb, newVerb) - } - return null + } + return null } + if (!oldVerb.defaultConsent && newVerb.defaultConsent) + return { + description: $localize`:@@schemaChangeVerbOptInToOptOut:Verb ${newVerb.label} changed from Opt In to Opt Out`, + old: oldVerb, + new: newVerb, + type: 'changed', + isEssentialVerb: isEssentialVerb, + objectChanges: this.detectObjectChanges(oldVerb, newVerb) + } + if (oldVerb.defaultConsent && !newVerb.defaultConsent) + return { + description: $localize`:@@schemaChangeVerbOptOutToOptIn:Verb ${newVerb.label} changed from Opt Out to Opt In`, + old: oldVerb, + new: newVerb, + type: 'changed', + isEssentialVerb: isEssentialVerb, + objectChanges: this.detectObjectChanges(oldVerb, newVerb) + } + return null + } - detectChanges(): void { - if (!this.newSchema || !this.oldSchema) return - - const matchingOldSchema = this.oldSchema.id === this.newSchema.id ? this.oldSchema : null - if (matchingOldSchema) { - const newSchemaVerbs = this.newSchema.groups.flatMap((group) => group.verbs) - const oldSchemaVerbs = matchingOldSchema.groups.flatMap((group) => group.verbs) - - for (const newSchemaVerb of newSchemaVerbs) { - const oldVerb = oldSchemaVerbs.find((oldVerb) => oldVerb.id === newSchemaVerb.id) - if (oldVerb) { - const verbChange = this.detectVerbChange(oldVerb, newSchemaVerb, false) - if (verbChange) this.updateSchemaChanges(matchingOldSchema, verbChange) - } else { - const verbChange: VerbChange = { - description: $localize`:@@schemaChangeVerbAdded:Verb ${newSchemaVerb.label} added`, - type: 'new', - new: newSchemaVerb, - isEssentialVerb: false, - objectChanges: [] - } - this.updateSchemaChanges(matchingOldSchema, verbChange) - } - } - - for (const oldVerb of oldSchemaVerbs) { - const matchingNewVerb = newSchemaVerbs.find((newVerb) => newVerb.id === oldVerb.id) - if (!matchingNewVerb) { - const verbChange: VerbChange = { - description: $localize`:@@schemaChangeVerbRemoved:Verb ${oldVerb.label} removed`, - old: oldVerb, - type: 'removed', - isEssentialVerb: false, - objectChanges: [] - } - this.updateSchemaChanges(matchingOldSchema, verbChange) - } - } - - for (const newEssentialVerb of this.newSchema.essential_verbs) { - const matchingOldVerb = matchingOldSchema.essential_verbs.find( - (e) => e.id === newEssentialVerb.id - ) + detectChanges(): void { + if (!this.newSchema || !this.oldSchema) return - if (matchingOldVerb) { - const verbChange = this.detectVerbChange( - matchingOldVerb, - newEssentialVerb, - true - ) - if (verbChange) this.updateSchemaChanges(matchingOldSchema, verbChange) - } else { - const verbChange: VerbChange = { - description: $localize`:@@schemaChangeEssentialVerbAdded:Essential Verb ${newEssentialVerb.label} added`, - type: 'new', - new: newEssentialVerb, - isEssentialVerb: true, - objectChanges: [] - } - this.updateSchemaChanges(this.newSchema, verbChange) - } - } + const matchingOldSchema = this.oldSchema.id === this.newSchema.id ? this.oldSchema : null + if (matchingOldSchema) { + const newSchemaVerbs = this.newSchema.verbs + const oldSchemaVerbs = matchingOldSchema.verbs - for (const oldEssentialVerb of matchingOldSchema.essential_verbs) { - const matchingNewVerb = this.newSchema.essential_verbs.find( - (e) => e.id === oldEssentialVerb.id - ) - - if (!matchingNewVerb) { - const verbChange: VerbChange = { - description: $localize`:@@schemaChangeEssentialVerbRemoved:Essential Verb ${oldEssentialVerb.label} removed`, - old: oldEssentialVerb, - type: 'removed', - isEssentialVerb: true, - objectChanges: [] - } - this.updateSchemaChanges(matchingOldSchema, verbChange) - } - } + for (const newSchemaVerb of newSchemaVerbs) { + const oldVerb = oldSchemaVerbs.find((oldVerb) => oldVerb.id === newSchemaVerb.id) + if (oldVerb) { + const verbChange = this.detectVerbChange(oldVerb, newSchemaVerb) + if (verbChange) this.updateSchemaChanges(matchingOldSchema, verbChange) } else { - // New schema added - this.schemaChanges.push({ - id: this.newSchema.id, - name: this.newSchema.name, - type: 'new' - }) - const verbChanges: VerbChange[] = [ - ...this.newSchema.groups.flatMap((group) => - group.verbs.map((verb) => { - const verbChange: VerbChange = { - description: $localize`:@@schemaChangeVerbAdded:Verb ${verb.label} added`, - type: 'new', - new: verb, - isEssentialVerb: false, - objectChanges: [] - } - return verbChange - }) - ), - ...this.newSchema.essential_verbs.map((verb) => { - const verbChange: VerbChange = { - description: $localize`:@@schemaChangeVerbAdded:Verb ${verb.label} added`, - type: 'new', - new: verb, - isEssentialVerb: false, - objectChanges: [] - } - return verbChange - }) - ] - for (const verbChange of verbChanges) - this.updateSchemaChanges(this.newSchema, verbChange) + const verbChange: VerbChange = { + description: $localize`:@@schemaChangeVerbAdded:Verb ${newSchemaVerb.label} added`, + type: 'new', + new: newSchemaVerb, + isEssentialVerb: false, + objectChanges: [] + } + this.updateSchemaChanges(matchingOldSchema, verbChange) } - const matchingNewSchema = this.newSchema.id === this.oldSchema.id ? this.newSchema : null - if (!matchingNewSchema) { - // Schema was remove - this.schemaChanges.push({ - id: this.oldSchema.id, - name: this.oldSchema.name, - type: 'removed' - }) - const verbChanges: VerbChange[] = [ - ...this.oldSchema.groups.flatMap((group) => - group.verbs.map((verb) => { - const verbChange: VerbChange = { - description: $localize`:@@schemaChangeVerbRemoved:Verb ${verb.label} removed`, - type: 'removed', - new: verb, - isEssentialVerb: false, - objectChanges: [] - } - return verbChange - }) - ), - ...this.oldSchema.essential_verbs.map((verb) => { - const verbChange: VerbChange = { - description: $localize`:@@schemaChangeVerbRemoved:Verb ${verb.label} removed`, - type: 'removed', - new: verb, - isEssentialVerb: false, - objectChanges: [] - } - return verbChange - }) - ] - for (const verbChange of verbChanges) - this.updateSchemaChanges(this.oldSchema, verbChange) + } + + for (const oldVerb of oldSchemaVerbs) { + const matchingNewVerb = newSchemaVerbs.find((newVerb) => newVerb.id === oldVerb.id) + if (!matchingNewVerb) { + const verbChange: VerbChange = { + description: $localize`:@@schemaChangeVerbRemoved:Verb ${oldVerb.label} removed`, + old: oldVerb, + type: 'removed', + isEssentialVerb: false, + objectChanges: [] + } + this.updateSchemaChanges(matchingOldSchema, verbChange) } + } - // Detect if verb from old schema was moved to a different group in new schema - this.detectVerbGroupChange().forEach((verbChange) => - this.updateSchemaChanges(this.oldSchema!, verbChange) + for (const newEssentialVerb of this.newSchema.essential_verbs) { + const matchingOldVerb = matchingOldSchema.essential_verbs.find( + (e) => e.id === newEssentialVerb.id ) - } - /** - * Detects if verbs have been moved from one group to another between old and new schema. - */ - detectVerbGroupChange(): VerbChange[] { - const groupsOldSchema = this.oldSchema?.groups ?? [] - const groupsNewSchema = this.newSchema?.groups ?? [] + if (matchingOldVerb) { + const verbChange = this.detectVerbChange( + matchingOldVerb, + newEssentialVerb + ) + if (verbChange) this.updateSchemaChanges(matchingOldSchema, verbChange) + } else { + const verbChange: VerbChange = { + description: $localize`:@@schemaChangeEssentialVerbAdded:Essential Verb ${newEssentialVerb.label} added`, + type: 'new', + new: newEssentialVerb, + isEssentialVerb: true, + objectChanges: [] + } + this.updateSchemaChanges(this.newSchema, verbChange) + } + } - const verbChanges: VerbChange[] = [] + for (const oldEssentialVerb of matchingOldSchema.essential_verbs) { + const matchingNewVerb = this.newSchema.essential_verbs.find( + (e) => e.id === oldEssentialVerb.id + ) - for (const groupOldSchema of groupsOldSchema) { - // Find matching group in new schema - const matchingGroupNewSchema = groupsNewSchema.find( - (group) => group.id === groupOldSchema.id - ) - if (matchingGroupNewSchema) { - for (const verb of groupOldSchema.verbs) { - const verbOldGroupNotInNewGroup = !matchingGroupNewSchema.verbs.find( - ({ id }) => id === verb.id - ) - if (verbOldGroupNotInNewGroup) { - // Check if verb was moved to another group in new schema - const otherGroup = this.findVerbInGroups(groupsNewSchema, verb) - if (otherGroup) { - const verbMovedGroupChange: VerbChange = { - description: $localize`:@@schemaChangeVerbMovedGroup:Verb ${verb.label} moved from group "${groupOldSchema.label}" to "${otherGroup.label}"`, - type: 'moved', - new: verb, - isEssentialVerb: false, - objectChanges: [] - } - verbChanges.push(verbMovedGroupChange) - } else { - // Verb was removed, already handled! - } - } - } - } else { - // Group from old schema doesn't exist in new schema, check if verbs were moved to another group - for (const verb of groupOldSchema.verbs) { - const otherGroup = this.findVerbInGroups(groupsNewSchema, verb) - if (otherGroup) { - const verbMovedGroupChange: VerbChange = { - description: $localize`:@@schemaChangeVerbMovedGroupAlt:Verb ${verb.label} moved to group "${otherGroup.label}"`, - type: 'moved', - new: verb, - isEssentialVerb: false, - objectChanges: [] - } - verbChanges.push(verbMovedGroupChange) - } - } - } + if (!matchingNewVerb) { + const verbChange: VerbChange = { + description: $localize`:@@schemaChangeEssentialVerbRemoved:Essential Verb ${oldEssentialVerb.label} removed`, + old: oldEssentialVerb, + type: 'removed', + isEssentialVerb: true, + objectChanges: [] + } + this.updateSchemaChanges(matchingOldSchema, verbChange) } - return verbChanges + } + } else { + // New schema added + this.schemaChanges.push({ + id: this.newSchema.id, + name: this.newSchema.name, + type: 'new' + }) + const verbChanges: VerbChange[] = [ + ...this.newSchema.verbs.map((verb) => + { + const verbChange: VerbChange = { + description: $localize`:@@schemaChangeVerbAdded:Verb ${verb.label} added`, + type: 'new', + new: verb, + isEssentialVerb: false, + objectChanges: [] + } + return verbChange + } + ), + ...this.newSchema.essential_verbs.map((verb) => { + const verbChange: VerbChange = { + description: $localize`:@@schemaChangeVerbAdded:Verb ${verb.label} added`, + type: 'new', + new: verb, + isEssentialVerb: false, + objectChanges: [] + } + return verbChange + }) + ] + for (const verbChange of verbChanges) + this.updateSchemaChanges(this.newSchema, verbChange) } - - /** - * Find verb in array of groups. - * @param groups - * @param verb - * @returns - */ - findVerbInGroups( - groups: ConsentGroupSchema[], - verb: XApiVerbSchema - ): ConsentGroupSchema | null { - for (const group of groups) { - for (const groupVerb of group.verbs) { - if (groupVerb.id === verb.id) return group + const matchingNewSchema = this.newSchema.id === this.oldSchema.id ? this.newSchema : null + if (!matchingNewSchema) { + // Schema was removed + this.schemaChanges.push({ + id: this.oldSchema.id, + name: this.oldSchema.name, + type: 'removed' + }) + const verbChanges: VerbChange[] = [ + ...this.oldSchema.verbs.map((verb) => { + const verbChange: VerbChange = { + description: $localize`:@@schemaChangeVerbRemoved:Verb ${verb.label} removed`, + type: 'removed', + new: verb, + isEssentialVerb: false, + objectChanges: [] } - } - return null + return verbChange + } + ), + ...this.oldSchema.essential_verbs.map((verb) => { + const verbChange: VerbChange = { + description: $localize`:@@schemaChangeVerbRemoved:Verb ${verb.label} removed`, + type: 'removed', + new: verb, + isEssentialVerb: false, + objectChanges: [] + } + return verbChange + }) + ] + for (const verbChange of verbChanges) + this.updateSchemaChanges(this.oldSchema, verbChange) } - updateSchemaChanges(schema: ProviderSchemaDefinition, verbChange: VerbChange): void { - if (!this.schemaChanges.find((e) => e.id === schema.id)) { - switch (verbChange.type) { - case 'new': - this.schemaChanges.push({ - id: schema.id, - name: schema.name, - type: 'changed', - newVerbs: [verbChange] - }) - return - case 'changed': - this.schemaChanges.push({ - id: schema.id, - name: schema.name, - type: 'changed', - changedVerbs: [verbChange] - }) - return - case 'removed': - this.schemaChanges.push({ - id: schema.id, - name: schema.name, - type: 'changed', - removedVerbs: [verbChange] - }) - return - case 'onlyObjectChanged': - this.schemaChanges.push({ - id: schema.id, - name: schema.name, - type: 'changed', - verbsWithChangedObjects: [verbChange] - }) - return - case 'moved': - this.schemaChanges.push({ - id: schema.id, - name: schema.name, - type: 'moved', - changedVerbs: [verbChange] - }) - return - default: - throw Error(`Verb change type '${verbChange.type}' didn't match!`) - } - } + } - this.schemaChanges = this.schemaChanges.map((schemaChange) => { - if (schemaChange.id === schema.id) { - if (verbChange.type === 'new') - return { - ...schemaChange, - newVerbs: [...(schemaChange.newVerbs ?? []), verbChange] - } - if (verbChange.type === 'changed') - return { - ...schemaChange, - changedVerbs: [...(schemaChange.changedVerbs ?? []), verbChange] - } - if (verbChange.type === 'removed') - return { - ...schemaChange, - removedVerbs: [...(schemaChange.removedVerbs ?? []), verbChange] - } - if (verbChange.type === 'onlyObjectChanged') - return { - ...schemaChange, - verbsWithChangedObjects: [ - ...(schemaChange.verbsWithChangedObjects ?? []), - verbChange - ] - } - if (verbChange.type === 'moved') - return { - ...schemaChange, - changedVerbs: [...(schemaChange.changedVerbs ?? []), verbChange] - } - } - return schemaChange - }) + + updateSchemaChanges(schema: ProviderSchemaDefinition, verbChange: VerbChange): void { + if (!this.schemaChanges.find((e) => e.id === schema.id)) { + switch (verbChange.type) { + case 'new': + this.schemaChanges.push({ + id: schema.id, + name: schema.name, + type: 'changed', + newVerbs: [verbChange] + }) + return + case 'changed': + this.schemaChanges.push({ + id: schema.id, + name: schema.name, + type: 'changed', + changedVerbs: [verbChange] + }) + return + case 'removed': + this.schemaChanges.push({ + id: schema.id, + name: schema.name, + type: 'changed', + removedVerbs: [verbChange] + }) + return + case 'onlyObjectChanged': + this.schemaChanges.push({ + id: schema.id, + name: schema.name, + type: 'changed', + verbsWithChangedObjects: [verbChange] + }) + return + case 'moved': + this.schemaChanges.push({ + id: schema.id, + name: schema.name, + type: 'moved', + changedVerbs: [verbChange] + }) + return + default: + throw Error(`Verb change type '${verbChange.type}' didn't match!`) + } } + this.schemaChanges = this.schemaChanges.map((schemaChange) => { + if (schemaChange.id === schema.id) { + if (verbChange.type === 'new') + return { + ...schemaChange, + newVerbs: [...(schemaChange.newVerbs ?? []), verbChange] + } + if (verbChange.type === 'changed') + return { + ...schemaChange, + changedVerbs: [...(schemaChange.changedVerbs ?? []), verbChange] + } + if (verbChange.type === 'removed') + return { + ...schemaChange, + removedVerbs: [...(schemaChange.removedVerbs ?? []), verbChange] + } + if (verbChange.type === 'onlyObjectChanged') + return { + ...schemaChange, + verbsWithChangedObjects: [ + ...(schemaChange.verbsWithChangedObjects ?? []), + verbChange + ] + } + if (verbChange.type === 'moved') + return { + ...schemaChange, + changedVerbs: [...(schemaChange.changedVerbs ?? []), verbChange] + } + } + return schemaChange + }) + } + protected readonly faWrench = faWrench protected readonly faPlus = faPlus protected readonly faTrash = faTrash diff --git a/src/frontend/src/app/consent-management/verb-groups/verb-groups.component.html b/src/frontend/src/app/consent-management/verb-groups/verb-groups.component.html new file mode 100644 index 0000000..29e706a --- /dev/null +++ b/src/frontend/src/app/consent-management/verb-groups/verb-groups.component.html @@ -0,0 +1,41 @@ +<nz-collapse> + <nz-collapse-panel [nzHeader]="panelHeader"> + <ng-template #panelHeader> + <span i18n="Verb Groups | Header @@groups"> + Verb Groups + </span> + </ng-template> + + <nz-collapse> + <nz-collapse-panel *ngFor="let group of verbGroups.concat(additionalVerbGroups)" [nzHeader]="group.label"> + <p class="subheader" i18n="Description @@description">> + Description + </p> + + <markdown [data]="group.description"></markdown> + + <p class="subheader" i18n="Purpose of collection @@purposeOfCollection">> + Purpose of collection + </p> + <markdown [data]="group.purposeOfCollection"></markdown> + + <p> + <span class="subheader" i18n="Contained Verbs: @@containedVerbs">Enthaltene Verben:</span> + <nz-list> + <nz-list-item *ngFor="let verb of group.verbs"> + {{verb.label}}: {{verb.description}} + </nz-list-item> + </nz-list> + </p> + </nz-collapse-panel> + </nz-collapse> + <p></p> + <button + (click)="createVerbGroup()" + nz-button + i18n="Create Data Packet @@createGroup"> + Create Data Packet + </button> + + </nz-collapse-panel> +</nz-collapse> diff --git a/src/frontend/src/app/consent-management/verb-groups/verb-groups.component.scss b/src/frontend/src/app/consent-management/verb-groups/verb-groups.component.scss new file mode 100644 index 0000000..41045ec --- /dev/null +++ b/src/frontend/src/app/consent-management/verb-groups/verb-groups.component.scss @@ -0,0 +1,3 @@ +.subheader { + font-weight: 500; +} diff --git a/src/frontend/src/app/consent-management/verb-groups/verb-groups.component.spec.ts b/src/frontend/src/app/consent-management/verb-groups/verb-groups.component.spec.ts new file mode 100644 index 0000000..89f6d30 --- /dev/null +++ b/src/frontend/src/app/consent-management/verb-groups/verb-groups.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { VerbGroupsComponent } from './verb-groups.component'; + +describe('VerbGroupsComponent', () => { + let component: VerbGroupsComponent; + let fixture: ComponentFixture<VerbGroupsComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ VerbGroupsComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(VerbGroupsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/src/app/consent-management/verb-groups/verb-groups.component.ts b/src/frontend/src/app/consent-management/verb-groups/verb-groups.component.ts new file mode 100644 index 0000000..f246c41 --- /dev/null +++ b/src/frontend/src/app/consent-management/verb-groups/verb-groups.component.ts @@ -0,0 +1,43 @@ +import { Component, Input } from '@angular/core' +import { XApiVerbGroupSchema, XApiVerbSchema } from '../consentDeclaration' +import { NzModalService } from 'ng-zorro-antd/modal' +import { CreateVerbGroupDialog } from '../../dialogs/create-verb-group-dialog/create-verb-group-dialog' +import { ApiService } from '../../services/api.service' +import { NzMessageService } from 'ng-zorro-antd/message' + +@Component({ + selector: 'app-verb-groups', + templateUrl: './verb-groups.component.html', + styleUrls: ['./verb-groups.component.scss'] +}) +export class VerbGroupsComponent { + @Input() provider_id: number = -1; + @Input() allVerbs: XApiVerbSchema[] = []; + @Input() verbGroups: XApiVerbGroupSchema[] = []; + + protected additionalVerbGroups: XApiVerbGroupSchema[] = []; + + constructor(private _apiService: ApiService, private _dialog: NzModalService, private _messageService: NzMessageService) { + } + + createVerbGroup(): void { + const dialogRef = this._dialog.create({ + nzContent: CreateVerbGroupDialog, + nzComponentParams: { + headerText: $localize`:@@createGroup:Create Data Packet`, + verbs: this.allVerbs, + } + }); + dialogRef.afterClose.subscribe(result => { + const {label, description, purposeOfCollection, requiresConsent, selectedVerbs} = result; + this._apiService.createOrUpdateVerbGroup(this.provider_id, "", label, description, purposeOfCollection, requiresConsent, selectedVerbs) + .subscribe(result => { + if(result.group) + { + this.additionalVerbGroups = [...this.additionalVerbGroups, result.group]; + this._messageService.success("Das Datenbündel wurde erstellt.") + } + }) + }); + } +} diff --git a/src/frontend/src/app/control-center/control-center.component.html b/src/frontend/src/app/control-center/control-center.component.html new file mode 100644 index 0000000..95e3c73 --- /dev/null +++ b/src/frontend/src/app/control-center/control-center.component.html @@ -0,0 +1,51 @@ +<div class="control-center"> + <h1 i18n="Control Center @@controlCenter">Control Center</h1> + + <h2 i18n="Users @@usersAndRoles">Users and Roles</h2> + <nz-table + #userTable + [nzData]="this.users" + > + <thead> + <tr> + <th></th> + <th>#</th> + <th>Vorname</th> + <th>Nachname</th> + <th>E-Mail</th> + <th>Admin</th> + <th>Pausiert</th> + <th>Datenschutzerklärung</th> + </tr> + </thead> + <tbody> + <ng-container *ngFor="let user of userTable.data"> + <tr> + <td [nzExpand]="expandSet.has(user.id)" + (nzExpandChange)="onExpandChange(user.id, $event)"></td> + <td>{{user.id}}</td> + <td>{{user.first_name}}</td> + <td>{{user.last_name}}</td> + <td>{{user.email}}</td> + <td>{{user.is_superuser}}</td> + <td>{{user.paused_data_recording}}</td> + <td>{{user.general_privacy_policy}}</td> + </tr> + <tr [nzExpand]="expandSet.has(user.id)"> + <td [attr.colspan]="8"> + <h3>Gruppen</h3> + <nz-checkbox-group [(ngModel)]="user_group_checkboxes[user.id]"> + </nz-checkbox-group> + + <h3>Rechte</h3> + <nz-checkbox-group [(ngModel)]="user_permission_checkboxes[user.id]"> + </nz-checkbox-group> + </td> + </tr> + </ng-container> + </tbody> + </nz-table> + + <app-provider> + </app-provider> +</div> diff --git a/src/frontend/src/app/control-center/control-center.component.scss b/src/frontend/src/app/control-center/control-center.component.scss new file mode 100644 index 0000000..1b9bbe4 --- /dev/null +++ b/src/frontend/src/app/control-center/control-center.component.scss @@ -0,0 +1,3 @@ +.control-center { + padding: 20px; +} diff --git a/src/frontend/src/app/control-center/control-center.component.spec.ts b/src/frontend/src/app/control-center/control-center.component.spec.ts new file mode 100644 index 0000000..3316735 --- /dev/null +++ b/src/frontend/src/app/control-center/control-center.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ControlCenterComponent } from './control-center.component'; + +describe('ControlCenterComponent', () => { + let component: ControlCenterComponent; + let fixture: ComponentFixture<ControlCenterComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ControlCenterComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ControlCenterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/frontend/src/app/control-center/control-center.component.ts b/src/frontend/src/app/control-center/control-center.component.ts new file mode 100644 index 0000000..22766d0 --- /dev/null +++ b/src/frontend/src/app/control-center/control-center.component.ts @@ -0,0 +1,74 @@ +import { Component } from '@angular/core' +import { NzMessageService } from 'ng-zorro-antd/message' +import { NzModalService } from 'ng-zorro-antd/modal' +import { ApiService, AuthGroup, AuthPermission, AuthUser } from '../services/api.service' + +interface AuthGroupCheckbox { + label: string + value: string + checked?: boolean +} +type UserGroupCheckboxMap = Record<number, AuthGroupCheckbox[]>; // id to checkboxes + +@Component({ + selector: 'app-control-center', + templateUrl: './control-center.component.html', + styleUrls: ['./control-center.component.scss'] +}) + +export class ControlCenterComponent { + protected users: AuthUser[] = []; + protected auth_permissions: AuthPermission[] = []; + protected auth_groups: AuthGroup[] = []; + + expandSet = new Set<number>(); + protected user_group_checkboxes: UserGroupCheckboxMap = {} + protected user_permission_checkboxes: UserGroupCheckboxMap = {} + + onExpandChange(id: number, checked: boolean): void { + if (checked) { + this.expandSet.clear(); + this.expandSet.add(id); + } else { + this.expandSet.delete(id); + } + } + constructor( + private _messageService: NzMessageService, + private _apiService: ApiService, + public dialog: NzModalService + ) { + + this._apiService.getGroups().subscribe(result => { + this.auth_groups = result; + this._apiService.getPermissions().subscribe(result => { + this.auth_permissions = result; + this._apiService.getUsers().subscribe(result => { + this.users = result; + let new_group_checkboxes: UserGroupCheckboxMap = {}; + let new_permission_checkboxes: UserGroupCheckboxMap = {}; + result.forEach((user) => { + new_group_checkboxes[user.id] = this.auth_groups.map((group) => { + return { + label: group.name, + value: group.id.toString(), + checked: user.groups.map(gr => gr.id).includes(group.id) + } + }) + new_permission_checkboxes[user.id] = this.auth_permissions.map((permission) => { + return { + label: permission.name, + value: permission.id.toString(), + checked: user.user_permissions.map(pr => pr.id).includes(permission.id) + } + }) + + }) + this.user_group_checkboxes = new_group_checkboxes; + this.user_permission_checkboxes = new_permission_checkboxes; + }) + }) + }) + } + +} diff --git a/src/frontend/src/app/dialogs/create-verb-group-dialog/create-verb-group-dialog.html b/src/frontend/src/app/dialogs/create-verb-group-dialog/create-verb-group-dialog.html new file mode 100644 index 0000000..26b629e --- /dev/null +++ b/src/frontend/src/app/dialogs/create-verb-group-dialog/create-verb-group-dialog.html @@ -0,0 +1,86 @@ +<div *nzModalTitle>{{ headerText }}</div> + +<div> + <form nz-form [formGroup]="form" (ngSubmit)="onSubmit()"> + + <nz-form-item> + <nz-form-label nzRequired [nzSpan]="10">Name</nz-form-label> + <nz-form-control [nzErrorTip]="labelError" [nzSpan]="14"> + <nz-input-group> + <input + nz-input + placeholder="Name" + [formControl]="labelController" + autocomplete="off" /> + </nz-input-group> + <ng-template #labelError let-control> + <ng-container *ngIf="control.hasError('required')" i18n="Name is required @@providerSchemaNameRequiredHint"> + Name is <strong>required</strong> + </ng-container> + </ng-template> + </nz-form-control> + </nz-form-item> + + + + <nz-form-item> + <nz-form-label nzRequired [nzSpan]="10" i18n="Description @@description">Description</nz-form-label> + <nz-form-control [nzSpan]="14" [nzErrorTip]="descriptionError"> + <nz-input-group> + <textarea + nz-input + [rows]="5" + [formControl]="descriptionController" + autocomplete="off"></textarea> + </nz-input-group> + <ng-template #descriptionError let-control> + <ng-container *ngIf="control.hasError('required')" i18n="Description is required @@descriptionRequiredHint"> + Description is <strong>required</strong> + </ng-container> + </ng-template> + </nz-form-control> + </nz-form-item> + + <nz-form-item> + <nz-form-label nzRequired [nzSpan]="10" i18n="Purpose @@purposeOfCollectionTitle">Purpose of collection</nz-form-label> + <nz-form-control [nzSpan]="14" [nzErrorTip]="purposeError"> + <nz-input-group> + <textarea + nz-input + [rows]="5" + [formControl]="purposeController" + autocomplete="off"></textarea> + </nz-input-group> + <ng-template #purposeError let-control> + <ng-container *ngIf="control.hasError('required')" i18n="Purpose is required @@purposeRequiredHint"> + Purpose is <strong>required</strong> + </ng-container> + </ng-template> + </nz-form-control> + </nz-form-item> + + <nz-form-item> + <nz-form-label nzRequired [nzSpan]="10" i18n="Consent is @@consentIs"> + Consent is + </nz-form-label> + <nz-form-control [nzSpan]="14"> + <nz-radio-group [formControl]="requiresConsentController"> + <label nz-radio nzValue="1" i18n="required @@required"> + required + </label><br /> + <label nz-radio nzValue="0" i18n="not required @@notRequired"> + not required + </label> + </nz-radio-group> + </nz-form-control> + </nz-form-item> + + </form> + <nz-checkbox-group [(ngModel)]="selectedVerbs"> + + </nz-checkbox-group> +</div> +<div *nzModalFooter> + <button nz-button (click)="handleClose()" i18n="@@cancel">Cancel</button> + <button nz-button i18n="Submit @@submit" (click)="onSubmit()"> Submit</button> +</div> diff --git a/src/frontend/src/app/dialogs/create-verb-group-dialog/create-verb-group-dialog.scss b/src/frontend/src/app/dialogs/create-verb-group-dialog/create-verb-group-dialog.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/frontend/src/app/dialogs/create-verb-group-dialog/create-verb-group-dialog.ts b/src/frontend/src/app/dialogs/create-verb-group-dialog/create-verb-group-dialog.ts new file mode 100644 index 0000000..12f31ab --- /dev/null +++ b/src/frontend/src/app/dialogs/create-verb-group-dialog/create-verb-group-dialog.ts @@ -0,0 +1,102 @@ +import { Component, OnInit, Input, SimpleChanges, OnChanges } from '@angular/core' +import { + AbstractControl, FormArray, + FormBuilder, + FormControl, + FormGroup, + ValidationErrors, + ValidatorFn, + Validators +} from '@angular/forms' +import * as moment from 'moment' +import { ApiService } from 'src/app/services/api.service' +import { NzModalRef } from 'ng-zorro-antd/modal' +import { faTimesCircle, faCheckCircle } from '@fortawesome/free-regular-svg-icons' +import { XApiVerbSchema } from '../../consent-management/consentDeclaration' + +interface SelectedVerb { + label: string + value: string + checked: boolean +} + +@Component({ + selector: 'app-create-verb-group-dialog', + templateUrl: './create-verb-group-dialog.html', + styleUrls: ['./create-verb-group-dialog.scss'] +}) +export class CreateVerbGroupDialog implements OnChanges, OnInit { + @Input() headerText: string = ''; + @Input() verbs: XApiVerbSchema[] = []; + + form: FormGroup + + protected selectedVerbs: SelectedVerb[] = []; + + constructor( + public _dialogRef: NzModalRef<CreateVerbGroupDialog>, + private _formBuilder: FormBuilder, + private _apiService: ApiService + ) { + this.form = this._formBuilder.group({ + label: [ + '', + Validators.required + ], + description: ['', Validators.required], + purposeOfCollection: ['', Validators.required], + requiresConsent: ["1", Validators.required], + }) + } + + get labelController(): FormControl { + return this.form.get('label') as FormControl + } + + get descriptionController(): FormControl { + return this.form.get('description') as FormControl + } + + get purposeController(): FormControl { + return this.form.get('purposeOfCollection') as FormControl + } + + get requiresConsentController(): FormControl { + return this.form.get('requiresConsent') as FormControl + } + + updateSelectedVerbs(): void { + this.selectedVerbs = this.verbs.map(verb => { + return { + label: verb.label, + value: verb.id, + checked: false + } + }) + } + ngOnChanges(changes: SimpleChanges) { + if (changes['verbs'] && this.verbs) { + this.updateSelectedVerbs(); + } + } + + ngOnInit(): void { + this.updateSelectedVerbs(); + } + + onSubmit(): void { + if (this.form.valid) + this._dialogRef.close({ + label: this.form.value.label, + description: this.form.value.description, + purposeOfCollection: this.form.value.purposeOfCollection, + requiresConsent: this.form.value.requiresConsent == "1", + selectedVerbs: this.selectedVerbs.filter(verb => verb.checked).map(verb => {return {id: verb.value}}), + }) + } + + handleClose(): void { + this._dialogRef.destroy() + } + +} diff --git a/src/frontend/src/app/home/home.component.html b/src/frontend/src/app/home/home.component.html index b75d9c9..83dae37 100644 --- a/src/frontend/src/app/home/home.component.html +++ b/src/frontend/src/app/home/home.component.html @@ -16,6 +16,6 @@ </div> <div> <h1>Dashboard</h1> - <p>Present results from Analytics Engine in graphically rich dashboards that easily integrate with existing learning platforms.</p> + <p>Present results from Analytics Engines in graphically rich dashboards that easily integrate with existing learning platforms.</p> </div> </div> diff --git a/src/frontend/src/app/home/home.component.scss b/src/frontend/src/app/home/home.component.scss index 0e4c73d..f887742 100644 --- a/src/frontend/src/app/home/home.component.scss +++ b/src/frontend/src/app/home/home.component.scss @@ -1,7 +1,7 @@ .home-wrapper { position: relative; height: 420px; - background: #F0ECF0; + background: #f7f7f7; .home-message { width: 90%; @@ -29,6 +29,6 @@ gap: 20px; flex-wrap: wrap; & > div { - width: 400px; + width: 300px; } } diff --git a/src/frontend/src/app/merge-data/merge-data.component.html b/src/frontend/src/app/merge-data/merge-data.component.html index e2daddc..ec5c74e 100644 --- a/src/frontend/src/app/merge-data/merge-data.component.html +++ b/src/frontend/src/app/merge-data/merge-data.component.html @@ -1,29 +1,25 @@ -<div class="content"> - <nz-card style='margin: 8px;'> - <nz-card-meta [nzTitle]="cardTitle" [nzDescription]="cardDescription"></nz-card-meta> - <ng-template #cardTitle i18n="Merge Data | Header entry @@mergeDataHeader"> - Merge Data - </ng-template> - <ng-template #cardDescription i18n="@@mergeDataDescription"> - This tool enables you to associate data which has been anonymized by use of a TAN to your account. All entries which match the given TAN and provider will be assigned. - </ng-template> - <p></p> - <form nz-form nzLayout="vertical" [formGroup]="mergeForm" (ngSubmit)="submit()"> - <nz-form-item> - <nz-form-label>Provider</nz-form-label> - <nz-form-control> - <nz-select formControlName="provider"> - <nz-option *ngFor="let provider of providers" [nzValue]="provider.id" [nzLabel]="provider.name"></nz-option> - </nz-select> - </nz-form-control> - </nz-form-item> - <nz-form-item> - <nz-form-label>TAN</nz-form-label> - <nz-form-control> - <input nz-input formControlName="tan" /> - </nz-form-control> - </nz-form-item> - <button type="submit" nz-button i18n="Merge Data | Header entry @@mergeDataHeader">Merge Data</button> - </form> - </nz-card> -</div> +<h1 i18n="Merge Data | Header entry @@mergeDataHeader"> + Merge Data +</h1> +<p i18n="@@mergeDataDescription"> + This tool enables you to associate data which has been anonymized by use of a TAN to your account. All entries which match the given TAN and provider will be assigned. +</p> + +<form nz-form nzLayout="vertical" [formGroup]="mergeForm" (ngSubmit)="submit()"> + <nz-form-item> + <nz-form-label>Provider</nz-form-label> + <nz-form-control> + <nz-select formControlName="provider"> + <nz-option *ngFor="let provider of providers" [nzValue]="provider.id" [nzLabel]="provider.name"></nz-option> + </nz-select> + </nz-form-control> + </nz-form-item> + <nz-form-item> + <nz-form-label>TAN</nz-form-label> + <nz-form-control> + <input nz-input formControlName="tan" /> + </nz-form-control> + </nz-form-item> + <button type="submit" nz-button i18n="Merge Data | Header entry @@mergeDataHeader">Merge Data</button> +</form> + diff --git a/src/frontend/src/app/navigation/header/header.component.html b/src/frontend/src/app/navigation/header/header.component.html index 654112c..c83a336 100644 --- a/src/frontend/src/app/navigation/header/header.component.html +++ b/src/frontend/src/app/navigation/header/header.component.html @@ -64,6 +64,15 @@ > Analytics Tokens </li> + + <li nz-menu-item + routerLink="/control-center" + *ngIf="loggedIn?.isProvider" + i18n="Control Center | Header Entry @@controlCenter" + [nzMatchRouter]="true" + > + Control Center + </li> </ul> <div class="account"> diff --git a/src/frontend/src/app/page-not-found/page-not-found.component.html b/src/frontend/src/app/page-not-found/page-not-found.component.html index 734ba63..2098f5f 100644 --- a/src/frontend/src/app/page-not-found/page-not-found.component.html +++ b/src/frontend/src/app/page-not-found/page-not-found.component.html @@ -1,6 +1,6 @@ <div class="not-found-wrapper"> <div class="not-found-content"> <img ngSrc="assets/not_found.svg" alt="Not found" height="308" width="393"> - <div class="message">Oops! We can't find that page.</div> + <div class="message" i18n="We can't find that page. | 404 message @@404message">Oops! We can't find that page.</div> </div> </div> diff --git a/src/frontend/src/app/services/api.service.ts b/src/frontend/src/app/services/api.service.ts index 79b00b8..53be7ff 100644 --- a/src/frontend/src/app/services/api.service.ts +++ b/src/frontend/src/app/services/api.service.ts @@ -3,7 +3,7 @@ import { ProviderId, ProviderSchema, UserConsent, - UserConsentVerbs + UserConsentVerbs, XApiVerbGroupSchema } from '../consent-management/consentDeclaration' import { HttpClient } from '@angular/common/http' import { Observable } from 'rxjs' @@ -32,6 +32,7 @@ export interface Provider { description: string createdAt: string versions: ProviderSchema[] + groups?: XApiVerbGroupSchema[] } export interface ApplicationTokens { @@ -74,11 +75,6 @@ export interface ProviderAnalyticsToken { analytics_tokens: AnalyticsToken[] } -export interface Verb { - id: string - label: string - selected: boolean -} interface VisualizationToken { id: number @@ -101,6 +97,33 @@ export interface UserConsentStatus { > } +export interface AuthPermission { + id: number + name: string + codename: string +} +export interface AuthGroup { + id: number + name: string + permissions: AuthPermission[] +} + +export interface AuthUser { + id: number + last_login?: Date + is_superuser: boolean + first_name: string + last_name: string + is_staff: boolean + is_active: boolean + date_joined: Date + email: string + paused_data_recording: boolean + general_privacy_policy: boolean + groups: AuthGroup[] + user_permissions: AuthPermission[] +} + @Injectable({ providedIn: 'root' }) @@ -112,6 +135,18 @@ export class ApiService { return this.http.get<Provider[]>(`${environment.apiUrl}/api/v1/consents/provider`) } + getUsers(): Observable<AuthUser[]> { + return this.http.get<AuthUser[]>(`${environment.apiUrl}/api/v1/auth/users`) + } + + getGroups(): Observable<AuthGroup[]> { + return this.http.get<AuthGroup[]>(`${environment.apiUrl}/api/v1/auth/groups`) + } + + getPermissions(): Observable<AuthPermission[]> { + return this.http.get<AuthPermission[]>(`${environment.apiUrl}/api/v1/auth/permissions`) + } + getUserConsent(providerId: number): Observable<UserConsentResponse> { return this.http.get<UserConsentResponse>( `${environment.apiUrl}/api/v1/consents/user/${providerId}` @@ -208,6 +243,20 @@ export class ApiService { ) } + createOrUpdateVerbGroup(provider_id: number, id: string, label: string, description: string, + purposeOfCollection: string, requiresConsent: boolean, + selectedVerbs: string[]) { + console.log(`${environment.apiUrl}/api/v1/consents/provider/${provider_id}/create-verb-group`) + return this.http.post<{message: string, group?: XApiVerbGroupSchema}>(`${environment.apiUrl}/api/v1/consents/provider/${provider_id}/create-verb-group`, { + id: id, + label: label, + description: description, + purposeOfCollection: purposeOfCollection, + requiresConsent: requiresConsent, + verbs: selectedVerbs + }) + } + saveAccessRelation(target: number, ids: number[]): Observable<AnalyticsToken> { return this.http.post<AnalyticsToken>( `${environment.apiUrl}/api/v1/provider/analytics-tokens/sync-engine-access`, diff --git a/src/frontend/src/environments/environment.prod.ts b/src/frontend/src/environments/environment.prod.ts index 8bb15f8..4309a07 100644 --- a/src/frontend/src/environments/environment.prod.ts +++ b/src/frontend/src/environments/environment.prod.ts @@ -4,7 +4,7 @@ export const environment = { pageVisibility: { analyses: true, consent_history: true, - consent_management: true, + consent_management: false, merge_data: true, data_disclosure: true, } diff --git a/src/frontend/src/locale/messages.de.xlf b/src/frontend/src/locale/messages.de.xlf index 7c46ada..5f9ce6e 100644 --- a/src/frontend/src/locale/messages.de.xlf +++ b/src/frontend/src/locale/messages.de.xlf @@ -14,7 +14,7 @@ </trans-unit> <trans-unit id="analysisIsActiveNote" datatype="html"> <source>This analysis is already active since you consented to all necessary data collections.</source> - <target>Diese Analyse ist bereits aktiv, da Sie der notwendigen Datenerfassung bereits zugestimmt haben.</target> + <target>Diese Analyse ist bereits aktiv, da du der notwendigen Datenerfassung bereits zugestimmt hast.</target> <context-group purpose="location"> <context context-type="sourcefile">src/app/analytics-engine/analysis-card/verbs-modal/verbs-modal.component.html</context> <context context-type="linenumber">14</context> @@ -30,7 +30,7 @@ </trans-unit> <trans-unit id="analysisNotActiveAlert" datatype="html"> <source> This analysis is not active, since you did not consent to the necessary data collections. You have to consent to all necessary data collections to activate this analysis. You can click the following button to consent to all data collections. You can navigate to the consent management to revert this. </source> - <target> Diese Analyse ist nicht aktiv, da Sie der notwendigen Datenerfassung nicht zugestimmt haben. Sie müssen jeglicher notwendiger Datenerfassung zustimmen, um diese Analyse zu aktivieren. Sie können den folgenden Button anklicken, um der Erfassung zuzustimmen. Sie können dies in der Zustimmungsverwaltung widerrufen. </target> + <target> Diese Analyse ist nicht aktiv, da du der notwendigen Datenerfassung nicht zugestimmt hast. Du musst jeglicher notwendiger Datenerfassung zustimmen, um diese Analyse zu aktivieren. Du kannst den folgenden Button anklicken, um der Erfassung zuzustimmen. Du kannst dies in der Zustimmungsverwaltung widerrufen. </target> <context-group purpose="location"> <context context-type="sourcefile">src/app/analytics-engine/analysis-card/verbs-modal/verbs-modal.component.html</context> <context context-type="linenumber">26,28</context> @@ -187,7 +187,7 @@ </trans-unit> <trans-unit id="accessibleEnginesHeader" datatype="html"> <source> Accessible Engines </source> - <target> Engines, auf die Sie Zugriff haben </target> + <target> Engines, auf die du Zugriff hast </target> <context-group purpose="location"> <context context-type="sourcefile">src/app/consent-management/analytics-tokens/analytics-tokens.component.html</context> <context context-type="linenumber">129,131</context> @@ -235,7 +235,7 @@ </trans-unit> <trans-unit id="deleteSelectedToken" datatype="html"> <source>Delete selected token</source> - <target>Ausgewählter Token gelöscht</target> + <target>Ausgewählten Token löschen</target> <context-group purpose="location"> <context context-type="sourcefile">src/app/consent-management/analytics-tokens/analytics-tokens.component.ts</context> <context context-type="linenumber">114</context> @@ -311,7 +311,7 @@ </trans-unit> <trans-unit id="mergeDataDescription" datatype="html"> <source>This tool enables you to associate data which has been anonymized by use of a TAN to your account. All entries which match the given TAN and provider will be assigned.</source> - <target>Dieses Werkzeug ermöglicht es, Daten, welche mittels einer TAN anonymisiert sind, zum aktuellen Nutzer zuzuordnen. Alle Einträge, welche auf die angegebene TAN und den angegebenen Provider zutreffen, werden zugeordnet.</target> + <target>Dieses Werkzeug ermöglicht es, Daten, welche mittels einer TAN anonymisiert sind, zum aktuellen Nutzer zuzuordnen. Alle Einträge, welche auf die angegebene TAN und das angegebene Quellsystem zutreffen, werden zugeordnet.</target> <context-group purpose="location"> <context context-type="sourcefile">src/app/merge-data/merge-data.component.html</context> <context context-type="linenumber">7</context> @@ -338,7 +338,7 @@ </trans-unit> <trans-unit id="pausedDataRecordingDescription" datatype="html"> <source> Would you like to continue sharing selected data with Polaris?</source> - <target> Möchten Sie die ausgewählten Daten weiterhin mit Polaris teilen?</target> + <target> Möchtest du die ausgewählten Daten weiterhin mit Polaris teilen?</target> <context-group purpose="location"> <context context-type="sourcefile">src/app/consent-management/pause-data-recording-dialog.html</context> <context context-type="linenumber">14,16</context> @@ -348,7 +348,7 @@ </trans-unit> <trans-unit id="continueDataRecordingDescription" datatype="html"> <source>Would you like to pause sharing selected data with Polaris?</source> - <target>Möchten Sie die ausgewählte Datenerfassung durch Polaris pausieren?</target> + <target>Möchtest du die ausgewählte Datenerfassung durch Polaris pausieren?</target> <context-group purpose="location"> <context context-type="sourcefile">src/app/consent-management/pause-data-recording-dialog.html</context> <context context-type="linenumber">19,24</context> @@ -409,7 +409,7 @@ </trans-unit> <trans-unit id="pauseVerbOrObjectConfirmation" datatype="html"> <source>Do you want the action to be executed?</source> - <target>Möchten Sie, dass die Aktion ausgeführt wird?</target> + <target>Möchtest du, dass die Aktion ausgeführt wird?</target> <context-group purpose="location"> <context context-type="sourcefile">src/app/consent-management/provider-settings/pause-verb-object-warning-dialog.html</context> <context context-type="linenumber">2</context> @@ -521,6 +521,10 @@ <source>Provider Schemas</source> <target>Anbieterschemas</target> </trans-unit> + <trans-unit id="latestTag" datatype="html"> + <source>latest</source> + <target>neustes</target> + </trans-unit> <trans-unit id="toggleDefinition" datatype="html"> <source>Toggle Definition</source> <target>Definition umschalten</target> @@ -559,7 +563,7 @@ </trans-unit> <trans-unit id="selectProviderSchema" datatype="html"> <source>Please select a JSON file.</source> - <target>Bitte geben Sie eine JSON-Datei an.</target> + <target>Bitte gib eine JSON-Datei an.</target> <context-group purpose="location"> <context context-type="sourcefile">src/app/consent-management/provider/provider.component.ts</context> <context context-type="linenumber">24</context> @@ -786,7 +790,7 @@ </trans-unit> <trans-unit id="firstUserConsentHint" datatype="html"> <source> You have not yet provided any information on data processing within the Polaris project. In the following steps, you can make an individual decision for each connected platform. </source> - <target> Sie haben noch keine Angaben zur Datenverarbeitung im Rahmen des Polaris-Projekts gemacht. In den folgenden Schritten können Sie eine individuelle Entscheidung für jede angeschlossene Plattform treffen. </target> + <target> Du hast noch keine Angaben zur Datenverarbeitung im Rahmen des Polaris-Projekts gemacht. In den folgenden Schritten kannst du eine individuelle Entscheidung für jede angeschlossene Plattform treffen. </target> <context-group purpose="location"> <context context-type="sourcefile">src/app/consent-management/wizard/wizard.component.html</context> <context context-type="linenumber">14,19</context> @@ -794,8 +798,8 @@ <note priority="1" from="description">First User Consent </note> </trans-unit> <trans-unit id="newProviderSchemaHint" datatype="html"> - <source> The provider uploaded a newer consent declaration. Please reviewed the changes. </source> - <target> Der Anbieter hat eine neuere Einverständniserklärung hochgeladen. Bitte überprüfen Sie die Änderungen. </target> + <source> The provider uploaded a newer schema. Please review the changes. </source> + <target> Der Anbieter hat ein neueres Schema hochgeladen. Bitte überprüfe die Änderungen. </target> <context-group purpose="location"> <context context-type="sourcefile">src/app/consent-management/wizard/wizard.component.html</context> <context context-type="linenumber">23,25</context> @@ -932,7 +936,7 @@ </trans-unit> <trans-unit id="pausedDataRecordingDescription" datatype="html"> <source> Would you like to continue sharing selected data with Polaris? </source> - <target> Möchten Sie das Teilen ausgewählter Daten mit Polaris fortsetzen? </target> + <target> Möchtest du das Teilen ausgewählter Daten mit Polaris fortsetzen? </target> <context-group purpose="location"> <context context-type="sourcefile">src/app/consent-management/pause-data-recording-dialog.html</context> <context context-type="linenumber">14,16</context> @@ -979,7 +983,7 @@ <source> Would you like to pause sharing selected data with Polaris? <x id="START_BOLD_TEXT" ctype="x-b" equiv-text="<b >"/>A delete job is set up for data deletion, which is only executed after a fixed time window.<x id="CLOSE_BOLD_TEXT" ctype="x-b" equiv-text="</b >"/></source> - <target> Möchten Sie die Erfassung ihrer Daten durch Polaris pausieren? <x id="START_BOLD_TEXT" ctype="x-b" equiv-text="<b + <target> Möchtest du die Erfassung deiner Daten durch Polaris pausieren? <x id="START_BOLD_TEXT" ctype="x-b" equiv-text="<b >"/> Für die Datenlöschung wird ein Löschautrag eingerichtet, welcher erst nach einem festen Zeitfenster ausgeführt wird. <x id="CLOSE_BOLD_TEXT" ctype="x-b" equiv-text="</b >"/> </target> <context-group purpose="location"> @@ -1052,7 +1056,7 @@ </trans-unit> <trans-unit id="selectProviderSchema" datatype="html"> <source>Please select a JSON file.</source> - <target>Bitte wählen Sie eine JSON-Datei aus.</target> + <target>Bitte wähle eine JSON-Datei aus.</target> <context-group purpose="location"> <context context-type="sourcefile">src/app/consent-management/provider/provider.component.ts</context> <context context-type="linenumber">19</context> @@ -1200,7 +1204,7 @@ <source>Delete data disclosure</source> <target>Datenauskunft löschen</target> <context-group purpose="location"> - <context context-type="sourcefile">src/app/data-disclosure/data-disclosure.component.ts</context> + <context context-type="sourcefile">src/app/data-disclosure/control-center.component.ts</context> <context context-type="linenumber">69</context> </context-group> </trans-unit> @@ -1208,7 +1212,7 @@ <source>Data disclosure deleted</source> <target>Datenauskunft wurde gelöscht</target> <context-group purpose="location"> - <context context-type="sourcefile">src/app/data-disclosure/data-disclosure.component.ts</context> + <context context-type="sourcefile">src/app/data-disclosure/control-center.component.ts</context> <context context-type="linenumber">73</context> </context-group> </trans-unit> @@ -1417,7 +1421,7 @@ </trans-unit> <trans-unit id="firstConsentDeclaration" datatype="html"> <source> You have not yet given consent for provider <x id="INTERPOLATION" equiv-text="{{ userData.provider.name }}"/>. </source> - <target> Sie haben bisher noch keine Zustimmung für den Provider <x id="INTERPOLATION" equiv-text="{{ userData.provider.name }}"/> gegeben. </target> + <target> Du hast bisher noch keine Zustimmung für den Provider <x id="INTERPOLATION" equiv-text="{{ userData.provider.name }}"/> gegeben. </target> <context-group purpose="location"> <context context-type="sourcefile">src/app/consent-management/wizard/wizard.component.html</context> <context context-type="linenumber">46,48</context> @@ -1426,7 +1430,7 @@ </trans-unit> <trans-unit id="consentDeclarationUpToDate" datatype="html"> <source> You do not need to make any changes to your existing consent form, as the provider has not made any changes in the meantime. </source> - <target> Sie müssen keine Änderungen an deiner bestehenden Einwilligungserklärung treffen, da der Provider in der Zwischenzeit keine Änderungen vorgenommen hat. </target> + <target> Du musst keine Änderungen an deiner bestehenden Einwilligungserklärung treffen, da der Provider in der Zwischenzeit keine Änderungen vorgenommen hat. </target> <context-group purpose="location"> <context context-type="sourcefile">src/app/consent-management/wizard/wizard.component.html</context> <context context-type="linenumber">41,44</context> @@ -1435,7 +1439,7 @@ </trans-unit> <trans-unit id="providerSchemaChanged" datatype="html"> <source> Since your last visit, the provider has made changes. For each option, you will now see your previous consent on the far left, and on the right, separated by an arrow, your current consent, which you can of course change. </source> - <target> Der Anbieter hat seit Ihrem letzten Besuch Änderungen vorgenommen. Für jede Option werden Sie Ihre vorherige Zustimmung auf der linken Seite sehen, während auf der rechten Seite, durch einen Pfeil getrennt, Ihre derzeitige Zustimmung dargestellt wird, mit der Möglichkeit, diese zu ändern. </target> + <target> Der Anbieter hat seit deinem letzten Besuch Änderungen vorgenommen. Für jede Option wirst du deine vorherige Zustimmung auf der linken Seite sehen, während auf der rechten Seite, durch einen Pfeil getrennt, deine derzeitige Zustimmung dargestellt wird, mit der Möglichkeit, diese zu ändern. </target> <context-group purpose="location"> <context context-type="sourcefile">src/app/consent-management/wizard/wizard.component.html</context> <context context-type="linenumber">20,25</context> @@ -1444,7 +1448,7 @@ </trans-unit> <trans-unit id="consentDeclarationUpToDate" datatype="html"> <source> You do not need to make any changes to your existing consent form, as the provider has not made any changes in the meantime. </source> - <target> Sie müssen keine Änderungen an Ihren bestehenden Zustimmungen vornehmen, da der Anbieter in der Zwischenzeit keine Änderungen vorgenommen hat. </target> + <target> Du musst keine Änderungen an deinen bestehenden Zustimmungen vornehmen, da der Anbieter in der Zwischenzeit keine Änderungen vorgenommen hat. </target> <context-group purpose="location"> <context context-type="sourcefile">src/app/consent-management/wizard/wizard.component.html</context> <context context-type="linenumber">41,44</context> @@ -1453,7 +1457,7 @@ </trans-unit> <trans-unit id="firstConsentDeclaration" datatype="html"> <source> You have not yet given consent for provider <x id="INTERPOLATION" equiv-text="{{ userData.provider.name }}"/>. </source> - <target> Sie haben bisher keine Zustimmung für den Anbieter <x id="INTERPOLATION" equiv-text="{{ userData.provider.name }}"/> erteilt. </target> + <target> Du hast bisher keine Zustimmung für den Anbieter <x id="INTERPOLATION" equiv-text="{{ userData.provider.name }}"/> erteilt. </target> <context-group purpose="location"> <context context-type="sourcefile">src/app/consent-management/wizard/wizard.component.html</context> <context context-type="linenumber">49,51</context> @@ -1462,7 +1466,7 @@ </trans-unit> <trans-unit id="changedProviderSchemaSummaryText" datatype="html"> <source> The provider has made changes since your last consent declaration which requires renewed consent from you. </source> - <target> Der Anbieter hat seit Ihrer letzten Einverständniserklärung Änderungen vorgenommen, welche eine erneute Zustimmung Ihrerseits erfordern. </target> + <target> Der Anbieter hat seit deiner letzten Einwilligung Änderungen vorgenommen, welche eine erneute Zustimmung von dir erfordern. </target> <context-group purpose="location"> <context context-type="sourcefile">src/app/consent-management/wizard/wizard.component.html</context> <context context-type="linenumber">74,77</context> @@ -1473,7 +1477,7 @@ </trans-unit> <trans-unit id="firstProviderConsentSummaryText" datatype="html"> <source> You have not previously given consent for this provider, this is your first consent declaration. </source> - <target> Sie haben für diesen Anbieter bisher keine Zustimmung erteilt, dies ist Ihre erste Einverständniserklärung. </target> + <target> Du hast für diesen Anbieter bisher keine Zustimmung erteilt, dies ist deine erste Einwilligung. </target> <context-group purpose="location"> <context context-type="sourcefile">src/app/consent-management/wizard/wizard.component.html</context> <context context-type="linenumber">96,99</context> @@ -1564,7 +1568,7 @@ </trans-unit> <trans-unit id="dataDisclosureProcessInfo" datatype="html"> <source> You can query all data collected about you at this point. The collection of the data takes place on a time-delayed basis. You will be informed about the completion via e-mail. Afterwards, the collected data will be available for download for a specified period of time and then will then be deleted automatically. </source> - <target> Sie können alle Daten, die bisher über Sie erfasst wurden, abfragen. Die Sammlung der Daten erfolgt zeitverzögert. Sie werden per E-Mail informiert, sobald der Vorgang abgeschlossen ist. Anschließend werden die gesammelten Daten für einen festen Zeitraum zum Herunterladen zur Verfügung stehen und danach automatisch gelöscht. </target> + <target> Du kannst alle Daten, die bisher über dich erfasst wurden, abfragen. Die Sammlung der Daten erfolgt zeitverzögert. Du wirst per E-Mail informiert, sobald der Vorgang abgeschlossen ist. Anschließend werden die gesammelten Daten für einen festen Zeitraum zum Herunterladen zur Verfügung stehen und danach automatisch gelöscht. </target> <context-group purpose="location"> <context context-type="sourcefile">src/app/data-disclosure/data-disclosure.component.html</context> <context context-type="linenumber">4,9</context> @@ -1616,6 +1620,10 @@ <note priority="1" from="description"> Table column header </note> <note priority="1" from="meaning">Created At </note> </trans-unit> + <trans-unit id="expires" datatype="html"> + <source>Expires</source> + <target>Läuft ab</target> + </trans-unit> <trans-unit id="availableUntil" datatype="html"> <source> Available until </source> <target> Verfügbar bis </target> @@ -1638,7 +1646,7 @@ <source>Request sent</source> <target>Anfrage verschickt</target> <context-group purpose="location"> - <context context-type="sourcefile">src/app/data-disclosure/data-disclosure.component.ts</context> + <context context-type="sourcefile">src/app/data-disclosure/control-center.component.ts</context> <context context-type="linenumber">47</context> </context-group> </trans-unit> @@ -1646,7 +1654,7 @@ <source>Delete data disclosure</source> <target>Datenauskunft löschen</target> <context-group purpose="location"> - <context context-type="sourcefile">src/app/data-disclosure/data-disclosure.component.ts</context> + <context context-type="sourcefile">src/app/data-disclosure/control-center.component.ts</context> <context context-type="linenumber">69</context> </context-group> </trans-unit> @@ -1654,7 +1662,7 @@ <source>Data disclosure deleted</source> <target>Datensauskunft gelöscht</target> <context-group purpose="location"> - <context context-type="sourcefile">src/app/data-disclosure/data-disclosure.component.ts</context> + <context context-type="sourcefile">src/app/data-disclosure/control-center.component.ts</context> <context context-type="linenumber">73</context> </context-group> </trans-unit> @@ -1766,7 +1774,7 @@ </trans-unit> <trans-unit id="deleteDialogDescription" datatype="html"> <source> Please confirm the action by entering <x id="START_BOLD_TEXT" ctype="x-b" equiv-text="<b>"/><x id="INTERPOLATION" equiv-text="{{confirmationText}}"/><x id="CLOSE_BOLD_TEXT" ctype="x-b" equiv-text="</b>"/>. </source> - <target> Bitte bestätigen Sie die Aktion, indem Sie <x id="START_BOLD_TEXT" ctype="x-b" equiv-text="<b>"/><x id="INTERPOLATION" equiv-text="{{confirmationText}}"/><x id="CLOSE_BOLD_TEXT" ctype="x-b" equiv-text="</b>"/> eingeben. </target> + <target> Bitte bestätige die Aktion, indem du <x id="START_BOLD_TEXT" ctype="x-b" equiv-text="<b>"/><x id="INTERPOLATION" equiv-text="{{confirmationText}}"/><x id="CLOSE_BOLD_TEXT" ctype="x-b" equiv-text="</b>"/> eingibst. </target> <context-group purpose="location"> <context context-type="sourcefile">src/app/dialogs/delete-dialog/delete-dialog.html</context> <context context-type="linenumber">4,6</context> @@ -1913,7 +1921,7 @@ </trans-unit> <trans-unit id="consentManagmentHeader" datatype="html"> <source>Consent Management</source> - <target>Einverständnis verwalten</target> + <target>Einwilligungsmanagement</target> <context-group purpose="location"> <context context-type="sourcefile">src/app/navigation/header/header.component.html</context> <context context-type="linenumber">21</context> @@ -1922,8 +1930,8 @@ <note priority="1" from="meaning">Consent Management </note> </trans-unit> <trans-unit id="consentHistoryHeader" datatype="html"> - <source>Consent History</source> - <target>Einverständnishistorie</target> + <source>Consent Management</source> + <target>Einwilligungsmanagement</target> <context-group purpose="location"> <context context-type="sourcefile">src/app/navigation/header/header.component.html</context> <context context-type="linenumber">21</context> @@ -1951,6 +1959,12 @@ <note priority="1" from="description"> Header Entry </note> <note priority="1" from="meaning">Analytics Tokens </note> </trans-unit> + <trans-unit id="controlCenter" datatype="html"> + <source>Control Center</source> + <target>Systemsteuerung</target> + <note priority="1" from="description"> Header Entry </note> + <note priority="1" from="meaning">Control Center</note> + </trans-unit> <trans-unit id="profileProfileDropDown" datatype="html"> <source>Profile</source> <target>Profil</target> @@ -2018,6 +2032,67 @@ <context context-type="linenumber">41</context> </context-group> </trans-unit> + <trans-unit id="uploadButton" datatype="html"> + <source>Select new image</source> + <target>Neues Bild auswählen</target> + </trans-unit> + <trans-unit id="uploadButtonDoUpload" datatype="html"> + <source>Upload selected image</source> + <target>Gewähltes Bild hochladen</target> + </trans-unit> + <trans-unit id="toastMessageMergeError" datatype="html"> + <source>Error</source> + <target>Fehler</target> + </trans-unit> + <trans-unit id="404message" datatype="html"> + <source>Oops! We can't find that page.</source> + <target>Diese Seite wurde nicht gefunden.</target> + </trans-unit> + <trans-unit id="usersAndRoles" datatype="html"> + <source>Users and Roles</source> + <target>Nutzende und Rollen</target> + </trans-unit> + <trans-unit id="dataSources" datatype="html"> + <source>Data Sources</source> + <target>Quellsysteme</target> + </trans-unit> + <trans-unit id="groups" datatype="html"> + <source>Data Packets</source> + <target>Datenbündel</target> + </trans-unit> + <trans-unit id="containedVerbs" datatype="html"> + <source>Contained Verbs:</source> + <target>Enthaltene Verben:</target> + </trans-unit> + <trans-unit id="createGroup" datatype="html"> + <source>Create New Data Packet</source> + <target>Neues Datenbündel erstellen</target> + </trans-unit> + <trans-unit id="descriptionRequiredHint" datatype="html"> + <source> Description is <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="<strong>"/>required<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="</strong>"/></source> + <target> Beschreibung wird <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="<strong>"/>benötigt<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="</strong>"/></target> + </trans-unit> + <trans-unit id="purposeOfCollectionTitle" datatype="html"> + <source>Purpose of collection</source> + <target>Verwendungszweck:</target> + </trans-unit> + <trans-unit id="purposeRequiredHint" datatype="html"> + <source> Purpose is <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="<strong>"/>required<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="</strong>"/></source> + <target> Verwendungszweck wird <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="<strong>"/>benötigt<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="</strong>"/></target> + </trans-unit> + <trans-unit id="consentIs" datatype="html"> + <source>Zustimmung wird</source> + </trans-unit> + <trans-unit id="required" datatype="html"> + <source>benötigt</source> + </trans-unit> + <trans-unit id="notRequired" datatype="html"> + <source>nicht benötigt</source> + </trans-unit> + <trans-unit id="includedVerbs" datatype="html"> + <source>Included verbs</source> + <target>Enthaltene Verben</target> + </trans-unit> </body> </file> </xliff> diff --git a/src/frontend/src/locale/messages.xlf b/src/frontend/src/locale/messages.xlf index ee2cae7..35ccfcc 100644 --- a/src/frontend/src/locale/messages.xlf +++ b/src/frontend/src/locale/messages.xlf @@ -463,6 +463,9 @@ <trans-unit id="providerSchemas" datatype="html"> <source>Provider Schemas</source> </trans-unit> + <trans-unit id="latestTag" datatype="html"> + <source>latest</source> + </trans-unit> <trans-unit id="toggleDefinition" datatype="html"> <source>Toggle Definition</source> <context-group purpose="location"> @@ -853,6 +856,9 @@ <note priority="1" from="description"> Table column header </note> <note priority="1" from="meaning">Created At </note> </trans-unit> + <trans-unit id="expires" datatype="html"> + <source>Expires</source> + </trans-unit> <trans-unit id="availableUntil" datatype="html"> <source> Available until </source> <context-group purpose="location"> @@ -872,21 +878,21 @@ <trans-unit id="successToastMessage" datatype="html"> <source>Request sent</source> <context-group purpose="location"> - <context context-type="sourcefile">src/app/data-disclosure/data-disclosure.component.ts</context> + <context context-type="sourcefile">src/app/data-disclosure/control-center.component.ts</context> <context context-type="linenumber">47</context> </context-group> </trans-unit> <trans-unit id="deleteDataDisclosure" datatype="html"> <source>Delete data disclosure</source> <context-group purpose="location"> - <context context-type="sourcefile">src/app/data-disclosure/data-disclosure.component.ts</context> + <context context-type="sourcefile">src/app/data-disclosure/control-center.component.ts</context> <context context-type="linenumber">69</context> </context-group> </trans-unit> <trans-unit id="deleteDataDisclosureSucces" datatype="html"> <source>Data disclosure deleted</source> <context-group purpose="location"> - <context context-type="sourcefile">src/app/data-disclosure/data-disclosure.component.ts</context> + <context context-type="sourcefile">src/app/data-disclosure/control-center.component.ts</context> <context context-type="linenumber">73</context> </context-group> </trans-unit> @@ -1126,7 +1132,7 @@ <note priority="1" from="meaning">Consent Management </note> </trans-unit> <trans-unit id="consentHistoryHeader" datatype="html"> - <source>Consent History</source> + <source>Consent Management</source> <context-group purpose="location"> <context context-type="sourcefile">src/app/navigation/header/header.component.html</context> <context context-type="linenumber">21</context> @@ -1168,6 +1174,11 @@ <note priority="1" from="description"> Header Entry </note> <note priority="1" from="meaning">Analytics Tokens </note> </trans-unit> + <trans-unit id="controlCenter" datatype="html"> + <source>Control Center</source> + <note priority="1" from="description"> Header Entry </note> + <note priority="1" from="meaning">Control Center</note> + </trans-unit> <trans-unit id="profileProfileDropDown" datatype="html"> <source>Profile</source> <context-group purpose="location"> @@ -1228,6 +1239,54 @@ <context context-type="linenumber">41</context> </context-group> </trans-unit> + <trans-unit id="uploadButton" datatype="html"> + <source>Select new image</source> + </trans-unit> + <trans-unit id="uploadButtonDoUpload" datatype="html"> + <source>Upload selected image</source> + </trans-unit> + <trans-unit id="toastMessageMergeError" datatype="html"> + <source>Error</source> + </trans-unit> + <trans-unit id="404message" datatype="html"> + <source>Oops! We can't find that page.</source> + </trans-unit> + <trans-unit id="usersAndRoles" datatype="html"> + <source>Users and Roles</source> + </trans-unit> + <trans-unit id="dataSources" datatype="html"> + <source>Data Sources</source> + </trans-unit> + <trans-unit id="groups" datatype="html"> + <source>Data Packets</source> + </trans-unit> + <trans-unit id="containedVerbs" datatype="html"> + <source>Contained Verbs:</source> + </trans-unit> + <trans-unit id="createGroup" datatype="html"> + <source>Create Data Packet</source> + </trans-unit> + <trans-unit id="descriptionRequiredHint" datatype="html"> + <source> Description is <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="<strong>"/>required<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="</strong>"/></source> + </trans-unit> + <trans-unit id="purposeOfCollectionTitle" datatype="html"> + <source>Purpose of collection</source> + </trans-unit> + <trans-unit id="purposeRequiredHint" datatype="html"> + <source> Purpose is <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="<strong>"/>required<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="</strong>"/></source> + </trans-unit> + <trans-unit id="consentIs" datatype="html"> + <source>Consent is</source> + </trans-unit> + <trans-unit id="required" datatype="html"> + <source>required</source> + </trans-unit> + <trans-unit id="notRequired" datatype="html"> + <source>not required</source> + </trans-unit> + <trans-unit id="includedVerbs" datatype="html"> + <source>Included verbs</source> + </trans-unit> </body> </file> </xliff> diff --git a/src/frontend/src/theme.less b/src/frontend/src/theme.less index c8229ff..41541e6 100644 --- a/src/frontend/src/theme.less +++ b/src/frontend/src/theme.less @@ -22,12 +22,12 @@ @primary-color-hover: #804d79; // Layout -@layout-body-background: #F0ECF0; -@layout-header-background: #F0ECF0; +@layout-body-background: #f7f7f7; +@layout-header-background: #f7f7f7; // colors: backgrounds and surfaces -@body-background: #F0ECF0; -@component-background: #F0ECF0; +@body-background: #f7f7f7; +@component-background: #f7f7f7; // colors: borders @popover-customize-border-color: #C6CDD4; @@ -64,3 +64,49 @@ @btn-default-bg: #fff; @btn-default-bg-hover: @primary-color; // TODO @btn-default-border: @primary-color; + +body { + font-size: 16px; +} +.content { + max-width: 1200px; + padding-top: 128px; + margin-left: 50%; + transform: translateX(-50%); +} +h1 { + font-size: 24px; + font-weight: 400; + margin-bottom: 40px; +} +h2 { + font-size: 20px; + font-weight: 400; + margin-bottom: 24px; +} + +p { + margin-bottom: 24px; +} +.ant-collapse-item { + border-color: #c6cdd4; + box-shadow: none; + border-radius: 0; +} + +.ant-collapse-header{ + background: #fff; + border-radius: 0; + font-size: 18px; + height: 56px; +} + +.ant-collapse-item-active > .ant-collapse-header { + color: @primary-color !important; + font-weight: 500; +} +.ant-collapse-content-box { + background: #fff; + border-color: #c6cdd4; + border-radius: 0; +} diff --git a/src/providers/migrations/0008_verb_remove_providerschema_essential_verbs_and_more.py b/src/providers/migrations/0008_verb_remove_providerschema_essential_verbs_and_more.py new file mode 100644 index 0000000..6487016 --- /dev/null +++ b/src/providers/migrations/0008_verb_remove_providerschema_essential_verbs_and_more.py @@ -0,0 +1,156 @@ +import json + +from django.core.exceptions import ObjectDoesNotExist +from django.db import migrations, models +import django.db.models.deletion + +groups_map = {} + +def migrate_schema_to_objects(apps, schema_editor): + ProviderSchema = apps.get_model('providers', 'ProviderSchema') + Verb = apps.get_model('providers', 'Verb') + VerbObject = apps.get_model('providers', 'VerbObject') + ProviderVerbGroup = apps.get_model('providers', 'ProviderVerbGroup') + + for schema in ProviderSchema.objects.all(): + groups_map[schema.id] = [] + groups_data = schema.groups + essential_verbs = schema.essential_verbs + for group_data in groups_data: + group = ProviderVerbGroup.objects.create( + provider=schema.provider, + provider_schema=schema, + group_id = group_data["id"], + label = group_data["label"], + description = group_data["description"], + purpose_of_collection = group_data["purposeOfCollection"], + requires_consent = True, + ) + groups_map[schema.id].append(group.id) + for verb_data in group_data["verbs"]: + try: + verb = Verb.objects.get( + provider=schema.provider, + provider_schema=schema, + verb_id=verb_data["id"], + ) + except ObjectDoesNotExist: + verb = Verb.objects.create( + provider=schema.provider, + provider_schema = schema, + verb_id = verb_data["id"], + label = verb_data["label"], + description = verb_data["description"], + default_consent = verb_data["defaultConsent"], + active = schema.superseded_by is None, + essential = False, + allow_anonymized_collection = verb_data["allowAnonymizedCollection"] + if "allowAnonymizedCollection" in verb_data.keys() else False, + ) + for verb_object_data in verb_data["objects"]: + verb_object = VerbObject.objects.create( + verb=verb, + object_id=verb_object_data["id"], + label=verb_object_data["label"] if "label" in verb_object_data.keys() else "", + object_type=verb_object_data["objectType"] if "objectType" in verb_object_data.keys() else "Activity", + matching=verb_object_data["matching"] if "matching" in verb_object_data.keys() else "definitionType", + definition=verb_object_data["definition"] + ) + group.verbs.add(verb) + for verb_data in essential_verbs: + try: + verb = Verb.objects.get( + provider=schema.provider, + provider_schema=schema, + verb_id=verb_data["id"], + ) + verb.essential = True + verb.save() + except ObjectDoesNotExist: + verb = Verb.objects.create( + provider=schema.provider, + provider_schema=schema, + verb_id=verb_data["id"], + label=verb_data["label"], + description=verb_data["description"], + default_consent=verb_data["defaultConsent"], + active=schema.superseded_by is None, + essential=True, + allow_anonymized_collection=verb_data["allowAnonymizedCollection"] + if "allowAnonymizedCollection" in verb_data.keys() else False, + ) + for verb_object_data in verb_data["objects"]: + verb_object = VerbObject.objects.create( + verb=verb, + object_id=verb_object_data["id"], + label=verb_object_data["label"] if "label" in verb_object_data.keys() else "", + object_type=verb_object_data["objectType"] if "objectType" in verb_object_data.keys() else "Activity", + matching=verb_object_data["matching"] if "matching" in verb_object_data.keys() else "definitionType", + definition=json.dumps(verb_object_data["definition"]) + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('providers', '0007_providerschema_additional_lrs'), + ] + + operations = [ + migrations.CreateModel( + name='Verb', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('verb_id', models.CharField(max_length=250)), + ('label', models.TextField()), + ('description', models.TextField()), + ('default_consent', models.BooleanField()), + ('active', models.BooleanField(default=True)), + ('essential', models.BooleanField(default=False)), + ('allow_anonymized_collection', models.BooleanField(default=False)), + ('provider', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='providers.provider')), + ], + ), + migrations.CreateModel( + name='VerbObject', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.CharField(max_length=250)), + ('object_type', models.CharField(max_length=250)), + ('matching', models.CharField(max_length=250)), + ('label', models.CharField(max_length=250)), + ('definition', models.JSONField()), + ('verb', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='providers.verb')), + ], + ), + migrations.AddField( + model_name='verb', + name='provider_schema', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='providers.providerschema'), + ), + migrations.CreateModel( + name='ProviderVerbGroup', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('group_id', models.CharField(max_length=250)), + ('label', models.TextField()), + ('description', models.TextField()), + ('purpose_of_collection', models.TextField()), + ('requires_consent', models.BooleanField(default=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('created', models.DateTimeField(auto_now_add=True)), + ('provider', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='providers.provider')), + ('provider_schema', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='providers.providerschema')), + ('verbs', models.ManyToManyField(to='providers.verb')), + ], + ), + migrations.RunPython(migrate_schema_to_objects, reverse_code=migrations.RunPython.noop), + migrations.RemoveField( + model_name='providerschema', + name='essential_verbs', + ), + migrations.RemoveField( + model_name='providerschema', + name='groups', + ), + ] diff --git a/src/providers/models.py b/src/providers/models.py index e344a2b..ab80e24 100644 --- a/src/providers/models.py +++ b/src/providers/models.py @@ -1,5 +1,3 @@ -from email.policy import default - from django.conf import settings from django.db import models @@ -20,14 +18,63 @@ class Provider(models.Model): class ProviderSchema(models.Model): provider = models.ForeignKey(Provider, on_delete=models.CASCADE) superseded_by = models.ForeignKey("self", null=True, on_delete=models.SET_NULL) - groups = models.JSONField() - essential_verbs = models.JSONField() additional_lrs = models.JSONField(default=list) updated = models.DateTimeField(auto_now=True) created = models.DateTimeField(auto_now_add=True) + def groups(self): + return ProviderVerbGroup.objects.filter(provider_schema=self).all() + + def verbs(self): + return Verb.objects.filter(essential=False, provider_schema=self).all() + + def essential_verbs(self): + return Verb.objects.filter(essential=True, provider_schema=self).all() + + def __str__(self): + return f"Provider Schema {self.id}: ({len(self.verbs())} verbs, {len(self.essential_verbs())} essential verbs, {len(self.groups())} groups)" + +class Verb(models.Model): + provider = models.ForeignKey(Provider, on_delete=models.CASCADE) + provider_schema = models.ForeignKey(ProviderSchema, on_delete=models.CASCADE) + verb_id = models.CharField(max_length=250) + label = models.TextField() + description = models.TextField() + default_consent = models.BooleanField() + active = models.BooleanField(default=True) + essential = models.BooleanField(default=False) + allow_anonymized_collection = models.BooleanField(default=False) + def __str__(self): - return "Provider Schema." + return self.verb_id + +class VerbObject(models.Model): + verb = models.ForeignKey(Verb, on_delete=models.CASCADE) + object_id = models.CharField(max_length=250) + object_type = models.CharField(max_length=250) + matching = models.CharField(max_length=250) + label = models.CharField(max_length=250) + definition = models.JSONField() + + +class ProviderVerbGroup(models.Model): + provider = models.ForeignKey(Provider, on_delete=models.CASCADE) + provider_schema = models.ForeignKey(ProviderSchema, on_delete=models.CASCADE) + group_id = models.CharField(max_length=250) + label = models.TextField() + description = models.TextField() + purpose_of_collection = models.TextField() + requires_consent = models.BooleanField(default=True) + updated = models.DateTimeField(auto_now=True) + created = models.DateTimeField(auto_now_add=True) + verbs = models.ManyToManyField(Verb) + + def __str__(self): + return (f"Provider Verb Group {self.id}:\n" + f"- contains {len(self.verbs.all())} verbs\n" + f"- group ID: {self.group_id}\n" + f"- provider Schema ID: {self.provider_schema_id}\n") + class ProviderAuthorization(models.Model): diff --git a/src/providers/serializers.py b/src/providers/serializers.py index c1fb7fd..852d1cf 100644 --- a/src/providers/serializers.py +++ b/src/providers/serializers.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta from rest_framework import serializers -from providers.models import AnalyticsToken, AnalyticsTokenVerb, Provider, ProviderSchema +from providers.models import AnalyticsToken, AnalyticsTokenVerb, Provider, ProviderSchema, ProviderVerbGroup from users.models import CustomUser @@ -14,10 +14,10 @@ class ProviderSerializer(serializers.ModelSerializer): class ConsentVerbSerializer(serializers.Serializer): - provider = serializers.PrimaryKeyRelatedField( - queryset=Provider.objects.all() - ) id = serializers.CharField() + group_id = serializers.PrimaryKeyRelatedField( + queryset=ProviderVerbGroup.objects.all() + ) consented = serializers.BooleanField() objects = serializers.CharField() diff --git a/src/providers/views.py b/src/providers/views.py index 1accfb7..f02f694 100644 --- a/src/providers/views.py +++ b/src/providers/views.py @@ -22,7 +22,7 @@ from rest_framework.views import APIView from rolepermissions.checkers import has_permission from rolepermissions.roles import get_user_roles -from backend.role_permission import IsAnalyst, IsProviderManager +from backend.role_permission import IsAnalyst, IsProviderManager, IsProvider from backend.roles import Roles from backend.utils import lrs_db from consents.views import JsonUploadParser @@ -94,19 +94,19 @@ class CreateProviderSchemaView(APIView): return JsonResponse( {"message": "invalid form data"}, status=status.HTTP_400_BAD_REQUEST ) - + provider = Provider.objects.get(provider_id) try: precedingSchema = ProviderSchema.objects.get(superseded_by__isnull=True) newSchema = ProviderSchema.objects.create( - object=object, provider=Provider.objects.get(provider_id) + object=object, provider=provider ) precedingSchema.superseded_by = newSchema precedingSchema.save() except ObjectDoesNotExist: newSchema = ProviderSchema.objects.create( - object=object, provider=Provider.objects.get(provider_id) + object=object, provider=provider ) return JsonResponse( @@ -170,17 +170,17 @@ class GetAnalyticsTokensAvailableVerbsList(APIView): def createAnalyticTokenVerbs(groups, essential_verbs, provider): verbs = [] for group in groups: - for verb in group["verbs"]: - verbs.append({"id" : verb["id"], "label" : verb["label"], "description" : verb["description"], "provider" : provider.id}) + for verb in group.verbs.all(): + verbs.append({"id" : verb.verb_id, "label" : verb.label, "description" : verb.description, "provider" : provider.id}) for verb in essential_verbs: # append essential verbs which might not be included in any groups - if len([verb_ for verb_ in verbs if verb_["id"] == verb["id"]]) == 0: - verbs.append({"id" : verb["id"], "label" : verb["label"], "description" : verb["description"], "provider" : provider.id}) + if len([verb_ for verb_ in verbs if verb_["id"] == verb.verb_id]) == 0: + verbs.append({"id" : verb.verb_id, "label" : verb.label, "description" : verb.description, "provider" : provider.id}) return verbs def prepareSchema(schema): return { "providerId" : schema.provider.id, "label" : schema.provider.name, - "analyticTokenVerbs" : createAnalyticTokenVerbs(schema.groups, schema.essential_verbs, schema.provider) + "analyticTokenVerbs" : createAnalyticTokenVerbs(schema.groups(), schema.essential_verbs(), schema.provider) } schemas = ProviderSchema.objects.filter(superseded_by=None) s = list(map(prepareSchema , schemas)) @@ -261,8 +261,9 @@ class UpdateAnalyticsTokenImage(APIView): token = AnalyticsToken.objects.filter(id=token_id).first() if not token: return Response({"message": "token doesn't exist"}, status=status.HTTP_404_NOT_FOUND) - - #TODO delete token image + os.remove("static/upload/analytic-tokens-images/"+token.image_path) + token.image_path = None + token.save() return Response({"message": "token image deleted"}, status=status.HTTP_200_OK) def post(self, request, token_id): @@ -777,7 +778,7 @@ class CreateVisualizationToken(APIView): provider = ProviderAuthorization.objects.filter(key=application_token).first() if provider is None: - print("Invald access token: " + application_token) + print("Invalid access token: " + application_token) return JsonResponse( {"message": "invalid access token"}, safe=False, diff --git a/src/static/provider_schema.schema.json b/src/static/provider_schema.schema.json index 958cae3..b2509dd 100644 --- a/src/static/provider_schema.schema.json +++ b/src/static/provider_schema.schema.json @@ -11,7 +11,7 @@ "description": { "type": "string" }, - "groups": { + "verbs": { "type": "array", "items": [ { @@ -26,84 +26,15 @@ "description": { "type": "string" }, - "showVerbDetails": { + "defaultConsent": { "type": "boolean" }, - "purposeOfCollection": { - "type": "string" - }, - "verbs": { + "objects": { "type": "array", - "items": [ - { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "label": { - "type": "string" - }, - "description": { - "type": "string" - }, - "defaultConsent": { - "type": "boolean" - }, - "allowAnonymizedCollection": { - "type": "boolean" - }, - "allowAnonymizedCollectionMinCount": { - "type": "integer" - }, - "objects": { - "type": "array", - "items": [ - { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "label": { - "type": "string" - }, - "defaultConsent": { - "type": "boolean" - }, - "matching": { - "enum": ["definitionType", "id"] - }, - "definition": { - "type": "object", - "properties": { - "name": { - "type": "object", - "properties": { - "enUS": { - "type": "string" - } - }, - "required": ["enUS"] - } - }, - "required": ["name"] - } - }, - "required": ["id", "label", "defaultConsent", "definition", "matching"] - } - ] - } - }, - "required": ["id", "label", "description", "defaultConsent", "objects"] - } - ] - }, - "isDefault": { - "type": "boolean" + "items": {} } }, - "required": ["id", "label", "description", "showVerbDetails", "purposeOfCollection", "verbs", "isDefault"] + "required": ["id", "label", "description", "defaultConsent", "objects"] } ] }, @@ -160,5 +91,5 @@ ] } }, - "required": ["id", "name", "description", "groups", "essentialVerbs"] + "required": ["id", "name", "description", "verbs", "essentialVerbs"] } diff --git a/src/static/provider_schema_h5p_v1.example.json b/src/static/provider_schema_h5p_v1.example.json index 2715878..aa94f1b 100644 --- a/src/static/provider_schema_h5p_v1.example.json +++ b/src/static/provider_schema_h5p_v1.example.json @@ -2,106 +2,86 @@ "id": "h5p-0", "name": "H5P", "description": "Open-source content collaboration framework", - "groups": [ + "verbs": [ { - "id": "default_group", - "label": "Default group", - "description": "default", - "showVerbDetails": true, - "purposeOfCollection": "Lorem Ipsum", - "verbs": [ + "id": "http://h5p.example.com/expapi/verbs/experienced", + "label": "Experienced", + "description": "Experienced", + "defaultConsent": true, + "objects": [ { - "id": "http://h5p.example.com/expapi/verbs/experienced", - "label": "Experienced", - "description": "Experienced", + "id": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", + "label": "1.1.1 Funktionen", "defaultConsent": true, - "objects": [ - { - "id": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", - "label": "1.1.1 Funktionen", - "defaultConsent": true, - "matching": "definitionType", - "definition": { - "type": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", - "name": { - "enUS": "1.1.1 Funktionen" - } - } + "matching": "definitionType", + "definition": { + "type": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", + "name": { + "enUS": "1.1.1 Funktionen" } - ] - }, + } + } + ] + }, + { + "id": "http://h5p.example.com/expapi/verbs/attempted", + "label": "Attempted", + "description": "Attempted", + "defaultConsent": true, + "objects": [ { - "id": "http://h5p.example.com/expapi/verbs/attempted", - "label": "Attempted", - "description": "Attempted", + "id": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", + "label": "2.3.1 Funktion Zirkulationsleitung", "defaultConsent": true, - "objects": [ - { - "id": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", - "label": "2.3.1 Funktion Zirkulationsleitung", - "defaultConsent": true, - "matching": "definitionType", - "definition": { - "type": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", - "name": { - "enUS": "2.3.1 Funktion Zirkulationsleitung" - } - } + "matching": "definitionType", + "definition": { + "type": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", + "name": { + "enUS": "2.3.1 Funktion Zirkulationsleitung" } - ] + } } - ], - "isDefault": true + ] }, { - "id": "group_2", - "label": "Group 2", + "id": "http://h5p.example.com/expapi/verbs/interacted", + "label": "Interacted", "description": "Lorem ipsum", - "showVerbDetails": true, - "purposeOfCollection": "Lorem Ipsum", - "verbs": [ + "defaultConsent": true, + "objects": [ { - "id": "http://h5p.example.com/expapi/verbs/interacted", - "label": "Interacted", - "description": "Lorem ipsum", + "id": "http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46", + "label": "1.2.3 Kappenventil", "defaultConsent": true, - "objects": [ - { - "id": "http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46", - "label": "1.2.3 Kappenventil", - "defaultConsent": true, - "matching": "definitionType", - "definition": { - "type": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", - "name": { - "enUS": "1.2.3 Kappenventil" - } - } + "matching": "definitionType", + "definition": { + "type": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", + "name": { + "enUS": "1.2.3 Kappenventil" } - ] - }, + } + } + ] + }, + { + "id": "http://h5p.example.com/expapi/verbs/answered", + "label": "Answered", + "description": "lorem ipsum", + "defaultConsent": false, + "objects": [ { - "id": "http://h5p.example.com/expapi/verbs/answered", - "label": "Answered", - "description": "lorem ipsum", + "id": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", + "label": "7.2.1 Ventil Basics", "defaultConsent": false, - "objects": [ - { - "id": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", - "label": "7.2.1 Ventil Basics", - "defaultConsent": false, - "matching": "definitionType", - "definition": { - "type": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", - "name": { - "enUS": "7.2.1 Ventil Basics" - } - } + "matching": "definitionType", + "definition": { + "type": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", + "name": { + "enUS": "7.2.1 Ventil Basics" } - ] + } } - ], - "isDefault": false + ] } ], "essentialVerbs": [ diff --git a/src/static/provider_schema_moodle_v1.example.json b/src/static/provider_schema_moodle_v1.example.json index 10eaf76..4773e2e 100644 --- a/src/static/provider_schema_moodle_v1.example.json +++ b/src/static/provider_schema_moodle_v1.example.json @@ -2,93 +2,83 @@ "id": "moodle-0", "name": "Moodle", "description": "Open-source learning management system", - "groups": [ + "verbs": [ { - "id": "default_group", - "label": "Default group", - "description": "default", - "showVerbDetails": true, - "purposeOfCollection": "Lorem Ipsum", - "verbs": [ + "id": "http://moodle.example.com/expapi/verbs/completed", + "label": "Completed", + "description": "Completed", + "defaultConsent": true, + "objects": [ { - "id": "http://moodle.example.com/expapi/verbs/completed", - "label": "Completed", - "description": "Completed", + "id": "http://moodle.example.com/expapi/activity/programming-course-python", + "label": "Python Programming Course", "defaultConsent": true, - "objects": [ - { - "id": "http://moodle.example.com/expapi/activity/programming-course-python", - "label": "Python Programming Course", - "defaultConsent": true, - "matching": "definitionType", - "definition": { - "type": "http://moodle.example.com/expapi/activity/programming-course-python", - "name": { - "enUS": "Python Programming Course" - } - } - }, - { - "id": "http://moodle.example.com/expapi/activity/foreign-language-course", - "label": "Foreign language course", - "defaultConsent": true, - "matching": "definitionType", - "definition": { - "type": "http://moodle.example.com/expapi/activity/foreign-language-course", - "name": { - "enUS": "Foreign language course" - } - } - } - ] - }, - { - "id": "http://moodle.example.com/expapi/verbs/started", - "label": "Started", - "description": "Started", - "defaultConsent": false, - "matching": "definitionType", - "objects": [] - }, - { - "id": "http://moodle.example.com/expapi/verbs/paused", - "label": "Paused", - "description": "paused", - "defaultConsent": false, "matching": "definitionType", - "objects": [] + "definition": { + "type": "http://moodle.example.com/expapi/activity/programming-course-python", + "name": { + "enUS": "Python Programming Course" + } + } }, { - "id": "http://moodle.example.com/expapi/verbs/created", - "label": "Created", - "description": "Created", - "defaultConsent": false, + "id": "http://moodle.example.com/expapi/activity/foreign-language-course", + "label": "Foreign language course", + "defaultConsent": true, "matching": "definitionType", - "objects": [] - }, + "definition": { + "type": "http://moodle.example.com/expapi/activity/foreign-language-course", + "name": { + "enUS": "Foreign language course" + } + } + } + ] + }, + { + "id": "http://moodle.example.com/expapi/verbs/started", + "label": "Started", + "description": "Started", + "defaultConsent": false, + "matching": "definitionType", + "objects": [] + }, + { + "id": "http://moodle.example.com/expapi/verbs/paused", + "label": "Paused", + "description": "paused", + "defaultConsent": false, + "matching": "definitionType", + "objects": [] + }, + { + "id": "http://moodle.example.com/expapi/verbs/created", + "label": "Created", + "description": "Created", + "defaultConsent": false, + "matching": "definitionType", + "objects": [] + }, + { + "id": "http://moodle.example.com/expapi/verbs/answered", + "label": "Answered", + "description": "Answered", + "defaultConsent": true, + "matching": "definitionType", + "objects": [ { - "id": "http://moodle.example.com/expapi/verbs/answered", - "label": "Answered", - "description": "Answered", + "id": "http://moodle.example.com/expapi/activity/poll-preferred-course-level", + "label": "Poll: Preferred course level", "defaultConsent": true, "matching": "definitionType", - "objects": [ - { - "id": "http://moodle.example.com/expapi/activity/poll-preferred-course-level", - "label": "Poll: Preferred course level", - "defaultConsent": true, - "matching": "definitionType", - "definition": { - "type": "http://moodle.example.com/expapi/activity/poll-preferred-course-level", - "name": { - "enUS": "Poll: Preferred course level" - } - } + "definition": { + "type": "http://moodle.example.com/expapi/activity/poll-preferred-course-level", + "name": { + "enUS": "Poll: Preferred course level" } - ] + } } - ], - "isDefault": true + ] } ], "essentialVerbs": [] diff --git a/src/users/models.py b/src/users/models.py index fb7e41f..6f52c18 100644 --- a/src/users/models.py +++ b/src/users/models.py @@ -23,4 +23,4 @@ class CustomUser(AbstractUser): objects = CustomUserManager() def __str__(self): - return "Custom user {}".format(self.email) + return f"Custom user {self.id}: {self.email}" diff --git a/src/users/serializers.py b/src/users/serializers.py index 199e368..a5b5952 100644 --- a/src/users/serializers.py +++ b/src/users/serializers.py @@ -1,8 +1,23 @@ +from django.contrib.auth.models import Group, Permission from rest_framework import serializers -from models import CustomUser +from .models import CustomUser +class PermissionSerializer(serializers.ModelSerializer): + class Meta: + model = Permission + fields = ('id', 'name', 'codename') + +class GroupSerializer(serializers.ModelSerializer): + # Nest the PermissionSerializer to include permission details + permissions = PermissionSerializer(many=True) + + class Meta: + model = Group + fields = ('id', 'name', 'permissions') class UserSerializer(serializers.ModelSerializer): + groups = GroupSerializer(many=True) + user_permissions = PermissionSerializer(many=True) class Meta: model = CustomUser - fields = "__all__" + exclude = ["password", "shibboleth_connector_identifier"] diff --git a/src/users/urls.py b/src/users/urls.py index 2fee424..740129c 100644 --- a/src/users/urls.py +++ b/src/users/urls.py @@ -12,4 +12,7 @@ urlpatterns = [ path("toggle-data-recording", views.ToggleDataRecording.as_view()), path("deleteUser", views.DeleteUserView.as_view()), path("mergeData", views.MergeDataView.as_view()), -] + path("users", views.UserListView.as_view()), + path("groups", views.GroupListView.as_view()), + path("permissions", views.PermissionListView.as_view()), +] \ No newline at end of file diff --git a/src/users/views.py b/src/users/views.py index f868efb..58f8e7a 100644 --- a/src/users/views.py +++ b/src/users/views.py @@ -5,8 +5,9 @@ from django.shortcuts import render from django.template.loader import render_to_string from django.utils import timezone from django.utils.html import strip_tags +from django.contrib.auth.models import Group, Permission, Permission from rest_framework import status -from rest_framework.permissions import IsAuthenticated +from rest_framework.permissions import IsAuthenticated, IsAdminUser from rest_framework.views import APIView from rolepermissions.checkers import has_permission from rolepermissions.roles import assign_role @@ -17,6 +18,7 @@ from consents.views import JsonUploadParser from data_removal.models import DataRemovalJob from .models import CustomUser +from .serializers import UserSerializer, GroupSerializer, PermissionSerializer class CreateUserView(APIView): @@ -152,3 +154,38 @@ class MergeDataView(APIView): return JsonResponse({"status": "success", "message": f'{result.modified_count} records updated'}, status=status.HTTP_200_OK) else: return JsonResponse({"status": "error", "message": f'0 records updated'}, status=status.HTTP_400_BAD_REQUEST) + + +class UserListView(APIView): + permission_classes = (IsAuthenticated, IsAdminUser) + + def get(self, request): + users = CustomUser.objects.all() + users = UserSerializer(users, many=True).data + return JsonResponse(users, + safe=False, + status=status.HTTP_200_OK) + + +class GroupListView(APIView): + """ + API endpoint that returns all auth groups with their permissions. + """ + def get(self, request, format=None): + groups = Group.objects.all() + serializer = GroupSerializer(groups, many=True) + return JsonResponse(serializer.data, + safe=False, + status=status.HTTP_200_OK) + + +class PermissionListView(APIView): + """ + API endpoint that returns all auth permissions. + """ + def get(self, request, format=None): + groups = Permission.objects.all() + serializer = PermissionSerializer(groups, many=True) + return JsonResponse(serializer.data, + safe=False, + status=status.HTTP_200_OK) \ No newline at end of file diff --git a/src/xapi/models.py b/src/xapi/models.py index 71a8362..beeb308 100644 --- a/src/xapi/models.py +++ b/src/xapi/models.py @@ -1,3 +1,2 @@ from django.db import models -# Create your models here. diff --git a/src/xapi/tests/tests.py b/src/xapi/tests/tests.py index c056eb8..17b4141 100644 --- a/src/xapi/tests/tests.py +++ b/src/xapi/tests/tests.py @@ -13,7 +13,7 @@ from rolepermissions.roles import assign_role from consents.models import UserConsents from consents.tests.tests_consent_operations import BaseTestCase -from providers.models import Provider, ProviderAuthorization, ProviderSchema +from providers.models import Provider, ProviderAuthorization, ProviderSchema, ProviderVerbGroup from users.models import CustomUser PROJECT_PATH = os.path.abspath(os.path.dirname(__name__)) @@ -32,6 +32,34 @@ class XAPITestCase(TestCase): test_provider_email = "test2@mail.com" test_provider_password = "test123" + verb_groups = [ + { + "provider_id": 1, + "id": "default_group", + "label": "Default group", + "description": "default", + "showVerbDetails": True, + "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, + "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/experienced"} + ] + }, + { + "provider_id": 1, + "id": "default_group", + "label": "Group 2", + "description": "Lorem ipsum", + "showVerbDetails": True, + "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, + "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/attempted"}, + {"id": "http://h5p.example.com/expapi/verbs/interacted"}, + ] + } + ] + def setUp(self): self.normal_user = normal_user = CustomUser.objects.create_user( self.test_user_email, self.test_user_password @@ -57,7 +85,7 @@ class XAPITestCase(TestCase): provider_client = APIClient() provider_client.credentials(HTTP_AUTHORIZATION="Bearer " + provider_token) - # we need provider schemas for this test + # we need provider schemas with groups for this test with open( os.path.join(PROJECT_PATH, "static/provider_schema_h5p_v1.example.json") ) as fp: @@ -69,10 +97,20 @@ class XAPITestCase(TestCase): self.assertEqual(response.status_code, 201) self.assertTrue("message" in response.json()) + # create some verb groups + for group in self.verb_groups: + group["provider_id"] = Provider.objects.latest('id').id # database id is 2 on second go bc autoincrement + response = provider_client.post( + "/api/v1/consents/provider/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + @patch("xapi.views.store_in_db", mock_store_in_lrs) def test_xapi(self): try: - provider = Provider.objects.order_by("id").first() + provider = Provider.objects.latest('id') except ObjectDoesNotExist: self.assertTrue(False) # provider was not created when uploading schema @@ -92,10 +130,12 @@ class XAPITestCase(TestCase): first = True accepted_verb = {} denied_verb = {} - for group in provider_schema.groups: - for verb in group["verbs"]: + for group in provider_schema.groups(): + for verb in group.verbs.all(): + objects = [{"id": obj.object_id, "objectType": obj.object_type, "matching": obj.matching, + "label": obj.label, "definition": obj.definition} for obj in verb.verbobject_set.all()] first_obj = True - for obj in verb["objects"]: # consent to the first object + for obj in objects: # consent to the first object obj["consented"] = first_obj first_obj = False # consent to the very first verb @@ -103,9 +143,9 @@ class XAPITestCase(TestCase): accepted_verb = verb UserConsents.objects.create( consented=True, - verb=verb["id"], - object=json.dumps(verb["objects"]), - provider_schema=provider_schema, + verb=verb, + object=json.dumps(objects), + verb_group=group, provider=provider, user=self.normal_user, ) @@ -114,9 +154,9 @@ class XAPITestCase(TestCase): denied_verb = verb UserConsents.objects.create( consented=False, - verb=verb["id"], - object=json.dumps(verb["objects"]), - provider_schema=provider_schema, + verb=verb, + object=json.dumps(objects), + verb_group=group, provider=provider, user=self.normal_user, ) @@ -128,11 +168,11 @@ class XAPITestCase(TestCase): "/xapi/statements", { "actor": {"mbox": f"mailto:{self.test_user_email}"}, - "verb": {"id": accepted_verb["id"]}, + "verb": {"id": accepted_verb.verb_id}, "object": { - "id": accepted_verb["objects"][0]["id"], + "id": accepted_verb.verbobject_set.first().object_id, "objectType": "Activity", - "definition": accepted_verb["objects"][0]["definition"], + "definition": accepted_verb.verbobject_set.first().definition, }, "timestamp": (datetime.now() + timedelta(0,30)).strftime("%Y-%m-%dT%H:%M:%SZ"), # timedelta is used to ensure the statement is "after" the consent }, @@ -234,6 +274,23 @@ class TestxAPIWithDataRecordingPause(BaseTestCase): ) self.assertEqual(response.status_code, 201) + # create a verb group + group = {"provider_id": Provider.objects.latest('id').id, "id": "default_group", "label": "Group 2", + "description": "Lorem ipsum", "showVerbDetails": True, "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/experienced"}, + {"id": "http://h5p.example.com/expapi/verbs/attempted"}, + {"id": "http://h5p.example.com/expapi/verbs/interacted"}, + {"id": "http://h5p.example.com/expapi/verbs/answered"} + ]} + response = self.provider_client.post( + "/api/v1/consents/provider/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + group_id = ProviderVerbGroup.objects.latest('id').id + # Create user consent for test user user_consent = [ { @@ -241,25 +298,25 @@ class TestxAPIWithDataRecordingPause(BaseTestCase): "providerSchemaId": 1, "verbs": [ { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/experienced", "consented": False, "objects": '[{"id":"http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF","label":"1.1.1 Funktionen","defaultConsent":true, "matching": "definitionType" ,"definition":{"type": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", "name":{"enUS":"1.1.1 Funktionen"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/attempted", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm","label":"2.3.1 Funktion Zirkulationsleitung","defaultConsent":true, "matching": "definitionType", "definition":{"type": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", "name":{"enUS":"2.3.1 Funktion Zirkulationsleitung"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/interacted", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46","label":"1.2.3 Kappenventil","defaultConsent":true, "matching": "definitionType", "definition":{"type": "http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46", "name":{"enUS":"1.2.3 Kappenventil"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/answered", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b","label":"7.2.1 Ventil Basics","defaultConsent":false, "matching": "definitionType", "definition":{"type": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", "name":{"enUS":"7.2.1 Ventil Basics"}},"consented":true}]', @@ -353,6 +410,23 @@ class TestxAPIStatementActorAccount(BaseTestCase): ) self.assertEqual(response.status_code, 201) + # create a verb group + group = {"provider_id": Provider.objects.latest('id').id, "id": "default_group", "label": "Group 2", + "description": "Lorem ipsum", "showVerbDetails": True, "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/experienced"}, + {"id": "http://h5p.example.com/expapi/verbs/attempted"}, + {"id": "http://h5p.example.com/expapi/verbs/interacted"}, + {"id": "http://h5p.example.com/expapi/verbs/answered"} + ]} + response = self.provider_client.post( + "/api/v1/consents/provider/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + group_id = ProviderVerbGroup.objects.latest('id').id + # Create user consent for test user user_consent = [ { @@ -360,25 +434,25 @@ class TestxAPIStatementActorAccount(BaseTestCase): "providerSchemaId": 1, "verbs": [ { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/experienced", "consented": False, "objects": '[{"id":"http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF","label":"1.1.1 Funktionen","defaultConsent":true, "matching": "definitionType" ,"definition":{"type": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", "name":{"enUS":"1.1.1 Funktionen"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/attempted", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm","label":"2.3.1 Funktion Zirkulationsleitung","defaultConsent":true, "matching": "definitionType", "definition":{"type": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", "name":{"enUS":"2.3.1 Funktion Zirkulationsleitung"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/interacted", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46","label":"1.2.3 Kappenventil","defaultConsent":true, "matching": "definitionType", "definition":{"type": "http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46", "name":{"enUS":"1.2.3 Kappenventil"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/answered", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b","label":"7.2.1 Ventil Basics","defaultConsent":false, "matching": "definitionType", "definition":{"type": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", "name":{"enUS":"7.2.1 Ventil Basics"}},"consented":true}]', @@ -438,6 +512,23 @@ class TestxAPIStatementActorAccount(BaseTestCase): ) self.assertEqual(response.status_code, 201) + # create a verb group + group = {"provider_id": Provider.objects.latest('id').id, "id": "default_group", "label": "Group 2", + "description": "Lorem ipsum", "showVerbDetails": True, "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/experienced"}, + {"id": "http://h5p.example.com/expapi/verbs/attempted"}, + {"id": "http://h5p.example.com/expapi/verbs/interacted"}, + {"id": "http://h5p.example.com/expapi/verbs/answered"} + ]} + response = self.provider_client.post( + "/api/v1/consents/provider/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + group_id = ProviderVerbGroup.objects.latest('id').id + # Create user consent for test user user_consent = [ { @@ -445,25 +536,25 @@ class TestxAPIStatementActorAccount(BaseTestCase): "providerSchemaId": 1, "verbs": [ { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/experienced", "consented": False, "objects": '[{"id":"http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF","label":"1.1.1 Funktionen","defaultConsent":true, "matching": "definitionType", "definition":{"type": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", "name":{"enUS":"1.1.1 Funktionen"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/attempted", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm","label":"2.3.1 Funktion Zirkulationsleitung","defaultConsent":true, "matching": "definitionType", "definition":{"type": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", "name":{"enUS":"2.3.1 Funktion Zirkulationsleitung"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/interacted", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46","label":"1.2.3 Kappenventil","defaultConsent":true, "matching": "definitionType", "definition":{"type": "http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46", "name":{"enUS":"1.2.3 Kappenventil"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/answered", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b","label":"7.2.1 Ventil Basics","defaultConsent":false, "matching": "definitionType" ,"definition":{"type": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", "name":{"enUS":"7.2.1 Ventil Basics"}},"consented":true}]', @@ -520,6 +611,23 @@ class TestxAPIStatementActorAccount(BaseTestCase): ) self.assertEqual(response.status_code, 201) + # create a verb group + group = {"provider_id": Provider.objects.latest('id').id, "id": "default_group", "label": "Group 2", + "description": "Lorem ipsum", "showVerbDetails": True, "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, "verbs": [ + {"id": "http://h5p.example.com/expapi/verbs/experienced"}, + {"id": "http://h5p.example.com/expapi/verbs/attempted"}, + {"id": "http://h5p.example.com/expapi/verbs/interacted"}, + {"id": "http://h5p.example.com/expapi/verbs/answered"} + ]} + response = self.provider_client.post( + "/api/v1/consents/provider/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + group_id = ProviderVerbGroup.objects.latest('id').id + # Create user consent for test user user_consent = [ { @@ -527,25 +635,25 @@ class TestxAPIStatementActorAccount(BaseTestCase): "providerSchemaId": 1, "verbs": [ { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/experienced", "consented": False, "objects": '[{"id":"http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF","label":"1.1.1 Funktionen","defaultConsent":true, "matching": "definitionType", "definition":{"type": "http://h5p.example.com/expapi/activity/QKGPPiIhI4zx9YAZZksLKigqyf7yW4WF", "name":{"enUS":"1.1.1 Funktionen"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/attempted", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm","label":"2.3.1 Funktion Zirkulationsleitung","defaultConsent":true, "matching": "definitionType", "definition":{"type": "http://h5p.example.com/expapi/activity/VeH7S8NeGlCRM1myYRDBjHMCknLqDLgm", "name":{"enUS":"2.3.1 Funktion Zirkulationsleitung"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/interacted", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46","label":"1.2.3 Kappenventil","defaultConsent":true, "matching": "definitionType", "definition":{"type": "http://h5p.example.com/expapi/activity/ofN2ODcnLRaVu30lUpzrWPqF2AcG7g46", "name":{"enUS":"1.2.3 Kappenventil"}},"consented":true}]', }, { - "provider": 1, + "group_id": group_id, "id": "http://h5p.example.com/expapi/verbs/answered", "consented": True, "objects": '[{"id":"http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b","label":"7.2.1 Ventil Basics","defaultConsent":false, "matching": "definitionType" ,"definition":{"type": "http://h5p.example.com/expapi/activity/K34IszYvGE4R0cC72Ean6msLfLCJtQ8b", "name":{"enUS":"7.2.1 Ventil Basics"}},"consented":true}]', @@ -617,37 +725,27 @@ class TestxAPIObjectMatchingDefinitionType(BaseTestCase): "id": "h5p-0", "name": "H5P", "description": "Open-source content collaboration framework", - "groups": [ + "verbs": [ { - "id": "default_group", - "label": "Default group", - "description": "default", - "showVerbDetails": True, - "purposeOfCollection": "Lorem Ipsum", - "verbs": [ + "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked", + "label": "Unlocked", + "description": "Actor unlocked an object", + "defaultConsent": True, + "objects": [ { - "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked", - "label": "Unlocked", - "description": "Actor unlocked an object", + "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/my-random-object-id", + "label": "Course", "defaultConsent": True, - "objects": [ - { - "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/my-random-object-id", - "label": "Course", - "defaultConsent": True, - "matching": "definitionType", - "definition": { - "type": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/course", - "name": { - "enUS": "A course within an LMS. Contains learning materials and activities" - }, - }, + "matching": "definitionType", + "definition": { + "type": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/course", + "name": { + "enUS": "A course within an LMS. Contains learning materials and activities" }, - ], - } + }, + }, ], - "isDefault": True, - }, + } ], "essentialVerbs": [], } @@ -666,6 +764,20 @@ class TestxAPIObjectMatchingDefinitionType(BaseTestCase): ) self.assertEqual(response.status_code, 201) + # create a verb group + group = {"provider_id": Provider.objects.latest('id').id, "id": "default_group", "label": "Group 2", + "description": "Lorem ipsum", "showVerbDetails": True, "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, "verbs": [ + {"id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked"}, + ]} + response = self.provider_client.post( + "/api/v1/consents/provider/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + group_id = ProviderVerbGroup.objects.latest('id').id + # Create user consent for test user user_consent = [ { @@ -673,7 +785,7 @@ class TestxAPIObjectMatchingDefinitionType(BaseTestCase): "providerSchemaId": 1, "verbs": [ { - "provider": 1, + "group_id": group_id, "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked", "consented": True, "objects": json.dumps( @@ -759,37 +871,27 @@ class TestxAPIObjectMatchingDefinitiondId(BaseTestCase): "id": "h5p-0", "name": "H5P", "description": "Open-source content collaboration framework", - "groups": [ + "verbs": [ { - "id": "default_group", - "label": "Default group", - "description": "default", - "showVerbDetails": True, - "purposeOfCollection": "Lorem Ipsum", - "verbs": [ + "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked", + "label": "Unlocked", + "description": "Actor unlocked an object", + "defaultConsent": True, + "objects": [ { - "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked", - "label": "Unlocked", - "description": "Actor unlocked an object", + "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/my-random-object-id", + "label": "Course", "defaultConsent": True, - "objects": [ - { - "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/my-random-object-id", - "label": "Course", - "defaultConsent": True, - "matching": "id", - "definition": { - "type": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/course", - "name": { - "enUS": "A course within an LMS. Contains learning materials and activities" - }, - }, + "matching": "id", + "definition": { + "type": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/course", + "name": { + "enUS": "A course within an LMS. Contains learning materials and activities" }, - ], - } + }, + }, ], - "isDefault": True, - }, + } ], "essentialVerbs": [], } @@ -808,6 +910,20 @@ class TestxAPIObjectMatchingDefinitiondId(BaseTestCase): ) self.assertEqual(response.status_code, 201) + # create a verb group + group = {"provider_id": Provider.objects.latest('id').id, "id": "default_group", "label": "Group 2", + "description": "Lorem ipsum", "showVerbDetails": True, "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, "verbs": [ + {"id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked"}, + ]} + response = self.provider_client.post( + "/api/v1/consents/provider/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + group_id = ProviderVerbGroup.objects.latest('id').id + # Create user consent for test user user_consent = [ { @@ -815,7 +931,7 @@ class TestxAPIObjectMatchingDefinitiondId(BaseTestCase): "providerSchemaId": 1, "verbs": [ { - "provider": 1, + "group_id": group_id, "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked", "consented": True, "objects": json.dumps( @@ -902,37 +1018,27 @@ class TestxAPITimestampAfterConsent(BaseTestCase): "id": "h5p-0", "name": "H5P", "description": "Open-source content collaboration framework", - "groups": [ + "verbs": [ { - "id": "default_group", - "label": "Default group", - "description": "default", - "showVerbDetails": True, - "purposeOfCollection": "Lorem Ipsum", - "verbs": [ + "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked", + "label": "Unlocked", + "description": "Actor unlocked an object", + "defaultConsent": True, + "objects": [ { - "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked", - "label": "Unlocked", - "description": "Actor unlocked an object", + "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/my-random-object-id", + "label": "Course", "defaultConsent": True, - "objects": [ - { - "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/my-random-object-id", - "label": "Course", - "defaultConsent": True, - "matching": "id", - "definition": { - "type": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/course", - "name": { - "enUS": "A course within an LMS. Contains learning materials and activities" - }, - }, + "matching": "id", + "definition": { + "type": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/course", + "name": { + "enUS": "A course within an LMS. Contains learning materials and activities" }, - ], - } + }, + }, ], - "isDefault": True, - }, + } ], "essentialVerbs": [], } @@ -951,6 +1057,20 @@ class TestxAPITimestampAfterConsent(BaseTestCase): ) self.assertEqual(response.status_code, 201) + # create a verb group + group = {"provider_id": Provider.objects.latest('id').id, "id": "default_group", "label": "Group 2", + "description": "Lorem ipsum", "showVerbDetails": True, "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, "verbs": [ + {"id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked"}, + ]} + response = self.provider_client.post( + "/api/v1/consents/provider/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + group_id = ProviderVerbGroup.objects.latest('id').id + # Create user consent for test user user_consent = [ { @@ -958,7 +1078,7 @@ class TestxAPITimestampAfterConsent(BaseTestCase): "providerSchemaId": 1, "verbs": [ { - "provider": 1, + "group_id": group_id, "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked", "consented": True, "objects": json.dumps( @@ -1043,37 +1163,27 @@ class TextxAPIAdditionalLrs(BaseTestCase): "id": "h5p-0", "name": "H5P", "description": "Open-source content collaboration framework", - "groups": [ + "verbs": [ { - "id": "default_group", - "label": "Default group", - "description": "default", - "showVerbDetails": True, - "purposeOfCollection": "Lorem Ipsum", - "verbs": [ + "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked", + "label": "Unlocked", + "description": "Actor unlocked an object", + "defaultConsent": True, + "objects": [ { - "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked", - "label": "Unlocked", - "description": "Actor unlocked an object", + "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/my-random-object-id", + "label": "Course", "defaultConsent": True, - "objects": [ - { - "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/my-random-object-id", - "label": "Course", - "defaultConsent": True, - "matching": "id", - "definition": { - "type": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/course", - "name": { - "enUS": "A course within an LMS. Contains learning materials and activities" - }, - }, + "matching": "id", + "definition": { + "type": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/course", + "name": { + "enUS": "A course within an LMS. Contains learning materials and activities" }, - ], - } + }, + }, ], - "isDefault": True, - }, + } ], "essentialVerbs": [], "additionalLrs": [ @@ -1117,6 +1227,20 @@ class TextxAPIAdditionalLrs(BaseTestCase): ) self.assertEqual(response.status_code, 201) + # create a verb group + group = {"provider_id": Provider.objects.latest('id').id, "id": "default_group", "label": "Group 2", + "description": "Lorem ipsum", "showVerbDetails": True, "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, "verbs": [ + {"id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked"}, + ]} + response = self.provider_client.post( + "/api/v1/consents/provider/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + group_id = ProviderVerbGroup.objects.latest('id').id + # Create user consent for test user user_consent = [ { @@ -1124,7 +1248,7 @@ class TextxAPIAdditionalLrs(BaseTestCase): "providerSchemaId": 1, "verbs": [ { - "provider": 1, + "group_id": group_id, "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked", "consented": True, "objects": json.dumps( diff --git a/src/xapi/tests/tests_verb_id_validation.py b/src/xapi/tests/tests_verb_id_validation.py index 6994f79..57c48fe 100644 --- a/src/xapi/tests/tests_verb_id_validation.py +++ b/src/xapi/tests/tests_verb_id_validation.py @@ -11,7 +11,7 @@ from rolepermissions.roles import assign_role from consents.models import UserConsents from consents.tests.tests_consent_operations import BaseTestCase -from providers.models import Provider, ProviderAuthorization, ProviderSchema +from providers.models import Provider, ProviderAuthorization, ProviderSchema, ProviderVerbGroup from users.models import CustomUser PROJECT_PATH = os.path.abspath(os.path.dirname(__name__)) @@ -26,67 +26,27 @@ class TestxAPIVerbAndObjectValidation(BaseTestCase): "id": "h5p-0", "name": "H5P", "description": "Open-source content collaboration framework", - "groups": [ + "verbs": [ { - "id": "default_group", - "label": "Default group", - "description": "default", - "showVerbDetails": True, - "purposeOfCollection": "Lorem Ipsum", - "verbs": [ + "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked", + "label": "Unlocked", + "description": "Actor unlocked an object", + "defaultConsent": True, + "objects": [ { - "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked", - "label": "Unlocked", - "description": "Actor unlocked an object", + "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/my-random-object-id", + "label": "Course", "defaultConsent": True, - "objects": [ - { - "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/my-random-object-id", - "label": "Course", - "defaultConsent": True, - "matching": "definitionType", - "definition": { - "type": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/course", - "name": { - "enUS": "A course within an LMS. Contains learning materials and activities" - }, - }, - }, - ], - } - ], - "isDefault": True, - }, - { - "id": "group_2", - "label": "Group Two", - "description": "my second group", - "showVerbDetails": True, - "purposeOfCollection": "Lorem Ipsum", - "verbs": [ - { - "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked", - "label": "Unlocked", - "description": "Actor unlocked an object", - "defaultConsent": True, - "objects": [ - { - "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/my-random-object-id", - "label": "Course", - "defaultConsent": True, - "matching": "definitionType", - "definition": { - "type": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/course", - "name": { - "enUS": "A course within an LMS. Contains learning materials and activities" - }, - }, + "matching": "definitionType", + "definition": { + "type": "https://xapi.elearn.rwth-aachen.de/definitions/lms/activities/course", + "name": { + "enUS": "A course within an LMS. Contains learning materials and activities" }, - ], - } + }, + }, ], - "isDefault": False, - }, + } ], "essentialVerbs": [], } @@ -103,6 +63,19 @@ class TestxAPIVerbAndObjectValidation(BaseTestCase): ) self.assertEqual(response.status_code, 201) + # create a verb group + group = {"provider_id": Provider.objects.latest('id').id, "id": "default_group", "label": "Group 2", + "description": "Lorem ipsum", "showVerbDetails": True, "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, "verbs": [ + {"id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked"}, + ]} + response = self.provider_client.post( + "/api/v1/consents/provider/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + group_id = ProviderVerbGroup.objects.latest('id').id # Create user consent for test user user_consent = [ { @@ -110,7 +83,7 @@ class TestxAPIVerbAndObjectValidation(BaseTestCase): "providerSchemaId": 1, "verbs": [ { - "provider": 1, + "group_id": group_id, "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked", "consented": True, "objects": json.dumps( @@ -132,7 +105,7 @@ class TestxAPIVerbAndObjectValidation(BaseTestCase): ), }, { - "provider": 1, + "group_id": group_id, "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked", "consented": False, "objects": json.dumps( @@ -183,6 +156,20 @@ class TestxAPIVerbAndObjectValidation(BaseTestCase): ) self.assertEqual(response.status_code, 201) + # create a verb group + group = {"provider_id": Provider.objects.latest('id').id, "id": "default_group", "label": "Group 2", + "description": "Lorem ipsum", "showVerbDetails": True, "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, "verbs": [ + {"id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked"}, + ]} + response = self.provider_client.post( + "/api/v1/consents/provider/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + group_id = ProviderVerbGroup.objects.latest('id').id + # Create user consent for test user user_consent = [ { @@ -190,7 +177,7 @@ class TestxAPIVerbAndObjectValidation(BaseTestCase): "providerSchemaId": 1, "verbs": [ { - "provider": 1, + "group_id": group_id, "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked", "consented": True, "objects": json.dumps( @@ -212,7 +199,7 @@ class TestxAPIVerbAndObjectValidation(BaseTestCase): ), }, { - "provider": 1, + "group_id": group_id, "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked", "consented": True, "objects": json.dumps( @@ -263,6 +250,20 @@ class TestxAPIVerbAndObjectValidation(BaseTestCase): ) self.assertEqual(response.status_code, 201) + # create a verb group + group = {"provider_id": Provider.objects.latest('id').id, "id": "default_group", "label": "Group 2", + "description": "Lorem ipsum", "showVerbDetails": True, "purposeOfCollection": "Lorem Ipsum", + "requiresConsent": True, "verbs": [ + {"id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked"}, + ]} + response = self.provider_client.post( + "/api/v1/consents/provider/create-verb-group", + group, + format="json", + ) + self.assertEqual(response.status_code, 200) + group_id = ProviderVerbGroup.objects.latest('id').id + # Create user consent for test user user_consent = [ { @@ -270,7 +271,7 @@ class TestxAPIVerbAndObjectValidation(BaseTestCase): "providerSchemaId": 1, "verbs": [ { - "provider": 1, + "group_id": group_id, "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked", "consented": True, "objects": json.dumps( @@ -292,7 +293,7 @@ class TestxAPIVerbAndObjectValidation(BaseTestCase): ), }, { - "provider": 1, + "group_id": group_id, "id": "https://xapi.elearn.rwth-aachen.de/definitions/lms/verbs/unlocked", "consented": True, "objects": json.dumps( diff --git a/src/xapi/views.py b/src/xapi/views.py index 515ecf5..2e4746a 100644 --- a/src/xapi/views.py +++ b/src/xapi/views.py @@ -17,7 +17,7 @@ from rest_framework.views import APIView from backend.utils import lrs_db from consents.models import UserConsents -from providers.models import Provider, ProviderAuthorization, ProviderSchema +from providers.models import Provider, ProviderAuthorization, ProviderSchema, Verb, VerbObject, ProviderVerbGroup from users.models import CustomUser import redis import zeep @@ -140,7 +140,7 @@ def process_statement(x_api_statement, provider, latest_schema): account_email = ( x_api_statement.get("actor", {}).get("account", {}).get("name", None) - ) + ) if mbox is None and account_email is None: return { @@ -160,7 +160,7 @@ def process_statement(x_api_statement, provider, latest_schema): if (not "verb" in x_api_statement) or (not "id" in x_api_statement["verb"]): return {"valid": False, "accepted": False, "reason": "No verb given"} - verb = x_api_statement["verb"]["id"] + verb_id = x_api_statement["verb"]["id"] if (not "object" in x_api_statement) or (not "id" in x_api_statement["object"]): return {"valid": False, "accepted": False, "reason": "No object given"} @@ -169,14 +169,19 @@ def process_statement(x_api_statement, provider, latest_schema): try: user = CustomUser.objects.get(email=email) except ObjectDoesNotExist: - return {"valid": False, "accepted": False, "message": "User not found"} + return {"valid": False, "accepted": False, "message": f"User not found ({email})"} x_api_statement["actor"]["mbox"] = "mailto:" + str(user.id) + "-polaris-id@polaris.com" + # find verb + verb_candidates = Verb.objects.filter(verb_id=verb_id, provider=provider, provider_schema=latest_schema) + if not verb_candidates: + return {"valid": False, "accepted": False, "message": "Verb not found"} + # essential verbs do not require consent - if not verb in [verb["id"] for verb in latest_schema.essential_verbs]: + if not verb_id in [verb.verb_id for verb in latest_schema.essential_verbs()]: - anon_verbs = [verb["id"] for verblist in [group["verbs"] for group in latest_schema.groups] for verb in verblist if verb.get("allowAnonymizedCollection", False)] + anon_verbs = [verb.verb_id for verblist in [group.verbs.all() for group in latest_schema.groups()] for verb in verblist if verb.allow_anonymized_collection] # has the user paused data collection altogether? if user.paused_data_recording: @@ -192,14 +197,13 @@ def process_statement(x_api_statement, provider, latest_schema): else: timestamp = datetime.datetime.now() # if the statement has no timestamp, use the current date s.t. validation does not fail as long as consent exists - # has the user given consent to this verb? - # maybe TODO: load correct provider schema pertaining to this user consent to validate the verb and objects fully + # has the user given consent to this verb at some point and is it still valid? user_consent = UserConsents.objects.filter( - user=user, provider=provider, verb=verb, consented=True, created__lte=timestamp, active=True + user=user, provider=provider, verb__in=verb_candidates, consented=True, created__lte=timestamp, active=True ).first() if not user_consent: - if verb in anon_verbs: + if verb_id in anon_verbs: return { "valid": True, "accepted": True, @@ -209,7 +213,7 @@ def process_statement(x_api_statement, provider, latest_schema): return { "valid": True, "accepted": False, - "reason": f"User has not given consent to verb id {verb}", + "reason": f"User has not given consent to verb id {verb_id}", } # has the user given consent to the object at hand? @@ -235,7 +239,7 @@ def process_statement(x_api_statement, provider, latest_schema): object_consent = True if not object_consent: - if verb in anon_verbs: + if verb_id in anon_verbs: return { "valid": True, "accepted": True, @@ -244,10 +248,10 @@ def process_statement(x_api_statement, provider, latest_schema): return { "valid": True, "accepted": False, - "reason": f"User has not given consent to this object: {object_statement} for verb {verb}", + "reason": f"User has not given consent to this object: {object_statement} for verb {verb_id}", } - return {"valid": True, "accepted": True} + return {"valid": True, "accepted": True} def process_tan_statement(x_api_statement): """ -- GitLab