Compare commits

...

60 Commits

Author SHA1 Message Date
Aaron Kable
e82fffbf7a Merge branch 'datatables-framework' into 'master'
[Feature] Basic framework for datatables server rendering

See merge request allianceauth/allianceauth!1785
2026-01-13 02:32:32 +00:00
Aaron Kable
fec35dbb1b Merge branch 'datatables-framework' of gitlab.com:aaronkable/allianceauth into datatables-framework 2026-01-13 10:31:06 +08:00
Aaron Kable
2767ab5aaa reset tox 2026-01-13 10:30:13 +08:00
Aaron Kable
f0815a40f7 Merge branch 'add-link-in.menu' into 'datatables-framework'
[ADD] Link in menu

See merge request aaronkable/allianceauth!6
2026-01-11 10:17:55 +00:00
Aaron Kable
6a8ff6ca26 menu 2026-01-11 18:17:06 +08:00
Peter Pfeufer
9300987a4d [ADD] Link in menu 2026-01-11 10:54:13 +01:00
Aaron Kable
7b661a0f42 add number fitlers 2026-01-09 19:40:23 +08:00
Aaron Kable
4799e0beb1 more tests 2026-01-09 10:40:00 +08:00
Aaron Kable
a4cce84f67 py3.8... 2026-01-09 10:26:27 +08:00
Aaron Kable
edf5b7bb8c colum control is & 2026-01-09 09:36:51 +08:00
Aaron Kable
7e338d09a2 Add Colum Control 2026-01-09 09:25:28 +08:00
Aaron Kable
775db62c7a add basic column control fitlering 2026-01-09 08:55:58 +08:00
Aaron Kable
6c2cbb069f check ordering exists 2026-01-09 07:51:06 +08:00
Aaron Kable
a01598e088 tidy up casting 2026-01-08 21:03:45 +08:00
Aaron Kable
e44802fa73 fix tests 2026-01-08 20:49:50 +08:00
Aaron Kable
ad1c17a255 parmas to dict 2026-01-08 20:11:40 +08:00
Aaron Kable
7eb0a6c473 tests 2026-01-08 18:43:47 +08:00
Aaron Kable
d36e3a1256 py 3.8 ... 2026-01-08 18:37:43 +08:00
Aaron Kable
a5c09e0fc7 Add more detail to docs 2026-01-08 18:21:03 +08:00
Aaron Kable
fb7ae95a8a fox template cxt 2026-01-08 17:58:10 +08:00
Aaron Kable
de43114681 pass url params into filters/qs 2026-01-08 17:45:26 +08:00
Aaron Kable
aec055b542 Add more some tests 2026-01-08 15:48:34 +08:00
Aaron Kable
9ed2e97068 Add some tests 2026-01-08 15:44:41 +08:00
Aaron Kable
4d30a59c9c pre-comit save issue 2026-01-08 08:51:54 +08:00
Aaron Kable
1c7c775029 refactor 2026-01-08 08:50:54 +08:00
Aaron Kable
d4e3addd6c Update Docs 2026-01-08 07:59:12 +08:00
Aaron Kable
940f6b14b7 first pass framework for datatables erver rendering 2026-01-07 20:00:52 +08:00
Ariel Rin
99c65d2a5d Merge branch 'helpful-comments' into 'master'
[ADD] Some helpful comments

See merge request allianceauth/allianceauth!1780
2026-01-02 02:43:08 +00:00
Ariel Rin
55125a8ff3 Merge branch 'missing-logo' into 'master'
[ADD] Missing logo to list of available SVG logos

See merge request allianceauth/allianceauth!1781
2026-01-02 02:35:23 +00:00
Ariel Rin
2fd0fcdbcb Merge branch 'notifications_fix' into 'master'
[Fix] wrong delete read url

See merge request allianceauth/allianceauth!1783
2026-01-02 02:35:11 +00:00
Swashman
2fe7bcf20e [Fix] wrong delete read url 2026-01-02 02:35:11 +00:00
Ariel Rin
70f314e578 Merge branch 'development-team' into 'master'
[CHANGE] Update development team

See merge request allianceauth/allianceauth!1782
2025-12-10 08:57:02 +00:00
Peter Pfeufer
bc1b1c3a8f [CHANGE] Update development team 2025-12-10 09:40:47 +01:00
Peter Pfeufer
453512db64 [ADD] Missing logo to list of available SVG logos 2025-11-23 14:02:30 +01:00
Peter Pfeufer
4047159fd1 [ADD] Some helpful comments
Since this is one of the most occurring issues when editing `local.py`
2025-11-22 22:29:24 +01:00
Joel Falknau
f5ddbb8004 version Bump 4.11.2 2025-11-13 11:48:01 +10:00
Joel Falknau
c45d5d7325 Allow the older ua generator to still pass on old python 2025-11-13 11:35:17 +10:00
Joel Falknau
c679ec0646 Version Bump 4.11.1 2025-11-13 10:24:14 +10:00
Joel Falknau
69a51bb08a update pre-commit 2025-11-13 10:17:23 +10:00
Ariel Rin
442c893c39 Merge branch 'master' into 'master'
Edit manager.py

See merge request allianceauth/allianceauth!1778
2025-11-03 10:33:00 +00:00
salartarium
33df15f882 Edit manager.py 2025-11-02 05:52:03 +00:00
Ariel Rin
5d56f0a66c Merge branch 'limit-max-python-version' into 'master'
[CHANGE] Limit max. Python version to what we test for

See merge request allianceauth/allianceauth!1773
2025-10-29 10:41:46 +00:00
Ariel Rin
0fbdbf3a8d Merge branch 'sidebar-total-notification-counter' into 'master'
Sidebar total notification counter

See merge request allianceauth/allianceauth!1774
2025-10-29 10:31:51 +00:00
Ariel Rin
5291bf6896 Merge branch 'translation-fixes' into 'master'
[FIX] Django `makemessages` is ignoring f-strings for translations now

See merge request allianceauth/allianceauth!1772
2025-10-29 00:23:39 +00:00
Ariel Rin
bd6c0fede5 Merge branch 'fix-directories' into 'master'
[HOTFIX] Directories

See merge request allianceauth/allianceauth!1775
2025-10-29 00:23:04 +00:00
Peter Pfeufer
23fe1703c3 [FIX] Directories 2025-10-28 22:33:33 +01:00
Peter Pfeufer
9139b0da56 [CHANGE] JS filename to better reflect what it is for 2025-10-28 09:41:33 +01:00
Peter Pfeufer
80145b313f [ADD] Total notification counter to sidebar 2025-10-28 09:38:20 +01:00
Peter Pfeufer
e96bdd12f9 [CHANGE] Limit max. Python version to what we test for 2025-10-27 11:23:03 +01:00
Joel Falknau
513b7b88f4 another trixie runner, not available to python 3.8 2025-10-27 20:08:15 +10:00
Joel Falknau
5e3fc5c1cb i dont understand this behaviour but okay fine 2025-10-27 19:51:29 +10:00
Ariel Rin
d3069db046 Merge branch 'srp_allow-missing-trailling-slash' into 'master'
SRP - Allow zkillboard links without a trailling slash to be pasted without errors

See merge request allianceauth/allianceauth!1771
2025-10-26 06:26:48 +00:00
Ariel Rin
24863eaf9e Merge branch 'srp_diplay-errors' into 'master'
SRP - Display form errors

See merge request allianceauth/allianceauth!1770
2025-10-26 06:25:27 +00:00
Joel Falknau
d138bd61c5 py38 not available on debian trixie 2025-10-26 12:45:49 +10:00
Joel Falknau
bff20ddd5d remove deprecated future 2025-10-26 11:25:06 +10:00
Joel Falknau
493e694410 update precommit 2025-10-26 11:24:57 +10:00
Peter Pfeufer
8c41d9da58 [FIX] Django makemessages is ignoring f-strings for translations now 2025-10-25 11:14:41 +02:00
Joel Falknau
9e0358a3ce bump test images to trixie 2025-10-25 18:39:24 +10:00
T'rahk Rokym
19bb6856a2 Allow zkillboard links without a trailling slash to be pasted without errors 2025-10-18 00:02:29 +02:00
T'rahk Rokym
9751315b97 Cleaner error display applied to any bootstrap_form in allianceauth
Credit to @ppfeufer for the code snipet
2025-10-17 23:15:42 +02:00
30 changed files with 881 additions and 115 deletions

