Compare commits

...

25 Commits

Author SHA1 Message Date
Ariel Rin
12383d79c8 Version Bump 3.6.0 2023-08-01 21:37:25 +10:00
Ariel Rin
56e2875650 Merge branch 'pre-commit-update' into 'master'
`pre-commit` Update and File Permission Fixes (redone)

See merge request allianceauth/allianceauth!1523
2023-08-01 11:22:37 +00:00
Peter Pfeufer
d0118e6c0b [FIX] File permissions 2023-08-01 12:55:19 +02:00
Peter Pfeufer
7075ccdf7a [CHANGE] Django Upgrade checks applied 2023-08-01 12:52:05 +02:00
Ariel Rin
b2d540c010 Merge branch 'add-public-routes-feature' into 'master'
Add public routes feature

See merge request allianceauth/allianceauth!1514
2023-08-01 10:20:14 +00:00
Erik Kalkoken
7cb7e2c77b Add public routes feature 2023-08-01 10:20:13 +00:00
Ariel Rin
5d6a4ab1a9 Merge branch 'feature-show-running-tasks' into 'master'
Show running tasks on dashboard

See merge request allianceauth/allianceauth!1515
2023-08-01 10:15:43 +00:00
Erik Kalkoken
1122d617bd Show running tasks on dashboard 2023-08-01 10:15:42 +00:00
Ariel Rin
ef33501e45 Merge branch 'proper-favicon-support' into 'master'
Proper favicon support

See merge request allianceauth/allianceauth!1520
2023-08-01 10:14:21 +00:00
Ariel Rin
08fd86db8f Merge branch 'migrate-pep-621' into 'master'
Migrate to PEP 621

See merge request allianceauth/allianceauth!1513
2023-07-25 09:32:29 +00:00
Erik Kalkoken
c4193c15fc Migrate to PEP 621 2023-07-25 09:32:28 +00:00
Ariel Rin
903074080e Merge branch 'unknown_discord_group_patch' into 'master'
Discord: Don't fail on unknown groups, simply remove them.

See merge request allianceauth/allianceauth!1504
2023-07-25 09:27:19 +00:00
Ariel Rin
3046a26a02 Merge branch 'AA-Timer-Absolute' into 'master'
Adding Absolute Timers to base timerboard

See merge request allianceauth/allianceauth!1518
2023-07-25 09:26:10 +00:00
Hamish W
951c4135c2 Adding Absolute Timers to base timerboard 2023-07-25 09:26:10 +00:00
Ariel Rin
b256a0c5e1 Merge branch 'next-params' into 'master'
Encode Next Param for Login Redirection

See merge request allianceauth/allianceauth!1519
2023-07-25 09:02:31 +00:00
Ariel Rin
212b9b0f60 Merge branch 'stringify-crontab-arguments' into 'master'
[FIX] `crontab` arguments are of type `string`, not `int`

See merge request allianceauth/allianceauth!1517
2023-07-25 08:55:25 +00:00
Peter Pfeufer
fc29d7e80d [ADD] All modern favicon versions generated by realfavicongenerator.net 2023-07-19 12:10:02 +02:00
Peter Pfeufer
ec536c66a0 [ADD] Favicon redirect to Nginx docs 2023-07-19 11:40:45 +02:00
Peter Pfeufer
749ece45e2 [ADD] Favicon redirect to Apache2 docs 2023-07-19 11:39:04 +02:00
Peter Pfeufer
b04c8873d0 [ADD] Directive for default favicon 2023-07-19 11:37:07 +02:00
Aaron Kable
9a77175bf3 Allow get params from next at login 2023-07-17 10:25:36 +08:00
Peter Pfeufer
5d4c7b9030 [FIX] crontab arguments here as well 2023-07-14 19:22:29 +02:00
Aaron Kable
5f80259d57 fix test 2023-07-13 20:28:35 +08:00
Peter Pfeufer
dcd6bd1b36 [FIX] crontab arguments are of type string, not int 2023-07-12 13:11:18 +02:00
Aaron Kable
1cae20fe5f Dont fail on unknown groups, simply remove them. 2023-05-19 05:00:39 +08:00
98 changed files with 830 additions and 270 deletions

View File

@@ -1,6 +0,0 @@
[settings]
profile=django
sections=FUTURE,STDLIB,THIRDPARTY,DJANGO,ESI,FIRSTPARTY,LOCALFOLDER
known_esi=esi
known_django=django
skip_gitignore=true

