Compare commits

..

4 Commits

Author SHA1 Message Date
Ariel Rin
7edb870028 Translate django.po in zh-Hans
69% of minimum 50% translated source file: 'django.po'
on 'zh-Hans'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format
2025-04-24 17:22:06 +00:00
Ariel Rin
b7ffe21c63 Translate django.po in zh-Hans
68% of minimum 50% translated source file: 'django.po'
on 'zh-Hans'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format
2025-04-24 16:11:34 +00:00
Ariel Rin
7ba29250bc Translate django.po in es
83% of minimum 50% translated source file: 'django.po'
on 'es'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format
2025-04-05 17:36:41 +00:00
Ariel Rin
1051e9298a Translate django.po in uk
100% translated source file: 'django.po'
on 'uk'.
2025-03-15 10:23:13 +00:00
223 changed files with 5752 additions and 11943 deletions

View File

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

View File

@@ -25,19 +25,19 @@ exclude: |
repos: repos:
# Code Upgrades # Code Upgrades
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.21.2 rev: v3.19.1
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py38-plus] args: [--py38-plus]
- repo: https://github.com/adamchainz/django-upgrade - repo: https://github.com/adamchainz/django-upgrade
rev: 1.29.1 rev: 1.22.2
hooks: hooks:
- id: django-upgrade - id: django-upgrade
args: [--target-version=4.2] args: [--target-version=4.2]
# 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
@@ -55,6 +55,8 @@ repos:
# - id: check-docstring-first # - 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
@@ -64,11 +66,11 @@ 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.6.0 rev: 3.2.0
hooks: hooks:
- id: editorconfig-checker - id: editorconfig-checker
- repo: https://github.com/igorshubovych/markdownlint-cli - repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.47.0 rev: v0.44.0
hooks: hooks:
- id: markdownlint - id: markdownlint
language: node language: node
@@ -76,7 +78,7 @@ repos:
- --disable=MD013 - --disable=MD013
# Infrastructure # Infrastructure
- repo: https://github.com/tox-dev/pyproject-fmt - repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.11.1 rev: v2.5.0
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt
name: pyproject.toml formatter name: pyproject.toml formatter
@@ -84,9 +86,9 @@ repos:
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/abravalheri/validate-pyproject - repo: https://github.com/abravalheri/validate-pyproject
rev: v0.24.1 rev: v0.23
hooks: hooks:
- id: validate-pyproject - id: validate-pyproject
name: Validate pyproject.toml name: Validate pyproject.toml

View File

@@ -1 +0,0 @@
* @allianceauth

View File

