Merge pull request 'Added sorting and pagination to all user tables' (#88) from user-tables into release

Reviewed-on: https://gitea.pangea.org/trustchain-oc1-orchestral/IdHub/pulls/88
This commit is contained in:
Elahi 2024-01-22 11:49:28 +00:00
commit 867a610de6
11 changed files with 314 additions and 173 deletions

View File

@ -8,6 +8,7 @@ from smtplib import SMTPException
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
from django.conf import settings from django.conf import settings
from django.template.loader import get_template
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from django.views.generic.edit import ( from django.views.generic.edit import (
@ -839,7 +840,7 @@ class SchemasImportAddView(SchemasMix):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
file_name = kwargs['file_schema'] file_name = kwargs['file_schema']
schemas_files = os.listdir(settings.SCHEMAS_DIR) 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!") messages.error(self.request, f"The schema {file_name} not exist!")
return redirect('idhub:admin_schemas_import') return redirect('idhub:admin_schemas_import')
@ -858,7 +859,10 @@ class SchemasImportAddView(SchemasMix):
except Exception: except Exception:
messages.error(self.request, _('This is not a valid schema!')) messages.error(self.request, _('This is not a valid schema!'))
return 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() schema.save()
return schema return schema
@ -870,6 +874,20 @@ class SchemasImportAddView(SchemasMix):
return data 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): class ImportView(ImportExport, SingleTableView):
template_name = "idhub/admin/import.html" template_name = "idhub/admin/import.html"

View File

@ -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',
),
),
]

View File

@ -438,6 +438,7 @@ class Schemas(models.Model):
created_at = models.DateTimeField(auto_now=True) created_at = models.DateTimeField(auto_now=True)
_name = models.CharField(max_length=250, null=True, db_column='name') _name = models.CharField(max_length=250, null=True, db_column='name')
_description = models.CharField(max_length=250, null=True, db_column='description') _description = models.CharField(max_length=250, null=True, db_column='description')
template_description = models.TextField(null=True)
@property @property
def get_schema(self): def get_schema(self):
@ -518,11 +519,8 @@ class VerificableCredential(models.Model):
def type(self): def type(self):
return self.schema.type return self.schema.type
def description(self): def get_description(self):
for des in json.loads(self.render()).get('description', []): return self.schema.template_description
if settings.LANGUAGE_CODE == des.get('lang'):
return des.get('value', '')
return ''
def get_status(self): def get_status(self):
return self.Status(self.status).label return self.Status(self.status).label
@ -569,7 +567,6 @@ class VerificableCredential(models.Model):
tmpl = get_template(template_name) tmpl = get_template(template_name)
return tmpl.render(context) return tmpl.render(context)
def get_issued_on(self): def get_issued_on(self):
if self.issued_on: if self.issued_on:
return self.issued_on.strftime("%m/%d/%Y") return self.issued_on.strftime("%m/%d/%Y")
@ -637,7 +634,7 @@ class Service(models.Model):
def get_roles(self): def get_roles(self):
if self.rol.exists(): 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") return _("None")
def __str__(self): def __str__(self):

View File

@ -1,39 +1,11 @@
{% extends "idhub/base.html" %} {% extends "idhub/base.html" %}
{% load i18n %} {% load i18n %}
{% load render_table from django_tables2 %}
{% block content %} {% block content %}
<h3> <h3>
<i class="{{ icon }}"></i> <i class="{{ icon }}"></i>
{{ subtitle }} {{ subtitle }}
</h3> </h3>
<div class="row mt-5"> {% render_table table %}
<div class="col">
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col"><button type="button" class="btn btn-grey border border-dark">{% trans 'Type' %}</button></th>
<th scope="col"><button type="button" class="btn btn-grey border border-dark">{% trans 'Details' %}</button></th>
<th scope="col"><button type="button" class="btn btn-grey border border-dark">{% trans 'Issued' %}</button></th>
<th scope="col" class="text-center"><button type="button" class="btn btn-grey border border-dark">{% trans 'Status' %}</button></th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for f in credentials.all %}
<tr style="font-size:15px;">
<td>{{ f.type }}</td>
<td>{{ f.description }}</td>
<td>{{ f.get_issued_on }}</td>
<td class="text-center">{{ f.get_status }}</td>
<td>
<a href="{% url 'idhub:user_credential' f.id %}" class="text-primary" title="{% trans 'View' %}"><i class="bi bi-eye"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,29 +1,12 @@
{% extends "idhub/base.html" %} {% extends "idhub/base.html" %}
{% load i18n %} {% load i18n %}
{% load render_table from django_tables2 %}
{% block content %} {% block content %}
<h3> <h3>
<i class="{{ icon }}"></i> <i class="{{ icon }}"></i>
{{ subtitle }} {{ subtitle }}
</h3> </h3>
<div class="table-responsive"> {% render_table table %}
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col"><button type="button" class="btn btn-grey border border-dark">{% trans 'Type' %}</button></th>
<th scope="col"><button type="button" class="btn btn-grey border border-dark">{% trans 'Description' %}</button></th>
<th scope="col"><button type="button" class="btn btn-grey border border-dark">{% trans 'Date' %}</button></th>
</tr>
</thead>
<tbody>
{% for ev in user.events.all %}
<tr>
<td>{{ ev.get_type_name }}</td>
<td>{{ ev.message }}</td>
<td>{{ ev.created }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %} {% endblock %}

