Compare commits

...

98 Commits

Author SHA1 Message Date
Ariel Rin
ac6f3c267f Version Bump v3.0.0a5 2022-06-06 23:02:04 +10:00
Ariel Rin
39ad625fa1 Revert "Remove MANIFEST.in, it's redundant the way we use it"
This reverts commit 13e2f4e27d.
2022-06-06 22:59:23 +10:00
Ariel Rin
1db67025bf Version Bump v3.0.0a4 2022-06-06 22:53:37 +10:00
Ariel Rin
13ab6c072a stick with old license_file line to keep pre-commit happy for now 2022-06-06 22:52:20 +10:00
Ariel Rin
cec1dd84ef include subpackages in setuptools package discovery 2022-06-06 12:39:42 +00:00
Ariel Rin
fae5805322 Version Bump v3.0.0a3 2022-06-06 11:41:26 +00:00
Ariel Rin
16ea9500be Fix Classifiers in new format 2022-06-06 11:38:04 +00:00
Ariel Rin
18f5dc0f47 Version Bump 3.0.0a2 2022-06-06 11:16:06 +00:00
Ariel Rin
ba3e941fe8 Merge branch 'issue/1335-aa-3x--django-4-ghost-migration-for-mumble' into 'v3.x'
Override abstract and explicitly set `related_name` for `MumbleUser`

See merge request allianceauth/allianceauth!1416
2022-06-06 10:56:42 +00:00
Peter Pfeufer
4c416b03b1 Override abstract and explicitly set related_name for MumbleUser 2022-06-06 10:56:42 +00:00
Ariel Rin
e2abb64171 Merge branch 'body-bottom-margin' into 'v3.x'
Bottom margin added to body

See merge request allianceauth/allianceauth!1415
2022-06-06 10:49:23 +00:00
Peter Pfeufer
c66aa13ae1 Bottom margin added to body 2022-06-06 10:49:22 +00:00
Ariel Rin
3b539c8577 dependency bumps for Django 4.0 2022-06-04 06:10:05 +00:00
Ariel Rin
4836559abe Merge branch 'fix-decimal_widthratio-template-tag' into 'v3.x'
[FIX] Division by zero in decimal_widthratio template tag

See merge request allianceauth/allianceauth!1419
2022-05-12 13:33:17 +00:00
Peter Pfeufer
17b06c8845 Make it a string in accordance to the return value type 2022-05-12 13:31:06 +02:00
Peter Pfeufer
8dd07b97c7 [FIX] Devision by zero in decimal_widthratio template tag
Fixes: #1343
2022-05-12 13:27:26 +02:00
Ariel Rin
9e139495ac Merge branch 'master' of https://gitlab.com/allianceauth/allianceauth into v3.x 2022-05-12 20:50:41 +10:00
Ariel Rin
23ad9d02d3 Merge branch 'cherry-pick-7fa76d6d' into 'v2.11.x'
Update GitLab CI to conform with the changes to artifacts collection, 2.11.x backport

See merge request allianceauth/allianceauth!1418
2022-05-12 04:30:07 +00:00
Ariel Rin
f99878cc29 Update .gitlab-ci.yml 2022-05-12 04:07:43 +00:00
Ariel Rin
e64431b06c Merge branch 'update-gitlab-ci' into 'v3.x'
Update GitLab CI to conform with the changes to artifacts collection

See merge request allianceauth/allianceauth!1417

(cherry picked from commit 7fa76d6d37)

a3cce358 Update GitLab CI to conform with the changes to artifacts collection
2022-05-12 04:06:04 +00:00
Ariel Rin
0b2993c1c3 Merge branch 'improve_notifications_2' into 'v2.11.x'
Improve notifications

See merge request allianceauth/allianceauth!1411
2022-05-12 04:02:17 +00:00
Erik Kalkoken
75bccf1b0f Improve notifications 2022-05-12 04:02:17 +00:00
Ariel Rin
7fa76d6d37 Merge branch 'update-gitlab-ci' into 'v3.x'
Update GitLab CI to conform with the changes to artifacts collection

See merge request allianceauth/allianceauth!1417
2022-05-12 04:00:34 +00:00
Ariel Rin
945bc92898 Merge branch 'admin-dash-improvement' into 'v2.11.x'
Improve Admin Celery Bar

See merge request allianceauth/allianceauth!1414
2022-05-12 03:57:02 +00:00
Ariel Rin
ec7d14a839 Merge branch 'fix_issue_1222' into 'v2.11.x'
Close security loopholes to make non-superuser admins usable

See merge request allianceauth/allianceauth!1413
2022-05-12 03:56:22 +00:00
Erik Kalkoken
dd1a368ff6 Close security loopholes to make non-superuser admins usable 2022-05-12 03:56:22 +00:00
Peter Pfeufer
a3cce35881 Update GitLab CI to conform with the changes to artifacts collection
See: https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportscobertura-removed
2022-05-11 17:51:43 +02:00
colcrunch
54085617dc Add a few pixels of margin-top to bar labels to better center them. 2022-04-16 15:46:01 -04:00
colcrunch
8cdc5af453 Improve celery bar by using decimalized width values (2 decimal places) to reduce likelyhood of an empty portion of the bar. 2022-04-16 15:44:53 -04:00
Ariel Rin
da93940e13 Just an empty Tag Commit, because 2.11.2 bump went wonky 2022-03-29 14:48:39 +10:00
Ariel Rin
f53b43d9dc Merge branch 'master' of https://gitlab.com/allianceauth/allianceauth into v2.11.x 2022-03-29 14:47:40 +10:00
Ariel Rin
497a167ca7 Version Bump v2.11.2 2022-03-29 14:46:59 +10:00
Ariel Rin
852c5a3037 Bump Django-ESI to 4.x, inc breaking CCP change in 4.0.1 2022-03-29 14:40:30 +10:00
Ariel Rin
90f6777a7a Version Bump 2.11.1 2022-03-20 14:42:39 +10:00
Ariel Rin
ed4f71a283 Merge branch 'remove-manifest-file' into 'v3.x'
Remove MANIFEST.in, it's redundant the way we use it

See merge request allianceauth/allianceauth!1410
2022-03-20 04:36:09 +00:00
Peter Pfeufer
13e2f4e27d Remove MANIFEST.in, it's redundant the way we use it
See: https://packaging.python.org/en/latest/guides/using-manifest-in/
2022-03-13 09:42:22 +01:00
Ariel Rin
17343dfeae Merge branch 'master' of https://gitlab.com/allianceauth/allianceauth into v3.x 2022-03-09 20:57:07 +10:00
Ariel Rin
4b5978fb58 Merge branch 'version-links' into 'v3.x'
Link "Latest Stable" and "Latest Pre-Release" versions to their tags on GitLab

See merge request allianceauth/allianceauth!1405
2022-03-09 10:05:05 +00:00
Ariel Rin
a8d890abaf Merge branch 'improve_task_statistics' into 'master'
Improve task statistics

See merge request allianceauth/allianceauth!1409
2022-03-09 10:04:14 +00:00
Erik Kalkoken
79379b444c Improve task statistics 2022-03-09 10:04:13 +00:00
Ariel Rin
ace1de5c68 Merge branch 'fix-docker-new-redis' into 'master'
Fix docker for new redis

See merge request allianceauth/allianceauth!1406
2022-03-09 10:02:01 +00:00
Ariel Rin
0a30eea3b4 Merge branch 'deprecated-settings' into 'v3.x'
Remove deprecated settings

See merge request allianceauth/allianceauth!1407
2022-03-09 09:53:15 +00:00
Ariel Rin
a15d281c40 Merge branch 'switch-to-setup.cfg' into 'v3.x'
Switch to setup.cfg due to deprecation of setup.py

See merge request allianceauth/allianceauth!1408
2022-03-09 09:50:40 +00:00
Peter Pfeufer
6846bb7cdc Switch to setup.cfg due to deprecation of setup.py 2022-03-09 09:50:40 +00:00
Peter Pfeufer
1d240a40dd Remove deprecated settings
Examples:

RemovedInDjango50Warning: The USE_L10N setting is deprecated. Starting with Django 5.0, localized formatting of data will always be enabled. For example Django will display numbers and dates using the format of the current locale.
  warnings.warn(USE_L10N_DEPRECATED_MSG, RemovedInDjango50Warning)

RemovedInDjango41Warning: 'allianceauth' defines default_app_config = 'allianceauth.apps.AllianceAuthConfig'. Django now detects this configuration automatically. You can remove default_app_config.
  app_config = AppConfig.create(entry)
2022-03-03 12:06:16 +01:00
Kevin McKernan
5d6128e9ea remove collectstatic command from dockerfile 2022-03-01 13:23:49 -07:00
Ariel Rin
c377bcec5f add redis to readthedocs container 2022-03-01 07:40:15 +00:00
Ariel Rin
9b74fb4dbd Attempted Docs Fixes 2022-03-01 03:33:12 +00:00
Peter Pfeufer
6744c0c143 Link Latest stable and Latest pre-release versions to their tags on git 2022-02-28 21:19:33 +01:00
Ariel Rin
1e9f5e6430 Version Bump 3.0.0a1 2022-02-26 06:26:36 +00:00
Ariel Rin
ceaa064e62 Merge branch 'usersettings' into 'v3.x'
Persistent User Settings

See merge request allianceauth/allianceauth!1333
2022-02-26 06:19:38 +00:00
Ariel Rin
1aad3e4512 Persistent User Settings 2022-02-26 06:19:38 +00:00
Ariel Rin
f83c3c2811 Merge branch 'ErikKalkoken/allianceauth-fix_character_names' into v3.x 2022-02-26 15:53:46 +10:00
Ariel Rin
a23ec6d318 switch new task module to new redis method 2022-02-26 15:52:15 +10:00
Ariel Rin
ecc53888bc Merge branch 'master' of https://gitlab.com/allianceauth/allianceauth into v3.x 2022-02-26 15:23:01 +10:00
Ariel Rin
e54f72091f specific known working dependency commit 2022-02-26 14:27:00 +10:00
Ariel Rin
75b5b28804 use Django-ESI 4.0.0a1, instead of Ariels branch 2022-02-20 23:24:26 +10:00
Ariel Rin
f81a2ed237 Merge branch 'django4' into 'v3.x'
dropin replace sleeksmpp with slixmpp, alter test

See merge request allianceauth/allianceauth!1399
2022-02-20 13:19:19 +00:00
Ariel Rin
49e01157e7 dropin replace sleeksmpp with slixmpp, alter test 2022-02-20 13:19:19 +00:00
Ariel Rin
28420a729e Merge branch 'html5-fixes' into 'v3.x'
[FIX] Use proper HTML5 tags instead of self-closing XML/(X)HTML tags

See merge request allianceauth/allianceauth!1398
2022-02-11 02:10:48 +00:00
Peter Pfeufer
52a4cf8d52 [FIX] Use proper HTML5 tags instead of self-closing XML/XHTML tags 2022-02-08 20:22:53 +01:00
Ariel Rin
703c2392a9 Merge branch 'django4' into 'v3.x'
v2.10.x Uplifts, DJ4, Py3.8 + More

See merge request allianceauth/allianceauth!1387
2022-02-08 13:04:45 +00:00
Ariel Rin
18c9a66437 Merge branch 'fix-ifequal-errors' into 'django4'
Fix ifequal errors

See merge request soratidus999/allianceauth!9
2022-02-06 07:16:56 +00:00
Ariel Rin
9687d57de9 Merge branch 'update-url-configs' into 'django4'
Switch to `path`, use `re_path` only when really needed

See merge request soratidus999/allianceauth!8
2022-02-06 07:16:46 +00:00
Peter Pfeufer
60c2e57d83 Fix ifequal errors 2022-02-02 16:12:43 +01:00
Peter Pfeufer
b14bff0145 We should do this properly .. 2022-02-02 15:28:36 +01:00
Peter Pfeufer
9166886665 That one slipped through the cracks ... 2022-02-02 15:27:07 +01:00
Peter Pfeufer
c74010d441 Docs updated 2022-02-02 15:25:45 +01:00
Peter Pfeufer
640a21e4db Switch to path, use re_path only when really needed 2022-02-02 15:09:48 +01:00
Ariel Rin
fd442a5735 django.conf.urls.url is deprecated 2022-02-02 21:56:01 +10:00
Ariel Rin
c7b99044bc django.conf.urls.url is deprecated, more to fix 2022-02-02 21:39:37 +10:00
Ariel Rin
234451a7d4 temporarily use django-esi MR 2022-02-02 21:37:01 +10:00
Ariel Rin
ffff904ab1 Pull specific commit from git temporarily 2022-02-02 15:18:20 +10:00
Ariel Rin
d71a26220c Merge branch 'v3.x' of https://gitlab.com/allianceauth/allianceauth into django4 2022-02-02 14:24:47 +10:00
Ariel Rin
beeeb8dc5d Merge branch 'v3.x' of https://gitlab.com/allianceauth/allianceauth into v3.x 2022-02-02 14:17:15 +10:00
Ariel Rin
19244cc4c6 Merge branch 'master' of https://gitlab.com/allianceauth/allianceauth into v3.x 2022-02-02 14:16:46 +10:00
Ariel Rin
cc94ba6b5e ensure latest django patch 2022-02-02 14:16:33 +10:00
Ariel Rin
c9926cc877 Merge tag 'v2.10.0' of https://gitlab.com/allianceauth/allianceauth into django4 2022-02-02 14:15:32 +10:00
Ariel Rin
1d14e1b0af Merge branch 'rediscache' into 'v3.x'
Swap the Redis Cache client

See merge request allianceauth/allianceauth!1394
2022-02-02 04:12:04 +00:00
Aaron Kable
297da44a5a Swap the Redis Cache client 2022-02-02 04:12:04 +00:00
Ariel Rin
402ff53a5c Merge branch 'master' of https://gitlab.com/allianceauth/allianceauth into django4 2022-02-02 12:38:24 +10:00
ErikKalkoken
2d6e4a0df1 Merge branch 'master' into fix_character_names 2022-02-01 00:31:25 +01:00
ErikKalkoken
defcfa3316 Character names are not unique 2022-01-05 19:47:15 +01:00
ErikKalkoken
3209b71b0a Fix imports and flake8 issues 2022-01-05 19:28:44 +01:00
Ariel Rin
80b3ca0a1e Merge branch 'v2.10.x' of https://gitlab.com/allianceauth/allianceauth into django4 2021-12-29 16:35:37 +10:00
Ariel Rin
8351bd2fa3 Forgot to remove a 3.7 test 2021-12-29 16:34:14 +10:00
Ariel Rin
255966ed3b Secret Detection was split from SAST 2021-12-29 16:32:42 +10:00
Ariel Rin
8d6ebf4770 Merge branch 'v2.10.x' of https://gitlab.com/allianceauth/allianceauth into django4 2021-12-29 15:52:39 +10:00
Ariel Rin
2ca752bf78 Merge branch 'master' of https://gitlab.com/allianceauth/allianceauth into django4 2021-12-29 15:52:01 +10:00
Ariel Rin
79e1192f67 Update Pre-Commit 2021-12-29 15:45:33 +10:00
Ariel Rin
ff610efc84 Merge branch 'master' of https://gitlab.com/allianceauth/allianceauth into v2.10.x 2021-12-28 21:58:38 +10:00
Ariel Rin
6b68a739ef Initial spam of version bumps 2021-12-24 17:43:15 +10:00
Ariel Rin
909bd0ba15 Fix deprecations removed in dj4 2021-12-24 17:42:53 +10:00
Ariel Rin
05110abc59 remove 3.7 testing 2021-12-24 16:25:29 +10:00
Ariel Rin
a64d99eb91 Target Py3.8 2021-12-24 14:48:30 +10:00
Ariel Rin
0e45403195 Merge branch 'master' of https://gitlab.com/allianceauth/allianceauth into v2.10.x 2021-12-24 14:41:09 +10:00
Ariel Rin
e16a9ffe65 update pre-commit 2021-12-24 14:26:15 +10:00
Ariel Rin
57de122ef8 Move away frfom 3.6 even for pre-commit 2021-12-24 14:23:41 +10:00
162 changed files with 1981 additions and 1017 deletions

