mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2026-02-11 09:36:24 +01:00
Compare commits
57 Commits
v4.11.0
...
f0815a40f7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0815a40f7 | ||
|
|
6a8ff6ca26 | ||
|
|
9300987a4d | ||
|
|
7b661a0f42 | ||
|
|
4799e0beb1 | ||
|
|
a4cce84f67 | ||
|
|
edf5b7bb8c | ||
|
|
7e338d09a2 | ||
|
|
775db62c7a | ||
|
|
6c2cbb069f | ||
|
|
a01598e088 | ||
|
|
e44802fa73 | ||
|
|
ad1c17a255 | ||
|
|
7eb0a6c473 | ||
|
|
d36e3a1256 | ||
|
|
a5c09e0fc7 | ||
|
|
fb7ae95a8a | ||
|
|
de43114681 | ||
|
|
aec055b542 | ||
|
|
9ed2e97068 | ||
|
|
4d30a59c9c | ||
|
|
1c7c775029 | ||
|
|
d4e3addd6c | ||
|
|
940f6b14b7 | ||
|
|
99c65d2a5d | ||
|
|
55125a8ff3 | ||
|
|
2fd0fcdbcb | ||
|
|
2fe7bcf20e | ||
|
|
70f314e578 | ||
|
|
bc1b1c3a8f | ||
|
|
453512db64 | ||
|
|
4047159fd1 | ||
|
|
f5ddbb8004 | ||
|
|
c45d5d7325 | ||
|
|
c679ec0646 | ||
|
|
69a51bb08a | ||
|
|
442c893c39 | ||
|
|
33df15f882 | ||
|
|
5d56f0a66c | ||
|
|
0fbdbf3a8d | ||
|
|
5291bf6896 | ||
|
|
bd6c0fede5 | ||
|
|
23fe1703c3 | ||
|
|
9139b0da56 | ||
|
|
80145b313f | ||
|
|
e96bdd12f9 | ||
|
|
513b7b88f4 | ||
|
|
5e3fc5c1cb | ||
|
|
d3069db046 | ||
|
|
24863eaf9e | ||
|
|
d138bd61c5 | ||
|
|
bff20ddd5d | ||
|
|
493e694410 | ||
|
|
8c41d9da58 | ||
|
|
9e0358a3ce | ||
|
|
19bb6856a2 | ||
|
|
9751315b97 |
@@ -25,7 +25,7 @@ before_script:
|
||||
pre-commit-check:
|
||||
<<: *only-default
|
||||
stage: pre-commit
|
||||
image: python:3.11-bookworm
|
||||
image: python:3.11-trixie
|
||||
# variables:
|
||||
# PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
|
||||
# cache:
|
||||
@@ -65,7 +65,7 @@ test-3.8-core:
|
||||
|
||||
test-3.9-core:
|
||||
<<: *only-default
|
||||
image: python:3.9-bookworm
|
||||
image: python:3.9-trixie
|
||||
script:
|
||||
- tox -e py39-core
|
||||
artifacts:
|
||||
@@ -77,7 +77,7 @@ test-3.9-core:
|
||||
|
||||
test-3.10-core:
|
||||
<<: *only-default
|
||||
image: python:3.10-bookworm
|
||||
image: python:3.10-trixie
|
||||
script:
|
||||
- tox -e py310-core
|
||||
artifacts:
|
||||
@@ -89,7 +89,7 @@ test-3.10-core:
|
||||
|
||||
test-3.11-core:
|
||||
<<: *only-default
|
||||
image: python:3.11-bookworm
|
||||
image: python:3.11-trixie
|
||||
script:
|
||||
- tox -e py311-core
|
||||
artifacts:
|
||||
@@ -101,7 +101,7 @@ test-3.11-core:
|
||||
|
||||
test-3.12-core:
|
||||
<<: *only-default
|
||||
image: python:3.12-bookworm
|
||||
image: python:3.12-trixie
|
||||
script:
|
||||
- tox -e py312-core
|
||||
artifacts:
|
||||
@@ -125,7 +125,7 @@ test-3.8-all:
|
||||
|
||||
test-3.9-all:
|
||||
<<: *only-default
|
||||
image: python:3.9-bookworm
|
||||
image: python:3.9-trixie
|
||||
script:
|
||||
- tox -e py39-all
|
||||
artifacts:
|
||||
@@ -137,7 +137,7 @@ test-3.9-all:
|
||||
|
||||
test-3.10-all:
|
||||
<<: *only-default
|
||||
image: python:3.10-bookworm
|
||||
image: python:3.10-trixie
|
||||
script:
|
||||
- tox -e py310-all
|
||||
artifacts:
|
||||
@@ -149,7 +149,7 @@ test-3.10-all:
|
||||
|
||||
test-3.11-all:
|
||||
<<: *only-default
|
||||
image: python:3.11-bookworm
|
||||
image: python:3.11-trixie
|
||||
script:
|
||||
- tox -e py311-all
|
||||
artifacts:
|
||||
@@ -162,7 +162,7 @@ test-3.11-all:
|
||||
|
||||
test-3.12-all:
|
||||
<<: *only-default
|
||||
image: python:3.12-bookworm
|
||||
image: python:3.12-trixie
|
||||
script:
|
||||
- tox -e py312-all
|
||||
artifacts:
|
||||
@@ -174,7 +174,7 @@ test-3.12-all:
|
||||
|
||||
build-test:
|
||||
stage: test
|
||||
image: python:3.11-bookworm
|
||||
image: python:3.11-trixie
|
||||
|
||||
before_script:
|
||||
- python -m pip install --upgrade pip
|
||||
@@ -193,13 +193,13 @@ build-test:
|
||||
|
||||
test-docs:
|
||||
<<: *only-default
|
||||
image: python:3.11-bookworm
|
||||
image: python:3.11-trixie
|
||||
script:
|
||||
- tox -e docs
|
||||
|
||||
deploy_production:
|
||||
stage: deploy
|
||||
image: python:3.11-bookworm
|
||||
image: python:3.11-trixie
|
||||
|
||||
before_script:
|
||||
- python -m pip install --upgrade pip
|
||||
|
||||
@@ -25,12 +25,12 @@ exclude: |
|
||||
repos:
|
||||
# Code Upgrades
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.21.0
|
||||
rev: v3.21.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py38-plus]
|
||||
- repo: https://github.com/adamchainz/django-upgrade
|
||||
rev: 1.29.0
|
||||
rev: 1.29.1
|
||||
hooks:
|
||||
- id: django-upgrade
|
||||
args: [--target-version=4.2]
|
||||
@@ -64,7 +64,7 @@ repos:
|
||||
- id: check-executables-have-shebangs
|
||||
- id: end-of-file-fixer
|
||||
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python
|
||||
rev: 3.4.0
|
||||
rev: 3.4.1
|
||||
hooks:
|
||||
- id: editorconfig-checker
|
||||
- repo: https://github.com/igorshubovych/markdownlint-cli
|
||||
@@ -76,7 +76,7 @@ repos:
|
||||
- --disable=MD013
|
||||
# Infrastructure
|
||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||
rev: v2.11.0
|
||||
rev: v2.11.1
|
||||
hooks:
|
||||
- id: pyproject-fmt
|
||||
name: pyproject.toml formatter
|
||||
@@ -84,7 +84,7 @@ repos:
|
||||
args:
|
||||
- --indent=4
|
||||
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
|
||||
rev: v0.24.1
|
||||
hooks:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ manage online service access.
|
||||
# This will make sure the app is always imported when
|
||||
# Django starts so that shared_task will use this app.
|
||||
|
||||
__version__ = '4.11.0'
|
||||
__version__ = '4.11.2'
|
||||
__title__ = 'Alliance Auth'
|
||||
__title_useragent__ = 'AllianceAuth'
|
||||
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
|
||||
|
||||
@@ -727,7 +727,8 @@ class TestEveSwaggerProvider(TestCase):
|
||||
my_provider = EveSwaggerProvider()
|
||||
my_client = my_provider.client
|
||||
operation = my_client.Universe.get_universe_factions()
|
||||
self.assertEqual(
|
||||
operation.future.request.headers['User-Agent'],
|
||||
f'AllianceAuth/{aa_version} (dummy@example.net; +{aa_url}) Django-ESI/{esi_version} (+{esi_url})'
|
||||
)
|
||||
expected_variants = {
|
||||
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})' # Django-ESI 7.x, Py38 Py39
|
||||
}
|
||||
self.assertIn(operation.future.request.headers['User-Agent'], expected_variants)
|
||||
|
||||
@@ -320,8 +320,8 @@ def click_fatlink_view(request, token, fat_hash=None):
|
||||
messages.warning(
|
||||
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')
|
||||
|
||||
209
allianceauth/framework/datatables.py
Normal file
209
allianceauth/framework/datatables.py
Normal 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)
|
||||
@@ -47,6 +47,12 @@
|
||||
/* Side Navigation
|
||||
------------------------------------------------------------------------------------- */
|
||||
@media all {
|
||||
.sidemenu-total-notifications-badge {
|
||||
position: absolute;
|
||||
left: 28px;
|
||||
font-size: 42.5% !important;
|
||||
}
|
||||
|
||||
#sidebar > div {
|
||||
width: 325px;
|
||||
}
|
||||
|
||||
269
allianceauth/framework/tests/test_datatables.py
Normal file
269
allianceauth/framework/tests/test_datatables.py
Normal 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)
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\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"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\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 ""
|
||||
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
||||
@@ -66,57 +66,57 @@ msgid "Czech"
|
||||
msgstr ""
|
||||
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
||||
@@ -125,22 +125,30 @@ msgstr ""
|
||||
msgid "Language"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/models.py:105
|
||||
#: allianceauth/authentication/models.py:106
|
||||
#: allianceauth/templates/allianceauth/night-toggle.html:6
|
||||
msgid "Night Mode"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/models.py:109
|
||||
#: allianceauth/authentication/models.py:110
|
||||
#: allianceauth/theme/templates/theme/theme_select.html:4
|
||||
msgid "Theme"
|
||||
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
|
||||
msgid "State changed to: %s"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/authentication/models.py:127
|
||||
#: allianceauth/authentication/models.py:134
|
||||
#, python-format
|
||||
msgid "Your user's state is now: %(state)s"
|
||||
msgstr ""
|
||||
@@ -200,8 +208,8 @@ msgstr ""
|
||||
|
||||
#: allianceauth/authentication/templates/authentication/tokens.html:7
|
||||
#: allianceauth/authentication/templates/authentication/tokens.html:11
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:133
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:136
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:158
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:161
|
||||
#: allianceauth/templates/allianceauth/top-menu-user-dropdown.html:62
|
||||
msgid "Token Management"
|
||||
msgstr ""
|
||||
@@ -747,8 +755,8 @@ msgstr ""
|
||||
#: allianceauth/fleetactivitytracking/views.py:323
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"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."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/framework/apps.py:16
|
||||
@@ -1486,37 +1494,45 @@ msgid "Notifications"
|
||||
msgstr ""
|
||||
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:83
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:86
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:108
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:111
|
||||
msgid "Alliance Auth Documentation"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:94
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:97
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:119
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:122
|
||||
msgid "Alliance Auth Discord"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:105
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:108
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:130
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:133
|
||||
msgid "Alliance Auth Git"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:118
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:121
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:143
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:146
|
||||
#: allianceauth/templates/allianceauth/top-menu-admin.html:9
|
||||
msgid "Admin"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:144
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:147
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:169
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:172
|
||||
msgid "Sign Out"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:155
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:158
|
||||
#: allianceauth/menu/templates/menu/menu-user.html:180
|
||||
#: 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:14
|
||||
msgid "Sign In"
|
||||
@@ -1777,9 +1793,9 @@ msgstr ""
|
||||
msgid "That service account already exists"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/services/abstract.py:103
|
||||
#: allianceauth/services/abstract.py:105
|
||||
#, python-brace-format
|
||||
msgid "Successfully set your {self.service_name} password"
|
||||
msgid "Successfully set your {service_name} password"
|
||||
msgstr ""
|
||||
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/srp/form.py:46
|
||||
#: allianceauth/srp/form.py:49
|
||||
msgid "Invalid Link. Please post a direct link to a killmail."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/srp/form.py:53
|
||||
#: allianceauth/srp/form.py:56
|
||||
msgid "After Action Report Link"
|
||||
msgstr ""
|
||||
|
||||
@@ -2596,7 +2612,7 @@ msgstr ""
|
||||
msgid "Saved changes to SRP fleet %(fleetname)s"
|
||||
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 "
|
||||
msgstr ""
|
||||
|
||||
@@ -3001,36 +3017,36 @@ msgstr ""
|
||||
msgid "Saved changes to the timer."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/views.py:55
|
||||
#: allianceauth/views.py:78
|
||||
msgid "Bad Request"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/views.py:57 allianceauth/views.py:87
|
||||
#: allianceauth/views.py:80 allianceauth/views.py:110
|
||||
msgid ""
|
||||
"Auth encountered an error processing your request, please try again. If the "
|
||||
"error persists, please contact the administrators."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/views.py:65
|
||||
#: allianceauth/views.py:88
|
||||
msgid "Permission Denied"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/views.py:67
|
||||
#: allianceauth/views.py:90
|
||||
msgid ""
|
||||
"You do not have permission to access the requested page. If you believe this "
|
||||
"is in error please contact the administrators."
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/views.py:75
|
||||
#: allianceauth/views.py:98
|
||||
msgid "Page Not Found"
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/views.py:77
|
||||
#: allianceauth/views.py:100
|
||||
msgid ""
|
||||
"Page does not exist. If you believe this is in error please contact the "
|
||||
"administrators. "
|
||||
msgstr ""
|
||||
|
||||
#: allianceauth/views.py:85
|
||||
#: allianceauth/views.py:108
|
||||
msgid "Internal Server Error"
|
||||
msgstr ""
|
||||
|
||||
@@ -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
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -11,10 +11,10 @@ environment =
|
||||
[program:beat]
|
||||
command = %(ENV_AA_COMMAND_CELERY)s
|
||||
-A %(ENV_AA_PROJECT_NAME)s beat
|
||||
directory = %(ENV_AA_VENV_DIRECTORY)s
|
||||
directory = %(ENV_AA_PROJECT_DIRECTORY)s
|
||||
user = %(ENV_AA_USER)s
|
||||
stdout_logfile = %(ENV_AA_VENV_DIRECTORY)s/log/%(program_name)s.log
|
||||
stderr_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_PROJECT_DIRECTORY)s/log/%(program_name)s.log
|
||||
autostart = true
|
||||
autorestart = true
|
||||
startsecs = 10
|
||||
@@ -26,12 +26,12 @@ command = %(ENV_AA_COMMAND_CELERY)s
|
||||
--pool=threads
|
||||
--concurrency=5
|
||||
-n %(program_name)s_%(process_num)02d
|
||||
directory = %(ENV_AA_VENV_DIRECTORY)s
|
||||
directory = %(ENV_AA_PROJECT_DIRECTORY)s
|
||||
user = %(ENV_AA_USER)s
|
||||
numprocs = 1
|
||||
process_name = %(program_name)s_%(process_num)02d
|
||||
stdout_logfile = %(ENV_AA_VENV_DIRECTORY)s/log/%(program_name)s.log
|
||||
stderr_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_PROJECT_DIRECTORY)s/log/%(program_name)s.log
|
||||
autostart = true
|
||||
autorestart = true
|
||||
startsecs = 10
|
||||
@@ -42,12 +42,12 @@ priority = 998
|
||||
{% if gunicorn %}
|
||||
[program:gunicorn]
|
||||
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
|
||||
--workers=3
|
||||
--timeout 120
|
||||
stdout_logfile = %(ENV_AA_VENV_DIRECTORY)s/log/%(program_name)s.log
|
||||
stderr_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_PROJECT_DIRECTORY)s/log/%(program_name)s.log
|
||||
autostart = true
|
||||
autorestart = true
|
||||
stopsignal = INT
|
||||
@@ -57,10 +57,10 @@ stopsignal = INT
|
||||
command = %(ENV_AA_COMMAND_MEMMON)s
|
||||
-p worker_00=256MB
|
||||
-p gunicorn=256MB
|
||||
directory = %(ENV_AA_VENV_DIRECTORY)s
|
||||
directory = %(ENV_AA_PROJECT_DIRECTORY)s
|
||||
events = TICK_60
|
||||
stdout_logfile = %(ENV_AA_VENV_DIRECTORY)s/log/memmon.log
|
||||
stderr_logfile = %(ENV_AA_VENV_DIRECTORY)s/log/memmon.log
|
||||
stdout_logfile = %(ENV_AA_PROJECT_DIRECTORY)s/log/memmon.log
|
||||
stderr_logfile = %(ENV_AA_PROJECT_DIRECTORY)s/log/memmon.log
|
||||
|
||||
[group:{{ project_name }}]
|
||||
programs = beat,worker{% if gunicorn %},gunicorn{% endif %}
|
||||
|
||||
@@ -100,7 +100,10 @@ class BaseSetPasswordServiceAccountView(ServicesCRUDMixin, BaseServiceView, Upda
|
||||
def post(self, request, *args, **kwargs):
|
||||
result = super().post(request, *args, **kwargs)
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -111,11 +111,11 @@ class Teamspeak3Manager:
|
||||
outlist = {}
|
||||
|
||||
if type(groups) == list:
|
||||
logger.debug("Recieved multiple groups. Iterating.")
|
||||
logger.debug("Received multiple groups. Iterating.")
|
||||
for group in groups:
|
||||
outlist[group['keys']['name']] = group['keys']['sgid']
|
||||
elif type(groups) == dict:
|
||||
logger.debug("Recieved single group.")
|
||||
logger.debug("Received single group.")
|
||||
outlist[groups['keys']['name']] = groups['keys']['sgid']
|
||||
logger.debug("Returning name/id pairing: %s" % outlist)
|
||||
return outlist
|
||||
|
||||
@@ -34,6 +34,9 @@ class SrpFleetUserRequestForm(forms.Form):
|
||||
_("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
|
||||
if not any(
|
||||
re.match(regex, data)
|
||||
|
||||
27
allianceauth/srp/tests/test_form.py
Normal file
27
allianceauth/srp/tests/test_form.py
Normal 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())
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
57
allianceauth/static/allianceauth/js/sidebar.js
Normal file
57
allianceauth/static/allianceauth/js/sidebar.js
Normal 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());
|
||||
});
|
||||
@@ -1,3 +1,3 @@
|
||||
{% load sri %}
|
||||
|
||||
{% sri_static 'allianceauth/js/sidebar-collapse.js' %}
|
||||
{% sri_static 'allianceauth/js/sidebar.js' %}
|
||||
|
||||
11
allianceauth/templates/django_bootstrap5/field_errors.html
Normal file
11
allianceauth/templates/django_bootstrap5/field_errors.html
Normal 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 %}
|
||||
@@ -1,7 +1,7 @@
|
||||
PROTOCOL=https://
|
||||
AUTH_SUBDOMAIN=%AUTH_SUBDOMAIN%
|
||||
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
|
||||
PROXY_HTTP_PORT=80
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
FROM python:3.11-slim
|
||||
ARG AUTH_VERSION=v4.11.0
|
||||
ARG AUTH_VERSION=v4.11.2
|
||||
ARG AUTH_PACKAGE=allianceauth==${AUTH_VERSION}
|
||||
ENV AUTH_USER=allianceauth
|
||||
ENV AUTH_GROUP=allianceauth
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -13,6 +13,7 @@ The Alliance Auth framework is split into several submodules, each of which is d
|
||||
|
||||
framework/api
|
||||
framework/css
|
||||
framework/datatables
|
||||
framework/js
|
||||
framework/templates
|
||||
framework/svg-sprite
|
||||
|
||||
181
docs/development/custom/framework/datatables.md
Normal file
181
docs/development/custom/framework/datatables.md
Normal 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.
|
||||
@@ -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 = ''
|
||||
|
||||
@@ -15,7 +15,7 @@ license = { file = "LICENSE" }
|
||||
authors = [
|
||||
{ name = "Alliance Auth", email = "adarnof@gmail.com" },
|
||||
]
|
||||
requires-python = ">=3.8"
|
||||
requires-python = ">=3.8,<3.13"
|
||||
classifiers = [
|
||||
"Environment :: Web Environment",
|
||||
"Framework :: Celery",
|
||||
@@ -31,8 +31,6 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
"Topic :: Internet :: WWW/HTTP",
|
||||
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
||||
]
|
||||
|
||||
10
tox.ini
10
tox.ini
@@ -7,7 +7,7 @@ envlist = py{38,39,310,311,312}-{all,core}, docs
|
||||
[testenv]
|
||||
setenv =
|
||||
all: DJANGO_SETTINGS_MODULE = tests.settings_all
|
||||
core: DJANGO_SETTINGS_MODULE = tests.settings_core
|
||||
; core: DJANGO_SETTINGS_MODULE = tests.settings_core
|
||||
basepython =
|
||||
py38: python3.8
|
||||
py39: python3.9
|
||||
@@ -18,10 +18,10 @@ deps=
|
||||
coverage
|
||||
install_command = pip install -e ".[test]" -U {opts} {packages}
|
||||
commands =
|
||||
all: coverage run runtests.py -v 2 --debug-mode
|
||||
core: coverage run runtests.py allianceauth.authentication.tests.test_app_settings -v 2 --debug-mode
|
||||
all: coverage report -m
|
||||
all: coverage xml
|
||||
all: coverage run runtests.py allianceauth.framework.tests.test_datatables -v 2 --debug-mode
|
||||
; core: coverage run runtests.py allianceauth.authentication.tests.test_app_settings -v 2 --debug-mode
|
||||
; all: coverage report -m
|
||||
; all: coverage xml
|
||||
|
||||
[testenv:docs]
|
||||
description = invoke sphinx-build to build the HTML docs
|
||||
|
||||
Reference in New Issue
Block a user