From c07c53a958ea0c432e3429aa0cfdc1334a102267 Mon Sep 17 00:00:00 2001 From: Elijah Date: Fri, 15 Dec 2023 14:32:49 +0100 Subject: [PATCH 01/63] Added custom command to create example data --- .../commands/create_example_data.py | 195 ++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 idhub/management/commands/create_example_data.py diff --git a/idhub/management/commands/create_example_data.py b/idhub/management/commands/create_example_data.py new file mode 100644 index 0000000..7b3f45c --- /dev/null +++ b/idhub/management/commands/create_example_data.py @@ -0,0 +1,195 @@ +import random +import string + +from django.core.management.base import BaseCommand +from django.db import IntegrityError +from idhub.models import Event, Rol, Service, UserRol +from idhub_auth.models import User + +DEFAULT_OBJECTS_CREATED = 30 +RANDOM_STRING_LENGTH = 30 +EMAIL_RANDOM_STRING_LENGTH = 10 + + +class Command(BaseCommand): + help = """ + Populate the database with dummy values. + You can either specify which model to create objects for, + or create objects for all models. + If no data is specified it will create 30 events, users, + services, roles, and user roles + + usage: create_example_data [-h] [--option args] + options: + --amount N Create N objects (includes events, users, services, + roles, and user roles) + --events N Create N events + --users N Create N users + --superusers N Create N superusers + --services N Create N services + --roles N Create N roles + --userroles N Create N user roles + --userrole u s Create a user role for user u and service s + """ + created_users = [] + created_roles = [] + created_services = [] + + def handle(self, *args, **options): + any_option_used = False + + if options["events"]: + self.create_events(options["events"]) + any_option_used = True + if options["users"]: + self.create_users(options["users"]) + any_option_used = True + if options["superusers"]: + self.create_superusers(options["superusers"]) + any_option_used = True + if options["services"]: + self.create_services(options["services"]) + any_option_used = True + if options["roles"]: + self.create_roles(options["roles"]) + any_option_used = True + if options["userroles"]: + self.create_user_roles(options["userroles"]) + any_option_used = True + if options["userrole"]: + user = options["userrole"][0] + service = options["userrole"][1] + self.create_user_roles(1, user, service) + any_option_used = True + + if options["amount"]: + self.create_all(options["amount"]) + any_option_used = True + + if not any_option_used: + self.create_all() + + def add_arguments(self, parser): + parser.add_argument("--amount", type=int, action='store') + parser.add_argument("--events", type=int) + parser.add_argument("--users", type=int) + parser.add_argument("--superusers", type=int) + parser.add_argument("--services", type=int) + parser.add_argument("--roles", type=int) + parser.add_argument("--userroles", type=int) + parser.add_argument("--userrole", nargs=2, type=str) + + def create_all(self, amount=DEFAULT_OBJECTS_CREATED): + self.create_events(amount) + self.create_users(amount) + self.create_roles(amount) + self.create_services(amount) + self.create_user_roles(amount) + + def create_events(self, amount, user=None): + created_event_amount = 0 + for value in range(0, amount): + try: + Event.objects.create( + type=random.randint(1, 30), + message=create_random_string(), + user=user + ) + created_event_amount += 1 + except IntegrityError: + self.stdout.write("Couldn't create event") + self.stdout.write(f"Created {created_event_amount} events") + + def create_users(self, amount): + created_user_amount = 0 + for value in range(0, amount): + email = create_random_string(EMAIL_RANDOM_STRING_LENGTH) + "@example.org" + try: + User.objects.create( + email=email, + # Could be improved, maybe using Faker + first_name=create_random_string(random.randint(5, 10)), + last_name=create_random_string(random.randint(5, 10)) + ) + self.created_users.append(email) + created_user_amount += 1 + except IntegrityError: + self.stdout.write("Couldn't create user " + email) + + self.stdout.write(f"Created {created_user_amount} users") + + def create_superusers(self, amount=0): + """Superusers can only be created from the specific command""" + created_superuser_amount = 0 + for value in range(0, amount): + email = create_random_string(EMAIL_RANDOM_STRING_LENGTH) + try: + User.objects.create_superuser(email) + created_superuser_amount += 1 + except IntegrityError: + self.stdout.write("Couldn't create superuser " + email) + self.stdout.write(f"Created {created_superuser_amount} users") + + def create_services(self, amount): + created_service_amount = 0 + for value in range(0, amount): + domain = create_random_string(random.randint(5, 15)) + try: + service = Service.objects.create( + domain=domain, + description=create_random_string( + random.randint(50, 100)) + ) + self.created_services.append(domain) + try: + associated_rol = Rol.objects.get(name=random.choice( + self.created_roles)) + service.rol.add(associated_rol.id) + except IntegrityError: + self.stdout.write( + f"Couldn't associate role with service {domain}") + created_service_amount += 1 + except IntegrityError: + self.stdout.write("Couldn't create service " + domain) + self.stdout.write(f"Created {created_service_amount} services") + + def create_roles(self, amount): + created_role_amount = 0 + for value in range(0, amount): + # Could be improved, maybe using Faker + name = create_random_string(random.randint(5, 10)) + try: + Rol.objects.create(name=name, + description=create_random_string( + random.randint(50, 100))) + created_role_amount += 1 + except IntegrityError: + self.stdout.write("Couldn't create role " + name) + self.created_roles.append(name) + self.stdout.write(f"Created {created_role_amount} roles") + + def create_user_roles(self, amount, user_id=None, service_id=None): + created_user_role_amount = 0 + user_id = user_id if user_id is not None else random.choice( + self.created_users) + service_id = service_id if service_id is not None else random.choice( + self.created_services) + for value in range(0, amount): + try: + user = User.objects.get(email=user_id) + service = Service.objects.get(domain=service_id) + UserRol.objects.create( + user=user, + service=service + ) + created_user_role_amount += 1 + except IntegrityError: + self.stdout.write("Couldn't create user role for user " + user.email) + user_id = random.choice(self.created_users) + service_id = random.choice(self.created_services) + self.stdout.write(f"Created {created_user_role_amount} user roles") + + +def create_random_string(string_length=RANDOM_STRING_LENGTH): + return ''.join(random.choices(string.ascii_uppercase + string.digits, + k=string_length)) From 592b0fd98030ae842c4da842a7ef874ef67ead93 Mon Sep 17 00:00:00 2001 From: Elijah Date: Fri, 15 Dec 2023 14:45:34 +0100 Subject: [PATCH 02/63] Fixed help tooltip to reflect the appropriate information --- .../commands/create_example_data.py | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/idhub/management/commands/create_example_data.py b/idhub/management/commands/create_example_data.py index 7b3f45c..62a04a3 100644 --- a/idhub/management/commands/create_example_data.py +++ b/idhub/management/commands/create_example_data.py @@ -12,25 +12,15 @@ EMAIL_RANDOM_STRING_LENGTH = 10 class Command(BaseCommand): - help = """ + """ Populate the database with dummy values. You can either specify which model to create objects for, or create objects for all models. If no data is specified it will create 30 events, users, - services, roles, and user roles - - usage: create_example_data [-h] [--option args] - options: - --amount N Create N objects (includes events, users, services, - roles, and user roles) - --events N Create N events - --users N Create N users - --superusers N Create N superusers - --services N Create N services - --roles N Create N roles - --userroles N Create N user roles - --userrole u s Create a user role for user u and service s + services, roles, and user roles. """ + + help = 'Populate the database with dummy values for testing.' created_users = [] created_roles = [] created_services = [] @@ -70,14 +60,27 @@ class Command(BaseCommand): self.create_all() def add_arguments(self, parser): - parser.add_argument("--amount", type=int, action='store') - parser.add_argument("--events", type=int) - parser.add_argument("--users", type=int) - parser.add_argument("--superusers", type=int) - parser.add_argument("--services", type=int) - parser.add_argument("--roles", type=int) - parser.add_argument("--userroles", type=int) - parser.add_argument("--userrole", nargs=2, type=str) + parser.add_argument( + '--amount', type=int, action='store', + help='Create N objects (includes events, users, services, roles, and user roles)' + ) + parser.add_argument('--events', type=int, + help='Create the specified number of events') + parser.add_argument('--users', type=int, + help='Create the specified number of users') + parser.add_argument('--superusers', type=int, + help='Create the specified number of superusers') + parser.add_argument('--services', type=int, + help='Create the specified number of services') + parser.add_argument('--roles', type=int, + help='Create the specified number of roles') + parser.add_argument('--userroles', type=int, + help='Create the specified number of user roles') + parser.add_argument( + '--userrole', nargs=2, type=str, + help='Create a user role for user U and service S', + metavar=('U', 'S'), + ) def create_all(self, amount=DEFAULT_OBJECTS_CREATED): self.create_events(amount) From 7a5d42f5860a1cd67a4c504382848728591fdded Mon Sep 17 00:00:00 2001 From: Elijah Date: Fri, 15 Dec 2023 17:33:35 +0100 Subject: [PATCH 03/63] Added commands to create custom user and superuser --- .../commands/create_example_data.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/idhub/management/commands/create_example_data.py b/idhub/management/commands/create_example_data.py index 62a04a3..54c8385 100644 --- a/idhub/management/commands/create_example_data.py +++ b/idhub/management/commands/create_example_data.py @@ -51,6 +51,14 @@ class Command(BaseCommand): service = options["userrole"][1] self.create_user_roles(1, user, service) any_option_used = True + if options["register"]: + email = options["register"][0] + password = options["register"][1] + self.create_user(email, password) + if options["register_superuser"]: + email = options["register_superuser"][0] + password = options["register_superuser"][1] + self.create_superuser(email, password) if options["amount"]: self.create_all(options["amount"]) @@ -81,6 +89,12 @@ class Command(BaseCommand): help='Create a user role for user U and service S', metavar=('U', 'S'), ) + parser.add_argument('--register', nargs=2, type=str, + help='Create a user with email E and password P', + metavar=('E', 'P')) + parser.add_argument('--register-superuser', nargs=2, type=str, + help='Create a superuser with email E and password P', + metavar=('E', 'P')) def create_all(self, amount=DEFAULT_OBJECTS_CREATED): self.create_events(amount) @@ -192,6 +206,12 @@ class Command(BaseCommand): service_id = random.choice(self.created_services) self.stdout.write(f"Created {created_user_role_amount} user roles") + def create_user(self, email, password): + User.objects.create_user(email, password) + + def create_superuser(self, email, password): + User.objects.create_superuser(email, password) + def create_random_string(string_length=RANDOM_STRING_LENGTH): return ''.join(random.choices(string.ascii_uppercase + string.digits, From b72129d627ad513773b946745503ddcf8f3937de Mon Sep 17 00:00:00 2001 From: Elijah Date: Fri, 15 Dec 2023 17:53:46 +0100 Subject: [PATCH 04/63] Added Faker to create_example_data to generate more realistic objects --- .../commands/create_example_data.py | 33 ++++++++----------- requirements.txt | 1 + 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/idhub/management/commands/create_example_data.py b/idhub/management/commands/create_example_data.py index 54c8385..89aa408 100644 --- a/idhub/management/commands/create_example_data.py +++ b/idhub/management/commands/create_example_data.py @@ -1,15 +1,18 @@ import random -import string from django.core.management.base import BaseCommand from django.db import IntegrityError from idhub.models import Event, Rol, Service, UserRol from idhub_auth.models import User +from faker import Faker + DEFAULT_OBJECTS_CREATED = 30 RANDOM_STRING_LENGTH = 30 EMAIL_RANDOM_STRING_LENGTH = 10 +fake = Faker() + class Command(BaseCommand): """ @@ -109,7 +112,7 @@ class Command(BaseCommand): try: Event.objects.create( type=random.randint(1, 30), - message=create_random_string(), + message=fake.paragraph(nb_sentences=3), user=user ) created_event_amount += 1 @@ -120,13 +123,12 @@ class Command(BaseCommand): def create_users(self, amount): created_user_amount = 0 for value in range(0, amount): - email = create_random_string(EMAIL_RANDOM_STRING_LENGTH) + "@example.org" + email = fake.email() try: User.objects.create( email=email, - # Could be improved, maybe using Faker - first_name=create_random_string(random.randint(5, 10)), - last_name=create_random_string(random.randint(5, 10)) + first_name=fake.first_name(), + last_name=fake.last_name() ) self.created_users.append(email) created_user_amount += 1 @@ -139,7 +141,7 @@ class Command(BaseCommand): """Superusers can only be created from the specific command""" created_superuser_amount = 0 for value in range(0, amount): - email = create_random_string(EMAIL_RANDOM_STRING_LENGTH) + email = fake.email() try: User.objects.create_superuser(email) created_superuser_amount += 1 @@ -150,12 +152,11 @@ class Command(BaseCommand): def create_services(self, amount): created_service_amount = 0 for value in range(0, amount): - domain = create_random_string(random.randint(5, 15)) + domain = fake.text(max_nb_chars=200) try: service = Service.objects.create( domain=domain, - description=create_random_string( - random.randint(50, 100)) + description=fake.text(max_nb_chars=250) ) self.created_services.append(domain) try: @@ -173,12 +174,11 @@ class Command(BaseCommand): def create_roles(self, amount): created_role_amount = 0 for value in range(0, amount): - # Could be improved, maybe using Faker - name = create_random_string(random.randint(5, 10)) + name = fake.job() try: Rol.objects.create(name=name, - description=create_random_string( - random.randint(50, 100))) + description=fake.text(max_nb_chars=250) + ) created_role_amount += 1 except IntegrityError: self.stdout.write("Couldn't create role " + name) @@ -211,8 +211,3 @@ class Command(BaseCommand): def create_superuser(self, email, password): User.objects.create_superuser(email, password) - - -def create_random_string(string_length=RANDOM_STRING_LENGTH): - return ''.join(random.choices(string.ascii_uppercase + string.digits, - k=string_length)) diff --git a/requirements.txt b/requirements.txt index 9b19238..ae069eb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,4 @@ jsonref==1.1.0 pyld==2.0.3 more-itertools==10.1.0 dj-database-url==2.1.0 +faker==21.0.0 From 5d934469c2d5ec5a36c5a38a2e9bde6f19737423 Mon Sep 17 00:00:00 2001 From: Elijah Date: Wed, 29 Nov 2023 14:41:15 +0100 Subject: [PATCH 05/63] Updated User management admin table --- idhub/admin/tables.py | 54 ++++++++++++++++++++++++- idhub/admin/views.py | 12 +++++- idhub/templates/idhub/admin/people.html | 6 ++- 3 files changed, 66 insertions(+), 6 deletions(-) diff --git a/idhub/admin/tables.py b/idhub/admin/tables.py index 79c22ba..252f149 100644 --- a/idhub/admin/tables.py +++ b/idhub/admin/tables.py @@ -1,14 +1,64 @@ import django_tables2 as tables -from django.utils.translation import gettext_lazy as _ +from django.utils.html import format_html + from idhub.models import Rol, Event from idhub_auth.models import User +class ButtonColumn(tables.Column): + attrs = { + "a": { + "type": "button", + "class": "text-primary", + "title": "'View'", + } + } + # django_tables will only call the render function if it doesn't find + # any empty values in the data, so we stop it from matching the data + # to any value considered empty + empty_values = () + + def render(self): + return format_html('') + + class UserTable(tables.Table): + view_user = ButtonColumn( + linkify={ + "viewname": "idhub:admin_people", + "args": [tables.A("pk")] + }, + orderable=False, + ) + membership = tables.Column(empty_values=()) + role = tables.Column(empty_values=()) + + def render_membership(self, record): + return record.get_memberships() + + def order_membership(self, queryset, is_descending): + # TODO: Test that this doesn't return more rows than it should + queryset = queryset.order_by( + ("-" if is_descending else "") + "memberships__type" + ) + + return (queryset, True) + + def render_role(self, record): + return record.get_roles() + + def order_role(self, queryset, is_descending): + queryset = queryset.order_by( + ("-" if is_descending else "") + "roles" + ) + + return (queryset, True) + class Meta: model = User template_name = "idhub/custom_table.html" - fields = ("first_name", "last_name", "email", "is_active", "is_admin") + fields = ("last_name", "first_name", "email", "membership", "role", + "view_user") class RolesTable(tables.Table): diff --git a/idhub/admin/views.py b/idhub/admin/views.py index b6dcbc8..6ea5acd 100644 --- a/idhub/admin/views.py +++ b/idhub/admin/views.py @@ -32,7 +32,8 @@ from idhub.admin.forms import ( UserRolForm, ) from idhub.admin.tables import ( - DashboardTable + DashboardTable, + UserTable ) from idhub.models import ( DID, @@ -82,10 +83,12 @@ class ImportExport(AdminView): section = "ImportExport" -class PeopleListView(People, TemplateView): +class PeopleListView(People, SingleTableView): template_name = "idhub/admin/people.html" subtitle = _('View users') icon = 'bi bi-person' + table_class = UserTable + model = User def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -94,6 +97,11 @@ class PeopleListView(People, TemplateView): }) return context + def get_queryset(self, **kwargs): + queryset = super().get_queryset(**kwargs) + + return queryset + class PeopleView(People, TemplateView): template_name = "idhub/admin/user.html" diff --git a/idhub/templates/idhub/admin/people.html b/idhub/templates/idhub/admin/people.html index 25b7c61..a40b318 100644 --- a/idhub/templates/idhub/admin/people.html +++ b/idhub/templates/idhub/admin/people.html @@ -1,12 +1,14 @@ {% extends "idhub/base_admin.html" %} {% load i18n %} +{% load render_table from django_tables2 %} {% block content %}

{{ subtitle }}

-
+{% render_table table %} +{% comment %}
@@ -35,5 +37,5 @@ {% endfor %}
-
+
{% endcomment %} {% endblock %} From 5fcfc86f01f4b153df64979a08f438533b2f0386 Mon Sep 17 00:00:00 2001 From: Elijah Date: Thu, 30 Nov 2023 17:05:31 +0100 Subject: [PATCH 06/63] Added Roles table --- idhub/admin/tables.py | 33 +++++++++++++++++++++++--- idhub/admin/views.py | 13 ++++++++-- idhub/templates/idhub/admin/roles.html | 24 ++----------------- 3 files changed, 43 insertions(+), 27 deletions(-) diff --git a/idhub/admin/tables.py b/idhub/admin/tables.py index 252f149..f19d450 100644 --- a/idhub/admin/tables.py +++ b/idhub/admin/tables.py @@ -10,9 +10,10 @@ class ButtonColumn(tables.Column): "a": { "type": "button", "class": "text-primary", - "title": "'View'", } } + # it makes no sense to order a column of buttons + orderable = False # django_tables will only call the render function if it doesn't find # any empty values in the data, so we stop it from matching the data # to any value considered empty @@ -28,11 +29,15 @@ class UserTable(tables.Table): "viewname": "idhub:admin_people", "args": [tables.A("pk")] }, - orderable=False, - ) + orderable=False + ) + membership = tables.Column(empty_values=()) role = tables.Column(empty_values=()) + def render_view_user(self): + return format_html('') + def render_membership(self, record): return record.get_memberships() @@ -62,6 +67,28 @@ class UserTable(tables.Table): class RolesTable(tables.Table): + view_role = ButtonColumn( + linkify={ + "viewname": "idhub:admin_rol_edit", + "args": [tables.A("pk")] + }, + orderable=False + ) + + delete_role = ButtonColumn( + linkify={ + "viewname": "idhub:admin_rol_del", + "args": [tables.A("pk")] + }, + orderable=False + ) + + def render_view_role(self): + return format_html('') + + def render_delete_role(self): + return format_html('') + class Meta: model = Rol template_name = "idhub/custom_table.html" diff --git a/idhub/admin/views.py b/idhub/admin/views.py index 6ea5acd..93e93f8 100644 --- a/idhub/admin/views.py +++ b/idhub/admin/views.py @@ -33,7 +33,8 @@ from idhub.admin.forms import ( ) from idhub.admin.tables import ( DashboardTable, - UserTable + UserTable, + RolesTable ) from idhub.models import ( DID, @@ -406,18 +407,26 @@ class PeopleRolDeleteView(PeopleView): return redirect('idhub:admin_people_edit', user.id) -class RolesView(AccessControl): +class RolesView(AccessControl, SingleTableView): template_name = "idhub/admin/roles.html" subtitle = _('Manage roles') + table_class = RolesTable icon = '' + model = Rol def get_context_data(self, **kwargs): + queryset = kwargs.pop('object_list', None) + if queryset is None: + self.object_list = self.model.objects.all() + context = super().get_context_data(**kwargs) context.update({ 'roles': Rol.objects, }) + return context + class RolRegisterView(AccessControl, CreateView): template_name = "idhub/admin/rol_register.html" subtitle = _('Add role') diff --git a/idhub/templates/idhub/admin/roles.html b/idhub/templates/idhub/admin/roles.html index d3d6593..0bc90aa 100644 --- a/idhub/templates/idhub/admin/roles.html +++ b/idhub/templates/idhub/admin/roles.html @@ -1,5 +1,6 @@ {% extends "idhub/base_admin.html" %} {% load i18n %} +{% load render_table from django_tables2 %} {% block content %}