@@ -1,15 +1,15 @@
# Alliance Auth # Alliance Auth
[![License](https://img.shields.io/badge/license-GPLv2-green)](https://pypi.org/project/allianceauth/) [![license](https://img.shields.io/badge/license-GPLv2-green)](https://pypi.org/project/allianceauth/)
[![Python Versions](https://img.shields.io/pypi/pyversions/allianceauth)](https://pypi.org/project/allianceauth/) [![python](https://img.shields.io/pypi/pyversions/allianceauth)](https://pypi.org/project/allianceauth/)
[![Django Versions](https://img.shields.io/pypi/djversions/allianceauth?label=django)](https://pypi.org/project/allianceauth/) [![django](https://img.shields.io/pypi/djversions/allianceauth?label=django)](https://pypi.org/project/allianceauth/)
[![Stable AA Version](https://img.shields.io/pypi/v/allianceauth?label=release)](https://pypi.org/project/allianceauth/) [![version](https://img.shields.io/pypi/v/allianceauth?label=release)](https://pypi.org/project/allianceauth/)
[![Pipeline Status](https://gitlab.com/allianceauth/allianceauth/badges/master/pipeline.svg)](https://gitlab.com/allianceauth/allianceauth/commits/master) [![pipeline status](https://gitlab.com/allianceauth/allianceauth/badges/master/pipeline.svg)](https://gitlab.com/allianceauth/allianceauth/commits/master)
[![Documentation Status](https://readthedocs.org/projects/allianceauth/badge/?version=latest)](https://allianceauth.readthedocs.io/?badge=latest) [![Documentation Status](https://readthedocs.org/projects/allianceauth/badge/?version=latest)](https://allianceauth.readthedocs.io/?badge=latest)
[![Test Coverage Report](https://gitlab.com/allianceauth/allianceauth/badges/master/coverage.svg)](https://gitlab.com/allianceauth/allianceauth/commits/master) [![coverage report](https://gitlab.com/allianceauth/allianceauth/badges/master/coverage.svg)](https://gitlab.com/allianceauth/allianceauth/commits/master)
[![Chat on Discord](https://img.shields.io/discord/399006117012832262.svg)](https://discord.gg/fjnHAmk) [![Chat on Discord](https://img.shields.io/discord/399006117012832262.svg)](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 ![screenshot](https://i.imgur.com/2tnX9kD.png)
![Flatly Theme](docs/_static/images/promotion/SampleInstallation-Flatly.png)
### Darkly Theme
![Darkly Theme](docs/_static/images/promotion/SampleInstallation-Darkly.png)
## Support ## Support
@@ -63,6 +57,7 @@ Here is an example of the Alliance Auth web site with a mixture of Services, App
- [Aaron Kable](https://gitlab.com/aaronkable/) - [Aaron Kable](https://gitlab.com/aaronkable/)
- [Ariel Rin](https://gitlab.com/soratidus999/) - [Ariel Rin](https://gitlab.com/soratidus999/)
- [Col Crunch](https://gitlab.com/colcrunch/) - [Col Crunch](https://gitlab.com/colcrunch/)
- [Erik Kalkoken](https://gitlab.com/ErikKalkoken/)
- [Rounon Dax](https://gitlab.com/ppfeufer) - [Rounon Dax](https://gitlab.com/ppfeufer)
- [snipereagle1](https://gitlab.com/mckernanin) - [snipereagle1](https://gitlab.com/mckernanin)
@@ -70,7 +65,6 @@ Here is an example of the Alliance Auth web site with a mixture of Services, App
- [Adarnof](https://gitlab.com/adarnof/) - [Adarnof](https://gitlab.com/adarnof/)
- [Basraah](https://gitlab.com/basraah/) - [Basraah](https://gitlab.com/basraah/)
- [Erik Kalkoken](https://gitlab.com/ErikKalkoken/)
### Beta Testers / Bug Fixers ### Beta Testers / Bug Fixers
@@ -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.

View File

@@ -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__ = '4.12.0' __version__ = '4.6.4'
__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__}'

View File

@@ -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')

View File

@@ -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 register, Tags
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

View File

@@ -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

View File

@@ -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",
),
),
]

View File

@@ -1,5 +1,4 @@
import logging import logging
from typing import ClassVar
from django.contrib.auth.models import User, Permission from django.contrib.auth.models import User, Permission
from django.db import models, transaction from django.db import models, transaction
@@ -28,7 +27,7 @@ class State(models.Model):
help_text="Factions to whose members this state is available.") 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']
@@ -97,8 +96,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,12 +111,6 @@ class UserProfile(models.Model):
null=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")
)
def assign_state(self, state=None, commit=True): def assign_state(self, state=None, commit=True):
if not state: if not state:
@@ -145,10 +137,8 @@ class UserProfile(models.Model):
sender=self.__class__, user=self.user, state=self.state sender=self.__class__, user=self.user, state=self.state
) )
def __str__(self) -> str: def __str__(self):
return str(self.user) return str(self.user)
class CharacterOwnership(models.Model): class CharacterOwnership(models.Model):
class Meta: class Meta:
default_permissions = ('change', 'delete') default_permissions = ('change', 'delete')
@@ -158,7 +148,7 @@ class CharacterOwnership(models.Model):
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()
def __str__(self): def __str__(self):
return f"{self.user}: {self.character}" return f"{self.user}: {self.character}"

View File

@@ -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)

View File

@@ -42,7 +42,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()})

View File

@@ -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>

View File

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

View File

@@ -1,9 +1,10 @@
{% load i18n %} {% load i18n %}
<form class="dropdown-item" action="{% url 'set_language' %}" method="post"> <div class="dropdown">
<form action="{% url 'set_language' %}" method="post">
{% csrf_token %} {% 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 %}
@@ -13,3 +14,4 @@
{% endfor %} {% endfor %}
</select> </select>
</form> </form>
</div>

View File

@@ -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)

View File

@@ -9,7 +9,6 @@ from allianceauth.tests.auth_utils import AuthUtils
from allianceauth.authentication.constants import ESI_ERROR_MESSAGE_OVERRIDES from allianceauth.authentication.constants import ESI_ERROR_MESSAGE_OVERRIDES
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:
@@ -18,7 +17,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:
@@ -28,64 +26,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)

View File

@@ -27,7 +27,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.templatetags.admin_status import _celery_stats
from .forms import RegistrationForm from .forms import RegistrationForm
from .models import CharacterOwnership from .models import CharacterOwnership
@@ -371,10 +370,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)

View File

@@ -13,7 +13,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):
@@ -28,7 +27,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'),
@@ -36,7 +35,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)

View File

@@ -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')

View File

@@ -1,6 +1,5 @@
import logging import logging
import os import os
from typing import ClassVar
from allianceauth.authentication.models import CharacterOwnership, UserProfile from allianceauth.authentication.models import CharacterOwnership, UserProfile
from bravado.exception import HTTPForbidden from bravado.exception import HTTPForbidden
@@ -41,9 +40,9 @@ class CorpStats(models.Model):
verbose_name = "corp stats" verbose_name = "corp stats"
verbose_name_plural = "corp stats" verbose_name_plural = "corp stats"
objects: ClassVar[CorpStatsManager] = CorpStatsManager() objects = CorpStatsManager()
def __str__(self) -> str: def __str__(self):
return f"{self.__class__.__name__} for {self.corp}" return f"{self.__class__.__name__} for {self.corp}"
def update(self): def update(self):

View File

@@ -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>&nbsp;</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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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")

View File

@@ -8,6 +8,7 @@ from allianceauth.custom_css.models import CustomCSS
# 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 pathlib import Path from pathlib import Path
@@ -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}">')

View File

@@ -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')

View File

@@ -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):
import allianceauth.eveonline.autogroups.signals import allianceauth.eveonline.autogroups.signals

View File

@@ -1,5 +1,4 @@
import logging import logging
from typing import ClassVar
from django.db import models, transaction 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
@@ -39,13 +38,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):
@@ -79,7 +78,7 @@ 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 __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@@ -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
@@ -74,51 +73,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())

View File

@@ -14,20 +14,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,

View File

@@ -1,5 +1,5 @@
import logging import logging
from typing import ClassVar, Union from typing import Union
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db import models from django.db import models
@@ -75,8 +75,8 @@ 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',])]
@@ -147,7 +147,7 @@ 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:
@@ -214,7 +214,7 @@ class EveCharacter(models.Model):
faction_id = models.PositiveIntegerField(blank=True, null=True, default=None) faction_id = models.PositiveIntegerField(blank=True, null=True, default=None)
faction_name = models.CharField(max_length=254, blank=True, null=True, default='') faction_name = models.CharField(max_length=254, blank=True, null=True, default='')
objects: ClassVar[EveCharacterManager] = EveCharacterManager() objects = EveCharacterManager()
provider = EveCharacterProviderManager() provider = EveCharacterProviderManager()
class Meta: class Meta:

View File

@@ -7,7 +7,7 @@ from jsonschema.exceptions import RefResolutionError
from django.conf import settings 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
@@ -185,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(
@@ -204,11 +202,7 @@ class EveSwaggerProvider(EveProvider):
def client(self): 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

View File

@@ -6,12 +6,6 @@ from jsonschema.exceptions import RefResolutionError
from django.test import TestCase from django.test import TestCase
from allianceauth import __url__ as aa_url
from allianceauth import __version__ as aa_version
from esi import __url__ as esi_url
from esi import __version__ as esi_version
from . import set_logger from . import set_logger
from .esi_client_stub import EsiClientStub from .esi_client_stub import EsiClientStub
from ..providers import ( from ..providers import (
@@ -723,12 +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()
expected_variants = { self.assertEqual(
f'AllianceAuth/{aa_version} (dummy@example.net; +{aa_url}) DjangoEsi/{esi_version} (+{esi_url})', # Django-ESI 8.0.0 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})' # Django-ESI 7.x, Py38 Py39 )
}
self.assertIn(operation.future.request.headers['User-Agent'], expected_variants)

View File

@@ -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')

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

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

View File

@@ -10,38 +10,23 @@ 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 char.character for char in CharacterOwnership.objects.filter(user=user)
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 []

View File

@@ -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")

View File

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

View File

@@ -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;
} }
} }
@@ -47,12 +25,6 @@
/* Side Navigation /* Side Navigation
------------------------------------------------------------------------------------- */ ------------------------------------------------------------------------------------- */
@media all { @media all {
.sidemenu-total-notifications-badge {
position: absolute;
left: 28px;
font-size: 42.5% !important;
}
#sidebar > div { #sidebar > div {
width: 325px; width: 325px;
} }
@@ -77,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;
} }
} }
@@ -90,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;
} }
} }
@@ -179,34 +154,3 @@
border-left-color: var(--bs-warning); border-left-color: var(--bs-warning);
} }
} }
/* DataTables 2
------------------------------------------------------------------------------------- */
@media all {
/* DataTables Processing Indicator
--------------------------------------------------------------------------------- */
div.dt-processing {
padding-top: 0.5rem !important;
}
div.dt-processing > div {
display: none;
}
svg.aa-datatables-process-indicator {
width: 2rem;
height: 2rem;
}
/* DataTables Extension: ColumnControl
--------------------------------------------------------------------------------- */
table.dataTable span.dtcc div.dtcc-search > div select {
background-color: var(--bs-body-bg);
color: var(--bs-body-color);
padding: 0.375rem;
}
table.dataTable .dt-column-header div.dtcc-search-title {
display: none;
}
}

