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.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"

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)
_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):

View file

@ -1,39 +1,11 @@
{% extends "idhub/base.html" %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% block content %}
<h3>
<i class="{{ icon }}"></i>
{{ subtitle }}
</h3>
<div class="row mt-5">
<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>
{% render_table table %}
{% endblock %}

View file

@ -1,29 +1,12 @@
{% extends "idhub/base.html" %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% block content %}
<h3>
<i class="{{ icon }}"></i>
{{ subtitle }}
</h3>
<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 '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>
{% render_table table %}
{% endblock %}

View file

@ -1,41 +1,15 @@
{% extends "idhub/base.html" %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% block content %}
<h3>
<i class="{{ icon }}"></i>
{{ subtitle }}
</h3>
<div class="row mt-5">
<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 '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>
{% render_table 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>
<!-- Modal -->
{% for d in dids.all %}

View file

@ -1,5 +1,6 @@
{% extends "idhub/base.html" %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% block content %}
<div class="row">
@ -33,33 +34,6 @@
{% bootstrap_form form %}
</form>
<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 %}

View file

@ -1,35 +1,12 @@
{% extends "idhub/base.html" %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% block content %}
<h3>
<i class="{{ icon }}"></i>
{{ subtitle }}
</h3>
<div class="row mt-5">
<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>
{% render_table table %}
{% endblock %}

View file

@ -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=[])

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.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"