diff --git a/allianceauth/framework/datatables.py b/allianceauth/framework/datatables.py new file mode 100644 index 00000000..f80e88f8 --- /dev/null +++ b/allianceauth/framework/datatables.py @@ -0,0 +1,115 @@ +import re + +from django.db.models import Model, Q +from django.http import HttpRequest, JsonResponse +from django.template import Context, Template +from django.template.loader import render_to_string +from django.views import View + +from allianceauth.services.hooks import get_extension_logger + + +logger = get_extension_logger(__name__) + +class DataTablesView(View): + """ + Basic DataTables server side table rendering + """ + model: Model = None + templates: list[str] = [] + columns: list[tuple] = [] + + def get_model_qs(self, request: HttpRequest): + return self.model.objects + + def get_param(self, request: HttpRequest, key: str, cast=str, default=""): + return cast(request.GET.get(key, default)) + + def filter_qs(self, search_string: str): + # Global Search + filter_q = Q() + if len(search_string) > 0: + val = search_string + for x in self.columns: + if x[0]: + # Apply search to all Columns + filter_q |= Q(**{f'{x[1]}__icontains': val}) + return filter_q + + def filter_col_qs(self, get: dict): + # Row Search + # TODO check if we have a regex search or not + # TODO check if searchable or not + col_id_regex = r"columns\[(?P[0-9]{1,})\]\[search\]\[value\]" + regex = re.compile(col_id_regex) + filter_q = Q() + for c in get: + _r = regex.findall(c) + if _r: + _c = self.columns[int(_r[0])] + if _c[0]: + if len(get[c]): + filter_q |= Q(**{f'{_c[1]}__iregex': get[c]}) + return filter_q + + def order_str(self, order_col, order_dir): + order = "" + _o = self.columns[order_col] + if _o[0]: + if order_dir == 'desc': + order = '-' + _o[1] + else: + order = _o[1] + return order + + def render_template(self, request: HttpRequest, template: str, ctx: dict): + if "{{" in template: + _template = Template(template) + return _template.render(Context(ctx)) + else: + return render_to_string( + template, + ctx, + request + ) + + def get(self, request: HttpRequest, *args, **kwargs): + + # Get all our Params out of GET + length = self.get_param(request, "length", int) + start = self.get_param(request, "start", int) + search_string = self.get_param(request, "search[value]") + order_col = self.get_param(request, "order[0][column]", int, 0) + order_dir = self.get_param(request, "order[0][dir]") + draw = self.get_param(request, "draw", int) + + limit = start + length + + # Searches + filter_q = Q() | self.filter_qs(search_string) | self.filter_col_qs(request.GET) + + # Build response rows + items = [] + qs = self.get_model_qs(request).filter(filter_q).order_by() + + # Apply ordering + order = self.order_str(order_col, order_dir) + if order != "": + qs = qs.order_by(order) + + # build output + for row in qs[start:limit]: + ctx = {"row": row} + row = [] + for t in self.templates: + row.append(self.render_template(request, t, ctx)) + items.append(row) + + # Build our output dict + datatables_data = {} + datatables_data['draw'] = draw + datatables_data['recordsTotal'] = self.get_model_qs(request).all().count() + datatables_data['recordsFiltered'] = qs.count() + datatables_data['data'] = items + + return JsonResponse(datatables_data) diff --git a/docs/development/custom/framework/datatables.md b/docs/development/custom/framework/datatables.md new file mode 100644 index 00000000..e1214b51 --- /dev/null +++ b/docs/development/custom/framework/datatables.md @@ -0,0 +1,183 @@ +# DataTables Server Side Rendering + +The `allianceauth.framework.datatables.DataTablesView` module provides a simple class based view to +implement simple server side filtering ordering and searching of DataTables. + +This is intended to make the life of our community apps developer a little +easier, so they don't have to reinvent the wheel. + +## Usage + +To use this view is as easy as defining your stub templates, and fields and adding the view to the `urls.py` + +Given the `EveCharacter` Model as our model of choice we would define our stubs like so + +## Add our Templates + +### template/appname/stubs/icon.html + +```html +{% load evelinks %} +{% character_portrait_url row 32 %} +``` + +### template/appname/stubs/name.html + +```html +{{ row.character_name }} ({{row.character_ownership.user.username}}) +``` + +### template/appname/stubs/corp.html + +```html +{{ row.corporation_name }} +``` + +### template/appname/list.html + +```html +{% extends "allianceauth/base-bs5.html" %} +{% load i18n %} +{% block page_title %} + {% translate "App Name" %} +{% endblock page_title %} +{% block content %} + + + + + + + + + + + + + + + + + + + + + + + + +
{% translate "Name" %}{% translate "Corporation" %}{% translate "Alliance" %}
{% translate "Name" %}{% translate "Corporation" %}{% translate "Alliance" %}
+{% endblock content %} +{% block extra_css %} + {% include 'bundles/datatables-css-bs5.html' %} +{% endblock %} +{% block extra_javascript %} + {% include 'bundles/datatables-js-bs5.html' %} + +{% endblock extra_javascript %} + +``` + +## Add our Views + +Then we can setup out view in our `appname/views.py` file. + +```python +from django.shortcuts import render +# Alliance Auth +from allianceauth.framework.datatables import DataTablesView +from allianceauth.eveonline.models import EveCharacter + +## Datatables server side view +class EveCharacterTable(DataTablesView): + model = EveCharacter + # Templates can be a html file or template language directly in the list below + templates = [ + "appname/stubs/icon.html", + "appname/stubs/name.html", + "appname/stubs/corp.html", + "{{ row.alliance_name }} {{ row.alliance_id }}" + ] + + # Define the columns for filtering and ordering + columns = [ + # (can_sort: bool, "field_for_queries_or_sort") + (False, ""), + (True, "character_name"), + (True, "corporation_name"), + (True, "alliance_name"), + ] + + # if you need to do some prefetch or pre-filtering you can overide this function + def get_model_qs(self, request: HttpRequest): + qs = self.model.objects + if not request.user.is_superuser: + # eg only show unlinked characters to non-superusers + # just an example + # filtering here will prevent people searching things that may not be visible to them + qs = qs.filter(character_ownership__isnull=True) + # maybe some character ownership select related for performance? + return qs.select_related("character_ownership", "character_ownership__user") + +## Main Page View +def show_table(request): + return render("appname/list.html") +``` + +## Add our Urls + +### appname/urls.py + +```python +from django.urls import path + +from . import views + +app_name = 'appname' + +urlpatterns = [ + path("list/", views.EveCharacterTable.as_view(), name='eve_character_table'), + path("tables/data_table", views.show_table, name='table') +] +``` + +and you are done.