View File

@@ -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');
});
});
});

View File

@@ -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")

View File

@@ -1,9 +0,0 @@
{% load i18n %}
<svg class="aa-datatables-process-indicator">
<use href="#aa-loading-spinner"></use>
</svg>
<p class="my-1">
{% translate "Loading …" %}
</p>

View File

@@ -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>

View File

@@ -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>

View File

@@ -2,8 +2,7 @@
{# {% 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 %}
@@ -11,5 +10,4 @@
<small class="text-muted">{{ subtitle }}</small> <small class="text-muted">{{ subtitle }}</small>
{% endif %} {% endif %}
</h1> </h1>
</header>
{% endif %} {% endif %}

View File

@@ -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

View File

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

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -19,7 +19,7 @@
{% translate "Join Requests" %} {% translate "Join Requests" %}
{% if acceptrequests %} {% if acceptrequests %}
<span id="acceptRequestsCounter" 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 id="leaveRequestsCounter" class="badge text-bg-secondary">{{ leaverequests|length }}</span> <span class="badge bg-secondary">{{ leaverequests|length }}</span>
{% endif %} {% endif %}
</a> </a>
</li> </li>
@@ -43,19 +43,19 @@
</li> </li>
{% endblock header_nav_collapse_left %} {% endblock header_nav_collapse_left %}
{% block content %} {% block content %}
<div class="tab-content"> <div class="tab-content">
<div id="add" class="tab-pane active"> <div id="add" class="tab-pane active">
{% if acceptrequests %} {% if acceptrequests %}
<div> <div class="table-responsive">
<table id="table-add" class="table table-responsive w-100"> <table class="table">
<thead> <thead>
<tr> <tr>
<th>{% translate "Character" %}</th> <th>{% translate "Character" %}</th>
<th>{% translate "Organization" %}</th> <th>{% translate "Organization" %}</th>
<th>{% translate "Group" %}</th> <th>{% translate "Group" %}</th>
<th></th> <th></th>
<th>{% translate "Corporation" %}</th>
</tr> </tr>
</thead> </thead>
@@ -89,24 +89,13 @@
<td>{{ acceptrequest.group.name }}</td> <td>{{ acceptrequest.group.name }}</td>
<td class="text-end"> <td class="text-end">
<div class="spinner-border spinner-border-sm mt-2 btns-join-{{acceptrequest.id}} d-none" role="status"> <a href="{% url 'groupmanagement:accept_request' acceptrequest.id %}" class="btn btn-success">
<span class="sr-only">Loading...</span>
</div>
<a id="{{acceptrequest.id}}" class="btn btn-success join-accept btns-join-{{acceptrequest.id}}">
{% translate "Accept" %} {% translate "Accept" %}
</a> </a>
<a id="{{acceptrequest.id}}" class="btn btn-danger join-reject btns-join-{{acceptrequest.id}}"> <a href="{% url 'groupmanagement:reject_request' acceptrequest.id %}" class="btn btn-danger">
{% translate "Reject" %} {% translate "Reject" %}
</a> </a>
</td> </td>
<td>
{% if acceptrequest.main_char %}
{{ acceptrequest.main_char.corporation_name }}
{% else %}
{% translate "(unknown)" %}
{% endif %}
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@@ -122,15 +111,14 @@
{% if not show_leave_tab %} {% if not show_leave_tab %}
<div id="leave" class="tab-pane"> <div id="leave" class="tab-pane">
{% if leaverequests %} {% if leaverequests %}
<div> <div class="table-responsive">
<table id="table-rem" class="table table-responsive w-100"> <table class="table">
<thead> <thead>
<tr> <tr>
<th>{% translate "Character" %}</th> <th>{% translate "Character" %}</th>
<th>{% translate "Organization" %}</th> <th>{% translate "Organization" %}</th>
<th>{% translate "Group" %}</th> <th>{% translate "Group" %}</th>
<th></th> <th></th>
<th>{% translate "Corporation" %}</th>
</tr> </tr>
</thead> </thead>
@@ -164,23 +152,14 @@
<td>{{ leaverequest.group.name }}</td> <td>{{ leaverequest.group.name }}</td>
<td class="text-end"> <td class="text-end">
<div class="spinner-border spinner-border-sm mt-2 btns-leave-{{leaverequest.id}} d-none" role="status"> <a href="{% url 'groupmanagement:leave_accept_request' leaverequest.id %}" class="btn btn-success">
<span class="sr-only">Loading...</span>
</div>
<a id="{{leaverequest.id}}" class="btn btn-success accept leave-accept btns-leave-{{leaverequest.id}}">
{% translate "Accept" %} {% translate "Accept" %}
</a> </a>
<a id="{{leaverequest.id}}" class="btn btn-danger reject leave-reject btns-leave-{{leaverequest.id}}">
<a href="{% url 'groupmanagement:leave_reject_request' leaverequest.id %}" class="btn btn-danger">
{% translate "Reject" %} {% translate "Reject" %}
</a> </a>
</td> </td>
<td>
{% if leaverequest.main_char %}
{{ leaverequest.main_char.corporation_name }}
{% else %}
{% translate "(unknown)" %}
{% endif %}
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@@ -193,189 +172,3 @@
{% endif %} {% endif %}
</div> </div>
{% endblock content %} {% endblock content %}
{% block extra_javascript %}
{% include 'bundles/datatables-js-bs5.html' %}
{% include "bundles/filterdropdown-js.html" %}
<script>
$(document).ready(function () {
let tableAdd = $("#table-add").DataTable({
filterDropDown: {
columns: [
{
idx: 4,
},
{
idx: 2,
}
],
bootstrap: true,
bootstrap_version: 5
},
columnDefs: [
{
target: 4,
visible: false,
},
],
paging: false,
responsive: true,
pageLength: -1
});
let tableRem = $("#table-rem").DataTable({
filterDropDown: {
columns: [
{
idx: 4,
},
{
idx: 2,
}
],
bootstrap: true,
bootstrap_version: 5
},
columnDefs: [
{
target: 4,
visible: false,
},
],
paging: false,
responsive: true,
pageLength: -1
});
// URL's for fetch requests
let acceptJoinURL = "{% url 'groupmanagement:accept_request' 0 %}";
acceptJoinURL = acceptJoinURL.substring(0, acceptJoinURL.length-2);
let rejectJoinURL = "{% url 'groupmanagement:reject_request' 0 %}";
rejectJoinURL = rejectJoinURL.substring(0, rejectJoinURL.length-2);
let acceptLeaveURL = "{% url 'groupmanagement:leave_accept_request' 0 %}";
acceptLeaveURL = acceptLeaveURL.substring(0, acceptLeaveURL.length-2);
let rejectLeaveURL = "{% url 'groupmanagement:leave_reject_request' 0 %}";
rejectLeaveURL = rejectLeaveURL.substring(0, rejectLeaveURL.length-2);
function removeRow(table, classLookup){
let btn = $(classLookup);
table.row($(btn[0]).parents('tr')).remove().draw();
}
function toggleButtons(classLookup){
let elems = document.querySelectorAll(classLookup);
elems.forEach(
function(e) {
e.classList.toggle('d-none');
}
);
}
function hitAuth(classLookup, table, URL){
toggleButtons(classLookup);
let output = fetch(URL)
.then(response => {
if (!response.ok) {
toggleButtons(classLookup);
return true;
}
removeRow(table, classLookup)
})
.catch(error => {
toggleButtons(classLookup);
return false;
});
toggleButtons(classLookup);
return output;
}
function decreaseCounterElement(elem){
count = Number(elem.innerText);
count -= 1;
if (!count){
elem.classList.add("d-none");
} else {
elem.innerText = count;
}
}
function decreaseCounter(id){
elem = document.getElementById(id);
if (elem){decreaseCounterElement(elem)}
}
function decreaseMenuCounter(){
decreaseCounter("globalNotificationCount");
let elem = document.querySelector("a[href='{% url "groupmanagement:management" %}']");
if (elem) {
let badge = elem.parentElement.querySelector("span");
if (badge){decreaseCounterElement(badge)}
if (elem.parentElement.parentElement.parentElement.tagName === "LI"){
let folderBadge = elem.parentElement.parentElement.parentElement.querySelector("span");
if (folderBadge){decreaseCounterElement(folderBadge)}
}
}
}
let acceptJoinButtons = document.querySelectorAll(".join-accept");
acceptJoinButtons.forEach(function(elem) {
elem.addEventListener("click", function(event) {
url = `${acceptJoinURL}${event.target.id}/`
let elemClass = `.btns-join-${event.target.id}`
if (hitAuth(elemClass, tableAdd, url)){
decreaseCounter("acceptRequestsCounter")
decreaseMenuCounter()
}
});
});
let rejectJoinButtons = document.querySelectorAll(".join-reject");
rejectJoinButtons.forEach(function(elem) {
elem.addEventListener("click", function(event) {
url = `${rejectJoinURL}${event.target.id}/`
let elemClass = `.btns-join-${event.target.id}`
if (hitAuth(elemClass, tableAdd, url)){
decreaseCounter("acceptRequestsCounter")
decreaseMenuCounter()
}
});
});
let acceptLeaveButtons = document.querySelectorAll(".leave-accept");
acceptLeaveButtons.forEach(function(elem) {
elem.addEventListener("click", function(event) {
url = `${acceptLeaveURL}${event.target.id}/`
let elemClass = `.btns-leave-${event.target.id}`
if (hitAuth(elemClass, tableRem, url)){
decreaseCounter("leaveRequestsCounter")
decreaseMenuCounter()
}
});
});
let rejectLeaveButtons = document.querySelectorAll(".leave-reject");
rejectLeaveButtons.forEach(function(elem) {
elem.addEventListener("click", function(event) {
url = `${rejectLeaveURL}${event.target.id}/`
let elemClass = `.btns-leave-${event.target.id}`
if (hitAuth(elemClass, tableRem, url)){
decreaseCounter("leaveRequestsCounter")
decreaseMenuCounter()
}
});
});
// Filter Dropdown sets widths so lets remove them when we tab change so they actually show.
$('a[data-bs-toggle="tab"]').on('shown.bs.tab', function (e) {
let elems = document.querySelectorAll(".form-select");
elems.forEach(
function(e) {
e.style.maxWidth = "";
}
);
});
});
</script>
{% endblock extra_javascript %}
{% block extra_css %}
{% include 'bundles/datatables-css-bs5.html' %}
{% endblock %}

View File

@@ -10,11 +10,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"),

View File

@@ -420,42 +420,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")

View File

@@ -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')

View File

@@ -1,4 +1,3 @@
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
from sortedm2m.fields import SortedManyToManyField from sortedm2m.fields import SortedManyToManyField
@@ -41,7 +40,7 @@ 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()
def __str__(self): def __str__(self):
return str(self.user) + " Application To " + str(self.form) return str(self.user) + " Application To " + str(self.form)

View File

@@ -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">

View File

@@ -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">

View File

@@ -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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
import logging import logging
from django.apps import AppConfig from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -13,7 +12,6 @@ logger = logging.getLogger(__name__)
class MenuConfig(AppConfig): class MenuConfig(AppConfig):
name = "allianceauth.menu" name = "allianceauth.menu"
label = "menu" label = "menu"
verbose_name = _("Menu")
def ready(self): def ready(self):
from allianceauth.menu.core import smart_sync from allianceauth.menu.core import smart_sync

View File

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

View File

@@ -1,4 +1,3 @@
from typing import ClassVar
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -69,7 +68,7 @@ class MenuItem(models.Model):
help_text=_("External URL this menu items will link to"), help_text=_("External URL this menu items will link to"),
) )
objects: ClassVar[MenuItemManager] = MenuItemManager() objects = MenuItemManager()
def __str__(self) -> str: def __str__(self) -> str:
return self.text return self.text