View File

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

View File

@@ -1,7 +0,0 @@
include LICENSE
include README.md
include MANIFEST.in
graft allianceauth
global-exclude __pycache__
global-exclude *.py[co]

0
README.md Executable file → Normal file
View File

View File

@@ -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.5.1'
__version__ = '3.6.0'
__title__ = 'Alliance Auth'
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
NAME = f'{__title__} v{__version__}'

View File

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

View File

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

0
allianceauth/authentication/models.py Executable file → Normal file
View File

View File

@@ -1,35 +1,44 @@
from collections import namedtuple
"""Counters for Task Statistics."""
import datetime as dt
from typing import NamedTuple, Optional
from .event_series import EventSeries
from .helpers import ItemCounter
"""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")
running_tasks = ItemCounter("running_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
running: 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)
earliest_events += earliest_if_exists(retried_tasks, earliest)
failed_count = failed_tasks.count(earliest=earliest)
earliest_events += earliest_if_exists(failed_tasks, earliest)
running_count = running_tasks.value()
return _TaskCounts(
succeeded=succeeded_count,
retried=retried_count,
@@ -37,4 +46,5 @@ def dashboard_results(hours: int) -> _TaskCounts:
total=succeeded_count + retried_count + failed_count,
earliest_task=min(earliest_events) if earliest_events else None,
hours=hours,
running=running_count,
)

View File

@@ -1,3 +1,5 @@
"""Event series for Task Statistics."""
import datetime as dt
import logging
from typing import List, Optional
@@ -73,8 +75,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."""
@@ -101,9 +103,9 @@ class EventSeries:
- 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 +113,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,

View File

@@ -0,0 +1,44 @@
"""Helpers for Task Statistics."""
from typing import Optional
from django.core.cache import cache
class ItemCounter:
"""A process safe item counter."""
CACHE_KEY_BASE = "allianceauth-item-counter"
DEFAULT_CACHE_TIMEOUT = 24 * 3600
def __init__(self, name: str) -> None:
if not name:
raise ValueError("Must define a name")
self._name = str(name)
@property
def _cache_key(self) -> str:
return f"{self.CACHE_KEY_BASE}-{self._name}"
def reset(self, init_value: int = 0):
"""Reset counter to initial value."""
cache.set(self._cache_key, init_value, self.DEFAULT_CACHE_TIMEOUT)
def incr(self, delta: int = 1):
"""Increment counter by delta."""
try:
cache.incr(self._cache_key, delta)
except ValueError:
pass
def decr(self, delta: int = 1):
"""Decrement counter by delta."""
try:
cache.decr(self._cache_key, delta)
except ValueError:
pass
def value(self) -> Optional[int]:
"""Return current value or None if not yet initialized."""
return cache.get(self._cache_key)

View File