View File

@ -1,41 +1,15 @@
{% extends "idhub/base.html" %} {% extends "idhub/base.html" %}
{% load i18n %} {% load i18n %}
{% load render_table from django_tables2 %}
{% block content %} {% block content %}
<h3> <h3>
<i class="{{ icon }}"></i> <i class="{{ icon }}"></i>
{{ subtitle }} {{ subtitle }}
</h3> </h3>
<div class="row mt-5"> {% render_table table %}
<div class="col"> <div class="form-actions-no-box">
<div class="table-responsive"> <a class="btn btn-green-user" href="{% url 'idhub:user_dids_new' %}">{% translate "Add Identity" %} <i class="bi bi-plus"></i></a>
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col"><button type="button" class="btn btn-grey border border-dark">{% trans 'Date' %}</button></th>
<th scope="col"><button type="button" class="btn btn-grey border border-dark">{% trans 'Label' %}</button></th>
<th scope="col"><button type="button" class="btn btn-grey border border-dark">ID</button></th>
<th scope="col"></th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for d in dids.all %}
<tr style="font-size:15px;">
<td>{{ d.created_at }}</td>
<td>{{ d.label }}</td>
<td>{{ d.did }}</td>
<td><a class="text-primary" href="{% url 'idhub:user_dids_edit' d.id %}" title="{% trans 'Edit' %}"><i class="bi bi-pencil-square"></i></a></td>
<td><a class="text-danger" href="jacascript:void()" data-bs-toggle="modal" data-bs-target="#confirm-delete-{{ d.id }}" title="{% trans 'Remove' %}"><i class="bi bi-trash"></i></a></td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="form-actions-no-box">
<a class="btn btn-green-user" href="{% url 'idhub:user_dids_new' %}">{% translate "Add Identity" %} <i class="bi bi-plus"></i></a>
</div>
</div>
</div>
</div> </div>
<!-- Modal --> <!-- Modal -->
{% for d in dids.all %} {% for d in dids.all %}

View File