View File

@@ -27,7 +27,7 @@
</a> </a>
{% if item.count >= 1 %} {% if item.count >= 1 %}
<span class="badge text-bg-primary m-2 align-self-center{% if not item.is_folder %} me-2{% endif %}"> <span class="badge bg-primary m-2 align-self-center{% if not item.is_folder %} me-2{% endif %}">
{{ item.count }} {{ item.count }}
</span> </span>
{% elif item.url %} {% elif item.url %}
@@ -43,7 +43,8 @@
aria-expanded="false" aria-expanded="false"
aria-controls="" aria-controls=""
> >
<i class="fa-solid fa-chevron-right"></i> <i class="fas fa-chevron-right"></i>
<i class="fas fa-chevron-down"></i>
</span> </span>
<ul <ul
class="collapse ps-1 w-100 border-start rounded-start border-light border-3" class="collapse ps-1 w-100 border-start rounded-start border-light border-3"

View File

@@ -2,13 +2,13 @@
{% load navactive %} {% load navactive %}
{% load auth_notifications %} {% load auth_notifications %}
<li class="nav-item" id="menu_item_notifications"> <li class="nav-item {% navactive request 'notifications:' %}" id="menu_item_notifications">
<a class="nav-link {% navactive request 'notifications:' %}" href="{% url 'notifications:list' %}"> <a class="nav-link" href="{% url 'notifications:list' %}">
{% with unread_count=request.user|user_unread_notification_count %} {% with unread_count=request.user|user_unread_notification_count %}
<i class="fa-solid fa-bell me-2 fa-fw{% if unread_count %} text-danger{% endif %}"></i> <i class="fa-solid fa-bell{% if unread_count %} text-danger{% endif %}"></i>
{% endwith %} {% endwith %}
<span class="d-lg-none d-md-inline"> <span class="d-lg-none d-md-inline m-2">
{% translate "Notifications" %} {% translate "Notifications" %}
</span> </span>
</a> </a>