View File

@@ -14,6 +14,7 @@ stages:
include:
- template: Dependency-Scanning.gitlab-ci.yml
- template: Security/SAST.gitlab-ci.yml
- template: Security/Secret-Detection.gitlab-ci.yml
before_script:
- apt-get update && apt-get install redis-server -y
@@ -24,7 +25,7 @@ before_script:
pre-commit-check:
<<: *only-default
stage: pre-commit
image: python:3.6-buster
image: python:3.8-bullseye
variables:
PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
cache:
@@ -46,16 +47,6 @@ dependency_scanning:
- python -V
- pip install wheel tox
test-3.7-core:
<<: *only-default
image: python:3.7-bullseye
script:
- tox -e py37-core
artifacts:
when: always
reports:
cobertura: coverage.xml
test-3.8-core:
<<: *only-default
image: python:3.8-bullseye
@@ -64,7 +55,9 @@ test-3.8-core:
artifacts:
when: always
reports:
cobertura: coverage.xml
coverage_report:
coverage_format: cobertura
path: coverage.xml
test-3.9-core:
<<: *only-default
@@ -74,7 +67,9 @@ test-3.9-core:
artifacts:
when: always
reports:
cobertura: coverage.xml
coverage_report:
coverage_format: cobertura
path: coverage.xml
test-3.10-core:
<<: *only-default
@@ -84,7 +79,9 @@ test-3.10-core:
artifacts:
when: always
reports:
cobertura: coverage.xml
coverage_report:
coverage_format: cobertura
path: coverage.xml
test-3.11-core:
<<: *only-default
@@ -94,19 +91,11 @@ test-3.11-core:
artifacts:
when: always
reports:
cobertura: coverage.xml
coverage_report:
coverage_format: cobertura
path: coverage.xml
allow_failure: true
test-3.7-all:
<<: *only-default
image: python:3.7-bullseye
script:
- tox -e py37-all
artifacts:
when: always
reports:
cobertura: coverage.xml
test-3.8-all:
<<: *only-default
image: python:3.8-bullseye
@@ -115,7 +104,9 @@ test-3.8-all:
artifacts:
when: always
reports:
cobertura: coverage.xml
coverage_report:
coverage_format: cobertura
path: coverage.xml
test-3.9-all:
<<: *only-default
@@ -125,7 +116,9 @@ test-3.9-all:
artifacts:
when: always
reports:
cobertura: coverage.xml
coverage_report:
coverage_format: cobertura
path: coverage.xml
test-3.10-all:
<<: *only-default
@@ -135,7 +128,9 @@ test-3.10-all:
artifacts:
when: always
reports:
cobertura: coverage.xml
coverage_report:
coverage_format: cobertura
path: coverage.xml
test-3.11-all:
<<: *only-default
@@ -145,7 +140,9 @@ test-3.11-all:
artifacts:
when: always
reports:
cobertura: coverage.xml
coverage_report:
coverage_format: cobertura
path: coverage.xml
allow_failure: true
deploy_production:
@@ -153,11 +150,13 @@ deploy_production:
image: python:3.10-bullseye
before_script:
- pip install twine wheel
- python -m pip install --upgrade pip
- python -m pip install --upgrade build
- python -m pip install --upgrade setuptools wheel twine
script:
- python setup.py sdist bdist_wheel
- twine upload dist/*
- python -m build
- python -m twine upload dist/*
rules:
- if: $CI_COMMIT_TAG

View File

@@ -5,7 +5,7 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
rev: v4.1.0
hooks:
- id: check-case-conflict
- id: check-json
@@ -22,13 +22,18 @@ repos:
args: [ '--remove' ]
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python
rev: 2.3.54
rev: 2.4.0
hooks:
- id: editorconfig-checker
exclude: ^(LICENSE|allianceauth\/static\/css\/themes\/bootstrap-locals.less|allianceauth\/eveonline\/swagger.json|(.*.po)|(.*.mo))
- repo: https://github.com/asottile/pyupgrade
rev: v2.29.0
rev: v2.31.0
hooks:
- id: pyupgrade
args: [ --py37-plus ]
args: [ --py38-plus ]
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v1.20.0
hooks:
- id: setup-cfg-fmt

View File

@@ -5,19 +5,22 @@
# Required
version: 2
# Set the version of Python and other tools you might need
build:
os: ubuntu-20.04
apt_packages:
- redis
tools:
python: "3.10"
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py
# Build documentation with MkDocs
#mkdocs:
# configuration: mkdocs.yml
# Optionally build your docs in additional formats such as PDF and ePub
formats: all
# Optionally set the version of Python and requirements required to build your docs
python:
version: 3.7
install:
- requirements: docs/requirements.txt

View File

@@ -1,8 +1,7 @@
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
__version__ = '2.11.0'
__version__ = '3.0.0a5'
__title__ = 'Alliance Auth'
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
NAME = f'{__title__} v{__version__}'
default_app_config = 'allianceauth.apps.AllianceAuthConfig'

View File

@@ -1 +0,0 @@
default_app_config = 'allianceauth.analytics.apps.AnalyticsConfig'

View File

@@ -1,5 +1,6 @@
from allianceauth.analytics.middleware import AnalyticsMiddleware
from unittest.mock import Mock
from django.http import HttpResponse
from django.test.testcases import TestCase
@@ -7,7 +8,7 @@ from django.test.testcases import TestCase
class TestAnalyticsMiddleware(TestCase):
def setUp(self):
self.middleware = AnalyticsMiddleware()
self.middleware = AnalyticsMiddleware(HttpResponse)
self.request = Mock()
self.request.headers = {
"User-Agent": "AUTOMATED TEST"

View File

@@ -1 +0,0 @@
default_app_config = 'allianceauth.authentication.apps.AuthenticationConfig'

View File

@@ -1,26 +1,44 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import User as BaseUser, \
Permission as BasePermission, Group
from django.contrib.auth.models import Group
from django.contrib.auth.models import Permission as BasePermission
from django.contrib.auth.models import User as BaseUser
from django.db.models import Count, Q
from allianceauth.services.hooks import ServicesHook
from django.db.models.signals import pre_save, post_save, pre_delete, \
post_delete, m2m_changed
from django.db.models.functions import Lower
from django.db.models.signals import (
m2m_changed,
post_delete,
post_save,
pre_delete,
pre_save
)
from django.dispatch import receiver
from django.forms import ModelForm
from django.utils.html import format_html
from django.urls import reverse
from django.utils.html import format_html
from django.utils.text import slugify
from allianceauth.authentication.models import State, get_guest_state,\
CharacterOwnership, UserProfile, OwnershipRecord
from allianceauth.hooks import get_hooks
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo,\
EveAllianceInfo, EveFactionInfo
from allianceauth.authentication.models import (
CharacterOwnership,
OwnershipRecord,
State,
UserProfile,
get_guest_state
)
from allianceauth.eveonline.models import (
EveAllianceInfo,
EveCharacter,
EveCorporationInfo,
EveFactionInfo
)
from allianceauth.eveonline.tasks import update_character
from .app_settings import AUTHENTICATION_ADMIN_USERS_MAX_GROUPS, \
AUTHENTICATION_ADMIN_USERS_MAX_CHARS
from allianceauth.hooks import get_hooks
from allianceauth.services.hooks import ServicesHook
from .app_settings import (
AUTHENTICATION_ADMIN_USERS_MAX_CHARS,
AUTHENTICATION_ADMIN_USERS_MAX_GROUPS
)
from .forms import UserChangeForm, UserProfileForm
def make_service_hooks_update_groups_action(service):
@@ -59,19 +77,10 @@ def make_service_hooks_sync_nickname_action(service):
return sync_nickname
class QuerysetModelForm(ModelForm):
# allows specifying FK querysets through kwarg
def __init__(self, querysets=None, *args, **kwargs):
querysets = querysets or {}
super().__init__(*args, **kwargs)
for field, qs in querysets.items():
self.fields[field].queryset = qs
class UserProfileInline(admin.StackedInline):
model = UserProfile
readonly_fields = ('state',)
form = QuerysetModelForm
form = UserProfileForm
verbose_name = ''
verbose_name_plural = 'Profile'
@@ -99,6 +108,7 @@ class UserProfileInline(admin.StackedInline):
return False
@admin.display(description="")
def user_profile_pic(obj):
"""profile pic column data for user objects
@@ -111,13 +121,10 @@ def user_profile_pic(obj):
'<img src="{}" class="img-circle">',
user_obj.profile.main_character.portrait_url(size=32)
)
else:
return None
user_profile_pic.short_description = ''
return None
@admin.display(description="user / main", ordering="username")
def user_username(obj):
"""user column data for user objects
@@ -139,18 +146,17 @@ def user_username(obj):
user_obj.username,
user_obj.profile.main_character.character_name
)
else:
return format_html(
'<strong><a href="{}">{}</a></strong>',
link,
user_obj.username,
)
user_username.short_description = 'user / main'
user_username.admin_order_field = 'username'
return format_html(
'<strong><a href="{}">{}</a></strong>',
link,
user_obj.username,
)
@admin.display(
description="Corporation / Alliance (Main)",
ordering="profile__main_character__corporation_name"
)
def user_main_organization(obj):
"""main organization column data for user objects
@@ -159,21 +165,15 @@ def user_main_organization(obj):
"""
user_obj = obj.user if hasattr(obj, 'user') else obj
if not user_obj.profile.main_character:
result = ''
else:
result = user_obj.profile.main_character.corporation_name
if user_obj.profile.main_character.alliance_id:
result += f'<br>{user_obj.profile.main_character.alliance_name}'
elif user_obj.profile.main_character.faction_name:
result += f'<br>{user_obj.profile.main_character.faction_name}'
return ''
result = user_obj.profile.main_character.corporation_name
if user_obj.profile.main_character.alliance_id:
result += f'<br>{user_obj.profile.main_character.alliance_name}'
elif user_obj.profile.main_character.faction_name:
result += f'<br>{user_obj.profile.main_character.faction_name}'
return format_html(result)
user_main_organization.short_description = 'Corporation / Alliance (Main)'
user_main_organization.admin_order_field = \
'profile__main_character__corporation_name'
class MainCorporationsFilter(admin.SimpleListFilter):
"""Custom filter to filter on corporations from mains only
@@ -196,15 +196,13 @@ class MainCorporationsFilter(admin.SimpleListFilter):
def queryset(self, request, qs):
if self.value() is None:
return qs.all()
else:
if qs.model == User:
return qs.filter(
profile__main_character__corporation_id=self.value()
)
else:
return qs.filter(
user__profile__main_character__corporation_id=self.value()
)
if qs.model == User:
return qs.filter(
profile__main_character__corporation_id=self.value()
)
return qs.filter(
user__profile__main_character__corporation_id=self.value()
)
class MainAllianceFilter(admin.SimpleListFilter):
@@ -217,12 +215,14 @@ class MainAllianceFilter(admin.SimpleListFilter):
parameter_name = 'main_alliance_id__exact'
def lookups(self, request, model_admin):
qs = EveCharacter.objects\
.exclude(alliance_id=None)\
.exclude(userprofile=None)\
.values('alliance_id', 'alliance_name')\
.distinct()\
qs = (
EveCharacter.objects
.exclude(alliance_id=None)
.exclude(userprofile=None)
.values('alliance_id', 'alliance_name')
.distinct()
.order_by(Lower('alliance_name'))
)
return tuple(
(x['alliance_id'], x['alliance_name']) for x in qs
)
@@ -230,13 +230,11 @@ class MainAllianceFilter(admin.SimpleListFilter):
def queryset(self, request, qs):
if self.value() is None:
return qs.all()
else:
if qs.model == User:
return qs.filter(profile__main_character__alliance_id=self.value())
else:
return qs.filter(
user__profile__main_character__alliance_id=self.value()
)
if qs.model == User:
return qs.filter(profile__main_character__alliance_id=self.value())
return qs.filter(
user__profile__main_character__alliance_id=self.value()
)
class MainFactionFilter(admin.SimpleListFilter):
@@ -249,12 +247,14 @@ class MainFactionFilter(admin.SimpleListFilter):
parameter_name = 'main_faction_id__exact'
def lookups(self, request, model_admin):
qs = EveCharacter.objects\
.exclude(faction_id=None)\
.exclude(userprofile=None)\
.values('faction_id', 'faction_name')\
.distinct()\
qs = (
EveCharacter.objects
.exclude(faction_id=None)
.exclude(userprofile=None)
.values('faction_id', 'faction_name')
.distinct()
.order_by(Lower('faction_name'))
)
return tuple(
(x['faction_id'], x['faction_name']) for x in qs
)
@@ -262,15 +262,14 @@ class MainFactionFilter(admin.SimpleListFilter):
def queryset(self, request, qs):
if self.value() is None:
return qs.all()
else:
if qs.model == User:
return qs.filter(profile__main_character__faction_id=self.value())
else:
return qs.filter(
user__profile__main_character__faction_id=self.value()
)
if qs.model == User:
return qs.filter(profile__main_character__faction_id=self.value())
return qs.filter(
user__profile__main_character__faction_id=self.value()
)
@admin.display(description="Update main character model from ESI")
def update_main_character_model(modeladmin, request, queryset):
tasks_count = 0
for obj in queryset:
@@ -279,21 +278,48 @@ def update_main_character_model(modeladmin, request, queryset):
tasks_count += 1
modeladmin.message_user(
request,
f'Update from ESI started for {tasks_count} characters'
request, f'Update from ESI started for {tasks_count} characters'
)
update_main_character_model.short_description = \
'Update main character model from ESI'
class UserAdmin(BaseUserAdmin):
"""Extending Django's UserAdmin model
Behavior of groups and characters columns can be configured via settings
"""
inlines = BaseUserAdmin.inlines + [UserProfileInline]
ordering = ('username', )
list_select_related = ('profile__state', 'profile__main_character')
show_full_result_count = True
list_display = (
user_profile_pic,
user_username,
'_state',
'_groups',
user_main_organization,
'_characters',
'is_active',
'date_joined',
'_role'
)
list_display_links = None
list_filter = (
'profile__state',
'groups',
MainCorporationsFilter,
MainAllianceFilter,
MainFactionFilter,
'is_active',
'date_joined',
'is_staff',
'is_superuser'
)
search_fields = ('username', 'character_ownerships__character__character_name')
readonly_fields = ('date_joined', 'last_login')
filter_horizontal = ('groups', 'user_permissions',)
form = UserChangeForm
class Media:
css = {
"all": ("authentication/css/admin.css",)
@@ -303,9 +329,21 @@ class UserAdmin(BaseUserAdmin):
qs = super().get_queryset(request)
return qs.prefetch_related("character_ownerships__character", "groups")
def get_actions(self, request):
actions = super(BaseUserAdmin, self).get_actions(request)
def get_form(self, request, obj=None, **kwargs):
"""Inject current request into change form object."""
MyForm = super().get_form(request, obj, **kwargs)
if obj:
class MyFormInjected(MyForm):
def __new__(cls, *args, **kwargs):
kwargs['request'] = request
return MyForm(*args, **kwargs)
return MyFormInjected
return MyForm
def get_actions(self, request):
actions = super().get_actions(request)
actions[update_main_character_model.__name__] = (
update_main_character_model,
update_main_character_model.__name__,
@@ -349,39 +387,6 @@ class UserAdmin(BaseUserAdmin):
)
return result
inlines = BaseUserAdmin.inlines + [UserProfileInline]
ordering = ('username', )
list_select_related = ('profile__state', 'profile__main_character')
show_full_result_count = True
list_display = (
user_profile_pic,
user_username,
'_state',
'_groups',
user_main_organization,
'_characters',
'is_active',
'date_joined',
'_role'
)
list_display_links = None
list_filter = (
'profile__state',
'groups',
MainCorporationsFilter,
MainAllianceFilter,
MainFactionFilter,
'is_active',
'date_joined',
'is_staff',
'is_superuser'
)
search_fields = (
'username',
'character_ownerships__character__character_name'
)
readonly_fields = ('date_joined', 'last_login')
def _characters(self, obj):
character_ownerships = list(obj.character_ownerships.all())
characters = [obj.character.character_name for obj in character_ownerships]
@@ -390,22 +395,16 @@ class UserAdmin(BaseUserAdmin):
AUTHENTICATION_ADMIN_USERS_MAX_CHARS
)
_characters.short_description = 'characters'
@admin.display(ordering="profile__state")
def _state(self, obj):
return obj.profile.state.name
_state.short_description = 'state'
_state.admin_order_field = 'profile__state'
def _groups(self, obj):
my_groups = sorted(group.name for group in list(obj.groups.all()))
return self._list_2_html_w_tooltips(
my_groups, AUTHENTICATION_ADMIN_USERS_MAX_GROUPS
)
_groups.short_description = 'groups'
def _role(self, obj):
if obj.is_superuser:
role = 'Superuser'
@@ -415,8 +414,6 @@ class UserAdmin(BaseUserAdmin):
role = 'User'
return role
_role.short_description = 'role'
def has_change_permission(self, request, obj=None):
return request.user.has_perm('auth.change_user')
@@ -438,9 +435,16 @@ class UserAdmin(BaseUserAdmin):
if obj_state:
matching_groups_qs = Group.objects.filter(authgroup__states=obj_state)
groups_qs = groups_qs | matching_groups_qs
kwargs["queryset"] = groups_qs.order_by(Lower('name'))
kwargs["queryset"] = groups_qs.order_by(Lower("name"))
return super().formfield_for_manytomany(db_field, request, **kwargs)
def get_readonly_fields(self, request, obj=None):
if obj and not request.user.is_superuser:
return self.readonly_fields + (
"is_staff", "is_superuser", "user_permissions"
)
return self.readonly_fields
@admin.register(State)
class StateAdmin(admin.ModelAdmin):
@@ -451,10 +455,9 @@ class StateAdmin(admin.ModelAdmin):
qs = super().get_queryset(request)
return qs.annotate(user_count=Count("userprofile__id"))
@admin.display(description="Users", ordering="user_count")
def _user_count(self, obj):
return obj.user_count
_user_count.short_description = 'Users'
_user_count.admin_order_field = 'user_count'
fieldsets = (
(None, {
@@ -510,13 +513,13 @@ class StateAdmin(admin.ModelAdmin):
)
return super().get_fieldsets(request, obj=obj)
def get_readonly_fields(self, request, obj=None):
if not request.user.is_superuser:
return self.readonly_fields + ("permissions",)
return self.readonly_fields
class BaseOwnershipAdmin(admin.ModelAdmin):
class Media:
css = {
"all": ("authentication/css/admin.css",)
}
list_select_related = (
'user__profile__state', 'user__profile__main_character', 'character')
list_display = (
@@ -537,6 +540,11 @@ class BaseOwnershipAdmin(admin.ModelAdmin):
MainAllianceFilter,
)
class Media:
css = {
"all": ("authentication/css/admin.css",)
}
def get_readonly_fields(self, request, obj=None):
if obj and obj.pk:
return 'owner_hash', 'character'

View File

@@ -1,8 +1,66 @@
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.forms import UserChangeForm as BaseUserChangeForm
from django.contrib.auth.models import Group
from django.core.exceptions import ValidationError
from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from allianceauth.authentication.models import User
class RegistrationForm(forms.Form):
email = forms.EmailField(label=_('Email'), max_length=254, required=True)
class _meta:
model = User
class UserProfileForm(ModelForm):
"""Allows specifying FK querysets through kwarg"""
def __init__(self, querysets=None, *args, **kwargs):
querysets = querysets or {}
super().__init__(*args, **kwargs)
for field, qs in querysets.items():
self.fields[field].queryset = qs
class UserChangeForm(BaseUserChangeForm):
"""Add custom cleaning to UserChangeForm"""
def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request") # Inject current request into form object
super().__init__(*args, **kwargs)
def clean(self):
cleaned_data = super().clean()
if not self.request.user.is_superuser:
if self.instance:
current_restricted = set(
self.instance.groups.filter(
authgroup__restricted=True
).values_list("pk", flat=True)
)
else:
current_restricted = set()
new_restricted = set(
cleaned_data["groups"].filter(
authgroup__restricted=True
).values_list("pk", flat=True)
)
if current_restricted != new_restricted:
restricted_removed = current_restricted - new_restricted
restricted_added = new_restricted - current_restricted
restricted_changed = restricted_removed | restricted_added
restricted_names_qs = Group.objects.filter(
pk__in=restricted_changed
).values_list("name", flat=True)
restricted_names = ",".join(list(restricted_names_qs))
raise ValidationError(
{
"groups": _(
"You are not allowed to add or remove these "
"restricted groups: %s" % restricted_names
)
}
)

View File

@@ -1,14 +1,16 @@
from django.conf.urls import url, include
from django.conf.urls import include
from allianceauth.authentication import views
from django.urls import re_path
from django.urls import path
urlpatterns = [
url(r'^activate/complete/$', views.activation_complete, name='registration_activation_complete'),
path('activate/complete/', views.activation_complete, name='registration_activation_complete'),
# The activation key can make use of any character from the
# URL-safe base64 alphabet, plus the colon as a separator.
url(r'^activate/(?P<activation_key>[-:\w]+)/$', views.ActivationView.as_view(), name='registration_activate'),
url(r'^register/$', views.RegistrationView.as_view(), name='registration_register'),
url(r'^register/complete/$', views.registration_complete, name='registration_complete'),
url(r'^register/closed/$', views.registration_closed, name='registration_disallowed'),
url(r'', include('django.contrib.auth.urls')),
re_path(r'^activate/(?P<activation_key>[-:\w]+)/$', views.ActivationView.as_view(), name='registration_activate'),
path('register/', views.RegistrationView.as_view(), name='registration_register'),
path('register/complete/', views.registration_complete, name='registration_complete'),
path('register/closed/', views.registration_closed, name='registration_disallowed'),
path('', include('django.contrib.auth.urls')),
]

View File

@@ -0,0 +1,45 @@
from django.conf import settings
from django.utils.deprecation import MiddlewareMixin
import logging
logger = logging.getLogger(__name__)
class UserSettingsMiddleware(MiddlewareMixin):
def process_response(self, request, response):
"""Django Middleware: User Settings."""
# Intercept the built in django /setlang/ view and also save it to Database.
# Note the annoymous user check, only logged in users will ever hit the DB here
if request.path == '/i18n/setlang/' and not request.user.is_anonymous:
try:
request.user.profile.language = request.POST['language']
request.user.profile.save()
except Exception as e:
logger.exception(e)
# Only act during the login flow, _after_ user is activated (step 2: post-sso)
elif request.path == '/sso/login' and not request.user.is_anonymous:
# Set the Language Cookie, if it doesnt match the DB
# Null = hasnt been set by the user ever, dont act.
try:
if request.user.profile.language != request.LANGUAGE_CODE and request.user.profile.language is not None:
response.set_cookie(key=settings.LANGUAGE_COOKIE_NAME,
value=request.user.profile.language,
max_age=settings.LANGUAGE_COOKIE_AGE)
except Exception as e:
logger.exception(e)
# Set our Night mode flag from the DB
# Null = hasnt been set by the user ever, dont act.
#
# Night mode intercept is not needed in this middleware.
# is saved direct to DB in NightModeRedirectView
try:
if request.user.profile.night_mode is not None:
request.session["NIGHT_MODE"] = request.user.profile.night_mode
except Exception as e:
logger.exception(e)
return response

View File

@@ -0,0 +1,23 @@
# Generated by Django 4.0.2 on 2022-02-26 03:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0019_merge_20211026_0919'),
]
operations = [
migrations.AddField(
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')], default='', max_length=10, verbose_name='Language'),
),
migrations.AddField(
model_name='userprofile',
name='night_mode',
field=models.BooleanField(blank=True, null=True, verbose_name='Night Mode'),
),
]

View File

@@ -2,9 +2,10 @@ import logging
from django.contrib.auth.models import User, Permission
from django.db import models, transaction
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo, EveFactionInfo
from allianceauth.notifications import notify
from django.conf import settings
from .managers import CharacterOwnershipManager, StateManager
@@ -62,9 +63,39 @@ class UserProfile(models.Model):
class Meta:
default_permissions = ('change',)
user = models.OneToOneField(User, related_name='profile', on_delete=models.CASCADE)
main_character = models.OneToOneField(EveCharacter, blank=True, null=True, on_delete=models.SET_NULL)
state = models.ForeignKey(State, on_delete=models.SET_DEFAULT, default=get_guest_state_pk)
user = models.OneToOneField(
User,
related_name='profile',
on_delete=models.CASCADE)
main_character = models.OneToOneField(
EveCharacter,
blank=True,
null=True,
on_delete=models.SET_NULL)
state = models.ForeignKey(
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,
blank=True,
default='')
night_mode = models.BooleanField(
_("Night Mode"),
blank=True,
null=True)
def assign_state(self, state=None, commit=True):
if not state:
@@ -93,8 +124,6 @@ class UserProfile(models.Model):
def __str__(self):
return str(self.user)
class CharacterOwnership(models.Model):
class Meta:
default_permissions = ('change', 'delete')

View File

@@ -1,6 +1,11 @@
import logging
from .models import CharacterOwnership, UserProfile, get_guest_state, State, OwnershipRecord
from .models import (
CharacterOwnership,
UserProfile,
get_guest_state,
State,
OwnershipRecord)
from django.contrib.auth.models import User
from django.db.models import Q
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed
@@ -11,7 +16,7 @@ from allianceauth.eveonline.models import EveCharacter
logger = logging.getLogger(__name__)
state_changed = Signal(providing_args=['user', 'state'])
state_changed = Signal()
def trigger_state_check(state):
@@ -71,7 +76,7 @@ def reassess_on_profile_save(sender, instance, created, *args, **kwargs):
@receiver(post_save, sender=User)
def create_required_models(sender, instance, created, *args, **kwargs):
# ensure all users have a model
# ensure all users have our Sub-Models
if created:
logger.debug(f'User {instance} created. Creating default UserProfile.')
UserProfile.objects.get_or_create(user=instance)

View File

@@ -0,0 +1,40 @@
from collections import namedtuple
import datetime as dt
from .event_series import EventSeries
"""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"]
)
def dashboard_results(hours: int) -> _TaskCounts:
"""Counts of all task events within the given timeframe."""
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()
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)
return _TaskCounts(
succeeded=succeeded_count,
retried=retried_count,
failed=failed_count,
total=succeeded_count + retried_count + failed_count,
earliest_task=min(earliest_events) if earliest_events else None,
hours=hours,
)

View File

@@ -1,74 +1,34 @@
import datetime as dt
from collections import namedtuple
from typing import Optional, List
from redis import Redis
from pytz import utc
from django.core.cache import cache
_TaskCounts = namedtuple(
"_TaskCounts", ["succeeded", "retried", "failed", "total", "earliest_task", "hours"]
)
def dashboard_results(hours: int) -> _TaskCounts:
"""Counts of all task events within the given timeframe."""
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()
succeeded = SucceededTaskSeries()
succeeded_count = succeeded.count(earliest=earliest)
earliest_events += earliest_if_exists(succeeded, earliest)
retried = RetriedTaskSeries()
retried_count = retried.count(earliest=earliest)
earliest_events += earliest_if_exists(retried, earliest)
failed = FailedTaskSeries()
failed_count = failed.count(earliest=earliest)
earliest_events += earliest_if_exists(failed, earliest)
return _TaskCounts(
succeeded=succeeded_count,
retried=retried_count,
failed=failed_count,
total=succeeded_count + retried_count + failed_count,
earliest_task=min(earliest_events) if earliest_events else None,
hours=hours,
)
from django_redis import get_redis_connection
class EventSeries:
"""Base class for recording and analysing a series of events.
"""API for recording and analysing a series of events."""
This class must be inherited from and the child class must define KEY_ID.
"""
_ROOT_KEY = "ALLIANCEAUTH_EVENT_SERIES"
_ROOT_KEY = "ALLIANCEAUTH_TASK_SERIES"
def __init__(
self,
redis: Redis = None,
) -> None:
if type(self) == EventSeries:
raise TypeError("Can not instantiate base class.")
if not hasattr(self, "KEY_ID"):
raise ValueError("KEY_ID not defined")
self._redis = cache.get_master_client() if not redis else redis
def __init__(self, key_id: str, redis: Redis = None) -> None:
self._redis = get_redis_connection("default") if not redis else redis
if not isinstance(self._redis, Redis):
raise TypeError(
"This class requires a Redis client, but none was provided "
"and the default Django cache backend is not Redis either."
)
self._key_id = str(key_id)
self.clear()
@property
def _key_counter(self):
return f"{self._ROOT_KEY}_{self.KEY_ID}_COUNTER"
return f"{self._ROOT_KEY}_{self._key_id}_COUNTER"
@property
def _key_sorted_set(self):
return f"{self._ROOT_KEY}_{self.KEY_ID}_SORTED_SET"
return f"{self._ROOT_KEY}_{self._key_id}_SORTED_SET"
def add(self, event_time: dt.datetime = None) -> None:
"""Add event.
@@ -133,21 +93,3 @@ class EventSeries:
@staticmethod
def _cast_scores_to_dt(score) -> dt.datetime:
return dt.datetime.fromtimestamp(float(score), tz=utc)
class SucceededTaskSeries(EventSeries):
"""A task has succeeded."""
KEY_ID = "SUCCEEDED"
class RetriedTaskSeries(EventSeries):
"""A task has been retried."""
KEY_ID = "RETRIED"
class FailedTaskSeries(EventSeries):
"""A task has failed."""
KEY_ID = "FAILED"

View File

@@ -1,15 +1,21 @@
from celery.signals import task_failure, task_retry, task_success, worker_ready
from celery.signals import (
task_failure,
task_internal_error,
task_retry,
task_success,
worker_ready
)
from django.conf import settings
from .event_series import FailedTaskSeries, RetriedTaskSeries, SucceededTaskSeries
from .counters import failed_tasks, retried_tasks, succeeded_tasks
def reset_counters():
"""Reset all counters for the celery status."""
SucceededTaskSeries().clear()
FailedTaskSeries().clear()
RetriedTaskSeries().clear()
succeeded_tasks.clear()
failed_tasks.clear()
retried_tasks.clear()
def is_enabled() -> bool:
@@ -27,16 +33,22 @@ def reset_counters_when_celery_restarted(*args, **kwargs):
@task_success.connect
def record_task_succeeded(*args, **kwargs):
if is_enabled():
SucceededTaskSeries().add()
succeeded_tasks.add()
@task_retry.connect
def record_task_retried(*args, **kwargs):
if is_enabled():
RetriedTaskSeries().add()
retried_tasks.add()
@task_failure.connect
def record_task_failed(*args, **kwargs):
if is_enabled():
FailedTaskSeries().add()
failed_tasks.add()
@task_internal_error.connect
def record_task_internal_error(*args, **kwargs):
if is_enabled():
failed_tasks.add()

View File

@@ -0,0 +1,51 @@
import datetime as dt
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,
)
class TestDashboardResults(TestCase):
def test_should_return_counts_for_given_timeframe_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
self.assertEqual(results.succeeded, 3)
self.assertEqual(results.retried, 2)
self.assertEqual(results.failed, 1)
self.assertEqual(results.total, 6)
self.assertEqual(results.earliest_task, earliest_task)
def test_should_work_with_no_data(self):
# given
succeeded_tasks.clear()
retried_tasks.clear()
failed_tasks.clear()
# when
results = dashboard_results(hours=1)
# then
self.assertEqual(results.succeeded, 0)
self.assertEqual(results.retried, 0)
self.assertEqual(results.failed, 0)
self.assertEqual(results.total, 0)
self.assertIsNone(results.earliest_task)

View File

@@ -4,46 +4,13 @@ from pytz import utc
from django.test import TestCase
from django.utils.timezone import now
from allianceauth.authentication.task_statistics.event_series import (
EventSeries,
FailedTaskSeries,
RetriedTaskSeries,
SucceededTaskSeries,
dashboard_results,
)
from allianceauth.authentication.task_statistics.event_series import EventSeries
class TestEventSeries(TestCase):
"""Testing EventSeries class."""
class IncompleteEvents(EventSeries):
"""Child class without KEY ID"""
class MyEventSeries(EventSeries):
KEY_ID = "TEST"
def test_should_create_object(self):
# when
events = self.MyEventSeries()
# then
self.assertIsInstance(events, self.MyEventSeries)
def test_should_abort_when_redis_client_invalid(self):
with self.assertRaises(TypeError):
self.MyEventSeries(redis="invalid")
def test_should_not_allow_instantiation_of_base_class(self):
with self.assertRaises(TypeError):
EventSeries()
def test_should_not_allow_creating_child_class_without_key_id(self):
with self.assertRaises(ValueError):
self.IncompleteEvents()
def test_should_add_event(self):
# given
events = self.MyEventSeries()
events.clear()
events = EventSeries("dummy")
# when
events.add()
# then
@@ -53,8 +20,7 @@ class TestEventSeries(TestCase):
def test_should_add_event_with_specified_time(self):
# given
events = self.MyEventSeries()
events.clear()
events = EventSeries("dummy")
my_time = dt.datetime(2021, 11, 1, 12, 15, tzinfo=utc)
# when
events.add(my_time)
@@ -65,8 +31,7 @@ class TestEventSeries(TestCase):
def test_should_count_events(self):
# given
events = self.MyEventSeries()
events.clear()
events = EventSeries("dummy")
events.add()
events.add()
# when
@@ -76,8 +41,7 @@ class TestEventSeries(TestCase):
def test_should_count_zero(self):
# given
events = self.MyEventSeries()
events.clear()
events = EventSeries("dummy")
# when
result = events.count()
# then
@@ -85,8 +49,7 @@ class TestEventSeries(TestCase):
def test_should_count_events_within_timeframe_1(self):
# given
events = self.MyEventSeries()
events.clear()
events = EventSeries("dummy")
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
@@ -101,8 +64,7 @@ class TestEventSeries(TestCase):
def test_should_count_events_within_timeframe_2(self):
# given
events = self.MyEventSeries()
events.clear()
events = EventSeries("dummy")
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
@@ -114,8 +76,7 @@ class TestEventSeries(TestCase):
def test_should_count_events_within_timeframe_3(self):
# given
events = self.MyEventSeries()
events.clear()
events = EventSeries("dummy")
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
@@ -127,8 +88,7 @@ class TestEventSeries(TestCase):
def test_should_clear_events(self):
# given
events = self.MyEventSeries()
events.clear()
events = EventSeries("dummy")
events.add()
events.add()
# when
@@ -138,8 +98,7 @@ class TestEventSeries(TestCase):
def test_should_return_date_of_first_event(self):
# given
events = self.MyEventSeries()
events.clear()
events = EventSeries("dummy")
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
@@ -151,8 +110,7 @@ class TestEventSeries(TestCase):
def test_should_return_date_of_first_event_with_range(self):
# given
events = self.MyEventSeries()
events.clear()
events = EventSeries("dummy")
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
@@ -166,57 +124,10 @@ class TestEventSeries(TestCase):
def test_should_return_all_events(self):
# given
events = self.MyEventSeries()
events.clear()
events = EventSeries("dummy")
events.add()
events.add()
# when
results = events.all()
# then
self.assertEqual(len(results), 2)
class TestDashboardResults(TestCase):
def test_should_return_counts_for_given_timeframe_only(self):
# given
earliest_task = now() - dt.timedelta(minutes=15)
succeeded = SucceededTaskSeries()
succeeded.clear()
succeeded.add(now() - dt.timedelta(hours=1, seconds=1))
succeeded.add(earliest_task)
succeeded.add()
succeeded.add()
retried = RetriedTaskSeries()
retried.clear()
retried.add(now() - dt.timedelta(hours=1, seconds=1))
retried.add(now() - dt.timedelta(seconds=30))
retried.add()
failed = FailedTaskSeries()
failed.clear()
failed.add(now() - dt.timedelta(hours=1, seconds=1))
failed.add()
# when
results = dashboard_results(hours=1)
# then
self.assertEqual(results.succeeded, 3)
self.assertEqual(results.retried, 2)
self.assertEqual(results.failed, 1)
self.assertEqual(results.total, 6)
self.assertEqual(results.earliest_task, earliest_task)
def test_should_work_with_no_data(self):
# given
succeeded = SucceededTaskSeries()
succeeded.clear()
retried = RetriedTaskSeries()
retried.clear()
failed = FailedTaskSeries()
failed.clear()
# when
results = dashboard_results(hours=1)
# then
self.assertEqual(results.succeeded, 0)
self.assertEqual(results.retried, 0)
self.assertEqual(results.failed, 0)
self.assertEqual(results.total, 0)
self.assertIsNone(results.earliest_task)

View File

@@ -4,10 +4,10 @@ from celery.exceptions import Retry
from django.test import TestCase, override_settings
from allianceauth.authentication.task_statistics.event_series import (
FailedTaskSeries,
RetriedTaskSeries,
SucceededTaskSeries,
from allianceauth.authentication.task_statistics.counters import (
failed_tasks,
retried_tasks,
succeeded_tasks,
)
from allianceauth.authentication.task_statistics.signals import (
reset_counters,
@@ -17,15 +17,16 @@ 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
events = SucceededTaskSeries()
events.clear()
succeeded_tasks.clear()
retried_tasks.clear()
failed_tasks.clear()
# when
with patch(
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
@@ -33,12 +34,15 @@ class TestTaskSignals(TestCase):
mock_update.return_value = None
update_character.delay(1)
# then
self.assertEqual(events.count(), 1)
self.assertEqual(succeeded_tasks.count(), 1)
self.assertEqual(retried_tasks.count(), 0)
self.assertEqual(failed_tasks.count(), 0)
def test_should_record_retried_task(self):
# given
events = RetriedTaskSeries()
events.clear()
succeeded_tasks.clear()
retried_tasks.clear()
failed_tasks.clear()
# when
with patch(
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
@@ -46,12 +50,15 @@ class TestTaskSignals(TestCase):
mock_update.side_effect = Retry
update_character.delay(1)
# then
self.assertEqual(events.count(), 1)
self.assertEqual(succeeded_tasks.count(), 0)
self.assertEqual(failed_tasks.count(), 0)
self.assertEqual(retried_tasks.count(), 1)
def test_should_record_failed_task(self):
# given
events = FailedTaskSeries()
events.clear()
succeeded_tasks.clear()
retried_tasks.clear()
failed_tasks.clear()
# when
with patch(
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
@@ -59,28 +66,21 @@ class TestTaskSignals(TestCase):
mock_update.side_effect = RuntimeError
update_character.delay(1)
# then
self.assertEqual(events.count(), 1)
self.assertEqual(succeeded_tasks.count(), 0)
self.assertEqual(retried_tasks.count(), 0)
self.assertEqual(failed_tasks.count(), 1)
@override_settings(ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED=False)
class TestResetCounters(TestCase):
def test_should_reset_counters(self):
# given
succeeded = SucceededTaskSeries()
succeeded.clear()
succeeded.add()
retried = RetriedTaskSeries()
retried.clear()
retried.add()
failed = FailedTaskSeries()
failed.clear()
failed.add()
succeeded_tasks.add()
retried_tasks.add()
failed_tasks.add()
# when
reset_counters()
# then
self.assertEqual(succeeded.count(), 0)
self.assertEqual(retried.count(), 0)
self.assertEqual(failed.count(), 0)
self.assertEqual(succeeded_tasks.count(), 0)
self.assertEqual(retried_tasks.count(), 0)
self.assertEqual(failed_tasks.count(), 0)
class TestIsEnabled(TestCase):

View File

@@ -7,8 +7,8 @@
<form class="form-signin" role="form" action="" method="POST">
{% csrf_token %}
{{ form|bootstrap }}
<br/>
<br>
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Submit" %}</button>
<br/>
<br>
</form>
{% endblock %}

View File

@@ -1,4 +1,17 @@
from django.db.models.signals import (
m2m_changed,
post_save,
pre_delete,
pre_save
)
from django.urls import reverse
from unittest import mock
MODULE_PATH = 'allianceauth.authentication'
def patch(target, *args, **kwargs):
return mock.patch(f'{MODULE_PATH}{target}', *args, **kwargs)
def get_admin_change_view_url(obj: object) -> str:

View File

@@ -2,6 +2,8 @@ from bs4 import BeautifulSoup
from urllib.parse import quote
from unittest.mock import patch, MagicMock
from django_webtest import WebTest
from django.contrib.admin.sites import AdminSite
from django.contrib.auth.models import Group
from django.test import TestCase, RequestFactory, Client
@@ -276,10 +278,10 @@ class TestOwnershipRecordAdmin(TestCaseWithTestData):
class TestStateAdmin(TestCaseWithTestData):
fixtures = ["disable_analytics"]
def setUp(self):
self.modeladmin = StateAdmin(
model=User, admin_site=AdminSite()
)
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.modeladmin = StateAdmin(model=User, admin_site=AdminSite())
def test_change_view_loads_normally(self):
User.objects.create_superuser(
@@ -543,7 +545,74 @@ class TestUserAdmin(TestCaseWithTestData):
self.assertEqual(response.status_code, expected)
class TestStateAdminChangeFormSuperuserExclusiveEdits(WebTest):
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.super_admin = User.objects.create_superuser("super_admin")
cls.staff_admin = User.objects.create_user("staff_admin")
cls.staff_admin.is_staff = True
cls.staff_admin.save()
cls.staff_admin = AuthUtils.add_permissions_to_user_by_name(
[
"authentication.add_state",
"authentication.change_state",
"authentication.view_state",
],
cls.staff_admin
)
cls.superuser_exclusive_fields = ["permissions",]
def test_should_show_all_fields_to_superuser_for_add(self):
# given
self.app.set_user(self.super_admin)
page = self.app.get("/admin/authentication/state/add/")
# when
form = page.forms["state_form"]
# then
for field in self.superuser_exclusive_fields:
with self.subTest(field=field):
self.assertIn(field, form.fields)
def test_should_not_show_all_fields_to_staff_admins_for_add(self):
# given
self.app.set_user(self.staff_admin)
page = self.app.get("/admin/authentication/state/add/")
# when
form = page.forms["state_form"]
# then
for field in self.superuser_exclusive_fields:
with self.subTest(field=field):
self.assertNotIn(field, form.fields)
def test_should_show_all_fields_to_superuser_for_change(self):
# given
self.app.set_user(self.super_admin)
state = AuthUtils.get_member_state()
page = self.app.get(f"/admin/authentication/state/{state.pk}/change/")
# when
form = page.forms["state_form"]
# then
for field in self.superuser_exclusive_fields:
with self.subTest(field=field):
self.assertIn(field, form.fields)
def test_should_not_show_all_fields_to_staff_admin_for_change(self):
# given
self.app.set_user(self.staff_admin)
state = AuthUtils.get_member_state()
page = self.app.get(f"/admin/authentication/state/{state.pk}/change/")
# when
form = page.forms["state_form"]
# then
for field in self.superuser_exclusive_fields:
with self.subTest(field=field):
self.assertNotIn(field, form.fields)
class TestUserAdminChangeForm(TestCase):
fixtures = ["disable_analytics"]
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
@@ -552,7 +621,7 @@ class TestUserAdminChangeForm(TestCase):
def test_should_show_groups_available_to_user_with_blue_state_only(self):
# given
superuser = User.objects.create_superuser("Super")
user = AuthUtils.create_user("Bruce Wayne")
user = AuthUtils.create_user("bruce_wayne")
character = AuthUtils.add_main_character_2(
user,
name="Bruce Wayne",
@@ -579,6 +648,126 @@ class TestUserAdminChangeForm(TestCase):
self.assertSetEqual(group_ids, {group_1.pk, group_2.pk})
class TestUserAdminChangeFormSuperuserExclusiveEdits(WebTest):
fixtures = ["disable_analytics"]
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.super_admin = User.objects.create_superuser("super_admin")
cls.staff_admin = User.objects.create_user("staff_admin")
cls.staff_admin.is_staff = True
cls.staff_admin.save()
cls.staff_admin = AuthUtils.add_permissions_to_user_by_name(
[
"auth.change_user",
"auth.view_user",
"authentication.change_user",
"authentication.change_userprofile",
"authentication.view_user"
],
cls.staff_admin
)
cls.superuser_exclusive_fields = [
"is_staff", "is_superuser", "user_permissions"
]
def setUp(self) -> None:
self.user = AuthUtils.create_user("bruce_wayne")
def test_should_show_all_fields_to_superuser_for_change(self):
# given
self.app.set_user(self.super_admin)
page = self.app.get(f"/admin/authentication/user/{self.user.pk}/change/")
# when
form = page.forms["user_form"]
# then
for field in self.superuser_exclusive_fields:
with self.subTest(field=field):
self.assertIn(field, form.fields)
def test_should_not_show_all_fields_to_staff_admin_for_change(self):
# given
self.app.set_user(self.staff_admin)
page = self.app.get(f"/admin/authentication/user/{self.user.pk}/change/")
# when
form = page.forms["user_form"]
# then
for field in self.superuser_exclusive_fields:
with self.subTest(field=field):
self.assertNotIn(field, form.fields)
def test_should_allow_super_admin_to_add_restricted_group_to_user(self):
# given
self.app.set_user(self.super_admin)
group_restricted = Group.objects.create(name="restricted group")
group_restricted.authgroup.restricted = True
group_restricted.authgroup.save()
page = self.app.get(f"/admin/authentication/user/{self.user.pk}/change/")
form = page.forms["user_form"]
# when
form["groups"].select_multiple(texts=["restricted group"])
response = form.submit("save")
# then
self.assertEqual(response.status_code, 302)
self.user.refresh_from_db()
self.assertIn(
"restricted group", self.user.groups.values_list("name", flat=True)
)
def test_should_not_allow_staff_admin_to_add_restricted_group_to_user(self):
# given
self.app.set_user(self.staff_admin)
group_restricted = Group.objects.create(name="restricted group")
group_restricted.authgroup.restricted = True
group_restricted.authgroup.save()
page = self.app.get(f"/admin/authentication/user/{self.user.pk}/change/")
form = page.forms["user_form"]
# when
form["groups"].select_multiple(texts=["restricted group"])
response = form.submit("save")
# then
self.assertEqual(response.status_code, 200)
self.assertIn(
"You are not allowed to add or remove these restricted groups",
response.text
)
def test_should_not_allow_staff_admin_to_remove_restricted_group_from_user(self):
# given
self.app.set_user(self.staff_admin)
group_restricted = Group.objects.create(name="restricted group")
group_restricted.authgroup.restricted = True
group_restricted.authgroup.save()
self.user.groups.add(group_restricted)
page = self.app.get(f"/admin/authentication/user/{self.user.pk}/change/")
form = page.forms["user_form"]
# when
form["groups"].select_multiple(texts=[])
response = form.submit("save")
# then
self.assertEqual(response.status_code, 200)
self.assertIn(
"You are not allowed to add or remove these restricted groups",
response.text
)
def test_should_allow_staff_admin_to_add_normal_group_to_user(self):
# given
self.app.set_user(self.super_admin)
Group.objects.create(name="normal group")
page = self.app.get(f"/admin/authentication/user/{self.user.pk}/change/")
form = page.forms["user_form"]
# when
form["groups"].select_multiple(texts=["normal group"])
response = form.submit("save")
# then
self.assertEqual(response.status_code, 302)
self.user.refresh_from_db()
self.assertIn("normal group", self.user.groups.values_list("name", flat=True))
class TestMakeServicesHooksActions(TestCaseWithTestData):
class MyServicesHookTypeA(ServicesHook):

View File

@@ -0,0 +1,175 @@
from unittest import mock
from allianceauth.authentication.middleware import UserSettingsMiddleware
from unittest.mock import Mock
from django.http import HttpResponse
from django.test.testcases import TestCase
class TestUserSettingsMiddlewareSaveLang(TestCase):
def setUp(self):
self.middleware = UserSettingsMiddleware(HttpResponse)
self.request = Mock()
self.request.headers = {
"User-Agent": "AUTOMATED TEST"
}
self.request.path = '/i18n/setlang/'
self.request.POST = {
'language': 'fr'
}
self.request.user.profile.language = 'de'
self.request.user.is_anonymous = False
self.response = Mock()
self.response.content = 'hello world'
def test_middleware_passthrough(self):
"""
Simply tests the middleware runs cleanly
"""
response = self.middleware.process_response(
self.request,
self.response
)
self.assertEqual(self.response, response)
def test_middleware_save_language_false_anonymous(self):
"""
Ensures the middleware wont change the usersettings
of a non-existent (anonymous) user
"""
self.request.user.is_anonymous = True
response = self.middleware.process_response(
self.request,
self.response
)
self.assertEqual(self.request.user.profile.language, 'de')
self.assertFalse(self.request.user.profile.save.called)
self.assertEqual(self.request.user.profile.save.call_count, 0)
def test_middleware_save_language_new(self):
"""
does the middleware change a language not set in the DB
"""
self.request.user.profile.language = None
response = self.middleware.process_response(
self.request,
self.response
)
self.assertEqual(self.request.user.profile.language, 'fr')
self.assertTrue(self.request.user.profile.save.called)
self.assertEqual(self.request.user.profile.save.call_count, 1)
def test_middleware_save_language_changed(self):
"""
Tests the middleware will change a language setting
"""
response = self.middleware.process_response(
self.request,
self.response
)
self.assertEqual(self.request.user.profile.language, 'fr')
self.assertTrue(self.request.user.profile.save.called)
self.assertEqual(self.request.user.profile.save.call_count, 1)
class TestUserSettingsMiddlewareLoginFlow(TestCase):
def setUp(self):
self.middleware = UserSettingsMiddleware(HttpResponse)
self.request = Mock()
self.request.headers = {
"User-Agent": "AUTOMATED TEST"
}
self.request.path = '/sso/login'
self.request.session = {
'NIGHT_MODE': False
}
self.request.LANGUAGE_CODE = 'en'
self.request.user.profile.language = 'de'
self.request.user.profile.night_mode = True
self.request.user.is_anonymous = False
self.response = Mock()
self.response.content = 'hello world'
def test_middleware_passthrough(self):
"""
Simply tests the middleware runs cleanly
"""
middleware_response = self.middleware.process_response(
self.request,
self.response
)
self.assertEqual(self.response, middleware_response)
def test_middleware_sets_language_cookie_true_no_cookie(self):
"""
tests the middleware will set a cookie, while none is set
"""
self.request.LANGUAGE_CODE = None
middleware_response = self.middleware.process_response(
self.request,
self.response
)
self.assertTrue(middleware_response.set_cookie.called)
self.assertEqual(middleware_response.set_cookie.call_count, 1)
args, kwargs = middleware_response.set_cookie.call_args
self.assertEqual(kwargs['value'], 'de')
def test_middleware_sets_language_cookie_true_wrong_cookie(self):
"""
tests the middleware will set a cookie, while a different value is set
"""
middleware_response = self.middleware.process_response(
self.request,
self.response
)
self.assertTrue(middleware_response.set_cookie.called)
self.assertEqual(middleware_response.set_cookie.call_count, 1)
args, kwargs = middleware_response.set_cookie.call_args
self.assertEqual(kwargs['value'], 'de')
def test_middleware_sets_language_cookie_false_anonymous(self):
"""
ensures the middleware wont set a value for a non existent user (anonymous)
"""
self.request.user.is_anonymous = True
middleware_response = self.middleware.process_response(
self.request,
self.response
)
self.assertFalse = middleware_response.set_cookie.called
self.assertEqual(middleware_response.set_cookie.call_count, 0)
def test_middleware_sets_language_cookie_false_already_set(self):
"""
tests the middleware skips setting the cookie, if its already set correctly
"""
self.request.user.profile.language = 'en'
middleware_response = self.middleware.process_response(
self.request,
self.response
)
self.assertFalse = middleware_response.set_cookie.called
self.assertEqual(middleware_response.set_cookie.call_count, 0)
def test_middleware_sets_night_mode_not_set(self):
"""
tests the middleware will set night_mode if not set
"""
self.request.session = {}
response = self.middleware.process_response(
self.request,
self.response
)
self.assertEqual(self.request.session["NIGHT_MODE"], True)
def test_middleware_sets_night_mode_set(self):
"""
tests the middleware will set night_mode if set.
"""
response = self.middleware.process_response(
self.request,
self.response
)
self.assertEqual(self.request.session["NIGHT_MODE"], True)

View File

@@ -0,0 +1,94 @@
from allianceauth.authentication.models import User, UserProfile
from allianceauth.eveonline.models import (
EveCharacter,
EveCorporationInfo,
EveAllianceInfo
)
from django.db.models.signals import (
pre_save,
post_save,
pre_delete,
m2m_changed
)
from allianceauth.tests.auth_utils import AuthUtils
from django.test.testcases import TestCase
from unittest.mock import Mock
from . import patch
class TestUserProfileSignals(TestCase):
def setUp(self):
state = AuthUtils.get_member_state()
self.char = EveCharacter.objects.create(
character_id='1234',
character_name='test character',
corporation_id='2345',
corporation_name='test corp',
corporation_ticker='tickr',
alliance_id='3456',
alliance_name='alliance name',
)
self.alliance = EveAllianceInfo.objects.create(
alliance_id='3456',
alliance_name='alliance name',
alliance_ticker='TIKR',
executor_corp_id='2345',
)
self.corp = EveCorporationInfo.objects.create(
corporation_id='2345',
corporation_name='corp name',
corporation_ticker='TIKK',
member_count=10,
alliance=self.alliance,
)
state.member_alliances.add(self.alliance)
state.member_corporations.add(self.corp)
self.member = AuthUtils.create_user('test user')
self.member.profile.main_character = self.char
self.member.profile.save()
@patch('.signals.create_required_models')
def test_create_required_models_triggered_true(
self, create_required_models):
"""
Create a User object here,
to generate UserProfile models
"""
post_save.connect(create_required_models, sender=User)
AuthUtils.create_user('test_create_required_models_triggered')
self.assertTrue = create_required_models.called
self.assertEqual(create_required_models.call_count, 1)
user = User.objects.get(username='test_create_required_models_triggered')
self.assertIsNot(UserProfile.objects.get(user=user), False)
@patch('.signals.create_required_models')
def test_create_required_models_triggered_false(
self, create_required_models):
"""
Only call a User object Update here,
which does not need to generate UserProfile models
"""
post_save.connect(create_required_models, sender=User)
char = EveCharacter.objects.create(
character_id='1266',
character_name='test character2',
corporation_id='2345',
corporation_name='test corp',
corporation_ticker='tickr',
alliance_id='3456',
alliance_name='alliance name',
)
self.member.profile.main_character = char
self.member.profile.save()
self.assertTrue = create_required_models.called
self.assertEqual(create_required_models.call_count, 0)
self.assertIsNot(UserProfile.objects.get(user=self.member), False)

View File

@@ -1,5 +1,4 @@
from django.conf.urls import url
from django.contrib.auth.decorators import login_required
from django.urls import path
from django.views.generic.base import TemplateView
from . import views
@@ -7,21 +6,21 @@ from . import views
app_name = 'authentication'
urlpatterns = [
url(r'^$', views.index, name='index'),
url(
r'^account/login/$',
path('', views.index, name='index'),
path(
'account/login/',
TemplateView.as_view(template_name='public/login.html'),
name='login'
),
url(
r'^account/characters/main/$',
path(
'account/characters/main/',
views.main_character_change,
name='change_main_character'
),
url(
r'^account/characters/add/$',
path(
'account/characters/add/',
views.add_character,
name='add_character'
),
url(r'^dashboard/$', views.dashboard, name='dashboard'),
path('dashboard/', views.dashboard, name='dashboard'),
]

View File

@@ -1 +0,0 @@
default_app_config = 'allianceauth.corputils.apps.CorpUtilsConfig'

View File

@@ -1,5 +1,5 @@
from allianceauth.services.hooks import MenuItemHook, UrlHook
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from allianceauth import hooks
from allianceauth.corputils import urls

View File

@@ -1,12 +1,11 @@
from django.conf.urls import url
from django.urls import path
from . import views
app_name = 'corputils'
urlpatterns = [
url(r'^$', views.corpstats_view, name='view'),
url(r'^add/$', views.corpstats_add, name='add'),
url(r'^(?P<corp_id>(\d)*)/$', views.corpstats_view, name='view_corp'),
url(r'^(?P<corp_id>(\d)+)/update/$', views.corpstats_update, name='update'),
url(r'^search/$', views.corpstats_search, name='search'),
]
path('', views.corpstats_view, name='view'),
path('add/', views.corpstats_add, name='add'),
path('<int:corp_id>/', views.corpstats_view, name='view_corp'),
path('<int:corp_id>/update/', views.corpstats_update, name='update'),
path('search/', views.corpstats_search, name='search'),
]

View File

@@ -6,7 +6,7 @@ from django.contrib.auth.decorators import login_required, permission_required,
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
from django.db import IntegrityError
from django.shortcuts import render, redirect, get_object_or_404
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from esi.decorators import token_required
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo

View File

@@ -1 +0,0 @@
default_app_config = 'allianceauth.eveonline.apps.EveonlineConfig'

View File

@@ -1 +0,0 @@
default_app_config = 'allianceauth.eveonline.autogroups.apps.EveAutogroupsConfig'

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.10 on 2022-01-05 18:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('eveonline', '0015_factions'),
]
operations = [
migrations.AlterField(
model_name='evecharacter',
name='character_name',
field=models.CharField(db_index=True, max_length=254),
),
]

View File

@@ -25,6 +25,8 @@ DOOMHEIM_CORPORATION_ID = 1000001
class EveFactionInfo(models.Model):
"""A faction in Eve Online."""
faction_id = models.PositiveIntegerField(unique=True, db_index=True)
faction_name = models.CharField(max_length=254, unique=True)
@@ -66,6 +68,8 @@ class EveFactionInfo(models.Model):
class EveAllianceInfo(models.Model):
"""An alliance in Eve Online."""
alliance_id = models.PositiveIntegerField(unique=True)
alliance_name = models.CharField(max_length=254, unique=True)
alliance_ticker = models.CharField(max_length=254)
@@ -132,6 +136,8 @@ class EveAllianceInfo(models.Model):
class EveCorporationInfo(models.Model):
"""A corporation in Eve Online."""
corporation_id = models.PositiveIntegerField(unique=True)
corporation_name = models.CharField(max_length=254, unique=True)
corporation_ticker = models.CharField(max_length=254)
@@ -195,9 +201,10 @@ class EveCorporationInfo(models.Model):
class EveCharacter(models.Model):
"""Character in Eve Online"""
"""A character in Eve Online."""
character_id = models.PositiveIntegerField(unique=True)
character_name = models.CharField(max_length=254, unique=True)
character_name = models.CharField(max_length=254, db_index=True)
corporation_id = models.PositiveIntegerField()
corporation_name = models.CharField(max_length=254)
corporation_ticker = models.CharField(max_length=5)

View File

@@ -40,7 +40,7 @@ def update_character(character_id: int) -> None:
def run_model_update():
"""Update all alliances, corporations and characters from ESI"""
# update existing corp models
#update existing corp models
for corp in EveCorporationInfo.objects.all().values('corporation_id'):
update_corp.apply_async(args=[corp['corporation_id']], priority=TASK_PRIORITY)

View File

@@ -1 +0,0 @@
default_app_config = 'allianceauth.fleetactivitytracking.apps.FatConfig'

View File

@@ -1,5 +1,5 @@
from . import urls
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from allianceauth import hooks
from allianceauth.services.hooks import MenuItemHook, UrlHook

View File

@@ -1,5 +1,5 @@
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
class FatlinkForm(forms.Form):

View File

@@ -21,7 +21,7 @@
<form class="form-signin" role="form" action="" method="POST">
{% csrf_token %}
{{ form|bootstrap }}
<br/>
<br>
<button class="btn btn-lg btn-primary btn-block" type="submit" name="submit_fat">{% translate "Create fatlink" %}</button>
</form>
</div>

View File

@@ -1,30 +1,30 @@
from django.conf.urls import url
from django.urls import path
from . import views
app_name = 'fleetactivitytracking'
urlpatterns = [
# FleetActivityTracking (FAT)
url(r'^$', views.fatlink_view, name='view'),
url(r'^statistics/$', views.fatlink_statistics_view, name='statistics'),
url(r'^statistics/corp/(\w+)$', views.fatlink_statistics_corp_view,
path('', views.fatlink_view, name='view'),
path('statistics/', views.fatlink_statistics_view, name='statistics'),
path('statistics/corp/<int:corpid>/', views.fatlink_statistics_corp_view,
name='statistics_corp'),
url(r'^statistics/corp/(?P<corpid>\w+)/(?P<year>[0-9]+)/(?P<month>[0-9]+)/',
path('statistics/corp/<int:corpid>/<int:year>/<int:month>/',
views.fatlink_statistics_corp_view,
name='statistics_corp_month'),
url(r'^statistics/(?P<year>[0-9]+)/(?P<month>[0-9]+)/$', views.fatlink_statistics_view,
path('statistics/<int:year>/<int:month>/', views.fatlink_statistics_view,
name='statistics_month'),
url(r'^user/statistics/$', views.fatlink_personal_statistics_view,
path('user/statistics/', views.fatlink_personal_statistics_view,
name='personal_statistics'),
url(r'^user/statistics/(?P<year>[0-9]+)/$', views.fatlink_personal_statistics_view,
path('user/statistics/<int:year>/', views.fatlink_personal_statistics_view,
name='personal_statistics_year'),
url(r'^user/statistics/(?P<year>[0-9]+)/(?P<month>[0-9]+)/$',
path('user/statistics/<int:year>/<int:month>/',
views.fatlink_monthly_personal_statistics_view,
name='personal_statistics_month'),
url(r'^user/(?P<char_id>[0-9]+)/statistics/(?P<year>[0-9]+)/(?P<month>[0-9]+)/$',
path('user/<int:char_id>/statistics/<int:year>/<int:month>/',
views.fatlink_monthly_personal_statistics_view,
name='user_statistics_month'),
url(r'^create/$', views.create_fatlink_view, name='create'),
url(r'^modify/(?P<fat_hash>[a-zA-Z0-9_-]+)/$', views.modify_fatlink_view, name='modify'),
url(r'^link/(?P<fat_hash>[a-zA-Z0-9]+)/$', views.click_fatlink_view, name='click'),
path('create/', views.create_fatlink_view, name='create'),
path('modify/<str:fat_hash>/', views.modify_fatlink_view, name='modify'),
path('link/<str:fat_hash>/', views.click_fatlink_view, name='click'),
]

View File

@@ -10,7 +10,7 @@ from django.contrib.auth.models import User
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.shortcuts import render, redirect, get_object_or_404, Http404
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from esi.decorators import token_required
from allianceauth.eveonline.providers import provider
from .forms import FatlinkForm

View File

@@ -1 +0,0 @@
default_app_config = 'allianceauth.groupmanagement.apps.GroupManagementConfig'

View File

@@ -1,19 +1,20 @@
from django import forms
from django.apps import apps
from django.contrib.auth.models import Permission
from django.contrib import admin
from django.contrib.auth.models import Group as BaseGroup, User
from django.core.exceptions import ValidationError
from django.contrib.auth.models import Group as BaseGroup
from django.contrib.auth.models import Permission, User
from django.db.models import Count
from django.db.models.functions import Lower
from django.db.models.signals import pre_save, post_save, pre_delete, \
post_delete, m2m_changed
from django.db.models.signals import (
m2m_changed,
post_delete,
post_save,
pre_delete,
pre_save
)
from django.dispatch import receiver
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from .models import AuthGroup, ReservedGroupName
from .models import GroupRequest
from .forms import GroupAdminForm, ReservedGroupNameAdminForm
from .models import AuthGroup, GroupRequest, ReservedGroupName
if 'eve_autogroups' in apps.app_configs:
_has_auto_groups = True
@@ -28,10 +29,12 @@ class AuthGroupInlineAdmin(admin.StackedInline):
'description',
'group_leaders',
'group_leader_groups',
'states', 'internal',
'states',
'internal',
'hidden',
'open',
'public'
'public',
'restricted',
)
verbose_name_plural = 'Auth Settings'
verbose_name = ''
@@ -50,6 +53,11 @@ class AuthGroupInlineAdmin(admin.StackedInline):
def has_change_permission(self, request, obj=None):
return request.user.has_perm('auth.change_group')
def get_readonly_fields(self, request, obj=None):
if not request.user.is_superuser:
return self.readonly_fields + ("restricted",)
return self.readonly_fields
if _has_auto_groups:
class IsAutoGroupFilter(admin.SimpleListFilter):
@@ -96,17 +104,6 @@ class HasLeaderFilter(admin.SimpleListFilter):
return queryset
class GroupAdminForm(forms.ModelForm):
def clean_name(self):
my_name = self.cleaned_data['name']
if ReservedGroupName.objects.filter(name__iexact=my_name).exists():
raise ValidationError(
_("This name has been reserved and can not be used for groups."),
code='reserved_name'
)
return my_name
class GroupAdmin(admin.ModelAdmin):
form = GroupAdminForm
list_select_related = ('authgroup',)
@@ -143,17 +140,14 @@ class GroupAdmin(admin.ModelAdmin):
def _description(self, obj):
return obj.authgroup.description
@admin.display(description="Members", ordering="member_count")
def _member_count(self, obj):
return obj.member_count
_member_count.short_description = 'Members'
_member_count.admin_order_field = 'member_count'
@admin.display(boolean=True)
def has_leader(self, obj):
return obj.authgroup.group_leaders.exists() or obj.authgroup.group_leader_groups.exists()
has_leader.boolean = True
def _properties(self, obj):
properties = list()
if _has_auto_groups and (
@@ -172,11 +166,10 @@ class GroupAdmin(admin.ModelAdmin):
properties.append('Public')
if not properties:
properties.append('Default')
if obj.authgroup.restricted:
properties.append('Restricted')
return properties
_properties.short_description = "properties"
filter_horizontal = ('permissions',)
inlines = (AuthGroupInlineAdmin,)
@@ -192,6 +185,11 @@ class GroupAdmin(admin.ModelAdmin):
ag_instance.save()
formset.save()
def get_readonly_fields(self, request, obj=None):
if not request.user.is_superuser:
return self.readonly_fields + ("permissions",)
return self.readonly_fields
class Group(BaseGroup):
class Meta:
@@ -216,33 +214,10 @@ class GroupRequestAdmin(admin.ModelAdmin):
'leave_request',
)
@admin.display(boolean=True, description="is leave request")
def _leave_request(self, obj) -> True:
return obj.leave_request
_leave_request.short_description = 'is leave request'
_leave_request.boolean = True
class ReservedGroupNameAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['created_by'].initial = self.current_user.username
self.fields['created_at'].initial = _("(auto)")
created_by = forms.CharField(disabled=True)
created_at = forms.CharField(disabled=True)
def clean_name(self):
my_name = self.cleaned_data['name'].lower()
if Group.objects.filter(name__iexact=my_name).exists():
raise ValidationError(
_("There already exists a group with that name."), code='already_exists'
)
return my_name
def clean_created_at(self):
return now()
@admin.register(ReservedGroupName)
class ReservedGroupNameAdmin(admin.ModelAdmin):

View File

@@ -1,4 +1,4 @@
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from allianceauth.services.hooks import MenuItemHook, UrlHook
from allianceauth import hooks

View File

@@ -0,0 +1,39 @@
from django import forms
from django.contrib.auth.models import Group
from django.core.exceptions import ValidationError
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from .models import ReservedGroupName
class GroupAdminForm(forms.ModelForm):
def clean_name(self):
my_name = self.cleaned_data['name']
if ReservedGroupName.objects.filter(name__iexact=my_name).exists():
raise ValidationError(
_("This name has been reserved and can not be used for groups."),
code='reserved_name'
)
return my_name
class ReservedGroupNameAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['created_by'].initial = self.current_user.username
self.fields['created_at'].initial = _("(auto)")
created_by = forms.CharField(disabled=True)
created_at = forms.CharField(disabled=True)
def clean_name(self):
my_name = self.cleaned_data['name'].lower()
if Group.objects.filter(name__iexact=my_name).exists():
raise ValidationError(
_("There already exists a group with that name."), code='already_exists'
)
return my_name
def clean_created_at(self):
return now()

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.10 on 2022-04-08 19:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('groupmanagement', '0018_reservedgroupname'),
]
operations = [
migrations.AddField(
model_name='authgroup',
name='restricted',
field=models.BooleanField(default=False, help_text='Group is restricted. This means that adding or removing users for this group requires a superuser admin.'),
),
]

View File

@@ -13,6 +13,7 @@ from allianceauth.notifications import notify
class GroupRequest(models.Model):
"""Request from a user for joining or leaving a group."""
leave_request = models.BooleanField(default=0)
user = models.ForeignKey(User, on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
@@ -44,6 +45,7 @@ class GroupRequest(models.Model):
class RequestLog(models.Model):
"""Log entry about who joined and left a group and who approved it."""
request_type = models.BooleanField(null=True)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
request_info = models.CharField(max_length=254)
@@ -95,6 +97,7 @@ class AuthGroup(models.Model):
Open - Users are automatically accepted into the group
Not Open - Users requests must be approved before they are added to the group
"""
group = models.OneToOneField(Group, on_delete=models.CASCADE, primary_key=True)
internal = models.BooleanField(
default=True,
@@ -126,6 +129,13 @@ class AuthGroup(models.Model):
"are no longer authenticated."
)
)
restricted = models.BooleanField(
default=False,
help_text=_(
"Group is restricted. This means that adding or removing users "
"for this group requires a superuser admin."
)
)
group_leaders = models.ManyToManyField(
User,
related_name='leads_groups',
@@ -185,6 +195,7 @@ class ReservedGroupName(models.Model):
This enables AA to ignore groups on other services (e.g. Discord) with that name.
"""
name = models.CharField(
_('name'),
max_length=150,

View File

@@ -1,5 +1,7 @@
from unittest.mock import patch
from django_webtest import WebTest
from django.conf import settings
from django.contrib import admin
from django.contrib.admin.sites import AdminSite
@@ -10,8 +12,10 @@ from allianceauth.authentication.models import CharacterOwnership, State
from allianceauth.eveonline.models import (
EveCharacter, EveCorporationInfo, EveAllianceInfo
)
from ..admin import HasLeaderFilter, GroupAdmin, Group
from allianceauth.tests.auth_utils import AuthUtils
from . import get_admin_change_view_url
from ..admin import HasLeaderFilter, GroupAdmin, Group
from ..models import ReservedGroupName
@@ -33,7 +37,6 @@ class MockRequest:
class TestGroupAdmin(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
@@ -468,6 +471,74 @@ class TestGroupAdmin(TestCase):
self.assertFalse(Group.objects.filter(name="new group").exists())
class TestGroupAdminChangeFormSuperuserExclusiveEdits(WebTest):
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.super_admin = User.objects.create_superuser("super_admin")
cls.staff_admin = User.objects.create_user("staff_admin")
cls.staff_admin.is_staff = True
cls.staff_admin.save()
cls.staff_admin = AuthUtils.add_permissions_to_user_by_name(
[
"auth.add_group",
"auth.change_group",
"auth.view_group",
"groupmanagement.add_group",
"groupmanagement.change_group",
"groupmanagement.view_group",
],
cls.staff_admin
)
cls.superuser_exclusive_fields = ["permissions", "authgroup-0-restricted"]
def test_should_show_all_fields_to_superuser_for_add(self):
# given
self.app.set_user(self.super_admin)
page = self.app.get("/admin/groupmanagement/group/add/")
# when
form = page.forms["group_form"]
# then
for field in self.superuser_exclusive_fields:
with self.subTest(field=field):
self.assertIn(field, form.fields)
def test_should_not_show_all_fields_to_staff_admins_for_add(self):
# given
self.app.set_user(self.staff_admin)
page = self.app.get("/admin/groupmanagement/group/add/")
# when
form = page.forms["group_form"]
# then
for field in self.superuser_exclusive_fields:
with self.subTest(field=field):
self.assertNotIn(field, form.fields)
def test_should_show_all_fields_to_superuser_for_change(self):
# given
self.app.set_user(self.super_admin)
group = Group.objects.create(name="Dummy group")
page = self.app.get(f"/admin/groupmanagement/group/{group.pk}/change/")
# when
form = page.forms["group_form"]
# then
for field in self.superuser_exclusive_fields:
with self.subTest(field=field):
self.assertIn(field, form.fields)
def test_should_not_show_all_fields_to_staff_admin_for_change(self):
# given
self.app.set_user(self.staff_admin)
group = Group.objects.create(name="Dummy group")
page = self.app.get(f"/admin/groupmanagement/group/{group.pk}/change/")
# when
form = page.forms["group_form"]
# then
for field in self.superuser_exclusive_fields:
with self.subTest(field=field):
self.assertNotIn(field, form.fields)
class TestReservedGroupNameAdmin(TestCase):
@classmethod
def setUpClass(cls):

View File

@@ -1,51 +1,50 @@
from django.urls import path
from . import views
from django.conf.urls import url
app_name = "groupmanagement"
urlpatterns = [
# groups
url(r"^groups/$", views.groups_view, name="groups"),
url(r"^group/request/join/(\w+)/$", views.group_request_add, name="request_add"),
url(
r"^group/request/leave/(\w+)/$", views.group_request_leave, name="request_leave"
path("groups", views.groups_view, name="groups"),
path("group/request/join/<int:group_id>/", views.group_request_add, name="request_add"),
path(
"group/request/leave/<int:group_id>/", views.group_request_leave, name="request_leave"
),
# group management
url(r"^groupmanagement/requests/$", views.group_management, name="management"),
url(r"^groupmanagement/membership/$", views.group_membership, name="membership"),
url(
r"^groupmanagement/membership/(\w+)/$",
path("groupmanagement/requests/", views.group_management, name="management"),
path("groupmanagement/membership/", views.group_membership, name="membership"),
path(
"groupmanagement/membership/<int:group_id>/",
views.group_membership_list,
name="membership",
),
url(
r"^groupmanagement/membership/(\w+)/audit-log/$",
path(
"groupmanagement/membership/<int:group_id>/audit-log/",
views.group_membership_audit,
name="audit_log",
),
url(
r"^groupmanagement/membership/(\w+)/remove/(\w+)/$",
path(
"groupmanagement/membership/<int:group_id>/remove/<int:user_id>/",
views.group_membership_remove,
name="membership_remove",
),
url(
r"^groupmanagement/request/join/accept/(\w+)/$",
path(
"groupmanagement/request/join/accept/<int:group_request_id>/",
views.group_accept_request,
name="accept_request",
),
url(
r"^groupmanagement/request/join/reject/(\w+)/$",
path(
"groupmanagement/request/join/reject/<int:group_request_id>/",
views.group_reject_request,
name="reject_request",
),
url(
r"^groupmanagement/request/leave/accept/(\w+)/$",
path(
"groupmanagement/request/leave/accept/<int:group_request_id>/",
views.group_leave_accept_request,
name="leave_accept_request",
),
url(
r"^groupmanagement/request/leave/reject/(\w+)/$",
path(
"groupmanagement/request/leave/reject/<int:group_request_id>/",
views.group_leave_reject_request,
name="leave_reject_request",
),

View File

@@ -9,7 +9,7 @@ from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.db.models import Count
from django.http import Http404
from django.shortcuts import render, redirect, get_object_or_404
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from allianceauth.notifications import notify

View File

@@ -1 +0,0 @@
default_app_config = 'allianceauth.hrapplications.apps.HRApplicationsConfig'

View File

@@ -1,4 +1,4 @@
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from allianceauth import hooks
from allianceauth.services.hooks import MenuItemHook, UrlHook

View File

@@ -1,5 +1,5 @@
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
class HRApplicationCommentForm(forms.Form):

View File

@@ -19,8 +19,8 @@
<div cass="text-center">{{ question.help_text }}</div>
{% endif %}
{% for choice in question.choices.all %}
<input type={% if question.multi_select == False %}"radio"{% else %}"checkbox"{% endif %} name="{{ question.pk }}" id="id_{{ question.pk }}" value="{{ choice.choice_text }}" />
<label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br />
<input type={% if question.multi_select == False %}"radio"{% else %}"checkbox"{% endif %} name="{{ question.pk }}" id="id_{{ question.pk }}" value="{{ choice.choice_text }}">
<label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
{% empty %}
<textarea class="form-control" cols="30" id="id_{{ question.pk }}" name="{{ question.pk }}" rows="4"></textarea>
{% endfor %}

View File

@@ -181,7 +181,7 @@
<form class="form-signin" role="form" action={% url 'hrapplications:search' %} method="POST">
{% csrf_token %}
{{ search_form|bootstrap }}
<br/>
<br>
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Search" %}</button>
</form>
</div>

View File

@@ -67,7 +67,7 @@
<form class="form-signin" role="form" action={% url 'hrapplications:search' %} method="POST">
{% csrf_token %}
{{ search_form|bootstrap }}
<br/>
<br>
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Search" %}</button>
</form>
</div>

View File

@@ -140,7 +140,7 @@
<form class="form-signin" role="form" action="" method="POST">
{% csrf_token %}
{{ comment_form|bootstrap }}
<br/>
<br>
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Add Comment" %}</button>
</form>
</div>

View File

@@ -1,31 +1,31 @@
from django.conf.urls import url
from django.urls import path
from . import views
app_name = 'hrapplications'
urlpatterns = [
url(r'^$', views.hr_application_management_view,
path('', views.hr_application_management_view,
name="index"),
url(r'^create/$', views.hr_application_create_view,
path('create/', views.hr_application_create_view,
name="create_view"),
url(r'^create/(\d+)', views.hr_application_create_view,
path('create/<int:form_id>/', views.hr_application_create_view,
name="create_view"),
url(r'^remove/(\w+)', views.hr_application_remove,
path('remove/<int:app_id>/', views.hr_application_remove,
name="remove"),
url(r'^view/(\w+)', views.hr_application_view,
path('view/<int:app_id>/', views.hr_application_view,
name="view"),
url(r'^personal/view/(\w+)', views.hr_application_personal_view,
path('personal/view/<int:app_id>/', views.hr_application_personal_view,
name="personal_view"),
url(r'^personal/removal/(\w+)',
path('personal/removal/<int:app_id>/',
views.hr_application_personal_removal,
name="personal_removal"),
url(r'^approve/(\w+)', views.hr_application_approve,
path('approve/<int:app_id>/', views.hr_application_approve,
name="approve"),
url(r'^reject/(\w+)', views.hr_application_reject,
path('reject/<int:app_id>/', views.hr_application_reject,
name="reject"),
url(r'^search/', views.hr_application_search,
path('search/', views.hr_application_search,
name="search"),
url(r'^mark_in_progress/(\w+)', views.hr_application_mark_in_progress,
path('mark_in_progress/<int:app_id>/', views.hr_application_mark_in_progress,
name="mark_in_progress"),
]
]

View File

@@ -1,9 +1,3 @@
from .core import notify # noqa: F401
default_app_config = 'allianceauth.notifications.apps.NotificationsConfig'
def notify(
user: object, title: str, message: str = None, level: str = 'info'
) -> None:
"""Sends a new notification to user. Convenience function to manager pendant."""
from .models import Notification
Notification.objects.notify_user(user, title, message, level)

View File

@@ -0,0 +1,33 @@
class NotifyApiWrapper:
"""Wrapper to create notify API."""
def __call__(self, *args, **kwargs): # provide old API for backwards compatibility
return self._add_notification(*args, **kwargs)
def danger(self, user: object, title: str, message: str = None) -> None:
"""Add danger notification for user."""
self._add_notification(user, title, message, level="danger")
def info(self, user: object, title: str, message: str = None) -> None:
"""Add info notification for user."""
self._add_notification(user=user, title=title, message=message, level="info")
def success(self, user: object, title: str, message: str = None) -> None:
"""Add success notification for user."""
self._add_notification(user, title, message, level="success")
def warning(self, user: object, title: str, message: str = None) -> None:
"""Add warning notification for user."""
self._add_notification(user, title, message, level="warning")
def _add_notification(
self, user: object, title: str, message: str = None, level: str = "info"
) -> None:
from .models import Notification
Notification.objects.notify_user(
user=user, title=title, message=message, level=level
)
notify = NotifyApiWrapper()

View File

@@ -5,91 +5,34 @@
{% block page_title %}{% translate "Notifications" %}{% endblock %}
{% block content %}
<div class="col-lg-12">
<h1 class="page-header text-center">{% translate "Notifications" %}</h1>
<div class="col-lg-12 container" id="example">
<div class="row">
<div class="col-lg-12">
<div class="panel panel-default">
<div class="panel-heading">
<ul class="nav nav-pills">
<li class="active"><a data-toggle="pill" href="#unread">{% translate "Unread" %}
<b>({{ unread|length }})</b></a></li>
<li><a data-toggle="pill" href="#read">{% translate "Read" %} <b>({{ read|length }})</b></a>
</li>
<div class="pull-right">
<a href="{% url 'notifications:mark_all_read' %}" class="btn btn-primary">{% translate "Mark All Read" %}</a>
<a href="{% url 'notifications:delete_all_read' %}" class="btn btn-danger">{% translate "Delete All Read" %}</a>
</div>
</ul>
</div>
<div class="panel-body">
<div class="tab-content">
<div id="unread" class="tab-pane fade in active">
<div class="table-responsive">
{% if unread %}
<table class="table table-condensed table-hover table-striped">
<tr>
<th class="text-center">{% translate "Timestamp" %}</th>
<th class="text-center">{% translate "Title" %}</th>
<th class="text-center">{% translate "Action" %}</th>
</tr>
{% for notif in unread %}
<tr class="{{ notif.level }}">
<td class="text-center">{{ notif.timestamp }}</td>
<td class="text-center">{{ notif.title }}</td>
<td class="text-center">
<a href="{% url 'notifications:view' notif.id %}" class="btn btn-success" title="View">
<span class="glyphicon glyphicon-eye-open"></span>
</a>
<a href="{% url 'notifications:remove' notif.id %}" class="btn btn-danger" title="Remove">
<span class="glyphicon glyphicon-remove"></span>
</a>
</td>
</tr>
{% endfor %}
</table>
{% else %}
<div class="alert alert-warning text-center">{% translate "No unread notifications." %}</div>
{% endif %}
</div>
</div>
<div id="read" class="tab-pane fade">
<div class="panel-body">
<div class="table-responsive">
{% if read %}
<table class="table table-condensed table-hover table-striped">
<tr>
<th class="text-center">{% translate "Timestamp" %}</th>
<th class="text-center">{% translate "Title" %}</th>
<th class="text-center">{% translate "Action" %}</th>
</tr>
{% for notif in read %}
<tr class="{{ notif.level }}">
<td class="text-center">{{ notif.timestamp }}</td>
<td class="text-center">{{ notif.title }}</td>
<td class="text-center">
<a href="{% url 'notifications:view' notif.id %}" class="btn btn-success" title="View">
<span class="glyphicon glyphicon-eye-open"></span>
</a>
<a href="{% url 'notifications:remove' notif.id %}" class="btn btn-danger" title="remove">
<span class="glyphicon glyphicon-remove"></span>
</a>
</td>
</tr>
{% endfor %}
</table>
{% else %}
<div class="alert alert-warning text-center">{% translate "No read notifications." %}</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
<h1 class="page-header text-center">{% translate "Notifications" %}</h1>
<div class="panel panel-default">
<div class="panel-heading">
<ul class="nav nav-pills">
<li class="active"><a data-toggle="tab" href="#unread">{% translate "Unread" %}<b>({{ unread|length }})</b></a></li>
<li><a data-toggle="tab" href="#read">{% translate "Read" %} <b>({{ read|length }})</b></a></li>
<div class="pull-right">
<a href="{% url 'notifications:mark_all_read' %}" class="btn btn-warning">{% translate "Mark All Read" %}</a>
<a href="{% url 'notifications:delete_all_read' %}" class="btn btn-danger">{% translate "Delete All Read" %}</a>
</div>
</ul>
</div>
<div class="panel-body">
<div class="tab-content">
<div id="unread" class="tab-pane fade in active">
{% include "notifications/list_partial.html" with notifications=unread %}
</div>
<div id="read" class="tab-pane fade">
{% include "notifications/list_partial.html" with notifications=read %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,29 @@
{% load i18n %}
{% if notifications %}
<div class="table-responsive">
<table class="table table-condensed table-hover table-striped">
<tr>
<th class="text-center">{% translate "Timestamp" %}</th>
<th class="text-center">{% translate "Title" %}</th>
<th class="text-center">{% translate "Action" %}</th>
</tr>
{% for notif in notifications %}
<tr class="{{ notif.level }}">
<td class="text-center">{{ notif.timestamp }}</td>
<td class="text-center">{{ notif.title }}</td>
<td class="text-center">
<a href="{% url 'notifications:view' notif.id %}" class="btn btn-primary" title="View">
<span class="glyphicon glyphicon-eye-open"></span>
</a>
<a href="{% url 'notifications:remove' notif.id %}" class="btn btn-danger" title="Remove">
<span class="glyphicon glyphicon-remove"></span>
</a>
</td>
</tr>
{% endfor %}
</table>
</div>
{% else %}
<div class="alert alert-default text-center">{% translate "No notifications." %}</div>
{% endif %}

View File

@@ -5,25 +5,22 @@
{% block page_title %}{% translate "View Notification" %}{% endblock page_title %}
{% block content %}
<h1 class="page-header text-center">
{% translate "View Notification" %}
<div class="text-right">
<a href="{% url 'notifications:list' %}" class="btn btn-primary btn-lg">
<span class="glyphicon glyphicon-arrow-left"></span>
</a>
</div>
</h1>
<div class="col-lg-12">
<h1 class="page-header text-center">
{% translate "View Notification" %}
<div class="text-right">
<a href="{% url 'notifications:list' %}" class="btn btn-primary btn-lg">
<span class="glyphicon glyphicon-arrow-left"></span>
</a>
</div>
</h1>
<div class="col-lg-12 container">
<div class="row">
<div class="col-lg-12">
<div class="panel panel-{{ notif.level }}">
<div class="panel-heading">{{ notif.timestamp }} {{ notif.title }}</div>
<div class="panel-body"><pre>{{ notif.message }}</pre></div>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<div class="panel panel-{{ notif.level }}">
<div class="panel-heading">{{ notif.timestamp }} {{ notif.title }}</div>
<div class="panel-body"><pre>{{ notif.message }}</pre></div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,85 @@
from django.test import TestCase
from allianceauth.tests.auth_utils import AuthUtils
from ..core import NotifyApiWrapper
from ..models import Notification
class TestUserNotificationCount(TestCase):
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.user = AuthUtils.create_user("bruce_wayne")
def test_should_add_danger_notification(self):
# given
notify = NotifyApiWrapper()
# when
notify.danger(user=self.user, title="title", message="message")
# then
obj = Notification.objects.first()
self.assertEqual(obj.user, self.user)
self.assertEqual(obj.title, "title")
self.assertEqual(obj.message, "message")
self.assertEqual(obj.level, Notification.Level.DANGER)
def test_should_add_info_notification(self):
# given
notify = NotifyApiWrapper()
# when
notify.info(user=self.user, title="title", message="message")
# then
obj = Notification.objects.first()
self.assertEqual(obj.user, self.user)
self.assertEqual(obj.title, "title")
self.assertEqual(obj.message, "message")
self.assertEqual(obj.level, Notification.Level.INFO)
def test_should_add_success_notification(self):
# given
notify = NotifyApiWrapper()
# when
notify.success(user=self.user, title="title", message="message")
# then
obj = Notification.objects.first()
self.assertEqual(obj.user, self.user)
self.assertEqual(obj.title, "title")
self.assertEqual(obj.message, "message")
self.assertEqual(obj.level, Notification.Level.SUCCESS)
def test_should_add_warning_notification(self):
# given
notify = NotifyApiWrapper()
# when
notify.warning(user=self.user, title="title", message="message")
# then
obj = Notification.objects.first()
self.assertEqual(obj.user, self.user)
self.assertEqual(obj.title, "title")
self.assertEqual(obj.message, "message")
self.assertEqual(obj.level, Notification.Level.WARNING)
def test_should_add_info_notification_via_callable(self):
# given
notify = NotifyApiWrapper()
# when
notify(user=self.user, title="title", message="message")
# then
obj = Notification.objects.first()
self.assertEqual(obj.user, self.user)
self.assertEqual(obj.title, "title")
self.assertEqual(obj.message, "message")
self.assertEqual(obj.level, Notification.Level.INFO)
def test_should_add_danger_notification_via_callable(self):
# given
notify = NotifyApiWrapper()
# when
notify(user=self.user, title="title", message="message", level="danger")
# then
obj = Notification.objects.first()
self.assertEqual(obj.user, self.user)
self.assertEqual(obj.title, "title")
self.assertEqual(obj.message, "message")
self.assertEqual(obj.level, Notification.Level.DANGER)

View File

@@ -4,11 +4,8 @@ from allianceauth.tests.auth_utils import AuthUtils
from .. import notify
from ..models import Notification
MODULE_PATH = 'allianceauth.notifications'
class TestUserNotificationCount(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = AuthUtils.create_user('magic_mike')
@@ -23,6 +20,18 @@ class TestUserNotificationCount(TestCase):
alliance_name='RIDERS'
)
def test_can_notify(self):
notify(self.user, 'dummy')
def test_can_notify_short(self):
# when
notify(self.user, "dummy")
# then
self.assertEqual(Notification.objects.filter(user=self.user).count(), 1)
def test_can_notify_full(self):
# when
notify(user=self.user, title="title", message="message", level="danger")
# then
obj = Notification.objects.first()
self.assertEqual(obj.user, self.user)
self.assertEqual(obj.title, "title")
self.assertEqual(obj.message, "message")
self.assertEqual(obj.level, "danger")

View File

@@ -1,16 +1,16 @@
from django.conf.urls import url
from django.urls import path
from . import views
app_name = 'notifications'
# Notifications
urlpatterns = [
url(r'^remove_notifications/(\w+)/$', views.remove_notification, name='remove'),
url(r'^notifications/mark_all_read/$', views.mark_all_read, name='mark_all_read'),
url(r'^notifications/delete_all_read/$', views.delete_all_read, name='delete_all_read'),
url(r'^notifications/$', views.notification_list, name='list'),
url(r'^notifications/(\w+)/$', views.notification_view, name='view'),
url(
r'^user_notifications_count/(?P<user_pk>\d+)/$',
path('remove_notifications/<int:notif_id>/', views.remove_notification, name='remove'),
path('notifications/mark_all_read/', views.mark_all_read, name='mark_all_read'),
path('notifications/delete_all_read/', views.delete_all_read, name='delete_all_read'),
path('notifications/', views.notification_list, name='list'),
path('notifications/<int:notif_id>/', views.notification_view, name='view'),
path(
'user_notifications_count/<int:user_pk>/',
views.user_notifications_count,
name='user_notifications_count'
),

View File

@@ -1 +0,0 @@
default_app_config = 'allianceauth.optimer.apps.OptimerConfig'

View File

@@ -1,5 +1,5 @@
from allianceauth.services.hooks import MenuItemHook, UrlHook
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from allianceauth import hooks
from . import urls

View File

@@ -1,5 +1,5 @@
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from allianceauth.optimer.form_widgets import DataListWidget

View File

@@ -19,7 +19,7 @@
<form class="form-signin" role="form" action="" method="POST">
{% csrf_token %}
{{ form|bootstrap }}
<br/>
<br>
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Create Fleet Operation" %}</button>
</form>
</div>

View File

@@ -21,7 +21,7 @@
<b>{% translate "Current Eve Time:" %} </b>
</div>
<strong class="label label-info text-left" id="current-time"></strong>
<br />
<br>
</div>
<h4><b>{% translate "Next Fleet Operations" %}</b></h4>

View File

@@ -24,7 +24,7 @@
<form class="form-signin" role="form" action="" method="POST">
{% csrf_token %}
{{ form|bootstrap }}
<br/>
<br>
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Update Fleet Operation" %}
</button>
</form>

View File

@@ -1,12 +1,12 @@
from django.conf.urls import url
from django.urls import path
from . import views
app_name = 'optimer'
urlpatterns = [
url(r'^$', views.optimer_view, name='view'),
url(r'^add$', views.add_optimer_view, name='add'),
url(r'^(\w+)/remove$', views.remove_optimer, name='remove'),
url(r'^(\w+)/edit$', views.edit_optimer, name='edit'),
]
path('', views.optimer_view, name='view'),
path('add/', views.add_optimer_view, name='add'),
path('<int:optimer_id>/remove/', views.remove_optimer, name='remove'),
path('<int:optimer_id>/edit/', views.edit_optimer, name='edit'),
]

View File

@@ -6,7 +6,7 @@ from django.contrib.auth.decorators import permission_required
from django.shortcuts import get_object_or_404
from django.shortcuts import render, redirect
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from .form import OpForm
from .models import OpTimer, OpTimerType

View File

@@ -1 +0,0 @@
default_app_config = 'allianceauth.permissions_tool.apps.PermissionsToolConfig'

View File

@@ -1,11 +1,12 @@
from django.conf.urls import url
from django.urls import re_path
from django.urls import path
from . import views
app_name = 'permissions_tool'
urlpatterns = [
url(r'^overview/$', views.permissions_overview, name='overview'),
url(r'^audit/(?P<app_label>[\w\-_]+)/(?P<model>[\w\-_]+)/(?P<codename>[\w\-_]+)/$', views.permissions_audit,
path('overview/', views.permissions_overview, name='overview'),
re_path(r'^audit/(?P<app_label>[\w\-_]+)/(?P<model>[\w\-_]+)/(?P<codename>[\w\-_]+)/$', views.permissions_audit,
name='audit'),
]

View File

@@ -68,6 +68,7 @@ BASE_DIR = os.path.dirname(PROJECT_DIR)
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'allianceauth.authentication.middleware.UserSettingsMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
@@ -153,8 +154,6 @@ TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
@@ -172,11 +171,8 @@ MESSAGE_TAGS = {
CACHES = {
"default": {
"BACKEND": "redis_cache.RedisCache",
"LOCATION": "localhost:6379",
"OPTIONS": {
"DB": 1,
}
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1" # change the 1 here to change the database used
}
}

View File

@@ -61,6 +61,13 @@ EMAIL_HOST_PASSWORD = ''
EMAIL_USE_TLS = True
DEFAULT_FROM_EMAIL = ''
# Cache compression can help on bigger auths where ram starts to become an issue.
# Uncomment the following 3 lines to enable.
#CACHES["default"]["OPTIONS"] = {
# "COMPRESSOR": "django_redis.compressors.lzma.LzmaCompressor",
#}
#######################################
# Add any custom settings below here. #
#######################################

View File

@@ -1,8 +1,9 @@
from django.conf.urls import include, url
from django.conf.urls import include
from allianceauth import urls
from django.urls import re_path
urlpatterns = [
url(r'', include(urls)),
re_path(r'', include(urls)),
]
handler500 = 'allianceauth.views.Generic500Redirect'

View File

@@ -1 +0,0 @@
default_app_config = 'allianceauth.services.apps.ServicesConfig'

View File

@@ -1,5 +1,5 @@
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
class FleetFormatterForm(forms.Form):

View File

@@ -1,4 +1,5 @@
from django.conf.urls import include, url
from django.conf.urls import include
from django.urls import re_path
from django.core.exceptions import ObjectDoesNotExist
from django.template.loader import render_to_string
from django.utils.functional import cached_property
@@ -9,7 +10,6 @@ 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
@@ -157,7 +157,7 @@ class MenuItemHook:
class UrlHook:
def __init__(self, urls, namespace, base_url):
self.include_pattern = url(base_url, include(urls, namespace=namespace))
self.include_pattern = re_path(base_url, include(urls, namespace=namespace))
class NameFormatter:

View File

@@ -1,3 +1 @@
default_app_config = 'allianceauth.services.modules.discord.apps.DiscordServiceConfig' # noqa
__title__ = 'Discord Service'

View File

@@ -8,7 +8,7 @@ from uuid import uuid1
from redis import Redis
import requests
from django.core.cache import caches
from django_redis import get_redis_connection
from allianceauth import __title__ as AUTH_TITLE, __url__, __version__
@@ -103,8 +103,7 @@ class DiscordClient:
self._access_token = str(access_token)
self._is_rate_limited = bool(is_rate_limited)
if not redis:
default_cache = caches['default']
self._redis = default_cache.get_master_client()
self._redis = get_redis_connection("default")
if not isinstance(self._redis, Redis):
raise RuntimeError(
'This class requires a Redis client, but none was provided '

View File

@@ -85,28 +85,24 @@ class TestBasicsAndHelpers(TestCase):
client = DiscordClient(TEST_BOT_TOKEN, mock_redis, is_rate_limited=True)
self.assertTrue(client.is_rate_limited)
@patch(MODULE_PATH + '.caches')
@patch(MODULE_PATH + '.get_redis_connection')
def test_use_default_redis_if_none_provided(self, mock_caches):
my_redis = MagicMock(spec=Redis)
mock_default_cache = MagicMock(**{'get_master_client.return_value': my_redis})
my_dict = {'default': mock_default_cache}
mock_caches.__getitem__.side_effect = my_dict.__getitem__
mock_caches.return_value = my_redis
client = DiscordClient(TEST_BOT_TOKEN)
self.assertTrue(mock_default_cache.get_master_client.called)
self.assertTrue(mock_caches.called)
self.assertEqual(client._redis, my_redis)
@patch(MODULE_PATH + '.caches')
@patch(MODULE_PATH + '.get_redis_connection')
def test_raise_exception_if_default_cache_is_not_redis(self, mock_caches):
my_redis = MagicMock()
mock_default_cache = MagicMock(**{'get_master_client.return_value': my_redis})
my_dict = {'default': mock_default_cache}
mock_caches.__getitem__.side_effect = my_dict.__getitem__
mock_caches.return_value = my_redis
with self.assertRaises(RuntimeError):
DiscordClient(TEST_BOT_TOKEN)
self.assertTrue(mock_default_cache.get_master_client.called)
self.assertTrue(mock_caches.called)
@requests_mock.Mocker()

View File

@@ -14,7 +14,7 @@ from requests.exceptions import HTTPError
import requests_mock
from django.contrib.auth.models import Group, User
from django.core.cache import caches
from django_redis import get_redis_connection
from django.shortcuts import reverse
from django.test import TransactionTestCase, TestCase
from django.test.utils import override_settings
@@ -87,8 +87,7 @@ remove_guild_member_request = DiscordRequest(
def clear_cache():
default_cache = caches['default']
redis = default_cache.get_master_client()
redis = get_redis_connection('default')
redis.flushall()
logger.info('Cache flushed')

View File

@@ -1,4 +1,5 @@
from django.conf.urls import url, include
from django.conf.urls import include
from django.urls import path
from . import views
@@ -6,13 +7,13 @@ app_name = 'discord'
module_urls = [
# Discord Service Control
url(r'^activate/$', views.activate_discord, name='activate'),
url(r'^deactivate/$', views.deactivate_discord, name='deactivate'),
url(r'^reset/$', views.reset_discord, name='reset'),
url(r'^callback/$', views.discord_callback, name='callback'),
url(r'^add_bot/$', views.discord_add_bot, name='add_bot'),
path('activate/', views.activate_discord, name='activate'),
path('deactivate/', views.deactivate_discord, name='deactivate'),
path('reset/', views.reset_discord, name='reset'),
path('callback/', views.discord_callback, name='callback'),
path('add_bot/', views.discord_add_bot, name='add_bot'),
]
urlpatterns = [
url(r'^discord/', include((module_urls, app_name), namespace=app_name))
path('discord/', include((module_urls, app_name), namespace=app_name))
]

View File

@@ -1 +0,0 @@
default_app_config = 'allianceauth.services.modules.discourse.apps.DiscourseServiceConfig'

View File

@@ -1,8 +1,8 @@
from django.conf.urls import url
from django.urls import path
from . import views
urlpatterns = [
# Discourse Service Control
url(r'^discourse/sso$', views.discourse_sso, name='auth_discourse_sso'),
path('discourse/sso', views.discourse_sso, name='auth_discourse_sso'),
]

View File

@@ -1 +0,0 @@
default_app_config = 'allianceauth.services.modules.example.apps.ExampleServiceConfig'

View File

@@ -1,4 +1,5 @@
from django.conf.urls import url, include
from django.conf.urls import include
from django.urls import path
app_name = 'example'
@@ -7,5 +8,5 @@ module_urls = [
]
urlpatterns = [
url(r'^example/', include((module_urls, app_name), namespace=app_name)),
path('example/', include((module_urls, app_name), namespace=app_name)),
]

View File

@@ -1 +0,0 @@
default_app_config = 'allianceauth.services.modules.ips4.apps.Ips4ServiceConfig'

View File

@@ -1,4 +1,5 @@
from django.conf.urls import url, include
from django.conf.urls import include
from django.urls import path
from . import views
@@ -6,12 +7,12 @@ app_name = 'ips4'
module_urls = [
# IPS4 Service Control
url(r'^activate/$', views.activate_ips4, name='activate'),
url(r'^deactivate/$', views.deactivate_ips4, name='deactivate'),
url(r'^reset_password/$', views.reset_ips4_password, name='reset_password'),
url(r'^set_password/$', views.set_ips4_password, name='set_password'),
path('activate/', views.activate_ips4, name='activate'),
path('deactivate/', views.deactivate_ips4, name='deactivate'),
path('reset_password/', views.reset_ips4_password, name='reset_password'),
path('set_password/', views.set_ips4_password, name='set_password'),
]
urlpatterns = [
url(r'^ips4/', include((module_urls, app_name), namespace=app_name))
path('ips4/', include((module_urls, app_name), namespace=app_name))
]

View File

@@ -1 +0,0 @@
default_app_config = 'allianceauth.services.modules.mumble.apps.MumbleServiceConfig'

View File

@@ -62,6 +62,12 @@ class MumbleManager(models.Manager):
class MumbleUser(AbstractServiceModel):
user = models.OneToOneField(
'auth.User',
primary_key=True,
on_delete=models.CASCADE,
related_name='mumble'
)
username = models.CharField(max_length=254, unique=True)
pwhash = models.CharField(max_length=90)
hashfn = models.CharField(max_length=20, default='sha1')

View File

@@ -3,7 +3,7 @@
<td class="text-center">{{ username }}</td>
<td class="text-center"><a href="mumble://{{ service_url }}">{{ service_url }}</a></td>
<td class="text-center">
{% ifequal username "" %}
{% if username == "" %}
<a href="{% url urls.auth_activate %}" title="Activate" class="btn btn-warning">
<span class="glyphicon glyphicon-ok"></span>
</a>
@@ -20,6 +20,6 @@
<a href="mumble://{{ connect_url }}" class="btn btn-success" title="Connect">
<span class="glyphicon glyphicon-arrow-right"></span>
</a>
{% endifequal %}
{% endif %}
</td>
</tr>

View File

@@ -1,4 +1,5 @@
from django.conf.urls import url, include
from django.conf.urls import include
from django.urls import path
from . import views
@@ -6,12 +7,12 @@ app_name = 'mumble'
module_urls = [
# Mumble service control
url(r'^activate/$', views.CreateAccountMumbleView.as_view(), name='activate'),
url(r'^deactivate/$', views.DeleteMumbleView.as_view(), name='deactivate'),
url(r'^reset_password/$', views.ResetPasswordMumbleView.as_view(), name='reset_password'),
url(r'^set_password/$', views.SetPasswordMumbleView.as_view(), name='set_password'),
path('activate/', views.CreateAccountMumbleView.as_view(), name='activate'),
path('deactivate/', views.DeleteMumbleView.as_view(), name='deactivate'),
path('reset_password/', views.ResetPasswordMumbleView.as_view(), name='reset_password'),
path('set_password/', views.SetPasswordMumbleView.as_view(), name='set_password'),
]
urlpatterns = [
url(r'^mumble/', include((module_urls, app_name), namespace=app_name))
path('mumble/', include((module_urls, app_name), namespace=app_name))
]

View File

@@ -1 +0,0 @@
default_app_config = 'allianceauth.services.modules.openfire.apps.OpenfireServiceConfig'

View File

@@ -1,5 +1,5 @@
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
class JabberBroadcastForm(forms.Form):

View File

@@ -3,7 +3,7 @@ import random
import string
from urllib.parse import urlparse
import sleekxmpp
import slixmpp
from django.conf import settings
from ofrestapi.users import Users as ofUsers
from ofrestapi import exception
@@ -172,13 +172,13 @@ class OpenfireManager:
raise PingBotException("Unable to connect to jabber server.")
class PingBot(sleekxmpp.ClientXMPP):
class PingBot(slixmpp.ClientXMPP):
"""
A copy-paste of the example client bot from
http://sleekxmpp.com/getting_started/sendlogout.html
"""
def __init__(self, jid, password, recipient, message):
sleekxmpp.ClientXMPP.__init__(self, jid, password)
slixmpp.ClientXMPP.__init__(self, jid, password)
self.reconnect_max_attempts = 5
self.auto_reconnect = False

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