mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2026-02-12 18:16:24 +01:00
Compare commits
12 Commits
v5.x
...
39071f7fc3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39071f7fc3 | ||
|
|
97f603c138 | ||
|
|
c9b07c12a0 | ||
|
|
fd84f7fe15 | ||
|
|
92d8c699eb | ||
|
|
9cc3283399 | ||
|
|
401c093b74 | ||
|
|
b3534f4f44 | ||
|
|
f88249c8fc | ||
|
|
ec34d7fd29 | ||
|
|
cd9d985732 | ||
|
|
1c1e219037 |
@@ -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-trixie
|
image: python:3.12-bookworm
|
||||||
# variables:
|
# variables:
|
||||||
# PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
|
# PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
|
||||||
# cache:
|
# cache:
|
||||||
@@ -53,7 +53,7 @@ secret_detection:
|
|||||||
|
|
||||||
test-3.10-core:
|
test-3.10-core:
|
||||||
<<: *only-default
|
<<: *only-default
|
||||||
image: python:3.10-trixie
|
image: python:3.10-bookworm
|
||||||
script:
|
script:
|
||||||
- tox -e py310-core
|
- tox -e py310-core
|
||||||
artifacts:
|
artifacts:
|
||||||
@@ -65,7 +65,7 @@ test-3.10-core:
|
|||||||
|
|
||||||
test-3.11-core:
|
test-3.11-core:
|
||||||
<<: *only-default
|
<<: *only-default
|
||||||
image: python:3.11-trixie
|
image: python:3.11-bookworm
|
||||||
script:
|
script:
|
||||||
- tox -e py311-core
|
- tox -e py311-core
|
||||||
artifacts:
|
artifacts:
|
||||||
@@ -77,7 +77,7 @@ test-3.11-core:
|
|||||||
|
|
||||||
test-3.12-core:
|
test-3.12-core:
|
||||||
<<: *only-default
|
<<: *only-default
|
||||||
image: python:3.12-trixie
|
image: python:3.12-bookworm
|
||||||
script:
|
script:
|
||||||
- tox -e py312-core
|
- tox -e py312-core
|
||||||
artifacts:
|
artifacts:
|
||||||
@@ -89,7 +89,7 @@ test-3.12-core:
|
|||||||
|
|
||||||
test-3.13-core:
|
test-3.13-core:
|
||||||
<<: *only-default
|
<<: *only-default
|
||||||
image: python:3.13-trixie
|
image: python:3.13-rc-bookworm
|
||||||
script:
|
script:
|
||||||
- tox -e py313-core
|
- tox -e py313-core
|
||||||
artifacts:
|
artifacts:
|
||||||
@@ -99,21 +99,9 @@ test-3.13-core:
|
|||||||
coverage_format: cobertura
|
coverage_format: cobertura
|
||||||
path: coverage.xml
|
path: coverage.xml
|
||||||
|
|
||||||
test-3.14-core:
|
|
||||||
<<: *only-default
|
|
||||||
image: python:3.14-trixie
|
|
||||||
script:
|
|
||||||
- tox -e py314-core
|
|
||||||
artifacts:
|
|
||||||
when: always
|
|
||||||
reports:
|
|
||||||
coverage_report:
|
|
||||||
coverage_format: cobertura
|
|
||||||
path: coverage.xml
|
|
||||||
|
|
||||||
test-3.10-all:
|
test-3.10-all:
|
||||||
<<: *only-default
|
<<: *only-default
|
||||||
image: python:3.10-trixie
|
image: python:3.10-bookworm
|
||||||
script:
|
script:
|
||||||
- tox -e py310-all
|
- tox -e py310-all
|
||||||
artifacts:
|
artifacts:
|
||||||
@@ -125,7 +113,7 @@ test-3.10-all:
|
|||||||
|
|
||||||
test-3.11-all:
|
test-3.11-all:
|
||||||
<<: *only-default
|
<<: *only-default
|
||||||
image: python:3.11-trixie
|
image: python:3.11-bookworm
|
||||||
script:
|
script:
|
||||||
- tox -e py311-all
|
- tox -e py311-all
|
||||||
artifacts:
|
artifacts:
|
||||||
@@ -138,7 +126,7 @@ test-3.11-all:
|
|||||||
|
|
||||||
test-3.12-all:
|
test-3.12-all:
|
||||||
<<: *only-default
|
<<: *only-default
|
||||||
image: python:3.12-trixie
|
image: python:3.12-bookworm
|
||||||
script:
|
script:
|
||||||
- tox -e py312-all
|
- tox -e py312-all
|
||||||
artifacts:
|
artifacts:
|
||||||
@@ -150,7 +138,7 @@ test-3.12-all:
|
|||||||
|
|
||||||
test-3.13-all:
|
test-3.13-all:
|
||||||
<<: *only-default
|
<<: *only-default
|
||||||
image: python:3.13-trixie
|
image: python:3.13-rc-bookworm
|
||||||
script:
|
script:
|
||||||
- tox -e py313-all
|
- tox -e py313-all
|
||||||
artifacts:
|
artifacts:
|
||||||
@@ -160,21 +148,9 @@ test-3.13-all:
|
|||||||
coverage_format: cobertura
|
coverage_format: cobertura
|
||||||
path: coverage.xml
|
path: coverage.xml
|
||||||
|
|
||||||
test-3.14-all:
|
|
||||||
<<: *only-default
|
|
||||||
image: python:3.14-trixie
|
|
||||||
script:
|
|
||||||
- tox -e py314-all
|
|
||||||
artifacts:
|
|
||||||
when: always
|
|
||||||
reports:
|
|
||||||
coverage_report:
|
|
||||||
coverage_format: cobertura
|
|
||||||
path: coverage.xml
|
|
||||||
|
|
||||||
build-test:
|
build-test:
|
||||||
stage: test
|
stage: test
|
||||||
image: python:3.12-trixie
|
image: python:3.12-bookworm
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- python -m pip install --upgrade pip
|
- python -m pip install --upgrade pip
|
||||||
@@ -193,13 +169,13 @@ build-test:
|
|||||||
|
|
||||||
test-docs:
|
test-docs:
|
||||||
<<: *only-default
|
<<: *only-default
|
||||||
image: python:3.12-trixie
|
image: python:3.12-bookworm
|
||||||
script:
|
script:
|
||||||
- tox -e docs
|
- tox -e docs
|
||||||
|
|
||||||
deploy_production:
|
deploy_production:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
image: python:3.12-trixie
|
image: python:3.12-bookworm
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- python -m pip install --upgrade pip
|
- python -m pip install --upgrade pip
|
||||||
|
|||||||
@@ -24,21 +24,27 @@ exclude: |
|
|||||||
)
|
)
|
||||||
|
|
||||||
repos:
|
repos:
|
||||||
# Code Upgrades
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
|
rev: v0.11.4
|
||||||
|
hooks:
|
||||||
|
# Run the linter, and only the linter
|
||||||
|
- id: ruff
|
||||||
|
|
||||||
- repo: https://github.com/adamchainz/django-upgrade
|
- repo: https://github.com/adamchainz/django-upgrade
|
||||||
rev: 1.29.0
|
rev: 1.24.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: django-upgrade
|
- id: django-upgrade
|
||||||
args: [--target-version=5.2]
|
args: [--target-version=5.1]
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
|
||||||
rev: v3.20.0
|
- repo: https://github.com/asottile/pyupgrade # Ruff doesnt get everything.
|
||||||
|
rev: v3.19.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args: [--py310-plus]
|
args: [--py310-plus]
|
||||||
|
|
||||||
# Formatting
|
# Formatting
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v6.0.0
|
rev: v5.0.0
|
||||||
hooks:
|
hooks:
|
||||||
# Identify invalid files
|
# Identify invalid files
|
||||||
- id: check-ast
|
- id: check-ast
|
||||||
@@ -53,9 +59,11 @@ repos:
|
|||||||
- id: detect-private-key
|
- id: detect-private-key
|
||||||
- id: check-case-conflict
|
- id: check-case-conflict
|
||||||
# Python checks
|
# Python checks
|
||||||
# - id: check-docstring-first
|
#
|
||||||
- id: debug-statements
|
- id: debug-statements
|
||||||
# - id: requirements-txt-fixer
|
# - id: requirements-txt-fixer
|
||||||
|
- id: fix-encoding-pragma
|
||||||
|
args: [--remove]
|
||||||
- id: fix-byte-order-marker
|
- id: fix-byte-order-marker
|
||||||
# General quality checks
|
# General quality checks
|
||||||
- id: mixed-line-ending
|
- id: mixed-line-ending
|
||||||
@@ -65,26 +73,29 @@ 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.2.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: editorconfig-checker
|
- id: editorconfig-checker
|
||||||
- repo: https://github.com/igorshubovych/markdownlint-cli
|
- repo: https://github.com/igorshubovych/markdownlint-cli
|
||||||
rev: v0.45.0
|
rev: v0.44.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: markdownlint
|
- id: markdownlint
|
||||||
language: node
|
language: node
|
||||||
args:
|
args:
|
||||||
- --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.5.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyproject-fmt
|
- id: pyproject-fmt
|
||||||
args:
|
args:
|
||||||
- --indent=4
|
- --indent=4
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- tox==4.32.0 # https://github.com/tox-dev/tox/releases/latest
|
- tox==4.24.1 # https://github.com/tox-dev/tox/releases/latest
|
||||||
|
- repo: https://github.com/tox-dev/tox-ini-fmt
|
||||||
|
rev: 1.5.0
|
||||||
|
hooks:
|
||||||
|
- id: tox-ini-fmt
|
||||||
- repo: https://github.com/abravalheri/validate-pyproject
|
- repo: https://github.com/abravalheri/validate-pyproject
|
||||||
rev: v0.24.1
|
rev: v0.24.1
|
||||||
hooks:
|
hooks:
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
* @allianceauth
|
|
||||||
34
README.md
34
README.md
@@ -1,15 +1,15 @@
|
|||||||
# Alliance Auth
|
# Alliance Auth
|
||||||
|
|
||||||
[](https://pypi.org/project/allianceauth/)
|
[](https://pypi.org/project/allianceauth/)
|
||||||
[](https://pypi.org/project/allianceauth/)
|
[](https://pypi.org/project/allianceauth/)
|
||||||
[](https://pypi.org/project/allianceauth/)
|
[](https://pypi.org/project/allianceauth/)
|
||||||
[](https://pypi.org/project/allianceauth/)
|
[](https://pypi.org/project/allianceauth/)
|
||||||
[](https://gitlab.com/allianceauth/allianceauth/commits/master)
|
[](https://gitlab.com/allianceauth/allianceauth/commits/master)
|
||||||
[](https://allianceauth.readthedocs.io/?badge=latest)
|
[](https://allianceauth.readthedocs.io/?badge=latest)
|
||||||
[](https://gitlab.com/allianceauth/allianceauth/commits/master)
|
[](https://gitlab.com/allianceauth/allianceauth/commits/master)
|
||||||
[](https://discord.gg/fjnHAmk)
|
[](https://discord.gg/fjnHAmk)
|
||||||
|
|
||||||
A flexible authentication platform for EVE Online to help in-game organizations manage access to applications and services. AA provides both, a stable core, and a robust framework for community development and custom applications.
|
An auth system for EVE Online to help in-game organizations manage online service access.
|
||||||
|
|
||||||
## Content
|
## Content
|
||||||
|
|
||||||
@@ -22,17 +22,17 @@ A flexible authentication platform for EVE Online to help in-game organizations
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Alliance Auth (AA) is a platform that helps Eve Online organizations efficiently manage access to applications and services.
|
Alliance Auth (AA) is a web site that helps Eve Online organizations efficiently manage access to applications and services.
|
||||||
|
|
||||||
Main features:
|
Main features:
|
||||||
|
|
||||||
- Automatically grants or revokes user access to external services (e.g.: Discord, Mumble) based on the user's current membership to [a variety of EVE Online affiliation](https://allianceauth.readthedocs.io/en/latest/features/core/states/) and [groups](https://allianceauth.readthedocs.io/en/latest/features/core/groups/)
|
- Automatically grants or revokes user access to external services (e.g. Discord, Mumble) and web apps (e.g. SRP requests) based on the user's current membership to [in-game organizations](https://allianceauth.readthedocs.io/en/latest/features/core/states/) and [groups](https://allianceauth.readthedocs.io/en/latest/features/core/groups/)
|
||||||
|
|
||||||
- Provides a central web site where users can directly access web apps (e.g. SRP requests, Fleet Schedule) and manage their access to external services and groups.
|
- Provides a central web site where users can directly access web apps (e.g. SRP requests, Fleet Schedule) and manage their access to external services and groups.
|
||||||
|
|
||||||
- Includes a set of connectors (called ["Services"](https://allianceauth.readthedocs.io/en/latest/features/services/)) for integrating access management with many popular external applications / services like Discord, Mumble, Teamspeak 3, SMF and others
|
- Includes a set of connectors (called ["services"](https://allianceauth.readthedocs.io/en/latest/features/services/)) for integrating access management with many popular external applications / services like Discord, Mumble, Teamspeak 3, SMF and others
|
||||||
|
|
||||||
- Includes a set of web [Apps](https://allianceauth.readthedocs.io/en/latest/features/apps/) which add many useful functions, e.g.: fleet schedule, timer board, SRP request management, fleet activity tracker
|
- Includes a set of web [apps](https://allianceauth.readthedocs.io/en/latest/features/apps/) which add many useful functions, e.g.: fleet schedule, timer board, SRP request management, fleet activity tracker
|
||||||
|
|
||||||
- Can be easily extended with additional services and apps. Many are provided by the community and can be found here: [Community Creations](https://gitlab.com/allianceauth/community-creations)
|
- Can be easily extended with additional services and apps. Many are provided by the community and can be found here: [Community Creations](https://gitlab.com/allianceauth/community-creations)
|
||||||
|
|
||||||
@@ -42,15 +42,9 @@ For further details about AA - including an installation guide and a full list o
|
|||||||
|
|
||||||
## Screenshot
|
## Screenshot
|
||||||
|
|
||||||
Here is an example of the Alliance Auth web site with a mixture of Services, Apps and Community Creations enabled:
|
Here is an example of the Alliance Auth web site with some plug-ins apps and services enabled:
|
||||||
|
|
||||||
### Flatly Theme
|

|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Darkly Theme
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
@@ -89,6 +83,6 @@ Alliance Auth is maintained and developed by the community and we welcome every
|
|||||||
|
|
||||||
To see what needs to be worked on please review our issue list or chat with our active developers on Discord.
|
To see what needs to be worked on please review our issue list or chat with our active developers on Discord.
|
||||||
|
|
||||||
Also, please make sure you have signed the [License Agreement](https://developers.eveonline.com/license-agreement) by logging in at [https://developers.eveonline.com](https://developers.eveonline.com) before submitting any pull requests.
|
Also, please make sure you have signed the [License Agreement](https://developers.eveonline.com/resource/license-agreement) by logging in at [https://developers.eveonline.com](https://developers.eveonline.com) before submitting any pull requests.
|
||||||
|
|
||||||
In addition to the core AA system we also very much welcome contributions to our growing list of 3rd party services and plugin apps. Please see [AA Community Creations](https://gitlab.com/allianceauth/community-creations) for details.
|
In addition to the core AA system we also very much welcome contributions to our growing list of 3rd party services and plugin apps. Please see [AA Community Creations](https://gitlab.com/allianceauth/community-creations) for details.
|
||||||
|
|||||||
@@ -5,8 +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__ = '5.0.0a3'
|
__version__ = '5.0.0a1'
|
||||||
__title__ = 'Alliance Auth'
|
__title__ = 'Alliance Auth'
|
||||||
__title_useragent__ = 'AllianceAuth'
|
|
||||||
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
|
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
|
||||||
NAME = f'{__title__} v{__version__}'
|
NAME = f'{__title__} v{__version__}'
|
||||||
|
|||||||
@@ -2,13 +2,12 @@
|
|||||||
{% load admin_status %}
|
{% load admin_status %}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id="celery-progress-bar-{{ label }}"
|
class="progress-bar bg-{{ level }} task-status-progress-bar"
|
||||||
class="progress-bar text-bg-{{ level }} task-status-progress-bar"
|
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
aria-valuenow="{% decimal_widthratio tasks_count tasks_total 100 %}"
|
aria-valuenow="{% decimal_widthratio tasks_count tasks_total 100 %}"
|
||||||
aria-valuemin="0"
|
aria-valuemin="0"
|
||||||
aria-valuemax="100"
|
aria-valuemax="100"
|
||||||
style="width: {% decimal_widthratio tasks_count tasks_total 100 %}%;"
|
style="width: {% decimal_widthratio tasks_count tasks_total 100 %}%;"
|
||||||
>
|
>
|
||||||
<span id="celery-progress-bar-{{ label }}-progress">{% widthratio tasks_count tasks_total 100 %}%</span>
|
<span>{% widthratio tasks_count tasks_total 100 %}%</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,32 +1,37 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
<div id="esi-alert" class="col-12 collapse">
|
<div id="esi-alert" class="col-12 collapse">
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
<p class="text-center ">{% translate 'Your Server received an ESI error response code of ' %}<b id="esi-code">?</b></p>
|
<p class="text-center ">{% translate 'Your Server received an ESI error response code of ' %}<b id="esi-code">?</b></p>
|
||||||
<hr>
|
<hr>
|
||||||
<pre id="esi-data" class="text-center text-wrap"></pre>
|
<pre id="esi-data" class="text-center text-wrap"></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
$(document).ready(() => {
|
|
||||||
const elements = {
|
|
||||||
card: document.getElementById('esi-alert'),
|
|
||||||
message: document.getElementById('esi-data'),
|
|
||||||
code: document.getElementById('esi-code')
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchGet({url: '{% url "authentication:esi_check" %}'})
|
|
||||||
.then(({status, data}) => {
|
|
||||||
console.log('ESI Check:', JSON.stringify({status, data}, null, 2));
|
|
||||||
|
|
||||||
if (status !== 200) {
|
|
||||||
elements.code.textContent = status;
|
|
||||||
elements.message.textContent = data.error;
|
|
||||||
|
|
||||||
new bootstrap.Collapse(elements.card, {toggle: true});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => console.error('Error fetching ESI check:', error));
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const elemCard = document.getElementById("esi-alert");
|
||||||
|
const elemMessage = document.getElementById("esi-data");
|
||||||
|
const elemCode = document.getElementById("esi-code");
|
||||||
|
|
||||||
|
fetch('{% url "authentication:esi_check" %}')
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
throw new Error("Something went wrong");
|
||||||
|
})
|
||||||
|
.then((responseJson) => {
|
||||||
|
console.log("ESI Check: ", JSON.stringify(responseJson, null, 2));
|
||||||
|
|
||||||
|
const status = responseJson.status;
|
||||||
|
if (status !== 200) {
|
||||||
|
elemCode.textContent = status
|
||||||
|
elemMessage.textContent = responseJson.data.error;
|
||||||
|
new bootstrap.Collapse(elemCard, {
|
||||||
|
toggle: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,79 +1,25 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
|
|
||||||
{% get_current_language as LANGUAGE_CODE %}
|
|
||||||
|
|
||||||
{% comment %}
|
|
||||||
Some translations used in the HTML and JavaScript code below.
|
|
||||||
We define them here so that they can be used in the JavaScript code as well with
|
|
||||||
the escapejs filter without having to redefine them later.
|
|
||||||
{% endcomment %}
|
|
||||||
{% translate "second" as l10nSecondSingular %}
|
|
||||||
{% translate "seconds" as l10nSecondPlural %}
|
|
||||||
{% translate "minute" as l10nMinuteSingular %}
|
|
||||||
{% translate "minutes" as l10nMinutePlural %}
|
|
||||||
{% translate "hour" as l10nHourSingular %}
|
|
||||||
{% translate "hours" as l10nHourPlural %}
|
|
||||||
{% translate "N/A" as l10nNA %}
|
|
||||||
{% translate "ERROR" as l10nError %}
|
|
||||||
{% translate "running" as l10nRunning %}
|
|
||||||
{% translate "queued" as l10nQueued %}
|
|
||||||
{% translate "succeeded" as l10nSucceeded %}
|
|
||||||
{% translate "retried" as l10nRetried %}
|
|
||||||
{% translate "failed" as l10nFailed %}
|
|
||||||
|
|
||||||
{% if debug %}
|
|
||||||
<div id="aa-dashboard-panel-debug" class="col-12 mb-3">
|
|
||||||
<div class="card text-bg-warning">
|
|
||||||
<div class="card-body">
|
|
||||||
{% translate "Debug mode" as widget_title %}
|
|
||||||
{% include "framework/dashboard/widget-title.html" with title=widget_title %}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<p class="text-center">
|
|
||||||
{% translate "Debug mode is currently turned on!<br>Make sure to turn it off as soon as you are finished testing." %}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if notifications %}
|
{% if notifications %}
|
||||||
<div id="aa-dashboard-panel-admin-application-notifications" class="col-12 mb-3">
|
<div id="aa-dashboard-panel-admin-application-notifications" class="col-12 mb-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% translate "Announcements" as widget_title %}
|
{% translate "AllianceAuth and 3rd party Applications Notifications" as widget_title %}
|
||||||
{% include "framework/dashboard/widget-title.html" with title=widget_title %}
|
{% include "framework/dashboard/widget-title.html" with title=widget_title %}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
{% for notif in notifications %}
|
{% for notif in notifications %}
|
||||||
<li class="list-group-item">
|
{% if not notif.is_hidden %}
|
||||||
<span class="badge text-bg-success me-2">{% translate "Open" %}</span>
|
<li class="list-group-item">
|
||||||
<a href="{{ notif.web_url }}" target="_blank">#{{ notif.iid }} {{ notif.title }}</a>
|
<span class="badge bg-info me-2">{{ notif.application_name }}</span>
|
||||||
</li>
|
<a href="{{ notif.announcement_url }}" target="_blank">#{{ notif.announcement_number }} {{ notif.announcement_text }}</a>
|
||||||
{% empty %}
|
</li>
|
||||||
<div class="alert alert-primary" role="alert">
|
{% endif %}
|
||||||
{% translate "No notifications at this time" %}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{# TODO maybe add some disclaimer that those are managed by application devs? #}
|
||||||
<div class="text-end pt-3">
|
|
||||||
<a href="https://gitlab.com/allianceauth/allianceauth/issues" target="_blank" class="me-1 text-decoration-none">
|
|
||||||
<span class="badge text-bg-danger">
|
|
||||||
<i class="fab fa-gitlab" aria-hidden="true"></i>
|
|
||||||
{% translate 'Powered by GitLab' %}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
<a href="https://discord.com/invite/fjnHAmk" target="_blank" class="text-decoration-none">
|
|
||||||
<span class="badge text-bg-info">
|
|
||||||
<i class="fab fa-discord" aria-hidden="true"></i>
|
|
||||||
{% translate 'Support Discord' %}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,7 +42,7 @@ the escapejs filter without having to redefine them later.
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="list-group-item text-bg-{% if latest_patch %}success{% elif latest_minor %}warning{% else %}danger{% endif %} w-100">
|
<li class="list-group-item bg-{% if latest_patch %}success{% elif latest_minor %}warning{% else %}danger{% endif %} w-100">
|
||||||
<a class="btn h-100 w-100" href="https://gitlab.com/allianceauth/allianceauth/-/releases/v{{ latest_patch_version }}">
|
<a class="btn h-100 w-100" href="https://gitlab.com/allianceauth/allianceauth/-/releases/v{{ latest_patch_version }}">
|
||||||
<h5 class="list-group-item-heading">{% translate "Latest Stable" %}</h5>
|
<h5 class="list-group-item-heading">{% translate "Latest Stable" %}</h5>
|
||||||
|
|
||||||
@@ -109,7 +55,7 @@ the escapejs filter without having to redefine them later.
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
{% if latest_beta %}
|
{% if latest_beta %}
|
||||||
<li class="list-group-item text-bg-info w-100">
|
<li class="list-group-item bg-info w-100">
|
||||||
<a class="btn h-100 w-100" href="https://gitlab.com/allianceauth/allianceauth/-/releases/v{{ latest_beta_version }}">
|
<a class="btn h-100 w-100" href="https://gitlab.com/allianceauth/allianceauth/-/releases/v{{ latest_beta_version }}">
|
||||||
<h5 class="list-group-item-heading">{% translate "Latest Pre-Release" %}</h5>
|
<h5 class="list-group-item-heading">{% translate "Latest Pre-Release" %}</h5>
|
||||||
|
|
||||||
@@ -132,27 +78,23 @@ the escapejs filter without having to redefine them later.
|
|||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
{% blocktranslate with total=tasks_total|intcomma latest=earliest_task|timesince|default:"?" %}
|
{% blocktranslate with total=tasks_total|intcomma latest=earliest_task|timesince|default:"?" %}
|
||||||
Status of <span id="total-task-count">?</span> processed tasks • last <span id="celery-uptime">?</span>
|
Status of {{ total }} processed tasks • last {{ latest }}
|
||||||
{% endblocktranslate %}
|
{% endblocktranslate %}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id="celery-tasks-progress-bar"
|
class="progress"
|
||||||
class="progress mb-2"
|
|
||||||
style="height: 21px;"
|
style="height: 21px;"
|
||||||
title="? {{ l10nSucceeded }}, ? {{ l10nRetried }}, ? {{ l10nFailed }}"
|
title="{{ tasks_succeeded|intcomma }} succeeded, {{ tasks_retried|intcomma }} retried, {{ tasks_failed|intcomma }} failed"
|
||||||
>
|
>
|
||||||
{% include "allianceauth/admin-status/celery_bar_partial.html" with label="succeeded" level="success" tasks_count=0 %}
|
{% include "admin-status/celery_bar_partial.html" with label="suceeded" level="success" tasks_count=tasks_succeeded %}
|
||||||
{% include "allianceauth/admin-status/celery_bar_partial.html" with label="retried" level="info" tasks_count=0 %}
|
{% include "admin-status/celery_bar_partial.html" with label="retried" level="info" tasks_count=tasks_retried %}
|
||||||
{% include "allianceauth/admin-status/celery_bar_partial.html" with label="failed" level="danger" tasks_count=0 %}
|
{% include "admin-status/celery_bar_partial.html" with label="failed" level="danger" tasks_count=tasks_failed %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<span id="running-task-count">?</span> {{ l10nRunning }} |
|
<span id="task-counts">?</span> {% translate 'running' %} |
|
||||||
<span id="queued-tasks-count">?</span> {{ l10nQueued }} |
|
<span id="queued-tasks-count">?</span> {% translate 'queued' %}
|
||||||
<span id="succeeded-tasks-count">?</span> {{ l10nSucceeded }} |
|
|
||||||
<span id="retried-tasks-count">?</span> {{ l10nRetried }} |
|
|
||||||
<span id="failed-tasks-count">?</span> {{ l10nFailed }}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -175,24 +117,34 @@ the escapejs filter without having to redefine them later.
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const taskQueueSettings = {
|
const elemRunning = document.getElementById("task-counts");
|
||||||
url: '{% url "authentication:task_counts" %}',
|
const elemQueued = document.getElementById("queued-tasks-count");
|
||||||
l10n: {
|
|
||||||
language: '{{ LANGUAGE_CODE }}',
|
fetch('{% url "authentication:task_counts" %}')
|
||||||
second_singular: '{{ l10nSecondSingular|escapejs }}',
|
.then((response) => {
|
||||||
second_plural: '{{ l10nSecondPlural|escapejs }}',
|
if (response.ok) {
|
||||||
minute_singular: '{{ l10nMinuteSingular|escapejs }}',
|
return response.json();
|
||||||
minute_plural: '{{ l10nMinutePlural|escapejs }}',
|
}
|
||||||
hour_singular: '{{ l10nHourSingular|escapejs }}',
|
throw new Error("Something went wrong");
|
||||||
hour_plural: '{{ l10nHourPlural|escapejs }}',
|
})
|
||||||
na: '{{ l10nNA|escapejs }}',
|
.then((responseJson) => {
|
||||||
error: '{{ l10nError|escapejs }}',
|
const running = responseJson.tasks_running;
|
||||||
running: '{{ l10nRunning|escapejs }}',
|
if (running == null) {
|
||||||
queued: '{{ l10nQueued|escapejs }}',
|
elemRunning.textContent = "N/A";
|
||||||
succeeded: '{{ l10nSucceeded|escapejs }}',
|
} else {
|
||||||
retried: '{{ l10nRetried|escapejs }}',
|
elemRunning.textContent = running.toLocaleString();
|
||||||
failed: '{{ l10nFailed|escapejs }}'
|
}
|
||||||
}
|
|
||||||
};
|
const queued = responseJson.tasks_queued;
|
||||||
|
if (queued == null) {
|
||||||
|
elemQueued.textContent = "N/A";
|
||||||
|
} else {
|
||||||
|
elemQueued.textContent = queued.toLocaleString();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
elemRunning.textContent = "ERROR";
|
||||||
|
elemQueued.textContent = "ERROR";
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% include "bundles/auth-dashboard-task-queue-js.html" %}
|
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ def status_overview() -> dict:
|
|||||||
"tasks_total": 0,
|
"tasks_total": 0,
|
||||||
"tasks_hours": 0,
|
"tasks_hours": 0,
|
||||||
"earliest_task": None,
|
"earliest_task": None,
|
||||||
"debug": settings.DEBUG if settings.DISPLAY_DEBUG else False,
|
|
||||||
}
|
}
|
||||||
response.update(_current_notifications())
|
response.update(_current_notifications())
|
||||||
response.update(_current_version_summary())
|
response.update(_current_version_summary())
|
||||||
|
|||||||
@@ -1,194 +0,0 @@
|
|||||||
import requests_mock
|
|
||||||
|
|
||||||
from allianceauth.admin_status.hooks import Announcement
|
|
||||||
from allianceauth.services.hooks import AppAnnouncementHook
|
|
||||||
from allianceauth.utils.testing import NoSocketsTestCase
|
|
||||||
|
|
||||||
|
|
||||||
class TestHooks(NoSocketsTestCase):
|
|
||||||
|
|
||||||
@requests_mock.mock()
|
|
||||||
def test_fetch_gitlab(self, requests_mocker):
|
|
||||||
# given
|
|
||||||
announcement_hook = AppAnnouncementHook("test GitLab app", "r0kym/allianceauth-example-plugin",
|
|
||||||
AppAnnouncementHook.Service.GITLAB)
|
|
||||||
requests_mocker.get(
|
|
||||||
"https://gitlab.com/api/v4/projects/r0kym%2Fallianceauth-example-plugin/issues?labels=announcement&state=opened",
|
|
||||||
json=[
|
|
||||||
{
|
|
||||||
"id": 166279127,
|
|
||||||
"iid": 1,
|
|
||||||
"project_id": 67653102,
|
|
||||||
"title": "Test GitLab issue",
|
|
||||||
"description": "Test issue",
|
|
||||||
"state": "opened",
|
|
||||||
"created_at": "2025-04-20T21:26:57.914Z",
|
|
||||||
"updated_at": "2025-04-21T11:04:30.501Z",
|
|
||||||
"closed_at": None,
|
|
||||||
"closed_by": None,
|
|
||||||
"labels": [
|
|
||||||
"announcement"
|
|
||||||
],
|
|
||||||
"milestone": None,
|
|
||||||
"assignees": [],
|
|
||||||
"author": {
|
|
||||||
"id": 14491514,
|
|
||||||
"username": "r0kym",
|
|
||||||
"public_email": "",
|
|
||||||
"name": "T'rahk Rokym",
|
|
||||||
"state": "active",
|
|
||||||
"locked": False,
|
|
||||||
"avatar_url": "https://gitlab.com/uploads/-/system/user/avatar/14491514/avatar.png",
|
|
||||||
"web_url": "https://gitlab.com/r0kym"
|
|
||||||
},
|
|
||||||
"type": "ISSUE",
|
|
||||||
"assignee": None,
|
|
||||||
"user_notes_count": 0,
|
|
||||||
"merge_requests_count": 0,
|
|
||||||
"upvotes": 0,
|
|
||||||
"downvotes": 0,
|
|
||||||
"due_date": None,
|
|
||||||
"confidential": False,
|
|
||||||
"discussion_locked": None,
|
|
||||||
"issue_type": "issue",
|
|
||||||
"web_url": "https://gitlab.com/r0kym/allianceauth-example-plugin/-/issues/1",
|
|
||||||
"time_stats": {
|
|
||||||
"time_estimate": 0,
|
|
||||||
"total_time_spent": 0,
|
|
||||||
"human_time_estimate": None,
|
|
||||||
"human_total_time_spent": None
|
|
||||||
},
|
|
||||||
"task_completion_status": {
|
|
||||||
"count": 0,
|
|
||||||
"completed_count": 0
|
|
||||||
},
|
|
||||||
"blocking_issues_count": 0,
|
|
||||||
"has_tasks": True,
|
|
||||||
"task_status": "0 of 0 checklist items completed",
|
|
||||||
"_links": {
|
|
||||||
"self": "https://gitlab.com/api/v4/projects/67653102/issues/1",
|
|
||||||
"notes": "https://gitlab.com/api/v4/projects/67653102/issues/1/notes",
|
|
||||||
"award_emoji": "https://gitlab.com/api/v4/projects/67653102/issues/1/award_emoji",
|
|
||||||
"project": "https://gitlab.com/api/v4/projects/67653102",
|
|
||||||
"closed_as_duplicate_of": None
|
|
||||||
},
|
|
||||||
"references": {
|
|
||||||
"short": "#1",
|
|
||||||
"relative": "#1",
|
|
||||||
"full": "r0kym/allianceauth-example-plugin#1"
|
|
||||||
},
|
|
||||||
"severity": "UNKNOWN",
|
|
||||||
"moved_to_id": None,
|
|
||||||
"imported": False,
|
|
||||||
"imported_from": "none",
|
|
||||||
"service_desk_reply_to": None
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
# when
|
|
||||||
announcements = announcement_hook.get_announcement_list()
|
|
||||||
# then
|
|
||||||
self.assertEqual(len(announcements), 1)
|
|
||||||
self.assertIn(Announcement(
|
|
||||||
application_name="test GitLab app",
|
|
||||||
announcement_url="https://gitlab.com/r0kym/allianceauth-example-plugin/-/issues/1",
|
|
||||||
announcement_number=1,
|
|
||||||
announcement_text="Test GitLab issue"
|
|
||||||
), announcements)
|
|
||||||
|
|
||||||
@requests_mock.mock()
|
|
||||||
def test_fetch_github(self, requests_mocker):
|
|
||||||
# given
|
|
||||||
announcement_hook = AppAnnouncementHook("test GitHub app", "r0kym/test", AppAnnouncementHook.Service.GITHUB)
|
|
||||||
requests_mocker.get(
|
|
||||||
"https://api.github.com/repos/r0kym/test/issues?labels=announcement",
|
|
||||||
json=[
|
|
||||||
{
|
|
||||||
"url": "https://api.github.com/repos/r0kym/test/issues/1",
|
|
||||||
"repository_url": "https://api.github.com/repos/r0kym/test",
|
|
||||||
"labels_url": "https://api.github.com/repos/r0kym/test/issues/1/labels{/name}",
|
|
||||||
"comments_url": "https://api.github.com/repos/r0kym/test/issues/1/comments",
|
|
||||||
"events_url": "https://api.github.com/repos/r0kym/test/issues/1/events",
|
|
||||||
"html_url": "https://github.com/r0kym/test/issues/1",
|
|
||||||
"id": 3007269496,
|
|
||||||
"node_id": "I_kwDOOc2YvM6zP0p4",
|
|
||||||
"number": 1,
|
|
||||||
"title": "GitHub issue",
|
|
||||||
"user": {
|
|
||||||
"login": "r0kym",
|
|
||||||
"id": 56434393,
|
|
||||||
"node_id": "MDQ6VXNlcjU2NDM0Mzkz",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/56434393?v=4",
|
|
||||||
"gravatar_id": "",
|
|
||||||
"url": "https://api.github.com/users/r0kym",
|
|
||||||
"html_url": "https://github.com/r0kym",
|
|
||||||
"followers_url": "https://api.github.com/users/r0kym/followers",
|
|
||||||
"following_url": "https://api.github.com/users/r0kym/following{/other_user}",
|
|
||||||
"gists_url": "https://api.github.com/users/r0kym/gists{/gist_id}",
|
|
||||||
"starred_url": "https://api.github.com/users/r0kym/starred{/owner}{/repo}",
|
|
||||||
"subscriptions_url": "https://api.github.com/users/r0kym/subscriptions",
|
|
||||||
"organizations_url": "https://api.github.com/users/r0kym/orgs",
|
|
||||||
"repos_url": "https://api.github.com/users/r0kym/repos",
|
|
||||||
"events_url": "https://api.github.com/users/r0kym/events{/privacy}",
|
|
||||||
"received_events_url": "https://api.github.com/users/r0kym/received_events",
|
|
||||||
"type": "User",
|
|
||||||
"user_view_type": "public",
|
|
||||||
"site_admin": False
|
|
||||||
},
|
|
||||||
"labels": [
|
|
||||||
{
|
|
||||||
"id": 8487814480,
|
|
||||||
"node_id": "LA_kwDOOc2YvM8AAAAB-enFUA",
|
|
||||||
"url": "https://api.github.com/repos/r0kym/test/labels/announcement",
|
|
||||||
"name": "announcement",
|
|
||||||
"color": "aaaaaa",
|
|
||||||
"default": False,
|
|
||||||
"description": None
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"state": "open",
|
|
||||||
"locked": False,
|
|
||||||
"assignee": None,
|
|
||||||
"assignees": [],
|
|
||||||
"milestone": None,
|
|
||||||
"comments": 0,
|
|
||||||
"created_at": "2025-04-20T22:41:10Z",
|
|
||||||
"updated_at": "2025-04-21T11:05:08Z",
|
|
||||||
"closed_at": None,
|
|
||||||
"author_association": "OWNER",
|
|
||||||
"active_lock_reason": None,
|
|
||||||
"sub_issues_summary": {
|
|
||||||
"total": 0,
|
|
||||||
"completed": 0,
|
|
||||||
"percent_completed": 0
|
|
||||||
},
|
|
||||||
"body": None,
|
|
||||||
"closed_by": None,
|
|
||||||
"reactions": {
|
|
||||||
"url": "https://api.github.com/repos/r0kym/test/issues/1/reactions",
|
|
||||||
"total_count": 0,
|
|
||||||
"+1": 0,
|
|
||||||
"-1": 0,
|
|
||||||
"laugh": 0,
|
|
||||||
"hooray": 0,
|
|
||||||
"confused": 0,
|
|
||||||
"heart": 0,
|
|
||||||
"rocket": 0,
|
|
||||||
"eyes": 0
|
|
||||||
},
|
|
||||||
"timeline_url": "https://api.github.com/repos/r0kym/test/issues/1/timeline",
|
|
||||||
"performed_via_github_app": None,
|
|
||||||
"state_reason": None
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
# when
|
|
||||||
announcements = announcement_hook.get_announcement_list()
|
|
||||||
# then
|
|
||||||
self.assertEqual(len(announcements), 1)
|
|
||||||
self.assertIn(Announcement(
|
|
||||||
application_name="test GitHub app",
|
|
||||||
announcement_url="https://github.com/r0kym/test/issues/1",
|
|
||||||
announcement_number=1,
|
|
||||||
announcement_text="GitHub issue"
|
|
||||||
), announcements)
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
class AnalyticsConfig(AppConfig):
|
class AnalyticsConfig(AppConfig):
|
||||||
name = 'allianceauth.analytics'
|
name = 'allianceauth.analytics'
|
||||||
label = 'analytics'
|
label = 'analytics'
|
||||||
verbose_name = _('Analytics')
|
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
# Generated by Django 5.1.6 on 2025-03-04 01:03
|
|
||||||
|
|
||||||
# This was built by Deleting Every Migration, Creating one from scratch
|
|
||||||
# And porting in anything necessary
|
|
||||||
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
def add_aa_team_token(apps, schema_editor):
|
|
||||||
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
|
|
||||||
token = Tokens()
|
|
||||||
|
|
||||||
token.type = 'GA-V4'
|
|
||||||
token.token = 'G-6LYSMYK8DE'
|
|
||||||
token.secret = 'KLlpjLZ-SRGozS5f5wb_kw'
|
|
||||||
token.name = 'AA Team Public Google Analytics (V4)'
|
|
||||||
token.save()
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
replaces = [('analytics', '0001_initial'), ('analytics', '0002_add_AA_Team_Token'), ('analytics', '0003_Generate_Identifier'), ('analytics', '0004_auto_20211015_0502'), ('analytics', '0005_alter_analyticspath_ignore_path'), ('analytics', '0006_more_ignore_paths'), ('analytics', '0007_analyticstokens_secret'), ('analytics', '0008_add_AA_GA-4_Team_Token '), ('analytics', '0009_remove_analyticstokens_ignore_paths_and_more'), ('analytics', '0010_alter_analyticsidentifier_options')]
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='AnalyticsIdentifier',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('identifier', models.UUIDField(default=uuid.uuid4, editable=False)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Analytics Identifier',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='AnalyticsTokens',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(max_length=254)),
|
|
||||||
('type', models.CharField(choices=[('GA-U', 'Google Analytics Universal'), ('GA-V4', 'Google Analytics V4')], max_length=254)),
|
|
||||||
('token', models.CharField(max_length=254)),
|
|
||||||
('secret', models.CharField(blank=True, max_length=254)),
|
|
||||||
('send_stats', models.BooleanField(default=False)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.RunPython(
|
|
||||||
add_aa_team_token
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -17,7 +17,6 @@ class AnalyticsIdentifier(SingletonModel):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Analytics Identifier"
|
verbose_name = "Analytics Identifier"
|
||||||
|
|
||||||
|
|
||||||
class AnalyticsTokens(models.Model):
|
class AnalyticsTokens(models.Model):
|
||||||
|
|
||||||
class Analytics_Type(models.TextChoices):
|
class Analytics_Type(models.TextChoices):
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.core.checks import register, Tags
|
from django.core.checks import Tags, register
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationConfig(AppConfig):
|
class AuthenticationConfig(AppConfig):
|
||||||
name = "allianceauth.authentication"
|
name = "allianceauth.authentication"
|
||||||
label = "authentication"
|
label = "authentication"
|
||||||
verbose_name = _("Authentication")
|
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from allianceauth.authentication import checks, signals # noqa: F401
|
from allianceauth.authentication import checks, signals # noqa: F401
|
||||||
|
|||||||
@@ -52,10 +52,4 @@ class UserSettingsMiddleware(MiddlewareMixin):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
|
|
||||||
# Minimize Menu
|
|
||||||
try:
|
|
||||||
request.session["MINIMIZE_SIDEBAR"] = request.user.profile.minimize_sidebar
|
|
||||||
except Exception as e:
|
|
||||||
pass # We don't care that an anonymous user has no profile (not logged in)
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
# Generated by Django 4.2.25 on 2025-10-14 22:02
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentication", "0024_alter_userprofile_language"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="userprofile",
|
|
||||||
name="minimize_sidebar",
|
|
||||||
field=models.BooleanField(
|
|
||||||
default=False,
|
|
||||||
help_text="Keep the sidebar menu minimized",
|
|
||||||
verbose_name="Minimize Sidebar Menu",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
# Generated by Django 5.1.6 on 2025-03-04 02:44
|
|
||||||
|
|
||||||
import django.contrib.auth.models
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
import allianceauth.authentication.models
|
|
||||||
|
|
||||||
|
|
||||||
def create_states(apps, schema_editor) -> None:
|
|
||||||
State = apps.get_model('authentication', 'State')
|
|
||||||
|
|
||||||
State.objects.update_or_create(name="Guest", defaults={'priority': 0, 'public': True})[0]
|
|
||||||
State.objects.update_or_create(name="Blue", defaults={'priority': 50, 'public': False})[0]
|
|
||||||
State.objects.update_or_create(name="Member", defaults={'priority': 100, 'public': False})[0]
|
|
||||||
|
|
||||||
|
|
||||||
def create_states_reverse(apps, schema_editor) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
replaces = [('authentication', '0001_initial'), ('authentication', '0002_auto_20160907_1914'), ('authentication', '0003_authservicesinfo_state'), ('authentication', '0004_create_permissions'), ('authentication', '0005_delete_perms'), ('authentication', '0006_auto_20160910_0542'), ('authentication', '0007_remove_authservicesinfo_is_blue'), ('authentication', '0008_set_state'), ('authentication', '0009_auto_20161021_0228'), ('authentication', '0010_only_one_authservicesinfo'), ('authentication', '0011_authservicesinfo_user_onetoonefield'), ('authentication', '0012_remove_add_delete_authservicesinfo_permissions'), ('authentication', '0013_service_modules'), ('authentication', '0014_fleetup_permission'), ('authentication', '0015_user_profiles'), ('authentication', '0016_ownershiprecord'), ('authentication', '0017_remove_fleetup_permission'), ('authentication', '0018_state_member_factions'), ('authentication', '0018_alter_state_name_length'), ('authentication', '0019_merge_20211026_0919'), ('authentication', '0020_userprofile_language_userprofile_night_mode'), ('authentication', '0021_alter_userprofile_language'), ('authentication', '0022_userprofile_theme'), ('authentication', '0023_alter_userprofile_language'), ('authentication', '0024_alter_userprofile_language')]
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
('esi', '0012_fix_token_type_choices'),
|
|
||||||
('eveonline', '0019_v5squash'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='CharacterOwnership',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('owner_hash', models.CharField(max_length=28, unique=True)),
|
|
||||||
('character', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='character_ownership', to='eveonline.evecharacter')),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='character_ownerships', to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'default_permissions': ('change', 'delete'),
|
|
||||||
'ordering': ['user', 'character__character_name'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='State',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(max_length=32, unique=True)),
|
|
||||||
('priority', models.IntegerField(help_text='Users get assigned the state with the highest priority available to them.', unique=True)),
|
|
||||||
('public', models.BooleanField(default=False, help_text='Make this state available to any character.')),
|
|
||||||
('member_alliances', models.ManyToManyField(blank=True, help_text='Alliances to whose members this state is available.', to='eveonline.eveallianceinfo')),
|
|
||||||
('member_characters', models.ManyToManyField(blank=True, help_text='Characters to which this state is available.', to='eveonline.evecharacter')),
|
|
||||||
('member_corporations', models.ManyToManyField(blank=True, help_text='Corporations to whose members this state is available.', to='eveonline.evecorporationinfo')),
|
|
||||||
('permissions', models.ManyToManyField(blank=True, to='auth.permission')),
|
|
||||||
('member_factions', models.ManyToManyField(blank=True, help_text='Factions to whose members this state is available.', to='eveonline.evefactioninfo')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'ordering': ['-priority'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='UserProfile',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('main_character', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='eveonline.evecharacter')),
|
|
||||||
('state', models.ForeignKey(default=allianceauth.authentication.models.get_guest_state_pk, on_delete=django.db.models.deletion.SET_DEFAULT, to='authentication.state')),
|
|
||||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
|
|
||||||
('night_mode', models.BooleanField(blank=True, null=True, verbose_name='Night Mode')),
|
|
||||||
('theme', models.CharField(blank=True, help_text='Bootstrap 5 Themes from https://bootswatch.com/ or Community Apps', max_length=200, null=True, verbose_name='Theme')),
|
|
||||||
('language', models.CharField(blank=True, choices=[('en', 'English'), ('cs-cz', 'Czech'), ('de', 'German'), ('es', 'Spanish'), ('it-it', 'Italian'), ('ja', 'Japanese'), ('ko-kr', 'Korean'), ('fr-fr', 'French'), ('ru', 'Russian'), ('nl-nl', 'Dutch'), ('pl-pl', 'Polish'), ('uk', 'Ukrainian'), ('zh-hans', 'Simplified Chinese')], default='', max_length=10, verbose_name='Language')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'default_permissions': ('change',),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Permission',
|
|
||||||
fields=[
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'proxy': True,
|
|
||||||
'verbose_name': 'permission',
|
|
||||||
'verbose_name_plural': 'permissions',
|
|
||||||
},
|
|
||||||
bases=('auth.permission',),
|
|
||||||
managers=[
|
|
||||||
('objects', django.contrib.auth.models.PermissionManager()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='User',
|
|
||||||
fields=[
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'proxy': True,
|
|
||||||
'verbose_name': 'user',
|
|
||||||
'verbose_name_plural': 'users',
|
|
||||||
},
|
|
||||||
bases=('auth.user',),
|
|
||||||
managers=[
|
|
||||||
('objects', django.contrib.auth.models.UserManager()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='OwnershipRecord',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('owner_hash', models.CharField(db_index=True, max_length=28)),
|
|
||||||
('created', models.DateTimeField(auto_now=True)),
|
|
||||||
('character', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ownership_records', to='eveonline.evecharacter')),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ownership_records', to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'ordering': ['-created'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.RunPython(create_states, create_states_reverse),
|
|
||||||
|
|
||||||
]
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import ClassVar
|
|
||||||
|
|
||||||
from django.contrib.auth.models import Permission, User
|
from django.contrib.auth.models import Permission, User
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
@@ -16,28 +15,22 @@ logger = logging.getLogger(__name__)
|
|||||||
class State(models.Model):
|
class State(models.Model):
|
||||||
name = models.CharField(max_length=32, unique=True)
|
name = models.CharField(max_length=32, unique=True)
|
||||||
permissions = models.ManyToManyField(Permission, blank=True)
|
permissions = models.ManyToManyField(Permission, blank=True)
|
||||||
priority = models.IntegerField(
|
priority = models.IntegerField(unique=True, help_text="Users get assigned the state with the highest priority available to them.")
|
||||||
unique=True, help_text="Users get assigned the state with the highest priority available to them."
|
|
||||||
)
|
|
||||||
|
|
||||||
member_characters = models.ManyToManyField(
|
member_characters = models.ManyToManyField(EveCharacter, blank=True,
|
||||||
EveCharacter, blank=True, help_text="Characters to which this state is available."
|
help_text="Characters to which this state is available.")
|
||||||
)
|
member_corporations = models.ManyToManyField(EveCorporationInfo, blank=True,
|
||||||
member_corporations = models.ManyToManyField(
|
help_text="Corporations to whose members this state is available.")
|
||||||
EveCorporationInfo, blank=True, help_text="Corporations to whose members this state is available."
|
member_alliances = models.ManyToManyField(EveAllianceInfo, blank=True,
|
||||||
)
|
help_text="Alliances to whose members this state is available.")
|
||||||
member_alliances = models.ManyToManyField(
|
member_factions = models.ManyToManyField(EveFactionInfo, blank=True,
|
||||||
EveAllianceInfo, blank=True, help_text="Alliances to whose members this state is available."
|
help_text="Factions to whose members this state is available.")
|
||||||
)
|
|
||||||
member_factions = models.ManyToManyField(
|
|
||||||
EveFactionInfo, blank=True, help_text="Factions to whose members this state is available."
|
|
||||||
)
|
|
||||||
public = models.BooleanField(default=False, help_text="Make this state available to any character.")
|
public = models.BooleanField(default=False, help_text="Make this state available to any character.")
|
||||||
|
|
||||||
objects: ClassVar[StateManager] = StateManager()
|
objects = StateManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["-priority"]
|
ordering = ['-priority']
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
@@ -55,11 +48,11 @@ class State(models.Model):
|
|||||||
super().delete(**kwargs)
|
super().delete(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
def get_guest_state() -> State:
|
def get_guest_state():
|
||||||
try:
|
try:
|
||||||
return State.objects.get(name="Guest")
|
return State.objects.get(name='Guest')
|
||||||
except State.DoesNotExist:
|
except State.DoesNotExist:
|
||||||
return State.objects.create(name="Guest", priority=0, public=True)
|
return State.objects.create(name='Guest', priority=0, public=True)
|
||||||
|
|
||||||
|
|
||||||
def get_guest_state_pk():
|
def get_guest_state_pk():
|
||||||
@@ -67,6 +60,8 @@ def get_guest_state_pk():
|
|||||||
|
|
||||||
|
|
||||||
class UserProfile(models.Model):
|
class UserProfile(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class Language(models.TextChoices):
|
class Language(models.TextChoices):
|
||||||
"""
|
"""
|
||||||
Choices for UserProfile.language
|
Choices for UserProfile.language
|
||||||
@@ -100,8 +95,7 @@ class UserProfile(models.Model):
|
|||||||
on_delete=models.SET_DEFAULT,
|
on_delete=models.SET_DEFAULT,
|
||||||
default=get_guest_state_pk)
|
default=get_guest_state_pk)
|
||||||
language = models.CharField(
|
language = models.CharField(
|
||||||
_("Language"),
|
_("Language"), max_length=10,
|
||||||
max_length=10,
|
|
||||||
choices=Language.choices,
|
choices=Language.choices,
|
||||||
blank=True,
|
blank=True,
|
||||||
default='')
|
default='')
|
||||||
@@ -113,35 +107,29 @@ class UserProfile(models.Model):
|
|||||||
_("Theme"),
|
_("Theme"),
|
||||||
max_length=200,
|
max_length=200,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
help_text="Bootstrap 5 Themes from https://bootswatch.com/ or Community Apps"
|
||||||
help_text="Bootstrap 5 Themes from https://bootswatch.com/ or Community Apps",
|
|
||||||
)
|
)
|
||||||
minimize_sidebar = models.BooleanField(
|
|
||||||
_("Minimize Sidebar Menu"),
|
|
||||||
default=False,
|
|
||||||
help_text=_("Keep the sidebar menu minimized")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ("change",)
|
default_permissions = ('change',)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return str(self.user)
|
return str(self.user)
|
||||||
|
|
||||||
def assign_state(self, state=None, commit=True) -> None:
|
def assign_state(self, state=None, commit=True):
|
||||||
if not state:
|
if not state:
|
||||||
state = State.objects.get_for_user(self.user)
|
state = State.objects.get_for_user(self.user)
|
||||||
if self.state != state:
|
if self.state != state:
|
||||||
self.state = state
|
self.state = state
|
||||||
if commit:
|
if commit:
|
||||||
logger.info(f"Updating {self.user} state to {self.state}")
|
logger.info(f'Updating {self.user} state to {self.state}')
|
||||||
self.save(update_fields=["state"])
|
self.save(update_fields=['state'])
|
||||||
notify(
|
notify(
|
||||||
self.user,
|
self.user,
|
||||||
_(f"State changed to: {state}"),
|
_(f'State changed to: {state}'),
|
||||||
_("Your user's state is now: %(state)s") % ({"state": state}),
|
_('Your user\'s state is now: %(state)s')
|
||||||
"info",
|
% ({'state': state}),
|
||||||
|
'info'
|
||||||
)
|
)
|
||||||
from allianceauth.authentication.signals import state_changed
|
from allianceauth.authentication.signals import state_changed
|
||||||
|
|
||||||
@@ -149,33 +137,34 @@ class UserProfile(models.Model):
|
|||||||
# Clear all attribute caches and reload the model that will get passed to the signals!
|
# Clear all attribute caches and reload the model that will get passed to the signals!
|
||||||
self.refresh_from_db()
|
self.refresh_from_db()
|
||||||
|
|
||||||
state_changed.send(sender=self.__class__, user=self.user, state=self.state)
|
state_changed.send(
|
||||||
|
sender=self.__class__, user=self.user, state=self.state
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CharacterOwnership(models.Model):
|
class CharacterOwnership(models.Model):
|
||||||
|
|
||||||
|
|
||||||
character = models.OneToOneField(EveCharacter, on_delete=models.CASCADE, related_name='character_ownership')
|
character = models.OneToOneField(EveCharacter, on_delete=models.CASCADE, related_name='character_ownership')
|
||||||
owner_hash = models.CharField(max_length=28, unique=True)
|
owner_hash = models.CharField(max_length=28, unique=True)
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="character_ownerships")
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='character_ownerships')
|
||||||
|
|
||||||
objects: ClassVar[CharacterOwnershipManager] = CharacterOwnershipManager()
|
|
||||||
|
|
||||||
|
objects = CharacterOwnershipManager()
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ('change', 'delete')
|
default_permissions = ('change', 'delete')
|
||||||
ordering = ['user', 'character__character_name']
|
ordering = ['user', 'character__character_name']
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"{self.user}: {self.character}"
|
return f"{self.user}: {self.character}"
|
||||||
|
|
||||||
|
|
||||||
class OwnershipRecord(models.Model):
|
class OwnershipRecord(models.Model):
|
||||||
character = models.ForeignKey(EveCharacter, on_delete=models.CASCADE, related_name="ownership_records")
|
character = models.ForeignKey(EveCharacter, on_delete=models.CASCADE, related_name='ownership_records')
|
||||||
owner_hash = models.CharField(max_length=28, db_index=True)
|
owner_hash = models.CharField(max_length=28, db_index=True)
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="ownership_records")
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='ownership_records')
|
||||||
created = models.DateTimeField(auto_now=True)
|
created = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["-created"]
|
ordering = ['-created']
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"{self.user}: {self.character} on {self.created}"
|
return f"{self.user}: {self.character} on {self.created}"
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ def dashboard_results(hours: int) -> _TaskCounts:
|
|||||||
my_earliest = events.first_event(earliest=earliest)
|
my_earliest = events.first_event(earliest=earliest)
|
||||||
return [my_earliest] if my_earliest else []
|
return [my_earliest] if my_earliest else []
|
||||||
|
|
||||||
earliest = dt.datetime.now(dt.timezone.utc) - dt.timedelta(hours=hours)
|
earliest = dt.datetime.utcnow() - dt.timedelta(hours=hours)
|
||||||
earliest_events = []
|
earliest_events = []
|
||||||
succeeded_count = succeeded_tasks.count(earliest=earliest)
|
succeeded_count = succeeded_tasks.count(earliest=earliest)
|
||||||
earliest_events += earliest_if_exists(succeeded_tasks, earliest)
|
earliest_events += earliest_if_exists(succeeded_tasks, earliest)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class EventSeries:
|
|||||||
- event_time: timestamp of event. Will use current time if not specified.
|
- event_time: timestamp of event. Will use current time if not specified.
|
||||||
"""
|
"""
|
||||||
if not event_time:
|
if not event_time:
|
||||||
event_time = dt.datetime.now(dt.timezone.utc)
|
event_time = dt.datetime.utcnow()
|
||||||
my_id = self._redis.incr(self._key_counter)
|
my_id = self._redis.incr(self._key_counter)
|
||||||
self._redis.zadd(self._key_sorted_set, {my_id: event_time.timestamp()})
|
self._redis.zadd(self._key_sorted_set, {my_id: event_time.timestamp()})
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td style="white-space:initial;">
|
<td style="white-space:initial;">
|
||||||
{% for s in t.scopes.all %}
|
{% for s in t.scopes.all %}
|
||||||
<span class="badge text-bg-secondary">{{ s.name }}</span>
|
<span class="badge bg-secondary">{{ s.name }}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
{% load theme_tags %}
|
{% load theme_tags %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" {% theme_html_tags %}>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<!-- Required meta tags -->
|
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<!-- End Required meta tags -->
|
<meta name="description" content="">
|
||||||
|
<meta name="author" content="">
|
||||||
|
<!-- TODO Bundle all the site specific stuff up into its own template for easy override -->
|
||||||
|
<meta property="og:title" content="{{ SITE_NAME }}">
|
||||||
|
<meta property="og:image" content="{{ SITE_URL }}{% static 'allianceauth/icons/apple-touch-icon.png' %}">
|
||||||
|
<meta property="og:description" content="Alliance Auth - An auth system for EVE Online to help in-game organizations manage online service access.">
|
||||||
|
|
||||||
<!-- Meta tags -->
|
|
||||||
{% include 'allianceauth/opengraph.html' %}
|
|
||||||
{% include 'allianceauth/icons.html' %}
|
{% include 'allianceauth/icons.html' %}
|
||||||
<!-- Meta tags -->
|
|
||||||
|
|
||||||
<title>{% block title %}{% block page_title %}{% endblock page_title %} - {{ SITE_NAME }}{% endblock title %}</title>
|
<title>{% block title %}{% block page_title %}{% endblock page_title %} - {{ SITE_NAME }}{% endblock title %}</title>
|
||||||
|
|
||||||
{% theme_css %}
|
{% theme_css %}
|
||||||
|
|
||||||
{% include 'bundles/fontawesome.html' %}
|
{% include 'bundles/fontawesome.html' %}
|
||||||
{% include 'bundles/auth-framework-css.html' %}
|
|
||||||
|
|
||||||
{% block extra_include %}
|
{% block extra_include %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
<form class="dropdown-item" action="{% url 'set_language' %}" method="post">
|
<div class="dropdown">
|
||||||
{% csrf_token %}
|
<form action="{% url 'set_language' %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
<select class="form-select" onchange="this.form.submit()" id="lang-select" name="language">
|
<select class="form-select" onchange="this.form.submit()" class="form-control" id="lang-select" name="language">
|
||||||
{% get_available_languages as LANGUAGES %}
|
{% get_available_languages as LANGUAGES %}
|
||||||
|
|
||||||
{% for lang_code, lang_name in LANGUAGES %}
|
{% for lang_code, lang_name in LANGUAGES %}
|
||||||
<option lang="{{ lang_code }}" value="{{ lang_code }}"{% if lang_code == LANGUAGE_CODE %} selected{% endif %}>
|
<option lang="{{ lang_code }}" value="{{ lang_code }}"{% if lang_code == LANGUAGE_CODE %} selected{% endif %}>
|
||||||
{{ lang_code|language_name_local|capfirst }} ({{ lang_code }})
|
{{ lang_code|language_name_local|capfirst }} ({{ lang_code }})
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ class TestUserSettingsMiddlewareLoginFlow(TestCase):
|
|||||||
self.request.LANGUAGE_CODE = 'en'
|
self.request.LANGUAGE_CODE = 'en'
|
||||||
self.request.user.profile.language = 'de'
|
self.request.user.profile.language = 'de'
|
||||||
self.request.user.profile.night_mode = True
|
self.request.user.profile.night_mode = True
|
||||||
self.request.user.profile.minimize_sidebar = False
|
|
||||||
self.request.user.is_anonymous = False
|
self.request.user.is_anonymous = False
|
||||||
self.response = Mock()
|
self.response = Mock()
|
||||||
self.response.content = 'hello world'
|
self.response.content = 'hello world'
|
||||||
@@ -174,26 +173,3 @@ class TestUserSettingsMiddlewareLoginFlow(TestCase):
|
|||||||
self.response
|
self.response
|
||||||
)
|
)
|
||||||
self.assertEqual(self.request.session["NIGHT_MODE"], True)
|
self.assertEqual(self.request.session["NIGHT_MODE"], True)
|
||||||
|
|
||||||
def test_middleware_set_mimimize_sidebar(self):
|
|
||||||
"""
|
|
||||||
tests the middleware will always set minimize_sidebar to False (default)
|
|
||||||
"""
|
|
||||||
|
|
||||||
response = self.middleware.process_response(
|
|
||||||
self.request,
|
|
||||||
self.response
|
|
||||||
)
|
|
||||||
self.assertEqual(self.request.session["MINIMIZE_SIDEBAR"], False)
|
|
||||||
|
|
||||||
def test_middleware_minimize_sidebar_when_set(self):
|
|
||||||
"""
|
|
||||||
tests the middleware will set mimimize_sidebar to True from DB
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.request.user.profile.minimize_sidebar = True
|
|
||||||
response = self.middleware.process_response(
|
|
||||||
self.request,
|
|
||||||
self.response
|
|
||||||
)
|
|
||||||
self.assertEqual(self.request.session["MINIMIZE_SIDEBAR"], True)
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ from allianceauth.authentication.views import esi_check, task_counts
|
|||||||
from allianceauth.tests.auth_utils import AuthUtils
|
from allianceauth.tests.auth_utils import AuthUtils
|
||||||
|
|
||||||
MODULE_PATH = "allianceauth.authentication.views"
|
MODULE_PATH = "allianceauth.authentication.views"
|
||||||
TEMPLATETAGS_PATH = "allianceauth.templatetags.admin_status"
|
|
||||||
|
|
||||||
|
|
||||||
def jsonresponse_to_dict(response) -> dict:
|
def jsonresponse_to_dict(response) -> dict:
|
||||||
@@ -19,7 +18,6 @@ def jsonresponse_to_dict(response) -> dict:
|
|||||||
|
|
||||||
@patch(MODULE_PATH + ".queued_tasks_count")
|
@patch(MODULE_PATH + ".queued_tasks_count")
|
||||||
@patch(MODULE_PATH + ".active_tasks_count")
|
@patch(MODULE_PATH + ".active_tasks_count")
|
||||||
@patch(MODULE_PATH + "._celery_stats")
|
|
||||||
class TestRunningTasksCount(TestCase):
|
class TestRunningTasksCount(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls) -> None:
|
def setUpClass(cls) -> None:
|
||||||
@@ -29,64 +27,36 @@ class TestRunningTasksCount(TestCase):
|
|||||||
cls.user.is_superuser = True
|
cls.user.is_superuser = True
|
||||||
cls.user.save()
|
cls.user.save()
|
||||||
|
|
||||||
def test_should_return_data(self, mock_celery_stats, mock_tasks_queued, mock_tasks_running):
|
def test_should_return_data(
|
||||||
|
self, mock_active_tasks_count, mock_queued_tasks_count
|
||||||
|
):
|
||||||
# given
|
# given
|
||||||
mock_tasks_running.return_value = 2
|
mock_active_tasks_count.return_value = 2
|
||||||
mock_tasks_queued.return_value = 3
|
mock_queued_tasks_count.return_value = 3
|
||||||
mock_celery_stats.return_value = {
|
|
||||||
"tasks_succeeded": 5,
|
|
||||||
"tasks_retried": 1,
|
|
||||||
"tasks_failed": 4,
|
|
||||||
"tasks_total": 11,
|
|
||||||
"tasks_hours": 24,
|
|
||||||
"earliest_task": "2025-08-14T22:47:54.853Z",
|
|
||||||
}
|
|
||||||
|
|
||||||
request = self.factory.get("/")
|
request = self.factory.get("/")
|
||||||
request.user = self.user
|
request.user = self.user
|
||||||
|
|
||||||
# when
|
# when
|
||||||
response = task_counts(request)
|
response = task_counts(request)
|
||||||
|
|
||||||
# then
|
# then
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertDictEqual(
|
self.assertDictEqual(
|
||||||
jsonresponse_to_dict(response),
|
jsonresponse_to_dict(response), {
|
||||||
{
|
"tasks_running": 2, "tasks_queued": 3}
|
||||||
"tasks_succeeded": 5,
|
|
||||||
"tasks_retried": 1,
|
|
||||||
"tasks_failed": 4,
|
|
||||||
"tasks_total": 11,
|
|
||||||
"tasks_hours": 24,
|
|
||||||
"earliest_task": "2025-08-14T22:47:54.853Z",
|
|
||||||
"tasks_running": 3,
|
|
||||||
"tasks_queued": 2,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_su_only(self, mock_celery_stats, mock_tasks_queued, mock_tasks_running):
|
def test_su_only(
|
||||||
|
self, mock_active_tasks_count, mock_queued_tasks_count
|
||||||
|
):
|
||||||
self.user.is_superuser = False
|
self.user.is_superuser = False
|
||||||
self.user.save()
|
self.user.save()
|
||||||
self.user.refresh_from_db()
|
self.user.refresh_from_db()
|
||||||
|
|
||||||
# given
|
# given
|
||||||
mock_tasks_running.return_value = 2
|
mock_active_tasks_count.return_value = 2
|
||||||
mock_tasks_queued.return_value = 3
|
mock_queued_tasks_count.return_value = 3
|
||||||
mock_celery_stats.return_value = {
|
|
||||||
"tasks_succeeded": 5,
|
|
||||||
"tasks_retried": 1,
|
|
||||||
"tasks_failed": 4,
|
|
||||||
"tasks_total": 11,
|
|
||||||
"tasks_hours": 24,
|
|
||||||
"earliest_task": "2025-08-14T22:47:54.853Z",
|
|
||||||
}
|
|
||||||
|
|
||||||
request = self.factory.get("/")
|
request = self.factory.get("/")
|
||||||
request.user = self.user
|
request.user = self.user
|
||||||
|
|
||||||
# when
|
# when
|
||||||
response = task_counts(request)
|
response = task_counts(request)
|
||||||
|
|
||||||
# then
|
# then
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ from allianceauth.hooks import get_hooks
|
|||||||
|
|
||||||
from .constants import ESI_ERROR_MESSAGE_OVERRIDES
|
from .constants import ESI_ERROR_MESSAGE_OVERRIDES
|
||||||
from .core.celery_workers import active_tasks_count, queued_tasks_count
|
from .core.celery_workers import active_tasks_count, queued_tasks_count
|
||||||
from allianceauth.admin_status.templatetags.admin_status import _celery_stats
|
|
||||||
from .forms import RegistrationForm
|
from .forms import RegistrationForm
|
||||||
from .models import CharacterOwnership
|
from .models import CharacterOwnership
|
||||||
|
|
||||||
@@ -369,10 +368,10 @@ def registration_closed(request):
|
|||||||
@user_passes_test(lambda u: u.is_superuser)
|
@user_passes_test(lambda u: u.is_superuser)
|
||||||
def task_counts(request) -> JsonResponse:
|
def task_counts(request) -> JsonResponse:
|
||||||
"""Return task counts as JSON for an AJAX call."""
|
"""Return task counts as JSON for an AJAX call."""
|
||||||
data = _celery_stats()
|
data = {
|
||||||
data.update(
|
"tasks_running": active_tasks_count(),
|
||||||
{"tasks_running": active_tasks_count(), "tasks_queued": queued_tasks_count()}
|
"tasks_queued": queued_tasks_count()
|
||||||
)
|
}
|
||||||
return JsonResponse(data)
|
return JsonResponse(data)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ class StartProject(BaseStartProject):
|
|||||||
parser.add_argument('--celery', help='The path to the celery executable.')
|
parser.add_argument('--celery', help='The path to the celery executable.')
|
||||||
parser.add_argument('--gunicorn', help='The path to the gunicorn executable.')
|
parser.add_argument('--gunicorn', help='The path to the gunicorn executable.')
|
||||||
parser.add_argument('--memmon', help='The path to the memmon executable.')
|
parser.add_argument('--memmon', help='The path to the memmon executable.')
|
||||||
parser.add_argument('--venv_directory', help='The path to the virtual environment directory.')
|
|
||||||
|
|
||||||
|
|
||||||
def create_project(parser, options, args):
|
def create_project(parser, options, args):
|
||||||
@@ -29,7 +28,7 @@ def create_project(parser, options, args):
|
|||||||
allianceauth_path = os.path.dirname(allianceauth.__file__)
|
allianceauth_path = os.path.dirname(allianceauth.__file__)
|
||||||
template_path = os.path.join(allianceauth_path, 'project_template')
|
template_path = os.path.join(allianceauth_path, 'project_template')
|
||||||
|
|
||||||
# Determine locations of commands to render supervisor configuration
|
# Determine locations of commands to render supervisor cond
|
||||||
command_options = {
|
command_options = {
|
||||||
'template': template_path,
|
'template': template_path,
|
||||||
'python': shutil.which('python'),
|
'python': shutil.which('python'),
|
||||||
@@ -37,7 +36,6 @@ def create_project(parser, options, args):
|
|||||||
'celery': shutil.which('celery'),
|
'celery': shutil.which('celery'),
|
||||||
'memmon': shutil.which('memmon'),
|
'memmon': shutil.which('memmon'),
|
||||||
'extensions': ['py', 'conf', 'json'],
|
'extensions': ['py', 'conf', 'json'],
|
||||||
'venv_directory': os.getenv('VIRTUAL_ENV'),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Strip 'start' out of the arguments, leaving project name (and optionally destination dir)
|
# Strip 'start' out of the arguments, leaving project name (and optionally destination dir)
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
class CorpUtilsConfig(AppConfig):
|
class CorpUtilsConfig(AppConfig):
|
||||||
name = 'allianceauth.corputils'
|
name = 'allianceauth.corputils'
|
||||||
label = 'corputils'
|
label = 'corputils'
|
||||||
verbose_name = _('Corporation Stats')
|
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
# Generated by Django 5.1.6 on 2025-03-04 01:29
|
|
||||||
|
|
||||||
# This was built by Deleting Every Migration, Creating one from scratch
|
|
||||||
# And porting in anything necessary
|
|
||||||
# Some functions were skipped as they only make sense _if you are migrating in place_
|
|
||||||
# i.e. permissions migration
|
|
||||||
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
replaces = [('corputils', '0001_initial'), ('corputils', '0002_migrate_permissions'), ('corputils', '0003_granular_permissions'), ('corputils', '0004_member_models'), ('corputils', '0005_cleanup_permissions')]
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('esi', '0012_fix_token_type_choices'),
|
|
||||||
('eveonline', '0019_v5squash'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='CorpStats',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('last_update', models.DateTimeField(auto_now=True)),
|
|
||||||
('corp', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='eveonline.evecorporationinfo')),
|
|
||||||
('token', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='esi.token')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'corp stats',
|
|
||||||
'verbose_name_plural': 'corp stats',
|
|
||||||
'permissions': (('view_corp_corpstats', 'Can view corp stats of their corporation.'), ('view_alliance_corpstats', 'Can view corp stats of members of their alliance.'), ('view_state_corpstats', 'Can view corp stats of members of their auth state.')),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='CorpMember',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('character_id', models.PositiveIntegerField()),
|
|
||||||
('character_name', models.CharField(max_length=37)),
|
|
||||||
('corpstats', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='members', to='corputils.corpstats')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'ordering': ['character_name'],
|
|
||||||
'unique_together': {('corpstats', 'character_id')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import ClassVar
|
|
||||||
|
|
||||||
from bravado.exception import HTTPForbidden
|
from bravado.exception import HTTPForbidden
|
||||||
|
|
||||||
@@ -34,8 +33,7 @@ class CorpStats(models.Model):
|
|||||||
corp = models.OneToOneField(EveCorporationInfo, on_delete=models.CASCADE)
|
corp = models.OneToOneField(EveCorporationInfo, on_delete=models.CASCADE)
|
||||||
last_update = models.DateTimeField(auto_now=True)
|
last_update = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
objects: ClassVar[CorpStatsManager] = CorpStatsManager()
|
objects = CorpStatsManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
permissions = (
|
permissions = (
|
||||||
('view_corp_corpstats', 'Can view corp stats of their corporation.'),
|
('view_corp_corpstats', 'Can view corp stats of their corporation.'),
|
||||||
@@ -45,10 +43,12 @@ class CorpStats(models.Model):
|
|||||||
verbose_name = "corp stats"
|
verbose_name = "corp stats"
|
||||||
verbose_name_plural = "corp stats"
|
verbose_name_plural = "corp stats"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"{self.__class__.__name__} for {self.corp}"
|
return f"{self.__class__.__name__} for {self.corp}"
|
||||||
|
|
||||||
def update(self) -> None:
|
def update(self):
|
||||||
try:
|
try:
|
||||||
c = self.token.get_esi_client(spec_file=SWAGGER_SPEC_PATH)
|
c = self.token.get_esi_client(spec_file=SWAGGER_SPEC_PATH)
|
||||||
assert c.Character.get_characters_character_id(character_id=self.token.character_id).result()['corporation_id'] == int(self.corp.corporation_id)
|
assert c.Character.get_characters_character_id(character_id=self.token.character_id).result()['corporation_id'] == int(self.corp.corporation_id)
|
||||||
@@ -101,11 +101,11 @@ class CorpStats(models.Model):
|
|||||||
return self.members.count()
|
return self.members.count()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def user_count(self) -> int:
|
def user_count(self):
|
||||||
return len({m.main_character for m in self.members.all() if m.main_character})
|
return len({m.main_character for m in self.members.all() if m.main_character})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def registered_member_count(self) -> int:
|
def registered_member_count(self):
|
||||||
return len(self.registered_members)
|
return len(self.registered_members)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -121,7 +121,7 @@ class CorpStats(models.Model):
|
|||||||
return self.members.filter(pk__in=[m.pk for m in self.members.all() if not m.registered])
|
return self.members.filter(pk__in=[m.pk for m in self.members.all() if not m.registered])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def main_count(self) -> int:
|
def main_count(self):
|
||||||
return len(self.mains)
|
return len(self.mains)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -134,10 +134,10 @@ class CorpStats(models.Model):
|
|||||||
def can_update(self, user):
|
def can_update(self, user):
|
||||||
return self.token.user == user or self.visible_to(user)
|
return self.token.user == user or self.visible_to(user)
|
||||||
|
|
||||||
def corp_logo(self, size=128) -> str:
|
def corp_logo(self, size=128):
|
||||||
return self.corp.logo_url(size)
|
return self.corp.logo_url(size)
|
||||||
|
|
||||||
def alliance_logo(self, size=128) -> str:
|
def alliance_logo(self, size=128):
|
||||||
if self.corp.alliance:
|
if self.corp.alliance:
|
||||||
return self.corp.alliance.logo_url(size)
|
return self.corp.alliance.logo_url(size)
|
||||||
else:
|
else:
|
||||||
@@ -158,7 +158,7 @@ class CorpMember(models.Model):
|
|||||||
return self.character_name
|
return self.character_name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def character(self) -> EveCharacter | None:
|
def character(self):
|
||||||
try:
|
try:
|
||||||
return EveCharacter.objects.get(character_id=self.character_id)
|
return EveCharacter.objects.get(character_id=self.character_id)
|
||||||
except EveCharacter.DoesNotExist:
|
except EveCharacter.DoesNotExist:
|
||||||
@@ -179,20 +179,20 @@ class CorpMember(models.Model):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def registered(self) -> bool:
|
def registered(self):
|
||||||
return CharacterOwnership.objects.filter(character__character_id=self.character_id).exists()
|
return CharacterOwnership.objects.filter(character__character_id=self.character_id).exists()
|
||||||
|
|
||||||
def portrait_url(self, size=32) -> str:
|
def portrait_url(self, size=32):
|
||||||
return EveCharacter.generic_portrait_url(self.character_id, size)
|
return EveCharacter.generic_portrait_url(self.character_id, size)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def portrait_url_32(self) -> str:
|
def portrait_url_32(self):
|
||||||
return self.portrait_url(32)
|
return self.portrait_url(32)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def portrait_url_64(self) -> str:
|
def portrait_url_64(self):
|
||||||
return self.portrait_url(64)
|
return self.portrait_url(64)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def portrait_url_128(self) -> str:
|
def portrait_url_128(self):
|
||||||
return self.portrait_url(128)
|
return self.portrait_url(128)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
{% endblock header_nav_brand %}
|
{% endblock header_nav_brand %}
|
||||||
|
|
||||||
{% block header_nav_collapse_left %}
|
{% block header_nav_collapse_left %}
|
||||||
<li class="nav-item dropdown mb-2 mb-lg-0">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false">
|
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false">
|
||||||
{% translate "Corporations" %}
|
{% translate "Corporations" %}
|
||||||
</a>
|
</a>
|
||||||
@@ -26,7 +26,11 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if perms.corputils.add_corpstats %}
|
{% if perms.corputils.add_corpstats %}
|
||||||
<li class="mt-3">
|
{% if available.count >= 1 %}
|
||||||
|
<li> </li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<li>
|
||||||
<a class="dropdown-item" href="{% url 'corputils:add' %}">
|
<a class="dropdown-item" href="{% url 'corputils:add' %}">
|
||||||
{% translate "Add corporation" %}
|
{% translate "Add corporation" %}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -138,7 +138,7 @@
|
|||||||
<td style="width: 30%;">{{ alt.corporation_name }}</td>
|
<td style="width: 30%;">{{ alt.corporation_name }}</td>
|
||||||
<td style="width: 30%;">{{ alt.alliance_name|default_if_none:"" }}</td>
|
<td style="width: 30%;">{{ alt.alliance_name|default_if_none:"" }}</td>
|
||||||
<td style="width: 5%;">
|
<td style="width: 5%;">
|
||||||
<a href="https://zkillboard.com/character/{{ alt.character_id }}/" class="badge text-bg-danger" target="_blank">
|
<a href="https://zkillboard.com/character/{{ alt.character_id }}/" class="badge bg-danger" target="_blank">
|
||||||
{% translate "Killboard" %}
|
{% translate "Killboard" %}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@@ -175,7 +175,7 @@
|
|||||||
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member }}"></td>
|
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member }}"></td>
|
||||||
<td>{{ member }}</td>
|
<td>{{ member }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="badge text-bg-danger" target="_blank">{% translate "Killboard" %}</a>
|
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="badge bg-danger" target="_blank">{% translate "Killboard" %}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ member.character_ownership.user.profile.main_character.character_name }}</td>
|
<td>{{ member.character_ownership.user.profile.main_character.character_name }}</td>
|
||||||
<td>{{ member.character_ownership.user.profile.main_character.corporation_name }}</td>
|
<td>{{ member.character_ownership.user.profile.main_character.corporation_name }}</td>
|
||||||
@@ -188,7 +188,7 @@
|
|||||||
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member.character_name }}"></td>
|
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member.character_name }}"></td>
|
||||||
<td>{{ member.character_name }}</td>
|
<td>{{ member.character_name }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="badge text-bg-danger" target="_blank">{% translate "Killboard" %}</a>
|
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="badge bg-danger" target="_blank">{% translate "Killboard" %}</a>
|
||||||
</td>
|
</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td></td>
|
<td></td>
|
||||||
@@ -219,7 +219,7 @@
|
|||||||
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member.character_name }}"></td>
|
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member.character_name }}"></td>
|
||||||
<td>{{ member.character_name }}</td>
|
<td>{{ member.character_name }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="badge text-bg-danger" target="_blank">
|
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="badge bg-danger" target="_blank">
|
||||||
{% translate "Killboard" %}
|
{% translate "Killboard" %}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
<td><img src="{{ result.1.portrait_url }}" class="img-circle" alt="{{ result.1.character_name }}"></td>
|
<td><img src="{{ result.1.portrait_url }}" class="img-circle" alt="{{ result.1.character_name }}"></td>
|
||||||
<td>{{ result.1.character_name }}</td>
|
<td>{{ result.1.character_name }}</td>
|
||||||
<td >{{ result.0.corp.corporation_name }}</td>
|
<td >{{ result.0.corp.corporation_name }}</td>
|
||||||
<td><a href="https://zkillboard.com/character/{{ result.1.character_id }}/" class="badge text-bg-danger" target="_blank">{% translate "Killboard" %}</a></td>
|
<td><a href="https://zkillboard.com/character/{{ result.1.character_id }}/" class="badge bg-danger" target="_blank">{% translate "Killboard" %}</a></td>
|
||||||
<td>{{ result.1.main_character.character_name }}</td>
|
<td>{{ result.1.main_character.character_name }}</td>
|
||||||
<td>{{ result.1.main_character.corporation_name }}</td>
|
<td>{{ result.1.main_character.corporation_name }}</td>
|
||||||
<td>{{ result.1.main_character.alliance_name }}</td>
|
<td>{{ result.1.main_character.alliance_name }}</td>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ Crontab App Config
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
class CrontabConfig(AppConfig):
|
class CrontabConfig(AppConfig):
|
||||||
@@ -13,4 +12,3 @@ class CrontabConfig(AppConfig):
|
|||||||
|
|
||||||
name = "allianceauth.crontab"
|
name = "allianceauth.crontab"
|
||||||
label = "crontab"
|
label = "crontab"
|
||||||
verbose_name = _("Crontab")
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ def random_default() -> float:
|
|||||||
|
|
||||||
|
|
||||||
class CronOffset(SingletonModel):
|
class CronOffset(SingletonModel):
|
||||||
|
|
||||||
minute = models.FloatField(_("Minute Offset"), default=random_default)
|
minute = models.FloatField(_("Minute Offset"), default=random_default)
|
||||||
hour = models.FloatField(_("Hour Offset"), default=random_default)
|
hour = models.FloatField(_("Hour Offset"), default=random_default)
|
||||||
day_of_month = models.FloatField(_("Day of Month Offset"), default=random_default)
|
day_of_month = models.FloatField(_("Day of Month Offset"), default=random_default)
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 5.1.6 on 2025-03-05 00:59
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('custom_css', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='customcss',
|
|
||||||
name='css',
|
|
||||||
field=models.TextField(blank=True, default='', help_text='This CSS will be added to the site after the default CSS.', verbose_name='Your custom CSS'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -22,7 +22,6 @@ class CustomCSS(SingletonModel):
|
|||||||
css = models.TextField(
|
css = models.TextField(
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_("Your custom CSS"),
|
verbose_name=_("Your custom CSS"),
|
||||||
default="",
|
|
||||||
help_text=_("This CSS will be added to the site after the default CSS."),
|
help_text=_("This CSS will be added to the site after the default CSS."),
|
||||||
)
|
)
|
||||||
timestamp = models.DateTimeField(auto_now=True)
|
timestamp = models.DateTimeField(auto_now=True)
|
||||||
@@ -46,7 +45,7 @@ class CustomCSS(SingletonModel):
|
|||||||
|
|
||||||
return str(_("Custom CSS"))
|
return str(_("Custom CSS"))
|
||||||
|
|
||||||
def save(self, *args, **kwargs) -> None:
|
def save(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Save method for CustomCSS
|
Save method for CustomCSS
|
||||||
|
|
||||||
@@ -62,7 +61,9 @@ class CustomCSS(SingletonModel):
|
|||||||
|
|
||||||
if self.css and len(self.css.replace(" ", "")) > 0:
|
if self.css and len(self.css.replace(" ", "")) > 0:
|
||||||
# Write the custom CSS to a file
|
# Write the custom CSS to a file
|
||||||
custom_css_file = open(f"{settings.STATIC_ROOT}allianceauth/custom-styles.css", "w+")
|
custom_css_file = open(
|
||||||
|
f"{settings.STATIC_ROOT}allianceauth/custom-styles.css", "w+"
|
||||||
|
)
|
||||||
custom_css_file.write(self.compress_css())
|
custom_css_file.write(self.compress_css())
|
||||||
custom_css_file.close()
|
custom_css_file.close()
|
||||||
else:
|
else:
|
||||||
@@ -104,7 +105,9 @@ class CustomCSS(SingletonModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Fragment values can loose zeros
|
# Fragment values can loose zeros
|
||||||
css = re.sub(pattern=r":\s*0(\.\d+([cm]m|e[mx]|in|p[ctx]))\s*;", repl=r":\1;", string=css)
|
css = re.sub(
|
||||||
|
pattern=r":\s*0(\.\d+([cm]m|e[mx]|in|p[ctx]))\s*;", repl=r":\1;", string=css
|
||||||
|
)
|
||||||
|
|
||||||
for rule in re.findall(pattern=r"([^{]+){([^}]*)}", string=css):
|
for rule in re.findall(pattern=r"([^{]+){([^}]*)}", string=css):
|
||||||
# We don't need spaces around operators
|
# We don't need spaces around operators
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from pathlib import Path
|
|||||||
# Django
|
# Django
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.template.defaulttags import register
|
from django.template.defaulttags import register
|
||||||
|
from django.templatetags.static import static
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from allianceauth.custom_css.models import CustomCSS
|
from allianceauth.custom_css.models import CustomCSS
|
||||||
@@ -19,7 +20,7 @@ def custom_css_static(path: str) -> str:
|
|||||||
Versioned static URL
|
Versioned static URL
|
||||||
This is to make sure to break the browser cache on CSS updates.
|
This is to make sure to break the browser cache on CSS updates.
|
||||||
|
|
||||||
Example: /static/allianceauth/custom-styles.css?v=1752004819.555084
|
Example: /static/allianceauth/custom-styles.css?v=1234567890
|
||||||
|
|
||||||
:param path:
|
:param path:
|
||||||
:type path:
|
:type path:
|
||||||
@@ -41,6 +42,7 @@ def custom_css_static(path: str) -> str:
|
|||||||
custom_css_version = (
|
custom_css_version = (
|
||||||
str(custom_css_changed).replace(" ", "").replace(":", "").replace("-", "")
|
str(custom_css_changed).replace(" ", "").replace(":", "").replace("-", "")
|
||||||
) # remove spaces, colons, and dashes
|
) # remove spaces, colons, and dashes
|
||||||
versioned_url = f"{settings.STATIC_URL}{path}?v={custom_css_version}"
|
static_url = static(path)
|
||||||
|
versioned_url = static_url + "?v=" + custom_css_version
|
||||||
|
|
||||||
return mark_safe(f'<link rel="stylesheet" href="{versioned_url}">')
|
return mark_safe(f'<link rel="stylesheet" href="{versioned_url}">')
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
class EveonlineConfig(AppConfig):
|
class EveonlineConfig(AppConfig):
|
||||||
name = 'allianceauth.eveonline'
|
name = 'allianceauth.eveonline'
|
||||||
label = 'eveonline'
|
label = 'eveonline'
|
||||||
verbose_name = _('EVE Online')
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
class EveAutogroupsConfig(AppConfig):
|
class EveAutogroupsConfig(AppConfig):
|
||||||
name = 'allianceauth.eveonline.autogroups'
|
name = 'allianceauth.eveonline.autogroups'
|
||||||
label = 'eve_autogroups'
|
label = 'eve_autogroups'
|
||||||
verbose_name = _('EVE Online Autogroups')
|
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# Generated by Django 5.1.6 on 2025-03-05 02:08
|
# Generated by Django 1.11.6 on 2017-12-23 04:30
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
@@ -10,9 +9,9 @@ class Migration(migrations.Migration):
|
|||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
('authentication', '0015_user_profiles'),
|
||||||
('authentication', '0025_v5squash'),
|
('auth', '0008_alter_user_username_max_length'),
|
||||||
('eveonline', '0019_v5squash'),
|
('eveonline', '0009_on_delete'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@@ -28,16 +27,27 @@ class Migration(migrations.Migration):
|
|||||||
('alliance_name_source', models.CharField(choices=[('ticker', 'Ticker'), ('name', 'Full name')], default='name', max_length=20)),
|
('alliance_name_source', models.CharField(choices=[('ticker', 'Ticker'), ('name', 'Full name')], default='name', max_length=20)),
|
||||||
('replace_spaces', models.BooleanField(default=False)),
|
('replace_spaces', models.BooleanField(default=False)),
|
||||||
('replace_spaces_with', models.CharField(blank=True, default='', help_text='Any spaces in the group name will be replaced with this.', max_length=10)),
|
('replace_spaces_with', models.CharField(blank=True, default='', help_text='Any spaces in the group name will be replaced with this.', max_length=10)),
|
||||||
('states', models.ManyToManyField(related_name='autogroups', to='authentication.state')),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='ManagedAllianceGroup',
|
name='ManagedAllianceGroup',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('alliance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eveonline.eveallianceinfo')),
|
('alliance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eveonline.EveAllianceInfo')),
|
||||||
('config', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eve_autogroups.autogroupsconfig')),
|
('config', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eve_autogroups.AutogroupsConfig')),
|
||||||
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.group')),
|
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.Group')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ManagedCorpGroup',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('config', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eve_autogroups.AutogroupsConfig')),
|
||||||
|
('corp', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eveonline.EveCorporationInfo')),
|
||||||
|
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.Group')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'abstract': False,
|
'abstract': False,
|
||||||
@@ -46,23 +56,16 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='autogroupsconfig',
|
model_name='autogroupsconfig',
|
||||||
name='alliance_managed_groups',
|
name='alliance_managed_groups',
|
||||||
field=models.ManyToManyField(help_text="A list of alliance groups created and maintained by this AutogroupConfig. You should not edit this list unless you know what you're doing.", related_name='alliance_managed_config', through='eve_autogroups.ManagedAllianceGroup', to='auth.group'),
|
field=models.ManyToManyField(help_text="A list of alliance groups created and maintained by this AutogroupConfig. You should not edit this list unless you know what you're doing.", related_name='alliance_managed_config', through='eve_autogroups.ManagedAllianceGroup', to='auth.Group'),
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ManagedCorpGroup',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('config', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eve_autogroups.autogroupsconfig')),
|
|
||||||
('corp', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eveonline.evecorporationinfo')),
|
|
||||||
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.group')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='autogroupsconfig',
|
model_name='autogroupsconfig',
|
||||||
name='corp_managed_groups',
|
name='corp_managed_groups',
|
||||||
field=models.ManyToManyField(help_text="A list of corporation groups created and maintained by this AutogroupConfig. You should not edit this list unless you know what you're doing.", related_name='corp_managed_config', through='eve_autogroups.ManagedCorpGroup', to='auth.group'),
|
field=models.ManyToManyField(help_text="A list of corporation groups created and maintained by this AutogroupConfig. You should not edit this list unless you know what you're doing.", related_name='corp_managed_config', through='eve_autogroups.ManagedCorpGroup', to='auth.Group'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='autogroupsconfig',
|
||||||
|
name='states',
|
||||||
|
field=models.ManyToManyField(related_name='autogroups', to='authentication.State'),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import ClassVar
|
|
||||||
from django.db import models, transaction
|
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth.models import Group, User
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
@@ -40,13 +39,13 @@ class AutogroupsConfigManager(models.Manager):
|
|||||||
"""
|
"""
|
||||||
if state is None:
|
if state is None:
|
||||||
state = user.profile.state
|
state = user.profile.state
|
||||||
|
for config in self.filter(states=state):
|
||||||
|
# grant user new groups for their state
|
||||||
|
config.update_group_membership_for_user(user)
|
||||||
for config in self.exclude(states=state):
|
for config in self.exclude(states=state):
|
||||||
# ensure user does not have groups from previous state
|
# ensure user does not have groups from previous state
|
||||||
config.remove_user_from_alliance_groups(user)
|
config.remove_user_from_alliance_groups(user)
|
||||||
config.remove_user_from_corp_groups(user)
|
config.remove_user_from_corp_groups(user)
|
||||||
for config in self.filter(states=state):
|
|
||||||
# grant user new groups for their state
|
|
||||||
config.update_group_membership_for_user(user)
|
|
||||||
|
|
||||||
|
|
||||||
class AutogroupsConfig(models.Model):
|
class AutogroupsConfig(models.Model):
|
||||||
@@ -80,25 +79,25 @@ class AutogroupsConfig(models.Model):
|
|||||||
max_length=10, default='', blank=True,
|
max_length=10, default='', blank=True,
|
||||||
help_text='Any spaces in the group name will be replaced with this.')
|
help_text='Any spaces in the group name will be replaced with this.')
|
||||||
|
|
||||||
objects: ClassVar[AutogroupsConfigManager] = AutogroupsConfigManager()
|
objects = AutogroupsConfigManager()
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return 'States: ' + (' '.join(list(self.states.all().values_list('name', flat=True))) if self.pk else str(None))
|
return 'States: ' + (' '.join(list(self.states.all().values_list('name', flat=True))) if self.pk else str(None))
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self):
|
||||||
return self.__class__.__name__
|
return self.__class__.__name__
|
||||||
|
|
||||||
def update_all_states_group_membership(self) -> None:
|
def update_all_states_group_membership(self):
|
||||||
list(map(self.update_group_membership_for_state, self.states.all()))
|
list(map(self.update_group_membership_for_state, self.states.all()))
|
||||||
|
|
||||||
def update_group_membership_for_state(self, state: State):
|
def update_group_membership_for_state(self, state: State):
|
||||||
list(map(self.update_group_membership_for_user, get_users_for_state(state)))
|
list(map(self.update_group_membership_for_user, get_users_for_state(state)))
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def update_group_membership_for_user(self, user: User) -> None:
|
def update_group_membership_for_user(self, user: User):
|
||||||
self.update_alliance_group_membership(user)
|
self.update_alliance_group_membership(user)
|
||||||
self.update_corp_group_membership(user)
|
self.update_corp_group_membership(user)
|
||||||
|
|
||||||
@@ -239,7 +238,6 @@ class ManagedGroup(models.Model):
|
|||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"Managed Group: {self.group.name}"
|
return f"Managed Group: {self.group.name}"
|
||||||
|
|
||||||
|
|
||||||
class ManagedCorpGroup(ManagedGroup):
|
class ManagedCorpGroup(ManagedGroup):
|
||||||
corp = models.ForeignKey(EveCorporationInfo, on_delete=models.CASCADE)
|
corp = models.ForeignKey(EveCorporationInfo, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
from allianceauth.eveonline.models import EveCorporationInfo
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from allianceauth.tests.auth_utils import AuthUtils
|
from allianceauth.tests.auth_utils import AuthUtils
|
||||||
@@ -75,51 +74,3 @@ class AutogroupsConfigManagerTestCase(TestCase):
|
|||||||
AutogroupsConfig.objects.update_groups_for_user(member)
|
AutogroupsConfig.objects.update_groups_for_user(member)
|
||||||
|
|
||||||
self.assertTrue(update_groups.called)
|
self.assertTrue(update_groups.called)
|
||||||
|
|
||||||
def test_update_group_membership_corp_in_two_configs(self):
|
|
||||||
# given
|
|
||||||
member = AuthUtils.create_member('test member')
|
|
||||||
AuthUtils.add_main_character_2(
|
|
||||||
member,
|
|
||||||
character_id='1234',
|
|
||||||
name='test character',
|
|
||||||
corp_id='2345',
|
|
||||||
corp_name='corp name',
|
|
||||||
corp_ticker='TIKK',
|
|
||||||
|
|
||||||
)
|
|
||||||
|
|
||||||
corp = EveCorporationInfo.objects.create(
|
|
||||||
corporation_id='2345',
|
|
||||||
corporation_name='corp name',
|
|
||||||
corporation_ticker='TIKK',
|
|
||||||
member_count=10,
|
|
||||||
)
|
|
||||||
|
|
||||||
member_state = AuthUtils.get_member_state()
|
|
||||||
member_config = AutogroupsConfig.objects.create(corp_groups=True)
|
|
||||||
member_config.states.add(member_state)
|
|
||||||
blue_state = AuthUtils.get_blue_state()
|
|
||||||
blue_state.member_corporations.add(corp)
|
|
||||||
blue_config = AutogroupsConfig.objects.create(corp_groups=True)
|
|
||||||
blue_config.states.add(blue_state)
|
|
||||||
|
|
||||||
member.profile.state = blue_state
|
|
||||||
member.profile.save()
|
|
||||||
|
|
||||||
AutogroupsConfig.objects.update_groups_for_user(member)
|
|
||||||
|
|
||||||
# Checks before test that the role is correctly applied
|
|
||||||
group = blue_config.get_corp_group(corp)
|
|
||||||
self.assertIn(group, member.groups.all())
|
|
||||||
|
|
||||||
# when
|
|
||||||
blue_state.member_corporations.remove(corp)
|
|
||||||
member_state.member_corporations.add(corp)
|
|
||||||
member.profile.state = member_state
|
|
||||||
member.profile.save()
|
|
||||||
|
|
||||||
# then
|
|
||||||
AutogroupsConfig.objects.update_groups_for_user(member)
|
|
||||||
group = member_config.get_corp_group(corp)
|
|
||||||
self.assertIn(group, member.groups.all())
|
|
||||||
|
|||||||
@@ -15,20 +15,10 @@ class EveCharacterProviderManager:
|
|||||||
class EveCharacterManager(models.Manager):
|
class EveCharacterManager(models.Manager):
|
||||||
provider = EveCharacterProviderManager()
|
provider = EveCharacterProviderManager()
|
||||||
|
|
||||||
def exclude_biomassed(self):
|
def create_character(self, character_id):
|
||||||
"""
|
|
||||||
Get a queryset of EveCharacter objects, excluding the "Doomheim" corporation (1000001).
|
|
||||||
|
|
||||||
:return:
|
|
||||||
:rtype:
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.exclude(corporation_id=1000001)
|
|
||||||
|
|
||||||
def create_character(self, character_id) -> models.Model:
|
|
||||||
return self.create_character_obj(self.provider.get_character(character_id))
|
return self.create_character_obj(self.provider.get_character(character_id))
|
||||||
|
|
||||||
def create_character_obj(self, character: providers.Character) -> models.Model:
|
def create_character_obj(self, character: providers.Character):
|
||||||
return self.create(
|
return self.create(
|
||||||
character_id=character.id,
|
character_id=character.id,
|
||||||
character_name=character.name,
|
character_name=character.name,
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
# Generated by Django 5.1.6 on 2025-03-04 01:29
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('eveonline', '0017_alliance_and_corp_names_are_not_unique'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='evecharacter',
|
|
||||||
name='alliance_name',
|
|
||||||
field=models.CharField(blank=True, default='', max_length=254, null=True),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='evecharacter',
|
|
||||||
name='alliance_ticker',
|
|
||||||
field=models.CharField(blank=True, default='', max_length=5, null=True),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='evecharacter',
|
|
||||||
name='faction_name',
|
|
||||||
field=models.CharField(blank=True, default='', max_length=254, null=True),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
# Generated by Django 5.1.6 on 2025-03-05 02:19
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
replaces = [('eveonline', '0001_initial'), ('eveonline', '0002_remove_eveapikeypair_error_count'), ('eveonline', '0003_auto_20161026_0149'), ('eveonline', '0004_eveapikeypair_sso_verified'), ('eveonline', '0005_remove_eveallianceinfo_member_count'), ('eveonline', '0006_allow_null_evecharacter_alliance'), ('eveonline', '0007_unique_id_name'), ('eveonline', '0008_remove_apikeys'), ('eveonline', '0009_on_delete'), ('eveonline', '0010_alliance_ticker'), ('eveonline', '0011_ids_to_integers'), ('eveonline', '0012_index_additions'), ('eveonline', '0013_evecorporationinfo_ceo_id'), ('eveonline', '0014_auto_20210105_1413'), ('eveonline', '0015_factions'), ('eveonline', '0016_character_names_are_not_unique'), ('eveonline', '0017_alliance_and_corp_names_are_not_unique'), ('eveonline', '0018_alter_evecharacter_alliance_name_and_more')]
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('auth', '__first__'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='EveAllianceInfo',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('alliance_id', models.PositiveIntegerField(unique=True)),
|
|
||||||
('alliance_name', models.CharField(db_index=True, max_length=254)),
|
|
||||||
('alliance_ticker', models.CharField(max_length=254)),
|
|
||||||
('executor_corp_id', models.PositiveIntegerField()),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'indexes': [models.Index(fields=['executor_corp_id'], name='eveonline_e_executo_7f3280_idx')],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='EveFactionInfo',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('faction_id', models.PositiveIntegerField(db_index=True, unique=True)),
|
|
||||||
('faction_name', models.CharField(max_length=254, unique=True)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='EveCorporationInfo',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('corporation_id', models.PositiveIntegerField(unique=True)),
|
|
||||||
('corporation_name', models.CharField(db_index=True, max_length=254)),
|
|
||||||
('corporation_ticker', models.CharField(max_length=254)),
|
|
||||||
('member_count', models.IntegerField()),
|
|
||||||
('alliance', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='eveonline.eveallianceinfo')),
|
|
||||||
('ceo_id', models.PositiveIntegerField(blank=True, default=None, null=True)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'indexes': [models.Index(fields=['ceo_id'], name='eveonline_e_ceo_id_eea7b8_idx')],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='EveCharacter',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('character_id', models.PositiveIntegerField(unique=True)),
|
|
||||||
('character_name', models.CharField(db_index=True, max_length=254)),
|
|
||||||
('corporation_id', models.PositiveIntegerField()),
|
|
||||||
('corporation_name', models.CharField(max_length=254)),
|
|
||||||
('corporation_ticker', models.CharField(max_length=5)),
|
|
||||||
('alliance_id', models.PositiveIntegerField(blank=True, default=None, null=True)),
|
|
||||||
('alliance_name', models.CharField(blank=True, default='', max_length=254, null=True)),
|
|
||||||
('alliance_ticker', models.CharField(blank=True, default='', max_length=5, null=True)),
|
|
||||||
('faction_id', models.PositiveIntegerField(blank=True, default=None, null=True)),
|
|
||||||
('faction_name', models.CharField(blank=True, default='', max_length=254, null=True)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'indexes': [models.Index(fields=['corporation_id'], name='eveonline_e_corpora_cb4cd9_idx'), models.Index(fields=['alliance_id'], name='eveonline_e_allianc_39ee2a_idx'), models.Index(fields=['corporation_name'], name='eveonline_e_corpora_893c60_idx'), models.Index(fields=['alliance_name'], name='eveonline_e_allianc_63fd98_idx'), models.Index(fields=['faction_id'], name='eveonline_e_faction_d5274e_idx')],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import ClassVar
|
|
||||||
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@@ -76,16 +75,14 @@ class EveAllianceInfo(models.Model):
|
|||||||
alliance_ticker = models.CharField(max_length=254)
|
alliance_ticker = models.CharField(max_length=254)
|
||||||
executor_corp_id = models.PositiveIntegerField()
|
executor_corp_id = models.PositiveIntegerField()
|
||||||
|
|
||||||
objects: ClassVar[EveAllianceManager] = EveAllianceManager()
|
objects = EveAllianceManager()
|
||||||
provider: ClassVar[EveAllianceProviderManager] = EveAllianceProviderManager()
|
provider = EveAllianceProviderManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
indexes = [models.Index(fields=['executor_corp_id',])]
|
indexes = [models.Index(fields=['executor_corp_id',])]
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.alliance_name
|
return self.alliance_name
|
||||||
|
def populate_alliance(self):
|
||||||
def populate_alliance(self) -> None:
|
|
||||||
alliance = self.provider.get_alliance(self.alliance_id)
|
alliance = self.provider.get_alliance(self.alliance_id)
|
||||||
for corp_id in alliance.corp_ids:
|
for corp_id in alliance.corp_ids:
|
||||||
if not EveCorporationInfo.objects.filter(corporation_id=corp_id).exists():
|
if not EveCorporationInfo.objects.filter(corporation_id=corp_id).exists():
|
||||||
@@ -104,6 +101,8 @@ class EveAllianceInfo(models.Model):
|
|||||||
self.save()
|
self.save()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generic_logo_url(
|
def generic_logo_url(
|
||||||
alliance_id: int, size: int = _DEFAULT_IMAGE_SIZE
|
alliance_id: int, size: int = _DEFAULT_IMAGE_SIZE
|
||||||
@@ -148,15 +147,13 @@ class EveCorporationInfo(models.Model):
|
|||||||
EveAllianceInfo, blank=True, null=True, on_delete=models.SET_NULL
|
EveAllianceInfo, blank=True, null=True, on_delete=models.SET_NULL
|
||||||
)
|
)
|
||||||
|
|
||||||
objects: ClassVar[EveCorporationManager] = EveCorporationManager()
|
objects = EveCorporationManager()
|
||||||
provider = EveCorporationProviderManager()
|
provider = EveCorporationProviderManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
indexes = [models.Index(fields=['ceo_id',]),]
|
indexes = [models.Index(fields=['ceo_id',]),]
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.corporation_name
|
return self.corporation_name
|
||||||
|
|
||||||
def update_corporation(self, corp: providers.Corporation = None):
|
def update_corporation(self, corp: providers.Corporation = None):
|
||||||
if corp is None:
|
if corp is None:
|
||||||
corp = self.provider.get_corporation(self.corporation_id)
|
corp = self.provider.get_corporation(self.corporation_id)
|
||||||
@@ -169,6 +166,8 @@ class EveCorporationInfo(models.Model):
|
|||||||
self.save()
|
self.save()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generic_logo_url(
|
def generic_logo_url(
|
||||||
corporation_id: int, size: int = _DEFAULT_IMAGE_SIZE
|
corporation_id: int, size: int = _DEFAULT_IMAGE_SIZE
|
||||||
@@ -210,12 +209,12 @@ class EveCharacter(models.Model):
|
|||||||
corporation_name = models.CharField(max_length=254)
|
corporation_name = models.CharField(max_length=254)
|
||||||
corporation_ticker = models.CharField(max_length=5)
|
corporation_ticker = models.CharField(max_length=5)
|
||||||
alliance_id = models.PositiveIntegerField(blank=True, null=True, default=None)
|
alliance_id = models.PositiveIntegerField(blank=True, null=True, default=None)
|
||||||
alliance_name = models.CharField(max_length=254, blank=True, null=True, default='') # noqa: DJ001
|
alliance_name = models.CharField(max_length=254, blank=True, default='')
|
||||||
alliance_ticker = models.CharField(max_length=5, blank=True, null=True, default='') # noqa: DJ001
|
alliance_ticker = models.CharField(max_length=5, blank=True, default='')
|
||||||
faction_id = models.PositiveIntegerField(blank=True, null=True, default=None)
|
faction_id = models.PositiveIntegerField(blank=True, default=None)
|
||||||
faction_name = models.CharField(max_length=254, blank=True, null=True, default='') # noqa: DJ001
|
faction_name = models.CharField(max_length=254, blank=True, default='')
|
||||||
|
|
||||||
objects: ClassVar[EveCharacterManager] = EveCharacterManager()
|
objects = EveCharacterManager()
|
||||||
provider = EveCharacterProviderManager()
|
provider = EveCharacterProviderManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import Literal
|
|
||||||
|
|
||||||
from bravado.client import SwaggerClient
|
|
||||||
from bravado.exception import HTTPError, HTTPNotFound, HTTPUnprocessableEntity
|
from bravado.exception import HTTPError, HTTPNotFound, HTTPUnprocessableEntity
|
||||||
from jsonschema.exceptions import RefResolutionError
|
from jsonschema.exceptions import RefResolutionError
|
||||||
|
|
||||||
@@ -10,7 +8,7 @@ from django.conf import settings
|
|||||||
|
|
||||||
from esi.clients import esi_client_factory
|
from esi.clients import esi_client_factory
|
||||||
|
|
||||||
from allianceauth import __version__, __title_useragent__, __url__
|
from allianceauth import __version__
|
||||||
from allianceauth.utils.django import StartupCommand
|
from allianceauth.utils.django import StartupCommand
|
||||||
|
|
||||||
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(
|
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(
|
||||||
@@ -51,10 +49,10 @@ class Entity:
|
|||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__} ({self.id}): {self.name}>"
|
return f"<{self.__class__.__name__} ({self.id}): {self.name}>"
|
||||||
|
|
||||||
def __bool__(self) -> bool:
|
def __bool__(self):
|
||||||
return bool(self.id)
|
return bool(self.id)
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
@@ -177,11 +175,7 @@ class EveProvider:
|
|||||||
|
|
||||||
|
|
||||||
class EveSwaggerProvider(EveProvider):
|
class EveSwaggerProvider(EveProvider):
|
||||||
def __init__(self, token=None, adapter=None) -> None:
|
def __init__(self, token=None, adapter=None):
|
||||||
self._token = token
|
|
||||||
self.adapter = adapter or self
|
|
||||||
self._faction_list = None # what are the odds this will change? could cache forever!
|
|
||||||
|
|
||||||
if settings.DEBUG or StartupCommand().is_management_command:
|
if settings.DEBUG or StartupCommand().is_management_command:
|
||||||
self._client = None
|
self._client = None
|
||||||
logger.info('ESI client will be loaded on-demand')
|
logger.info('ESI client will be loaded on-demand')
|
||||||
@@ -191,9 +185,7 @@ class EveSwaggerProvider(EveProvider):
|
|||||||
self._client = esi_client_factory(
|
self._client = esi_client_factory(
|
||||||
token=token,
|
token=token,
|
||||||
spec_file=SWAGGER_SPEC_PATH,
|
spec_file=SWAGGER_SPEC_PATH,
|
||||||
ua_appname=__title_useragent__,
|
app_info_text=f"allianceauth v{__version__}"
|
||||||
ua_version=__version__,
|
|
||||||
ua_url=__url__
|
|
||||||
)
|
)
|
||||||
except (HTTPError, RefResolutionError):
|
except (HTTPError, RefResolutionError):
|
||||||
logger.exception(
|
logger.exception(
|
||||||
@@ -202,15 +194,15 @@ class EveSwaggerProvider(EveProvider):
|
|||||||
)
|
)
|
||||||
self._client = None
|
self._client = None
|
||||||
|
|
||||||
|
self._token = token
|
||||||
|
self.adapter = adapter or self
|
||||||
|
self._faction_list = None # what are the odds this will change? could cache forever!
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def client(self) -> SwaggerClient:
|
def client(self):
|
||||||
if self._client is None:
|
if self._client is None:
|
||||||
self._client = esi_client_factory(
|
self._client = esi_client_factory(
|
||||||
token=self._token,
|
token=self._token, spec_file=SWAGGER_SPEC_PATH, app_info_text=("allianceauth v" + __version__)
|
||||||
spec_file=SWAGGER_SPEC_PATH,
|
|
||||||
ua_appname=__title_useragent__,
|
|
||||||
ua_version=__version__,
|
|
||||||
ua_url=__url__
|
|
||||||
)
|
)
|
||||||
return self._client
|
return self._client
|
||||||
|
|
||||||
|
|||||||
1
allianceauth/eveonline/swagger.json
Normal file
1
allianceauth/eveonline/swagger.json
Normal file
File diff suppressed because one or more lines are too long
@@ -676,6 +676,16 @@ class TestEveSwaggerProvider(TestCase):
|
|||||||
self.assertTrue(mock_esi_client_factory.called)
|
self.assertTrue(mock_esi_client_factory.called)
|
||||||
self.assertIsNotNone(my_provider._client)
|
self.assertIsNotNone(my_provider._client)
|
||||||
|
|
||||||
|
@patch(MODULE_PATH + '.SWAGGER_SPEC_PATH', SWAGGER_OLD_SPEC_PATH)
|
||||||
|
@patch(MODULE_PATH + '.settings.DEBUG', False)
|
||||||
|
@patch('socket.socket')
|
||||||
|
def test_create_client_on_normal_startup_w_old_swagger_spec(
|
||||||
|
self, mock_socket
|
||||||
|
):
|
||||||
|
mock_socket.side_effect = Exception('Network blocked for testing')
|
||||||
|
my_provider = EveSwaggerProvider()
|
||||||
|
self.assertIsNone(my_provider._client)
|
||||||
|
|
||||||
@patch(MODULE_PATH + '.settings.DEBUG', True)
|
@patch(MODULE_PATH + '.settings.DEBUG', True)
|
||||||
@patch(MODULE_PATH + '.esi_client_factory')
|
@patch(MODULE_PATH + '.esi_client_factory')
|
||||||
def test_dont_create_client_on_debug_startup(self, mock_esi_client_factory):
|
def test_dont_create_client_on_debug_startup(self, mock_esi_client_factory):
|
||||||
@@ -707,11 +717,11 @@ class TestEveSwaggerProvider(TestCase):
|
|||||||
self.assertIsNotNone(my_provider._client)
|
self.assertIsNotNone(my_provider._client)
|
||||||
self.assertEqual(my_client, 'my_client')
|
self.assertEqual(my_client, 'my_client')
|
||||||
|
|
||||||
|
@patch(MODULE_PATH + '.__version__', '1.0.0')
|
||||||
def test_user_agent_header(self):
|
def test_user_agent_header(self):
|
||||||
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(
|
self.assertEqual(
|
||||||
operation.future.request.headers['User-Agent'],
|
operation.future.request.headers['User-Agent'], 'allianceauth v1.0.0 dummy@example.net'
|
||||||
f'AllianceAuth/{aa_version} (dummy@example.net; +{aa_url}) Django-ESI/{esi_version} (+{esi_url})'
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
class FatConfig(AppConfig):
|
class FatConfig(AppConfig):
|
||||||
name = 'allianceauth.fleetactivitytracking'
|
name = 'allianceauth.fleetactivitytracking'
|
||||||
label = 'fleetactivitytracking'
|
label = 'fleetactivitytracking'
|
||||||
verbose_name = _('Fleet Activity Tracking')
|
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
# Generated by Django 5.1.6 on 2025-03-04 01:20
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
import django.utils.timezone
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
import allianceauth.framework.api.user
|
|
||||||
|
|
||||||
|
|
||||||
def create_permissions(apps, schema_editor) -> None:
|
|
||||||
# Remnant of AAv0
|
|
||||||
User = apps.get_model('auth', 'User')
|
|
||||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
|
||||||
Permission = apps.get_model('auth', 'Permission')
|
|
||||||
ct = ContentType.objects.get_for_model(User)
|
|
||||||
Permission.objects.get_or_create(codename="fleetactivitytracking", content_type=ct, name="fleetactivitytracking")
|
|
||||||
Permission.objects.get_or_create(codename="fleetactivitytracking_statistics", content_type=ct, name="fleetactivitytracking_statistics")
|
|
||||||
|
|
||||||
|
|
||||||
def reverse(apps, schema_editor) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
replaces = [('fleetactivitytracking', '0001_initial'), ('fleetactivitytracking', '0002_auto_20160905_2220'), ('fleetactivitytracking', '0003_auto_20160906_2354'), ('fleetactivitytracking', '0004_make_strings_more_stringy'), ('fleetactivitytracking', '0005_remove_fat_name'), ('fleetactivitytracking', '0006_auto_20180803_0430'), ('fleetactivitytracking', '0007_sentinel_user')]
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
('eveonline', '0019_v5squash'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Fatlink',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('fatdatetime', models.DateTimeField(default=django.utils.timezone.now)),
|
|
||||||
('duration', models.PositiveIntegerField()),
|
|
||||||
('fleet', models.CharField(max_length=254)),
|
|
||||||
('hash', models.CharField(max_length=254, unique=True)),
|
|
||||||
('creator', models.ForeignKey(on_delete=models.SET(allianceauth.framework.api.user.get_sentinel_user), to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'permissions': ()
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Fat',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('system', models.CharField(max_length=30)),
|
|
||||||
('shiptype', models.CharField(max_length=100)),
|
|
||||||
('station', models.CharField(max_length=125)),
|
|
||||||
('character', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eveonline.evecharacter')),
|
|
||||||
('fatlink', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fleetactivitytracking.fatlink')),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'unique_together': {('character', 'fatlink')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.RunPython(create_permissions, reverse)
|
|
||||||
]
|
|
||||||
@@ -13,14 +13,6 @@ class Fatlink(models.Model):
|
|||||||
hash = models.CharField(max_length=254, unique=True)
|
hash = models.CharField(max_length=254, unique=True)
|
||||||
creator = models.ForeignKey(User, on_delete=models.SET(get_sentinel_user))
|
creator = models.ForeignKey(User, on_delete=models.SET(get_sentinel_user))
|
||||||
|
|
||||||
class Meta:
|
|
||||||
permissions = (
|
|
||||||
# Intentionally Commented out
|
|
||||||
# AAv0 has these in the Auth_ Content Type
|
|
||||||
# ('fleetactivitytracking', 'fleetactivitytracking'),
|
|
||||||
# ('fleetactivitytracking_statistics', 'fleetactivitytracking_statistics'),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.fleet
|
return self.fleet
|
||||||
|
|
||||||
@@ -34,7 +26,7 @@ class Fat(models.Model):
|
|||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = (("character", "fatlink"),)
|
unique_together = (('character', 'fatlink'),)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"Fat-link for {self.character.character_name}"
|
return f"Fat-link for {self.character.character_name}"
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<th class="text-center">{% translate "Character" %}</th>
|
<th class="text-center">{% translate "Character" %}</th>
|
||||||
<th class="text-center">{% translate "System" %}</th>
|
<th class="text-center">{% translate "System" %}</th>
|
||||||
<th class="text-center">{% translate "Ship" %}</th>
|
<th class="text-center">{% translate "Ship" %}</th>
|
||||||
<th class="text-center">{% translate "EVE time" %}</th>
|
<th class="text-center">{% translate "Eve Time" %}</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th class="text-center">{% translate "Fleet" %}</th>
|
<th class="text-center">{% translate "Fleet" %}</th>
|
||||||
<th class="text-center">{% translate "Creator" %}</th>
|
<th class="text-center">{% translate "Creator" %}</th>
|
||||||
<th class="text-center">{% translate "EVE time" %}</th>
|
<th class="text-center">{% translate "Eve Time" %}</th>
|
||||||
<th class="text-center">{% translate "Duration" %}</th>
|
<th class="text-center">{% translate "Duration" %}</th>
|
||||||
<th class="text-center">{% translate "Edit" %}</th>
|
<th class="text-center">{% translate "Edit" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
{% for link in created_fats %}
|
{% for link in created_fats %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<a href="{% url 'fatlink:click' link.hash %}" class="badge text-bg-primary">
|
<a href="{% url 'fatlink:click' link.hash %}" class="badge bg-primary">
|
||||||
{{ link.fleet }}
|
{{ link.fleet }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
<th scope="col" class="text-center">{% translate "Character" %}</th>
|
<th scope="col" class="text-center">{% translate "Character" %}</th>
|
||||||
<th scope="col" class="text-center">{% translate "System" %}</th>
|
<th scope="col" class="text-center">{% translate "System" %}</th>
|
||||||
<th scope="col" class="text-center">{% translate "Ship" %}</th>
|
<th scope="col" class="text-center">{% translate "Ship" %}</th>
|
||||||
<th scope="col" class="text-center">{% translate "EVE time" %}</th>
|
<th scope="col" class="text-center">{% translate "Eve Time" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{% for fat in fats %}
|
{% for fat in fats %}
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
<th scope="col" class="text-center">{% translate "Name" %}</th>
|
<th scope="col" class="text-center">{% translate "Name" %}</th>
|
||||||
<th scope="col" class="text-center">{% translate "Creator" %}</th>
|
<th scope="col" class="text-center">{% translate "Creator" %}</th>
|
||||||
<th scope="col" class="text-center">{% translate "Fleet" %}</th>
|
<th scope="col" class="text-center">{% translate "Fleet" %}</th>
|
||||||
<th scope="col" class="text-center">{% translate "EVE time" %}</th>
|
<th scope="col" class="text-center">{% translate "Eve Time" %}</th>
|
||||||
<th scope="col" class="text-center">{% translate "Duration" %}</th>
|
<th scope="col" class="text-center">{% translate "Duration" %}</th>
|
||||||
<th scope="col" class="text-center">{% translate "Edit" %}</th>
|
<th scope="col" class="text-center">{% translate "Edit" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
{% for link in fatlinks %}
|
{% for link in fatlinks %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<a href="{% url 'fatlink:click' link.hash %}" class="badge text-bg-primary">{{ link.fleet }}</a>
|
<a href="{% url 'fatlink:click' link.hash %}" class="badge bg-primary">{{ link.fleet }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">{{ link.creator.username }}</td>
|
<td class="text-center">{{ link.creator.username }}</td>
|
||||||
<td class="text-center">{{ link.fleet }}</td>
|
<td class="text-center">{{ link.fleet }}</td>
|
||||||
|
|||||||
@@ -9,39 +9,24 @@ from allianceauth.authentication.models import CharacterOwnership
|
|||||||
from allianceauth.eveonline.models import EveCharacter
|
from allianceauth.eveonline.models import EveCharacter
|
||||||
|
|
||||||
|
|
||||||
def get_all_characters_from_user(user: User, main_first: bool = False) -> list:
|
def get_all_characters_from_user(user: User) -> list:
|
||||||
"""
|
"""
|
||||||
Get all characters from a user
|
Get all characters from a user or an empty list
|
||||||
This function retrieves all characters associated with a given user, optionally ordering them
|
when no characters are found for the user or the user is None
|
||||||
with the main character first.
|
|
||||||
If the user is None, an empty list is returned.
|
|
||||||
|
|
||||||
:param user: The user whose characters are to be retrieved
|
:param user:
|
||||||
:type user: User
|
:type user:
|
||||||
:param main_first: If True, the main character will be listed first
|
:return:
|
||||||
:type main_first: bool
|
:rtype:
|
||||||
:return: A list of EveCharacter objects associated with the user
|
|
||||||
:rtype: list[EveCharacter]
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if user is None:
|
if user is None:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if main_first:
|
characters = [
|
||||||
characters = [
|
char.character for char in CharacterOwnership.objects.filter(user=user)
|
||||||
char.character
|
]
|
||||||
for char in CharacterOwnership.objects.filter(user=user).order_by(
|
|
||||||
"-character__userprofile", "character__character_name"
|
|
||||||
)
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
characters = [
|
|
||||||
char.character
|
|
||||||
for char in CharacterOwnership.objects.filter(user=user).order_by(
|
|
||||||
"character__character_name"
|
|
||||||
)
|
|
||||||
]
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ Framework App Config
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
class FrameworkConfig(AppConfig):
|
class FrameworkConfig(AppConfig):
|
||||||
@@ -13,4 +12,3 @@ class FrameworkConfig(AppConfig):
|
|||||||
|
|
||||||
name = "allianceauth.framework"
|
name = "allianceauth.framework"
|
||||||
label = "framework"
|
label = "framework"
|
||||||
verbose_name = _("Framework")
|
|
||||||
|
|||||||
@@ -5,33 +5,11 @@
|
|||||||
* to be used throughout Alliance Auth and its Community Apps
|
* to be used throughout Alliance Auth and its Community Apps
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* General
|
|
||||||
------------------------------------------------------------------------------------- */
|
|
||||||
@media all {
|
|
||||||
.navbar-toggler.collapsed {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
ul#nav-right:has(li) + ul#nav-right-character-control > li:first-child {
|
|
||||||
display: list-item !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
form.is-submitting button[type="submit"] {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media all and (max-width: 991px) {
|
|
||||||
ul#nav-left:has(li) + ul#nav-right + ul#nav-right-character-control > li:first-child {
|
|
||||||
display: list-item !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Bootstrap fixes
|
/* Bootstrap fixes
|
||||||
------------------------------------------------------------------------------------- */
|
------------------------------------------------------------------------------------- */
|
||||||
@media all {
|
@media all {
|
||||||
.table {
|
.table {
|
||||||
--bs-table-bg: transparent !important;
|
--bs-table-bg: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,12 +49,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Chevron icons */
|
/* Chevron icons */
|
||||||
#sidebar-menu span[data-bs-toggle="collapse"] > i.fa-chevron-right {
|
#sidebar-menu span[data-bs-toggle="collapse"][aria-expanded="true"] > i.fa-chevron-down,
|
||||||
transition: 0.25s transform ease-in-out;
|
#sidebar-menu span[data-bs-toggle="collapse"][aria-expanded="false"] > i.fa-chevron-right {
|
||||||
|
display: block;
|
||||||
|
width: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar-menu span[data-bs-toggle="collapse"][aria-expanded="true"] > i.fa-chevron-right {
|
#sidebar-menu span[data-bs-toggle="collapse"][aria-expanded="true"] > i.fa-chevron-right,
|
||||||
transform: rotate(90deg);
|
#sidebar-menu span[data-bs-toggle="collapse"][aria-expanded="false"] > i.fa-chevron-down {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,47 +65,47 @@
|
|||||||
------------------------------------------------------------------------------------- */
|
------------------------------------------------------------------------------------- */
|
||||||
@media all {
|
@media all {
|
||||||
.cursor-auto {
|
.cursor-auto {
|
||||||
cursor: auto !important;
|
cursor: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cursor-default {
|
.cursor-default {
|
||||||
cursor: default !important;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cursor-pointer {
|
.cursor-pointer {
|
||||||
cursor: pointer !important;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cursor-wait {
|
.cursor-wait {
|
||||||
cursor: wait !important;
|
cursor: wait;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cursor-text {
|
.cursor-text {
|
||||||
cursor: text !important;
|
cursor: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cursor-move {
|
.cursor-move {
|
||||||
cursor: move !important;
|
cursor: move;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cursor-help {
|
.cursor-help {
|
||||||
cursor: help !important;
|
cursor: help;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cursor-not-allowed {
|
.cursor-not-allowed {
|
||||||
cursor: not-allowed !important;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cursor-inherit {
|
.cursor-inherit {
|
||||||
cursor: inherit !important;
|
cursor: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cursor-zoom-in {
|
.cursor-zoom-in {
|
||||||
cursor: zoom-in !important;
|
cursor: zoom-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cursor-zoom-out {
|
.cursor-zoom-out {
|
||||||
cursor: zoom-out !important;
|
cursor: zoom-out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,343 +0,0 @@
|
|||||||
/**
|
|
||||||
* Functions and utilities for the Alliance Auth framework.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* jshint -W097 */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the given item is an array.
|
|
||||||
*
|
|
||||||
* @usage
|
|
||||||
* ```javascript
|
|
||||||
* if (isArray(someVariable)) {
|
|
||||||
* console.log('This is an array');
|
|
||||||
* } else {
|
|
||||||
* console.log('This is not an array');
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @param {*} item - The item to check.
|
|
||||||
* @returns {boolean} True if the item is an array, false otherwise.
|
|
||||||
*/
|
|
||||||
const isArray = (item) => {
|
|
||||||
return Array.isArray(item);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the given item is a plain object, excluding arrays and dates.
|
|
||||||
*
|
|
||||||
* @usage
|
|
||||||
* ```javascript
|
|
||||||
* if (isObject(someVariable)) {
|
|
||||||
* console.log('This is a plain object');
|
|
||||||
* } else {
|
|
||||||
* console.log('This is not a plain object');
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @param {*} item - The item to check.
|
|
||||||
* @returns {boolean} True if the item is a plain object, false otherwise.
|
|
||||||
*/
|
|
||||||
const isObject = (item) => {
|
|
||||||
return (
|
|
||||||
item && typeof item === 'object' && !isArray(item) && !(item instanceof Date)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch data from an ajax URL
|
|
||||||
*
|
|
||||||
* Do not call this function directly, use `fetchGet` or `fetchPost` instead.
|
|
||||||
*
|
|
||||||
* @param {string} url The URL to fetch data from
|
|
||||||
* @param {string} method The HTTP method to use for the request (default: 'get')
|
|
||||||
* @param {string|null} csrfToken The CSRF token to include in the request headers (default: null)
|
|
||||||
* @param {string|null} payload The payload (JSON|Object) to send with the request (default: null)
|
|
||||||
* @param {boolean} responseIsJson Whether the response is expected to be JSON or not (default: true)
|
|
||||||
* @returns {Promise<string>} The fetched data
|
|
||||||
* @throws {Error} Throws an error when:
|
|
||||||
* - The method is not valid (only `get` and `post` are allowed).
|
|
||||||
* - The CSRF token is required but not provided for POST requests.
|
|
||||||
* - The payload is not an object when using POST method.
|
|
||||||
* - The response status is not OK (HTTP 200-299).
|
|
||||||
* - There is a network error or if the response cannot be parsed as JSON.
|
|
||||||
*/
|
|
||||||
const _fetchAjaxData = async ({
|
|
||||||
url,
|
|
||||||
method = 'get',
|
|
||||||
csrfToken = null,
|
|
||||||
payload = null,
|
|
||||||
responseIsJson = true
|
|
||||||
}) => {
|
|
||||||
const normalizedMethod = method.toLowerCase();
|
|
||||||
|
|
||||||
// Validate the method
|
|
||||||
const validMethods = ['get', 'post'];
|
|
||||||
|
|
||||||
if (!validMethods.includes(normalizedMethod)) {
|
|
||||||
throw new Error(`Invalid method: ${method}. Valid methods are: get, post`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const headers = {};
|
|
||||||
|
|
||||||
// Set headers based on response type
|
|
||||||
if (responseIsJson) {
|
|
||||||
headers['Accept'] = 'application/json'; // jshint ignore:line
|
|
||||||
headers['Content-Type'] = 'application/json';
|
|
||||||
}
|
|
||||||
|
|
||||||
let requestUrl = url;
|
|
||||||
let body = null;
|
|
||||||
|
|
||||||
if (normalizedMethod === 'post') {
|
|
||||||
if (!csrfToken) {
|
|
||||||
throw new Error('CSRF token is required for POST requests');
|
|
||||||
}
|
|
||||||
|
|
||||||
headers['X-CSRFToken'] = csrfToken;
|
|
||||||
|
|
||||||
if (payload !== null && !isObject(payload)) {
|
|
||||||
throw new Error('Payload must be an object when using POST method');
|
|
||||||
}
|
|
||||||
|
|
||||||
body = payload ? JSON.stringify(payload) : null;
|
|
||||||
} else if (normalizedMethod === 'get' && payload) {
|
|
||||||
const queryParams = new URLSearchParams(payload).toString(); // jshint ignore:line
|
|
||||||
|
|
||||||
requestUrl += (url.includes('?') ? '&' : '?') + queryParams;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Throws an error with a formatted message.
|
|
||||||
*
|
|
||||||
* @param {Response} response The error object containing the message to throw.
|
|
||||||
*/
|
|
||||||
const throwHTTPStatusError = (response) => {
|
|
||||||
throw new Error(`Error: ${response.status} - ${response.statusText}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(requestUrl, {
|
|
||||||
method: method.toUpperCase(),
|
|
||||||
headers: headers,
|
|
||||||
body: body
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Throws an error if the response status is not OK (HTTP 200-299).
|
|
||||||
* This is used to handle HTTP errors gracefully.
|
|
||||||
*/
|
|
||||||
if (!response.ok) {
|
|
||||||
throwHTTPStatusError(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
return responseIsJson ? await response.json() : await response.text();
|
|
||||||
} catch (error) {
|
|
||||||
// Log the error message to the console
|
|
||||||
console.log(`Error: ${error.message}`);
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch data from an ajax URL using the GET method.
|
|
||||||
* This function is a wrapper around _fetchAjaxData to simplify GET requests.
|
|
||||||
*
|
|
||||||
* @usage
|
|
||||||
* ```javascript
|
|
||||||
* fetchGet({
|
|
||||||
* url: url,
|
|
||||||
* responseIsJson: false
|
|
||||||
* }).then((data) => {
|
|
||||||
* // Process the fetched data
|
|
||||||
* }).catch((error) => {
|
|
||||||
* console.error(`Error: ${error.message}`);
|
|
||||||
*
|
|
||||||
* // Handle the error appropriately
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @param {string} url The URL to fetch data from
|
|
||||||
* @param {string|null} payload The payload (JSON) to send with the request (default: null)
|
|
||||||
* @param {boolean} responseIsJson Whether the response is expected to be JSON or not (default: true)
|
|
||||||
* @return {Promise<string>} The fetched data
|
|
||||||
*/
|
|
||||||
const fetchGet = async ({
|
|
||||||
url,
|
|
||||||
payload = null,
|
|
||||||
responseIsJson = true
|
|
||||||
}) => {
|
|
||||||
return await _fetchAjaxData({
|
|
||||||
url: url,
|
|
||||||
method: 'get',
|
|
||||||
payload: payload,
|
|
||||||
responseIsJson: responseIsJson
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch data from an ajax URL using the POST method.
|
|
||||||
* This function is a wrapper around _fetchAjaxData to simplify POST requests.
|
|
||||||
* It requires a CSRF token for security purposes.
|
|
||||||
*
|
|
||||||
* @usage
|
|
||||||
* ```javascript
|
|
||||||
* fetchPost({
|
|
||||||
* url: url,
|
|
||||||
* csrfToken: csrfToken,
|
|
||||||
* payload: {
|
|
||||||
* key: 'value',
|
|
||||||
* anotherKey: 'anotherValue'
|
|
||||||
* },
|
|
||||||
* responseIsJson: true
|
|
||||||
* }).then((data) => {
|
|
||||||
* // Process the fetched data
|
|
||||||
* }).catch((error) => {
|
|
||||||
* console.error(`Error: ${error.message}`);
|
|
||||||
*
|
|
||||||
* // Handle the error appropriately
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @param {string} url The URL to fetch data from
|
|
||||||
* @param {string|null} csrfToken The CSRF token to include in the request headers (default: null)
|
|
||||||
* @param {string|null} payload The payload (JSON) to send with the request (default: null)
|
|
||||||
* @param {boolean} responseIsJson Whether the response is expected to be JSON or not (default: true)
|
|
||||||
* @return {Promise<string>} The fetched data
|
|
||||||
*/
|
|
||||||
const fetchPost = async ({
|
|
||||||
url,
|
|
||||||
csrfToken,
|
|
||||||
payload = null,
|
|
||||||
responseIsJson = true
|
|
||||||
}) => {
|
|
||||||
return await _fetchAjaxData({
|
|
||||||
url: url,
|
|
||||||
method: 'post',
|
|
||||||
csrfToken: csrfToken,
|
|
||||||
payload: payload,
|
|
||||||
responseIsJson: responseIsJson
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively merges properties from source objects into a target object. If a property at the current level is an object,
|
|
||||||
* and both target and source have it, the property is merged. Otherwise, the source property overwrites the target property.
|
|
||||||
* This function does not modify the source objects and prevents prototype pollution by not allowing __proto__, constructor,
|
|
||||||
* and prototype property names.
|
|
||||||
*
|
|
||||||
* @usage
|
|
||||||
* ```javascript
|
|
||||||
* const target = {a: 1, b: {c: 2}};
|
|
||||||
* const source1 = {b: {d: 3}, e: 4 };
|
|
||||||
* const source2 = {a: 5, b: {c: 6}};
|
|
||||||
*
|
|
||||||
* const merged = objectDeepMerge(target, source1, source2);
|
|
||||||
*
|
|
||||||
* console.log(merged); // {a: 5, b: {c: 6, d: 3}, e: 4}
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @param {Object} target The target object to merge properties into.
|
|
||||||
* @param {...Object} sources One or more source objects from which to merge properties.
|
|
||||||
* @returns {Object} The target object after merging properties from sources.
|
|
||||||
*/
|
|
||||||
function objectDeepMerge (target, ...sources) {
|
|
||||||
if (!sources.length) {
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterate through each source object without modifying the `sources` array.
|
|
||||||
sources.forEach(source => {
|
|
||||||
if (isObject(target) && isObject(source)) {
|
|
||||||
for (const key in source) {
|
|
||||||
if (isObject(source[key])) {
|
|
||||||
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
||||||
continue; // Skip potentially dangerous keys to prevent prototype pollution.
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!target[key] || !isObject(target[key])) {
|
|
||||||
target[key] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
objectDeepMerge(target[key], source[key]);
|
|
||||||
} else {
|
|
||||||
target[key] = source[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats a number according to the specified locale.
|
|
||||||
* This function uses the Intl.NumberFormat API to format the number.
|
|
||||||
*
|
|
||||||
* @usage
|
|
||||||
* In your Django template get the current language code:
|
|
||||||
* ```django
|
|
||||||
* {% get_current_language as LANGUAGE_CODE %}
|
|
||||||
* ```
|
|
||||||
* Then use it in your JavaScript:
|
|
||||||
* ```javascript
|
|
||||||
* const userLocale = '{{ LANGUAGE_CODE }}'; // e.g., 'en-US', 'de-DE'
|
|
||||||
* const number = 1234567.89;
|
|
||||||
* const formattedNumber = numberFormatter({
|
|
||||||
* value: number,
|
|
||||||
* locales: userLocale,
|
|
||||||
* options: {
|
|
||||||
* style: 'currency',
|
|
||||||
* currency: 'ISK'
|
|
||||||
* }
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* // Output will vary based on locale
|
|
||||||
* // e.g., '1,234,567.89' for 'en-US', '1.234.567,89' for 'de-DE'
|
|
||||||
* console.log(formattedNumber);
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @param {number} value The number to format
|
|
||||||
* @param {string | string[]} locales The locale(s) to use for formatting (e.g., 'en-US', 'de-DE', ['en-US', 'de-DE']). If not provided, the browser's default locale will be used and any language settings from the user will be ignored.
|
|
||||||
* @param {Object} [options={}] Additional options for number formatting (see `Intl.NumberFormat` documentation - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat)
|
|
||||||
* @return {string} The formatted number as a string
|
|
||||||
*/
|
|
||||||
const numberFormatter = ({value, locales, options = {}}) => {
|
|
||||||
console.log('Formatting number:', value, 'for locale(s):', locales, 'with options:', options);
|
|
||||||
const formatter = new Intl.NumberFormat(locales, {
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
minimumFractionDigits: 0,
|
|
||||||
...options
|
|
||||||
});
|
|
||||||
|
|
||||||
return formatter.format(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When the document is ready …
|
|
||||||
*/
|
|
||||||
$(document).ready(() => {
|
|
||||||
/**
|
|
||||||
* Prevent double form submits by adding a class to the form
|
|
||||||
* when it is submitted.
|
|
||||||
*
|
|
||||||
* This class can be used to show a visual indicator that the form is being
|
|
||||||
* submitted, such as a spinner.
|
|
||||||
*
|
|
||||||
* This is useful to prevent users from double-clicking the submit button
|
|
||||||
* and submitting the form multiple times.
|
|
||||||
*/
|
|
||||||
document.querySelectorAll('form').forEach((form) => {
|
|
||||||
form.addEventListener('submit', (e) => {
|
|
||||||
// Prevent if already submitting
|
|
||||||
if (form.classList.contains('is-submitting')) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add class to hook our visual indicator on
|
|
||||||
form.classList.add('is-submitting');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
"""
|
|
||||||
Custom static files storage for Alliance Auth.
|
|
||||||
|
|
||||||
This module defines a custom static files storage class for
|
|
||||||
Alliance Auth, named `AaManifestStaticFilesStorage`.
|
|
||||||
|
|
||||||
Using `ManifestStaticFilesStorage` will give us a hashed name for
|
|
||||||
our static files, which is useful for cache busting.
|
|
||||||
|
|
||||||
This storage class extends Django's `ManifestStaticFilesStorage` to ignore missing files,
|
|
||||||
which the original class does not handle, and log them in debug mode.
|
|
||||||
It is useful for handling cases where static files may not exist, such as when a
|
|
||||||
CSS file references a background image that is not present in the static files directory.
|
|
||||||
|
|
||||||
With debug mode enabled, it will print a message for each missing file when running `collectstatic`,
|
|
||||||
which can help identify issues with static file references during development.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.staticfiles.storage import ManifestStaticFilesStorage
|
|
||||||
|
|
||||||
|
|
||||||
class AaManifestStaticFilesStorage(ManifestStaticFilesStorage):
|
|
||||||
"""
|
|
||||||
Custom static files storage that ignores missing files.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _cleanup_name(cls, name: str) -> str:
|
|
||||||
"""
|
|
||||||
Clean up the name by removing quotes.
|
|
||||||
This method is used to ensure that the name does not contain any quotes,
|
|
||||||
which can cause issues with file paths.
|
|
||||||
|
|
||||||
:param name: The name of the static file.
|
|
||||||
:type name: str
|
|
||||||
:return: The cleaned-up name without quotes.
|
|
||||||
:rtype: str
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Remove quotes from the name
|
|
||||||
return name.replace('"', "").replace("'", "")
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Initialize the static files storage, ignoring missing files.
|
|
||||||
|
|
||||||
:param args:
|
|
||||||
:type args:
|
|
||||||
:param kwargs:
|
|
||||||
:type kwargs:
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.missing_files = []
|
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def hashed_name(self, name, content=None, filename=None):
|
|
||||||
"""
|
|
||||||
Generate a hashed name for the given static file, ignoring missing files.
|
|
||||||
|
|
||||||
Ignore missing files, e.g. non-existent background image referenced from css.
|
|
||||||
Returns the original filename if the referenced file doesn't exist.
|
|
||||||
|
|
||||||
:param name: The name of the static file to hash.
|
|
||||||
:type name: str
|
|
||||||
:param content: The content of the static file, if available.
|
|
||||||
:type content: bytes | None
|
|
||||||
:param filename: The original filename of the static file, if available.
|
|
||||||
:type filename: str | None
|
|
||||||
:return: The hashed name of the static file, or the original name if the file is missing.
|
|
||||||
:rtype: str
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
clean_name = self._cleanup_name(name)
|
|
||||||
|
|
||||||
return super().hashed_name(clean_name, content, filename)
|
|
||||||
except ValueError as e:
|
|
||||||
if settings.DEBUG:
|
|
||||||
# In debug mode, we log the missing file message
|
|
||||||
message = e.args[0].split(" with ")[0]
|
|
||||||
self.missing_files.append(message)
|
|
||||||
# print(f'\x1b[0;30;41m{message}\x1b[0m')
|
|
||||||
|
|
||||||
return name
|
|
||||||
|
|
||||||
def post_process(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Post-process the static files, printing any missing files in debug mode.
|
|
||||||
|
|
||||||
:param args:
|
|
||||||
:type args:
|
|
||||||
:param kwargs:
|
|
||||||
:type kwargs:
|
|
||||||
:return:
|
|
||||||
:rtype:
|
|
||||||
"""
|
|
||||||
|
|
||||||
yield from super().post_process(*args, **kwargs)
|
|
||||||
|
|
||||||
if settings.DEBUG:
|
|
||||||
# In debug mode, print the missing files
|
|
||||||
for message in sorted(set(self.missing_files)):
|
|
||||||
print(f"\x1b[0;30;41m{message}\x1b[0m")
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<li class="nav-item">
|
|
||||||
<a href="{{ url }}" class="nav-link py-lg-0">
|
|
||||||
<span class="btn btn-{{ btn_modifier|default:'primary' }} d-none d-lg-inline-block">
|
|
||||||
{% if fa_icon and icon_on_desktop %}<i class="{{ fa_icon }} me-2"></i>{% endif %}
|
|
||||||
{{ title }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="d-inline-block d-lg-none">
|
|
||||||
{% if fa_icon and icon_on_mobile %}<i class="{{ fa_icon }} fa-fw me-2"></i>{% endif %}
|
|
||||||
{{ title }}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<li class="nav-item">
|
|
||||||
<a href="{{ url }}" class="nav-link">
|
|
||||||
<span class="d-none d-lg-inline-block" title="{{ title }}">
|
|
||||||
<i class="{{ fa_icon }}"></i>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="d-inline-block d-lg-none">
|
|
||||||
{% if icon_on_mobile %}<i class="{{ fa_icon }} me-2 fa-fw"></i>{% endif %}
|
|
||||||
{{ title }}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
@@ -2,14 +2,12 @@
|
|||||||
{# {% include "framework/header/page-header.html" with title="Foobar" subtitle="Barfoo" %}#}
|
{# {% include "framework/header/page-header.html" with title="Foobar" subtitle="Barfoo" %}#}
|
||||||
|
|
||||||
{% if title %}
|
{% if title %}
|
||||||
<header class="aa-page-header mb-3">
|
<h1 class="page-header text-center mb-3">
|
||||||
<h1 class="page-header text-center">
|
{{ title }}
|
||||||
{{ title }}
|
|
||||||
|
|
||||||
{% if subtitle %}
|
{% if subtitle %}
|
||||||
<br>
|
<br>
|
||||||
<small class="text-muted">{{ subtitle }}</small>
|
<small class="text-muted">{{ subtitle }}</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
</header>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
<svg id="alliance-auth-svg-sprite" width="0" height="0" display="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<!-- Alliance Auth Logo -->
|
|
||||||
<symbol id="aa-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
|
||||||
<g transform="translate(41.953499,36.607802)">
|
|
||||||
<path style="display:inline;fill:#e14852;stroke-width:0.32" d="M 131.07236,159.67687 C 109.26615,147.02458 91.302022,136.55002 91.152067,136.40007 l -0.272649,-0.27265 23.786292,-13.82371 c 13.08247,-7.60304 23.9186,-13.82025 24.08029,-13.81602 l 0.294,0.008 15.93273,36.83413 c 8.763,20.25877 15.891,36.95054 15.84,37.09283 l -0.0927,0.25869 z" />
|
|
||||||
<path style="display:inline;fill:#436195;stroke-width:0.32" d="m 1.28,182.46369 c 0,-0.16969 17.354495,-40.46543 38.565546,-89.546103 L 78.411088,3.68 C 79.919052,1.4903841 82.294641,0.02199886 86.08,0.01224344 89.865359,0.00248802 92.288,1.4677954 93.674477,3.5158445 l 21.668143,50.1206965 21.66814,50.120699 -0.26538,0.23285 C 136.59942,104.11816 106.528,121.61441 69.92,142.87065 33.312,164.12688 2.892,181.80046 2.32,182.14527 l -1.04,0.62693 z" />
|
|
||||||
</g>
|
|
||||||
</symbol>
|
|
||||||
|
|
||||||
<!-- Loading Spinner -->
|
|
||||||
<symbol id="aa-loading-spinner" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<g>
|
|
||||||
<circle cx="12" cy="12" r="10" fill="none" stroke-width="1" stroke-linecap="round">
|
|
||||||
<animate attributeName="stroke-dasharray" dur="1.5s" calcMode="spline" values="0 150;42 150;42 150;42 150" keyTimes="0;0.475;0.95;1" keySplines="0.42,0,0.58,1;0.42,0,0.58,1;0.42,0,0.58,1" repeatCount="indefinite" />
|
|
||||||
<animate attributeName="stroke-dashoffset" dur="1.5s" calcMode="spline" values="0;-16;-59;-59" keyTimes="0;0.475;0.95;1" keySplines="0.42,0,0.58,1;0.42,0,0.58,1;0.42,0,0.58,1" repeatCount="indefinite" />
|
|
||||||
</circle>
|
|
||||||
<animateTransform attributeName="transform" type="rotate" dur="2s" values="0 12 12;360 12 12" repeatCount="indefinite" />
|
|
||||||
</g>
|
|
||||||
</symbol>
|
|
||||||
|
|
||||||
<!-- Mumble Logo -->
|
|
||||||
<symbol id="aa-mumble-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400">
|
|
||||||
<defs>
|
|
||||||
<radialGradient id="c" cx="206.64" cy="214.43" r="190.25" gradientTransform="matrix(.97267 .016175 -.016656 .97474 9.2188 2.0744)" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-opacity="0" offset="0" />
|
|
||||||
<stop stop-opacity=".019608" offset=".81721" />
|
|
||||||
<stop stop-opacity=".1451" offset=".89931" />
|
|
||||||
<stop stop-opacity=".20784" offset=".91199" />
|
|
||||||
<stop stop-opacity=".25098" offset=".95598" />
|
|
||||||
<stop stop-opacity=".33333" offset="1" />
|
|
||||||
</radialGradient>
|
|
||||||
</defs>
|
|
||||||
<g transform="translate(0 -652.36)">
|
|
||||||
<path transform="matrix(1.0811 0 0 1.1043 -22.438 617.98)" d="m385.62 214.43c0 96.124-80.133 174.05-178.98 174.05-98.849 0-178.98-77.924-178.98-174.05s80.133-174.05 178.98-174.05c98.849 0 178.98 77.924 178.98 174.05z" fill="#1a1a1a" stroke="#000" stroke-linejoin="round" stroke-width="4.576" />
|
|
||||||
<path transform="matrix(1.0706 0 0 1.101 -22.082 583.62)" d="m385.62 214.43c0 96.124-80.133 174.05-178.98 174.05-98.849 0-178.98-77.924-178.98-174.05s80.133-174.05 178.98-174.05c98.849 0 178.98 77.924 178.98 174.05z" opacity="0" />
|
|
||||||
<path transform="matrix(1.0423 0 0 1.0695 -13.736 622.74)" d="m385.62 214.43c0 96.124-80.133 174.05-178.98 174.05-98.849 0-178.98-77.924-178.98-174.05s80.133-174.05 178.98-174.05c98.849 0 178.98 77.924 178.98 174.05z" fill="#fff" opacity=".9" />
|
|
||||||
<path transform="matrix(1.0641 0 0 1.0787 -20.794 620.64)" d="m385.62 214.43c0 96.124-80.133 174.05-178.98 174.05-98.849 0-178.98-77.924-178.98-174.05s80.133-174.05 178.98-174.05c98.849 0 178.98 77.924 178.98 174.05z" fill="#fff" stroke="#333" stroke-linejoin="round" stroke-width="1.4127" />
|
|
||||||
<path transform="matrix(1.0857 0 0 1.109 -24.345 616.21)" d="m385.62 214.43c0 96.124-80.133 174.05-178.98 174.05-98.849 0-178.98-77.924-178.98-174.05s80.133-174.05 178.98-174.05c98.849 0 178.98 77.924 178.98 174.05z" fill="none" stroke="#000" stroke-linejoin="round" stroke-width="1.8304" />
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<path transform="matrix(1.0765 0 0 1.1009 -20.514 -34.696)" d="m385.62 214.43a178.98 174.05 0 1 1-357.96 0 178.98 174.05 0 1 1 357.96 0z" fill="url(#c)" opacity=".75" />
|
|
||||||
</g>
|
|
||||||
<g fill-rule="evenodd">
|
|
||||||
<path transform="matrix(1.05 0 0 1.05 -5.3555 .50955)" d="m152.41 31.61c-24.652-0.61541-49.623 15.705-55.853 40.126-1.4511 5.9204-2.0429 11.533-2.1475 17.251v63.623h25c0.0881-22.382-0.12668-44.644 0.1701-67.072 0.76858-14.243 11.773-29.258 27.049-29.084 0.11203 22.669-0.22918 45.351 0.18004 68.011 1.3028 18.426 18.762 33.676 37.243 32.114 11.546-0.2802 23.178 0.67313 34.648-0.72475 17.466-3.2744 29.553-21.063 27.929-38.449v-60.857c15.888-1.1603 27.938 14.263 28.642 29.084 0.29501 22.427 0.0825 44.692 0.1701 67.072h25v-67.5c-0.81797-7.2761-1.9718-16.18-5.9149-23.198-10.229-20.751-34.153-31.948-56.717-30.261-6.591-0.83713-13.681 3.6197-15.487 9.8666 0.10876 26.739 0.18577 53.486-0.015 80.22-0.75343 11.2-11.79 19.764-22.805 18.342-7.7921 0.33854-16.594 0.0136-21.908-6.6817-7.1623-7.5704-4.7632-18.405-5.1836-27.812 0.0193-21.719-0.0713-43.418 0.1249-65.1-3.2593-6.5913-10.503-9.9936-17.679-8.9119l-1.1877-0.01641-1.2582-0.04042h2.5e-4z" stroke="#fff" />
|
|
||||||
<path d="m107.27 156.26v177.84c-35.128-3.8535-62.737-42.188-62.737-88.922 0-46.734 27.609-85.068 62.737-88.922z" fill="url(#l)" opacity=".9666" />
|
|
||||||
</g>
|
|
||||||
<g fill-rule="evenodd" stroke="#fff">
|
|
||||||
<path transform="matrix(1.05 0 0 1.05 -5.3555 .50955)" d="m290.42 313.16c-0.69916-7e-3 -3.3105-0.57507-3.9404-0.16697 0 0-1.0356 3.0168-4.6042 5.6725-3.1327 2.3314-6.1076 4.5662-9.2946 6.6625-2.8616 1.8823-5.9328 3.9177-8.8098 5.3024-2.264 1.0896-4.114 1.2482-4.114 1.2482h-32.22c-2.0127 0-3.6618 1.5872-3.6618 3.5625v0.875c0 1.9753 1.649 3.5938 3.6618 3.5938h33.879c0.77968 0 3.5971-0.82022 5.2725-1.5553 4.1768-1.8326 6.899-4.166 11.71-7.0274 5.1144-3.2712 14.573-10.886 14.573-10.886 1.6797-1.0883 2.1278-3.289 1.0189-4.9375l-0.47763-0.75c-0.69304-1.0303-1.8278-1.5824-2.9931-1.5938z" />
|
|
||||||
<path transform="matrix(1.05 0 0 1.05 -5.3555 .50955)" d="m288.25 148.44v169.38c33.455-3.67 59.75-40.179 59.75-84.688s-26.295-81.018-59.75-84.688z" opacity=".9666" />
|
|
||||||
<path transform="matrix(1.05 0 0 1.05 -5.3555 .50955)" d="m106.22 149.34v169.38c-33.455-3.67-59.75-40.179-59.75-84.688s26.295-81.018 59.75-84.688z" opacity=".9666" />
|
|
||||||
<path transform="matrix(1.3048 0 0 1.2146 -20.461 -43.8)" d="m194.74 325.86a22.13 13.831 0 1 1-44.26 0 22.13 13.831 0 1 1 44.26 0z" opacity=".9666" />
|
|
||||||
<rect transform="matrix(1.0433 0 0 1.05 -4.6563 .094873)" x="274.72" y="146.09" width="13.329" height="171.95" rx="3.8877" ry="3.5401" opacity=".9666" stroke-width="1.0538" />
|
|
||||||
<rect transform="matrix(1.0433 0 0 1.05 -3.8348 .094873)" x="106.56" y="147.08" width="13.063" height="171.96" rx="3.8101" ry="3.5403" opacity=".9666" stroke-width="1.0432" />
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<rect x="131.64" y="188.83" width="140.83" height="111.89" fill-rule="evenodd" />
|
|
||||||
<path transform="matrix(1.1007 0 0 2.0001 -23.812 -190.28)" d="m189.84 226.69c-4e-5 2.3125-0.43754 4.3438-1.3125 6.0938-0.87504 1.75-2.0521 3.1979-3.5312 4.3438-1.75 1.375-3.6719 2.3542-5.7656 2.9375-2.0938 0.58334-4.7552 0.875-7.9844 0.875h-18.625v-46.531h16.438c3.4166 5e-5 6.0052 0.13026 7.7656 0.39063 1.7604 0.26046 3.4114 0.80734 4.9531 1.6406 1.6666 0.89588 2.9114 2.0938 3.7344 3.5938 0.82288 1.5 1.2343 3.2292 1.2344 5.1875-4e-5 2.2709-0.56775 4.2917-1.7031 6.0625-1.1354 1.7709-2.7032 3.073-4.7031 3.9062v0.25c2.875 0.6042 5.177 1.8386 6.9062 3.7031 1.7291 1.8646 2.5937 4.3802 2.5938 7.5469zm-14.969-19.125c-3e-5 -0.74996-0.19274-1.5208-0.57813-2.3125-0.38544-0.79163-0.9844-1.3645-1.7969-1.7188-0.77086-0.33329-1.6823-0.51558-2.7344-0.54687-1.0521-0.0312-2.6198-0.0468-4.7031-0.0469h-0.8125v9.8438h1.4688c2 3e-5 3.401-0.0208 4.2031-0.0625 0.80207-0.0416 1.6302-0.26038 2.4844-0.65625 0.93747-0.43747 1.5833-1.0416 1.9375-1.8125 0.35414-0.7708 0.53122-1.6666 0.53125-2.6875zm2.9375 18.906c-3e-5 -1.4375-0.2917-2.5625-0.875-3.375-0.58336-0.81248-1.4584-1.4271-2.625-1.8438-0.70836-0.27081-1.6823-0.42185-2.9219-0.45312-1.2396-0.0312-2.9011-0.0469-4.9844-0.0469h-2.1562v11.656h0.625c3.0416 1e-5 5.1458-0.0208 6.3125-0.0625 1.1666-0.0416 2.3541-0.3229 3.5625-0.84375 1.0625-0.45832 1.8385-1.1302 2.3281-2.0156 0.48956-0.88541 0.73435-1.8906 0.73438-3.0156z" fill="url(#f)" />
|
|
||||||
<path transform="matrix(1.1007 0 0 2.0001 -28.291 -190.6)" d="m189.84 226.69c-4e-5 2.3125-0.43754 4.3438-1.3125 6.0938-0.87504 1.75-2.0521 3.1979-3.5312 4.3438-1.75 1.375-3.6719 2.3542-5.7656 2.9375-2.0938 0.58334-4.7552 0.875-7.9844 0.875h-18.625v-46.531h16.438c3.4166 5e-5 6.0052 0.13026 7.7656 0.39063 1.7604 0.26046 3.4114 0.80734 4.9531 1.6406 1.6666 0.89588 2.9114 2.0938 3.7344 3.5938 0.82288 1.5 1.2343 3.2292 1.2344 5.1875-4e-5 2.2709-0.56775 4.2917-1.7031 6.0625-1.1354 1.7709-2.7032 3.073-4.7031 3.9062v0.25c2.875 0.6042 5.177 1.8386 6.9062 3.7031 1.7291 1.8646 2.5937 4.3802 2.5938 7.5469zm-14.969-19.125c-3e-5 -0.74996-0.19274-1.5208-0.57813-2.3125-0.38544-0.79163-0.9844-1.3645-1.7969-1.7188-0.77086-0.33329-1.6823-0.51558-2.7344-0.54687-1.0521-0.0312-2.6198-0.0468-4.7031-0.0469h-0.8125v9.8438h1.4688c2 3e-5 3.401-0.0208 4.2031-0.0625 0.80207-0.0416 1.6302-0.26038 2.4844-0.65625 0.93747-0.43747 1.5833-1.0416 1.9375-1.8125 0.35414-0.7708 0.53122-1.6666 0.53125-2.6875zm2.9375 18.906c-3e-5 -1.4375-0.2917-2.5625-0.875-3.375-0.58336-0.81248-1.4584-1.4271-2.625-1.8438-0.70836-0.27081-1.6823-0.42185-2.9219-0.45312-1.2396-0.0312-2.9011-0.0469-4.9844-0.0469h-2.1562v11.656h0.625c3.0416 1e-5 5.1458-0.0208 6.3125-0.0625 1.1666-0.0416 2.3541-0.3229 3.5625-0.84375 1.0625-0.45832 1.8385-1.1302 2.3281-2.0156 0.48956-0.88541 0.73435-1.8906 0.73438-3.0156z" fill="#fff" />
|
|
||||||
<path transform="matrix(1.1007 0 0 2.0001 -23.812 -190.28)" d="m227.56 240.94h-31.062v-46.531h11.688v37.656h19.375z" fill="url(#e)" />
|
|
||||||
<path d="m187.31 197.56v94.531h14.125 0.0625 21.375v-19.25h-21.375v-75.281h-14.188z" fill="#fff" />
|
|
||||||
<path transform="matrix(1.1007 0 0 2.0001 -23.812 -190.28)" d="m233.12 240.94v-46.531h31.469v8.875h-19.844v8.1562h18.281v8.875h-18.281v11.75h19.844v8.875z" fill="url(#d)" />
|
|
||||||
<path d="m227.81 197.5v94.531h14.188 21.375v-19.25h-21.375v-22.281h19.656v-18.656h-19.656v-15.094h21.375v-19.25h-21.375-0.0625-14.125z" fill="#fff" />
|
|
||||||
</g>
|
|
||||||
<g fill-rule="evenodd">
|
|
||||||
<path d="m155.37 33.01c-25.884-0.64618-52.104 16.49-58.645 42.133-1.5237 6.2165-2.1451 12.11-2.2549 18.114v66.804h26.25c0.0925-23.501-0.13301-46.876 0.17861-70.426 0.807-14.955 12.362-30.721 28.401-30.539 0.11763 23.802-0.24064 47.619 0.18905 71.412 1.368 19.347 19.7 35.36 39.106 33.72 12.123-0.29421 24.336 0.70678 36.38-0.76099 18.339-3.4381 31.031-22.116 29.325-40.372v-63.9c16.682-1.2183 29.334 14.976 30.074 30.539 0.30976 23.549 0.0866 46.927 0.17861 70.426h26.25v-70.875c-0.85887-7.6399-2.0704-16.989-6.2106-24.358-10.74-21.789-35.861-33.545-59.553-31.774-6.9206-0.87899-14.366 3.8007-16.261 10.36 0.1142 28.075 0.19506 56.161-0.0157 84.231-0.7911 11.76-12.38 20.752-23.945 19.259-8.1817 0.35546-17.423 0.0143-23.004-7.0158-7.5204-7.949-5.0013-19.326-5.4428-29.202 0.0203-22.805-0.0749-45.589 0.13115-68.355-3.4223-6.9208-11.028-10.493-18.563-9.3575l-1.2471-0.01723-1.3211-0.04244h2.6e-4z" fill="url(#i)" />
|
|
||||||
<path d="m154.53 34.269c-31.655 0-56.169 21.233-59.981 48.759h26.972c3.6448-12.426 13.114-21.517 24.511-22.575 0.74035-0.69976 1.772-1.1484 2.9203-1.1484h26.611l0.0984-15.619s-2.3784-3.7253-4.2984-5.4797c-2.1754-1.9877-5.1118-3.4283-8.0062-3.7406-2.4252-0.26165-5.1996-0.04115-8.8266-0.19688z" fill="url(#j)" />
|
|
||||||
<path transform="matrix(1.3048 0 0 1.2146 -20.434 -43.907)" d="m194.74 325.86a22.13 13.831 0 1 1-44.26 0 22.13 13.831 0 1 1 44.26 0z" fill="url(#b)" opacity=".9666" />
|
|
||||||
<path d="m298.66 157.26v177.84c35.128-3.8535 62.738-42.188 62.738-88.922 0-46.734-27.609-85.068-62.738-88.922z" fill="url(#k)" opacity=".9666" />
|
|
||||||
<path d="m247.97 33.422c31.655 0 56.169 21.233 59.981 48.759h-26.972c-3.6448-12.426-13.114-21.517-24.511-22.575-0.74036-0.69976-1.772-1.1484-2.9203-1.1484h-26.611l-0.0984-15.619s2.3784-3.7253 4.2984-5.4797c2.1754-1.9877 5.1118-3.4283 8.0062-3.7406 2.4252-0.26165 5.1996-0.04115 8.8266-0.19688z" fill="url(#h)" />
|
|
||||||
<path d="m107.92 156.15v177.84c-35.128-3.8535-62.737-42.188-62.737-88.922 0-46.734 27.609-85.068 62.737-88.922z" fill="url(#g)" opacity=".9666" />
|
|
||||||
</g>
|
|
||||||
</symbol>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,11 +1,10 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
class GroupManagementConfig(AppConfig):
|
class GroupManagementConfig(AppConfig):
|
||||||
name = 'allianceauth.groupmanagement'
|
name = 'allianceauth.groupmanagement'
|
||||||
label = 'groupmanagement'
|
label = 'groupmanagement'
|
||||||
verbose_name = _('Group Management')
|
verbose_name = 'Group Management'
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from . import signals # noqa: F401
|
from . import signals # noqa: F401
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
# Generated by Django 5.1.6 on 2025-03-04 02:50
|
|
||||||
|
|
||||||
import django.contrib.auth.models
|
|
||||||
import django.db.models.deletion
|
|
||||||
import django.utils.timezone
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
def create_permissions(apps, schema_editor) -> None:
|
|
||||||
# Remnant of AAv0
|
|
||||||
User = apps.get_model('auth', 'User')
|
|
||||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
|
||||||
Permission = apps.get_model('auth', 'Permission')
|
|
||||||
ct = ContentType.objects.get_for_model(User)
|
|
||||||
Permission.objects.get_or_create(codename="group_management", content_type=ct, name="group_management")
|
|
||||||
|
|
||||||
|
|
||||||
def reverse(apps, schema_editor) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
replaces = [('groupmanagement', '0001_initial'), ('groupmanagement', '0002_auto_20160906_2354'), ('groupmanagement', '0003_default_groups'), ('groupmanagement', '0004_authgroup'), ('groupmanagement', '0005_authgroup_public'), ('groupmanagement', '0006_request_groups_perm'), ('groupmanagement', '0007_on_delete'), ('groupmanagement', '0008_remove_authgroup_permissions'), ('groupmanagement', '0009_requestlog'), ('groupmanagement', '0010_authgroup_states'), ('groupmanagement', '0011_requestlog_date'), ('groupmanagement', '0012_group_leads'), ('groupmanagement', '0013_fix_requestlog_date_field'), ('groupmanagement', '0014_auto_20200918_1412'), ('groupmanagement', '0015_make_descriptions_great_again'), ('groupmanagement', '0016_remove_grouprequest_status_field'), ('groupmanagement', '0017_improve_groups_documentation'), ('groupmanagement', '0018_reservedgroupname'), ('groupmanagement', '0019_adding_restricted_to_groups')]
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
('authentication', '0025_v5squash'),
|
|
||||||
('eveonline', '0019_v5squash'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='GroupRequest',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('leave_request', models.BooleanField(default=0)),
|
|
||||||
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.group')),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='AuthGroup',
|
|
||||||
fields=[
|
|
||||||
('group', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='auth.group')),
|
|
||||||
('internal', models.BooleanField(default=True, help_text='Internal group, users cannot see, join or request to join this group.<br>Used for groups such as Members, Corp_*, Alliance_* etc.<br><b>Overrides Hidden and Open options when selected.</b>')),
|
|
||||||
('hidden', models.BooleanField(default=True, help_text='Group is hidden from users but can still join with the correct link.')),
|
|
||||||
('open', models.BooleanField(default=False, help_text='Group is open and users will be automatically added upon request.<br>If the group is not open users will need their request manually approved.')),
|
|
||||||
('description', models.TextField(blank=True, help_text='Short description <i>(max. 512 characters)</i> of the group shown to users.', max_length=512)),
|
|
||||||
('group_leaders', models.ManyToManyField(blank=True, help_text='Group leaders can process requests for this group. Use the <code>auth.group_management</code> permission to allow a user to manage all groups.<br>', related_name='leads_groups', to=settings.AUTH_USER_MODEL)),
|
|
||||||
('public', models.BooleanField(default=False, help_text='Group is public. Any registered user is able to join this group, with visibility based on the other options set for this group.<br>Auth will not remove users from this group automatically when they are no longer authenticated.')),
|
|
||||||
('group_leader_groups', models.ManyToManyField(blank=True, help_text='Members of leader groups can process requests for this group. Use the <code>auth.group_management</code> permission to allow a user to manage all groups.<br>', related_name='leads_group_groups', to='auth.group')),
|
|
||||||
('states', models.ManyToManyField(blank=True, help_text='States listed here will have the ability to join this group provided they have the proper permissions.<br>', related_name='valid_states', to='authentication.state')),
|
|
||||||
('restricted', models.BooleanField(default=False, help_text='Group is restricted. This means that adding or removing users for this group requires a superuser admin.')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'default_permissions': (),
|
|
||||||
'permissions': (('request_groups', 'Can request non-public groups'),),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Group',
|
|
||||||
fields=[
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'group',
|
|
||||||
'indexes': [],
|
|
||||||
'proxy': True,
|
|
||||||
'verbose_name_plural': 'groups',
|
|
||||||
},
|
|
||||||
bases=('auth.group',),
|
|
||||||
managers=[
|
|
||||||
('objects', django.contrib.auth.models.GroupManager()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='RequestLog',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('request_type', models.BooleanField(null=True)),
|
|
||||||
('request_info', models.CharField(max_length=254)),
|
|
||||||
('action', models.BooleanField(default=0)),
|
|
||||||
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.group')),
|
|
||||||
('request_actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
|
||||||
('date', models.DateTimeField(auto_now_add=True)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ReservedGroupName',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(help_text='Name that can not be used for groups.', max_length=150, unique=True, verbose_name='name')),
|
|
||||||
('reason', models.TextField(help_text='Reason why this name is reserved.', verbose_name='reason')),
|
|
||||||
('created_by', models.CharField(help_text='Name of the user who created this entry.', max_length=255, verbose_name='created by')),
|
|
||||||
('created_at', models.DateTimeField(default=django.utils.timezone.now, help_text='Date when this entry was created', verbose_name='created at')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.RunPython(create_permissions, reverse)
|
|
||||||
]
|
|
||||||
@@ -53,7 +53,7 @@ class RequestLog(models.Model):
|
|||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.pk
|
return self.pk
|
||||||
|
|
||||||
def requestor(self) -> str:
|
def requestor(self):
|
||||||
return self.request_info.split(":")[0]
|
return self.request_info.split(":")[0]
|
||||||
|
|
||||||
def type_to_str(self):
|
def type_to_str(self):
|
||||||
@@ -176,9 +176,6 @@ class AuthGroup(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
permissions = (
|
permissions = (
|
||||||
("request_groups", _("Can request non-public groups")),
|
("request_groups", _("Can request non-public groups")),
|
||||||
# Intentionally Commented out
|
|
||||||
# AAv0 has these in the Auth_ Content Type
|
|
||||||
# ('group_management', 'group_management'))
|
|
||||||
)
|
)
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
|
|
||||||
|
|||||||
@@ -39,12 +39,12 @@
|
|||||||
|
|
||||||
<td>
|
<td>
|
||||||
{% if group.authgroup.hidden %}
|
{% if group.authgroup.hidden %}
|
||||||
<span class="badge text-bg-info">{% translate "Hidden" %}</span>
|
<span class="badge bg-info">{% translate "Hidden" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if group.authgroup.open %}
|
{% if group.authgroup.open %}
|
||||||
<span class="badge text-bg-success">{% translate "Open" %}</span>
|
<span class="badge bg-success">{% translate "Open" %}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge text-bg-secondary">{% translate "Requestable" %}</span>
|
<span class="badge bg-secondary">{% translate "Requestable" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'groupmanagement:management' %}">{% translate "Group Management" %}
|
<a class="nav-link" href="{% url 'groupmanagement:management' %}">{% translate "Group Management" %}
|
||||||
{% if req_count %}
|
{% if req_count %}
|
||||||
<span class="badge text-bg-secondary">{{ req_count }}</span>
|
<span class="badge bg-secondary">{{ req_count }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -33,8 +33,8 @@
|
|||||||
<th>{% translate "Description" %}</th>
|
<th>{% translate "Description" %}</th>
|
||||||
<th>
|
<th>
|
||||||
{% translate "Leaders" %}<br>
|
{% translate "Leaders" %}<br>
|
||||||
<span class="my-1 me-1 fw-lighter badge text-bg-primary">{% translate "User" %}</span>
|
<span class="my-1 me-1 fw-lighter badge bg-primary">{% translate "User" %}</span>
|
||||||
<span class="my-1 me-1 fw-lighter badge text-bg-secondary">{% translate "Group" %}</span>
|
<span class="my-1 me-1 fw-lighter badge bg-secondary">{% translate "Group" %}</span>
|
||||||
</th>
|
</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -53,13 +53,13 @@
|
|||||||
{% if g.group.authgroup.group_leaders.all.count %}
|
{% if g.group.authgroup.group_leaders.all.count %}
|
||||||
{% for leader in g.group.authgroup.group_leaders.all %}
|
{% for leader in g.group.authgroup.group_leaders.all %}
|
||||||
{% if leader.profile.main_character %}
|
{% if leader.profile.main_character %}
|
||||||
<span class="my-1 me-1 badge text-bg-primary">{{leader.profile.main_character}}</span>
|
<span class="my-1 me-1 badge bg-primary">{{leader.profile.main_character}}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if g.group.authgroup.group_leader_groups.all.count %}
|
{% if g.group.authgroup.group_leader_groups.all.count %}
|
||||||
{% for group in g.group.authgroup.group_leader_groups.all %}
|
{% for group in g.group.authgroup.group_leader_groups.all %}
|
||||||
<span class="my-1 me-1 badge text-bg-secondary">{{group.name}}</span>
|
<span class="my-1 me-1 badge bg-secondary">{{group.name}}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
@@ -70,8 +70,8 @@
|
|||||||
{% translate "Leave" %}
|
{% translate "Leave" %}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button type="button" class="btn btn-secondary cursor-help me-1" data-bs-tooltip="aa-tooltip" title="{% translate 'Request pending' %}">
|
<button type="button" class="btn btn-primary" disabled>
|
||||||
<i class="fa-regular fa-hourglass-half"></i>
|
{% translate "Pending" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif not g.request %}
|
{% elif not g.request %}
|
||||||
@@ -85,13 +85,9 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<button type="button" class="btn btn-secondary cursor-help me-1" data-bs-tooltip="aa-tooltip" title="{% translate 'Request pending' %}">
|
<button type="button" class="btn btn-primary" disabled>
|
||||||
<i class="fa-regular fa-hourglass-half"></i>
|
{% translate "Pending" %}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<a href="{% url 'groupmanagement:request_retract' g.group.id %}" class="btn btn-danger">
|
|
||||||
{% translate "Retract" %}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
{% translate "Join Requests" %}
|
{% translate "Join Requests" %}
|
||||||
|
|
||||||
{% if acceptrequests %}
|
{% if acceptrequests %}
|
||||||
<span class="badge text-bg-secondary">{{ acceptrequests|length }}</span>
|
<span class="badge bg-secondary">{{ acceptrequests|length }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
{% translate "Leave Requests" %}
|
{% translate "Leave Requests" %}
|
||||||
|
|
||||||
{% if leaverequests %}
|
{% if leaverequests %}
|
||||||
<span class="badge text-bg-secondary">{{ leaverequests|length }}</span>
|
<span class="badge bg-secondary">{{ leaverequests|length }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -11,11 +11,6 @@ urlpatterns = [
|
|||||||
path(
|
path(
|
||||||
"group/request/leave/<int:group_id>/", views.group_request_leave, name="request_leave"
|
"group/request/leave/<int:group_id>/", views.group_request_leave, name="request_leave"
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"group/request/retract/<int:group_id>/",
|
|
||||||
views.group_request_retract,
|
|
||||||
name="request_retract"
|
|
||||||
),
|
|
||||||
# group management
|
# group management
|
||||||
path("groupmanagement/requests/", views.group_management, name="management"),
|
path("groupmanagement/requests/", views.group_management, name="management"),
|
||||||
path("groupmanagement/membership/", views.group_membership, name="membership"),
|
path("groupmanagement/membership/", views.group_membership, name="membership"),
|
||||||
|
|||||||
@@ -411,42 +411,3 @@ def group_request_leave(request, group_id):
|
|||||||
grouprequest.notify_leaders()
|
grouprequest.notify_leaders()
|
||||||
messages.success(request, _('Applied to leave group %(group)s.') % {"group": group})
|
messages.success(request, _('Applied to leave group %(group)s.') % {"group": group})
|
||||||
return redirect("groupmanagement:groups")
|
return redirect("groupmanagement:groups")
|
||||||
|
|
||||||
@login_required
|
|
||||||
def group_request_retract(request, group_id):
|
|
||||||
logger.debug(
|
|
||||||
f"group_request_retract called by user {request.user} for group id {group_id}"
|
|
||||||
)
|
|
||||||
group = get_object_or_404(Group, id=group_id)
|
|
||||||
|
|
||||||
if not GroupManager.check_internal_group(group):
|
|
||||||
logger.warning(
|
|
||||||
f"User {request.user} attempted to retract group request for "
|
|
||||||
f"group id {group_id} but it is not a joinable group"
|
|
||||||
)
|
|
||||||
messages.warning(
|
|
||||||
request,
|
|
||||||
_("You cannot retract that request"),
|
|
||||||
)
|
|
||||||
return redirect('groupmanagement:groups')
|
|
||||||
|
|
||||||
try:
|
|
||||||
group_request = GroupRequest.objects.get(
|
|
||||||
user=request.user, group=group, leave_request=False
|
|
||||||
)
|
|
||||||
group_request.delete()
|
|
||||||
|
|
||||||
logger.info(f"Deleted group request for user {request.user} to group {group}")
|
|
||||||
messages.success(
|
|
||||||
request, _('Retracted application to group %(group)s.') % {"group": group}
|
|
||||||
)
|
|
||||||
except GroupRequest.DoesNotExist:
|
|
||||||
logger.info(
|
|
||||||
f"{request.user} attempted to retract group request for "
|
|
||||||
f"group id {group_id} but has no open request"
|
|
||||||
)
|
|
||||||
messages.warning(
|
|
||||||
request, _("You have no open request for that group.")
|
|
||||||
)
|
|
||||||
|
|
||||||
return redirect("groupmanagement:groups")
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
class HRApplicationsConfig(AppConfig):
|
class HRApplicationsConfig(AppConfig):
|
||||||
name = 'allianceauth.hrapplications'
|
name = 'allianceauth.hrapplications'
|
||||||
label = 'hrapplications'
|
label = 'hrapplications'
|
||||||
verbose_name = _('HR Applications')
|
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
# Generated by Django 5.1.6 on 2025-03-04 04:31
|
|
||||||
|
|
||||||
import sortedm2m.fields
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
def create_permissions(apps, schema_editor) -> None:
|
|
||||||
# Remnant of AAv0
|
|
||||||
User = apps.get_model('auth', 'User')
|
|
||||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
|
||||||
Permission = apps.get_model('auth', 'Permission')
|
|
||||||
ct = ContentType.objects.get_for_model(User)
|
|
||||||
Permission.objects.get_or_create(codename="human_resources", content_type=ct, name="human_resources")
|
|
||||||
|
|
||||||
|
|
||||||
def reverse(apps, schema_editor) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
replaces = [('hrapplications', '0001_initial'), ('hrapplications', '0002_choices_for_questions'), ('hrapplications', '0003_applicationquestion_multi_select'), ('hrapplications', '0004_make_strings_more_stringy'), ('hrapplications', '0005_sorted_questions'), ('hrapplications', '0006_remove_legacy_models'), ('hrapplications', '0007_auto_20200918_1412')]
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
('eveonline', '0019_v5squash'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ApplicationQuestion',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('title', models.CharField(max_length=254, verbose_name='Question')),
|
|
||||||
('help_text', models.CharField(blank=True, max_length=254, null=True)),
|
|
||||||
('multi_select', models.BooleanField(default=False)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ApplicationForm',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('corp', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='eveonline.evecorporationinfo')),
|
|
||||||
('questions', sortedm2m.fields.SortedManyToManyField(help_text=None, to='hrapplications.applicationquestion')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ApplicationChoice',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('choice_text', models.CharField(max_length=200, verbose_name='Choice')),
|
|
||||||
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='hrapplications.applicationquestion')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Application',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('approved', models.BooleanField(blank=True, default=None, null=True)),
|
|
||||||
('created', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('form', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='applications', to='hrapplications.applicationform')),
|
|
||||||
('reviewer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
|
||||||
('reviewer_character', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='eveonline.evecharacter')),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='applications', to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'permissions': (('approve_application', 'Can approve applications'), ('reject_application', 'Can reject applications'), ('view_apis', 'Can view applicant APIs')),
|
|
||||||
'unique_together': {('form', 'user')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ApplicationComment',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('text', models.TextField()),
|
|
||||||
('created', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='hrapplications.application')),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ApplicationResponse',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('answer', models.TextField()),
|
|
||||||
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='hrapplications.application')),
|
|
||||||
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hrapplications.applicationquestion')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'unique_together': {('question', 'application')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.RunPython(create_permissions, reverse)
|
|
||||||
|
|
||||||
]
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
from sortedm2m.fields import SortedManyToManyField
|
from sortedm2m.fields import SortedManyToManyField
|
||||||
|
|
||||||
from typing import ClassVar
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
@@ -30,13 +29,6 @@ class ApplicationForm(models.Model):
|
|||||||
questions = SortedManyToManyField(ApplicationQuestion)
|
questions = SortedManyToManyField(ApplicationQuestion)
|
||||||
corp = models.OneToOneField(EveCorporationInfo, on_delete=models.CASCADE)
|
corp = models.OneToOneField(EveCorporationInfo, on_delete=models.CASCADE)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
permissions = (
|
|
||||||
# Intentionally Commented out
|
|
||||||
# AAv0 has these in the Auth_ Content Type
|
|
||||||
# ('human_resources', 'human_resources'))
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return str(self.corp)
|
return str(self.corp)
|
||||||
|
|
||||||
@@ -49,13 +41,13 @@ class Application(models.Model):
|
|||||||
reviewer_character = models.ForeignKey(EveCharacter, on_delete=models.SET_NULL, blank=True, null=True)
|
reviewer_character = models.ForeignKey(EveCharacter, on_delete=models.SET_NULL, blank=True, null=True)
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
objects: ClassVar[ApplicationManager] = ApplicationManager()
|
objects = ApplicationManager()
|
||||||
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
permissions = (
|
permissions = (
|
||||||
('approve_application', 'Can approve applications'),
|
('approve_application', 'Can approve applications'), ('reject_application', 'Can reject applications'),
|
||||||
('reject_application', 'Can reject applications'),
|
('view_apis', 'Can view applicant APIs'),)
|
||||||
)
|
|
||||||
unique_together = ('form', 'user')
|
unique_together = ('form', 'user')
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@@ -83,14 +75,14 @@ class ApplicationResponse(models.Model):
|
|||||||
question = models.ForeignKey(ApplicationQuestion, on_delete=models.CASCADE)
|
question = models.ForeignKey(ApplicationQuestion, on_delete=models.CASCADE)
|
||||||
application = models.ForeignKey(Application, on_delete=models.CASCADE, related_name='responses')
|
application = models.ForeignKey(Application, on_delete=models.CASCADE, related_name='responses')
|
||||||
answer = models.TextField()
|
answer = models.TextField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ('question', 'application')
|
unique_together = ('question', 'application')
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return str(self.application) + " Answer To " + str(self.question)
|
return str(self.application) + " Answer To " + str(self.question)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationComment(models.Model):
|
class ApplicationComment(models.Model):
|
||||||
application = models.ForeignKey(Application, on_delete=models.CASCADE, related_name='comments')
|
application = models.ForeignKey(Application, on_delete=models.CASCADE, related_name='comments')
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
|||||||
@@ -43,11 +43,11 @@
|
|||||||
<td class="text-center">{{ personal_app.form.corp.corporation_name }}</td>
|
<td class="text-center">{{ personal_app.form.corp.corporation_name }}</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
{% if personal_app.approved == None %}
|
{% if personal_app.approved == None %}
|
||||||
<div class="badge text-bg-warning">{% translate "Pending" %}</div>
|
<div class="badge bg-warning">{% translate "Pending" %}</div>
|
||||||
{% elif personal_app.approved == True %}
|
{% elif personal_app.approved == True %}
|
||||||
<div class="badge text-bg-success">{% translate "Approved" %}</div>
|
<div class="badge bg-success">{% translate "Approved" %}</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="badge text-bg-danger">{% translate "Rejected" %}</div>
|
<div class="badge bg-danger">{% translate "Rejected" %}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
@@ -133,14 +133,14 @@
|
|||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
{% if app.approved == None %}
|
{% if app.approved == None %}
|
||||||
{% if app.reviewer_str %}
|
{% if app.reviewer_str %}
|
||||||
<div class="badge text-bg-info">{% translate "Reviewer:" %} {{ app.reviewer_str }}</div>
|
<div class="badge bg-info">{% translate "Reviewer:" %} {{ app.reviewer_str }}</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="badge text-bg-warning">{% translate "Pending" %}</div>
|
<div class="badge bg-warning">{% translate "Pending" %}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif app.approved == True %}
|
{% elif app.approved == True %}
|
||||||
<div class="badge text-bg-success">{% translate "Approved" %}</div>
|
<div class="badge bg-success">{% translate "Approved" %}</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="badge text-bg-danger">{% translate "Rejected" %}</div>
|
<div class="badge bg-danger">{% translate "Rejected" %}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
@@ -177,14 +177,14 @@
|
|||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
{% if app.approved == None %}
|
{% if app.approved == None %}
|
||||||
{% if app.reviewer_str %}
|
{% if app.reviewer_str %}
|
||||||
<div class="badge text-bg-info">{% translate "Reviewer:" %} {{ app.reviewer_str }}</div>
|
<div class="badge bg-info">{% translate "Reviewer:" %} {{ app.reviewer_str }}</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="badge text-bg-warning">{% translate "Pending" %}</div>
|
<div class="badge bg-warning">{% translate "Pending" %}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif app.approved == True %}
|
{% elif app.approved == True %}
|
||||||
<div class="badge text-bg-success">{% translate "Approved" %}</div>
|
<div class="badge bg-success">{% translate "Approved" %}</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="badge text-bg-danger">{% translate "Rejected" %}</div>
|
<div class="badge bg-danger">{% translate "Rejected" %}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
|
|||||||
@@ -43,11 +43,11 @@
|
|||||||
<td>{{ app.form.corp }}</td>
|
<td>{{ app.form.corp }}</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
{% if app.approved == None %}
|
{% if app.approved == None %}
|
||||||
<div class="badge text-bg-warning">{% translate "Pending" %}</div>
|
<div class="badge bg-warning">{% translate "Pending" %}</div>
|
||||||
{% elif app.approved == True %}
|
{% elif app.approved == True %}
|
||||||
<div class="badge text-bg-success">{% translate "Approved" %}</div>
|
<div class="badge bg-success">{% translate "Approved" %}</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="badge text-bg-danger">{% translate "Rejected" %}</div>
|
<div class="badge bg-danger">{% translate "Rejected" %}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header text-bg-info">
|
<div class="card-header bg-info">
|
||||||
<div class="card-title mb-0">{% translate "Applicant" %}</div>
|
<div class="card-title mb-0">{% translate "Applicant" %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header text-bg-info">
|
<div class="card-header bg-info">
|
||||||
<div class="card-title mb-0">{% translate "Characters" %}</div>
|
<div class="card-title mb-0">{% translate "Characters" %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user