@@ -8,31 +9,10 @@

-
- - - - - - - - - - - {% for rol in roles.all %} - - - - - - - {% endfor %} - -
{{ rol.name }}{{ rol.description|default:""}}
+ {% render_table table %} -
{% endblock %} From 4aedfe6a5c5f13a5ae9a3539b0e887fda56eef70 Mon Sep 17 00:00:00 2001 From: Elijah Date: Thu, 30 Nov 2023 17:06:00 +0100 Subject: [PATCH 07/63] Added tests for the User model and view --- idhub/templates/idhub/admin/people.html | 30 --------------- idhub/tests/test_models.py | 43 +++++++++++++++++++++- idhub/tests/test_views.py | 49 +++++++++++++++++++++++-- 3 files changed, 88 insertions(+), 34 deletions(-) diff --git a/idhub/templates/idhub/admin/people.html b/idhub/templates/idhub/admin/people.html index a40b318..4862f82 100644 --- a/idhub/templates/idhub/admin/people.html +++ b/idhub/templates/idhub/admin/people.html @@ -8,34 +8,4 @@ {{ subtitle }} {% render_table table %} -{% comment %}
- - - - - - - - - - - - - {% for user in users %} - - - - - - - - - {% endfor %} - -
{{ user.last_name|default:'' }}{{ user.first_name|default:'' }}{{ user.email }} - {{ user.get_memberships }} - - {{ user.get_roles }} -
-
{% endcomment %} {% endblock %} diff --git a/idhub/tests/test_models.py b/idhub/tests/test_models.py index 62bcdb3..351bfab 100644 --- a/idhub/tests/test_models.py +++ b/idhub/tests/test_models.py @@ -14,4 +14,45 @@ class EventModelTest(TestCase): self.assertEqual(event.message, 'Test Event') self.assertEqual(event.get_type_name(), 'User registered') - # Add more tests for other model methods and properties + +class UserTest(TestCase): + """ + Tests the very basic aspects of the User model, + like field properties and methods behaving as expected. + Further testing is recommended. + """ + + def setUp(self): + self.user = User.objects.create( + email="test@example.com", + is_admin=True, + first_name="Dummy", + last_name="Dummyson" + ) + + def test_field_properties(self): + user = User.objects.get(email="test@example.com") + self.assertEqual(user._meta.get_field('email').max_length, 255) + self.assertTrue(user._meta.get_field('email').unique) + self.assertTrue(user._meta.get_field('is_active').default) + self.assertFalse(user._meta.get_field('is_admin').default) + + def test_string_representation(self): + self.assertEqual(str(self.user), "test@example.com") + + def test_has_perm(self): + self.assertTrue(self.user.has_perm(None)) + + def test_has_module_perms(self): + self.assertTrue(self.user.has_module_perms(None)) + + def test_is_staff_property(self): + self.assertTrue(self.user.is_staff) + + def test_get_memberships(self): + # TODO + pass + + def test_get_roles(self): + # TODO + pass diff --git a/idhub/tests/test_views.py b/idhub/tests/test_views.py index cb9bbd5..fd131c0 100644 --- a/idhub/tests/test_views.py +++ b/idhub/tests/test_views.py @@ -1,7 +1,8 @@ from django.urls import reverse -from django.test import TestCase +from django.test import TestCase, RequestFactory from idhub_auth.models import User +from idhub.admin.views import PeopleListView class AdminDashboardViewTest(TestCase): @@ -38,13 +39,55 @@ class AdminDashboardViewTest(TestCase): self.assertTemplateUsed(response, 'auth/login.html') def test_login_admin_user(self): - self.client.login(email='adminuser@example.org', password='adminpass12') + self.client.login(email='adminuser@example.org', + password='adminpass12') response = self.client.get(reverse('idhub:admin_dashboard')) self.assertEqual(response.status_code, 200) def test_view_uses_correct_template(self): - self.client.login(email='adminuser@example.org', password='adminpass12') + self.client.login(email='adminuser@example.org', + password='adminpass12') response = self.client.get(reverse('idhub:admin_dashboard')) self.assertTemplateUsed(response, 'idhub/admin/dashboard.html') + + +class PeopleListViewTest(TestCase): + + def setUp(self): + # Set up a RequestFactory to create mock requests + self.factory = RequestFactory() + + # Create some user instances for testing + self.user = User.objects.create_user(email='normaluser@example.org', + password='testpass12') + self.admin_user = User.objects.create_superuser( + email='adminuser@example.org', + password='adminpass12') + + # Create a request object for the view + self.request = self.factory.get(reverse('idhub:admin_people_list')) + + self.request.user = self.admin_user + + def test_template_used(self): + response = PeopleListView.as_view()(self.request) + + self.assertEqual(response.template_name[0], "idhub/admin/people.html") + + def test_context_data(self): + response = PeopleListView.as_view()(self.request) + + self.assertIn('users', response.context_data) + + # Assuming 2 users were created + self.assertEqual(len(response.context_data['users']), 2) + + def test_get_queryset(self): + view = PeopleListView() + view.setup(self.request) + queryset = view.get_queryset() + + # Assuming 2 users in the database + self.assertEqual(queryset.count(), 2) From 611a7abf95e776fa7e2c100265c7b2a12a5046d8 Mon Sep 17 00:00:00 2001 From: Elijah Date: Fri, 1 Dec 2023 09:46:39 +0100 Subject: [PATCH 08/63] Extended tests for User admin table and models --- idhub/tests/test_models.py | 25 ++++++++++++++++++++----- idhub/tests/test_tables.py | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/idhub/tests/test_models.py b/idhub/tests/test_models.py index 351bfab..b2e61b0 100644 --- a/idhub/tests/test_models.py +++ b/idhub/tests/test_models.py @@ -1,5 +1,5 @@ from django.test import TestCase -from idhub.models import Event +from idhub.models import Event, Membership, Rol, UserRol, Service from idhub_auth.models import User @@ -50,9 +50,24 @@ class UserTest(TestCase): self.assertTrue(self.user.is_staff) def test_get_memberships(self): - # TODO - pass + Membership.objects.create(user=self.user, + type=Membership.Types.BENEFICIARY) + Membership.objects.create(user=self.user, + type=Membership.Types.EMPLOYEE) + + # We test for the length because the order in which the string + # is given in get_memberships is non-deterministic + self.assertEqual(len(self.user.get_memberships()), + len("Beneficiary, Employee")) def test_get_roles(self): - # TODO - pass + user = User.objects.get(email="test@example.com") + service = Service.objects.create(domain="Test Service") + role1 = Rol.objects.create(name="Role 1") + role2 = Rol.objects.create(name="Role 2") + service.rol.add(role1, role2) + UserRol.objects.create(user=user, service=service) + + # We test for the length because the order in which the string + # is given in get_roles is non-deterministic + self.assertEqual(len(user.get_roles()), len("Role 1, Role 2")) diff --git a/idhub/tests/test_tables.py b/idhub/tests/test_tables.py index a78fde9..3aee017 100644 --- a/idhub/tests/test_tables.py +++ b/idhub/tests/test_tables.py @@ -4,8 +4,8 @@ from django.test import TestCase from django.urls import reverse from idhub_auth.models import User -from idhub.admin.tables import DashboardTable -from idhub.models import Event +from idhub.admin.tables import DashboardTable, UserTable +from idhub.models import Event, Membership, Rol, UserRol, Service class AdminDashboardTableTest(TestCase): @@ -63,3 +63,34 @@ class AdminDashboardTableTest(TestCase): def test_pagination(self): # TODO pass + + +class UserTableTest(TestCase): + + def setUp(self): + self.user1 = User.objects.create(email="user1@example.com") + self.user2 = User.objects.create(email="user2@example.com") + Membership.objects.create(user=self.user1, + type=Membership.Types.BENEFICIARY) + + # Set up roles and services + service = Service.objects.create(domain="Test Service") + role = Rol.objects.create(name="Role 1") + service.rol.add(role) + UserRol.objects.create(user=self.user1, service=service) + + self.table = UserTable(User.objects.all()) + + def test_membership_column_render(self): + # Get the user instance for the first row + user = self.table.rows[0].record + # Use the render_membership method of UserTable + rendered_column = self.table.columns['membership'].render(user) + self.assertIn("Beneficiary", str(rendered_column)) + + def test_role_column_render(self): + # Get the user instance for the first row + user = self.table.rows[0].record + # Use the render_role method of UserTable + rendered_column = self.table.columns['role'].render(user) + self.assertIn("Role 1", str(rendered_column)) From fa3eeec32767cdf30f0946ddee45bd80d0265273 Mon Sep 17 00:00:00 2001 From: Elijah Date: Fri, 1 Dec 2023 10:29:32 +0100 Subject: [PATCH 09/63] Small refactor so that RolesView doesn't query database twice --- idhub/admin/views.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/idhub/admin/views.py b/idhub/admin/views.py index 93e93f8..bc93abe 100644 --- a/idhub/admin/views.py +++ b/idhub/admin/views.py @@ -419,12 +419,7 @@ class RolesView(AccessControl, SingleTableView): if queryset is None: self.object_list = self.model.objects.all() - context = super().get_context_data(**kwargs) - context.update({ - 'roles': Rol.objects, - }) - - return context + return super().get_context_data(**kwargs) class RolRegisterView(AccessControl, CreateView): From b143e5749ca2120012ecc808e105df058a728af8 Mon Sep 17 00:00:00 2001 From: Elijah Date: Fri, 1 Dec 2023 12:08:22 +0100 Subject: [PATCH 10/63] Added DID and Services tables --- idhub/admin/tables.py | 101 ++++++++++++++++++- idhub/admin/views.py | 34 +++++-- idhub/templates/idhub/admin/credentials.html | 1 + idhub/templates/idhub/admin/dids.html | 4 + idhub/templates/idhub/admin/services.html | 26 +---- 5 files changed, 131 insertions(+), 35 deletions(-) diff --git a/idhub/admin/tables.py b/idhub/admin/tables.py index f19d450..ec06042 100644 --- a/idhub/admin/tables.py +++ b/idhub/admin/tables.py @@ -1,7 +1,7 @@ import django_tables2 as tables from django.utils.html import format_html -from idhub.models import Rol, Event +from idhub.models import Rol, Event, Service, VerificableCredential, DID from idhub_auth.models import User @@ -95,8 +95,107 @@ class RolesTable(tables.Table): fields = ("name", "description") +class ServicesTable(tables.Table): + domain = tables.Column(verbose_name="Service") + role = tables.Column(empty_values=()) + edit_service = ButtonColumn( + linkify={ + "viewname": "idhub:admin_service_edit", + "args": [tables.A("pk")] + }, + orderable=False + ) + + delete_service = ButtonColumn( + linkify={ + "viewname": "idhub:admin_service_del", + "args": [tables.A("pk")] + }, + orderable=False + ) + + def render_role(self, record): + return record.get_roles() + + def render_edit_service(self): + return format_html('') + + def render_delete_service(self): + return format_html('') + + def order_role(self, queryset, is_descending): + queryset = queryset.order_by( + ("-" if is_descending else "") + "rol" + ) + + return (queryset, True) + + class Meta: + model = Service + template_name = "idhub/custom_table.html" + fields = ("domain", "description", "role", + "edit_service", "delete_service") + + class DashboardTable(tables.Table): class Meta: model = Event template_name = "idhub/custom_table.html" fields = ("type", "message", "created") + + +class CredentialTable(tables.Table): + type = tables.Column(empty_values=()) + details = tables.Column(empty_values=()) + issued_on = tables.Column(verbose_name="Issued") + view_credential = ButtonColumn( + linkify={ + "viewname": "idhub:admin_credential", + "args": [tables.A("pk")] + }, + orderable=False + ) + + def render_type(self, record): + return record.type() + + def render_details(self, record): + return record.description() + + def render_view_credential(self): + return format_html('') + + class Meta: + model = VerificableCredential + template_name = "idhub/custom_table.html" + fields = ("type", "details", "issued_on", "status", "user") + + +class DIDTable(tables.Table): + created_at = tables.Column(verbose_name="Date") + did = tables.Column(verbose_name="ID") + edit_did = ButtonColumn( + linkify={ + "viewname": "idhub:admin_dids_edit", + "args": [tables.A("pk")] + }, + orderable=False, + verbose_name="Edit DID" + ) + delete_template_code = """""" + delete_did = tables.TemplateColumn(template_code=delete_template_code, + orderable=False, + verbose_name="Delete DID") + + def render_edit_did(self): + return format_html('') + + class Meta: + model = DID + template_name = "idhub/custom_table.html" + fields = ("created_at", "label", "did", "edit_did", "delete_did") diff --git a/idhub/admin/views.py b/idhub/admin/views.py index bc93abe..1509433 100644 --- a/idhub/admin/views.py +++ b/idhub/admin/views.py @@ -34,7 +34,10 @@ from idhub.admin.forms import ( from idhub.admin.tables import ( DashboardTable, UserTable, - RolesTable + RolesTable, + ServicesTable, + CredentialTable, + DIDTable ) from idhub.models import ( DID, @@ -473,17 +476,20 @@ class RolDeleteView(AccessControl): return redirect('idhub:admin_roles') -class ServicesView(AccessControl): +class ServicesView(AccessControl, SingleTableView): template_name = "idhub/admin/services.html" + table_class = ServicesTable subtitle = _('Manage services') icon = '' + model = Service def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context.update({ - 'services': Service.objects, - }) - return context + queryset = kwargs.pop('object_list', None) + if queryset is None: + self.object_list = self.model.objects.all() + + return super().get_context_data(**kwargs) + class ServiceRegisterView(AccessControl, CreateView): template_name = "idhub/admin/service_register.html" @@ -546,10 +552,12 @@ class ServiceDeleteView(AccessControl): return redirect('idhub:admin_services') -class CredentialsView(Credentials): +class CredentialsView(Credentials, SingleTableView): template_name = "idhub/admin/credentials.html" + table_class = CredentialTable subtitle = _('View credentials') icon = '' + model = VerificableCredential def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -632,19 +640,26 @@ class DeleteCredentialsView(Credentials): return redirect(self.success_url) -class DidsView(Credentials): +class DidsView(Credentials, SingleTableView): template_name = "idhub/admin/dids.html" + table_class = DIDTable subtitle = _('Manage identities (DID)') icon = 'bi bi-patch-check-fill' wallet = True + model = DID def get_context_data(self, **kwargs): + queryset = kwargs.pop('object_list', None) + if queryset is None: + self.object_list = self.model.objects.all() + context = super().get_context_data(**kwargs) context.update({ 'dids': DID.objects.filter(user=self.request.user), }) return context + class DidRegisterView(Credentials, CreateView): template_name = "idhub/admin/did_register.html" subtitle = _('Add a new organizational identity (DID)') @@ -900,4 +915,3 @@ class ImportAddView(ImportExport, FormView): else: messages.error(self.request, _("Error importing the file!")) return super().form_valid(form) - diff --git a/idhub/templates/idhub/admin/credentials.html b/idhub/templates/idhub/admin/credentials.html index cab6a97..43dc413 100644 --- a/idhub/templates/idhub/admin/credentials.html +++ b/idhub/templates/idhub/admin/credentials.html @@ -1,5 +1,6 @@ {% extends "idhub/base_admin.html" %} {% load i18n %} +{% load render_table from django_tables2 %} {% block content %}