View File

@@ -25,7 +25,7 @@ before_script:
pre-commit-check: pre-commit-check:
<<: *only-default <<: *only-default
stage: pre-commit stage: pre-commit
image: python:3.11-bookworm image: python:3.11-trixie
# variables: # variables:
# PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit # PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
# cache: # cache:
@@ -65,7 +65,7 @@ test-3.8-core:
test-3.9-core: test-3.9-core:
<<: *only-default <<: *only-default
image: python:3.9-bookworm image: python:3.9-trixie
script: script:
- tox -e py39-core - tox -e py39-core
artifacts: artifacts:
@@ -77,7 +77,7 @@ test-3.9-core:
test-3.10-core: test-3.10-core:
<<: *only-default <<: *only-default
image: python:3.10-bookworm image: python:3.10-trixie
script: script:
- tox -e py310-core - tox -e py310-core
artifacts: artifacts:
@@ -89,7 +89,7 @@ test-3.10-core:
test-3.11-core: test-3.11-core:
<<: *only-default <<: *only-default
image: python:3.11-bookworm image: python:3.11-trixie
script: script:
- tox -e py311-core - tox -e py311-core
artifacts: artifacts:
@@ -101,7 +101,7 @@ test-3.11-core:
test-3.12-core: test-3.12-core:
<<: *only-default <<: *only-default
image: python:3.12-bookworm image: python:3.12-trixie
script: script:
- tox -e py312-core - tox -e py312-core
artifacts: artifacts:
@@ -125,7 +125,7 @@ test-3.8-all:
test-3.9-all: test-3.9-all:
<<: *only-default <<: *only-default
image: python:3.9-bookworm image: python:3.9-trixie
script: script:
- tox -e py39-all - tox -e py39-all
artifacts: artifacts:
@@ -137,7 +137,7 @@ test-3.9-all:
test-3.10-all: test-3.10-all:
<<: *only-default <<: *only-default
image: python:3.10-bookworm image: python:3.10-trixie
script: script:
- tox -e py310-all - tox -e py310-all
artifacts: artifacts:
@@ -149,7 +149,7 @@ test-3.10-all:
test-3.11-all: test-3.11-all:
<<: *only-default <<: *only-default
image: python:3.11-bookworm image: python:3.11-trixie
script: script:
- tox -e py311-all - tox -e py311-all
artifacts: artifacts:
@@ -162,7 +162,7 @@ test-3.11-all:
test-3.12-all: test-3.12-all:
<<: *only-default <<: *only-default
image: python:3.12-bookworm image: python:3.12-trixie
script: script:
- tox -e py312-all - tox -e py312-all
artifacts: artifacts:
@@ -174,7 +174,7 @@ test-3.12-all:
build-test: build-test:
stage: test stage: test
image: python:3.11-bookworm image: python:3.11-trixie
before_script: before_script:
- python -m pip install --upgrade pip - python -m pip install --upgrade pip
@@ -193,13 +193,13 @@ build-test:
test-docs: test-docs:
<<: *only-default <<: *only-default
image: python:3.11-bookworm image: python:3.11-trixie
script: script:
- tox -e docs - tox -e docs
deploy_production: deploy_production:
stage: deploy stage: deploy
image: python:3.11-bookworm image: python:3.11-trixie
before_script: before_script:
- python -m pip install --upgrade pip - python -m pip install --upgrade pip

View File

@@ -25,12 +25,12 @@ exclude: |
repos: repos:
# Code Upgrades # Code Upgrades
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.21.0 rev: v3.21.1
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py38-plus] args: [--py38-plus]
- repo: https://github.com/adamchainz/django-upgrade - repo: https://github.com/adamchainz/django-upgrade
rev: 1.29.0 rev: 1.29.1
hooks: hooks:
- id: django-upgrade - id: django-upgrade
args: [--target-version=4.2] args: [--target-version=4.2]
@@ -64,7 +64,7 @@ repos:
- id: check-executables-have-shebangs - id: check-executables-have-shebangs
- id: end-of-file-fixer - id: end-of-file-fixer
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python - repo: https://github.com/editorconfig-checker/editorconfig-checker.python
rev: 3.4.0 rev: 3.4.1
hooks: hooks:
- id: editorconfig-checker - id: editorconfig-checker
- repo: https://github.com/igorshubovych/markdownlint-cli - repo: https://github.com/igorshubovych/markdownlint-cli
@@ -76,7 +76,7 @@ repos:
- --disable=MD013 - --disable=MD013
# Infrastructure # Infrastructure
- repo: https://github.com/tox-dev/pyproject-fmt - repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.11.0 rev: v2.11.1
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt
name: pyproject.toml formatter name: pyproject.toml formatter
@@ -84,7 +84,7 @@ repos:
args: args:
- --indent=4 - --indent=4
additional_dependencies: additional_dependencies:
- tox==4.31.0 # https://github.com/tox-dev/tox/releases/latest - tox==4.32.0 # https://github.com/tox-dev/tox/releases/latest
- repo: https://github.com/abravalheri/validate-pyproject - repo: https://github.com/abravalheri/validate-pyproject
rev: v0.24.1 rev: v0.24.1
hooks: hooks:

View File

