diff --git a/idhub/admin/views.py b/idhub/admin/views.py index d0b9405..7bb3f8d 100644 --- a/idhub/admin/views.py +++ b/idhub/admin/views.py @@ -8,6 +8,7 @@ from smtplib import SMTPException from django_tables2 import SingleTableView from django.conf import settings +from django.template.loader import get_template from django.utils.translation import gettext_lazy as _ from django.views.generic.base import TemplateView from django.views.generic.edit import ( @@ -833,13 +834,13 @@ class SchemasImportView(SchemasMix): if not Schemas.objects.filter(file_schema=x).exists()] return schemas - + class SchemasImportAddView(SchemasMix): def get(self, request, *args, **kwargs): file_name = kwargs['file_schema'] schemas_files = os.listdir(settings.SCHEMAS_DIR) - if not file_name in schemas_files: + if file_name not in schemas_files: messages.error(self.request, f"The schema {file_name} not exist!") return redirect('idhub:admin_schemas_import') @@ -858,7 +859,10 @@ class SchemasImportAddView(SchemasMix): except Exception: messages.error(self.request, _('This is not a valid schema!')) return - schema = Schemas.objects.create(file_schema=file_name, data=data, type=name) + schema = Schemas.objects.create(file_schema=file_name, + data=data, type=name, + template_description=self.get_description() + ) schema.save() return schema @@ -870,6 +874,20 @@ class SchemasImportAddView(SchemasMix): return data + def get_template_description(self): + context = self.get_context() + template_name = 'credentials/{}'.format( + self.schema.file_schema + ) + tmpl = get_template(template_name) + return tmpl.render(context) + + def get_description(self): + for des in json.loads(self.render()).get('description', []): + if settings.LANGUAGE_CODE == des.get('lang'): + return des.get('value', '') + return '' + class ImportView(ImportExport, SingleTableView): template_name = "idhub/admin/import.html" diff --git a/idhub/migrations/0002_schemas_template_description_alter_did_label_and_more.py b/idhub/migrations/0002_schemas_template_description_alter_did_label_and_more.py new file mode 100644 index 0000000..866e23c --- /dev/null +++ b/idhub/migrations/0002_schemas_template_description_alter_did_label_and_more.py @@ -0,0 +1,82 @@ +# Generated by Django 4.2.5 on 2023-12-19 17:27 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ('idhub', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='schemas', + name='template_description', + field=models.TextField(null=True), + ), + migrations.AlterField( + model_name='did', + name='label', + field=models.CharField(max_length=50, verbose_name='Label'), + ), + migrations.AlterField( + model_name='event', + name='created', + field=models.DateTimeField(auto_now=True, verbose_name='Date'), + ), + migrations.AlterField( + model_name='event', + name='message', + field=models.CharField(max_length=350, verbose_name='Description'), + ), + migrations.AlterField( + model_name='event', + name='type', + field=models.PositiveSmallIntegerField( + choices=[ + (1, 'User registered'), + (2, 'User welcomed'), + (3, 'Data update requested by user'), + (4, 'Data update requested. Pending approval by administrator'), + (5, "User's data updated by admin"), + (6, 'Your data updated by admin'), + (7, 'User deactivated by admin'), + (8, 'DID created by user'), + (9, 'DID created'), + (10, 'DID deleted'), + (11, 'Credential deleted by user'), + (12, 'Credential deleted'), + (13, 'Credential issued for user'), + (14, 'Credential issued'), + (15, 'Credential presented by user'), + (16, 'Credential presented'), + (17, 'Credential enabled'), + (18, 'Credential available'), + (19, 'Credential revoked by admin'), + (20, 'Credential revoked'), + (21, 'Role created by admin'), + (22, 'Role modified by admin'), + (23, 'Role deleted by admin'), + (24, 'Service created by admin'), + (25, 'Service modified by admin'), + (26, 'Service deleted by admin'), + (27, 'Organisational DID created by admin'), + (28, 'Organisational DID deleted by admin'), + (29, 'User deactivated'), + (30, 'User activated'), + ], + verbose_name='Event', + ), + ), + migrations.AlterField( + model_name='userrol', + name='service', + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='users', + to='idhub.service', + verbose_name='Service', + ), + ), + ] diff --git a/idhub/models.py b/idhub/models.py index 83dd90a..40b0b5d 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -438,6 +438,7 @@ class Schemas(models.Model): created_at = models.DateTimeField(auto_now=True) _name = models.CharField(max_length=250, null=True, db_column='name') _description = models.CharField(max_length=250, null=True, db_column='description') + template_description = models.TextField(null=True) @property def get_schema(self): @@ -518,11 +519,8 @@ class VerificableCredential(models.Model): def type(self): return self.schema.type - def description(self): - for des in json.loads(self.render()).get('description', []): - if settings.LANGUAGE_CODE == des.get('lang'): - return des.get('value', '') - return '' + def get_description(self): + return self.schema.template_description def get_status(self): return self.Status(self.status).label @@ -569,7 +567,6 @@ class VerificableCredential(models.Model): tmpl = get_template(template_name) return tmpl.render(context) - def get_issued_on(self): if self.issued_on: return self.issued_on.strftime("%m/%d/%Y") @@ -637,7 +634,7 @@ class Service(models.Model): def get_roles(self): if self.rol.exists(): - return ", ".join([x.name for x in self.rol.all()]) + return ", ".join([x.name for x in self.rol.order_by("name")]) return _("None") def __str__(self): diff --git a/idhub/templates/idhub/user/credentials.html b/idhub/templates/idhub/user/credentials.html index 6f68e56..fcb0378 100644 --- a/idhub/templates/idhub/user/credentials.html +++ b/idhub/templates/idhub/user/credentials.html @@ -1,39 +1,11 @@ {% extends "idhub/base.html" %} {% load i18n %} +{% load render_table from django_tables2 %} {% block content %}