@@ -1,14 +1,15 @@
"""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_postrun, task_prerun, task_retry,
task_success, worker_ready,
)
from django.conf import settings
from .counters import failed_tasks, retried_tasks, succeeded_tasks
from .counters import (
failed_tasks, retried_tasks, running_tasks, succeeded_tasks,
)
def reset_counters():
@@ -16,9 +17,11 @@ def reset_counters():
succeeded_tasks.clear()
failed_tasks.clear()
retried_tasks.clear()
running_tasks.reset()
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)
)
@@ -52,3 +55,15 @@ def record_task_failed(*args, **kwargs):
def record_task_internal_error(*args, **kwargs):
if is_enabled():
failed_tasks.add()
@task_prerun.connect
def record_task_prerun(*args, **kwargs):
if is_enabled():
running_tasks.incr()
@task_postrun.connect
def record_task_postrun(*args, **kwargs):
if is_enabled():
running_tasks.decr()

View File

@@ -8,25 +8,31 @@ from allianceauth.authentication.task_statistics.counters import (
succeeded_tasks,
retried_tasks,
failed_tasks,
running_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()
running_tasks.reset(8)
# when
results = dashboard_results(hours=1)
# then
@@ -35,12 +41,14 @@ class TestDashboardResults(TestCase):
self.assertEqual(results.failed, 1)
self.assertEqual(results.total, 6)
self.assertEqual(results.earliest_task, earliest_task)
self.assertEqual(results.running, 8)
def test_should_work_with_no_data(self):
# given
succeeded_tasks.clear()
retried_tasks.clear()
failed_tasks.clear()
running_tasks.reset()
# when
results = dashboard_results(hours=1)
# then
@@ -49,3 +57,4 @@ class TestDashboardResults(TestCase):
self.assertEqual(results.failed, 0)
self.assertEqual(results.total, 0)
self.assertIsNone(results.earliest_task)
self.assertEqual(results.running, 0)

View File

@@ -0,0 +1,74 @@
from unittest import TestCase
from allianceauth.authentication.task_statistics.helpers import ItemCounter
COUNTER_NAME = "test-counter"
class TestItemCounter(TestCase):
def test_can_create_counter(self):
# when
counter = ItemCounter(COUNTER_NAME)
# then
self.assertIsInstance(counter, ItemCounter)
def test_can_reset_counter_to_default(self):
# given
counter = ItemCounter(COUNTER_NAME)
# when
counter.reset()
# then
self.assertEqual(counter.value(), 0)
def test_can_reset_counter_to_custom_value(self):
# given
counter = ItemCounter(COUNTER_NAME)
# when
counter.reset(42)
# then
self.assertEqual(counter.value(), 42)
def test_can_increment_counter_by_default(self):
# given
counter = ItemCounter(COUNTER_NAME)
counter.reset(0)
# when
counter.incr()
# then
self.assertEqual(counter.value(), 1)
def test_can_increment_counter_by_custom_value(self):
# given
counter = ItemCounter(COUNTER_NAME)
counter.reset(0)
# when
counter.incr(8)
# then
self.assertEqual(counter.value(), 8)
def test_can_decrement_counter_by_default(self):
# given
counter = ItemCounter(COUNTER_NAME)
counter.reset(9)
# when
counter.decr()
# then
self.assertEqual(counter.value(), 8)
def test_can_decrement_counter_by_custom_value(self):
# given
counter = ItemCounter(COUNTER_NAME)
counter.reset(9)
# when
counter.decr(8)
# then
self.assertEqual(counter.value(), 1)
def test_can_decrement_counter_below_zero(self):
# given
counter = ItemCounter(COUNTER_NAME)
counter.reset(0)
# when
counter.decr(1)
# then
self.assertEqual(counter.value(), -1)

View File

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

View File

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

View File

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

View File

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

0
allianceauth/groupmanagement/views.py Executable file → Normal file
View File

2
allianceauth/hrapplications/admin.py Executable file → Normal file
View 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
View File

0
allianceauth/hrapplications/models.py Executable file → Normal file
View File

0
allianceauth/hrapplications/views.py Executable file → Normal file
View File

View File

@@ -1,3 +1 @@
from .core import notify # noqa: F401
default_app_config = 'allianceauth.notifications.apps.NotificationsConfig'

View File

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

View File

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

View File

@@ -32,6 +32,10 @@ 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.
# If you want to allow specific apps to have public views
# you can put there names here (same name as in INSTALLED_APPS):
APPS_WITH_PUBLIC_VIEWS = []
# Enter credentials to use MySQL/MariaDB. Comment out to use sqlite3
DATABASES['default'] = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

View 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

View File

@@ -1,5 +1,4 @@
from django.conf.urls import include
from django.urls import path
from django.urls import include, path
app_name = 'example'

View File

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

View 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

View 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/openfire/manager.py Executable file → Normal file
View File

View 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/phpbb3/manager.py Executable file → Normal file
View File

View 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

View 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

View File

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

View 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

View File

0
allianceauth/services/modules/teamspeak3/util/ts3.py Executable file → Normal file
View File

View 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/templates/services/services.html Executable file → Normal file
View File

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

View File

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

0
allianceauth/srp/__init__.py Executable file → Normal file
View File

0
allianceauth/srp/admin.py Executable file → Normal file
View File

0
allianceauth/srp/form.py Executable file → Normal file
View File

0
allianceauth/srp/models.py Executable file → Normal file
View File

0
allianceauth/srp/tests/__init__.py Executable file → Normal file
View File

0
allianceauth/srp/tests/test_managers.py Executable file → Normal file
View File

0
allianceauth/srp/views.py Executable file → Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

View 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 483 B

After

Width:  |  Height:  |  Size: 941 B

BIN
allianceauth/static/allianceauth/icons/favicon-32x32.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 868 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View 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

View 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"
}

View 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);
}
});
});

View File

@@ -92,8 +92,11 @@
{% include "allianceauth/admin-status/celery_bar_partial.html" with label="failed" level="danger" tasks_count=tasks_failed %}
</div>
<p>
{% blocktranslate with running_count=tasks_running|default_if_none:"?"|intcomma %}
{{ running_count }} running |
{% endblocktranslate %}
{% blocktranslate with queue_length=task_queue_length|default_if_none:"?"|intcomma %}
{{ queue_length }} queued tasks
{{ queue_length }} queued
{% endblocktranslate %}
</p>
</div>

View File

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

View File

@@ -0,0 +1,3 @@
{% load static %}
<script src="{% static 'allianceauth/js/timerboard.js' %}"></script>

View File

@@ -40,7 +40,7 @@ def decimal_widthratio(this_value, max_value, max_width) -> str:
if max_value == 0:
return str(0)
return str(round(this_value/max_value * max_width, 2))
return str(round(this_value / max_value * max_width, 2))
@register.inclusion_tag('allianceauth/admin-status/overview.html')
@@ -54,7 +54,8 @@ def status_overview() -> dict:
"tasks_failed": 0,
"tasks_total": 0,
"tasks_hours": 0,
"earliest_task": None
"earliest_task": None,
"tasks_running": 0
}
response.update(_current_notifications())
response.update(_current_version_summary())
@@ -72,7 +73,8 @@ def _celery_stats() -> dict:
"tasks_failed": results.failed,
"tasks_total": results.total,
"tasks_hours": results.hours,
"earliest_task": results.earliest_task
"earliest_task": results.earliest_task,
"tasks_running": results.running,
}

View File

@@ -0,0 +1,72 @@
from unittest import TestCase
from unittest.mock import patch, MagicMock
from django.urls import URLPattern
from allianceauth.services.hooks import UrlHook
from allianceauth.urls import urls_from_apps
MODULE_PATH = "allianceauth.urls"
@patch(MODULE_PATH + ".main_character_required")
@patch(MODULE_PATH + ".decorate_url_patterns")
class TestUrlsFromApps(TestCase):
def test_should_decorate_url_by_default(self, mock_decorate_url_patterns, mock_main_character_required):
# given
def hook_function():
return UrlHook(urlconf_module, "my_namespace", r"^my_app/")
view = MagicMock(name="view")
path = MagicMock(spec=URLPattern, name="path")
path.callback = view
urlconf_module = [patch], "my_app"
# when
result = urls_from_apps([hook_function], [])
# then
self.assertIsInstance(result[0], URLPattern)
self.assertTrue(mock_decorate_url_patterns.called)
args, _ = mock_decorate_url_patterns.call_args
decorator = args[1]
self.assertEqual(decorator, mock_main_character_required)
excluded_views = args[2]
self.assertIsNone(excluded_views)
def test_should_not_decorate_when_excluded(self, mock_decorate_url_patterns, mock_main_character_required):
# given
def hook_function():
return UrlHook(urlconf_module, "my_namespace", r"^my_app/", ["excluded_view"])
view = MagicMock(name="view")
path = MagicMock(spec=URLPattern, name="path")
path.callback = view
urlconf_module = [patch], "my_app"
# when
result = urls_from_apps([hook_function], ["my_app"])
# then
self.assertIsInstance(result[0], URLPattern)
self.assertTrue(mock_decorate_url_patterns.called)
args, _ = mock_decorate_url_patterns.call_args
decorator = args[1]
self.assertEqual(decorator, mock_main_character_required)
excluded_views = args[2]
self.assertSetEqual(excluded_views, {"excluded_view"})
def test_should_decorate_when_app_has_no_permission(self, mock_decorate_url_patterns, mock_main_character_required):
# given
def hook_function():
return UrlHook(urlconf_module, "my_namespace", r"^my_app/", ["excluded_view"])
view = MagicMock(name="view")
path = MagicMock(spec=URLPattern, name="path")
path.callback = view
urlconf_module = [patch], "my_app"
# when
result = urls_from_apps([hook_function], ["other_app"])
# then
self.assertIsInstance(result[0], URLPattern)
self.assertTrue(mock_decorate_url_patterns.called)
args, _ = mock_decorate_url_patterns.call_args
decorator = args[1]
self.assertEqual(decorator, mock_main_character_required)
excluded_views = args[2]
self.assertIsNone(excluded_views)

0
allianceauth/timerboard/__init__.py Executable file → Normal file
View File

0
allianceauth/timerboard/admin.py Executable file → Normal file
View File

35
allianceauth/timerboard/form.py Executable file → Normal file
View File

@@ -61,14 +61,17 @@ class TimerForm(forms.ModelForm):
structure = forms.ChoiceField(choices=structure_choices, required=True, label=_("Structure Type"))
timer_type = forms.ChoiceField(choices=TimerType.choices, label=_("Timer Type"))
objective = forms.ChoiceField(choices=objective_choices, required=True, label=_("Objective"))
days_left = forms.IntegerField(required=True, label=_("Days Remaining"), validators=[MinValueValidator(0)])
hours_left = forms.IntegerField(required=True, label=_("Hours Remaining"),
absolute_checkbox = forms.BooleanField(label=_("Absolute Timer"), required=False, initial=False)
absolute_time = forms.CharField(required=False,label=_("Date and Time"))
days_left = forms.IntegerField(required=False, label=_("Days Remaining"), validators=[MinValueValidator(0)])
hours_left = forms.IntegerField(required=False, label=_("Hours Remaining"),
validators=[MinValueValidator(0), MaxValueValidator(23)])
minutes_left = forms.IntegerField(required=True, label=_("Minutes Remaining"),
minutes_left = forms.IntegerField(required=False, label=_("Minutes Remaining"),
validators=[MinValueValidator(0), MaxValueValidator(59)])
important = forms.BooleanField(label=_("Important"), required=False)
corp_timer = forms.BooleanField(label=_("Corp-Restricted"), required=False)
def save(self, commit=True):
timer = super().save(commit=False)
@@ -77,18 +80,30 @@ class TimerForm(forms.ModelForm):
corporation = character.corporation
logger.debug("Determined timer save request on behalf "
"of character {} corporation {}".format(character, corporation))
# calculate future time
future_time = datetime.timedelta(days=self.cleaned_data['days_left'], hours=self.cleaned_data['hours_left'],
minutes=self.cleaned_data['minutes_left'])
current_time = timezone.now()
eve_time = current_time + future_time
logger.debug(
f"Determined timer eve time is {eve_time} - current time {current_time}, adding {future_time}")
days_left = self.cleaned_data['days_left']
hours_left = self.cleaned_data['hours_left']
minutes_left = self.cleaned_data['minutes_left']
absolute_time = self.cleaned_data['absolute_time']
if days_left or hours_left or minutes_left:
# Calculate future time
future_time = datetime.timedelta(days=days_left, hours=hours_left, minutes=minutes_left)
current_time = timezone.now()
eve_time = current_time + future_time
logger.debug(f"Determined timer eve time is {eve_time} - current time {current_time}, adding {future_time}")
elif absolute_time:
# Use absolute time
eve_time = absolute_time
else:
raise ValueError("Either future time or absolute time must be provided.")
timer.eve_time = eve_time
timer.eve_character = character
timer.eve_corp = corporation
timer.user = self.user
if commit:
timer.save()
return timer

0
allianceauth/timerboard/models.py Executable file → Normal file
View File

View File

@@ -30,3 +30,21 @@
</div>
{% endblock content %}
{% block extra_javascript %}
{% include 'bundles/jquery-datetimepicker-js.html' %}
{% endblock %}
{% block extra_script %}
$('#id_start').datetimepicker({
setlocale: '{{ LANGUAGE_CODE }}',
{% if NIGHT_MODE %}
theme: 'dark',
{% else %}
theme: 'default',
{% endif %}
mask: true,
format: 'Y-m-d H:i',
minDate: 0
});
{% endblock extra_script %}

View File

@@ -12,3 +12,19 @@
{% block submit_button_text %}
{% translate "Create Timer" %}
{% endblock %}
{% block extra_javascript %}
{% include 'bundles/timerboard-js.html' %}
{% include 'bundles/jquery-datetimepicker-js.html' %}
{% include 'bundles/jquery-datetimepicker-css.html' %}
{% endblock %}
{% block extra_script %}
$('input#id_absolute_time').datetimepicker({
setlocale: '{{ LANGUAGE_CODE }}',
theme: {% if NIGHT_MODE %}'dark'{% else %}'default'{% endif %},
format: 'Y-m-d H:i',
minDate: 0,
defaultDate: null
});
{% endblock extra_script %}

View File

@@ -12,3 +12,23 @@
{% block submit_button_text %}
{% translate "Update Structure Timer" %}
{% endblock %}
{% block extra_javascript %}
{% include 'bundles/timerboard-js.html' %}
{% include 'bundles/jquery-datetimepicker-js.html' %}
{% include 'bundles/jquery-datetimepicker-css.html' %}
{% endblock %}
{% block extra_script %}
$('input#id_absolute_time').datetimepicker({
setlocale: '{{ LANGUAGE_CODE }}',
{% if NIGHT_MODE %}
theme: 'dark',
{% else %}
theme: 'default',
{% endif %}
mask: true,
format: 'Y-m-d H:i',
defaultDate: null
});
{% endblock extra_script %}

0
allianceauth/timerboard/views.py Executable file → Normal file
View File

54
allianceauth/urls.py Executable file → Normal file
View File

@@ -1,24 +1,54 @@
from django.urls import path
import esi.urls
from typing import List, Iterable, Callable
from django.conf.urls import include
import esi.urls
from django.conf import settings
from django.contrib import admin
from django.urls import URLPattern, include, path
from django.views.generic.base import TemplateView
import allianceauth.authentication.views
import allianceauth.authentication.urls
import allianceauth.notifications.urls
import allianceauth.authentication.views
import allianceauth.groupmanagement.urls
import allianceauth.notifications.urls
import allianceauth.services.urls
from allianceauth.authentication.decorators import main_character_required, decorate_url_patterns
from allianceauth import NAME
from allianceauth import views
from allianceauth import NAME, views
from allianceauth.authentication import hmac_urls
from allianceauth.authentication.decorators import (
decorate_url_patterns,
main_character_required
)
from allianceauth.hooks import get_hooks
admin.site.site_header = NAME
def urls_from_apps(
apps_hook_functions: Iterable[Callable], public_views_allowed: List[str]
) -> List[URLPattern]:
"""Return urls from apps and add default decorators."""
url_patterns = []
allowed_apps = set(public_views_allowed)
for app_hook_function in apps_hook_functions:
url_hook = app_hook_function()
app_pattern = url_hook.include_pattern
excluded_views = (
url_hook.excluded_views
if app_pattern.app_name in allowed_apps
else None
)
url_patterns += [
path(
"",
decorate_url_patterns(
[app_pattern], main_character_required, excluded_views
)
)
]
return url_patterns
# Functional/Untranslated URL's
urlpatterns = [
# Locale
@@ -49,8 +79,6 @@ urlpatterns = [
path('night/', views.NightModeRedirectView.as_view(), name='nightmode')
]
# Append app urls
app_urls = get_hooks('url_hook')
for app in app_urls:
urlpatterns += [path('', decorate_url_patterns([app().include_pattern], main_character_required))]
url_hooks = get_hooks("url_hook")
public_views_allows = getattr(settings, "APPS_WITH_PUBLIC_VIEWS", [])
urlpatterns += urls_from_apps(url_hooks, public_views_allows)

View File

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

View File

@@ -1,5 +1,5 @@
FROM python:3.9-slim
ARG AUTH_VERSION=v3.5.1
ARG AUTH_VERSION=v3.6.0
ARG AUTH_PACKAGE=allianceauth==${AUTH_VERSION}
ENV VIRTUAL_ENV=/opt/venv
ENV AUTH_USER=allianceauth

View File

@@ -1,54 +1,65 @@
# URL Hooks
```eval_rst
.. note::
URLs added through URL Hooks are protected by a decorator which ensures the requesting user is logged in and has a main character set.
```
## Base functionality
The URL hooks allow you to dynamically specify URL patterns from your plugin app or service. To achieve this you should subclass or instantiate the `services.hooks.UrlHook` class and then register the URL patterns with the hook.
To register a UrlHook class you would do the following:
@hooks.register('url_hook')
def register_urls():
return UrlHook(app_name.urls, 'app_name', r^'app_name/')
```python
@hooks.register('url_hook')
def register_urls():
return UrlHook(app_name.urls, 'app_name', r^'app_name/')
```
### Public views
The `UrlHook` class specifies some parameters/instance variables required for URL pattern inclusion.
In addition is it possible to make views public. Normally, all views are automatically decorated with the `main_character_required` decorator. That decorator ensures a user needs to be logged in and have a main before he can access that view. This feature protects against a community app sneaking in a public view without the administrator knowing about it.
`UrlHook(urls, app_name, base_url)`
An app can opt-out of this feature by adding a list of views to be excluded when registering the URLs. See the `excluded_views` parameter for details.
#### urls
The urls module to include. See [the Django docs](https://docs.djangoproject.com/en/dev/topics/http/urls/#example) for designing urlpatterns.
#### 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](https://docs.djangoproject.com/en/dev/topics/http/urls/#how-django-processes-a-request) ordering applies (hook URLs are the last ones registered).
```eval_rst
.. note::
Note that for a public view to work, administrators need to also explicitly allow apps to have public views in their AA installation, by adding the apps label to ``APPS_WITH_PUBLIC_VIEWS`` setting.
```
### Example
## Examples
An app called `plugin` provides a single view:
def index(request):
return render(request, 'plugin/index.html')
```python
def index(request):
return render(request, 'plugin/index.html')
```
The app's `urls.py` would look like so:
from django.urls import path
import plugin.views
```python
from django.urls import path
import plugin.views
urlpatterns = [
path('index/', plugins.views.index, name='index'),
]
urlpatterns = [
path('index/', plugins.views.index, name='index'),
]
```
Subsequently it would implement the UrlHook in a dedicated `auth_hooks.py` file like so:
from alliance_auth import hooks
from services.hooks import UrlHook
import plugin.urls
```python
from alliance_auth import hooks
from services.hooks import UrlHook
import plugin.urls
@hooks.register('url_hook')
def register_urls():
return UrlHook(plugin.urls, 'plugin', r^'plugin/')
@hooks.register('url_hook')
def register_urls():
return UrlHook(plugin.urls, 'plugin', r^'plugin/')
```
When this app is included in the project's `settings.INSTALLED_APPS` users would access the index view by navigating to `https://example.com/plugin/index`.
## API
```eval_rst
.. autoclass:: allianceauth.services.hooks.UrlHook
:members:
```

View File

@@ -113,7 +113,7 @@ By default Corp Stats are only updated on demand. If you want to automatically r
```python
CELERYBEAT_SCHEDULE['update_all_corpstats'] = {
'task': 'allianceauth.corputils.tasks.update_all_corpstats',
'schedule': crontab(minute=0, hour="*/6"),
'schedule': crontab(minute="0", hour="*/6"),
}
```

View File

@@ -29,7 +29,7 @@ DISCORD_SYNC_NAMES = False
CELERYBEAT_SCHEDULE['discord.update_all_usernames'] = {
'task': 'discord.update_all_usernames',
'schedule': crontab(minute=0, hour='*/12'),
'schedule': crontab(minute='0', hour='*/12'),
}
```

View File

@@ -75,6 +75,7 @@ Place your virtual host configuration in the appropriate section within `/etc/ht
ProxyPassMatch ^/static !
ProxyPassMatch ^/robots.txt !
ProxyPassMatch ^/favicon.ico !
ProxyPass / http://127.0.0.1:8000/
ProxyPassReverse / http://127.0.0.1:8000/
@@ -82,6 +83,7 @@ Place your virtual host configuration in the appropriate section within `/etc/ht
Alias "/static" "/var/www/myauth/static"
Alias "/robots.txt" "/var/www/myauth/static/robots.txt"
Alias "/favicon.ico" "/var/www/myauth/static/allianceauth/icons/favicon.ico"
<Directory "/var/www/myauth/static">
Require all granted
@@ -91,6 +93,11 @@ Place your virtual host configuration in the appropriate section within `/etc/ht
SetHandler None
Require all granted
</Location>
<Location "/favicon.ico">
SetHandler None
Require all granted
</Location>
</VirtualHost>
```

View File

@@ -83,8 +83,6 @@ server {
server_name example.com;
location = /favicon.ico { access_log off; log_not_found off; }
location /static {
alias /var/www/myauth/static;
autoindex off;
@@ -94,6 +92,10 @@ server {
alias /var/www/myauth/static/robots.txt;
}
location /favicon.ico {
alias /var/www/myauth/static/allianceauth/icons/favicon.ico;
}
# Gunicorn config goes below
location / {
include proxy_params;

View File

@@ -1,6 +1,91 @@
[build-system]
requires = [
"setuptools>=42",
"wheel"
requires = ["flit_core >=3.2,<4"]
build-backend = "flit_core.buildapi"
[project]
name = "allianceauth"
dynamic = ["version", "description"]
readme = "README.md"
license = {file = "LICENSE"}
requires-python = ">=3.8"
authors = [
{ name = "Alliance Auth", email = "adarnof@gmail.com" },
]
build-backend = "setuptools.build_meta"
keywords = [
"allianceauth",
"eveonline",
]
classifiers = [
"Environment :: Web Environment",
"Framework :: Django",
"Framework :: Django :: 4.0",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU General Public License v2 (GPLv2)",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
]
dependencies = [
"bcrypt",
"beautifulsoup4",
"celery-once>=3.0.1",
"celery>=5.2.0,<6",
"django-bootstrap-form",
"django-celery-beat>=2.3.0",
"django-esi>=4.0.1",
"django-redis>=5.2.0",
"django-registration>=3.3,<3.4",
"django-sortedm2m",
"django>=4.0.9,<4.1.0",
"dnspython",
"mysqlclient>=2.1.0",
"openfire-restapi",
"packaging>=21.0",
"passlib",
"pydiscourse",
"python-slugify>=1.2",
"redis>=4.0.0",
"requests-oauthlib",
"requests>=2.9.1",
"semantic-version",
"slixmpp",
]
[project.optional-dependencies]
test = [
"coverage>=4.3.1",
"django-webtest",
"requests-mock>=1.2.0",
]
[project.scripts]
allianceauth = "allianceauth.bin.allianceauth:main"
[project.urls]
Homepage = "https://gitlab.com/allianceauth/allianceauth"
Documentation = "https://allianceauth.readthedocs.io/"
Source = "https://gitlab.com/allianceauth/allianceauth"
Tracker = "https://gitlab.com/allianceauth/allianceauth/-/issues"
[tool.flit.module]
name = "allianceauth"
[tool.isort]
profile = "django"
sections = [
"FUTURE",
"STDLIB",
"THIRDPARTY",
"DJANGO",
"ESI",
"FIRSTPARTY",
"LOCALFOLDER"
]
known_esi = ["esi"]
known_django = ["django"]
skip_gitignore = true

View File

@@ -1,79 +0,0 @@
[metadata]
name = allianceauth
version = attr: allianceauth.__version__
description = An auth system for EVE Online to help in-game organizations manage online service access.
long_description = file: README.md
long_description_content_type = text/markdown
author = Alliance Auth
author_email = adarnof@gmail.com
license = GPL-2.0
license_files = LICENSE
classifiers =
Environment :: Web Environment
Framework :: Django
Framework :: Django :: 4
Intended Audience :: Developers
License :: OSI Approved :: GNU General Public License v2 (GPLv2)
Operating System :: POSIX :: Linux
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Topic :: Internet :: WWW/HTTP
Topic :: Internet :: WWW/HTTP :: Dynamic Content
home_page = https://gitlab.com/allianceauth/allianceauth
keywords =
allianceauth
eveonline
project_urls =
Issue / Bug Reports = https://gitlab.com/allianceauth/allianceauth/-/issues
Documentation = https://allianceauth.readthedocs.io/
[options]
packages = find_namespace:
install_requires =
bcrypt
beautifulsoup4
celery>=5.2.0,<6
celery-once>=3.0.1
django>=4.0.9,<4.1.0
django-bootstrap-form
django-celery-beat>=2.3.0
django-esi>=4.0.1
django-redis>=5.2.0
django-registration>=3.3,<3.4
django-sortedm2m
dnspython
mysqlclient>=2.1.0
openfire-restapi
packaging>=21.0
passlib
pydiscourse
python-slugify>=1.2
redis>=4.0.0
requests>=2.9.1
requests-oauthlib
semantic-version
slixmpp
python_requires = >=3.8
include_package_data = True
zip_safe = False
[options.packages.find]
include = allianceauth*
[options.entry_points]
console_scripts =
allianceauth = allianceauth.bin.allianceauth:main
[options.extras_require]
test =
coverage>=4.3.1
django-webtest
requests-mock>=1.2.0
[options.package_data]
* = *