View File

@@ -2,51 +2,28 @@
{% load evelinks %} {% load evelinks %}
{% load theme_tags %} {% load theme_tags %}
<div <div id="aa-user-info" class="w-100 d-flex flex-column justify-content-center align-items-center text-center py-1 border-top border-secondary {% if not user.is_authenticated %}position-absolute bottom-0{% endif %}">
id="aa-user-info"
class="w-100 d-flex flex-column justify-content-center align-items-center text-center py-1 border-top border-secondary {% if not user.is_authenticated %}position-absolute bottom-0{% endif %}"
>
<div class="d-flex mb-0 w-100"> <div class="d-flex mb-0 w-100">
<div class="p-2 position-relative m-2"> <div class="p-2 position-relative m-2">
{% if user.is_authenticated %} {% if user.is_authenticated %}
{% with request.user.profile.main_character as main %} {% with request.user.profile.main_character as main %}
<img <img class="rounded-circle" src="{{ main.character_id|character_portrait_url:64 }}" alt="{{ main.character_name }}">
class="rounded-circle" <img class="rounded-circle position-absolute bottom-0 start-0" src="{{ main.corporation_logo_url_32 }}" alt="{{ main.corporation_name }}">
src="{{ main.character_id|character_portrait_url:64 }}"
alt="{{ main.character_name }}"
>
<img
class="rounded-circle position-absolute bottom-0 start-0"
src="{{ main.corporation_logo_url_32 }}"
alt="{{ main.corporation_name }}"
>
{% if main.alliance_id %} {% if main.alliance_id %}
<img <img class="rounded-circle position-absolute bottom-0 end-0" src="{{ main.alliance_logo_url_32 }}" alt="{{ main.alliance_name }}">
class="rounded-circle position-absolute bottom-0 end-0"
src="{{ main.alliance_logo_url_32 }}"
alt="{{ main.alliance_name }}"
>
{% elif main.faction_id %} {% elif main.faction_id %}
<img <img class="rounded-circle position-absolute bottom-0 end-0" src="{{ main.faction_logo_url_32 }}" alt="{{ main.faction_name }}">
class="rounded-circle position-absolute bottom-0 end-0"
src="{{ main.faction_logo_url_32 }}"
alt="{{ main.faction_name }}"
>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% else %} {% else %}
{% include "bundles/image-auth-logo.html" with logo_width="64px" %} {% include "bundles/image-auth-logo.html" with logo_width="64px" %}
{% endif %} {% endif %}
</div> </div>
<div class="align-self-center text-start"> <div class="align-self-center text-start">
{% if user.is_authenticated %} {% if user.is_authenticated %}
{% with request.user.profile.main_character as main %} {% with request.user.profile.main_character as main %}
<h5 class="m-0">{{ main.character_name }}</h5> <h5 class="m-0">{{ main.character_name }}</h5>
<p class="m-0 small">{{ main.corporation_name }}</p> <p class="m-0 small">{{ main.corporation_name }}</p>
{% if main.alliance_id %} {% if main.alliance_id %}
<p class="m-0 small">{{ main.alliance_name }}</p> <p class="m-0 small">{{ main.alliance_name }}</p>
{% elif main.faction_id %} {% elif main.faction_id %}
@@ -57,131 +34,56 @@
<h5 class="m-0">{{ SITE_NAME }}</h5> <h5 class="m-0">{{ SITE_NAME }}</h5>
{% endif %} {% endif %}
</div> </div>
<div class="ms-auto dropup"> <div class="ms-auto dropup">
<button type="button" class="h-100 btn" data-bs-toggle="dropdown" aria-expanded="false"> <button type="button" class="h-100 btn" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-gear fa-fw text-light"></i> <i class="fa-solid fa-gear fa-fw text-light"></i>
</button> </button>
<ul class="dropdown-menu" style="min-width: 200px;"> <ul class="dropdown-menu" style="min-width: 200px;">
<li><h6 class="dropdown-header">{% translate "Language" %}</h6></li> <li><h6 class="dropdown-header">{% translate "Language" %}</h6></li>
<li>
<a class="dropdown-item">{% include "public/lang_select.html" %}</a>
</li>
<li><h6 class="dropdown-header">{% translate "Theme" %}</h6></li>
<li> <li>
{% include "public/lang_select.html" %} <a class="dropdown-item">
</li>
{% theme_select %} {% theme_select %}
</a>
{% if user.is_authenticated and not request.is_mobile_device %}
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">{% translate "Sidebar" %}</h6></li>
<li>
<form class="dropdown-item" action="{% url 'minimize_sidebar' %}?next={{ request.path|urlencode }}" method="post">
{% csrf_token %}
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="toggle-sidebar"
onchange="this.form.submit()"
{% if request.session.MINIMIZE_SIDEBAR %}checked{% endif %}
>
<label class="form-check-label" for="toggle-sidebar">
{% translate "Minimize Sidebar" %}
</label>
</div>
</form>
</li> </li>
{% endif %}
{% if user.is_superuser %} {% if user.is_superuser %}
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">{% translate "Super User" %}</h6></li> <li><h6 class="dropdown-header">{% translate "Super User" %}</h6></li>
<li> <li>
<a <a class="dropdown-item" href="https://allianceauth.readthedocs.io/" title="Alliance Auth Documentation"><i class="fa-solid fa-book fa-fw"></i> Alliance Auth Documentation</a>
class="dropdown-item"
href="https://allianceauth.readthedocs.io/"
title="{% translate 'Alliance Auth Documentation' %}"
>
<i class="fa-solid fa-book fa-fw"></i>
{% translate "Alliance Auth Documentation" %}
</a>
</li> </li>
<li> <li>
<a <a class="dropdown-item" href="https://discord.gg/fjnHAmk" title="Alliance Auth Discord"><i class="fa-brands fa-discord fa-fw"></i> Alliance Auth Discord</a>
class="dropdown-item"
href="https://discord.gg/fjnHAmk"
title="{% translate 'Alliance Auth Discord' %}"
>
<i class="fa-brands fa-discord fa-fw"></i>
{% translate "Alliance Auth Discord" %}
</a>
</li> </li>
<li> <li>
<a <a class="dropdown-item" href="https://gitlab.com/allianceauth/allianceauth" title="Alliance Auth Git"><i class="fa-brands fa-gitlab fa-fw"></i> Alliance Auth Git</a>
class="dropdown-item"
href="https://gitlab.com/allianceauth/allianceauth"
title="{% translate 'Alliance Auth Git' %}"
>
<i class="fa-brands fa-gitlab fa-fw"></i>
{% translate "Alliance Auth Git" %}
</a>
</li> </li>
{% endif %} {% endif %}
{% if user.is_staff %} {% if user.is_staff %}
<li> <li>
<a <a class="dropdown-item" href="{% url 'admin:index' %}">
class="dropdown-item" <i class="fa-solid fa-gear fa-fw"></i> {% translate "Admin" %}
href="{% url 'admin:index' %}"
title="{% translate 'Admin' %}"
>
<i class="fa-solid fa-gear fa-fw"></i>
{% translate "Admin" %}
</a> </a>
</li> </li>
{% endif %} {% endif %}
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
{% if user.is_authenticated %} {% if user.is_authenticated %}
<li> <li>
<a <a class="dropdown-item" href="{% url 'authentication:token_management' %}">
class="dropdown-item" <i class="fa-solid fa-user-lock fa-fw"></i> Token Management
href="{% url 'authentication:token_management' %}"
title="{% translate 'Token Management' %}"
>
<i class="fa-solid fa-user-lock fa-fw"></i>
{% translate "Token Management" %}
</a> </a>
</li> </li>
<li> <li>
<a <a class="dropdown-item text-danger" href="{% url 'logout' %}" title="{% translate 'Sign Out' %}"><i class="fa-solid fa-right-from-bracket fa-fw "></i> {% translate 'Sign Out' %}</a>
class="dropdown-item text-danger"
href="{% url 'logout' %}"
title="{% translate 'Sign Out' %}"
>
<i class="fa-solid fa-right-from-bracket fa-fw"></i>
{% translate 'Sign Out' %}
</a>
</li> </li>
{% else %} {% else %}
<li> <li>
<a <a class="dropdown-item text-success" href="{% url 'authentication:login' %}" title="{% translate 'Sign In' %}"> <i class="fa-solid fa-right-to-bracket fa-fw "></i> {% translate 'Sign In' %}</a>
class="dropdown-item text-success"
href="{% url 'authentication:login' %}"
title="{% translate 'Sign In' %}"
>
<i class="fa-solid fa-right-to-bracket fa-fw"></i>
{% translate 'Sign In' %}
</a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>