@@ -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/) - [Aaron Kable](https://gitlab.com/aaronkable/)
- [Ariel Rin](https://gitlab.com/soratidus999/) - [Ariel Rin](https://gitlab.com/soratidus999/)
- [Col Crunch](https://gitlab.com/colcrunch/) - [Col Crunch](https://gitlab.com/colcrunch/)
- [Erik Kalkoken](https://gitlab.com/ErikKalkoken/)
- [Rounon Dax](https://gitlab.com/ppfeufer) - [Rounon Dax](https://gitlab.com/ppfeufer)
- [snipereagle1](https://gitlab.com/mckernanin) - [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/) - [Adarnof](https://gitlab.com/adarnof/)
- [Basraah](https://gitlab.com/basraah/) - [Basraah](https://gitlab.com/basraah/)
- [Erik Kalkoken](https://gitlab.com/ErikKalkoken/)
### Beta Testers / Bug Fixers ### Beta Testers / Bug Fixers

View File

@@ -5,7 +5,7 @@ manage online service access.
# This will make sure the app is always imported when # This will make sure the app is always imported when
# Django starts so that shared_task will use this app. # Django starts so that shared_task will use this app.
__version__ = '4.11.0' __version__ = '4.11.2'
__title__ = 'Alliance Auth' __title__ = 'Alliance Auth'
__title_useragent__ = 'AllianceAuth' __title_useragent__ = 'AllianceAuth'
__url__ = 'https://gitlab.com/allianceauth/allianceauth' __url__ = 'https://gitlab.com/allianceauth/allianceauth'

View File

@@ -727,7 +727,8 @@ class TestEveSwaggerProvider(TestCase):
my_provider = EveSwaggerProvider() my_provider = EveSwaggerProvider()
my_client = my_provider.client my_client = my_provider.client
operation = my_client.Universe.get_universe_factions() operation = my_client.Universe.get_universe_factions()
self.assertEqual( expected_variants = {
operation.future.request.headers['User-Agent'], f'AllianceAuth/{aa_version} (dummy@example.net; +{aa_url}) DjangoEsi/{esi_version} (+{esi_url})', # Django-ESI 8.0.0
f'AllianceAuth/{aa_version} (dummy@example.net; +{aa_url}) Django-ESI/{esi_version} (+{esi_url})' f'AllianceAuth/{aa_version} (dummy@example.net; +{aa_url}) Django-ESI/{esi_version} (+{esi_url})' # Django-ESI 7.x, Py38 Py39
) }
self.assertIn(operation.future.request.headers['User-Agent'], expected_variants)

View File

@@ -320,8 +320,8 @@ def click_fatlink_view(request, token, fat_hash=None):
messages.warning( messages.warning(
request, request,
_( _(
f"Cannot register the fleet participation for {character.character_name}. The character needs to be online." "Cannot register the fleet participation for {character_name}. The character needs to be online."
), ).format(character_name=character.character_name)
) )
return redirect('fatlink:view') return redirect('fatlink:view')

View File

@@ -0,0 +1,209 @@
from collections import defaultdict
import re
from typing import List
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 nested_param_dict(dict):
"""
Helper to create infinite depth default dicts for setting from params
"""
def __setitem__(self, item, value):
if "." in item:
head, path = item.split(".", 1)
try:
head = int(head)
except ValueError:
pass
obj = self.setdefault(head, nested_param_dict())
obj[path] = value
else:
super().__setitem__(item, value)
def defaultdict_to_dict(d):
"""
Helper to convert default dict back to dict
"""
if isinstance(d, defaultdict):
d = {k: defaultdict_to_dict(v) for k, v in d.items()}
return d
class DataTablesView(View):
model: Model = None
columns: List[tuple] = []
def get_model_qs(self, request: HttpRequest, *args, **kwargs):
return self.model.objects
def filter_qs(self, table_conf: dict):
# Search
filter_qs = Q()
for id, c in table_conf["columns"].items():
_c = self.columns[int(id)][0]
if c.get("searchable", False) and len(_c) > 0:
if c.get("columnControl", False):
_sv = str(c["columnControl"]["search"]["value"])
"""contains, equal, ends, starts, empty"""
_logic = str(c["columnControl"]["search"]["logic"])
"""text, date, num"""
_type = str(c["columnControl"]["search"]["type"])
if _type == "text":
if _logic == "empty":
filter_qs &= Q(**{f'{_c}': ""})
elif len(_sv) > 0:
if _logic == "contains":
filter_qs &= Q(**{f'{_c}__icontains': _sv})
elif _logic == "starts":
filter_qs &= Q(**{f'{_c}__istartswith': _sv})
elif _logic == "ends":
filter_qs &= Q(**{f'{_c}__iendswith': _sv})
elif _logic == "equal":
filter_qs &= Q(**{f'{_c}': _sv})
elif _type == "num":
if _logic == "empty":
filter_qs &= Q(**{f'{_c}__isnull': True})
elif len(_sv) > 0:
try:
if _logic == "greater":
filter_qs &= Q(**{f'{_c}__gt': float(_sv)})
elif _logic == "less":
filter_qs &= Q(**{f'{_c}__lt': float(_sv)})
elif _logic == "greaterOrEqual":
filter_qs &= Q(**{f'{_c}__gte': float(_sv)})
elif _logic == "lessOrEqual":
filter_qs &= Q(**{f'{_c}__lte': float(_sv)})
elif _logic == "equal":
filter_qs &= Q(**{f'{_c}': float(_sv)})
except ValueError:
pass
else:
_sv = str(c["search"]["value"])
if len(_sv) > 0:
if c["search"]["regex"]:
filter_qs |= Q(**{f'{_c}__iregex': _sv})
else:
filter_qs |= Q(**{f'{_c}__icontains': _sv})
_gsv = str(table_conf["search"]["value"])
if len(_gsv) > 0:
filter_qs |= Q(**{f'{_c}__icontains': _gsv})
return filter_qs
def except_qs(self, table_conf: dict):
# Search
except_qs = Q()
for id, c in table_conf["columns"].items():
_c = self.columns[int(id)][0]
if c.get("searchable", False) and len(_c) > 0:
if c.get("columnControl", False):
_sv = str(c["columnControl"]["search"]["value"])
"""notContains, notEqual, notEmpty"""
_logic = str(c["columnControl"]["search"]["logic"])
"""text, date, num"""
_type = str(c["columnControl"]["search"]["type"])
if _type == "text":
if _logic == "notEmpty":
except_qs |= Q(**{f'{_c}': ""})
elif len(_sv) > 0:
if _logic == "notContains":
except_qs |= Q(**{f'{_c}__icontains': _sv})
elif _logic == "notEqual":
except_qs |= Q(**{f'{_c}': _sv})
elif _type == "num":
if _logic == "notEmpty":
except_qs |= Q(**{f'{_c}__isnull': False})
elif len(_sv) > 0:
if _logic == "notEqual":
try:
except_qs |= Q(**{f'{_c}': float(_sv)})
except ValueError:
pass
return except_qs
def get_table_config(self, get: dict):
_cols = nested_param_dict()
for c, v in get.items():
_keys = [_k for _k in c.replace("]", "").split("[")]
_v = v
if v in ["true", "false"]:
_v = _v == "true"
else:
try:
_v = int(_v)
except ValueError:
pass # not an integer
_cols[".".join(_keys)] = _v
return defaultdict_to_dict(_cols)
def get_order(self, table_conf: dict):
order = []
for oc, od in table_conf.get("order", {}).items():
_c = table_conf["columns"][od["column"]]
if _c["orderable"]:
if od["dir"] == "desc":
order.append("-" + self.columns[int(od["column"])][0])
else:
order.append(self.columns[int(od["column"])][0])
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):
table_conf = self.get_table_config(request.GET)
draw = int(table_conf["draw"])
start = int(table_conf["start"])
length = int(table_conf["length"])
limit = start + length
# Build response rows
items = []
qs = self.get_model_qs(
request,
*args,
**kwargs
).filter(
self.filter_qs(table_conf)
).exclude(
self.except_qs(table_conf)
).order_by(
*self.get_order(table_conf)
)
# build output
for row in qs[start:limit]:
ctx = {"row": row}
row = []
for t in self.columns:
row.append(self.render_template(request, t[1], ctx))
items.append(row)
# Build our output dict
datatables_data = {}
datatables_data['draw'] = draw
datatables_data['recordsTotal'] = self.get_model_qs(request, *args, **kwargs).all().count()
datatables_data['recordsFiltered'] = qs.count()
datatables_data['data'] = items
return JsonResponse(datatables_data)

View File

@@ -47,6 +47,12 @@
/* Side Navigation /* Side Navigation
------------------------------------------------------------------------------------- */ ------------------------------------------------------------------------------------- */
@media all { @media all {
.sidemenu-total-notifications-badge {
position: absolute;
left: 28px;
font-size: 42.5% !important;
}
#sidebar > div { #sidebar > div {
width: 325px; width: 325px;
} }

View File

@@ -0,0 +1,269 @@
"""
Test sentinel user
"""
import json
import re
# Django
from allianceauth.tests.auth_utils import AuthUtils
from django.test import RequestFactory, TestCase
from django.http import HttpRequest
# Alliance Auth
from allianceauth.framework.datatables import DataTablesView
from allianceauth.eveonline.models import EveCharacter
class TestView(DataTablesView):
model=EveCharacter
columns = [
("", "{{ row.character_id }}"),
("character_name", "{{ row.character_name }}"),
("corporation_name", "{{ row.corporation_name }}"),
("alliance_name", "{{ row.alliance_name }}"),
]
class TestDataTables(TestCase):
def setUp(self):
self.get_params = {
'draw': '1',
'columns[0][data]': '0',
'columns[0][name]': '',
'columns[0][searchable]': 'false',
'columns[0][orderable]': 'false',
'columns[0][search][value]': '',
'columns[0][search][regex]': 'false',
'columns[1][data]': '1',
'columns[1][name]': '',
'columns[1][searchable]': 'true',
'columns[1][orderable]': 'true',
'columns[1][search][value]': '',
'columns[1][search][regex]': 'false',
'columns[2][data]': '2',
'columns[2][name]': '',
'columns[2][searchable]': 'true',
'columns[2][orderable]': 'false',
'columns[2][search][value]': '',
'columns[2][search][regex]': 'false',
'columns[3][data]': '3',
'columns[3][name]': '',
'columns[3][searchable]': 'true',
'columns[3][orderable]': 'true',
'columns[3][search][value]': '',
'columns[3][search][regex]': 'false',
'order[0][column]': '1',
'order[0][dir]': 'asc',
'start': '0',
'length': '10',
'search[value]': '',
'search[regex]': 'false',
'_': '123456789'
}
@classmethod
def setUpClass(cls) -> None:
"""
Set up eve models
"""
super().setUpClass()
cls.factory = RequestFactory()
cls.user = AuthUtils.create_user("bruce_wayne")
cls.user.is_superuser = True
cls.user.save()
EveCharacter.objects.all().delete()
for i in range(1,16):
EveCharacter.objects.create(
character_id=1000+i,
character_name=f"{1000+i} - Test Character - {1000+i}",
corporation_id=2000+i,
corporation_name=f"{2000+i} - Test Corporation",
)
for i in range(16,21):
EveCharacter.objects.create(
character_id=1000+i,
character_name=f"{1000+i} - Test Character - {1000+i}",
corporation_id=2000+i,
corporation_name=f"{2000+i} - Test Corporation",
alliance_id=3000+i,
alliance_name=f"{3000+i} - Test Alliance",
)
def test_view_default(self):
self.client.force_login(self.user)
request = self.factory.get('/fake-url/', data=self.get_params)
response = TestView()
response.setup(request)
data = json.loads(response.get(request).content)["data"]
self.assertEqual(data[0][0], "1001")
self.assertEqual(data[9][0], "1010")
def test_view_reverse_sort(self):
self.get_params["order[0][dir]"] = "desc"
self.client.force_login(self.user)
request = self.factory.get('/fake-url/', data=self.get_params)
response = TestView()
response.setup(request)
data = json.loads(response.get(request).content)["data"]
self.assertEqual(data[0][0], "1020")
self.assertEqual(data[9][0], "1011")
def test_view_no_sort(self):
self.get_params.pop("order[0][column]")
self.get_params.pop("order[0][dir]")
self.client.force_login(self.user)
request = self.factory.get('/fake-url/', data=self.get_params)
response = TestView()
response.setup(request)
data = json.loads(response.get(request).content)["data"]
self.assertEqual(data[0][0], "1001")
self.assertEqual(data[9][0], "1010")
def test_view_non_sortable_sort(self):
self.get_params["order[0][dir]"] = "desc"
self.get_params["order[0][column]"] = "0"
self.client.force_login(self.user)
request = self.factory.get('/fake-url/', data=self.get_params)
response = TestView()
response.setup(request)
data = json.loads(response.get(request).content)["data"]
self.assertEqual(data[0][0], "1001")
self.assertEqual(data[9][0], "1010")
def test_view_20_rows(self):
self.get_params["length"] = "20"
self.client.force_login(self.user)
request = self.factory.get('/fake-url/', data=self.get_params)
response = TestView()
response.setup(request)
data = json.loads(response.get(request).content)["data"]
self.assertEqual(data[0][0], "1001")
self.assertEqual(data[19][0], "1020")
def test_view_global_search(self):
self.get_params["search[value]"] = "1020"
self.client.force_login(self.user)
request = self.factory.get('/fake-url/', data=self.get_params)
response = TestView()
response.setup(request)
data = json.loads(response.get(request).content)["data"]
self.assertEqual(len(data), 1)
self.assertEqual(data[0][0], "1020")
def test_view_col_1_search(self):
self.get_params["columns[1][search][value]"] = "1020"
self.client.force_login(self.user)
request = self.factory.get('/fake-url/', data=self.get_params)
response = TestView()
response.setup(request)
data = json.loads(response.get(request).content)["data"]
self.assertEqual(len(data), 1)
self.assertEqual(data[0][0], "1020")
def test_view_col_1_search_empty(self):
self.get_params["columns[1][search][value]"] = "zzz"
self.client.force_login(self.user)
request = self.factory.get('/fake-url/', data=self.get_params)
response = TestView()
response.setup(request)
data = json.loads(response.get(request).content)["data"]
self.assertEqual(len(data), 0)
def test_view_cc_3_search_empty(self):
self.get_params["columns[3][columnControl][search][value]"] = ""
self.get_params["columns[3][columnControl][search][logic]"] = "empty"
self.get_params["columns[3][columnControl][search][type]"] = "text"
self.get_params["length"] = "20"
self.client.force_login(self.user)
request = self.factory.get('/fake-url/', data=self.get_params)
response = TestView()
response.setup(request)
data = json.loads(response.get(request).content)["data"]
self.assertEqual(len(data), 15)
def test_view_cc_3_search_not_empty(self):
self.get_params["columns[3][columnControl][search][value]"] = ""
self.get_params["columns[3][columnControl][search][logic]"] = "notEmpty"
self.get_params["columns[3][columnControl][search][type]"] = "text"
self.client.force_login(self.user)
request = self.factory.get('/fake-url/', data=self.get_params)
response = TestView()
response.setup(request)
data = json.loads(response.get(request).content)["data"]
self.assertEqual(len(data), 5)
def test_view_cc_1_search_ends_with(self):
self.get_params["columns[1][columnControl][search][value]"] = "9"
self.get_params["columns[1][columnControl][search][logic]"] = "ends"
self.get_params["columns[1][columnControl][search][type]"] = "text"
self.client.force_login(self.user)
request = self.factory.get('/fake-url/', data=self.get_params)
response = TestView()
response.setup(request)
data = json.loads(response.get(request).content)["data"]
self.assertEqual(len(data), 2)
def test_view_cc_1_search_starts_with(self):
self.get_params["columns[1][columnControl][search][value]"] = "1009"
self.get_params["columns[1][columnControl][search][logic]"] = "starts"
self.get_params["columns[1][columnControl][search][type]"] = "text"
self.client.force_login(self.user)
request = self.factory.get('/fake-url/', data=self.get_params)
response = TestView()
response.setup(request)
data = json.loads(response.get(request).content)["data"]
self.assertEqual(len(data), 1)
def test_view_cc_1_search_not_contains(self):
self.get_params["columns[1][columnControl][search][value]"] = "100"
self.get_params["columns[1][columnControl][search][logic]"] = "notContains"
self.get_params["columns[1][columnControl][search][type]"] = "text"
self.get_params["length"] = "20"
self.client.force_login(self.user)
request = self.factory.get('/fake-url/', data=self.get_params)
response = TestView()
response.setup(request)
data = json.loads(response.get(request).content)["data"]
self.assertEqual(len(data), 11)
def test_view_cc_1_search_contains(self):
self.get_params["columns[1][columnControl][search][value]"] = "100"
self.get_params["columns[1][columnControl][search][logic]"] = "contains"
self.get_params["columns[1][columnControl][search][type]"] = "text"
self.get_params["length"] = "20"
self.client.force_login(self.user)
request = self.factory.get('/fake-url/', data=self.get_params)
response = TestView()
response.setup(request)
data = json.loads(response.get(request).content)["data"]
self.assertEqual(len(data), 9)
def test_view_cc_1_search_equal(self):
self.get_params["columns[1][columnControl][search][value]"] = "1001 - Test Character - 1001"
self.get_params["columns[1][columnControl][search][logic]"] = "equal"
self.get_params["columns[1][columnControl][search][type]"] = "text"
self.get_params["length"] = "20"
self.client.force_login(self.user)
request = self.factory.get('/fake-url/', data=self.get_params)
response = TestView()
response.setup(request)
data = json.loads(response.get(request).content)["data"]
self.assertEqual(len(data), 1)
def test_view_cc_1_search_not_equal(self):
self.get_params["columns[1][columnControl][search][value]"] = "1001 - Test Character - 1001"
self.get_params["columns[1][columnControl][search][logic]"] = "notEqual"
self.get_params["columns[1][columnControl][search][type]"] = "text"
self.get_params["length"] = "20"
self.client.force_login(self.user)
request = self.factory.get('/fake-url/', data=self.get_params)
response = TestView()
response.setup(request)
data = json.loads(response.get(request).content)["data"]
self.assertEqual(len(data), 19)

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-21 13:44+1000\n" "POT-Creation-Date: 2025-11-13 10:19+1000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -57,7 +57,7 @@ msgid "You are not allowed to add or remove these restricted groups: %s"
msgstr "" msgstr ""
#: allianceauth/authentication/models.py:72 #: allianceauth/authentication/models.py:72
#: allianceauth/project_template/project_name/settings/base.py:104 #: allianceauth/project_template/project_name/settings/base.py:105
msgid "English" msgid "English"
msgstr "" msgstr ""
@@ -66,57 +66,57 @@ msgid "Czech"
msgstr "" msgstr ""
#: allianceauth/authentication/models.py:74 #: allianceauth/authentication/models.py:74
#: allianceauth/project_template/project_name/settings/base.py:106 #: allianceauth/project_template/project_name/settings/base.py:107
msgid "German" msgid "German"
msgstr "" msgstr ""
#: allianceauth/authentication/models.py:75 #: allianceauth/authentication/models.py:75
#: allianceauth/project_template/project_name/settings/base.py:107 #: allianceauth/project_template/project_name/settings/base.py:108
msgid "Spanish" msgid "Spanish"
msgstr "" msgstr ""
#: allianceauth/authentication/models.py:76 #: allianceauth/authentication/models.py:76
#: allianceauth/project_template/project_name/settings/base.py:108 #: allianceauth/project_template/project_name/settings/base.py:109
msgid "Italian" msgid "Italian"
msgstr "" msgstr ""
#: allianceauth/authentication/models.py:77 #: allianceauth/authentication/models.py:77
#: allianceauth/project_template/project_name/settings/base.py:109 #: allianceauth/project_template/project_name/settings/base.py:110
msgid "Japanese" msgid "Japanese"
msgstr "" msgstr ""
#: allianceauth/authentication/models.py:78 #: allianceauth/authentication/models.py:78
#: allianceauth/project_template/project_name/settings/base.py:110 #: allianceauth/project_template/project_name/settings/base.py:111
msgid "Korean" msgid "Korean"
msgstr "" msgstr ""
#: allianceauth/authentication/models.py:79 #: allianceauth/authentication/models.py:79
#: allianceauth/project_template/project_name/settings/base.py:111 #: allianceauth/project_template/project_name/settings/base.py:112
msgid "French" msgid "French"
msgstr "" msgstr ""
#: allianceauth/authentication/models.py:80 #: allianceauth/authentication/models.py:80
#: allianceauth/project_template/project_name/settings/base.py:114 #: allianceauth/project_template/project_name/settings/base.py:115
msgid "Russian" msgid "Russian"
msgstr "" msgstr ""
#: allianceauth/authentication/models.py:81 #: allianceauth/authentication/models.py:81
#: allianceauth/project_template/project_name/settings/base.py:112 #: allianceauth/project_template/project_name/settings/base.py:113
msgid "Dutch" msgid "Dutch"
msgstr "" msgstr ""
#: allianceauth/authentication/models.py:82 #: allianceauth/authentication/models.py:82
#: allianceauth/project_template/project_name/settings/base.py:113 #: allianceauth/project_template/project_name/settings/base.py:114
msgid "Polish" msgid "Polish"
msgstr "" msgstr ""
#: allianceauth/authentication/models.py:83 #: allianceauth/authentication/models.py:83
#: allianceauth/project_template/project_name/settings/base.py:115 #: allianceauth/project_template/project_name/settings/base.py:116
msgid "Ukrainian" msgid "Ukrainian"
msgstr "" msgstr ""
#: allianceauth/authentication/models.py:84 #: allianceauth/authentication/models.py:84
#: allianceauth/project_template/project_name/settings/base.py:116 #: allianceauth/project_template/project_name/settings/base.py:117
msgid "Simplified Chinese" msgid "Simplified Chinese"
msgstr "" msgstr ""
@@ -125,22 +125,30 @@ msgstr ""
msgid "Language" msgid "Language"
msgstr "" msgstr ""
#: allianceauth/authentication/models.py:105 #: allianceauth/authentication/models.py:106
#: allianceauth/templates/allianceauth/night-toggle.html:6 #: allianceauth/templates/allianceauth/night-toggle.html:6
msgid "Night Mode" msgid "Night Mode"
msgstr "" msgstr ""
#: allianceauth/authentication/models.py:109 #: allianceauth/authentication/models.py:110
#: allianceauth/theme/templates/theme/theme_select.html:4 #: allianceauth/theme/templates/theme/theme_select.html:4
msgid "Theme" msgid "Theme"
msgstr "" msgstr ""
#: allianceauth/authentication/models.py:126 #: allianceauth/authentication/models.py:117
msgid "Minimize Sidebar Menu"
msgstr ""
#: allianceauth/authentication/models.py:119
msgid "Keep the sidebar menu minimized"
msgstr ""
#: allianceauth/authentication/models.py:133
#, python-format #, python-format
msgid "State changed to: %s" msgid "State changed to: %s"
msgstr "" msgstr ""
#: allianceauth/authentication/models.py:127 #: allianceauth/authentication/models.py:134
#, python-format #, python-format
msgid "Your user's state is now: %(state)s" msgid "Your user's state is now: %(state)s"
msgstr "" msgstr ""
@@ -200,8 +208,8 @@ msgstr ""
#: allianceauth/authentication/templates/authentication/tokens.html:7 #: allianceauth/authentication/templates/authentication/tokens.html:7
#: allianceauth/authentication/templates/authentication/tokens.html:11 #: allianceauth/authentication/templates/authentication/tokens.html:11
#: allianceauth/menu/templates/menu/menu-user.html:133 #: allianceauth/menu/templates/menu/menu-user.html:158
#: allianceauth/menu/templates/menu/menu-user.html:136 #: allianceauth/menu/templates/menu/menu-user.html:161
#: allianceauth/templates/allianceauth/top-menu-user-dropdown.html:62 #: allianceauth/templates/allianceauth/top-menu-user-dropdown.html:62
msgid "Token Management" msgid "Token Management"
msgstr "" msgstr ""
@@ -747,8 +755,8 @@ msgstr ""
#: allianceauth/fleetactivitytracking/views.py:323 #: allianceauth/fleetactivitytracking/views.py:323
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"Cannot register the fleet participation for {character.character_name}. The " "Cannot register the fleet participation for {character_name}. The character "
"character needs to be online." "needs to be online."
msgstr "" msgstr ""
#: allianceauth/framework/apps.py:16 #: allianceauth/framework/apps.py:16
@@ -1486,37 +1494,45 @@ msgid "Notifications"
msgstr "" msgstr ""
#: allianceauth/menu/templates/menu/menu-user.html:77 #: allianceauth/menu/templates/menu/menu-user.html:77
msgid "Sidebar"
msgstr ""
#: allianceauth/menu/templates/menu/menu-user.html:93
msgid "Minimize Sidebar"
msgstr ""
#: allianceauth/menu/templates/menu/menu-user.html:102
msgid "Super User" msgid "Super User"
msgstr "" msgstr ""
#: allianceauth/menu/templates/menu/menu-user.html:83 #: allianceauth/menu/templates/menu/menu-user.html:108
#: allianceauth/menu/templates/menu/menu-user.html:86 #: allianceauth/menu/templates/menu/menu-user.html:111
msgid "Alliance Auth Documentation" msgid "Alliance Auth Documentation"
msgstr "" msgstr ""
#: allianceauth/menu/templates/menu/menu-user.html:94 #: allianceauth/menu/templates/menu/menu-user.html:119
#: allianceauth/menu/templates/menu/menu-user.html:97 #: allianceauth/menu/templates/menu/menu-user.html:122
msgid "Alliance Auth Discord" msgid "Alliance Auth Discord"
msgstr "" msgstr ""
#: allianceauth/menu/templates/menu/menu-user.html:105 #: allianceauth/menu/templates/menu/menu-user.html:130
#: allianceauth/menu/templates/menu/menu-user.html:108 #: allianceauth/menu/templates/menu/menu-user.html:133
msgid "Alliance Auth Git" msgid "Alliance Auth Git"
msgstr "" msgstr ""
#: allianceauth/menu/templates/menu/menu-user.html:118 #: allianceauth/menu/templates/menu/menu-user.html:143
#: allianceauth/menu/templates/menu/menu-user.html:121 #: allianceauth/menu/templates/menu/menu-user.html:146
#: allianceauth/templates/allianceauth/top-menu-admin.html:9 #: allianceauth/templates/allianceauth/top-menu-admin.html:9
msgid "Admin" msgid "Admin"
msgstr "" msgstr ""
#: allianceauth/menu/templates/menu/menu-user.html:144 #: allianceauth/menu/templates/menu/menu-user.html:169
#: allianceauth/menu/templates/menu/menu-user.html:147 #: allianceauth/menu/templates/menu/menu-user.html:172
msgid "Sign Out" msgid "Sign Out"
msgstr "" msgstr ""
#: allianceauth/menu/templates/menu/menu-user.html:155 #: allianceauth/menu/templates/menu/menu-user.html:180
#: allianceauth/menu/templates/menu/menu-user.html:158 #: allianceauth/menu/templates/menu/menu-user.html:183
#: allianceauth/templates/allianceauth/top-menu-rh-default.html:13 #: allianceauth/templates/allianceauth/top-menu-rh-default.html:13
#: allianceauth/templates/allianceauth/top-menu-rh-default.html:14 #: allianceauth/templates/allianceauth/top-menu-rh-default.html:14
msgid "Sign In" msgid "Sign In"
@@ -1777,9 +1793,9 @@ msgstr ""
msgid "That service account already exists" msgid "That service account already exists"
msgstr "" msgstr ""
#: allianceauth/services/abstract.py:103 #: allianceauth/services/abstract.py:105
#, python-brace-format #, python-brace-format
msgid "Successfully set your {self.service_name} password" msgid "Successfully set your {service_name} password"
msgstr "" msgstr ""
#: allianceauth/services/apps.py:8 allianceauth/services/auth_hooks.py:12 #: allianceauth/services/apps.py:8 allianceauth/services/auth_hooks.py:12
@@ -2318,11 +2334,11 @@ msgstr ""
msgid "Invalid Link. Please use zkillboard.com or kb.evetools.org" msgid "Invalid Link. Please use zkillboard.com or kb.evetools.org"
msgstr "" msgstr ""
#: allianceauth/srp/form.py:46 #: allianceauth/srp/form.py:49
msgid "Invalid Link. Please post a direct link to a killmail." msgid "Invalid Link. Please post a direct link to a killmail."
msgstr "" msgstr ""
#: allianceauth/srp/form.py:53 #: allianceauth/srp/form.py:56
msgid "After Action Report Link" msgid "After Action Report Link"
msgstr "" msgstr ""
@@ -2596,7 +2612,7 @@ msgstr ""
msgid "Saved changes to SRP fleet %(fleetname)s" msgid "Saved changes to SRP fleet %(fleetname)s"
msgstr "" msgstr ""
#: allianceauth/templates/allianceauth/admin-status/esi_check.html:4 #: allianceauth/templates/allianceauth/admin-status/esi_check.html:5
msgid "Your Server received an ESI error response code of " msgid "Your Server received an ESI error response code of "
msgstr "" msgstr ""
@@ -3001,36 +3017,36 @@ msgstr ""
msgid "Saved changes to the timer." msgid "Saved changes to the timer."
msgstr "" msgstr ""
#: allianceauth/views.py:55 #: allianceauth/views.py:78
msgid "Bad Request" msgid "Bad Request"
msgstr "" msgstr ""
#: allianceauth/views.py:57 allianceauth/views.py:87 #: allianceauth/views.py:80 allianceauth/views.py:110
msgid "" msgid ""
"Auth encountered an error processing your request, please try again. If the " "Auth encountered an error processing your request, please try again. If the "
"error persists, please contact the administrators." "error persists, please contact the administrators."
msgstr "" msgstr ""
#: allianceauth/views.py:65 #: allianceauth/views.py:88
msgid "Permission Denied" msgid "Permission Denied"
msgstr "" msgstr ""
#: allianceauth/views.py:67 #: allianceauth/views.py:90
msgid "" msgid ""
"You do not have permission to access the requested page. If you believe this " "You do not have permission to access the requested page. If you believe this "
"is in error please contact the administrators." "is in error please contact the administrators."
msgstr "" msgstr ""
#: allianceauth/views.py:75 #: allianceauth/views.py:98
msgid "Page Not Found" msgid "Page Not Found"
msgstr "" msgstr ""
#: allianceauth/views.py:77 #: allianceauth/views.py:100
msgid "" msgid ""
"Page does not exist. If you believe this is in error please contact the " "Page does not exist. If you believe this is in error please contact the "
"administrators. " "administrators. "
msgstr "" msgstr ""
#: allianceauth/views.py:85 #: allianceauth/views.py:108
msgid "Internal Server Error" msgid "Internal Server Error"
msgstr "" msgstr ""

View File

@@ -1,4 +1,4 @@
from __future__ import annotations from __future__ import annotations # Still needed for Python 3.8, replaced with better implementations in Py39+
import logging import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING

View File

@@ -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 %} {% 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 %} {% 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 %} {% 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 %} {% endblock %}

View File

@@ -57,10 +57,10 @@ DATABASES['default'] = {
# CCP's developer portal # CCP's developer portal
# Logging in to auth requires the publicData scope (can be overridden through the # 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). # 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_ID = ''
ESI_SSO_CLIENT_SECRET = '' 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. # 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. # It's recommended to use a free service like SparkPost or Elastic Email to send email.

View File

@@ -11,10 +11,10 @@ environment =
[program:beat] [program:beat]
command = %(ENV_AA_COMMAND_CELERY)s command = %(ENV_AA_COMMAND_CELERY)s
-A %(ENV_AA_PROJECT_NAME)s beat -A %(ENV_AA_PROJECT_NAME)s beat
directory = %(ENV_AA_VENV_DIRECTORY)s directory = %(ENV_AA_PROJECT_DIRECTORY)s
user = %(ENV_AA_USER)s user = %(ENV_AA_USER)s
stdout_logfile = %(ENV_AA_VENV_DIRECTORY)s/log/%(program_name)s.log stdout_logfile = %(ENV_AA_PROJECT_DIRECTORY)s/log/%(program_name)s.log
stderr_logfile = %(ENV_AA_VENV_DIRECTORY)s/log/%(program_name)s.log stderr_logfile = %(ENV_AA_PROJECT_DIRECTORY)s/log/%(program_name)s.log
autostart = true autostart = true
autorestart = true autorestart = true
startsecs = 10 startsecs = 10
@@ -26,12 +26,12 @@ command = %(ENV_AA_COMMAND_CELERY)s
--pool=threads --pool=threads
--concurrency=5 --concurrency=5
-n %(program_name)s_%(process_num)02d -n %(program_name)s_%(process_num)02d
directory = %(ENV_AA_VENV_DIRECTORY)s directory = %(ENV_AA_PROJECT_DIRECTORY)s
user = %(ENV_AA_USER)s user = %(ENV_AA_USER)s
numprocs = 1 numprocs = 1
process_name = %(program_name)s_%(process_num)02d process_name = %(program_name)s_%(process_num)02d
stdout_logfile = %(ENV_AA_VENV_DIRECTORY)s/log/%(program_name)s.log stdout_logfile = %(ENV_AA_PROJECT_DIRECTORY)s/log/%(program_name)s.log
stderr_logfile = %(ENV_AA_VENV_DIRECTORY)s/log/%(program_name)s.log stderr_logfile = %(ENV_AA_PROJECT_DIRECTORY)s/log/%(program_name)s.log
autostart = true autostart = true
autorestart = true autorestart = true
startsecs = 10 startsecs = 10
@@ -42,12 +42,12 @@ priority = 998
{% if gunicorn %} {% if gunicorn %}
[program:gunicorn] [program:gunicorn]
user = %(ENV_AA_USER)s user = %(ENV_AA_USER)s
directory = %(ENV_AA_VENV_DIRECTORY)s directory = %(ENV_AA_PROJECT_DIRECTORY)s
command = %(ENV_AA_COMMAND_GUNICORN)s %(ENV_AA_PROJECT_NAME)s.wsgi command = %(ENV_AA_COMMAND_GUNICORN)s %(ENV_AA_PROJECT_NAME)s.wsgi
--workers=3 --workers=3
--timeout 120 --timeout 120
stdout_logfile = %(ENV_AA_VENV_DIRECTORY)s/log/%(program_name)s.log stdout_logfile = %(ENV_AA_PROJECT_DIRECTORY)s/log/%(program_name)s.log
stderr_logfile = %(ENV_AA_VENV_DIRECTORY)s/log/%(program_name)s.log stderr_logfile = %(ENV_AA_PROJECT_DIRECTORY)s/log/%(program_name)s.log
autostart = true autostart = true
autorestart = true autorestart = true
stopsignal = INT stopsignal = INT
@@ -57,10 +57,10 @@ stopsignal = INT
command = %(ENV_AA_COMMAND_MEMMON)s command = %(ENV_AA_COMMAND_MEMMON)s
-p worker_00=256MB -p worker_00=256MB
-p gunicorn=256MB -p gunicorn=256MB
directory = %(ENV_AA_VENV_DIRECTORY)s directory = %(ENV_AA_PROJECT_DIRECTORY)s
events = TICK_60 events = TICK_60
stdout_logfile = %(ENV_AA_VENV_DIRECTORY)s/log/memmon.log stdout_logfile = %(ENV_AA_PROJECT_DIRECTORY)s/log/memmon.log
stderr_logfile = %(ENV_AA_VENV_DIRECTORY)s/log/memmon.log stderr_logfile = %(ENV_AA_PROJECT_DIRECTORY)s/log/memmon.log
[group:{{ project_name }}] [group:{{ project_name }}]
programs = beat,worker{% if gunicorn %},gunicorn{% endif %} programs = beat,worker{% if gunicorn %},gunicorn{% endif %}

View File

@@ -100,7 +100,10 @@ class BaseSetPasswordServiceAccountView(ServicesCRUDMixin, BaseServiceView, Upda
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
result = super().post(request, *args, **kwargs) result = super().post(request, *args, **kwargs)
if self.get_form().is_valid(): if self.get_form().is_valid():
messages.success(request, _(f"Successfully set your {self.service_name} password")) messages.success(
request,
_("Successfully set your {service_name} password").format(service_name=self.service_name)
)
return result return result

View File

@@ -111,11 +111,11 @@ class Teamspeak3Manager:
outlist = {} outlist = {}
if type(groups) == list: if type(groups) == list:
logger.debug("Recieved multiple groups. Iterating.") logger.debug("Received multiple groups. Iterating.")
for group in groups: for group in groups:
outlist[group['keys']['name']] = group['keys']['sgid'] outlist[group['keys']['name']] = group['keys']['sgid']
elif type(groups) == dict: elif type(groups) == dict:
logger.debug("Recieved single group.") logger.debug("Received single group.")
outlist[groups['keys']['name']] = groups['keys']['sgid'] outlist[groups['keys']['name']] = groups['keys']['sgid']
logger.debug("Returning name/id pairing: %s" % outlist) logger.debug("Returning name/id pairing: %s" % outlist)
return outlist return outlist

View File

@@ -34,6 +34,9 @@ class SrpFleetUserRequestForm(forms.Form):
_("Invalid Link. Please use zkillboard.com or kb.evetools.org") _("Invalid Link. Please use zkillboard.com or kb.evetools.org")
) )
if re.match(r"^http[s]?:\/\/zkillboard\.com\/", data) and not data.endswith("/"):
data += "/"
# Check if it's an actual kill mail # Check if it's an actual kill mail
if not any( if not any(
re.match(regex, data) re.match(regex, data)

View File

@@ -0,0 +1,27 @@
from django.test import TestCase
from allianceauth.srp.form import SrpFleetUserRequestForm
class TestForms(TestCase):
def test_allow_missing_trailing_slash_zkillboard(self):
form = SrpFleetUserRequestForm(
data = {
"killboard_link": "https://zkillboard.com/kill/130429493",
"additional_info": "Details",
}
)
form.cleaned_data = {"killboard_link": "https://zkillboard.com/kill/130429493"}
self.assertEqual("https://zkillboard.com/kill/130429493/", form.clean_killboard_link())
def test_not_add_trailling_slash_kb_evetools(self):
form = SrpFleetUserRequestForm(
data = {
"killboard_link": "https://kb.evetools.org/kill/130429493",
}
)
form.cleaned_data = {"killboard_link": "https://kb.evetools.org/kill/130429493"}
self.assertEqual("https://kb.evetools.org/kill/130429493", form.clean_killboard_link())

View File

@@ -1,13 +0,0 @@
$(document).ready(() => {
'use strict';
const activeChildMenuItem = document.querySelector('ul#sidebar-menu ul.collapse a.active');
if (activeChildMenuItem) {
const activeChildMenuUl = activeChildMenuItem.closest('ul');
activeChildMenuUl.classList.add('show');
document.querySelectorAll(`[data-bs-target^="#${activeChildMenuUl.id}"]`)
.forEach(element => element.setAttribute('aria-expanded', true));
}
});

View File

@@ -0,0 +1,57 @@
$(document).ready(() => {
'use strict';
/**
* Collect all badges in the sidebar menu that are not part of a collapsible submenu, and calculate the total notification count.
* Show a total notification badge in the navbar if there are any notifications.
*/
const totalNotificationsBadge = () => {
const badges = [];
let notificationCount = 0;
document.querySelectorAll('#sidebar-menu .badge').forEach(b => {
const li = b.closest('li');
if (!li || !li.querySelector('ul.collapse')) {
badges.push(b);
notificationCount += parseInt(b.textContent);
}
});
if (badges.length > 0 && notificationCount > 0) {
const notificationBadge = document.createElement('span');
notificationBadge.classList.add(
'badge',
'text-bg-danger',
'align-self-center',
'sidemenu-notification-badge',
'sidemenu-total-notifications-badge'
);
notificationBadge.textContent = String(notificationCount);
document.querySelector('a.navbar-brand i.fa-solid').prepend(notificationBadge);
}
};
/**
* Find the active child menu item in the sidebar menu, if any, and ensure its parent submenu is expanded.
*/
const expandChildMenu = () => {
const activeChildMenuItem = document.querySelector('ul#sidebar-menu ul.collapse a.active');
if (activeChildMenuItem) {
const activeChildMenuUl = activeChildMenuItem.closest('ul');
activeChildMenuUl.classList.add('show');
document.querySelectorAll(`[data-bs-target^="#${activeChildMenuUl.id}"]`)
.forEach(element => element.setAttribute('aria-expanded', 'true'));
}
};
// Execute functions on document ready
[
totalNotificationsBadge,
expandChildMenu
].forEach(fn => fn());
});

View File

@@ -1,3 +1,3 @@
{% load sri %} {% load sri %}
{% sri_static 'allianceauth/js/sidebar-collapse.js' %} {% sri_static 'allianceauth/js/sidebar.js' %}

View File

@@ -0,0 +1,11 @@
{# Template override to display the errors of django Forms when using `boostrap_form` form the `django-bootstrap5` #}
{# library. #}
{# See: https://github.com/zostera/django-bootstrap5/pull/767 #}
{% if field_errors %}
<div id="{{ field.auto_id }}_error" class="invalid-feedback">
{% for text in field_errors %}
<div>{{ text }}</div>
{% endfor %}
</div>
{% endif %}

View File

@@ -1,7 +1,7 @@
PROTOCOL=https:// PROTOCOL=https://
AUTH_SUBDOMAIN=%AUTH_SUBDOMAIN% AUTH_SUBDOMAIN=%AUTH_SUBDOMAIN%
DOMAIN=%DOMAIN% DOMAIN=%DOMAIN%
AA_DOCKER_TAG=registry.gitlab.com/allianceauth/allianceauth/auth:v4.11.0 AA_DOCKER_TAG=registry.gitlab.com/allianceauth/allianceauth/auth:v4.11.2
# Nginx Proxy Manager # Nginx Proxy Manager
PROXY_HTTP_PORT=80 PROXY_HTTP_PORT=80

View File

@@ -1,5 +1,5 @@
FROM python:3.11-slim FROM python:3.11-slim
ARG AUTH_VERSION=v4.11.0 ARG AUTH_VERSION=v4.11.2
ARG AUTH_PACKAGE=allianceauth==${AUTH_VERSION} ARG AUTH_PACKAGE=allianceauth==${AUTH_VERSION}
ENV AUTH_USER=allianceauth ENV AUTH_USER=allianceauth
ENV AUTH_GROUP=allianceauth ENV AUTH_GROUP=allianceauth

View File

@@ -17,9 +17,7 @@ DATABASES["default"] = {
"PASSWORD": os.environ.get("AA_DB_PASSWORD"), "PASSWORD": os.environ.get("AA_DB_PASSWORD"),
"HOST": os.environ.get("AA_DB_HOST"), "HOST": os.environ.get("AA_DB_HOST"),
"PORT": os.environ.get("AA_DB_PORT", "3306"), "PORT": os.environ.get("AA_DB_PORT", "3306"),
"OPTIONS": { "OPTIONS": {"charset": os.environ.get("AA_DB_CHARSET", "utf8mb4")},
"charset": os.environ.get("AA_DB_CHARSET", "utf8mb4")
}
} }
# Register an application at https://developers.eveonline.com for Authentication # 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 # 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 # 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). # 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_ID = os.environ.get("ESI_SSO_CLIENT_ID")
ESI_SSO_CLIENT_SECRET = os.environ.get("ESI_SSO_CLIENT_SECRET") 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 = os.environ.get(
"ESI_USER_CONTACT_EMAIL" "ESI_USER_CONTACT_EMAIL"
) # A server maintainer that CCP can contact in case of issues. ) # A server maintainer that CCP can contact in case of issues.
@@ -70,7 +67,6 @@ INSTALLED_APPS += [
# 'allianceauth.permissions_tool', # 'allianceauth.permissions_tool',
# 'allianceauth.srp', # 'allianceauth.srp',
# 'allianceauth.timerboard', # 'allianceauth.timerboard',
# https://allianceauth.readthedocs.io/en/latest/features/services/index.html # https://allianceauth.readthedocs.io/en/latest/features/services/index.html
# 'allianceauth.services.modules.discord', # 'allianceauth.services.modules.discord',
# 'allianceauth.services.modules.discourse', # 'allianceauth.services.modules.discourse',

View File

@@ -13,6 +13,7 @@ The Alliance Auth framework is split into several submodules, each of which is d
framework/api framework/api
framework/css framework/css
framework/datatables
framework/js framework/js
framework/templates framework/templates
framework/svg-sprite framework/svg-sprite

View File

@@ -0,0 +1,181 @@
# 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
```django
{% load evelinks %}
{% character_portrait_url row 32 %}
```
### template/appname/stubs/name.html
```django
{{ row.character_name }} <span class="text-small">({{ row.character_ownership.user.username }})</span>
```
### template/appname/stubs/corp.html
```django
{{ row.corporation_name }}
```
### template/appname/list.html
```django
{% 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>
</table>
{% endblock content %}
{% block extra_css %}
<link rel="stylesheet" href="https://cdn.datatables.net/2.3.6/css/dataTables.bootstrap5.css" />
<link href="https://cdn.datatables.net/columncontrol/1.2.0/css/columnControl.bootstrap5.min.css" rel="stylesheet">
{% endblock %}
{% block extra_javascript %}
<script src="https://cdn.datatables.net/2.3.6/js/dataTables.js"></script>
<script src="https://cdn.datatables.net/2.3.6/js/dataTables.bootstrap5.js"></script>
<script src="https://cdn.datatables.net/columncontrol/1.2.0/js/dataTables.columnControl.min.js"></script>
<script>
$(document).ready(function() {
$('#table').DataTable({
serverSide: true,
ajax: '{% url "appname:table" %}',
columnDefs: [
{
targets: [0],
columnControl: [],
sortable: false,
searchable: false
},
{
targets: [1,2,3],
columnControl: [
{
target: 0,
content: []
},
{
target: 1,
content: ['search']
}
],
}
],
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.
### Columns definition
The `columns` must be defined as a 2 part tuple
- Part 1 is the database field that will be used for filtering and ordering. If this is a foreign key you need to point to a field that is compatible with `__icontains` like `charField` or `textField`. It can be `None`/`False`/`""` if no ordering for filtering is required for this row.
- Examples for the EveCharacter Model:
- `character_name`
- `character_ownership__user__username`
- `character_ownership__user__profile__main_character__character_name`
- Part 2 is a string that is used to the render the column for each row. This can be a html stub or a string containing django style template language.
- Examples for the EveCharacter Model
- `{{ row.character_name }}`
- `{{ row.character_ownership.user.username }}`
- `{{ row.character_ownership.user.profile.main_character.character_name }}`
- `appname/stubs/character_img.html`
### appname/views.py
```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
# Define the columns as a tuple.
# String for field name for filtering and ordering
# String for the render template
# Templates can be a html file or template language directly in the list below
columns = [
# ("field_for_queries_or_sort", template: str)
("", "appname/stubs/icon.html"),
("character_name", "appname/stubs/name.html"),
("corporation_name", "appname/stubs/corp.html"),
("alliance_name", "{{ row.alliance_name }} {{ row.alliance_id }}"),
]
# 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.

View File

@@ -28,6 +28,7 @@ The following icons are available in the Alliance Auth SVG sprite:
- `aa-logo`: The Alliance Auth logo - `aa-logo`: The Alliance Auth logo
- `aa-loading-spinner`: A loading spinner icon - `aa-loading-spinner`: A loading spinner icon
- `aa-mumble-logo`: The Mumble logo
### Alliance Auth Logo ### Alliance Auth Logo

View File

@@ -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/ # Be sure to set the callback URLto https://example.com/discord/callback/
# substituting your domain for example.com in Discord's developer portal # substituting your domain for example.com in Discord's developer portal
# (Be sure to add the trailing slash) # (Be sure to add the trailing slash)
DISCORD_CALLBACK_URL = f"{SITE_URL}/discord/callback/" # Do NOT change this line!
DISCORD_GUILD_ID = '' DISCORD_GUILD_ID = ''
DISCORD_CALLBACK_URL = f"{SITE_URL}/discord/callback/"
DISCORD_APP_ID = '' DISCORD_APP_ID = ''
DISCORD_APP_SECRET = '' DISCORD_APP_SECRET = ''
DISCORD_BOT_TOKEN = '' DISCORD_BOT_TOKEN = ''

View File

@@ -15,7 +15,7 @@ license = { file = "LICENSE" }
authors = [ authors = [
{ name = "Alliance Auth", email = "adarnof@gmail.com" }, { name = "Alliance Auth", email = "adarnof@gmail.com" },
] ]
requires-python = ">=3.8" requires-python = ">=3.8,<3.13"
classifiers = [ classifiers = [
"Environment :: Web Environment", "Environment :: Web Environment",
"Framework :: Celery", "Framework :: Celery",
@@ -31,8 +31,6 @@ classifiers = [
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
] ]