{{ subtitle }}

-
-
-
- - - - - - - - - - - - {% for f in credentials.all %} - - - - - - - - {% endfor %} - -
{{ f.type }}{{ f.description }}{{ f.get_issued_on }}{{ f.get_status }} - -
-
-
-
+{% render_table table %} {% endblock %} diff --git a/idhub/templates/idhub/user/dashboard.html b/idhub/templates/idhub/user/dashboard.html index c13f776..22777ee 100644 --- a/idhub/templates/idhub/user/dashboard.html +++ b/idhub/templates/idhub/user/dashboard.html @@ -1,29 +1,12 @@ {% extends "idhub/base.html" %} {% load i18n %} +{% load render_table from django_tables2 %} {% block content %}

{{ subtitle }}

-
- - - - - - - - - - {% for ev in user.events.all %} - - - - - - {% endfor %} - -
{{ ev.get_type_name }}{{ ev.message }}{{ ev.created }}
-
+{% render_table table %} + {% endblock %} diff --git a/idhub/templates/idhub/user/dids.html b/idhub/templates/idhub/user/dids.html index 28177e2..dc3bfc4 100644 --- a/idhub/templates/idhub/user/dids.html +++ b/idhub/templates/idhub/user/dids.html @@ -1,41 +1,15 @@ {% extends "idhub/base.html" %} {% load i18n %} +{% load render_table from django_tables2 %} {% block content %}

{{ subtitle }}

-
-
-
- - - - - - - - - - - - {% for d in dids.all %} - - - - - - - - {% endfor %} - -
{{ d.created_at }}{{ d.label }}{{ d.did }}
- -
-
+{% render_table table %} +
+ {% translate "Add Identity" %}
{% for d in dids.all %} diff --git a/idhub/templates/idhub/user/profile.html b/idhub/templates/idhub/user/profile.html index 3a5d5f3..6efdc48 100644 --- a/idhub/templates/idhub/user/profile.html +++ b/idhub/templates/idhub/user/profile.html @@ -1,5 +1,6 @@ {% extends "idhub/base.html" %} {% load i18n %} +{% load render_table from django_tables2 %} {% block content %}
@@ -33,33 +34,6 @@ {% bootstrap_form form %}
+{% render_table table %} -
-
-
- - - - - - - - - - - {% for membership in object.memberships.all %} - - - - - - - {% endfor %} - -
{{ membership.get_type }}{{ membership.start_date|default:'' }}{{ membership.end_date|default:'' }} - -
-
-
-
{% endblock %} diff --git a/idhub/templates/idhub/user/roles.html b/idhub/templates/idhub/user/roles.html index 2c32e97..22777ee 100644 --- a/idhub/templates/idhub/user/roles.html +++ b/idhub/templates/idhub/user/roles.html @@ -1,35 +1,12 @@ {% extends "idhub/base.html" %} {% load i18n %} +{% load render_table from django_tables2 %} {% block content %}