View File

@@ -3,9 +3,9 @@
{% load menu_menu_items %} {% load menu_menu_items %}
<div class="col-auto px-0"> <div class="col-auto px-0">
<div class="collapse collapse-horizontal {% if user.is_authenticated and not request.is_mobile_device and not request.session.MINIMIZE_SIDEBAR %}show{% endif %}" tabindex="-1" id="sidebar"> <div class="collapse collapse-horizontal" tabindex="-1" id="sidebar">
<div> <div>
<div class="nav-padding navbar-dark text-bg-dark px-0 d-flex flex-column overflow-hidden vh-100 {% if not user.is_authenticated %}position-relative{% endif %}"> <div class="nav-padding navbar-dark bg-dark text-light px-0 d-flex flex-column overflow-hidden vh-100 {% if not user.is_authenticated %}position-relative{% endif %}">
{% if user.is_authenticated %} {% if user.is_authenticated %}
<ul id="sidebar-menu" class="navbar-nav flex-column mb-auto overflow-auto pt-2"> <ul id="sidebar-menu" class="navbar-nav flex-column mb-auto overflow-auto pt-2">
<li class="d-flex flex-wrap m-2 p-2 pt-0 pb-0 mt-0 mb-0 me-0 pe-0"> <li class="d-flex flex-wrap m-2 p-2 pt-0 pb-0 mt-0 mb-0 me-0 pe-0">