diff --git a/idhub/templates/idhub/admin/dids.html b/idhub/templates/idhub/admin/dids.html index c5cb896..4ae6213 100644 --- a/idhub/templates/idhub/admin/dids.html +++ b/idhub/templates/idhub/admin/dids.html @@ -1,5 +1,6 @@ {% extends "idhub/base_admin.html" %} {% load i18n %} +{% load render_table from django_tables2 %} {% block content %}

@@ -8,6 +9,8 @@

+ {% render_table table %} + {% comment %}
@@ -31,6 +34,7 @@ {% endfor %}
+ {% endcomment %} diff --git a/idhub/templates/idhub/admin/services.html b/idhub/templates/idhub/admin/services.html index 639d1c1..8b4693b 100644 --- a/idhub/templates/idhub/admin/services.html +++ b/idhub/templates/idhub/admin/services.html @@ -1,5 +1,6 @@ {% extends "idhub/base_admin.html" %} {% load i18n %} +{% load render_table from django_tables2 %} {% block content %}

@@ -8,33 +9,10 @@

-
- - - - - - - - - - - - {% for service in services.all %} - - - - - - - - {% endfor %} - -
{{ service.domain }}{{ service.description }}{{ service.get_roles }}
+ {% render_table table %} -
{% endblock %} From 0e4d93652b0d109b4f7e974810fc9e6b809742a9 Mon Sep 17 00:00:00 2001 From: Elijah Date: Wed, 6 Dec 2023 11:45:04 +0100 Subject: [PATCH 11/63] Added Data table --- idhub/admin/tables.py | 19 ++++++++++++++++++- idhub/admin/views.py | 7 +++++-- idhub/templates/idhub/admin/import.html | 2 ++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/idhub/admin/tables.py b/idhub/admin/tables.py index ec06042..ce19c48 100644 --- a/idhub/admin/tables.py +++ b/idhub/admin/tables.py @@ -1,7 +1,14 @@ import django_tables2 as tables from django.utils.html import format_html -from idhub.models import Rol, Event, Service, VerificableCredential, DID +from idhub.models import ( + Rol, + Event, + Service, + VerificableCredential, + DID, + File_datas +) from idhub_auth.models import User @@ -199,3 +206,13 @@ class DIDTable(tables.Table): model = DID template_name = "idhub/custom_table.html" fields = ("created_at", "label", "did", "edit_did", "delete_did") + + +class DataTable(tables.Table): + created_at = tables.Column(verbose_name="Date") + file_name = tables.Column(verbose_name="File") + + class Meta: + model = File_datas + template_name = "idhub/custom_table.html" + fields = ("created_at", "file_name", "success") diff --git a/idhub/admin/views.py b/idhub/admin/views.py index 1509433..d24eb6e 100644 --- a/idhub/admin/views.py +++ b/idhub/admin/views.py @@ -37,7 +37,8 @@ from idhub.admin.tables import ( RolesTable, ServicesTable, CredentialTable, - DIDTable + DIDTable, + DataTable ) from idhub.models import ( DID, @@ -867,10 +868,12 @@ class SchemasImportAddView(SchemasMix): return data -class ImportView(ImportExport, TemplateView): +class ImportView(ImportExport, SingleTableView): template_name = "idhub/admin/import.html" + table_class = DataTable subtitle = _('Import data') icon = '' + model = File_datas def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) diff --git a/idhub/templates/idhub/admin/import.html b/idhub/templates/idhub/admin/import.html index c0e3f64..6c280e4 100644 --- a/idhub/templates/idhub/admin/import.html +++ b/idhub/templates/idhub/admin/import.html @@ -1,5 +1,6 @@ {% extends "idhub/base_admin.html" %} {% load i18n %} +{% load render_table from django_tables2 %} {% block content %}

