Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
424246df26 | ||
|
|
563e2210ef | ||
|
|
02a1078005 | ||
|
|
30107de44e | ||
|
|
77a08cd218 | ||
|
|
e5a09027e5 | ||
|
|
52b6c5d341 | ||
|
|
8b895b76b5 | ||
|
|
babd71702f | ||
|
|
3ec3cbdff7 | ||
|
|
51611e1237 | ||
|
|
39519bab91 | ||
|
|
90dc6a4d4c | ||
|
|
53ffd7f885 | ||
|
|
efc7475228 | ||
|
|
380c41400b | ||
|
|
079c12a72e | ||
|
|
4f1ebedc44 | ||
|
|
66822107e3 | ||
|
|
7856cd5ce4 | ||
|
|
36b3077caa | ||
|
|
1786f3a642 | ||
|
|
55927c6f15 | ||
|
|
8fbe0ba45d | ||
|
|
1563805ddb | ||
|
|
c58ed53369 | ||
|
|
32128ace1c | ||
|
|
7290eaad7e | ||
|
|
f23d4f4dd1 | ||
|
|
ab3f10e6f2 | ||
|
|
20187cc73e | ||
|
|
1f55fbfccc | ||
|
|
12383d79c8 | ||
|
|
56e2875650 | ||
|
|
d0118e6c0b | ||
|
|
7075ccdf7a | ||
|
|
b2d540c010 | ||
|
|
7cb7e2c77b | ||
|
|
5d6a4ab1a9 | ||
|
|
1122d617bd | ||
|
|
ef33501e45 | ||
|
|
08fd86db8f | ||
|
|
c4193c15fc | ||
|
|
903074080e | ||
|
|
3046a26a02 | ||
|
|
951c4135c2 | ||
|
|
b256a0c5e1 | ||
|
|
212b9b0f60 | ||
|
|
fc29d7e80d | ||
|
|
ec536c66a0 | ||
|
|
749ece45e2 | ||
|
|
b04c8873d0 | ||
|
|
9a77175bf3 | ||
|
|
5d4c7b9030 | ||
|
|
5f80259d57 | ||
|
|
dcd6bd1b36 | ||
|
|
6f4dffe930 | ||
|
|
56d70e6c74 | ||
|
|
5e14ea4573 | ||
|
|
c743eca0f7 | ||
|
|
2002f24178 | ||
|
|
6412aedf53 | ||
|
|
939df08b95 | ||
|
|
d8506aa753 | ||
|
|
3f2cdac658 | ||
|
|
d57ab01ff3 | ||
|
|
91b62bbe9d | ||
|
|
557a52e3c8 | ||
|
|
f8fefd92a5 | ||
|
|
f2c43ee921 | ||
|
|
99945b0146 | ||
|
|
abb9dc4db6 | ||
|
|
eba5b80cde | ||
|
|
5b39c887a5 | ||
|
|
183363e789 | ||
|
|
d8704f4d8f | ||
|
|
165ee44a63 | ||
|
|
e8f508cecb | ||
|
|
3044f18900 | ||
|
|
1cae20fe5f | ||
|
|
79637020f3 | ||
|
|
2d34422e2d |
@@ -25,7 +25,7 @@ before_script:
|
||||
pre-commit-check:
|
||||
<<: *only-default
|
||||
stage: pre-commit
|
||||
image: python:3.8-bullseye
|
||||
image: python:3.10-bullseye
|
||||
variables:
|
||||
PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
|
||||
cache:
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
[settings]
|
||||
profile=django
|
||||
sections=FUTURE,STDLIB,THIRDPARTY,DJANGO,ESI,FIRSTPARTY,LOCALFOLDER
|
||||
known_esi=esi
|
||||
known_django=django
|
||||
skip_gitignore=true
|
||||
@@ -7,12 +7,33 @@ repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: check-case-conflict
|
||||
- id: check-json
|
||||
- id: check-xml
|
||||
# Identify invalid files
|
||||
- id: check-ast
|
||||
- id: check-yaml
|
||||
- id: check-json
|
||||
- id: check-toml
|
||||
- id: check-xml
|
||||
|
||||
# git checks
|
||||
- id: check-merge-conflict
|
||||
- id: check-added-large-files
|
||||
args: [ --maxkb=1000 ]
|
||||
- id: detect-private-key
|
||||
- id: check-case-conflict
|
||||
|
||||
# Python checks
|
||||
# - id: check-docstring-first
|
||||
- id: debug-statements
|
||||
# - id: requirements-txt-fixer
|
||||
- id: fix-encoding-pragma
|
||||
args: [ --remove ]
|
||||
- id: fix-byte-order-marker
|
||||
|
||||
# General quality checks
|
||||
- id: mixed-line-ending
|
||||
args: [ --fix=lf ]
|
||||
- id: trailing-whitespace
|
||||
args: [ --markdown-linebreak-ext=md ]
|
||||
exclude: |
|
||||
(?x)(
|
||||
\.min\.css|
|
||||
@@ -21,6 +42,7 @@ repos:
|
||||
\.mo|
|
||||
swagger\.json
|
||||
)
|
||||
- id: check-executables-have-shebangs
|
||||
- id: end-of-file-fixer
|
||||
exclude: |
|
||||
(?x)(
|
||||
@@ -30,13 +52,9 @@ repos:
|
||||
\.mo|
|
||||
swagger\.json
|
||||
)
|
||||
- id: mixed-line-ending
|
||||
args: [ '--fix=lf' ]
|
||||
- id: fix-encoding-pragma
|
||||
args: [ '--remove' ]
|
||||
|
||||
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python
|
||||
rev: 2.7.1
|
||||
rev: 2.7.2
|
||||
hooks:
|
||||
- id: editorconfig-checker
|
||||
exclude: |
|
||||
@@ -48,14 +66,14 @@ repos:
|
||||
swagger\.json
|
||||
)
|
||||
|
||||
- repo: https://github.com/adamchainz/django-upgrade
|
||||
rev: 1.14.0
|
||||
hooks:
|
||||
- id: django-upgrade
|
||||
args: [ --target-version=4.0 ]
|
||||
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.3.1
|
||||
rev: v3.10.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [ --py38-plus ]
|
||||
|
||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||
rev: v2.2.0
|
||||
hooks:
|
||||
- id: setup-cfg-fmt
|
||||
args: [ --include-version-classifiers ]
|
||||
|
||||
0
.tx/config_20230406134150.bak
Executable file → Normal file
@@ -1,7 +0,0 @@
|
||||
include LICENSE
|
||||
include README.md
|
||||
include MANIFEST.in
|
||||
graft allianceauth
|
||||
|
||||
global-exclude __pycache__
|
||||
global-exclude *.py[co]
|
||||
@@ -1,7 +1,11 @@
|
||||
"""An auth system for EVE Online to help in-game organizations
|
||||
manage online service access.
|
||||
"""
|
||||
|
||||
# This will make sure the app is always imported when
|
||||
# Django starts so that shared_task will use this app.
|
||||
|
||||
__version__ = '3.4.0'
|
||||
__version__ = '3.7.1'
|
||||
__title__ = 'Alliance Auth'
|
||||
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
|
||||
NAME = f'{__title__} v{__version__}'
|
||||
|
||||
@@ -65,7 +65,7 @@ class StateBackend(ModelBackend):
|
||||
# we've seen this character owner before. Re-attach to their old user account
|
||||
user = records[0].user
|
||||
if user.profile.main_character:
|
||||
if ownership.user.profile.main_character.character_id != token.character_id:
|
||||
if user.profile.main_character.character_id != token.character_id:
|
||||
## this is an alt, enforce main only due to trust issues in SSO.
|
||||
if request:
|
||||
messages.error("Unable to authenticate with this Character, Please log in with the main character associated with this account. Then add this character from the dashboard.")
|
||||
|
||||
0
allianceauth/authentication/core/__init__.py
Normal file
48
allianceauth/authentication/core/celery_workers.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""API for interacting with celery workers."""
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from amqp.exceptions import ChannelError
|
||||
from celery import current_app
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def active_tasks_count() -> Optional[int]:
|
||||
"""Return count of currently active tasks
|
||||
or None if celery workers are not online.
|
||||
"""
|
||||
inspect = current_app.control.inspect()
|
||||
return _tasks_count(inspect.active())
|
||||
|
||||
|
||||
def _tasks_count(data: dict) -> Optional[int]:
|
||||
"""Return count of tasks in data from celery inspect API."""
|
||||
try:
|
||||
tasks = itertools.chain(*data.values())
|
||||
except AttributeError:
|
||||
return None
|
||||
return len(list(tasks))
|
||||
|
||||
|
||||
def queued_tasks_count() -> Optional[int]:
|
||||
"""Return count of queued tasks. Return None if there was an error."""
|
||||
try:
|
||||
with current_app.connection_or_acquire() as conn:
|
||||
result = conn.default_channel.queue_declare(
|
||||
queue=getattr(settings, "CELERY_DEFAULT_QUEUE", "celery"), passive=True
|
||||
)
|
||||
return result.message_count
|
||||
|
||||
except ChannelError:
|
||||
# Queue doesn't exist, probably empty
|
||||
return 0
|
||||
|
||||
except Exception:
|
||||
logger.exception("Failed to get celery queue length")
|
||||
|
||||
return None
|
||||
@@ -1,18 +1,28 @@
|
||||
from django.conf.urls import include
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from functools import wraps
|
||||
from django.shortcuts import redirect
|
||||
from typing import Callable, Iterable, Optional
|
||||
|
||||
from django.urls import include
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
|
||||
def user_has_main_character(user):
|
||||
return bool(user.profile.main_character)
|
||||
|
||||
|
||||
def decorate_url_patterns(urls, decorator):
|
||||
def decorate_url_patterns(
|
||||
urls, decorator: Callable, excluded_views: Optional[Iterable] = None
|
||||
):
|
||||
"""Decorate views given in url patterns except when they are explicitly excluded.
|
||||
|
||||
Args:
|
||||
- urls: Django URL patterns
|
||||
- decorator: Decorator to be added to each view
|
||||
- exclude_views: Optional iterable of view names to be excluded
|
||||
"""
|
||||
url_list, app_name, namespace = include(urls)
|
||||
|
||||
def process_patterns(url_patterns):
|
||||
@@ -22,6 +32,8 @@ def decorate_url_patterns(urls, decorator):
|
||||
process_patterns(pattern.url_patterns)
|
||||
else:
|
||||
# this is a pattern
|
||||
if excluded_views and pattern.lookup_str in excluded_views:
|
||||
return
|
||||
pattern.callback = decorator(pattern.callback)
|
||||
|
||||
process_patterns(url_list)
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
from django.conf.urls import include
|
||||
|
||||
from allianceauth.authentication import views
|
||||
from django.urls import re_path
|
||||
from django.urls import path
|
||||
from django.urls import include, re_path, path
|
||||
|
||||
urlpatterns = [
|
||||
path('activate/complete/', views.activation_complete, name='registration_activation_complete'),
|
||||
|
||||
0
allianceauth/authentication/managers.py
Executable file → Normal file
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 4.0.10 on 2023-05-28 15:36
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("authentication", "0020_userprofile_language_userprofile_night_mode"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="userprofile",
|
||||
name="language",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("en", "English"),
|
||||
("de", "German"),
|
||||
("es", "Spanish"),
|
||||
("zh-hans", "Chinese Simplified"),
|
||||
("ru", "Russian"),
|
||||
("ko", "Korean"),
|
||||
("fr", "French"),
|
||||
("ja", "Japanese"),
|
||||
("it", "Italian"),
|
||||
("uk", "Ukrainian"),
|
||||
],
|
||||
default="",
|
||||
max_length=10,
|
||||
verbose_name="Language",
|
||||
),
|
||||
),
|
||||
]
|
||||
29
allianceauth/authentication/models.py
Executable file → Normal file
@@ -63,6 +63,22 @@ class UserProfile(models.Model):
|
||||
class Meta:
|
||||
default_permissions = ('change',)
|
||||
|
||||
class Language(models.TextChoices):
|
||||
"""
|
||||
Choices for UserProfile.language
|
||||
"""
|
||||
|
||||
ENGLISH = 'en', _('English')
|
||||
GERMAN = 'de', _('German')
|
||||
SPANISH = 'es', _('Spanish')
|
||||
CHINESE = 'zh-hans', _('Chinese Simplified')
|
||||
RUSSIAN = 'ru', _('Russian')
|
||||
KOREAN = 'ko', _('Korean')
|
||||
FRENCH = 'fr', _('French')
|
||||
JAPANESE = 'ja', _('Japanese')
|
||||
ITALIAN = 'it', _('Italian')
|
||||
UKRAINIAN = 'uk', _('Ukrainian')
|
||||
|
||||
user = models.OneToOneField(
|
||||
User,
|
||||
related_name='profile',
|
||||
@@ -76,20 +92,9 @@ class UserProfile(models.Model):
|
||||
State,
|
||||
on_delete=models.SET_DEFAULT,
|
||||
default=get_guest_state_pk)
|
||||
LANGUAGE_CHOICES = [
|
||||
('en', _('English')),
|
||||
('de', _('German')),
|
||||
('es', _('Spanish')),
|
||||
('zh-hans', _('Chinese Simplified')),
|
||||
('ru', _('Russian')),
|
||||
('ko', _('Korean')),
|
||||
('fr', _('French')),
|
||||
('ja', _('Japanese')),
|
||||
('it', _('Italian')),
|
||||
]
|
||||
language = models.CharField(
|
||||
_("Language"), max_length=10,
|
||||
choices=LANGUAGE_CHOICES,
|
||||
choices=Language.choices,
|
||||
blank=True,
|
||||
default='')
|
||||
night_mode = models.BooleanField(
|
||||
|
||||
@@ -1,29 +1,34 @@
|
||||
from collections import namedtuple
|
||||
"""Counters for Task Statistics."""
|
||||
|
||||
import datetime as dt
|
||||
from typing import NamedTuple, Optional
|
||||
|
||||
from .event_series import EventSeries
|
||||
|
||||
|
||||
"""Global series for counting task events."""
|
||||
# Global series for counting task events.
|
||||
succeeded_tasks = EventSeries("SUCCEEDED_TASKS")
|
||||
retried_tasks = EventSeries("RETRIED_TASKS")
|
||||
failed_tasks = EventSeries("FAILED_TASKS")
|
||||
|
||||
|
||||
_TaskCounts = namedtuple(
|
||||
"_TaskCounts", ["succeeded", "retried", "failed", "total", "earliest_task", "hours"]
|
||||
)
|
||||
class _TaskCounts(NamedTuple):
|
||||
succeeded: int
|
||||
retried: int
|
||||
failed: int
|
||||
total: int
|
||||
earliest_task: Optional[dt.datetime]
|
||||
hours: int
|
||||
|
||||
|
||||
def dashboard_results(hours: int) -> _TaskCounts:
|
||||
"""Counts of all task events within the given timeframe."""
|
||||
"""Counts of all task events within the given time frame."""
|
||||
|
||||
def earliest_if_exists(events: EventSeries, earliest: dt.datetime) -> list:
|
||||
my_earliest = events.first_event(earliest=earliest)
|
||||
return [my_earliest] if my_earliest else []
|
||||
|
||||
earliest = dt.datetime.utcnow() - dt.timedelta(hours=hours)
|
||||
earliest_events = list()
|
||||
earliest_events = []
|
||||
succeeded_count = succeeded_tasks.count(earliest=earliest)
|
||||
earliest_events += earliest_if_exists(succeeded_tasks, earliest)
|
||||
retried_count = retried_tasks.count(earliest=earliest)
|
||||
|
||||
@@ -1,61 +1,31 @@
|
||||
"""Event series for Task Statistics."""
|
||||
|
||||
import datetime as dt
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from pytz import utc
|
||||
from redis import Redis, RedisError
|
||||
from redis import Redis
|
||||
|
||||
from allianceauth.utils.cache import get_redis_client
|
||||
from .helpers import get_redis_client_or_stub
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _RedisStub:
|
||||
"""Stub of a Redis client.
|
||||
|
||||
It's purpose is to prevent EventSeries objects from trying to access Redis
|
||||
when it is not available. e.g. when the Sphinx docs are rendered by readthedocs.org.
|
||||
"""
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def incr(self, *args, **kwargs):
|
||||
return 0
|
||||
|
||||
def zadd(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def zcount(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def zrangebyscore(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class EventSeries:
|
||||
"""API for recording and analyzing a series of events."""
|
||||
|
||||
_ROOT_KEY = "ALLIANCEAUTH_EVENT_SERIES"
|
||||
|
||||
def __init__(self, key_id: str, redis: Redis = None) -> None:
|
||||
self._redis = get_redis_client() if not redis else redis
|
||||
try:
|
||||
if not self._redis.ping():
|
||||
raise RuntimeError()
|
||||
except (AttributeError, RedisError, RuntimeError):
|
||||
logger.exception(
|
||||
"Failed to establish a connection with Redis. "
|
||||
"This EventSeries object is disabled.",
|
||||
)
|
||||
self._redis = _RedisStub()
|
||||
def __init__(self, key_id: str, redis: Optional[Redis] = None) -> None:
|
||||
self._redis = get_redis_client_or_stub() if not redis else redis
|
||||
self._key_id = str(key_id)
|
||||
self.clear()
|
||||
|
||||
@property
|
||||
def is_disabled(self):
|
||||
"""True when this object is disabled, e.g. Redis was not available at startup."""
|
||||
return isinstance(self._redis, _RedisStub)
|
||||
return hasattr(self._redis, "IS_STUB")
|
||||
|
||||
@property
|
||||
def _key_counter(self):
|
||||
@@ -73,8 +43,8 @@ class EventSeries:
|
||||
"""
|
||||
if not event_time:
|
||||
event_time = dt.datetime.utcnow()
|
||||
id = self._redis.incr(self._key_counter)
|
||||
self._redis.zadd(self._key_sorted_set, {id: event_time.timestamp()})
|
||||
my_id = self._redis.incr(self._key_counter)
|
||||
self._redis.zadd(self._key_sorted_set, {my_id: event_time.timestamp()})
|
||||
|
||||
def all(self) -> List[dt.datetime]:
|
||||
"""List of all known events."""
|
||||
@@ -95,15 +65,15 @@ class EventSeries:
|
||||
self._redis.delete(self._key_counter)
|
||||
|
||||
def count(self, earliest: dt.datetime = None, latest: dt.datetime = None) -> int:
|
||||
"""Count of events, can be restricted to given timeframe.
|
||||
"""Count of events, can be restricted to given time frame.
|
||||
|
||||
Args:
|
||||
- earliest: Date of first events to count(inclusive), or -infinite if not specified
|
||||
- latest: Date of last events to count(inclusive), or +infinite if not specified
|
||||
"""
|
||||
min = "-inf" if not earliest else earliest.timestamp()
|
||||
max = "+inf" if not latest else latest.timestamp()
|
||||
return self._redis.zcount(self._key_sorted_set, min=min, max=max)
|
||||
minimum = "-inf" if not earliest else earliest.timestamp()
|
||||
maximum = "+inf" if not latest else latest.timestamp()
|
||||
return self._redis.zcount(self._key_sorted_set, min=minimum, max=maximum)
|
||||
|
||||
def first_event(self, earliest: dt.datetime = None) -> Optional[dt.datetime]:
|
||||
"""Date/Time of first event. Returns `None` if series has no events.
|
||||
@@ -111,10 +81,10 @@ class EventSeries:
|
||||
Args:
|
||||
- earliest: Date of first events to count(inclusive), or any if not specified
|
||||
"""
|
||||
min = "-inf" if not earliest else earliest.timestamp()
|
||||
minimum = "-inf" if not earliest else earliest.timestamp()
|
||||
event = self._redis.zrangebyscore(
|
||||
self._key_sorted_set,
|
||||
min,
|
||||
minimum,
|
||||
"+inf",
|
||||
withscores=True,
|
||||
start=0,
|
||||
|
||||
49
allianceauth/authentication/task_statistics/helpers.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""Helpers for Task Statistics."""
|
||||
|
||||
import logging
|
||||
|
||||
from redis import Redis, RedisError
|
||||
|
||||
from allianceauth.utils.cache import get_redis_client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _RedisStub:
|
||||
"""Stub of a Redis client.
|
||||
|
||||
It's purpose is to prevent EventSeries objects from trying to access Redis
|
||||
when it is not available. e.g. when the Sphinx docs are rendered by readthedocs.org.
|
||||
"""
|
||||
|
||||
IS_STUB = True
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def incr(self, *args, **kwargs):
|
||||
return 0
|
||||
|
||||
def zadd(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def zcount(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def zrangebyscore(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
def get_redis_client_or_stub() -> Redis:
|
||||
"""Return AA's default cache client or a stub if Redis is not available."""
|
||||
redis = get_redis_client()
|
||||
try:
|
||||
if not redis.ping():
|
||||
raise RuntimeError()
|
||||
except (AttributeError, RedisError, RuntimeError):
|
||||
logger.exception(
|
||||
"Failed to establish a connection with Redis. "
|
||||
"This EventSeries object is disabled.",
|
||||
)
|
||||
return _RedisStub()
|
||||
return redis
|
||||
@@ -1,9 +1,7 @@
|
||||
"""Signals for Task Statistics."""
|
||||
|
||||
from celery.signals import (
|
||||
task_failure,
|
||||
task_internal_error,
|
||||
task_retry,
|
||||
task_success,
|
||||
worker_ready
|
||||
task_failure, task_internal_error, task_retry, task_success, worker_ready,
|
||||
)
|
||||
|
||||
from django.conf import settings
|
||||
@@ -19,6 +17,7 @@ def reset_counters():
|
||||
|
||||
|
||||
def is_enabled() -> bool:
|
||||
"""Return True if task statistics are enabled, else return False."""
|
||||
return not bool(
|
||||
getattr(settings, "ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED", False)
|
||||
)
|
||||
|
||||
@@ -4,29 +4,30 @@ from django.test import TestCase
|
||||
from django.utils.timezone import now
|
||||
|
||||
from allianceauth.authentication.task_statistics.counters import (
|
||||
dashboard_results,
|
||||
succeeded_tasks,
|
||||
retried_tasks,
|
||||
failed_tasks,
|
||||
dashboard_results, failed_tasks, retried_tasks, succeeded_tasks,
|
||||
)
|
||||
|
||||
|
||||
class TestDashboardResults(TestCase):
|
||||
def test_should_return_counts_for_given_timeframe_only(self):
|
||||
def test_should_return_counts_for_given_time_frame_only(self):
|
||||
# given
|
||||
earliest_task = now() - dt.timedelta(minutes=15)
|
||||
|
||||
succeeded_tasks.clear()
|
||||
succeeded_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
|
||||
succeeded_tasks.add(earliest_task)
|
||||
succeeded_tasks.add()
|
||||
succeeded_tasks.add()
|
||||
|
||||
retried_tasks.clear()
|
||||
retried_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
|
||||
retried_tasks.add(now() - dt.timedelta(seconds=30))
|
||||
retried_tasks.add()
|
||||
|
||||
failed_tasks.clear()
|
||||
failed_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
|
||||
failed_tasks.add()
|
||||
|
||||
# when
|
||||
results = dashboard_results(hours=1)
|
||||
# then
|
||||
|
||||
@@ -1,48 +1,19 @@
|
||||
import datetime as dt
|
||||
from unittest.mock import patch
|
||||
|
||||
from pytz import utc
|
||||
from redis import RedisError
|
||||
|
||||
from django.test import TestCase
|
||||
from django.utils.timezone import now
|
||||
|
||||
from allianceauth.authentication.task_statistics.event_series import (
|
||||
EventSeries,
|
||||
_RedisStub,
|
||||
)
|
||||
from allianceauth.authentication.task_statistics.helpers import _RedisStub
|
||||
|
||||
MODULE_PATH = "allianceauth.authentication.task_statistics.event_series"
|
||||
|
||||
|
||||
class TestEventSeries(TestCase):
|
||||
def test_should_abort_without_redis_client(self):
|
||||
# when
|
||||
with patch(MODULE_PATH + ".get_redis_client") as mock:
|
||||
mock.return_value = None
|
||||
events = EventSeries("dummy")
|
||||
# then
|
||||
self.assertTrue(events._redis, _RedisStub)
|
||||
self.assertTrue(events.is_disabled)
|
||||
|
||||
def test_should_disable_itself_if_redis_not_available_1(self):
|
||||
# when
|
||||
with patch(MODULE_PATH + ".get_redis_client") as mock_get_master_client:
|
||||
mock_get_master_client.return_value.ping.side_effect = RedisError
|
||||
events = EventSeries("dummy")
|
||||
# then
|
||||
self.assertIsInstance(events._redis, _RedisStub)
|
||||
self.assertTrue(events.is_disabled)
|
||||
|
||||
def test_should_disable_itself_if_redis_not_available_2(self):
|
||||
# when
|
||||
with patch(MODULE_PATH + ".get_redis_client") as mock_get_master_client:
|
||||
mock_get_master_client.return_value.ping.return_value = False
|
||||
events = EventSeries("dummy")
|
||||
# then
|
||||
self.assertIsInstance(events._redis, _RedisStub)
|
||||
self.assertTrue(events.is_disabled)
|
||||
|
||||
def test_should_add_event(self):
|
||||
# given
|
||||
events = EventSeries("dummy")
|
||||
@@ -166,3 +137,15 @@ class TestEventSeries(TestCase):
|
||||
results = events.all()
|
||||
# then
|
||||
self.assertEqual(len(results), 2)
|
||||
|
||||
def test_should_not_report_as_disabled_when_initialized_normally(self):
|
||||
# given
|
||||
events = EventSeries("dummy")
|
||||
# when/then
|
||||
self.assertFalse(events.is_disabled)
|
||||
|
||||
def test_should_report_as_disabled_when_initialized_with_redis_stub(self):
|
||||
# given
|
||||
events = EventSeries("dummy", redis=_RedisStub())
|
||||
# when/then
|
||||
self.assertTrue(events.is_disabled)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
from redis import RedisError
|
||||
|
||||
from allianceauth.authentication.task_statistics.helpers import (
|
||||
_RedisStub, get_redis_client_or_stub,
|
||||
)
|
||||
|
||||
MODULE_PATH = "allianceauth.authentication.task_statistics.helpers"
|
||||
|
||||
|
||||
class TestGetRedisClient(TestCase):
|
||||
def test_should_return_mock_if_redis_not_available_1(self):
|
||||
# when
|
||||
with patch(MODULE_PATH + ".get_redis_client") as mock_get_master_client:
|
||||
mock_get_master_client.return_value.ping.side_effect = RedisError
|
||||
result = get_redis_client_or_stub()
|
||||
# then
|
||||
self.assertIsInstance(result, _RedisStub)
|
||||
|
||||
def test_should_return_mock_if_redis_not_available_2(self):
|
||||
# when
|
||||
with patch(MODULE_PATH + ".get_redis_client") as mock_get_master_client:
|
||||
mock_get_master_client.return_value.ping.return_value = False
|
||||
result = get_redis_client_or_stub()
|
||||
# then
|
||||
self.assertIsInstance(result, _RedisStub)
|
||||
@@ -17,16 +17,17 @@ from allianceauth.eveonline.tasks import update_character
|
||||
|
||||
|
||||
@override_settings(
|
||||
CELERY_ALWAYS_EAGER=True,ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED=False
|
||||
CELERY_ALWAYS_EAGER=True, ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED=False
|
||||
)
|
||||
class TestTaskSignals(TestCase):
|
||||
fixtures = ["disable_analytics"]
|
||||
|
||||
def test_should_record_successful_task(self):
|
||||
# given
|
||||
def setUp(self) -> None:
|
||||
succeeded_tasks.clear()
|
||||
retried_tasks.clear()
|
||||
failed_tasks.clear()
|
||||
|
||||
def test_should_record_successful_task(self):
|
||||
# when
|
||||
with patch(
|
||||
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
|
||||
@@ -39,10 +40,6 @@ class TestTaskSignals(TestCase):
|
||||
self.assertEqual(failed_tasks.count(), 0)
|
||||
|
||||
def test_should_record_retried_task(self):
|
||||
# given
|
||||
succeeded_tasks.clear()
|
||||
retried_tasks.clear()
|
||||
failed_tasks.clear()
|
||||
# when
|
||||
with patch(
|
||||
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
|
||||
@@ -55,10 +52,6 @@ class TestTaskSignals(TestCase):
|
||||
self.assertEqual(retried_tasks.count(), 1)
|
||||
|
||||
def test_should_record_failed_task(self):
|
||||
# given
|
||||
succeeded_tasks.clear()
|
||||
retried_tasks.clear()
|
||||
failed_tasks.clear()
|
||||
# when
|
||||
with patch(
|
||||
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<select onchange="this.form.submit()" class="form-control" id="lang-select" name="language">
|
||||
{% get_language_info_list for LANGUAGES as languages %}
|
||||
{% for language in languages %}
|
||||
<option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected="selected"{% endif %}>
|
||||
<option lang="{{ language.code }}" value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected="selected"{% endif %}>
|
||||
{{ language.name_local|capfirst }} ({{ language.code }})
|
||||
</option>
|
||||
{% endfor %}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
{% block page_title %}{% translate "Login" %}{% endblock %}
|
||||
|
||||
{% block middle_box_content %}
|
||||
<a href="{% url 'auth_sso_login' %}{% if request.GET.next %}?next={{request.GET.next}}{%endif%}">
|
||||
<a href="{% url 'auth_sso_login' %}{% if request.GET.next %}?next={{request.GET.next | urlencode}}{%endif%}">
|
||||
<img class="img-responsive center-block" src="{% static 'allianceauth/authentication/img/sso/EVE_SSO_Login_Buttons_Large_Black.png' %}" alt="{% translate 'Login with Eve SSO' %}">
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
0
allianceauth/authentication/tests/core/__init__.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from amqp.exceptions import ChannelError
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from allianceauth.authentication.core.celery_workers import (
|
||||
active_tasks_count, queued_tasks_count,
|
||||
)
|
||||
|
||||
MODULE_PATH = "allianceauth.authentication.core.celery_workers"
|
||||
|
||||
|
||||
@patch(MODULE_PATH + ".current_app")
|
||||
class TestActiveTasksCount(TestCase):
|
||||
def test_should_return_correct_count_when_no_active_tasks(self, mock_current_app):
|
||||
# given
|
||||
mock_current_app.control.inspect.return_value.active.return_value = {
|
||||
"queue": []
|
||||
}
|
||||
# when
|
||||
result = active_tasks_count()
|
||||
# then
|
||||
self.assertEqual(result, 0)
|
||||
|
||||
def test_should_return_correct_task_count_for_active_tasks(self, mock_current_app):
|
||||
# given
|
||||
mock_current_app.control.inspect.return_value.active.return_value = {
|
||||
"queue": [1, 2, 3]
|
||||
}
|
||||
# when
|
||||
result = active_tasks_count()
|
||||
# then
|
||||
self.assertEqual(result, 3)
|
||||
|
||||
def test_should_return_correct_task_count_for_multiple_queues(
|
||||
self, mock_current_app
|
||||
):
|
||||
# given
|
||||
mock_current_app.control.inspect.return_value.active.return_value = {
|
||||
"queue_1": [1, 2],
|
||||
"queue_2": [3, 4],
|
||||
}
|
||||
# when
|
||||
result = active_tasks_count()
|
||||
# then
|
||||
self.assertEqual(result, 4)
|
||||
|
||||
def test_should_return_none_when_celery_not_available(self, mock_current_app):
|
||||
# given
|
||||
mock_current_app.control.inspect.return_value.active.return_value = None
|
||||
# when
|
||||
result = active_tasks_count()
|
||||
# then
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
@patch(MODULE_PATH + ".current_app")
|
||||
class TestQueuedTasksCount(TestCase):
|
||||
def test_should_return_queue_length_when_queue_exists(self, mock_current_app):
|
||||
# given
|
||||
mock_conn = (
|
||||
mock_current_app.connection_or_acquire.return_value.__enter__.return_value
|
||||
)
|
||||
mock_conn.default_channel.queue_declare.return_value.message_count = 7
|
||||
# when
|
||||
result = queued_tasks_count()
|
||||
# then
|
||||
self.assertEqual(result, 7)
|
||||
|
||||
def test_should_return_0_when_queue_does_not_exists(self, mock_current_app):
|
||||
# given
|
||||
mock_current_app.connection_or_acquire.side_effect = ChannelError
|
||||
# when
|
||||
result = queued_tasks_count()
|
||||
# then
|
||||
self.assertEqual(result, 0)
|
||||
|
||||
def test_should_return_None_on_other_errors(self, mock_current_app):
|
||||
# given
|
||||
mock_current_app.connection_or_acquire.side_effect = RuntimeError
|
||||
# when
|
||||
result = queued_tasks_count()
|
||||
# then
|
||||
self.assertIsNone(result)
|
||||
@@ -4,16 +4,16 @@ from urllib import parse
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.http.response import HttpResponse
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.urls import reverse, URLPattern
|
||||
|
||||
from allianceauth.eveonline.models import EveCharacter
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
|
||||
from ..decorators import main_character_required
|
||||
from ..models import CharacterOwnership
|
||||
|
||||
from ..decorators import decorate_url_patterns, main_character_required
|
||||
from ..models import CharacterOwnership
|
||||
|
||||
MODULE_PATH = 'allianceauth.authentication'
|
||||
|
||||
@@ -66,3 +66,33 @@ class DecoratorTestCase(TestCase):
|
||||
setattr(self.request, 'user', self.main_user)
|
||||
response = self.dummy_view(self.request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class TestDecorateUrlPatterns(TestCase):
|
||||
def test_should_add_decorator_by_default(self):
|
||||
# given
|
||||
decorator = mock.MagicMock(name="decorator")
|
||||
view = mock.MagicMock(name="view")
|
||||
path = mock.MagicMock(spec=URLPattern, name="path")
|
||||
path.callback = view
|
||||
path.lookup_str = "my_lookup_str"
|
||||
urls = [path]
|
||||
urlconf_module = urls
|
||||
# when
|
||||
decorate_url_patterns(urlconf_module, decorator)
|
||||
# then
|
||||
self.assertEqual(path.callback, decorator(view))
|
||||
|
||||
def test_should_not_add_decorator_when_excluded(self):
|
||||
# given
|
||||
decorator = mock.MagicMock(name="decorator")
|
||||
view = mock.MagicMock(name="view")
|
||||
path = mock.MagicMock(spec=URLPattern, name="path")
|
||||
path.callback = view
|
||||
path.lookup_str = "my_lookup_str"
|
||||
urls = [path]
|
||||
urlconf_module = urls
|
||||
# when
|
||||
decorate_url_patterns(urlconf_module, decorator, excluded_views=["my_lookup_str"])
|
||||
# then
|
||||
self.assertEqual(path.callback, view)
|
||||
|
||||
@@ -9,12 +9,8 @@ from django.core.cache import cache
|
||||
from django.test import TestCase
|
||||
|
||||
from allianceauth.templatetags.admin_status import (
|
||||
status_overview,
|
||||
_fetch_list_from_gitlab,
|
||||
_current_notifications,
|
||||
_current_version_summary,
|
||||
_fetch_notification_issues_from_gitlab,
|
||||
_latests_versions
|
||||
_current_notifications, _current_version_summary, _fetch_list_from_gitlab,
|
||||
_fetch_notification_issues_from_gitlab, _latests_versions, status_overview,
|
||||
)
|
||||
|
||||
MODULE_PATH = 'allianceauth.templatetags'
|
||||
@@ -56,14 +52,10 @@ TEST_VERSION = '2.6.5'
|
||||
|
||||
class TestStatusOverviewTag(TestCase):
|
||||
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
|
||||
@patch(MODULE_PATH + '.admin_status._fetch_celery_queue_length')
|
||||
@patch(MODULE_PATH + '.admin_status._current_version_summary')
|
||||
@patch(MODULE_PATH + '.admin_status._current_notifications')
|
||||
def test_status_overview(
|
||||
self,
|
||||
mock_current_notifications,
|
||||
mock_current_version_info,
|
||||
mock_fetch_celery_queue_length
|
||||
self, mock_current_notifications, mock_current_version_info
|
||||
):
|
||||
# given
|
||||
notifications = {
|
||||
@@ -82,7 +74,6 @@ class TestStatusOverviewTag(TestCase):
|
||||
'latest_beta_version': '2.4.4a1',
|
||||
}
|
||||
mock_current_version_info.return_value = version_info
|
||||
mock_fetch_celery_queue_length.return_value = 3
|
||||
# when
|
||||
result = status_overview()
|
||||
# then
|
||||
@@ -96,7 +87,6 @@ class TestStatusOverviewTag(TestCase):
|
||||
self.assertEqual(result["latest_minor_version"], '2.4.0')
|
||||
self.assertEqual(result["latest_patch_version"], '2.4.5')
|
||||
self.assertEqual(result["latest_beta_version"], '2.4.4a1')
|
||||
self.assertEqual(result["task_queue_length"], 3)
|
||||
|
||||
|
||||
class TestNotifications(TestCase):
|
||||
|
||||
39
allianceauth/authentication/tests/test_views.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import RequestFactory, TestCase
|
||||
|
||||
from allianceauth.authentication.views import task_counts
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
|
||||
MODULE_PATH = "allianceauth.authentication.views"
|
||||
|
||||
|
||||
def jsonresponse_to_dict(response) -> dict:
|
||||
return json.loads(response.content)
|
||||
|
||||
|
||||
@patch(MODULE_PATH + ".queued_tasks_count")
|
||||
@patch(MODULE_PATH + ".active_tasks_count")
|
||||
class TestRunningTasksCount(TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
super().setUpClass()
|
||||
cls.factory = RequestFactory()
|
||||
cls.user = AuthUtils.create_user("bruce_wayne")
|
||||
|
||||
def test_should_return_data(
|
||||
self, mock_active_tasks_count, mock_queued_tasks_count
|
||||
):
|
||||
# given
|
||||
mock_active_tasks_count.return_value = 2
|
||||
mock_queued_tasks_count.return_value = 3
|
||||
request = self.factory.get("/")
|
||||
request.user = self.user
|
||||
# when
|
||||
response = task_counts(request)
|
||||
# then
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertDictEqual(
|
||||
jsonresponse_to_dict(response), {"tasks_running": 2, "tasks_queued": 3}
|
||||
)
|
||||
@@ -38,4 +38,5 @@ urlpatterns = [
|
||||
name='token_refresh'
|
||||
),
|
||||
path('dashboard/', views.dashboard, name='dashboard'),
|
||||
path('task-counts/', views.task_counts, name='task_counts'),
|
||||
]
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
import logging
|
||||
|
||||
from django_registration.backends.activation.views import (
|
||||
REGISTRATION_SALT, ActivationView as BaseActivationView,
|
||||
RegistrationView as BaseRegistrationView,
|
||||
)
|
||||
from django_registration.signals import user_registered
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import login, authenticate
|
||||
from django.contrib.auth import authenticate, login
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.models import User
|
||||
from django.core import signing
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from allianceauth.eveonline.models import EveCharacter
|
||||
from esi.decorators import token_required
|
||||
from esi.models import Token
|
||||
|
||||
from django_registration.backends.activation.views import (
|
||||
RegistrationView as BaseRegistrationView,
|
||||
ActivationView as BaseActivationView,
|
||||
REGISTRATION_SALT
|
||||
)
|
||||
from django_registration.signals import user_registered
|
||||
from allianceauth.eveonline.models import EveCharacter
|
||||
|
||||
from .models import CharacterOwnership
|
||||
from .core.celery_workers import active_tasks_count, queued_tasks_count
|
||||
from .forms import RegistrationForm
|
||||
from .models import CharacterOwnership
|
||||
|
||||
if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS:
|
||||
_has_auto_groups = True
|
||||
@@ -61,6 +61,7 @@ def dashboard(request):
|
||||
}
|
||||
return render(request, 'authentication/dashboard.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def token_management(request):
|
||||
tokens = request.user.token_set.all()
|
||||
@@ -70,6 +71,7 @@ def token_management(request):
|
||||
}
|
||||
return render(request, 'authentication/tokens.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def token_delete(request, token_id=None):
|
||||
try:
|
||||
@@ -83,6 +85,7 @@ def token_delete(request, token_id=None):
|
||||
messages.warning(request, "Token does not exist")
|
||||
return redirect('authentication:token_management')
|
||||
|
||||
|
||||
@login_required
|
||||
def token_refresh(request, token_id=None):
|
||||
try:
|
||||
@@ -127,7 +130,7 @@ def main_character_change(request, token):
|
||||
def add_character(request, token):
|
||||
if CharacterOwnership.objects.filter(character__character_id=token.character_id).filter(
|
||||
owner_hash=token.character_owner_hash).filter(user=request.user).exists():
|
||||
messages.success(request, _('Added %(name)s to your account.'% ({'name': token.character_name})))
|
||||
messages.success(request, _('Added %(name)s to your account.' % ({'name': token.character_name})))
|
||||
else:
|
||||
messages.error(request, _('Failed to add %(name)s to your account: they already have an account.' % ({'name': token.character_name})))
|
||||
return redirect('authentication:dashboard')
|
||||
@@ -268,8 +271,11 @@ class ActivationView(BaseActivationView):
|
||||
|
||||
def validate_key(self, activation_key):
|
||||
try:
|
||||
dump = signing.loads(activation_key, salt=REGISTRATION_SALT,
|
||||
max_age=settings.ACCOUNT_ACTIVATION_DAYS * 86400)
|
||||
dump = signing.loads(
|
||||
activation_key,
|
||||
salt=REGISTRATION_SALT,
|
||||
max_age=settings.ACCOUNT_ACTIVATION_DAYS * 86400
|
||||
)
|
||||
return dump
|
||||
except signing.BadSignature:
|
||||
return None
|
||||
@@ -299,3 +305,12 @@ def activation_complete(request):
|
||||
def registration_closed(request):
|
||||
messages.error(request, _('Registration of new accounts is not allowed at this time.'))
|
||||
return redirect('authentication:login')
|
||||
|
||||
|
||||
def task_counts(request) -> JsonResponse:
|
||||
"""Return task counts as JSON for an AJAX call."""
|
||||
data = {
|
||||
"tasks_running": active_tasks_count(),
|
||||
"tasks_queued": queued_tasks_count()
|
||||
}
|
||||
return JsonResponse(data)
|
||||
|
||||
@@ -14,6 +14,7 @@ def sync_user_groups(modeladmin, request, queryset):
|
||||
agc.update_all_states_group_membership()
|
||||
|
||||
|
||||
@admin.register(AutogroupsConfig)
|
||||
class AutogroupsConfigAdmin(admin.ModelAdmin):
|
||||
formfield_overrides = {
|
||||
models.CharField: {'strip': False}
|
||||
@@ -36,6 +37,5 @@ class AutogroupsConfigAdmin(admin.ModelAdmin):
|
||||
return actions
|
||||
|
||||
|
||||
admin.site.register(AutogroupsConfig, AutogroupsConfigAdmin)
|
||||
admin.site.register(ManagedCorpGroup)
|
||||
admin.site.register(ManagedAllianceGroup)
|
||||
|
||||
0
allianceauth/eveonline/views.py
Executable file → Normal file
0
allianceauth/groupmanagement/views.py
Executable file → Normal file
2
allianceauth/hrapplications/admin.py
Executable file → Normal file
@@ -10,6 +10,7 @@ class ChoiceInline(admin.TabularInline):
|
||||
verbose_name_plural = 'Choices (optional)'
|
||||
verbose_name= 'Choice'
|
||||
|
||||
@admin.register(ApplicationQuestion)
|
||||
class QuestionAdmin(admin.ModelAdmin):
|
||||
fieldsets = [
|
||||
(None, {'fields': ['title', 'help_text', 'multi_select']}),
|
||||
@@ -18,6 +19,5 @@ class QuestionAdmin(admin.ModelAdmin):
|
||||
|
||||
admin.site.register(Application)
|
||||
admin.site.register(ApplicationComment)
|
||||
admin.site.register(ApplicationQuestion, QuestionAdmin)
|
||||
admin.site.register(ApplicationForm)
|
||||
admin.site.register(ApplicationResponse)
|
||||
|
||||
0
allianceauth/hrapplications/forms.py
Executable file → Normal file
0
allianceauth/hrapplications/models.py
Executable file → Normal file
18
allianceauth/hrapplications/views.py
Executable file → Normal file
@@ -57,7 +57,7 @@ def hr_application_create_view(request, form_id=None):
|
||||
app_form = get_object_or_404(ApplicationForm, id=form_id)
|
||||
if request.method == "POST":
|
||||
if Application.objects.filter(user=request.user).filter(form=app_form).exists():
|
||||
logger.warn(f"User {request.user} attempting to duplicate application to {app_form.corp}")
|
||||
logger.warning(f"User {request.user} attempting to duplicate application to {app_form.corp}")
|
||||
else:
|
||||
application = Application(user=request.user, form=app_form)
|
||||
application.save()
|
||||
@@ -92,7 +92,7 @@ def hr_application_personal_view(request, app_id):
|
||||
}
|
||||
return render(request, 'hrapplications/view.html', context=context)
|
||||
else:
|
||||
logger.warn(f"User {request.user} not authorized to view {app}")
|
||||
logger.warning(f"User {request.user} not authorized to view {app}")
|
||||
return redirect('hrapplications:personal_view')
|
||||
|
||||
|
||||
@@ -105,9 +105,9 @@ def hr_application_personal_removal(request, app_id):
|
||||
logger.info(f"User {request.user} deleting {app}")
|
||||
app.delete()
|
||||
else:
|
||||
logger.warn(f"User {request.user} attempting to delete reviewed app {app}")
|
||||
logger.warning(f"User {request.user} attempting to delete reviewed app {app}")
|
||||
else:
|
||||
logger.warn(f"User {request.user} not authorized to delete {app}")
|
||||
logger.warning(f"User {request.user} not authorized to delete {app}")
|
||||
return redirect('hrapplications:index')
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ def hr_application_view(request, app_id):
|
||||
logger.info(f"Saved comment by user {request.user} to {app}")
|
||||
return redirect('hrapplications:view', app_id)
|
||||
else:
|
||||
logger.warn("User %s does not have permission to add ApplicationComments" % request.user)
|
||||
logger.warning("User %s does not have permission to add ApplicationComments" % request.user)
|
||||
return redirect('hrapplications:view', app_id)
|
||||
else:
|
||||
logger.debug("Returning blank HRApplication comment form.")
|
||||
@@ -171,7 +171,7 @@ def hr_application_approve(request, app_id):
|
||||
app.save()
|
||||
notify(app.user, "Application Accepted", message="Your application to %s has been approved." % app.form.corp, level="success")
|
||||
else:
|
||||
logger.warn(f"User {request.user} not authorized to approve {app}")
|
||||
logger.warning(f"User {request.user} not authorized to approve {app}")
|
||||
return redirect('hrapplications:index')
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ def hr_application_reject(request, app_id):
|
||||
app.save()
|
||||
notify(app.user, "Application Rejected", message="Your application to %s has been rejected." % app.form.corp, level="danger")
|
||||
else:
|
||||
logger.warn(f"User {request.user} not authorized to reject {app}")
|
||||
logger.warning(f"User {request.user} not authorized to reject {app}")
|
||||
return redirect('hrapplications:index')
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@ def hr_application_search(request):
|
||||
app_list = app_list.filter(
|
||||
form__corp__corporation_id=request.user.profile.main_character.corporation_id)
|
||||
except AttributeError:
|
||||
logger.warn(
|
||||
logger.warning(
|
||||
"User %s missing main character model: unable to filter applications to search" % request.user)
|
||||
|
||||
applications = app_list.filter(
|
||||
@@ -246,6 +246,6 @@ def hr_application_mark_in_progress(request, app_id):
|
||||
app.save()
|
||||
notify(app.user, "Application In Progress", message=f"Your application to {app.form.corp} is being reviewed by {app.reviewer_str}")
|
||||
else:
|
||||
logger.warn(
|
||||
logger.warning(
|
||||
f"User {request.user} unable to mark {app} in progress: already being reviewed by {app.reviewer}")
|
||||
return redirect("hrapplications:view", app_id)
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
from .core import notify # noqa: F401
|
||||
|
||||
default_app_config = 'allianceauth.notifications.apps.NotificationsConfig'
|
||||
|
||||
@@ -15,18 +15,22 @@ class NotificationAdmin(admin.ModelAdmin):
|
||||
ordering = ("-timestamp", )
|
||||
search_fields = ["user__username", "user__profile__main_character__character_name"]
|
||||
|
||||
@admin.display(
|
||||
ordering="user__profile__main_character__character_name"
|
||||
)
|
||||
def _main(self, obj):
|
||||
try:
|
||||
return obj.user.profile.main_character
|
||||
except AttributeError:
|
||||
return obj.user
|
||||
|
||||
_main.admin_order_field = "user__profile__main_character__character_name"
|
||||
|
||||
@admin.display(
|
||||
ordering="user__profile__state__name"
|
||||
)
|
||||
def _state(self, obj):
|
||||
return obj.user.profile.state
|
||||
|
||||
_state.admin_order_field = "user__profile__state__name"
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
@@ -44,7 +44,7 @@ def notification_view(request, notif_id):
|
||||
notif.mark_viewed()
|
||||
return render(request, 'notifications/view.html', context)
|
||||
else:
|
||||
logger.warn(
|
||||
logger.warning(
|
||||
"User %s not authorized to view notif_id %s belonging to user %s",
|
||||
request.user,
|
||||
notif_id, notif.user
|
||||
|
||||
@@ -13,6 +13,10 @@ app = Celery('{{ project_name }}')
|
||||
# the configuration object to child processes.
|
||||
app.config_from_object('django.conf:settings')
|
||||
|
||||
# Automatically try to establish the connection to the AMQP broker on
|
||||
# Celery startup if it is unavailable.
|
||||
app.conf.broker_connection_retry_on_startup = True
|
||||
|
||||
# setup priorities ( 0 Highest, 9 Lowest )
|
||||
app.conf.broker_transport_options = {
|
||||
'priority_steps': list(range(10)), # setup que to have 10 steps
|
||||
|
||||
@@ -41,23 +41,23 @@ CELERYBEAT_SCHEDULER = "django_celery_beat.schedulers.DatabaseScheduler"
|
||||
CELERYBEAT_SCHEDULE = {
|
||||
'esi_cleanup_callbackredirect': {
|
||||
'task': 'esi.tasks.cleanup_callbackredirect',
|
||||
'schedule': crontab(minute=0, hour='*/4'),
|
||||
'schedule': crontab(minute='0', hour='*/4'),
|
||||
},
|
||||
'esi_cleanup_token': {
|
||||
'task': 'esi.tasks.cleanup_token',
|
||||
'schedule': crontab(minute=0, hour=0),
|
||||
'schedule': crontab(minute='0', hour='0'),
|
||||
},
|
||||
'run_model_update': {
|
||||
'task': 'allianceauth.eveonline.tasks.run_model_update',
|
||||
'schedule': crontab(minute=0, hour="*/6"),
|
||||
'schedule': crontab(minute='0', hour="*/6"),
|
||||
},
|
||||
'check_all_character_ownership': {
|
||||
'task': 'allianceauth.authentication.tasks.check_all_character_ownership',
|
||||
'schedule': crontab(minute=0, hour='*/4'),
|
||||
'schedule': crontab(minute='0', hour='*/4'),
|
||||
},
|
||||
'analytics_daily_stats': {
|
||||
'task': 'allianceauth.analytics.tasks.analytics_daily_stats',
|
||||
'schedule': crontab(minute=0, hour=2),
|
||||
'schedule': crontab(minute='0', hour='2'),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,13 @@ INSTALLED_APPS += [
|
||||
# To change the logging level for extensions, uncomment the following line.
|
||||
# LOGGING['handlers']['extension_file']['level'] = 'DEBUG'
|
||||
|
||||
# By default, apps are prevented from having public views for security reasons.
|
||||
# To allow specific apps to have public views, add them to APPS_WITH_PUBLIC_VIEWS
|
||||
# » The format is the same as in INSTALLED_APPS
|
||||
# » The app developer must also explicitly allow public views for their app
|
||||
APPS_WITH_PUBLIC_VIEWS = [
|
||||
|
||||
]
|
||||
|
||||
# Enter credentials to use MySQL/MariaDB. Comment out to use sqlite3
|
||||
DATABASES['default'] = {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from django.conf.urls import include
|
||||
from allianceauth import urls
|
||||
from django.urls import re_path
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'', include(urls)),
|
||||
path('', include(urls)),
|
||||
]
|
||||
|
||||
handler500 = 'allianceauth.views.Generic500Redirect'
|
||||
|
||||
@@ -66,6 +66,8 @@ class NameFormatConfigAdmin(admin.ModelAdmin):
|
||||
form = NameFormatConfigForm
|
||||
list_display = ('service_name', 'get_state_display_string')
|
||||
|
||||
@admin.display(
|
||||
description='States'
|
||||
)
|
||||
def get_state_display_string(self, obj):
|
||||
return ', '.join([state.name for state in obj.states.all()])
|
||||
get_state_display_string.short_description = 'States'
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
from django.conf.urls import include
|
||||
from django.urls import re_path
|
||||
from string import Formatter
|
||||
from typing import Iterable, Optional
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import include, re_path
|
||||
from django.utils.functional import cached_property
|
||||
from django.conf import settings
|
||||
from string import Formatter
|
||||
|
||||
from allianceauth.hooks import get_hooks
|
||||
|
||||
from .models import NameFormatConfig
|
||||
|
||||
|
||||
def get_extension_logger(name):
|
||||
"""
|
||||
Takes the name of a plugin/extension and generates a child logger of the extensions logger
|
||||
@@ -156,8 +158,32 @@ class MenuItemHook:
|
||||
|
||||
|
||||
class UrlHook:
|
||||
def __init__(self, urls, namespace, base_url):
|
||||
"""A hook for registering the URLs of a Django app.
|
||||
|
||||
Args:
|
||||
- urls: The urls module to include
|
||||
- namespace: The URL namespace to apply. This is usually just the app name.
|
||||
- base_url: The URL prefix to match against in regex form.
|
||||
Example ``r'^app_name/'``.
|
||||
This prefix will be applied in front of all URL patterns included.
|
||||
It is possible to use the same prefix as existing apps (or no prefix at all),
|
||||
but standard URL resolution ordering applies
|
||||
(hook URLs are the last ones registered).
|
||||
- excluded_views: Optional list of views to be excluded
|
||||
from auto-decorating them with the
|
||||
default ``main_character_required`` decorator, e.g. to make them public.
|
||||
Views must be specified by their qualified name,
|
||||
e.g. ``["example.views.my_public_view"]``
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
urls,
|
||||
namespace: str,
|
||||
base_url: str,
|
||||
excluded_views : Optional[Iterable[str]] = None
|
||||
):
|
||||
self.include_pattern = re_path(base_url, include(urls, namespace=namespace))
|
||||
self.excluded_views = set(excluded_views or [])
|
||||
|
||||
|
||||
class NameFormatter:
|
||||
|
||||
@@ -6,6 +6,7 @@ from ...admin import ServicesUserAdmin
|
||||
from . import __title__
|
||||
from .models import DiscordUser
|
||||
from .utils import LoggerAddTag
|
||||
from .auth_hooks import DiscordService
|
||||
|
||||
logger = LoggerAddTag(logging.getLogger(__name__), __title__)
|
||||
|
||||
@@ -27,6 +28,6 @@ class DiscordUserAdmin(ServicesUserAdmin):
|
||||
|
||||
@admin.display(description='Discord Username', ordering='username')
|
||||
def _username(self, obj):
|
||||
if obj.username and obj.discriminator:
|
||||
return f'{obj.username}#{obj.discriminator}'
|
||||
return ''
|
||||
return DiscordService.get_discord_username(
|
||||
username=obj.username, discriminator=obj.discriminator
|
||||
)
|
||||
|
||||
@@ -30,6 +30,29 @@ class DiscordService(ServicesHook):
|
||||
self.access_perm = 'discord.access_discord'
|
||||
self.name_format = '{character_name}'
|
||||
|
||||
@staticmethod
|
||||
def get_discord_username(username:str, discriminator:str) -> str:
|
||||
"""
|
||||
Determine the Discord username (Old and new format)
|
||||
:param username:
|
||||
:type username:
|
||||
:param discriminator:
|
||||
:type discriminator:
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
|
||||
if username and discriminator:
|
||||
discord_username = f'{username}#{discriminator}'
|
||||
|
||||
# New Discord user name format
|
||||
if discriminator == '0':
|
||||
discord_username = f'@{username}'
|
||||
else:
|
||||
discord_username = ''
|
||||
|
||||
return discord_username
|
||||
|
||||
def delete_user(self, user: User, notify_user: bool = False) -> None:
|
||||
if self.user_has_account(user):
|
||||
logger.debug('Deleting user %s %s account', user, self.name)
|
||||
@@ -43,10 +66,19 @@ class DiscordService(ServicesHook):
|
||||
user_has_account = True
|
||||
username = request.user.discord.username
|
||||
discriminator = request.user.discord.discriminator
|
||||
if username and discriminator:
|
||||
discord_username = f'{username}#{discriminator}'
|
||||
else:
|
||||
discord_username = ''
|
||||
|
||||
discord_username = self.get_discord_username(
|
||||
username=username, discriminator=discriminator
|
||||
)
|
||||
|
||||
# if username and discriminator:
|
||||
# discord_username = f'{username}#{discriminator}'
|
||||
#
|
||||
# # New Discord user name format
|
||||
# if discriminator == '0':
|
||||
# discord_username = f'@{username}'
|
||||
# else:
|
||||
# discord_username = ''
|
||||
else:
|
||||
discord_username = ''
|
||||
user_has_account = False
|
||||
|
||||
@@ -588,16 +588,17 @@ class DiscordClient:
|
||||
return None # User is no longer a member
|
||||
guild_roles = RolesSet(self.guild_roles(guild_id=guild_id))
|
||||
logger.debug('Current guild roles: %s', guild_roles.ids())
|
||||
_roles = set(member_info.roles)
|
||||
if not guild_roles.has_roles(member_info.roles):
|
||||
guild_roles = RolesSet(
|
||||
self.guild_roles(guild_id=guild_id, use_cache=False)
|
||||
)
|
||||
if not guild_roles.has_roles(member_info.roles):
|
||||
role_ids = set(member_info.roles).difference(guild_roles.ids())
|
||||
raise RuntimeError(
|
||||
f'Discord user {user_id} has unknown roles: {role_ids}'
|
||||
)
|
||||
return guild_roles.subset(member_info.roles)
|
||||
logger.warning(f'Discord user {user_id} has unknown roles: {role_ids}')
|
||||
for _r in role_ids:
|
||||
_roles.remove(_r)
|
||||
return guild_roles.subset(_roles)
|
||||
|
||||
@classmethod
|
||||
def _is_member_unknown_error(cls, r: requests.Response) -> bool:
|
||||
|
||||
@@ -899,8 +899,8 @@ class TestGuildMemberRoles(NoSocketsTestCase):
|
||||
mock_guild_roles.return_value = {role_a, role_b}
|
||||
client = DiscordClientStub(TEST_BOT_TOKEN, mock_redis)
|
||||
# when/then
|
||||
with self.assertRaises(RuntimeError):
|
||||
client.guild_member_roles(TEST_GUILD_ID, TEST_USER_ID)
|
||||
roles = client.guild_member_roles(TEST_GUILD_ID, TEST_USER_ID)
|
||||
self.assertEqual(roles, RolesSet([role_a]))
|
||||
|
||||
# TODO: Re-enable after adding Discord general error handling
|
||||
# def test_should_raise_exception_if_member_info_is_invalid(
|
||||
|
||||
0
allianceauth/services/modules/discord/tests/piloting_tasks.py
Executable file → Normal file
@@ -150,3 +150,23 @@ class TestDiscordService(NoSocketsTestCase):
|
||||
self.assertTemplateUsed(service.service_ctrl_template)
|
||||
self.assertIn('/discord/reset/', response)
|
||||
self.assertIn('/discord/deactivate/', response)
|
||||
|
||||
def test_new_discord_username_format(self):
|
||||
"""
|
||||
Test if we get Discord's new username format
|
||||
:return:
|
||||
:rtype:
|
||||
"""
|
||||
|
||||
# given
|
||||
username = 'william_riker'
|
||||
discriminator = '0' # Seems to be returned as 0 for Discord's new username format
|
||||
|
||||
# when
|
||||
discord_username = DiscordService.get_discord_username(
|
||||
username=username, discriminator=discriminator
|
||||
)
|
||||
|
||||
# then
|
||||
expected_username = '@william_riker'
|
||||
self.assertEqual(first=discord_username, second=expected_username)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from django.conf.urls import include
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ class DiscourseTasks:
|
||||
DiscourseManager.update_groups(user)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
logger.warn("Discourse group sync failed for %s, retrying in 10 mins" % user)
|
||||
logger.warning("Discourse group sync failed for %s, retrying in 10 mins" % user)
|
||||
raise self.retry(countdown=60 * 10)
|
||||
logger.debug("Updated user %s discourse groups." % user)
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from django.conf.urls import include
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
app_name = 'example'
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@ from django.contrib import admin
|
||||
from .models import Ips4User
|
||||
|
||||
|
||||
@admin.register(Ips4User)
|
||||
class Ips4UserAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'username', 'id')
|
||||
search_fields = ('user__username', 'username', 'id')
|
||||
|
||||
admin.site.register(Ips4User, Ips4UserAdmin)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from django.conf.urls import include
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from django.conf.urls import include
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
0
allianceauth/services/modules/openfire/manager.py
Executable file → Normal file
0
allianceauth/services/modules/openfire/templates/services/openfire/broadcast.html
Executable file → Normal file
@@ -1,5 +1,4 @@
|
||||
from django.conf.urls import include
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
2
allianceauth/services/modules/phpbb3/manager.py
Executable file → Normal file
@@ -176,7 +176,7 @@ class Phpbb3Manager:
|
||||
logger.debug(f"Proceeding to add phpbb user {username_clean} and pwhash starting with {pwhash[0:5]}")
|
||||
# check if the username was simply revoked
|
||||
if Phpbb3Manager.check_user(username_clean):
|
||||
logger.warn("Unable to add phpbb user with username %s - already exists. Updating user instead." % username)
|
||||
logger.warning("Unable to add phpbb user with username %s - already exists. Updating user instead." % username)
|
||||
Phpbb3Manager.__update_user_info(username_clean, email, pwhash)
|
||||
else:
|
||||
try:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from django.conf.urls import include
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from django.conf.urls import include
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
@@ -36,10 +36,12 @@ class AuthTSgroupAdmin(admin.ModelAdmin):
|
||||
kwargs['queryset'] = TSgroup.objects.exclude(ts_group_name__in=ReservedGroupName.objects.values_list('name', flat=True))
|
||||
return super().formfield_for_manytomany(db_field, request, **kwargs)
|
||||
|
||||
@admin.display(
|
||||
description='ts groups'
|
||||
)
|
||||
def _ts_group(self, obj):
|
||||
return [x for x in obj.ts_group.all().order_by('ts_group_id')]
|
||||
|
||||
_ts_group.short_description = 'ts groups'
|
||||
# _ts_group.admin_order_field = 'profile__state'
|
||||
|
||||
|
||||
|
||||
0
allianceauth/services/modules/teamspeak3/manager.py
Executable file → Normal file
@@ -1,5 +1,4 @@
|
||||
from django.conf.urls import include
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
0
allianceauth/services/modules/teamspeak3/util/__init__.py
Executable file → Normal file
0
allianceauth/services/modules/teamspeak3/util/ts3.py
Executable file → Normal file
@@ -44,7 +44,7 @@ def activate_teamspeak3(request):
|
||||
def verify_teamspeak3(request):
|
||||
logger.debug("verify_teamspeak3 called by user %s" % request.user)
|
||||
if not Teamspeak3Tasks.has_account(request.user):
|
||||
logger.warn("Unable to validate user %s teamspeak: no teamspeak data" % request.user)
|
||||
logger.warning("Unable to validate user %s teamspeak: no teamspeak data" % request.user)
|
||||
return redirect("services:services")
|
||||
if request.method == "POST":
|
||||
form = TeamspeakJoinForm(request.POST)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from django.conf.urls import include
|
||||
from django.urls import path
|
||||
from django.urls import include, path
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
0
allianceauth/services/templates/services/services.html
Executable file → Normal file
30
allianceauth/services/tests/test_hooks.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from unittest import TestCase
|
||||
|
||||
from allianceauth.services.hooks import UrlHook
|
||||
from allianceauth.groupmanagement import urls
|
||||
|
||||
|
||||
class TestUrlHook(TestCase):
|
||||
def test_can_create_simple_hook(self):
|
||||
# when
|
||||
obj = UrlHook(urls, "groupmanagement", r"^groupmanagement/")
|
||||
# then
|
||||
self.assertEqual(obj.include_pattern.app_name, "groupmanagement")
|
||||
self.assertFalse(obj.excluded_views)
|
||||
|
||||
def test_can_create_hook_with_excluded_views(self):
|
||||
# when
|
||||
obj = UrlHook(
|
||||
urls,
|
||||
"groupmanagement",
|
||||
r"^groupmanagement/",
|
||||
["groupmanagement.views.group_management"],
|
||||
)
|
||||
# then
|
||||
self.assertEqual(obj.include_pattern.app_name, "groupmanagement")
|
||||
self.assertIn("groupmanagement.views.group_management", obj.excluded_views)
|
||||
|
||||
def test_should_raise_error_when_called_with_invalid_excluded_views(self):
|
||||
# when/then
|
||||
with self.assertRaises(TypeError):
|
||||
UrlHook(urls, "groupmanagement", r"^groupmanagement/", 99)
|
||||
@@ -1,4 +1,4 @@
|
||||
from django.conf.urls import include
|
||||
from django.urls import include
|
||||
from allianceauth.hooks import get_hooks
|
||||
from django.urls import path
|
||||
|
||||
|
||||
0
allianceauth/services/views.py
Executable file → Normal file
0
allianceauth/srp/__init__.py
Executable file → Normal file
0
allianceauth/srp/admin.py
Executable file → Normal file
0
allianceauth/srp/form.py
Executable file → Normal file
0
allianceauth/srp/models.py
Executable file → Normal file
0
allianceauth/srp/tests/__init__.py
Executable file → Normal file
0
allianceauth/srp/tests/test_managers.py
Executable file → Normal file
0
allianceauth/srp/views.py
Executable file → Normal file
@@ -122,7 +122,7 @@ ul.list-group.list-group-horizontal > li.list-group-item {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.navbar-nav > li.top-user-menu.with-main-character a {
|
||||
.navbar-nav > li.top-user-menu a {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 26 KiB |
BIN
allianceauth/static/allianceauth/icons/apple-touch-icon.png
Executable file → Normal file
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 7.0 KiB |
9
allianceauth/static/allianceauth/icons/browserconfig.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/static/allianceauth/icons/mstile-150x150.png"/>
|
||||
<TileColor>#2d89ef</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
BIN
allianceauth/static/allianceauth/icons/favicon-16x16.png
Executable file → Normal file
|
Before Width: | Height: | Size: 483 B After Width: | Height: | Size: 941 B |
BIN
allianceauth/static/allianceauth/icons/favicon-32x32.png
Executable file → Normal file
|
Before Width: | Height: | Size: 868 B After Width: | Height: | Size: 1.5 KiB |
BIN
allianceauth/static/allianceauth/icons/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
allianceauth/static/allianceauth/icons/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
41
allianceauth/static/allianceauth/icons/safari-pinned-tab.svg
Normal file
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="575.000000pt" height="575.000000pt" viewBox="0 0 575.000000 575.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,575.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M2765 5731 c-58 -21 -122 -62 -143 -92 -8 -11 -32 -62 -54 -112 -21
|
||||
-51 -45 -105 -53 -122 -7 -16 -47 -109 -88 -205 -112 -261 -375 -871 -415
|
||||
-960 -11 -25 -61 -142 -112 -260 -50 -118 -102 -237 -115 -265 -12 -27 -71
|
||||
-162 -130 -300 -217 -505 -297 -691 -310 -720 -8 -16 -145 -334 -305 -705
|
||||
-160 -371 -298 -691 -307 -710 -28 -63 -40 -91 -168 -390 -70 -162 -133 -308
|
||||
-140 -325 -8 -16 -25 -55 -38 -85 -13 -30 -35 -80 -47 -110 -12 -30 -31 -73
|
||||
-41 -95 -16 -35 -36 -81 -106 -244 l-14 -33 33 20 c18 11 103 60 188 110 85
|
||||
49 171 99 190 111 19 11 55 32 80 46 25 14 70 40 101 58 208 121 326 189 339
|
||||
198 8 5 94 55 190 110 96 56 191 111 210 122 19 11 46 27 60 34 14 8 46 26 71
|
||||
41 69 41 557 326 577 337 9 6 68 39 130 75 62 36 125 71 140 79 15 8 29 18 30
|
||||
23 2 4 8 8 13 8 4 0 46 22 91 49 46 27 99 58 118 68 19 11 172 100 340 198
|
||||
168 98 320 186 338 197 18 10 103 59 187 108 84 49 169 98 187 108 18 11 56
|
||||
33 84 49 27 17 70 41 95 55 24 14 123 72 219 128 96 56 195 114 220 128 25 13
|
||||
50 29 57 36 9 8 -16 74 -103 276 -63 146 -121 279 -128 295 -8 17 -28 64 -46
|
||||
105 -34 80 -55 127 -75 175 -71 163 -148 341 -355 820 -133 308 -250 578 -260
|
||||
600 -10 22 -44 101 -75 175 -32 74 -66 153 -76 175 -10 22 -65 148 -121 281
|
||||
-113 263 -126 282 -224 326 -67 29 -173 34 -239 9z"/>
|
||||
<path d="M4500 2370 c-14 -11 -28 -20 -32 -20 -6 0 -107 -58 -158 -90 -8 -5
|
||||
-28 -17 -45 -26 -16 -9 -168 -97 -337 -195 -169 -99 -322 -187 -340 -198 -18
|
||||
-10 -58 -33 -88 -51 -30 -18 -71 -42 -90 -53 -44 -25 -414 -239 -418 -242 -2
|
||||
-2 16 -14 40 -27 24 -14 153 -89 288 -168 135 -78 259 -150 275 -160 17 -9
|
||||
116 -67 220 -127 105 -61 206 -120 225 -130 19 -11 86 -50 148 -87 62 -36 132
|
||||
-77 155 -90 23 -13 98 -56 167 -96 68 -40 138 -81 155 -90 16 -9 64 -37 105
|
||||
-62 41 -24 82 -47 90 -51 8 -4 29 -15 45 -26 151 -92 639 -370 643 -366 3 3
|
||||
-1 17 -8 32 -7 16 -56 129 -110 253 -93 216 -131 302 -150 345 -5 11 -62 142
|
||||
-126 290 -64 149 -124 288 -134 310 -10 22 -39 90 -64 150 -26 61 -54 124 -61
|
||||
141 -8 16 -30 68 -50 114 -78 183 -84 197 -124 283 -17 37 -31 74 -31 80 0 7
|
||||
-4 17 -9 22 -4 6 -41 87 -81 180 -40 94 -73 171 -74 172 -1 1 -12 -7 -26 -17z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
19
allianceauth/static/allianceauth/icons/site.webmanifest
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/allianceauth/icons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/allianceauth/icons/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
30
allianceauth/static/allianceauth/js/timerboard.js
Normal file
@@ -0,0 +1,30 @@
|
||||
$(document).ready(() => {
|
||||
'use strict';
|
||||
const inputAbsoluteTime = $('input#id_absolute_time');
|
||||
const inputCountdown = $('#id_days_left, #id_hours_left, #id_minutes_left');
|
||||
|
||||
//inputAbsoluteTime.prop('disabled', true);
|
||||
inputAbsoluteTime.parent().hide()
|
||||
inputAbsoluteTime.parent().prev('label').hide()
|
||||
inputCountdown.prop('required', true);
|
||||
|
||||
$('input#id_absolute_checkbox').change(function () {
|
||||
if ($(this).prop("checked")) {
|
||||
// check box enabled
|
||||
inputAbsoluteTime.parent().show()
|
||||
inputAbsoluteTime.parent().prev('label').show()
|
||||
inputCountdown.parent().hide()
|
||||
inputCountdown.parent().prev('label').hide()
|
||||
inputAbsoluteTime.prop('required', true);
|
||||
inputCountdown.prop('required', false);
|
||||
} else {
|
||||
// Checkbox is not checked
|
||||
inputAbsoluteTime.parent().hide()
|
||||
inputAbsoluteTime.parent().prev('label').hide()
|
||||
inputCountdown.parent().show()
|
||||
inputCountdown.parent().prev('label').show()
|
||||
inputAbsoluteTime.prop('required', false);
|
||||
inputCountdown.prop('required', true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -92,9 +92,8 @@
|
||||
{% include "allianceauth/admin-status/celery_bar_partial.html" with label="failed" level="danger" tasks_count=tasks_failed %}
|
||||
</div>
|
||||
<p>
|
||||
{% blocktranslate with queue_length=task_queue_length|default_if_none:"?"|intcomma %}
|
||||
{{ queue_length }} queued tasks
|
||||
{% endblocktranslate %}
|
||||
<span id="task-counts">?</span> {% translate 'running' %} |
|
||||
<span id="queued-tasks-count">?</span> {% translate 'queued' %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,3 +101,36 @@
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const elemRunning = document.getElementById("task-counts");
|
||||
const elemQueued = document.getElementById("queued-tasks-count");
|
||||
|
||||
fetch('{% url "authentication:task_counts" %}')
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
throw new Error("Something went wrong");
|
||||
})
|
||||
.then((responseJson) => {
|
||||
const running = responseJson.tasks_running;
|
||||
if (running == null) {
|
||||
elemRunning.textContent = "N/A";
|
||||
} else {
|
||||
elemRunning.textContent = running.toLocaleString();
|
||||
}
|
||||
|
||||
const queued = responseJson.tasks_queued;
|
||||
if (queued == null) {
|
||||
elemQueued.textContent = "N/A";
|
||||
} else {
|
||||
elemQueued.textContent = queued.toLocaleString();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
elemRunning.textContent = "ERROR";
|
||||
elemQueued.textContent = "ERROR";
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% load navactive %}
|
||||
{% load auth_notifications %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="{{ LANGUAGE_CODE }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
@@ -21,33 +21,37 @@
|
||||
</head>
|
||||
|
||||
<body class="{% if NIGHT_MODE %}template-dark-mode{% else %}template-light-mode{% endif %}">
|
||||
{% if user.is_authenticated %}
|
||||
<div id="wrapper" class="container">
|
||||
<!-- Navigation -->
|
||||
{% include 'allianceauth/top-menu.html' %}
|
||||
<div class="row" id="site-body-wrapper">
|
||||
<div id="wrapper" class="container">
|
||||
<!-- Navigation -->
|
||||
{% include 'allianceauth/top-menu.html' %}
|
||||
|
||||
<div class="clearfix{% if user.is_authenticated %} row{% endif %}" id="site-body-wrapper">
|
||||
{% if user.is_authenticated %}
|
||||
{% include 'allianceauth/side-menu.html' %}
|
||||
<div class="col-sm-10">
|
||||
{% include 'allianceauth/messages.html' %}
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
{% endif %}
|
||||
|
||||
<div class="{% if user.is_authenticated %}col-sm-10{% else %}col-sm-12{% endif %}">
|
||||
{% include 'allianceauth/messages.html' %}
|
||||
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% include 'bundles/bootstrap-js.html' %}
|
||||
{% include 'bundles/jquery-visibility-js.html' %}
|
||||
|
||||
<script>
|
||||
let notificationUPdateSettings = {
|
||||
notificationsListViewUrl: "{% url 'notifications:list' %}",
|
||||
notificationsRefreshTime: "{% notifications_refresh_time %}",
|
||||
userNotificationsCountViewUrl: "{% url 'notifications:user_notifications_count' request.user.pk %}"
|
||||
};
|
||||
</script>
|
||||
{% include 'bundles/refresh-notifications-js.html' %}
|
||||
{% if user.is_authenticated %}
|
||||
<script>
|
||||
let notificationUPdateSettings = {
|
||||
notificationsListViewUrl: "{% url 'notifications:list' %}",
|
||||
notificationsRefreshTime: "{% notifications_refresh_time %}",
|
||||
userNotificationsCountViewUrl: "{% url 'notifications:user_notifications_count' request.user.pk %}"
|
||||
};
|
||||
</script>
|
||||
{% include 'bundles/refresh-notifications-js.html' %}
|
||||
{% endif %}
|
||||
{% include 'bundles/evetime-js.html' %}
|
||||
|
||||
{% block extra_javascript %}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
{% load static %}
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{% static 'allianceauth/icons/apple-touch-icon.png' %}">
|
||||
<link rel="icon" type="image/png" href="{% static 'allianceauth/icons/favicon-16x16.png' %}" sizes="16x16">
|
||||
<link rel="icon" type="image/png" href="{% static 'allianceauth/icons/favicon-32x32.png' %}" sizes="32x32">
|
||||
<link rel="icon" type="image/png" href="{% static 'allianceauth/icons/favicon-96x96.png' %}" sizes="96x96">
|
||||
<link rel="apple-touch-icon" href="{% static 'allianceauth/icons/apple-touch-icon.png' %}">
|
||||
<link rel="manifest" href="{% static 'allianceauth/icons/site.webmanifest' %}">
|
||||
<link rel="mask-icon" href="{% static 'allianceauth/icons/safari-pinned-tab.svg' %}" color="#5bbad5">
|
||||
<link rel="shortcut icon" type="image/png" href="{% static 'allianceauth/icons/favicon.ico' %}">
|
||||
<meta name="msapplication-TileColor" content="#2d89ef">
|
||||
<meta name="msapplication-config" content="{% static 'allianceauth/icons/browserconfig.xml' %}">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
@@ -11,7 +11,10 @@
|
||||
</span>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% translate "User Menu" %}
|
||||
<img class="img-rounded ra-avatar" src="{{ 1|character_portrait_url:32 }}" alt="{{ main.character_name }}">
|
||||
<span class="hidden-sm hidden-md hidden-lg">
|
||||
{% translate "User Menu" %}
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="caret"></span>
|
||||
</a>
|
||||
|
||||