View File

@@ -1,65 +0,0 @@
"""
Alliance Auth Middleware
"""
from user_agents import parse
class DeviceDetectionMiddleware:
"""
Middleware to detect the type of device making the request.
Sets flags on the request object for easy access in views and templates.
Flags include:
- is_mobile: True if the device is a mobile phone.
- is_tablet: True if the device is a tablet.
- is_mobile_device: True if the device is either a mobile phone or a tablet.
- is_touch_capable: True if the device has touch capabilities.
- is_pc: True if the device is a desktop or laptop computer.
- is_bot: True if the device is identified as a bot or crawler.
"""
def __init__(self, get_response):
"""
Initialize the middleware with the get_response callable.
:param get_response:
:type get_response:
"""
self.get_response = get_response
def __call__(self, request):
"""
Process the incoming request to determine if it's from a mobile device.
This method is called when the middleware is invoked. It inspects the
`user-agent` header of the incoming HTTP request to determine the type
of client making the request (e.g., mobile, tablet, PC, bot, etc.).
Flags are set on the `request` object to indicate the client type.
:param request: The HTTP request object.
:type request: HttpRequest
:return: The HTTP response object after processing the request.
:rtype: HttpResponse
"""
# Retrieve the user-agent string from the request headers
user_agent_string = request.headers.get("user-agent", "")
# Parse the user-agent string to extract client information
user_agent = parse(user_agent_string)
# Set flags on the request object based on the client type
request.is_mobile = user_agent.is_mobile # True if the client is a mobile phone
request.is_tablet = user_agent.is_tablet # True if the client is a tablet
request.is_mobile_device = user_agent.is_mobile or user_agent.is_tablet # True if mobile phone or tablet
request.is_touch_capable = user_agent.is_touch_capable # True if the client supports touch input
request.is_pc = user_agent.is_pc # True if the client is a PC
request.is_bot = user_agent.is_bot # True if the client is a bot
# Pass the request to the next middleware or view and get the response
response = self.get_response(request)
# Return the processed response
return response

