mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2026-02-10 17:16:22 +01:00
Compare commits
10 Commits
v4.11.2
...
f8ac13dbf8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8ac13dbf8 | ||
|
|
940f6b14b7 | ||
|
|
99c65d2a5d | ||
|
|
55125a8ff3 | ||
|
|
2fd0fcdbcb | ||
|
|
2fe7bcf20e | ||
|
|
70f314e578 | ||
|
|
bc1b1c3a8f | ||
|
|
453512db64 | ||
|
|
4047159fd1 |
@@ -63,7 +63,6 @@ Here is an example of the Alliance Auth web site with a mixture of Services, App
|
||||
- [Aaron Kable](https://gitlab.com/aaronkable/)
|
||||
- [Ariel Rin](https://gitlab.com/soratidus999/)
|
||||
- [Col Crunch](https://gitlab.com/colcrunch/)
|
||||
- [Erik Kalkoken](https://gitlab.com/ErikKalkoken/)
|
||||
- [Rounon Dax](https://gitlab.com/ppfeufer)
|
||||
- [snipereagle1](https://gitlab.com/mckernanin)
|
||||
|
||||
@@ -71,6 +70,7 @@ Here is an example of the Alliance Auth web site with a mixture of Services, App
|
||||
|
||||
- [Adarnof](https://gitlab.com/adarnof/)
|
||||
- [Basraah](https://gitlab.com/basraah/)
|
||||
- [Erik Kalkoken](https://gitlab.com/ErikKalkoken/)
|
||||
|
||||
### Beta Testers / Bug Fixers
|
||||
|
||||
|
||||
115
allianceauth/framework/datatables.py
Normal file
115
allianceauth/framework/datatables.py
Normal file
@@ -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<id>[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)
|
||||
@@ -33,7 +33,7 @@
|
||||
{% include "framework/header/nav-collapse-icon.html" with fa_icon="fa-solid fa-check-double" url=nav_item_link title=nav_item_title icon_on_mobile=True %}
|
||||
|
||||
{% translate "Delete all read notifications" as nav_item_title %}
|
||||
{% url "notifications:mark_all_read" as nav_item_link %}
|
||||
{% url "notifications:delete_all_read" as nav_item_link %}
|
||||
{% include "framework/header/nav-collapse-icon.html" with fa_icon="fa-solid fa-trash-can" url=nav_item_link title=nav_item_title icon_on_mobile=True %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -57,10 +57,10 @@ DATABASES['default'] = {
|
||||
# CCP's developer portal
|
||||
# Logging in to auth requires the publicData scope (can be overridden through the
|
||||
# LOGIN_TOKEN_SCOPES setting). Other apps may require more (see their docs).
|
||||
ESI_SSO_CALLBACK_URL = f"{SITE_URL}/sso/callback" # Do NOT change this line!
|
||||
ESI_SSO_CLIENT_ID = ''
|
||||
ESI_SSO_CLIENT_SECRET = ''
|
||||
ESI_SSO_CALLBACK_URL = f"{SITE_URL}/sso/callback"
|
||||
ESI_USER_CONTACT_EMAIL = '' # A server maintainer that CCP can contact in case of issues.
|
||||
ESI_USER_CONTACT_EMAIL = '' # A server maintainer that CCP can contact in case of issues.
|
||||
|
||||
# By default, emails are validated before new users can log in.
|
||||
# It's recommended to use a free service like SparkPost or Elastic Email to send email.
|
||||
|
||||
@@ -17,9 +17,7 @@ DATABASES["default"] = {
|
||||
"PASSWORD": os.environ.get("AA_DB_PASSWORD"),
|
||||
"HOST": os.environ.get("AA_DB_HOST"),
|
||||
"PORT": os.environ.get("AA_DB_PORT", "3306"),
|
||||
"OPTIONS": {
|
||||
"charset": os.environ.get("AA_DB_CHARSET", "utf8mb4")
|
||||
}
|
||||
"OPTIONS": {"charset": os.environ.get("AA_DB_CHARSET", "utf8mb4")},
|
||||
}
|
||||
|
||||
# Register an application at https://developers.eveonline.com for Authentication
|
||||
@@ -27,10 +25,9 @@ DATABASES["default"] = {
|
||||
# to https://example.com/sso/callback substituting your domain for example.com
|
||||
# Logging in to auth requires the publicData scope (can be overridden through the
|
||||
# LOGIN_TOKEN_SCOPES setting). Other apps may require more (see their docs).
|
||||
|
||||
ESI_SSO_CALLBACK_URL = f"{SITE_URL}/sso/callback" # Do NOT change this line!
|
||||
ESI_SSO_CLIENT_ID = os.environ.get("ESI_SSO_CLIENT_ID")
|
||||
ESI_SSO_CLIENT_SECRET = os.environ.get("ESI_SSO_CLIENT_SECRET")
|
||||
ESI_SSO_CALLBACK_URL = f"{SITE_URL}/sso/callback"
|
||||
ESI_USER_CONTACT_EMAIL = os.environ.get(
|
||||
"ESI_USER_CONTACT_EMAIL"
|
||||
) # A server maintainer that CCP can contact in case of issues.
|
||||
@@ -70,7 +67,6 @@ INSTALLED_APPS += [
|
||||
# 'allianceauth.permissions_tool',
|
||||
# 'allianceauth.srp',
|
||||
# 'allianceauth.timerboard',
|
||||
|
||||
# https://allianceauth.readthedocs.io/en/latest/features/services/index.html
|
||||
# 'allianceauth.services.modules.discord',
|
||||
# 'allianceauth.services.modules.discourse',
|
||||
|
||||
183
docs/development/custom/framework/datatables.md
Normal file
183
docs/development/custom/framework/datatables.md
Normal file
@@ -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 }} <span class="text-small">({{row.character_ownership.user.username}})</span>
|
||||
```
|
||||
|
||||
### 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 %}
|
||||
<table class="table table-striped w-100" id="table">
|
||||
<!-- Normal Header Rows -->
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{% translate "Name" %}</th>
|
||||
<th>{% translate "Corporation" %}</th>
|
||||
<th>{% translate "Alliance" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<!-- Put the footer in the header so it doesn't trigger the sorting on searching -->
|
||||
<!-- only footers with text will get given an input -->
|
||||
<!-- This is just an option, you do it however you like -->
|
||||
<tfoot style="display: table-header-group;">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{% translate "Name" %}</th>
|
||||
<th>{% translate "Corporation" %}</th>
|
||||
<th>{% translate "Alliance" %}</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
<!-- Empty tbody -->
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock content %}
|
||||
{% block extra_css %}
|
||||
{% include 'bundles/datatables-css-bs5.html' %}
|
||||
{% endblock %}
|
||||
{% block extra_javascript %}
|
||||
{% include 'bundles/datatables-js-bs5.html' %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#table').DataTable({
|
||||
serverSide: true,
|
||||
ajax: '/appname/tables/data_table',
|
||||
// This is for the column searching
|
||||
initComplete: function () {
|
||||
this.api()
|
||||
.columns()
|
||||
.every(function () {
|
||||
let column = this;
|
||||
let title = column.footer().textContent;
|
||||
if (title != ""){
|
||||
// Create input element
|
||||
let input = document.createElement('input');
|
||||
input.classList.add("w-100")
|
||||
input.placeholder = title;
|
||||
column.footer().replaceChildren(input);
|
||||
|
||||
// Event listener for user input
|
||||
input.addEventListener('keyup', () => {
|
||||
if (column.search() !== this.value) {
|
||||
column.search(input.value).draw();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
columnDefs: [
|
||||
{ "searchable": false, "targets": [0] },
|
||||
{ "sortable": false, "targets": [0] },
|
||||
],
|
||||
order: [
|
||||
[1, "asc"]
|
||||
],
|
||||
pageLength: 10,
|
||||
responsive : true
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% 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.
|
||||
@@ -28,6 +28,7 @@ The following icons are available in the Alliance Auth SVG sprite:
|
||||
|
||||
- `aa-logo`: The Alliance Auth logo
|
||||
- `aa-loading-spinner`: A loading spinner icon
|
||||
- `aa-mumble-logo`: The Mumble logo
|
||||
|
||||
### Alliance Auth Logo
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ Make the following changes in your auth project's settings file (`local.py`):
|
||||
# Be sure to set the callback URLto https://example.com/discord/callback/
|
||||
# substituting your domain for example.com in Discord's developer portal
|
||||
# (Be sure to add the trailing slash)
|
||||
DISCORD_CALLBACK_URL = f"{SITE_URL}/discord/callback/" # Do NOT change this line!
|
||||
DISCORD_GUILD_ID = ''
|
||||
DISCORD_CALLBACK_URL = f"{SITE_URL}/discord/callback/"
|
||||
DISCORD_APP_ID = ''
|
||||
DISCORD_APP_SECRET = ''
|
||||
DISCORD_BOT_TOKEN = ''
|
||||
|
||||
Reference in New Issue
Block a user