{{ subtitle }}

-
-
-
- - - - - - - - - - {% for rol in user.roles.all %} - - - - - - {% endfor %} - -
{{ rol.service.get_roles }}{{ rol.service.description }}{{ rol.service.domain }}
-
-
-
-
+{% render_table table %} {% endblock %} diff --git a/idhub/user/forms.py b/idhub/user/forms.py index 5ac04ad..07b134d 100644 --- a/idhub/user/forms.py +++ b/idhub/user/forms.py @@ -1,21 +1,10 @@ -import requests from django import forms from django.conf import settings from django.utils.translation import gettext_lazy as _ - -from idhub_auth.models import User from idhub.models import DID, VerificableCredential from oidc4vp.models import Organization -class ProfileForm(forms.ModelForm): - MANDATORY_FIELDS = ['first_name', 'last_name', 'email'] - - class Meta: - model = User - fields = ('first_name', 'last_name', 'email') - - class RequestCredentialForm(forms.Form): did = forms.ChoiceField(label=_("Did"), choices=[]) credential = forms.ChoiceField(label=_("Credential"), choices=[]) diff --git a/idhub/user/tables.py b/idhub/user/tables.py new file mode 100644 index 0000000..0cdb4fb --- /dev/null +++ b/idhub/user/tables.py @@ -0,0 +1,144 @@ +from django.utils.html import format_html +import django_tables2 as tables +from idhub.models import Event, Membership, UserRol, DID, VerificableCredential + + +class ButtonColumn(tables.Column): + attrs = { + "a": { + "type": "button", + "class": "text-primary", + } + } + # 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 + empty_values = () + + +class DashboardTable(tables.Table): + type = tables.Column(verbose_name="Event") + message = tables.Column(verbose_name="Description") + created = tables.Column(verbose_name="Date") + + class Meta: + model = Event + template_name = "idhub/custom_table.html" + fields = ("type", "message", "created") + + +class PersonalInfoTable(tables.Table): + type = tables.Column(verbose_name="Membership") + credential = ButtonColumn( + # TODO: See where this should actually link + linkify="idhub:user_credentials", + orderable=False + ) + + def render_credential(self): + return format_html('') + + class Meta: + model = Membership + template_name = "idhub/custom_table.html" + fields = ("type", "start_date", "end_date", "credential") + + +class RolesTable(tables.Table): + name = tables.Column(verbose_name="Role", empty_values=()) + description = tables.Column(empty_values=()) + service = tables.Column() + + def render_name(self, record): + return record.service.get_roles() + + def render_description(self, record): + return record.service.description + + def render_service(self, record): + return record.service.domain + + def order_name(self, queryset, is_descending): + queryset = queryset.order_by( + ("-" if is_descending else "") + "service__rol__name" + ) + + return (queryset, True) + + def order_description(self, queryset, is_descending): + queryset = queryset.order_by( + ("-" if is_descending else "") + "service__description" + ) + + return (queryset, True) + + def order_service(self, queryset, is_descending): + queryset = queryset.order_by( + ("-" if is_descending else "") + "service__domain" + ) + + return (queryset, True) + + class Meta: + model = UserRol + template_name = "idhub/custom_table.html" + fields = ("name", "description", "service") + + +class DIDTable(tables.Table): + created_at = tables.Column(verbose_name="Date") + did = tables.Column(verbose_name="ID") + edit = ButtonColumn( + linkify={ + "viewname": "idhub:user_dids_edit", + "args": [tables.A("pk")] + }, + orderable=False + ) + delete_template_code = """""" + delete = tables.TemplateColumn(template_code=delete_template_code, + orderable=False) + + def render_edit(self): + return format_html('') + + class Meta: + model = DID + template_name = "idhub/custom_table.html" + fields = ("created_at", "label", "did", "edit", "delete") + + +class CredentialsTable(tables.Table): + description = tables.Column(verbose_name="Details", empty_values=()) + + def render_description(self, record): + return record.get_description() + + def render_status(self, record): + return record.get_status() + + def order_type(self, queryset, is_descending): + queryset = queryset.order_by( + ("-" if is_descending else "") + "schema__type" + ) + + return (queryset, True) + + def order_description(self, queryset, is_descending): + queryset = queryset.order_by( + ("-" if is_descending else "") + "schema__template_description" + ) + + return (queryset, True) + + class Meta: + model = VerificableCredential + template_name = "idhub/custom_table.html" + fields = ("type", "description", "issued_on", "status") diff --git a/idhub/user/views.py b/idhub/user/views.py index e6e28dc..07430f5 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -12,13 +12,21 @@ from django.shortcuts import get_object_or_404, redirect from django.urls import reverse_lazy from django.http import HttpResponse from django.contrib import messages +from django_tables2 import SingleTableView +from idhub.user.tables import ( + DashboardTable, + PersonalInfoTable, + RolesTable, + DIDTable, + CredentialsTable +) from idhub.user.forms import ( - ProfileForm, - RequestCredentialForm, - DemandAuthorizationForm + RequestCredentialForm, + DemandAuthorizationForm ) from idhub.mixins import UserView -from idhub.models import DID, VerificableCredential, Event +from idhub.models import DID, VerificableCredential, Event, Membership +from idhub_auth.models import User class MyProfile(UserView): @@ -31,21 +39,35 @@ class MyWallet(UserView): section = "MyWallet" -class DashboardView(UserView, TemplateView): +class DashboardView(UserView, SingleTableView): template_name = "idhub/user/dashboard.html" + table_class = DashboardTable title = _('Dashboard') subtitle = _('Events') icon = 'bi bi-bell' section = "Home" + def get_queryset(self, **kwargs): + queryset = Event.objects.select_related('user').filter( + user=self.request.user) -class ProfileView(MyProfile, UpdateView): + return queryset + + +class ProfileView(MyProfile, UpdateView, SingleTableView): template_name = "idhub/user/profile.html" + table_class = PersonalInfoTable subtitle = _('My personal data') icon = 'bi bi-person-gear' - from_class = ProfileForm fields = ('first_name', 'last_name', 'email') success_url = reverse_lazy('idhub:user_profile') + model = User + + def get_queryset(self, **kwargs): + queryset = Membership.objects.select_related('user').filter( + user=self.request.user) + + return queryset def get_object(self): return self.request.user @@ -61,11 +83,17 @@ class ProfileView(MyProfile, UpdateView): return super().form_valid(form) -class RolesView(MyProfile, TemplateView): +class RolesView(MyProfile, SingleTableView): template_name = "idhub/user/roles.html" + table_class = RolesTable subtitle = _('My roles') icon = 'fa-brands fa-critical-role' + def get_queryset(self, **kwargs): + queryset = self.request.user.roles.all() + + return queryset + class GDPRView(MyProfile, TemplateView): template_name = "idhub/user/gdpr.html" @@ -73,20 +101,17 @@ class GDPRView(MyProfile, TemplateView): icon = 'bi bi-file-earmark-medical' -class CredentialsView(MyWallet, TemplateView): +class CredentialsView(MyWallet, SingleTableView): template_name = "idhub/user/credentials.html" + table_class = CredentialsTable subtitle = _('Credential management') icon = 'bi bi-patch-check-fill' - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - creds = VerificableCredential.objects.filter( - user=self.request.user - ) - context.update({ - 'credentials': creds, - }) - return context + def get_queryset(self): + queryset = VerificableCredential.objects.filter( + user=self.request.user) + + return queryset class CredentialView(MyWallet, TemplateView): @@ -180,8 +205,9 @@ class DemandAuthorizationView(MyWallet, FormView): return super().form_valid(form) -class DidsView(MyWallet, TemplateView): +class DidsView(MyWallet, SingleTableView): template_name = "idhub/user/dids.html" + table_class = DIDTable subtitle = _('Identities (DIDs)') icon = 'bi bi-patch-check-fill' @@ -192,6 +218,11 @@ class DidsView(MyWallet, TemplateView): }) return context + def get_queryset(self, **kwargs): + queryset = DID.objects.filter(user=self.request.user) + + return queryset + class DidRegisterView(MyWallet, CreateView): template_name = "idhub/user/did_register.html"