@@ -8,6 +9,7 @@

+ {% render_table table %}
From dd8999794169e486d0f789b49231ce580286e62a Mon Sep 17 00:00:00 2001 From: Elijah Date: Thu, 14 Dec 2023 20:27:34 +0100 Subject: [PATCH 12/63] Restored Credentials table to old behavior --- idhub/admin/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idhub/admin/views.py b/idhub/admin/views.py index d24eb6e..cd82a3a 100644 --- a/idhub/admin/views.py +++ b/idhub/admin/views.py @@ -553,7 +553,7 @@ class ServiceDeleteView(AccessControl): return redirect('idhub:admin_services') -class CredentialsView(Credentials, SingleTableView): +class CredentialsView(Credentials): template_name = "idhub/admin/credentials.html" table_class = CredentialTable subtitle = _('View credentials') From c55db2495424b56091bed085cc64c777f2647fae Mon Sep 17 00:00:00 2001 From: Elijah Date: Fri, 15 Dec 2023 18:40:56 +0100 Subject: [PATCH 13/63] Fixed templates and data admin tables --- idhub/admin/tables.py | 28 ++++++++++++++++- idhub/admin/views.py | 19 ++++++----- idhub/templates/idhub/admin/import.html | 30 ++---------------- idhub/templates/idhub/admin/schemas.html | 40 ++++-------------------- 4 files changed, 47 insertions(+), 70 deletions(-) diff --git a/idhub/admin/tables.py b/idhub/admin/tables.py index ce19c48..a95ad63 100644 --- a/idhub/admin/tables.py +++ b/idhub/admin/tables.py @@ -7,7 +7,8 @@ from idhub.models import ( Service, VerificableCredential, DID, - File_datas + File_datas, + Schemas ) from idhub_auth.models import User @@ -216,3 +217,28 @@ class DataTable(tables.Table): model = File_datas template_name = "idhub/custom_table.html" fields = ("created_at", "file_name", "success") + + +class TemplateTable(tables.Table): + view_schema = ButtonColumn( + linkify={ + "viewname": "idhub:admin_schemas_download", + "args": [tables.A("pk")] + }, + orderable=False + ) + delete_template_code = """""" + delete_schema = tables.TemplateColumn(template_code=delete_template_code, + orderable=False, + verbose_name="Delete schema") + + class Meta: + model = Schemas + template_name = "idhub/custom_table.html" + fields = ("created_at", "file_schema", "name", "description", + "view_schema", "delete_schema") diff --git a/idhub/admin/views.py b/idhub/admin/views.py index cd82a3a..255f5ab 100644 --- a/idhub/admin/views.py +++ b/idhub/admin/views.py @@ -38,7 +38,8 @@ from idhub.admin.tables import ( ServicesTable, CredentialTable, DIDTable, - DataTable + DataTable, + TemplateTable ) from idhub.models import ( DID, @@ -730,19 +731,21 @@ class WalletConfigIssuesView(Credentials): wallet = True -class SchemasView(SchemasMix): +class SchemasView(SchemasMix, SingleTableView): template_name = "idhub/admin/schemas.html" + table_class = TemplateTable subtitle = _('View credential templates') icon = '' + model = Schemas def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context.update({ - 'schemas': Schemas.objects, - }) - return context + queryset = kwargs.pop('object_list', None) + if queryset is None: + self.object_list = self.model.objects.all() + + return super().get_context_data(**kwargs) + - class SchemasDeleteView(SchemasMix): def get(self, request, *args, **kwargs): diff --git a/idhub/templates/idhub/admin/import.html b/idhub/templates/idhub/admin/import.html index 6c280e4..87c0cc3 100644 --- a/idhub/templates/idhub/admin/import.html +++ b/idhub/templates/idhub/admin/import.html @@ -7,32 +7,8 @@ {{ subtitle }} -
-
- {% render_table table %} -
-
- - - - - - - - - {% for f in dates.all %} - - - - - - {% endfor %} - -
{{ f.created_at }}{{ f.file_name }}{% if f.success %}{% else %}{% endif %}
- -
-
+{% render_table table %} + {% endblock %} diff --git a/idhub/templates/idhub/admin/schemas.html b/idhub/templates/idhub/admin/schemas.html index 6afcf41..543c89b 100644 --- a/idhub/templates/idhub/admin/schemas.html +++ b/idhub/templates/idhub/admin/schemas.html @@ -1,47 +1,19 @@ {% extends "idhub/base_admin.html" %} {% load i18n %} +{% load render_table from django_tables2 %} {% block content %}

{{ subtitle }}

-
-
-
- - - - - - - - - - - - - {% for schema in schemas.all %} - - - - - - - - - {% endfor %} - -
{{ schema.created_at }}{{ schema.file_schema }}{{ schema.name }}{{ schema.description }}
- -
-
+{% render_table table %} + -{% for schema in schemas.all %} -