View File

@@ -1,8 +1,6 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class NotificationsConfig(AppConfig): class NotificationsConfig(AppConfig):
name = 'allianceauth.notifications' name = 'allianceauth.notifications'
label = 'notifications' label = 'notifications'
verbose_name = _('Notifications')

View File

@@ -1,5 +1,4 @@
import logging import logging
from typing import ClassVar
from django.db import models from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
@@ -57,7 +56,7 @@ class Notification(models.Model):
timestamp = models.DateTimeField(auto_now_add=True, db_index=True) timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
viewed = models.BooleanField(default=False, db_index=True) viewed = models.BooleanField(default=False, db_index=True)
objects: ClassVar[NotificationManager] = NotificationManager() objects = NotificationManager()
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.user}: {self.title}" return f"{self.user}: {self.title}"

View File

@@ -15,26 +15,30 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" id="unread-tab" data-bs-toggle="tab" data-bs-target="#unread" type="button" role="tab" aria-controls="unread" aria-selected="true"> <a class="nav-link active" id="unread-tab" data-bs-toggle="tab" data-bs-target="#unread" type="button" role="tab" aria-controls="unread" aria-selected="true">
{% translate "Unread" %} {% translate "Unread" %}
<span class="badge text-bg-secondary">{{ unread|length }}</span> <span class="badge bg-secondary">{{ unread|length }}</span>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" id="read-tab" data-bs-toggle="tab" data-bs-target="#read" type="button" role="tab" aria-controls="read" aria-selected="false"> <a class="nav-link" id="read-tab" data-bs-toggle="tab" data-bs-target="#read" type="button" role="tab" aria-controls="read" aria-selected="false">
{% translate "Read" %} {% translate "Read" %}
<span class="badge text-bg-secondary">{{ read|length }}</span> <span class="badge bg-secondary">{{ read|length }}</span>
</a> </a>
</li> </li>
{% endblock %} {% endblock %}
{% block header_nav_collapse_right %} {% block header_nav_collapse_right %}
{% translate "Mark all notifications as read" as nav_item_title %} <li class="nav-item">
{% url "notifications:mark_all_read" as nav_item_link %} <a href="{% url 'notifications:mark_all_read' %}" class="nav-link" title="{% translate 'Mark all notifications as read' %}">
{% include "framework/header/nav-collapse-icon.html" with fa_icon="fa-solid fa-check-double" url=nav_item_link title=nav_item_title icon_on_mobile=True %} <i class="fa-solid fa-check-double"></i>
</a>
</li>
{% translate "Delete all read notifications" as nav_item_title %} <li class="nav-item">
{% url "notifications:delete_all_read" as nav_item_link %} <a href="{% url 'notifications:delete_all_read' %}" class="nav-link" title="{% translate 'Delete all read notifications' %}">
{% include "framework/header/nav-collapse-icon.html" with fa_icon="fa-solid fa-trash-can" url=nav_item_link title=nav_item_title icon_on_mobile=True %} <i class="fa-solid fa-trash-can"></i>
</a>
</li>
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View File

@@ -17,7 +17,7 @@
</div> </div>
<div class="card border-{{ notif.level }}"> <div class="card border-{{ notif.level }}">
<div class="card-header text-bg-{{ notif.level }}"> <div class="card-header bg-{{ notif.level }}">
{{ notif.timestamp }} {{ notif.timestamp }}
{{ notif.title }} {{ notif.title }}
</div> </div>

View File

@@ -1,8 +1,6 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class OptimerConfig(AppConfig): class OptimerConfig(AppConfig):
name = 'allianceauth.optimer' name = 'allianceauth.optimer'
label = 'optimer' label = 'optimer'
verbose_name = _('Fleet Operations')

Some files were not shown because too many files have changed in this diff Show More