Merge pull request 'Added pagination and column sorting to the admin dashboard table' (#80) from tables2-admin-dashboard into main
Reviewed-on: https://gitea.pangea.org/trustchain-oc1-orchestral/IdHub/pulls/80
This commit is contained in:
commit
5eb606c4a4
28
idhub/admin/tables.py
Normal file
28
idhub/admin/tables.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import django_tables2 as tables
|
||||||
|
from idhub.models import Rol, Event
|
||||||
|
from idhub_auth.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class UserTable(tables.Table):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
template_name = "idhub/custom_table.html"
|
||||||
|
fields = ("first_name", "last_name", "email", "is_active", "is_admin")
|
||||||
|
|
||||||
|
|
||||||
|
class RolesTable(tables.Table):
|
||||||
|
class Meta:
|
||||||
|
model = Rol
|
||||||
|
template_name = "idhub/custom_table.html"
|
||||||
|
fields = ("name", "description")
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
|
@ -5,6 +5,7 @@ import pandas as pd
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from jsonschema import validate
|
from jsonschema import validate
|
||||||
from smtplib import SMTPException
|
from smtplib import SMTPException
|
||||||
|
from django_tables2 import SingleTableView
|
||||||
|
|
||||||
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 _
|
||||||
|
@ -30,6 +31,9 @@ from idhub.admin.forms import (
|
||||||
SchemaForm,
|
SchemaForm,
|
||||||
UserRolForm,
|
UserRolForm,
|
||||||
)
|
)
|
||||||
|
from idhub.admin.tables import (
|
||||||
|
DashboardTable
|
||||||
|
)
|
||||||
from idhub.models import (
|
from idhub.models import (
|
||||||
DID,
|
DID,
|
||||||
Event,
|
Event,
|
||||||
|
@ -43,19 +47,15 @@ from idhub.models import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DashboardView(AdminView, TemplateView):
|
class DashboardView(AdminView, SingleTableView):
|
||||||
template_name = "idhub/admin/dashboard.html"
|
template_name = "idhub/admin/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"
|
||||||
|
model = Event
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
context.update({
|
|
||||||
'events': Event.objects.filter(user=None),
|
|
||||||
})
|
|
||||||
return context
|
|
||||||
|
|
||||||
class People(AdminView):
|
class People(AdminView):
|
||||||
title = _("User management")
|
title = _("User management")
|
||||||
|
|
|
@ -1,29 +1,11 @@
|
||||||
{% extends "idhub/base_admin.html" %}
|
{% extends "idhub/base_admin.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 'Event' %}</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 events %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ ev.get_type_name }}</td>
|
|
||||||
<td>{{ ev.message }}</td>
|
|
||||||
<td>{{ ev.created }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
98
idhub/templates/idhub/custom_table.html
Normal file
98
idhub/templates/idhub/custom_table.html
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
{% load django_tables2 %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block table-wrapper %}
|
||||||
|
<div class="table-container">
|
||||||
|
{% block table %}
|
||||||
|
<table class= "table table-striped table-sm">
|
||||||
|
{% block table.thead %}
|
||||||
|
{% if table.show_header %}
|
||||||
|
<thead {{ table.attrs.thead.as_html }}>
|
||||||
|
<tr>
|
||||||
|
{% for column in table.columns %}
|
||||||
|
<th scope="col" {{ column.attrs.th.as_html }}>
|
||||||
|
{% if column.orderable %}
|
||||||
|
<a type="button" class="btn btn-grey border border-dark"
|
||||||
|
href="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}">{{ column.header }}</a>
|
||||||
|
{% else %}
|
||||||
|
{{ column.header }}
|
||||||
|
{% endif %}
|
||||||
|
</th>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock table.thead %}
|
||||||
|
{% block table.tbody %}
|
||||||
|
<tbody {{ table.attrs.tbody.as_html }}>
|
||||||
|
{% for row in table.paginated_rows %}
|
||||||
|
{% block table.tbody.row %}
|
||||||
|
<tr {{ row.attrs.as_html }}>
|
||||||
|
{% for column, cell in row.items %}
|
||||||
|
<td {{ column.attrs.td.as_html }}>{% if column.localize == None %}{{ cell }}{% else %}{% if column.localize %}{{ cell|localize }}{% else %}{{ cell|unlocalize }}{% endif %}{% endif %}</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% endblock table.tbody.row %}
|
||||||
|
{% empty %}
|
||||||
|
{% if table.empty_text %}
|
||||||
|
{% block table.tbody.empty_text %}
|
||||||
|
<tr><td colspan="{{ table.columns|length }}">{{ table.empty_text }}</td></tr>
|
||||||
|
{% endblock table.tbody.empty_text %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
{% endblock table.tbody %}
|
||||||
|
{% block table.tfoot %}
|
||||||
|
{% if table.has_footer %}
|
||||||
|
<tfoot {{ table.attrs.tfoot.as_html }}>
|
||||||
|
<tr>
|
||||||
|
{% for column in table.columns %}
|
||||||
|
<td {{ column.attrs.tf.as_html }}>{{ column.footer }}</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock table.tfoot %}
|
||||||
|
</table>
|
||||||
|
{% endblock table %}
|
||||||
|
|
||||||
|
{% block pagination %}
|
||||||
|
{% if table.page and table.paginator.num_pages > 1 %}
|
||||||
|
<ul class="pagination">
|
||||||
|
{% if table.page.has_previous %}
|
||||||
|
{% block pagination.previous %}
|
||||||
|
<li class="previous">
|
||||||
|
<a type="button" class="btn btn-grey border border-dark" href="{% querystring table.prefixed_page_field=table.page.previous_page_number %}">
|
||||||
|
{% trans 'Previous' %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endblock pagination.previous %}
|
||||||
|
{% endif %}
|
||||||
|
{% if table.page.has_previous or table.page.has_next %}
|
||||||
|
{% block pagination.range %}
|
||||||
|
{% for p in table.page|table_page_range:table.paginator %}
|
||||||
|
<li {% if p == table.page.number %}class="active"{% endif %}>
|
||||||
|
{% if p == '...' %}
|
||||||
|
<a type="button" class="btn btn-grey border border-dark" href="#">{{ p }}</a>
|
||||||
|
{% else %}
|
||||||
|
<a type="button" class="btn btn-grey border border-dark" href="{% querystring table.prefixed_page_field=p %}">
|
||||||
|
{{ p }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock pagination.range %}
|
||||||
|
{% endif %}
|
||||||
|
{% if table.page.has_next %}
|
||||||
|
{% block pagination.next %}
|
||||||
|
<li class="next">
|
||||||
|
<a type="button" class="btn btn-grey border border-dark" href="{% querystring table.prefixed_page_field=table.page.next_page_number %}">
|
||||||
|
{% trans 'Next' %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endblock pagination.next %}
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock pagination %}
|
||||||
|
</div>
|
||||||
|
{% endblock table-wrapper %}
|
|
@ -1,3 +0,0 @@
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
0
idhub/tests/__init__.py
Normal file
0
idhub/tests/__init__.py
Normal file
17
idhub/tests/test_models.py
Normal file
17
idhub/tests/test_models.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
from idhub.models import Event
|
||||||
|
from idhub_auth.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class EventModelTest(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
user = User.objects.create(email='testuser@example.org')
|
||||||
|
Event.objects.create(message='Test Event', type=1, user=user)
|
||||||
|
|
||||||
|
def test_event_creation(self):
|
||||||
|
event = Event.objects.get(id=1)
|
||||||
|
self.assertEqual(event.message, 'Test Event')
|
||||||
|
self.assertEqual(event.get_type_name(), 'User registered')
|
||||||
|
|
||||||
|
# Add more tests for other model methods and properties
|
65
idhub/tests/test_tables.py
Normal file
65
idhub/tests/test_tables.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class AdminDashboardTableTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.admin_user = User.objects.create_superuser(
|
||||||
|
email='adminuser@example.org',
|
||||||
|
password='adminpass12')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
# Creating test events with different dates
|
||||||
|
Event.objects.create(message='Event 1', type=1,
|
||||||
|
created=datetime(2023, 1, 3))
|
||||||
|
Event.objects.create(message='Event 2', type=2,
|
||||||
|
created=datetime(2023, 1, 2))
|
||||||
|
Event.objects.create(message='Event 3', type=3,
|
||||||
|
created=datetime(2023, 1, 25))
|
||||||
|
|
||||||
|
def test_sorting_by_date(self):
|
||||||
|
# Create the table
|
||||||
|
table = DashboardTable(Event.objects.all())
|
||||||
|
|
||||||
|
# Apply sorting
|
||||||
|
table.order_by = 'created'
|
||||||
|
|
||||||
|
# Fetch the sorted records
|
||||||
|
sorted_records = list(table.rows)
|
||||||
|
|
||||||
|
# Verify the order is as expected
|
||||||
|
self.assertTrue(sorted_records[0].record.created
|
||||||
|
< sorted_records[1].record.created)
|
||||||
|
self.assertTrue(sorted_records[1].record.created
|
||||||
|
< sorted_records[2].record.created)
|
||||||
|
|
||||||
|
def test_table_in_template(self):
|
||||||
|
self.client.login(email='adminuser@example.org', password='adminpass12')
|
||||||
|
response = self.client.get(reverse('idhub:admin_dashboard'))
|
||||||
|
|
||||||
|
self.assertTemplateUsed(response, 'idhub/custom_table.html')
|
||||||
|
|
||||||
|
def test_table_data(self):
|
||||||
|
Event.objects.create(message="test_event", type=2)
|
||||||
|
Event.objects.create(message="test_event", type=9)
|
||||||
|
|
||||||
|
table = DashboardTable(Event.objects.all())
|
||||||
|
self.assertTrue(isinstance(table, DashboardTable))
|
||||||
|
self.assertEqual(len(table.rows), Event.objects.count())
|
||||||
|
|
||||||
|
def test_table_columns(self):
|
||||||
|
table = DashboardTable(Event.objects.all())
|
||||||
|
expected_columns = ['type', 'message', 'created']
|
||||||
|
for column in expected_columns:
|
||||||
|
self.assertIn(column, table.columns)
|
||||||
|
|
||||||
|
def test_pagination(self):
|
||||||
|
# TODO
|
||||||
|
pass
|
18
idhub/tests/test_templates.py
Normal file
18
idhub/tests/test_templates.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.test import Client, TestCase
|
||||||
|
|
||||||
|
from idhub_auth.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = Client()
|
||||||
|
self.admin_user = User.objects.create_superuser(
|
||||||
|
email='adminuser@example.org',
|
||||||
|
password='adminpass12')
|
||||||
|
|
||||||
|
def test_dashboard_template(self):
|
||||||
|
self.client.login(email='adminuser@example.org', password='adminpass12')
|
||||||
|
response = self.client.get(reverse('idhub:admin_dashboard'))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTemplateUsed(response, 'idhub/base_admin.html')
|
50
idhub/tests/test_views.py
Normal file
50
idhub/tests/test_views.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from idhub_auth.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class AdminDashboardViewTest(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
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')
|
||||||
|
|
||||||
|
def test_view_url_exists_at_desired_location(self):
|
||||||
|
response = self.client.get('/admin/dashboard/', follow=True)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_view_redirects_to_login_when_not_authenticated(self):
|
||||||
|
response = self.client.get(reverse("idhub:admin_dashboard"),
|
||||||
|
follow=True)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTemplateUsed(response, 'auth/login.html')
|
||||||
|
|
||||||
|
def test_view_redirects_on_incorrect_login_attempt(self):
|
||||||
|
self.client.login(email='adminuser@example.org', password='wrongpass')
|
||||||
|
response = self.client.get(reverse('idhub:admin_dashboard'))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
def test_view_redirects_to_login_on_incorrect_login_attempt(self):
|
||||||
|
self.client.login(email='adminuser@example.org', password='wrongpass')
|
||||||
|
response = self.client.get(reverse('idhub:admin_dashboard'),
|
||||||
|
follow=True)
|
||||||
|
|
||||||
|
self.assertTemplateUsed(response, 'auth/login.html')
|
||||||
|
|
||||||
|
def test_login_admin_user(self):
|
||||||
|
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')
|
||||||
|
response = self.client.get(reverse('idhub:admin_dashboard'))
|
||||||
|
|
||||||
|
self.assertTemplateUsed(response, 'idhub/admin/dashboard.html')
|
|
@ -1,6 +1,7 @@
|
||||||
django==4.2.5
|
django==4.2.5
|
||||||
django-bootstrap5==23.3
|
django-bootstrap5==23.3
|
||||||
django-extensions==3.2.3
|
django-extensions==3.2.3
|
||||||
|
django-tables2==2.6.0
|
||||||
black==23.9.1
|
black==23.9.1
|
||||||
python-decouple==3.8
|
python-decouple==3.8
|
||||||
jsonschema==4.19.1
|
jsonschema==4.19.1
|
||||||
|
|
|
@ -70,6 +70,7 @@ INSTALLED_APPS = [
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'django_extensions',
|
'django_extensions',
|
||||||
'django_bootstrap5',
|
'django_bootstrap5',
|
||||||
|
'django_tables2',
|
||||||
'idhub_auth',
|
'idhub_auth',
|
||||||
'idhub'
|
'idhub'
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in a new issue