@ -1,5 +1,6 @@
{% extends "idhub/base.html" %} {% extends "idhub/base.html" %}
{% load i18n %} {% load i18n %}
{% load render_table from django_tables2 %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
@ -33,33 +34,6 @@
{% bootstrap_form form %} {% bootstrap_form form %}
</form> </form>
<hr /> <hr />
{% render_table table %}
<div class="row">
<div class="col">
<div class="table-responsive">
<table class="table table-striped table-sm text-center">
<thead>
<tr>
<th scope="col"><button type="button" class="btn btn-grey border border-dark">{% trans 'Membership' %}</button></th>
<th scope="col"><button type="button" class="btn btn-grey border border-dark">{% trans 'From' %}</button></th>
<th scope="col"><button type="button" class="btn btn-grey border border-dark">{% trans 'To' %}</button></th>
<th scope="col"><button type="button" class="btn btn-grey border border-dark">{% trans 'Credentials' %}</button></th>
</tr>
</thead>
<tbody>
{% for membership in object.memberships.all %}
<tr>
<td>{{ membership.get_type }}</td>
<td>{{ membership.start_date|default:'' }}</td>
<td>{{ membership.end_date|default:'' }}</td>
<td>
<a href="javascript:void()" class="text-primary" title="{% trans 'View' %}"><i class="bi bi-eye"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,35 +1,12 @@
{% extends "idhub/base.html" %} {% extends "idhub/base.html" %}
{% load i18n %} {% load i18n %}
{% load render_table from django_tables2 %}
{% block content %} {% block content %}
<h3> <h3>
<i class="{{ icon }}"></i> <i class="{{ icon }}"></i>
{{ subtitle }} {{ subtitle }}
</h3> </h3>
<div class="row mt-5"> {% render_table table %}
<div class="col">
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col"><button type="button" class="btn btn-grey border border-dark">{% trans 'Role' %}</button></th>
<th scope="col"><button type="button" class="btn btn-grey border border-dark">{% trans 'Description' %}</button></th>
<th scope="col"><button type="button" class="btn btn-grey border border-dark">{% trans 'Service' %}</button></th>
</tr>
</thead>
<tbody>
{% for rol in user.roles.all %}
<tr>
<td>{{ rol.service.get_roles }}</td>
<td>{{ rol.service.description }}</td>
<td>{{ rol.service.domain }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,21 +1,10 @@
import requests
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from idhub_auth.models import User
from idhub.models import DID, VerificableCredential from idhub.models import DID, VerificableCredential
from oidc4vp.models import Organization 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): class RequestCredentialForm(forms.Form):
did = forms.ChoiceField(label=_("Did"), choices=[]) did = forms.ChoiceField(label=_("Did"), choices=[])
credential = forms.ChoiceField(label=_("Credential"), choices=[]) credential = forms.ChoiceField(label=_("Credential"), choices=[])

144
idhub/user/tables.py Normal file
View File

@ -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('<i class="bi bi-eye"></i>')
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 = """<a class="text-danger"
href="javascript:void()"
data-bs-toggle="modal"
data-bs-target="#confirm-delete-{{ record.id }}"
title="Remove"
><i class="bi bi-trash"></i></a>"""
delete = tables.TemplateColumn(template_code=delete_template_code,
orderable=False)
def render_edit(self):
return format_html('<i class="bi bi-pencil-square"></i>')
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")

View File

@ -12,13 +12,21 @@ from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.http import HttpResponse from django.http import HttpResponse
from django.contrib import messages 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 ( from idhub.user.forms import (
ProfileForm, RequestCredentialForm,
RequestCredentialForm, DemandAuthorizationForm
DemandAuthorizationForm
) )
from idhub.mixins import UserView 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): class MyProfile(UserView):
@ -31,21 +39,35 @@ class MyWallet(UserView):
section = "MyWallet" section = "MyWallet"
class DashboardView(UserView, TemplateView): class DashboardView(UserView, SingleTableView):
template_name = "idhub/user/dashboard.html" template_name = "idhub/user/dashboard.html"
table_class = DashboardTable
title = _('Dashboard') title = _('Dashboard')
subtitle = _('Events') subtitle = _('Events')
icon = 'bi bi-bell' icon = 'bi bi-bell'
section = "Home" 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" template_name = "idhub/user/profile.html"
table_class = PersonalInfoTable
subtitle = _('My personal data') subtitle = _('My personal data')
icon = 'bi bi-person-gear' icon = 'bi bi-person-gear'
from_class = ProfileForm
fields = ('first_name', 'last_name', 'email') fields = ('first_name', 'last_name', 'email')
success_url = reverse_lazy('idhub:user_profile') 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): def get_object(self):
return self.request.user return self.request.user
@ -61,11 +83,17 @@ class ProfileView(MyProfile, UpdateView):
return super().form_valid(form) return super().form_valid(form)
class RolesView(MyProfile, TemplateView): class RolesView(MyProfile, SingleTableView):
template_name = "idhub/user/roles.html" template_name = "idhub/user/roles.html"
table_class = RolesTable
subtitle = _('My roles') subtitle = _('My roles')
icon = 'fa-brands fa-critical-role' icon = 'fa-brands fa-critical-role'
def get_queryset(self, **kwargs):
queryset = self.request.user.roles.all()
return queryset
class GDPRView(MyProfile, TemplateView): class GDPRView(MyProfile, TemplateView):
template_name = "idhub/user/gdpr.html" template_name = "idhub/user/gdpr.html"
@ -73,20 +101,17 @@ class GDPRView(MyProfile, TemplateView):
icon = 'bi bi-file-earmark-medical' icon = 'bi bi-file-earmark-medical'
class CredentialsView(MyWallet, TemplateView): class CredentialsView(MyWallet, SingleTableView):
template_name = "idhub/user/credentials.html" template_name = "idhub/user/credentials.html"
table_class = CredentialsTable
subtitle = _('Credential management') subtitle = _('Credential management')
icon = 'bi bi-patch-check-fill' icon = 'bi bi-patch-check-fill'
def get_context_data(self, **kwargs): def get_queryset(self):
context = super().get_context_data(**kwargs) queryset = VerificableCredential.objects.filter(
creds = VerificableCredential.objects.filter( user=self.request.user)
user=self.request.user
) return queryset
context.update({
'credentials': creds,
})
return context
class CredentialView(MyWallet, TemplateView): class CredentialView(MyWallet, TemplateView):
@ -180,8 +205,9 @@ class DemandAuthorizationView(MyWallet, FormView):
return super().form_valid(form) return super().form_valid(form)
class DidsView(MyWallet, TemplateView): class DidsView(MyWallet, SingleTableView):
template_name = "idhub/user/dids.html" template_name = "idhub/user/dids.html"
table_class = DIDTable
subtitle = _('Identities (DIDs)') subtitle = _('Identities (DIDs)')
icon = 'bi bi-patch-check-fill' icon = 'bi bi-patch-check-fill'
@ -192,6 +218,11 @@ class DidsView(MyWallet, TemplateView):
}) })
return context return context
def get_queryset(self, **kwargs):
queryset = DID.objects.filter(user=self.request.user)
return queryset
class DidRegisterView(MyWallet, CreateView): class DidRegisterView(MyWallet, CreateView):
template_name = "idhub/user/did_register.html" template_name = "idhub/user/did_register.html"