Compare commits

...

184 Commits

Author SHA1 Message Date
Ariel Rin
5de19c43df Version Bump 4.0.0a4 2023-11-09 00:13:45 +10:00
Ariel Rin
6a0ddc9a83 Merge branch 'docker-superlance' into 'v4.x'
Add superlance/memmon path to the project bootstrap

See merge request allianceauth/allianceauth!1560
2023-11-08 13:39:11 +00:00
Ariel Rin
03be66d11f multilines dont work in our yaml 2023-11-08 23:26:12 +10:00
Ariel Rin
7e312bb95f four args now 2023-11-08 20:25:24 +10:00
Ariel Rin
c92fee78e2 add superlance/memmon path to the bootstrap 2023-11-08 20:01:07 +10:00
Ariel Rin
658a8cd6ce Merge branch 'task-queue-progressbar-background' into 'v4.x'
[FIX] Celery task status bar background

See merge request allianceauth/allianceauth!1559
2023-11-08 03:31:48 +00:00
Peter Pfeufer
c1dc130766 [FIX] Celery task status bar background 2023-11-07 15:21:44 +01:00
Ariel Rin
35f5573b63 Merge branch 'js-fixes' into 'v4.x'
JS Fixes

See merge request allianceauth/allianceauth!1556
2023-11-06 13:49:31 +00:00
Peter Pfeufer
21f0a96422 [CHANGE] Modernize and convert to ES6+ 2023-10-31 22:00:43 +01:00
Peter Pfeufer
9e47d19337 [ADD] Missing semicolons 2023-10-31 21:48:18 +01:00
Peter Pfeufer
2c5972d0ab [ADD] Refresh notification icon script
Similar to what we have in AAv3 for the notification count
2023-10-31 21:47:40 +01:00
Ariel Rin
ee41d62c13 Merge branch 'quickfix-services-control-template' into 'v4.x'
[REMOVE] Deprecated overrides …

See merge request allianceauth/allianceauth!1555
2023-10-31 13:29:48 +00:00
Peter Pfeufer
346b4014a9 [ADD] Missing semicolon
Just for the sake of it …
2023-10-31 14:13:01 +01:00
Peter Pfeufer
9b56a441ed [REMOVE] Deprecated overrides … 2023-10-31 14:10:26 +01:00
Ariel Rin
068bf1ae7a Merge branch 'services-template-improvements' into 'v4.x'
Services template improvements

See merge request allianceauth/allianceauth!1553
2023-10-31 12:42:08 +00:00
Peter Pfeufer
5be686e3ca [FIX] Username check 2023-10-31 13:28:38 +01:00
Ariel Rin
a215b4411c Merge branch 'v4theme' into 'v4.x'
Missing Import on views

See merge request allianceauth/allianceauth!1554
2023-10-31 12:14:15 +00:00
Ariel Rin
e15cfa0fb1 Missing Import on views 2023-10-31 12:14:14 +00:00
Peter Pfeufer
46d51699f4 [CHANGE] Service delete confirm template converted to BS5 2023-10-31 11:59:12 +01:00
Peter Pfeufer
ff30a136d5 [CHANGE] Service credentials template converted to BS5 2023-10-31 11:56:18 +01:00
Peter Pfeufer
6dcf3304d5 Merge remote-tracking branch 'origin/services-template-improvements' into services-template-improvements 2023-10-31 11:49:49 +01:00
Peter Pfeufer
beddeea338 [CHANGE] Discourse service status badge text 2023-10-31 11:49:18 +01:00
Ariel Rin
69723937f7 Merge branch 'update-project-classifier' into 'v4.x'
[CHANGE] Update project classifier for Django

See merge request allianceauth/allianceauth!1551
2023-10-31 10:01:14 +00:00
Ariel Rin
c541f56ee2 Merge branch 'fix-dashboard-timers' into 'v4.x'
[FIX] EveCorporationInfo matching query does not exist

See merge request allianceauth/allianceauth!1552
2023-10-31 10:01:03 +00:00
Peter Pfeufer
7e887e5e34 [ADD] General template include for username line 2023-10-31 10:47:06 +01:00
Peter Pfeufer
072327c79f [CHANGE] Comment active section for nor for Discourse service
The Discourse service doesn't seem to have anything to determine weather it's active or not.
2023-10-31 10:38:01 +01:00
Peter Pfeufer
28af3ff11e [CHANGE] Sort service card information to be a bit more uniform in apperance 2023-10-31 10:27:49 +01:00
Peter Pfeufer
e3b151f2fb [CHANGE] Use BS5 forms 2023-10-31 10:25:04 +01:00
Peter Pfeufer
f87d7dbdf8 [FIX] Normalization of TeamSpeak3 service name 2023-10-31 10:12:02 +01:00
Peter Pfeufer
a04e6ae3d0 [FIX] Normalization of IPSuite4 service name 2023-10-31 10:09:43 +01:00
Peter Pfeufer
15042f5e77 [FIX] Capitalization of Discord service name 2023-10-31 10:07:47 +01:00
Peter Pfeufer
6e25361d5e [FIX] Capitalization of XenForo service name 2023-10-31 10:06:36 +01:00
Peter Pfeufer
9e639a0eeb [FIX] Capitalization of SMF service name 2023-10-31 10:04:55 +01:00
Peter Pfeufer
257fbdef36 [FIX] Capitalization of Jabber service name 2023-10-31 10:03:06 +01:00
Peter Pfeufer
df003c8ec5 [CHANGE] TS³ service template 2023-10-31 09:57:54 +01:00
Peter Pfeufer
ba22685eb8 [CHANGE] Discourse service template 2023-10-31 09:53:09 +01:00
Peter Pfeufer
773288072a [CHANGE] Discord service 2023-10-31 09:48:29 +01:00
Peter Pfeufer
63afb13d25 [CHANGE] Mumble service template 2023-10-31 09:48:07 +01:00
Peter Pfeufer
5dd286bbe7 [CHANGE] Set a default via status template 2023-10-31 09:47:22 +01:00
Peter Pfeufer
8aaa8172ca [CHANGE] Only show username when there is a username 2023-10-31 08:58:30 +01:00
Peter Pfeufer
b68b401146 [FIX] EveCorporationInfo matching query does not exist 2023-10-29 11:55:42 +01:00
Peter Pfeufer
a6526d6f78 [CHANGE] Update project classifier for Django 2023-10-28 00:12:40 +02:00
Ariel Rin
7898594909 Version Bump 4.0.0a3 2023-10-27 23:23:48 +10:00
Ariel Rin
cfd12ee3cc Merge branch 'v4theme' into 'v4.x'
remove rogue span tag

See merge request allianceauth/allianceauth!1546
2023-10-27 13:22:38 +00:00
Ariel Rin
2c9177b19f remove rogue span tag 2023-10-27 13:22:38 +00:00
Ariel Rin
abff26fb6e Version Bump 4.0.0a2 2023-10-27 22:48:06 +10:00
Ariel Rin
e8c3b5225c Merge branch 'v4.x' of gitlab.com:allianceauth/allianceauth into v4.x 2023-10-27 22:42:53 +10:00
Ariel Rin
98fd1dcc4c Merge branch 'master' of gitlab.com:allianceauth/allianceauth into v4.x 2023-10-27 22:42:30 +10:00
Ariel Rin
cfe46e4ca5 Merge branch 'fix-same-name-template-tag-modules' into 'v4.x'
[FIX] Give template tag modules unique names

See merge request allianceauth/allianceauth!1548
2023-10-27 12:15:27 +00:00
Peter Pfeufer
4675193416 [REEMOVE] services.templatetags as its no longer needed 2023-10-27 14:08:44 +02:00
Ariel Rin
a84fa1ca69 Merge branch 'aa-css-framework' into 'v4.x'
Alliance Auth CSS Framework

See merge request allianceauth/allianceauth!1544
2023-10-27 11:58:16 +00:00
Ariel Rin
8f6cb0b9bb Merge branch 'fix-notification-colors' into 'v4.x'
[FIX]  Restore notification level colours

See merge request allianceauth/allianceauth!1547
2023-10-27 11:54:40 +00:00
Ariel Rin
1c8634f1c8 Merge branch 'theme-work' into 'v4.x'
Theme test fixes

See merge request allianceauth/allianceauth!1549
2023-10-27 11:33:08 +00:00
Aaron Kable
2a21599d45 BS5 Theme test fixes 2023-10-27 11:33:08 +00:00
Peter Pfeufer
e379c01655 [FIX] Typo in variable name while we're at it 2023-10-27 08:31:55 +02:00
Peter Pfeufer
afa3d2e7cc [FIX] Give template tags modules unique names
This fixes the check error:

?: (templates.E003) 'menu_items' is used for multiple template tag modules: 'allianceauth.menu.templatetags.menu_items', 'allianceauth.services.templatetags.menu_items'
2023-10-27 08:28:09 +02:00
Peter Pfeufer
e5ed33aeec [FIX] [Bootstrap] Uniform default table background
Also removed empty style and class arguments while I was at it
2023-10-26 18:21:23 +02:00
Peter Pfeufer
b12471e775 [CHANGE] Bring back the level colors to the notification list 2023-10-26 15:30:54 +02:00
Peter Pfeufer
5e70dab11f [FIX] Ensure the modifier is only applied to elements with the base class 2023-10-24 12:54:20 +02:00
Peter Pfeufer
f728c786b3 [FIX] Grammar and indentation (it's important!) 2023-10-24 12:27:08 +02:00
Peter Pfeufer
7056912d54 [FIX] Path to image in .md file 2023-10-24 11:58:34 +02:00
Peter Pfeufer
7efed950ca [CHANGE] Seems it needs to be a PNG image … 2023-10-24 11:53:00 +02:00
Peter Pfeufer
886acf2005 [ADD] CSS framework docs to custom documentation index 2023-10-24 11:36:54 +02:00
Peter Pfeufer
b2dec3bff2 [FIX] Typo … 2023-10-24 11:31:15 +02:00
Peter Pfeufer
f0a402e141 [ADD] Documentation 2023-10-24 11:25:52 +02:00
Peter Pfeufer
2e2afd7923 [ADD] Callout boxes
Not quite alerts, but custom and helpful notes for folks. Requires a base and modifier class.
2023-10-24 11:05:15 +02:00
Peter Pfeufer
e9ea09bc56 [CHANGE] Rename auth-base-bs5.css to auth-framework.css 2023-10-24 10:58:45 +02:00
Ariel Rin
186fa1be03 Correct Django version back to !1541 after porting !1513 2023-10-24 12:37:37 +10:00
Ariel Rin
37d1d84fc3 Merge branch 'aa4-bs5-template-fixes' into 'v4.x'
v4 Template fixes

See merge request allianceauth/allianceauth!1541
2023-10-24 02:04:50 +00:00
Peter Pfeufer
ee24706e43 v4 Template fixes 2023-10-24 02:04:49 +00:00
Ariel Rin
07e85727ea Merge branch 'v4theme' into 'v4.x'
Theme handling improvements

See merge request allianceauth/allianceauth!1542
2023-10-21 09:08:28 +00:00
Ariel Rin
4912f0f8f0 Theme handling improvements 2023-10-21 09:08:28 +00:00
Ariel Rin
424246df26 Version Bump 3.7.1 2023-10-19 14:00:29 +10:00
Ariel Rin
563e2210ef Bump Django-ESI to >=5.0.0 2023-10-19 13:11:35 +10:00
Ariel Rin
02a1078005 Merge branch 'remove-thirdparty' into 'master'
Remove outdated supervisor configs - refer to docs

See merge request allianceauth/allianceauth!1533
2023-10-19 03:03:35 +00:00
Ariel Rin
30107de44e Merge branch 'docs-precommit' into 'master'
Add code-style docs

Closes #1379

See merge request allianceauth/allianceauth!1534
2023-10-19 03:03:14 +00:00
Ariel Rin
77a08cd218 Add code-style docs 2023-10-07 22:32:19 +10:00
Ariel Rin
e5a09027e5 Remove outdated supervisor configs - refer to docs 2023-10-07 21:53:03 +10:00
Ariel Rin
24376262f0 minor doc structure changes 2023-10-07 21:27:08 +10:00
Ariel Rin
efe0c6963b move doc dependencies to pyproject 2023-10-07 20:45:39 +10:00
Ariel Rin
a4644028ae file path typos 2023-10-07 20:44:16 +10:00
Ariel Rin
3a77b4a429 Add missing docker tags, make docker buildsteps more readable 2023-10-07 19:57:55 +10:00
Ariel Rin
52b6c5d341 Rework default celery configuration and documentation 2023-10-07 19:31:53 +10:00
Ariel Rin
fa375a551c Merge branch 'celry' into 'v4.x'
Rework default celery configuration and documentation

See merge request allianceauth/allianceauth!1482
2023-10-07 09:28:04 +00:00
Aaron Kable
00a93e6fe9 Rework default celery configuration and documentation 2023-10-07 09:28:04 +00:00
Ariel Rin
656e69d4b2 4.4.0a1 - use pep44, refer to !1323 2023-10-07 18:54:02 +10:00
Ariel Rin
3b55d370d0 Merge branch 'v4.x' of gitlab.com:allianceauth/allianceauth into v4.x 2023-10-07 18:40:45 +10:00
Ariel Rin
5c126ffe82 Version Bump 4.0.0-alpha.1 2023-10-07 18:40:00 +10:00
Ariel Rin
99be753836 Merge branch 'v4.x-theme' into 'v4.x'
BS5 Theme

See merge request allianceauth/allianceauth!1464
2023-10-07 08:20:22 +00:00
Aaron Kable
2e78aa5f26 BS5 Theme 2023-10-07 08:20:22 +00:00
Ariel Rin
567d97f38a Merge branch 'master' of gitlab.com:allianceauth/allianceauth into v4.x 2023-10-07 17:38:30 +10:00
Ariel Rin
8b895b76b5 Version Bump 3.7.0 2023-10-07 15:27:11 +10:00
Ariel Rin
babd71702f Merge branch 'docs' into 'master'
Remove CentOS Section from NGINX docs

See merge request allianceauth/allianceauth!1531
2023-10-07 04:59:12 +00:00
Ariel Rin
3ec3cbdff7 Merge branch 'fix-tasks-running' into 'master'
Fix tasks running counter

See merge request allianceauth/allianceauth!1529
2023-10-07 04:52:04 +00:00
Erik Kalkoken
51611e1237 Fix tasks running counter 2023-10-07 04:52:03 +00:00
Ariel Rin
39519bab91 Merge branch 'new-discord-username-format' into 'master'
[ADD] Respect and display the new Discord username format when eligible

See merge request allianceauth/allianceauth!1526
2023-10-07 04:40:46 +00:00
Ariel Rin
90dc6a4d4c Merge branch 'fix-reference-before-assignment' into 'master'
[FIX] Reference before assignment

Closes #1369 and #1375

See merge request allianceauth/allianceauth!1530
2023-10-07 04:40:00 +00:00
Ariel Rin
53ffd7f885 Merge branch 'set-lang-attribute' into 'master'
[ADD] Language code to page and language selector

See merge request allianceauth/allianceauth!1528
2023-10-07 04:39:01 +00:00
Ariel Rin
efc7475228 Merge branch 'celery-broker-connection' into 'master'
Prepare our Celery config for Celery 6

See merge request allianceauth/allianceauth!1532
2023-10-07 04:38:21 +00:00
Peter Pfeufer
380c41400b [CHANGE] Updating celery.py to prevent deprecation warning
```
[2023-08-14 06:41:04,904: WARNING/MainProcess] /mnt/sda1/Development/Python/AllianceAuth/venv-3.11/lib/python3.11/site-packages/celery/worker/consumer/consumer.py:498: CPendingDeprecationWarning: The broker_connection_retry configuration setting will no longer determine
whether broker connection retries are made during startup in Celery 6.0 and above.
If you wish to retain the existing behavior for retrying connections on startup,
you should set broker_connection_retry_on_startup to True.
```
2023-09-15 11:42:47 +02:00
colcrunch
079c12a72e Remove CentOS heading and add notes about the differing config methods to the relevant lines in the install section. 2023-09-07 07:11:05 -04:00
Peter Pfeufer
4f1ebedc44 [FIX] Reference before assignment
`ownership` doesn't exist at this point.
To get the main character, `user` is used here.


```
Traceback (most recent call last):
  File "/home/allianceserver/venv/auth/lib/python3.10/site-packages/django/core/handlers/exception.py", line 56, in inner
    response = get_response(request)
  File "/home/allianceserver/venv/auth/lib/python3.10/site-packages/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/home/allianceserver/venv/auth/lib/python3.10/site-packages/esi/decorators.py", line 116, in _wrapped_view
    return view_func(request, token, *args, **kwargs)
  File "/home/allianceserver/venv/auth/lib/python3.10/site-packages/allianceauth/authentication/views.py", line 156, in sso_login
    user = authenticate(token=token)
  File "/home/allianceserver/venv/auth/lib/python3.10/site-packages/django/views/decorators/debug.py", line 42, in sensitive_variables_wrapper
    return func(*func_args, **func_kwargs)
  File "/home/allianceserver/venv/auth/lib/python3.10/site-packages/django/contrib/auth/__init__.py", line 77, in authenticate
    user = backend.authenticate(request, **credentials)
  File "/home/allianceserver/venv/auth/lib/python3.10/site-packages/allianceauth/authentication/backends.py", line 68, in authenticate
    if ownership.user.profile.main_character.character_id != token.character_id:
UnboundLocalError: local variable 'ownership' referenced before assignment
```
2023-09-01 20:46:00 +02:00
Peter Pfeufer
66822107e3 [ADD] Language code to page and language selector 2023-08-28 21:40:28 +02:00
Peter Pfeufer
7856cd5ce4 [ADD] Respect and display the new Discord username format when eligible 2023-08-26 00:47:08 +02:00
Ariel Rin
d6821b3fd6 Merge branch 'master' of gitlab.com:allianceauth/allianceauth into v4.x 2023-08-14 15:13:54 +10:00
Ariel Rin
90375246fd Merge branch 'analytics' into 'v4.x'
Analytics UA to V4 Conversion

See merge request allianceauth/allianceauth!1500
2023-08-14 03:31:33 +00:00
Ariel Rin
a2f217ace5 Analytics UA to V4 Conversion 2023-08-14 03:31:33 +00:00
Ariel Rin
25cf2fdcd5 Merge branch 'docs' into 'v4.x'
V4.x Docker Refactoring and Docs

See merge request allianceauth/allianceauth!1507
2023-08-14 03:05:44 +00:00
Ariel Rin
4305ae7995 V4.x Docker Refactoring and Docs 2023-08-14 03:05:44 +00:00
Ariel Rin
36b3077caa Version Bump 3.6.1 2023-08-10 14:37:27 +10:00
Ariel Rin
1786f3a642 Merge branch 'prepare-base-template-for-public-views' into 'master'
[FIX] Templates prepared for public views

See merge request allianceauth/allianceauth!1525
2023-08-10 04:23:48 +00:00
Peter Pfeufer
55927c6f15 [FIX] Allow messages also for non-logged-in user to be displayed 2023-08-08 23:20:43 +02:00
Peter Pfeufer
8fbe0ba45d [CHANGE] Comment 2023-08-07 07:23:21 +02:00
Peter Pfeufer
1563805ddb [ADD] CCP's "No Character" character image as user menu image for public 2023-08-05 18:51:12 +02:00
Ariel Rin
c58ed53369 Merge branch 'fix-negative-running-tasks' into 'master'
Fix negative running tasks

See merge request allianceauth/allianceauth!1524
2023-08-05 09:17:42 +00:00
Peter Pfeufer
32128ace1c [FIX] Better explanation in local.py project template 2023-08-02 23:35:25 +02:00
Peter Pfeufer
7290eaad7e [FIX] Notifications menu item removed in public views 2023-08-02 23:22:14 +02:00
Peter Pfeufer
f23d4f4dd1 [FIX] base.html prepared for public views
Certain things need to be behind `{% if user.is_authenticated %}` in order for the base template to play nice with public views.
2023-08-02 23:17:28 +02:00
ErikKalkoken
ab3f10e6f2 Add more tests for ItemCounter 2023-08-02 16:11:24 +02:00
ErikKalkoken
20187cc73e Add locks to ensure process safety 2023-08-02 15:58:36 +02:00
ErikKalkoken
1f55fbfccc Add minimum to ItemCounter and refactor redis client init 2023-08-02 15:41:47 +02:00
Ariel Rin
12383d79c8 Version Bump 3.6.0 2023-08-01 21:37:25 +10:00
Ariel Rin
56e2875650 Merge branch 'pre-commit-update' into 'master'
`pre-commit` Update and File Permission Fixes (redone)

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

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

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

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

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

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

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

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

See merge request allianceauth/allianceauth!1517
2023-07-25 08:55:25 +00:00
Peter Pfeufer
fc29d7e80d [ADD] All modern favicon versions generated by realfavicongenerator.net 2023-07-19 12:10:02 +02:00
Peter Pfeufer
ec536c66a0 [ADD] Favicon redirect to Nginx docs 2023-07-19 11:40:45 +02:00
Peter Pfeufer
749ece45e2 [ADD] Favicon redirect to Apache2 docs 2023-07-19 11:39:04 +02:00
Peter Pfeufer
b04c8873d0 [ADD] Directive for default favicon 2023-07-19 11:37:07 +02:00
Aaron Kable
9a77175bf3 Allow get params from next at login 2023-07-17 10:25:36 +08:00
Peter Pfeufer
5d4c7b9030 [FIX] crontab arguments here as well 2023-07-14 19:22:29 +02:00
Aaron Kable
5f80259d57 fix test 2023-07-13 20:28:35 +08:00
Peter Pfeufer
dcd6bd1b36 [FIX] crontab arguments are of type string, not int 2023-07-12 13:11:18 +02:00
Ariel Rin
6f4dffe930 Version Bump 3.5.1 2023-07-11 22:38:26 +10:00
Ariel Rin
56d70e6c74 Add pkg-config to dockerfile 2023-07-11 22:36:15 +10:00
Ariel Rin
5e14ea4573 Version Bump 3.5.0 2023-07-11 13:01:50 +10:00
Ariel Rin
c743eca0f7 Merge branch 'fix-use-of-deprecated-function-calls' into 'master'
[FIX] Use of deprecated `logger.warn()` function calls

See merge request allianceauth/allianceauth!1516
2023-07-11 02:36:59 +00:00
Ariel Rin
2002f24178 Merge branch 'limit-django-registration' into 'master'
[COMPATIBILITY] Limit `django-registration` to <3.4

See merge request allianceauth/allianceauth!1512
2023-07-07 23:57:30 +00:00
Ariel Rin
6412aedf53 Merge branch 'add-docs-for-external-link' into 'master'
Add customization example to docs

See merge request allianceauth/allianceauth!1511
2023-07-07 23:56:07 +00:00
Erik Kalkoken
939df08b95 Add customization example to docs 2023-07-07 23:56:07 +00:00
Ariel Rin
d8506aa753 Merge branch 'pkg-config' into 'master'
add pkg-config to our system package dependencies, for mysqlclient 2.2.0

Closes #1376

See merge request allianceauth/allianceauth!1509
2023-07-07 23:54:41 +00:00
Ariel Rin
3f2cdac658 Merge branch 'devdocs-1' into 'master'
Add pkg-config install to dev setup and expand python downgrade note.

See merge request allianceauth/allianceauth!1510
2023-07-07 23:54:39 +00:00
Peter Pfeufer
d57ab01ff3 [FIX] Use of deprecated logger.warn() function calls 2023-07-07 17:51:55 +02:00
Peter Pfeufer
91b62bbe9d [COMPATIBILITY] Limit django-registration to <3.4 2023-07-04 12:51:12 +02:00
Max Tsero
557a52e3c8 Add pkg-config install to dev setup and expand python downgrade note. 2023-07-02 10:39:23 +00:00
Ariel Rin
f8fefd92a5 add pkg-config to our system package dependencies, for mysqlclient 2.2.0 2023-06-27 11:08:59 +10:00
Ariel Rin
f2c43ee921 Merge branch 'add-docs-app-unintall' into 'master'
Improve documentation for app removal

See merge request allianceauth/allianceauth!1508
2023-06-20 14:34:32 +00:00
Erik Kalkoken
99945b0146 Improve documentation for app removal 2023-06-20 14:34:32 +00:00
Ariel Rin
4aff4006e3 Merge branch 'v4-bumps' into 'v4.x'
V4.x Major versions

See merge request allianceauth/allianceauth!1502
2023-06-09 06:07:39 +00:00
Ariel Rin
55c188f2d0 more docker image bumps 2023-06-03 17:32:30 +10:00
Ariel Rin
f36f824a4b run pre-commit 2023-06-03 17:13:51 +10:00
Ariel Rin
6fbf33bcdd update pre-commit 2023-06-03 17:13:17 +10:00
Ariel Rin
ed3c2c8529 dj4.1 #31395 changed testdata upstream, breaks setting up classes or something 2023-06-03 17:08:06 +10:00
Ariel Rin
abb9dc4db6 Merge branch 'master' into 'master'
Add utf8mb4 unicode option to mysql config in local.py

See merge request allianceauth/allianceauth!1503
2023-06-03 07:04:52 +00:00
Valiantiam
eba5b80cde Add utf8mb4 unicode option to mysql config in local.py 2023-06-03 07:04:52 +00:00
Ariel Rin
5b39c887a5 Merge branch 'master' into 'master'
Add Grafana datasoure and dashboard provisioning

See merge request allianceauth/allianceauth!1501
2023-06-03 05:06:23 +00:00
Ariel Rin
183363e789 Merge branch 'add-ukrainian-to-userprofile-language-selection' into 'master'
[ADDED] Ukrainian to `UserProfile.LANGUAGE_CHOICES`

See merge request allianceauth/allianceauth!1505
2023-06-03 05:04:34 +00:00
Ariel Rin
d8704f4d8f Merge branch 'add-ipv6-to-nginx-config' into 'master'
[ADDED] IPv6 to Nginx config

See merge request allianceauth/allianceauth!1506
2023-06-03 05:03:17 +00:00
Peter Pfeufer
165ee44a63 [ADDED] IPv6 to Nginx config
IPv6 is almost 25 years old, time to add it to our config …
2023-05-31 09:29:04 +02:00
Peter Pfeufer
e8f508cecb [CHANGE] Switched to more modern models.TextChoices class for languages 2023-05-28 18:31:50 +02:00
Peter Pfeufer
3044f18900 [ADDED] Ukrainian to UserProfile.LANGUAGE_CHOICES 2023-05-28 17:38:51 +02:00
Aaron Kable
1cae20fe5f Dont fail on unknown groups, simply remove them. 2023-05-19 05:00:39 +08:00
MillerUk
79637020f3 add grafana datasoure and dashboard provisioning 2023-05-16 22:14:11 +00:00
Ariel Rin
05d7fb1f63 repr workaround no longer needed 2023-05-03 14:23:10 +10:00
Ariel Rin
3b19db2564 let pre-commit do some work 2023-05-03 13:18:05 +10:00
Ariel Rin
98aa44c070 dj 4.2 2023-05-03 13:17:55 +10:00
Ariel Rin
8d46ee65af target dj4.2 2023-05-03 12:51:56 +10:00
Ariel Rin
49780b871d python bumps 2023-05-03 12:46:49 +10:00
Ariel Rin
2b7d24fc28 Merge branch 'v4.x' of https://gitlab.com/allianceauth/allianceauth into v4-bumps 2023-05-03 12:24:46 +10:00
Ariel Rin
b8f86a618f py312 rc tests 2023-05-03 12:20:17 +10:00
Ariel Rin
9921011742 docker bumps 2023-05-03 12:19:52 +10:00
MillerUk
2d34422e2d add grafana datasoure and dashboard provisioning 2023-04-30 20:32:40 +00:00
340 changed files with 6106 additions and 3334 deletions

View File

@@ -25,7 +25,7 @@ before_script:
pre-commit-check: pre-commit-check:
<<: *only-default <<: *only-default
stage: pre-commit stage: pre-commit
image: python:3.8-bullseye image: python:3.11-bullseye
variables: variables:
PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
cache: cache:
@@ -112,6 +112,19 @@ test-pvpy-core:
path: coverage.xml path: coverage.xml
allow_failure: true allow_failure: true
test-3.12-core:
<<: *only-default
image: python:3.12-rc-bullseye
script:
- tox -e py312-core
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
allow_failure: true
test-3.8-all: test-3.8-all:
<<: *only-default <<: *only-default
image: python:3.8-bullseye image: python:3.8-bullseye
@@ -174,9 +187,22 @@ test-pvpy-all:
path: coverage.xml path: coverage.xml
allow_failure: true allow_failure: true
test-3.12-all:
<<: *only-default
image: python:3.12-rc-bullseye
script:
- tox -e py312-all
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
allow_failure: true
build-test: build-test:
stage: test stage: test
image: python:3.10-bullseye image: python:3.11-bullseye
before_script: before_script:
- python -m pip install --upgrade pip - python -m pip install --upgrade pip
@@ -195,13 +221,13 @@ build-test:
test-docs: test-docs:
<<: *only-default <<: *only-default
image: python:3.10-bullseye image: python:3.11-bullseye
script: script:
- tox -e docs - tox -e docs
deploy_production: deploy_production:
stage: deploy stage: deploy
image: python:3.10-bullseye image: python:3.11-bullseye
before_script: before_script:
- python -m pip install --upgrade pip - python -m pip install --upgrade pip
@@ -217,10 +243,10 @@ deploy_production:
build-image: build-image:
before_script: [] before_script: []
image: docker:20.10.10 image: docker:24.0
stage: docker stage: docker
services: services:
- docker:20.10.10-dind - docker:24.0-dind
script: | script: |
CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -) CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -)
IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_COMMIT_SHORT_SHA IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_COMMIT_SHORT_SHA
@@ -230,12 +256,10 @@ build-image:
LATEST_TAG=$CI_REGISTRY_IMAGE/auth:latest LATEST_TAG=$CI_REGISTRY_IMAGE/auth:latest
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
docker build . -t $IMAGE_TAG -f docker/Dockerfile --build-arg AUTH_VERSION=$(echo $CI_COMMIT_TAG | cut -c 2-) docker run --privileged --rm tonistiigi/binfmt --uninstall qemu-*
docker tag $IMAGE_TAG $CURRENT_TAG docker run --privileged --rm tonistiigi/binfmt --install all
docker tag $IMAGE_TAG $MINOR_TAG docker buildx create --use --name new-builder
docker tag $IMAGE_TAG $MAJOR_TAG docker buildx build . --tag $IMAGE_TAG --tag $CURRENT_TAG --tag $MINOR_TAG --tag $MAJOR_TAG --tag $LATEST_TAG --file docker/Dockerfile --platform linux/amd64,linux/arm64 --push --build-arg AUTH_VERSION=$(echo $CI_COMMIT_TAG | cut -c 2-)
docker tag $IMAGE_TAG $LATEST_TAG
docker image push --all-tags $CI_REGISTRY_IMAGE/auth
rules: rules:
- if: $CI_COMMIT_TAG - if: $CI_COMMIT_TAG
when: delayed when: delayed
@@ -243,17 +267,19 @@ build-image:
build-image-dev: build-image-dev:
before_script: [] before_script: []
image: docker:20.10.10 image: docker:24.0
stage: docker stage: docker
services: services:
- docker:20.10.10-dind - docker:24.0-dind
script: | script: |
CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -) CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -)
IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_COMMIT_BRANCH-$CI_COMMIT_SHORT_SHA IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_COMMIT_BRANCH-$CI_COMMIT_SHORT_SHA
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
docker build . -t $IMAGE_TAG -f docker/Dockerfile --build-arg AUTH_PACKAGE=git+https://gitlab.com/allianceauth/allianceauth@$CI_COMMIT_BRANCH docker run --privileged --rm tonistiigi/binfmt --uninstall qemu-*
docker push $IMAGE_TAG docker run --privileged --rm tonistiigi/binfmt --install all
docker buildx create --use --name new-builder
docker buildx build . --tag $IMAGE_TAG --file docker/Dockerfile --platform linux/amd64,linux/arm64 --push --build-arg AUTH_PACKAGE=git+https://gitlab.com/allianceauth/allianceauth@$CI_COMMIT_BRANCH
rules: rules:
- if: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == ""' - if: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == ""'
when: manual when: manual
@@ -262,17 +288,19 @@ build-image-dev:
build-image-mr: build-image-mr:
before_script: [] before_script: []
image: docker:20.10.10 image: docker:24.0
stage: docker stage: docker
services: services:
- docker:20.10.10-dind - docker:24.0-dind
script: | script: |
CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -) CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -)
IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME-$CI_COMMIT_SHORT_SHA IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME-$CI_COMMIT_SHORT_SHA
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
docker build . -t $IMAGE_TAG -f docker/Dockerfile --build-arg AUTH_PACKAGE=git+$CI_MERGE_REQUEST_SOURCE_PROJECT_URL@$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME docker run --privileged --rm tonistiigi/binfmt --uninstall qemu-*
docker push $IMAGE_TAG docker run --privileged --rm tonistiigi/binfmt --install all
docker buildx create --use --name new-builder
docker buildx build . --tag $IMAGE_TAG --file docker/Dockerfile --platform linux/amd64,linux/arm64 --push --build-arg AUTH_PACKAGE=git+$CI_MERGE_REQUEST_SOURCE_PROJECT_URL@$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
rules: rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: manual when: manual

View File

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

View File

@@ -7,12 +7,33 @@ repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0 rev: v4.4.0
hooks: hooks:
- id: check-case-conflict # Identify invalid files
- id: check-json - id: check-ast
- id: check-xml
- id: check-yaml - id: check-yaml
- id: check-json
- id: check-toml
- id: check-xml
# git checks
- id: check-merge-conflict
- id: check-added-large-files
args: [ --maxkb=1000 ]
- id: detect-private-key
- id: check-case-conflict
# Python checks
# - id: check-docstring-first
- id: debug-statements
# - id: requirements-txt-fixer
- id: fix-encoding-pragma
args: [ --remove ]
- id: fix-byte-order-marker - id: fix-byte-order-marker
# General quality checks
- id: mixed-line-ending
args: [ --fix=lf ]
- id: trailing-whitespace - id: trailing-whitespace
args: [ --markdown-linebreak-ext=md ]
exclude: | exclude: |
(?x)( (?x)(
\.min\.css| \.min\.css|
@@ -21,6 +42,7 @@ repos:
\.mo| \.mo|
swagger\.json swagger\.json
) )
- id: check-executables-have-shebangs
- id: end-of-file-fixer - id: end-of-file-fixer
exclude: | exclude: |
(?x)( (?x)(
@@ -30,13 +52,9 @@ repos:
\.mo| \.mo|
swagger\.json swagger\.json
) )
- id: mixed-line-ending
args: [ '--fix=lf' ]
- id: fix-encoding-pragma
args: [ '--remove' ]
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python - repo: https://github.com/editorconfig-checker/editorconfig-checker.python
rev: 2.7.1 rev: 2.7.2
hooks: hooks:
- id: editorconfig-checker - id: editorconfig-checker
exclude: | exclude: |
@@ -49,13 +67,19 @@ repos:
) )
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.3.1 rev: v3.10.1
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [ --py38-plus ] args: [ --py38-plus ]
- repo: https://github.com/adamchainz/django-upgrade
rev: 1.14.0
hooks:
- id: django-upgrade
args: [--target-version=4.2]
- repo: https://github.com/asottile/setup-cfg-fmt - repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.2.0 rev: v2.3.0
hooks: hooks:
- id: setup-cfg-fmt - id: setup-cfg-fmt
args: [ --include-version-classifiers ] args: [ --include-version-classifiers ]

View File

@@ -23,4 +23,7 @@ formats: all
# Optionally set the version of Python and requirements required to build your docs # Optionally set the version of Python and requirements required to build your docs
python: python:
install: install:
- requirements: docs/requirements.txt - method: pip
path: .
extra_requirements:
- docs

0
.tx/config_20230406134150.bak Executable file → Normal file
View File

View File

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

0
README.md Executable file → Normal file
View File

View File

@@ -1,7 +1,11 @@
"""An auth system for EVE Online to help in-game organizations
manage online service access.
"""
# This will make sure the app is always imported when # This will make sure the app is always imported when
# Django starts so that shared_task will use this app. # Django starts so that shared_task will use this app.
__version__ = '3.4.0' __version__ = '4.0.0a4'
__title__ = 'Alliance Auth' __title__ = 'Alliance Auth'
__url__ = 'https://gitlab.com/allianceauth/allianceauth' __url__ = 'https://gitlab.com/allianceauth/allianceauth'
NAME = f'{__title__} v{__version__}' NAME = f'{__title__} v{__version__}'

View File

@@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from .models import AnalyticsIdentifier, AnalyticsPath, AnalyticsTokens from .models import AnalyticsIdentifier, AnalyticsTokens
@admin.register(AnalyticsIdentifier) @admin.register(AnalyticsIdentifier)
@@ -13,9 +13,3 @@ class AnalyticsIdentifierAdmin(admin.ModelAdmin):
class AnalyticsTokensAdmin(admin.ModelAdmin): class AnalyticsTokensAdmin(admin.ModelAdmin):
search_fields = ['name', ] search_fields = ['name', ]
list_display = ('name', 'type',) list_display = ('name', 'type',)
@admin.register(AnalyticsPath)
class AnalyticsPathAdmin(admin.ModelAdmin):
search_fields = ['ignore_path', ]
list_display = ('ignore_path',)

View File

@@ -4,6 +4,3 @@ from django.apps import AppConfig
class AnalyticsConfig(AppConfig): class AnalyticsConfig(AppConfig):
name = 'allianceauth.analytics' name = 'allianceauth.analytics'
label = 'analytics' label = 'analytics'
def ready(self):
import allianceauth.analytics.signals

View File

@@ -3,11 +3,10 @@
"model": "analytics.AnalyticsTokens", "model": "analytics.AnalyticsTokens",
"pk": 1, "pk": 1,
"fields": { "fields": {
"name": "AA Team Public Google Analytics (Universal)", "name": "AA Team Public Google Analytics (V4)",
"type": "GA-V4", "type": "GA-V4",
"token": "UA-186249766-2", "token": "G-6LYSMYK8DE",
"send_page_views": "False", "secret": "KLlpjLZ-SRGozS5f5wb_kw",
"send_celery_tasks": "False",
"send_stats": "False" "send_stats": "False"
} }
}, },

View File

@@ -1,52 +0,0 @@
from bs4 import BeautifulSoup
from django.conf import settings
from django.utils.deprecation import MiddlewareMixin
from .models import AnalyticsTokens, AnalyticsIdentifier
from .tasks import send_ga_tracking_web_view
import re
class AnalyticsMiddleware(MiddlewareMixin):
def process_response(self, request, response):
"""Django Middleware: Process Page Views and creates Analytics Celery Tasks"""
if getattr(settings, "ANALYTICS_DISABLED", False):
return response
analyticstokens = AnalyticsTokens.objects.all()
client_id = AnalyticsIdentifier.objects.get(id=1).identifier.hex
try:
title = BeautifulSoup(
response.content, "html.parser").html.head.title.text
except AttributeError:
title = ''
for token in analyticstokens:
# Check if Page View Sending is Disabled
if token.send_page_views is False:
continue
# Check Exclusions
ignore = False
for ignore_path in token.ignore_paths.values():
ignore_path_regex = re.compile(ignore_path["ignore_path"])
if re.search(ignore_path_regex, request.path) is not None:
ignore = True
if ignore is True:
continue
tracking_id = token.token
locale = request.LANGUAGE_CODE
path = request.path
try:
useragent = request.headers["User-Agent"]
except KeyError:
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
send_ga_tracking_web_view.s(tracking_id=tracking_id,
client_id=client_id,
page=path,
title=title,
locale=locale,
useragent=useragent).\
apply_async(priority=9)
return response

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.6 on 2022-08-30 05:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('analytics', '0006_more_ignore_paths'),
]
operations = [
migrations.AddField(
model_name='analyticstokens',
name='secret',
field=models.CharField(blank=True, max_length=254),
),
]

View File

@@ -0,0 +1,64 @@
# Generated by Django 3.1.4 on 2020-12-30 08:53
from django.db import migrations
from django.core.exceptions import ObjectDoesNotExist
def add_aa_team_token(apps, schema_editor):
# We can't import the Person model directly as it may be a newer
# version than this migration expects. We use the historical version.
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
AnalyticsPath = apps.get_model('analytics', 'AnalyticsPath')
token = Tokens()
try:
ua_token = Tokens.objects.get(token="UA-186249766-2")
original_send_page_views = ua_token.send_page_views
original_send_celery_tasks = ua_token.send_celery_tasks
original_send_stats = ua_token.send_stats
except ObjectDoesNotExist:
original_send_page_views = True
original_send_celery_tasks = True
original_send_stats = True
try:
user_notifications_count = AnalyticsPath.objects.get(ignore_path=r"^\/user_notifications_count\/.*",)
except ObjectDoesNotExist:
user_notifications_count = AnalyticsPath.objects.create(ignore_path=r"^\/user_notifications_count\/.*")
try:
admin = AnalyticsPath.objects.get(ignore_path=r"^\/admin\/.*")
except ObjectDoesNotExist:
admin = AnalyticsPath.objects.create(ignore_path=r"^\/admin\/.*")
try:
account_activate = AnalyticsPath.objects.get(ignore_path=r"^\/account\/activate\/.*")
except ObjectDoesNotExist:
account_activate = AnalyticsPath.objects.create(ignore_path=r"^\/account\/activate\/.*")
token.type = 'GA-V4'
token.token = 'G-6LYSMYK8DE'
token.secret = 'KLlpjLZ-SRGozS5f5wb_kw'
token.send_page_views = original_send_page_views
token.send_celery_tasks = original_send_celery_tasks
token.send_stats = original_send_stats
token.name = 'AA Team Public Google Analytics (V4)'
token.save()
token.ignore_paths.add(admin, user_notifications_count, account_activate)
token.save()
def remove_aa_team_token(apps, schema_editor):
# Have to define some code to remove this identifier
# In case of migration rollback?
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
token = Tokens.objects.filter(token="G-6LYSMYK8DE").delete()
class Migration(migrations.Migration):
dependencies = [
('analytics', '0007_analyticstokens_secret'),
]
operations = [migrations.RunPython(
add_aa_team_token, remove_aa_team_token)]

View File

@@ -0,0 +1,28 @@
# Generated by Django 4.0.10 on 2023-05-08 05:24
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('analytics', '0008_add_AA_GA-4_Team_Token '),
]
operations = [
migrations.RemoveField(
model_name='analyticstokens',
name='ignore_paths',
),
migrations.RemoveField(
model_name='analyticstokens',
name='send_celery_tasks',
),
migrations.RemoveField(
model_name='analyticstokens',
name='send_page_views',
),
migrations.DeleteModel(
name='AnalyticsPath',
),
]

View File

@@ -7,22 +7,19 @@ from uuid import uuid4
class AnalyticsIdentifier(models.Model): class AnalyticsIdentifier(models.Model):
identifier = models.UUIDField(default=uuid4, identifier = models.UUIDField(
editable=False) default=uuid4,
editable=False)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.pk and AnalyticsIdentifier.objects.exists(): if not self.pk and AnalyticsIdentifier.objects.exists():
# Force a single object # Force a single object
raise ValidationError('There is can be only one \ raise ValidationError('There is can be only one \
AnalyticsIdentifier instance') AnalyticsIdentifier instance')
self.pk = self.id = 1 # If this happens to be deleted and recreated, force it to be 1 self.pk = self.id = 1 # If this happens to be deleted and recreated, force it to be 1
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
class AnalyticsPath(models.Model):
ignore_path = models.CharField(max_length=254, default="/example/", help_text="Regex Expression, If matched no Analytics Page View is sent")
class AnalyticsTokens(models.Model): class AnalyticsTokens(models.Model):
class Analytics_Type(models.TextChoices): class Analytics_Type(models.TextChoices):
@@ -32,7 +29,5 @@ class AnalyticsTokens(models.Model):
name = models.CharField(max_length=254) name = models.CharField(max_length=254)
type = models.CharField(max_length=254, choices=Analytics_Type.choices) type = models.CharField(max_length=254, choices=Analytics_Type.choices)
token = models.CharField(max_length=254, blank=False) token = models.CharField(max_length=254, blank=False)
send_page_views = models.BooleanField(default=False) secret = models.CharField(max_length=254, blank=True)
send_celery_tasks = models.BooleanField(default=False)
send_stats = models.BooleanField(default=False) send_stats = models.BooleanField(default=False)
ignore_paths = models.ManyToManyField(AnalyticsPath, blank=True)

View File

@@ -1,55 +0,0 @@
import logging
from celery.signals import task_failure, task_success
from django.conf import settings
from allianceauth.analytics.tasks import analytics_event
logger = logging.getLogger(__name__)
@task_failure.connect
def process_failure_signal(
exception, traceback,
sender, task_id, signal,
args, kwargs, einfo, **kw):
logger.debug("Celery task_failure signal %s" % sender.__class__.__name__)
if getattr(settings, "ANALYTICS_DISABLED", False):
return
category = sender.__module__
if 'allianceauth.analytics' not in category:
if category.endswith(".tasks"):
category = category[:-6]
action = sender.__name__
label = f"{exception.__class__.__name__}"
analytics_event(category=category,
action=action,
label=label)
@task_success.connect
def celery_success_signal(sender, result=None, **kw):
logger.debug("Celery task_success signal %s" % sender.__class__.__name__)
if getattr(settings, "ANALYTICS_DISABLED", False):
return
category = sender.__module__
if 'allianceauth.analytics' not in category:
if category.endswith(".tasks"):
category = category[:-6]
action = sender.__name__
label = "Success"
value = 0
if isinstance(result, int):
value = result
analytics_event(category=category,
action=action,
label=label,
value=value)

View File

@@ -3,7 +3,6 @@ import logging
from django.conf import settings from django.conf import settings
from django.apps import apps from django.apps import apps
from celery import shared_task from celery import shared_task
from allianceauth import __version__
from .models import AnalyticsTokens, AnalyticsIdentifier from .models import AnalyticsTokens, AnalyticsIdentifier
from .utils import ( from .utils import (
install_stat_addons, install_stat_addons,
@@ -12,14 +11,14 @@ from .utils import (
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
BASE_URL = "https://www.google-analytics.com/" BASE_URL = "https://www.google-analytics.com"
DEBUG_URL = f"{BASE_URL}debug/collect" DEBUG_URL = f"{BASE_URL}/debug/mp/collect"
COLLECTION_URL = f"{BASE_URL}collect" COLLECTION_URL = f"{BASE_URL}/mp/collect"
if getattr(settings, "ANALYTICS_ENABLE_DEBUG", False) and settings.DEBUG: if getattr(settings, "ANALYTICS_ENABLE_DEBUG", False) and settings.DEBUG:
# Force sending of analytics data during in a debug/test environemt # Force sending of analytics data during in a debug/test environment
# Usefull for developers working on this feature. # Useful for developers working on this feature.
logger.warning( logger.warning(
"You have 'ANALYTICS_ENABLE_DEBUG' Enabled! " "You have 'ANALYTICS_ENABLE_DEBUG' Enabled! "
"This debug instance will send analytics data!") "This debug instance will send analytics data!")
@@ -31,40 +30,38 @@ if settings.DEBUG is True:
ANALYTICS_URL = DEBUG_URL ANALYTICS_URL = DEBUG_URL
def analytics_event(category: str, def analytics_event(namespace: str,
action: str, task: str,
label: str, label: str = "",
value: int = 0, result: str = "",
value: int = 1,
event_type: str = 'Celery'): event_type: str = 'Celery'):
""" """
Send a Google Analytics Event for each token stored Send a Google Analytics Event for each token stored
Includes check for if its enabled/disabled Includes check for if its enabled/disabled
Args: Args:
`category` (str): Celery Namespace `namespace` (str): Celery Namespace
`action` (str): Task Name `task` (str): Task Name
`label` (str): Optional, Task Success/Exception `label` (str): Optional, additional task label
`value` (int): Optional, If bulk, Query size, can be a binary True/False `result` (str): Optional, Task Success/Exception
`value` (int): Optional, If bulk, Query size, can be a Boolean
`event_type` (str): Optional, Celery or Stats only, Default to Celery `event_type` (str): Optional, Celery or Stats only, Default to Celery
""" """
analyticstokens = AnalyticsTokens.objects.all() for token in AnalyticsTokens.objects.filter(type='GA-V4'):
client_id = AnalyticsIdentifier.objects.get(id=1).identifier.hex if event_type == 'Stats':
for token in analyticstokens:
if event_type == 'Celery':
allowed = token.send_celery_tasks
elif event_type == 'Stats':
allowed = token.send_stats allowed = token.send_stats
else: else:
allowed = False allowed = False
if allowed is True: if allowed is True:
tracking_id = token.token
send_ga_tracking_celery_event.s( send_ga_tracking_celery_event.s(
tracking_id=tracking_id, measurement_id=token.token,
client_id=client_id, secret=token.secret,
category=category, namespace=namespace,
action=action, task=task,
label=label, label=label,
result=result,
value=value).apply_async(priority=9) value=value).apply_async(priority=9)
@@ -72,136 +69,104 @@ def analytics_event(category: str,
def analytics_daily_stats(): def analytics_daily_stats():
"""Celery Task: Do not call directly """Celery Task: Do not call directly
Gathers a series of daily statistics and sends analytics events containing them Gathers a series of daily statistics
Sends analytics events containing them
""" """
users = install_stat_users() users = install_stat_users()
tokens = install_stat_tokens() tokens = install_stat_tokens()
addons = install_stat_addons() addons = install_stat_addons()
logger.debug("Running Daily Analytics Upload") logger.debug("Running Daily Analytics Upload")
analytics_event(category='allianceauth.analytics', analytics_event(namespace='allianceauth.analytics',
action='send_install_stats', task='send_install_stats',
label='existence', label='existence',
value=1, value=1,
event_type='Stats') event_type='Stats')
analytics_event(category='allianceauth.analytics', analytics_event(namespace='allianceauth.analytics',
action='send_install_stats', task='send_install_stats',
label='users', label='users',
value=users, value=users,
event_type='Stats') event_type='Stats')
analytics_event(category='allianceauth.analytics', analytics_event(namespace='allianceauth.analytics',
action='send_install_stats', task='send_install_stats',
label='tokens', label='tokens',
value=tokens, value=tokens,
event_type='Stats') event_type='Stats')
analytics_event(category='allianceauth.analytics', analytics_event(namespace='allianceauth.analytics',
action='send_install_stats', task='send_install_stats',
label='addons', label='addons',
value=addons, value=addons,
event_type='Stats') event_type='Stats')
for appconfig in apps.get_app_configs(): for appconfig in apps.get_app_configs():
analytics_event(category='allianceauth.analytics', analytics_event(namespace='allianceauth.analytics',
action='send_extension_stats', task='send_extension_stats',
label=appconfig.label, label=appconfig.label,
value=1, value=1,
event_type='Stats') event_type='Stats')
@shared_task()
def send_ga_tracking_web_view(
tracking_id: str,
client_id: str,
page: str,
title: str,
locale: str,
useragent: str) -> requests.Response:
"""Celery Task: Do not call directly
Sends Page View events to GA, Called only via analytics.middleware
Parameters
----------
`tracking_id` (str): Unique Server Identifier
`client_id` (str): GA Token
`page` (str): Page Path
`title` (str): Page Title
`locale` (str): Browser Language
`useragent` (str): Browser UserAgent
Returns
-------
requests.Reponse Object
"""
headers = {"User-Agent": useragent}
payload = {
'v': '1',
'tid': tracking_id,
'cid': client_id,
't': 'pageview',
'dp': page,
'dt': title,
'ul': locale,
'ua': useragent,
'aip': 1,
'an': "allianceauth",
'av': __version__
}
response = requests.post(
ANALYTICS_URL, data=payload,
timeout=5, headers=headers)
logger.debug(f"Analytics Page View HTTP{response.status_code}")
return response
@shared_task() @shared_task()
def send_ga_tracking_celery_event( def send_ga_tracking_celery_event(
tracking_id: str, measurement_id: str,
client_id: str, secret: str,
category: str, namespace: str,
action: str, task: str,
label: str, label: str = "",
value: int) -> requests.Response: result: str = "",
value: int = 1):
"""Celery Task: Do not call directly """Celery Task: Do not call directly
Sends Page View events to GA, Called only via analytics.middleware Sends an events to GA
Parameters Parameters
---------- ----------
`tracking_id` (str): Unique Server Identifier `measurement_id` (str): GA Token
`client_id` (str): GA Token `secret` (str): GA Authentication Secret
`category` (str): Celery Namespace `namespace` (str): Celery Namespace
`action` (str): Task Name `task` (str): Task Name
`label` (str): Optional, Task Success/Exception `label` (str): Optional, additional task label
`result` (str): Optional, Task Success/Exception
`value` (int): Optional, If bulk, Query size, can be a binary True/False `value` (int): Optional, If bulk, Query size, can be a binary True/False
Returns
-------
requests.Reponse Object
""" """
headers = { parameters = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"} 'measurement_id': measurement_id,
'api_secret': secret
}
payload = { payload = {
'v': '1', 'client_id': AnalyticsIdentifier.objects.get(id=1).identifier.hex,
'tid': tracking_id, "user_properties": {
'cid': client_id, "allianceauth_version": {
't': 'event', "value": "allianceauth_version"
'ec': category,
'ea': action,
'el': label,
'ev': value,
'aip': 1,
'an': "allianceauth",
'av': __version__
} }
},
response = requests.post( 'non_personalized_ads': True,
ANALYTICS_URL, data=payload, "events": [{
timeout=5, headers=headers) "name": "celery_event",
logger.debug(f"Analytics Celery/Stats Event HTTP{response.status_code}") "params": {
return response "namespace": namespace,
"task": task,
'result': result,
'label': label,
"value": value
}
}]
}
try:
response = requests.post(
ANALYTICS_URL,
params=parameters,
json=payload,
timeout=10)
response.raise_for_status()
logger.debug(
f"Analytics Celery/Stats Event HTTP{response.status_code}")
return response.status_code
except requests.exceptions.HTTPError as e:
logger.debug(e)
return response.status_code
except requests.exceptions.ConnectionError as e:
logger.debug(e)
return "Failed"

View File

@@ -1,109 +0,0 @@
from unittest.mock import patch
from urllib.parse import parse_qs
import requests_mock
from django.test import override_settings
from allianceauth.analytics.tasks import ANALYTICS_URL
from allianceauth.eveonline.tasks import update_character
from allianceauth.tests.auth_utils import AuthUtils
from allianceauth.utils.testing import NoSocketsTestCase
@override_settings(CELERY_ALWAYS_EAGER=True)
@requests_mock.mock()
class TestAnalyticsForViews(NoSocketsTestCase):
@override_settings(ANALYTICS_DISABLED=False)
def test_should_run_analytics(self, requests_mocker):
# given
requests_mocker.post(ANALYTICS_URL)
user = AuthUtils.create_user("Bruce Wayne")
self.client.force_login(user)
# when
response = self.client.get("/dashboard/")
# then
self.assertEqual(response.status_code, 200)
self.assertTrue(requests_mocker.called)
@override_settings(ANALYTICS_DISABLED=True)
def test_should_not_run_analytics(self, requests_mocker):
# given
requests_mocker.post(ANALYTICS_URL)
user = AuthUtils.create_user("Bruce Wayne")
self.client.force_login(user)
# when
response = self.client.get("/dashboard/")
# then
self.assertEqual(response.status_code, 200)
self.assertFalse(requests_mocker.called)
@override_settings(CELERY_ALWAYS_EAGER=True)
@requests_mock.mock()
class TestAnalyticsForTasks(NoSocketsTestCase):
@override_settings(ANALYTICS_DISABLED=False)
@patch("allianceauth.eveonline.models.EveCharacter.objects.update_character")
def test_should_run_analytics_for_successful_task(
self, requests_mocker, mock_update_character
):
# given
requests_mocker.post(ANALYTICS_URL)
user = AuthUtils.create_user("Bruce Wayne")
character = AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
# when
update_character.delay(character.character_id)
# then
self.assertTrue(mock_update_character.called)
self.assertTrue(requests_mocker.called)
payload = parse_qs(requests_mocker.last_request.text)
self.assertListEqual(payload["el"], ["Success"])
@override_settings(ANALYTICS_DISABLED=True)
@patch("allianceauth.eveonline.models.EveCharacter.objects.update_character")
def test_should_not_run_analytics_for_successful_task(
self, requests_mocker, mock_update_character
):
# given
requests_mocker.post(ANALYTICS_URL)
user = AuthUtils.create_user("Bruce Wayne")
character = AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
# when
update_character.delay(character.character_id)
# then
self.assertTrue(mock_update_character.called)
self.assertFalse(requests_mocker.called)
@override_settings(ANALYTICS_DISABLED=False)
@patch("allianceauth.eveonline.models.EveCharacter.objects.update_character")
def test_should_run_analytics_for_failed_task(
self, requests_mocker, mock_update_character
):
# given
requests_mocker.post(ANALYTICS_URL)
mock_update_character.side_effect = RuntimeError
user = AuthUtils.create_user("Bruce Wayne")
character = AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
# when
update_character.delay(character.character_id)
# then
self.assertTrue(mock_update_character.called)
self.assertTrue(requests_mocker.called)
payload = parse_qs(requests_mocker.last_request.text)
self.assertNotEqual(payload["el"], ["Success"])
@override_settings(ANALYTICS_DISABLED=True)
@patch("allianceauth.eveonline.models.EveCharacter.objects.update_character")
def test_should_not_run_analytics_for_failed_task(
self, requests_mocker, mock_update_character
):
# given
requests_mocker.post(ANALYTICS_URL)
mock_update_character.side_effect = RuntimeError
user = AuthUtils.create_user("Bruce Wayne")
character = AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
# when
update_character.delay(character.character_id)
# then
self.assertTrue(mock_update_character.called)
self.assertFalse(requests_mocker.called)

View File

@@ -1,24 +0,0 @@
from allianceauth.analytics.middleware import AnalyticsMiddleware
from unittest.mock import Mock
from django.http import HttpResponse
from django.test.testcases import TestCase
class TestAnalyticsMiddleware(TestCase):
def setUp(self):
self.middleware = AnalyticsMiddleware(HttpResponse)
self.request = Mock()
self.request.headers = {
"User-Agent": "AUTOMATED TEST"
}
self.request.path = '/testURL/'
self.request.session = {}
self.request.LANGUAGE_CODE = 'en'
self.response = Mock()
self.response.content = 'hello world'
def test_middleware(self):
response = self.middleware.process_response(self.request, self.response)
self.assertEqual(self.response, response)

View File

@@ -23,4 +23,5 @@ class TestAnalyticsIdentifier(TestCase):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
AnalyticsIdentifier.objects.create(identifier=uuid_2) AnalyticsIdentifier.objects.create(identifier=uuid_2)
self.assertEqual(AnalyticsIdentifier.objects.count(), 1) self.assertEqual(AnalyticsIdentifier.objects.count(), 1)
self.assertEqual(AnalyticsIdentifier.objects.get(pk=1).identifier, UUID(uuid_1)) self.assertEqual(AnalyticsIdentifier.objects.get(
pk=1).identifier, UUID(uuid_1))

View File

@@ -4,12 +4,11 @@ from django.test.utils import override_settings
from allianceauth.analytics.tasks import ( from allianceauth.analytics.tasks import (
analytics_event, analytics_event,
send_ga_tracking_celery_event, send_ga_tracking_celery_event)
send_ga_tracking_web_view)
from allianceauth.utils.testing import NoSocketsTestCase from allianceauth.utils.testing import NoSocketsTestCase
GOOGLE_ANALYTICS_DEBUG_URL = 'https://www.google-analytics.com/debug/collect' GOOGLE_ANALYTICS_DEBUG_URL = 'https://www.google-analytics.com/debug/mp/collect'
@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True) @override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
@@ -18,195 +17,53 @@ class TestAnalyticsTasks(NoSocketsTestCase):
def test_analytics_event(self, requests_mocker): def test_analytics_event(self, requests_mocker):
requests_mocker.register_uri('POST', GOOGLE_ANALYTICS_DEBUG_URL) requests_mocker.register_uri('POST', GOOGLE_ANALYTICS_DEBUG_URL)
analytics_event( analytics_event(
category='allianceauth.analytics', namespace='allianceauth.analytics',
action='send_tests', task='send_tests',
label='test', label='test',
value=1, value=1,
event_type='Stats') result="Success",
event_type='Stats')
def test_send_ga_tracking_web_view_sent(self, requests_mocker):
"""This test sends if the event SENDS to google.
Not if it was successful.
"""
# given
requests_mocker.register_uri('POST', GOOGLE_ANALYTICS_DEBUG_URL)
tracking_id = 'UA-186249766-2'
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
page = '/index/'
title = 'Hello World'
locale = 'en'
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
# when
response = send_ga_tracking_web_view(
tracking_id,
client_id,
page,
title,
locale,
useragent)
# then
self.assertEqual(response.status_code, 200)
def test_send_ga_tracking_web_view_success(self, requests_mocker):
# given
requests_mocker.register_uri(
'POST',
GOOGLE_ANALYTICS_DEBUG_URL,
json={"hitParsingResult":[{'valid': True}]}
)
tracking_id = 'UA-186249766-2'
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
page = '/index/'
title = 'Hello World'
locale = 'en'
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
# when
json_response = send_ga_tracking_web_view(
tracking_id,
client_id,
page,
title,
locale,
useragent).json()
# then
self.assertTrue(json_response["hitParsingResult"][0]["valid"])
def test_send_ga_tracking_web_view_invalid_token(self, requests_mocker):
# given
requests_mocker.register_uri(
'POST',
GOOGLE_ANALYTICS_DEBUG_URL,
json={
"hitParsingResult":[
{
'valid': False,
'parserMessage': [
{
'messageType': 'INFO',
'description': 'IP Address from this hit was anonymized to 1.132.110.0.',
'messageCode': 'VALUE_MODIFIED'
},
{
'messageType': 'ERROR',
'description': "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.",
'messageCode': 'VALUE_INVALID', 'parameter': 'tid'
}
],
'hit': '/debug/collect?v=1&tid=UA-IntentionallyBadTrackingID-2&cid=ab33e241fbf042b6aa77c7655a768af7&t=pageview&dp=/index/&dt=Hello World&ul=en&ua=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36&aip=1&an=allianceauth&av=2.9.0a2'
}
]
}
)
tracking_id = 'UA-IntentionallyBadTrackingID-2'
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
page = '/index/'
title = 'Hello World'
locale = 'en'
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
# when
json_response = send_ga_tracking_web_view(
tracking_id,
client_id,
page,
title,
locale,
useragent).json()
# then
self.assertFalse(json_response["hitParsingResult"][0]["valid"])
self.assertEqual(
json_response["hitParsingResult"][0]["parserMessage"][1]["description"],
"The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details."
)
# [{'valid': False, 'parserMessage': [{'messageType': 'INFO', 'description': 'IP Address from this hit was anonymized to 1.132.110.0.', 'messageCode': 'VALUE_MODIFIED'}, {'messageType': 'ERROR', 'description': "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.", 'messageCode': 'VALUE_INVALID', 'parameter': 'tid'}], 'hit': '/debug/collect?v=1&tid=UA-IntentionallyBadTrackingID-2&cid=ab33e241fbf042b6aa77c7655a768af7&t=pageview&dp=/index/&dt=Hello World&ul=en&ua=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36&aip=1&an=allianceauth&av=2.9.0a2'}]
def test_send_ga_tracking_celery_event_sent(self, requests_mocker): def test_send_ga_tracking_celery_event_sent(self, requests_mocker):
# given # given
requests_mocker.register_uri('POST', GOOGLE_ANALYTICS_DEBUG_URL) requests_mocker.register_uri('POST', GOOGLE_ANALYTICS_DEBUG_URL)
tracking_id = 'UA-186249766-2' token = 'G-6LYSMYK8DE'
client_id = 'ab33e241fbf042b6aa77c7655a768af7' secret = 'KLlpjLZ-SRGozS5f5wb_kw',
category = 'test' category = 'test'
action = 'test' action = 'test'
label = 'test' label = 'test'
value = '1' value = '1'
# when # when
response = send_ga_tracking_celery_event( task = send_ga_tracking_celery_event(
tracking_id, token,
client_id, secret,
category, category,
action, action,
label, label,
value) value)
# then # then
self.assertEqual(response.status_code, 200) self.assertEqual(task, 200)
def test_send_ga_tracking_celery_event_success(self, requests_mocker): def test_send_ga_tracking_celery_event_success(self, requests_mocker):
# given # given
requests_mocker.register_uri( requests_mocker.register_uri(
'POST', 'POST',
GOOGLE_ANALYTICS_DEBUG_URL, GOOGLE_ANALYTICS_DEBUG_URL,
json={"hitParsingResult":[{'valid': True}]} json={"validationMessages": []}
) )
tracking_id = 'UA-186249766-2' token = 'G-6LYSMYK8DE'
client_id = 'ab33e241fbf042b6aa77c7655a768af7' secret = 'KLlpjLZ-SRGozS5f5wb_kw',
category = 'test' category = 'test'
action = 'test' action = 'test'
label = 'test' label = 'test'
value = '1' value = '1'
# when # when
json_response = send_ga_tracking_celery_event( task = send_ga_tracking_celery_event(
tracking_id, token,
client_id, secret,
category, category,
action, action,
label, label,
value).json() value)
# then # then
self.assertTrue(json_response["hitParsingResult"][0]["valid"]) self.assertTrue(task, 200)
def test_send_ga_tracking_celery_event_invalid_token(self, requests_mocker):
# given
requests_mocker.register_uri(
'POST',
GOOGLE_ANALYTICS_DEBUG_URL,
json={
"hitParsingResult":[
{
'valid': False,
'parserMessage': [
{
'messageType': 'INFO',
'description': 'IP Address from this hit was anonymized to 1.132.110.0.',
'messageCode': 'VALUE_MODIFIED'
},
{
'messageType': 'ERROR',
'description': "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.",
'messageCode': 'VALUE_INVALID', 'parameter': 'tid'
}
],
'hit': '/debug/collect?v=1&tid=UA-IntentionallyBadTrackingID-2&cid=ab33e241fbf042b6aa77c7655a768af7&t=pageview&dp=/index/&dt=Hello World&ul=en&ua=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36&aip=1&an=allianceauth&av=2.9.0a2'
}
]
}
)
tracking_id = 'UA-IntentionallyBadTrackingID-2'
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
category = 'test'
action = 'test'
label = 'test'
value = '1'
# when
json_response = send_ga_tracking_celery_event(
tracking_id,
client_id,
category,
action,
label,
value).json()
# then
self.assertFalse(json_response["hitParsingResult"][0]["valid"])
self.assertEqual(
json_response["hitParsingResult"][0]["parserMessage"][1]["description"],
"The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details."
)

View File

@@ -18,17 +18,17 @@ def create_testdata():
'abc@example.com', 'abc@example.com',
'password' 'password'
) )
#Token.objects.all().delete() # Token.objects.all().delete()
#Token.objects.create( # Token.objects.create(
# character_id=101, # character_id=101,
# character_name='character1', # character_name='character1',
# access_token='my_access_token' # access_token='my_access_token'
#) # )
#Token.objects.create( # Token.objects.create(
# character_id=102, # character_id=102,
# character_name='character2', # character_name='character2',
# access_token='my_access_token' # access_token='my_access_token'
#) # )
class TestAnalyticsUtils(TestCase): class TestAnalyticsUtils(TestCase):
@@ -40,7 +40,7 @@ class TestAnalyticsUtils(TestCase):
users = install_stat_users() users = install_stat_users()
self.assertEqual(users, expected) self.assertEqual(users, expected)
#def test_install_stat_tokens(self): # def test_install_stat_tokens(self):
# create_testdata() # create_testdata()
# expected = 2 # expected = 2
# #

View File

@@ -1,5 +1,30 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.core.checks import Warning, Error, register
class AllianceAuthConfig(AppConfig): class AllianceAuthConfig(AppConfig):
name = 'allianceauth' name = 'allianceauth'
@register()
def check_settings(app_configs, **kwargs):
from django.conf import settings
errors = []
if hasattr(settings, "SITE_URL"):
if settings.SITE_URL[-1] == "/":
errors.append(Warning(
"'SITE_URL' Has a trailing slash. This may lead to incorrect links being generated by Auth."))
else:
errors.append(Error(
"No 'SITE_URL' found is settings. This may lead to incorrect links being generated by Auth or Errors in 3rd party modules."))
if hasattr(settings, "CSRF_TRUSTED_ORIGINS"):
if hasattr(settings, "SITE_URL"):
if settings.SITE_URL not in settings.CSRF_TRUSTED_ORIGINS:
errors.append(Warning(
"'SITE_URL' not found in 'CSRF_TRUSTED_ORIGINS'. Auth may not load pages correctly until this is rectified."))
else:
errors.append(Error(
"No 'CSRF_TRUSTED_ORIGINS' found is settings, Auth may not load pages correctly until this is rectified"))
return errors

View File

@@ -288,7 +288,7 @@ class UserAdmin(BaseUserAdmin):
Behavior of groups and characters columns can be configured via settings Behavior of groups and characters columns can be configured via settings
""" """
inlines = BaseUserAdmin.inlines + [UserProfileInline] inlines = [UserProfileInline]
ordering = ('username', ) ordering = ('username', )
list_select_related = ('profile__state', 'profile__main_character') list_select_related = ('profile__state', 'profile__main_character')
show_full_result_count = True show_full_result_count = True

View File

@@ -0,0 +1,45 @@
from allianceauth.hooks import DashboardItemHook
from allianceauth import hooks
from .views import dashboard_characters, dashboard_groups, dashboard_admin
class UserCharactersHook(DashboardItemHook):
def __init__(self):
DashboardItemHook.__init__(
self,
dashboard_characters,
5
)
class UserGroupsHook(DashboardItemHook):
def __init__(self):
DashboardItemHook.__init__(
self,
dashboard_groups,
5
)
class AdminHook(DashboardItemHook):
def __init__(self):
DashboardItemHook.__init__(
self,
dashboard_admin,
0
)
@hooks.register('dashboard_hook')
def register_character_hook():
return UserCharactersHook()
@hooks.register('dashboard_hook')
def register_groups_hook():
return UserGroupsHook()
@hooks.register('dashboard_hook')
def register_admin_hook():
return AdminHook()

View File

@@ -65,7 +65,7 @@ class StateBackend(ModelBackend):
# we've seen this character owner before. Re-attach to their old user account # we've seen this character owner before. Re-attach to their old user account
user = records[0].user user = records[0].user
if user.profile.main_character: if user.profile.main_character:
if ownership.user.profile.main_character.character_id != token.character_id: if user.profile.main_character.character_id != token.character_id:
## this is an alt, enforce main only due to trust issues in SSO. ## this is an alt, enforce main only due to trust issues in SSO.
if request: if request:
messages.error("Unable to authenticate with this Character, Please log in with the main character associated with this account. Then add this character from the dashboard.") messages.error("Unable to authenticate with this Character, Please log in with the main character associated with this account. Then add this character from the dashboard.")

View File

@@ -0,0 +1,48 @@
"""API for interacting with celery workers."""
import itertools
import logging
from typing import Optional
from amqp.exceptions import ChannelError
from celery import current_app
from django.conf import settings
logger = logging.getLogger(__name__)
def active_tasks_count() -> Optional[int]:
"""Return count of currently active tasks
or None if celery workers are not online.
"""
inspect = current_app.control.inspect()
return _tasks_count(inspect.active())
def _tasks_count(data: dict) -> Optional[int]:
"""Return count of tasks in data from celery inspect API."""
try:
tasks = itertools.chain(*data.values())
except AttributeError:
return None
return len(list(tasks))
def queued_tasks_count() -> Optional[int]:
"""Return count of queued tasks. Return None if there was an error."""
try:
with current_app.connection_or_acquire() as conn:
result = conn.default_channel.queue_declare(
queue=getattr(settings, "CELERY_DEFAULT_QUEUE", "celery"), passive=True
)
return result.message_count
except ChannelError:
# Queue doesn't exist, probably empty
return 0
except Exception:
logger.exception("Failed to get celery queue length")
return None

View File

@@ -1,18 +1,31 @@
from django.conf.urls import include from django.urls import include
from django.contrib.auth.decorators import user_passes_test from django.contrib.auth.decorators import user_passes_test
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from functools import wraps from functools import wraps
from django.shortcuts import redirect from typing import Callable, Iterable, Optional
from django.urls import include
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required, user_passes_test
from django.core.exceptions import PermissionDenied
from django.shortcuts import redirect
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.contrib.auth.decorators import login_required
def user_has_main_character(user): def user_has_main_character(user):
return bool(user.profile.main_character) return bool(user.profile.main_character)
def decorate_url_patterns(urls, decorator): def decorate_url_patterns(
urls, decorator: Callable, excluded_views: Optional[Iterable] = None
):
"""Decorate views given in url patterns except when they are explicitly excluded.
Args:
- urls: Django URL patterns
- decorator: Decorator to be added to each view
- exclude_views: Optional iterable of view names to be excluded
"""
url_list, app_name, namespace = include(urls) url_list, app_name, namespace = include(urls)
def process_patterns(url_patterns): def process_patterns(url_patterns):
@@ -22,6 +35,8 @@ def decorate_url_patterns(urls, decorator):
process_patterns(pattern.url_patterns) process_patterns(pattern.url_patterns)
else: else:
# this is a pattern # this is a pattern
if excluded_views and pattern.lookup_str in excluded_views:
return
pattern.callback = decorator(pattern.callback) pattern.callback = decorator(pattern.callback)
process_patterns(url_list) process_patterns(url_list)

View File

@@ -1,8 +1,5 @@
from django.conf.urls import include
from allianceauth.authentication import views from allianceauth.authentication import views
from django.urls import re_path from django.urls import include, re_path, path
from django.urls import path
urlpatterns = [ urlpatterns = [
path('activate/complete/', views.activation_complete, name='registration_activation_complete'), path('activate/complete/', views.activation_complete, name='registration_activation_complete'),

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

View File

@@ -31,6 +31,7 @@ class UserSettingsMiddleware(MiddlewareMixin):
except Exception as e: except Exception as e:
logger.exception(e) logger.exception(e)
# AA v3 NIGHT_MODE
# Set our Night mode flag from the DB # Set our Night mode flag from the DB
# Null = hasnt been set by the user ever, dont act. # Null = hasnt been set by the user ever, dont act.
# #
@@ -42,4 +43,13 @@ class UserSettingsMiddleware(MiddlewareMixin):
except Exception as e: except Exception as e:
logger.exception(e) logger.exception(e)
# AA v4 Themes
# Null = has not been set by the user ever, dont act
# DEFAULT_THEME or DEFAULT_THEME_DARK will be used in get_theme()
try:
if request.user.profile.theme is not None:
request.session["THEME"] = request.user.profile.theme
except Exception as e:
logger.exception(e)
return response return response

View File

@@ -0,0 +1,34 @@
# Generated by Django 4.0.10 on 2023-05-28 15:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentication", "0020_userprofile_language_userprofile_night_mode"),
]
operations = [
migrations.AlterField(
model_name="userprofile",
name="language",
field=models.CharField(
blank=True,
choices=[
("en", "English"),
("de", "German"),
("es", "Spanish"),
("zh-hans", "Chinese Simplified"),
("ru", "Russian"),
("ko", "Korean"),
("fr", "French"),
("ja", "Japanese"),
("it", "Italian"),
("uk", "Ukrainian"),
],
default="",
max_length=10,
verbose_name="Language",
),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.10 on 2023-10-07 07:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0021_alter_userprofile_language'),
]
operations = [
migrations.AddField(
model_name='userprofile',
name='theme',
field=models.CharField(blank=True, help_text='Bootstrap 5 Themes from https://bootswatch.com/ or Community Apps', max_length=200, null=True, verbose_name='Theme'),
),
]

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

@@ -63,6 +63,22 @@ class UserProfile(models.Model):
class Meta: class Meta:
default_permissions = ('change',) default_permissions = ('change',)
class Language(models.TextChoices):
"""
Choices for UserProfile.language
"""
ENGLISH = 'en', _('English')
GERMAN = 'de', _('German')
SPANISH = 'es', _('Spanish')
CHINESE = 'zh-hans', _('Chinese Simplified')
RUSSIAN = 'ru', _('Russian')
KOREAN = 'ko', _('Korean')
FRENCH = 'fr', _('French')
JAPANESE = 'ja', _('Japanese')
ITALIAN = 'it', _('Italian')
UKRAINIAN = 'uk', _('Ukrainian')
user = models.OneToOneField( user = models.OneToOneField(
User, User,
related_name='profile', related_name='profile',
@@ -76,26 +92,22 @@ class UserProfile(models.Model):
State, State,
on_delete=models.SET_DEFAULT, on_delete=models.SET_DEFAULT,
default=get_guest_state_pk) 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 = models.CharField(
_("Language"), max_length=10, _("Language"), max_length=10,
choices=LANGUAGE_CHOICES, choices=Language.choices,
blank=True, blank=True,
default='') default='')
night_mode = models.BooleanField( night_mode = models.BooleanField(
_("Night Mode"), _("Night Mode"),
blank=True, blank=True,
null=True) null=True)
theme = models.CharField(
_("Theme"),
max_length=200,
blank=True,
null=True,
help_text="Bootstrap 5 Themes from https://bootswatch.com/ or Community Apps"
)
def assign_state(self, state=None, commit=True): def assign_state(self, state=None, commit=True):
if not state: if not state:

View File

@@ -1,29 +1,34 @@
from collections import namedtuple """Counters for Task Statistics."""
import datetime as dt import datetime as dt
from typing import NamedTuple, Optional
from .event_series import EventSeries from .event_series import EventSeries
# Global series for counting task events.
"""Global series for counting task events."""
succeeded_tasks = EventSeries("SUCCEEDED_TASKS") succeeded_tasks = EventSeries("SUCCEEDED_TASKS")
retried_tasks = EventSeries("RETRIED_TASKS") retried_tasks = EventSeries("RETRIED_TASKS")
failed_tasks = EventSeries("FAILED_TASKS") failed_tasks = EventSeries("FAILED_TASKS")
_TaskCounts = namedtuple( class _TaskCounts(NamedTuple):
"_TaskCounts", ["succeeded", "retried", "failed", "total", "earliest_task", "hours"] succeeded: int
) retried: int
failed: int
total: int
earliest_task: Optional[dt.datetime]
hours: int
def dashboard_results(hours: int) -> _TaskCounts: def dashboard_results(hours: int) -> _TaskCounts:
"""Counts of all task events within the given timeframe.""" """Counts of all task events within the given time frame."""
def earliest_if_exists(events: EventSeries, earliest: dt.datetime) -> list: def earliest_if_exists(events: EventSeries, earliest: dt.datetime) -> list:
my_earliest = events.first_event(earliest=earliest) my_earliest = events.first_event(earliest=earliest)
return [my_earliest] if my_earliest else [] return [my_earliest] if my_earliest else []
earliest = dt.datetime.utcnow() - dt.timedelta(hours=hours) earliest = dt.datetime.utcnow() - dt.timedelta(hours=hours)
earliest_events = list() earliest_events = []
succeeded_count = succeeded_tasks.count(earliest=earliest) succeeded_count = succeeded_tasks.count(earliest=earliest)
earliest_events += earliest_if_exists(succeeded_tasks, earliest) earliest_events += earliest_if_exists(succeeded_tasks, earliest)
retried_count = retried_tasks.count(earliest=earliest) retried_count = retried_tasks.count(earliest=earliest)

View File

@@ -1,61 +1,31 @@
"""Event series for Task Statistics."""
import datetime as dt import datetime as dt
import logging import logging
from typing import List, Optional from typing import List, Optional
from pytz import utc from pytz import utc
from redis import Redis, RedisError from redis import Redis
from allianceauth.utils.cache import get_redis_client from .helpers import get_redis_client_or_stub
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class _RedisStub:
"""Stub of a Redis client.
It's purpose is to prevent EventSeries objects from trying to access Redis
when it is not available. e.g. when the Sphinx docs are rendered by readthedocs.org.
"""
def delete(self, *args, **kwargs):
pass
def incr(self, *args, **kwargs):
return 0
def zadd(self, *args, **kwargs):
pass
def zcount(self, *args, **kwargs):
pass
def zrangebyscore(self, *args, **kwargs):
pass
class EventSeries: class EventSeries:
"""API for recording and analyzing a series of events.""" """API for recording and analyzing a series of events."""
_ROOT_KEY = "ALLIANCEAUTH_EVENT_SERIES" _ROOT_KEY = "ALLIANCEAUTH_EVENT_SERIES"
def __init__(self, key_id: str, redis: Redis = None) -> None: def __init__(self, key_id: str, redis: Optional[Redis] = None) -> None:
self._redis = get_redis_client() if not redis else redis self._redis = get_redis_client_or_stub() if not redis else redis
try:
if not self._redis.ping():
raise RuntimeError()
except (AttributeError, RedisError, RuntimeError):
logger.exception(
"Failed to establish a connection with Redis. "
"This EventSeries object is disabled.",
)
self._redis = _RedisStub()
self._key_id = str(key_id) self._key_id = str(key_id)
self.clear() self.clear()
@property @property
def is_disabled(self): def is_disabled(self):
"""True when this object is disabled, e.g. Redis was not available at startup.""" """True when this object is disabled, e.g. Redis was not available at startup."""
return isinstance(self._redis, _RedisStub) return hasattr(self._redis, "IS_STUB")
@property @property
def _key_counter(self): def _key_counter(self):
@@ -73,8 +43,8 @@ class EventSeries:
""" """
if not event_time: if not event_time:
event_time = dt.datetime.utcnow() event_time = dt.datetime.utcnow()
id = self._redis.incr(self._key_counter) my_id = self._redis.incr(self._key_counter)
self._redis.zadd(self._key_sorted_set, {id: event_time.timestamp()}) self._redis.zadd(self._key_sorted_set, {my_id: event_time.timestamp()})
def all(self) -> List[dt.datetime]: def all(self) -> List[dt.datetime]:
"""List of all known events.""" """List of all known events."""
@@ -95,15 +65,15 @@ class EventSeries:
self._redis.delete(self._key_counter) self._redis.delete(self._key_counter)
def count(self, earliest: dt.datetime = None, latest: dt.datetime = None) -> int: def count(self, earliest: dt.datetime = None, latest: dt.datetime = None) -> int:
"""Count of events, can be restricted to given timeframe. """Count of events, can be restricted to given time frame.
Args: Args:
- earliest: Date of first events to count(inclusive), or -infinite if not specified - earliest: Date of first events to count(inclusive), or -infinite if not specified
- latest: Date of last events to count(inclusive), or +infinite if not specified - latest: Date of last events to count(inclusive), or +infinite if not specified
""" """
min = "-inf" if not earliest else earliest.timestamp() minimum = "-inf" if not earliest else earliest.timestamp()
max = "+inf" if not latest else latest.timestamp() maximum = "+inf" if not latest else latest.timestamp()
return self._redis.zcount(self._key_sorted_set, min=min, max=max) return self._redis.zcount(self._key_sorted_set, min=minimum, max=maximum)
def first_event(self, earliest: dt.datetime = None) -> Optional[dt.datetime]: def first_event(self, earliest: dt.datetime = None) -> Optional[dt.datetime]:
"""Date/Time of first event. Returns `None` if series has no events. """Date/Time of first event. Returns `None` if series has no events.
@@ -111,10 +81,10 @@ class EventSeries:
Args: Args:
- earliest: Date of first events to count(inclusive), or any if not specified - earliest: Date of first events to count(inclusive), or any if not specified
""" """
min = "-inf" if not earliest else earliest.timestamp() minimum = "-inf" if not earliest else earliest.timestamp()
event = self._redis.zrangebyscore( event = self._redis.zrangebyscore(
self._key_sorted_set, self._key_sorted_set,
min, minimum,
"+inf", "+inf",
withscores=True, withscores=True,
start=0, start=0,

View File

@@ -0,0 +1,49 @@
"""Helpers for Task Statistics."""
import logging
from redis import Redis, RedisError
from allianceauth.utils.cache import get_redis_client
logger = logging.getLogger(__name__)
class _RedisStub:
"""Stub of a Redis client.
It's purpose is to prevent EventSeries objects from trying to access Redis
when it is not available. e.g. when the Sphinx docs are rendered by readthedocs.org.
"""
IS_STUB = True
def delete(self, *args, **kwargs):
pass
def incr(self, *args, **kwargs):
return 0
def zadd(self, *args, **kwargs):
pass
def zcount(self, *args, **kwargs):
pass
def zrangebyscore(self, *args, **kwargs):
pass
def get_redis_client_or_stub() -> Redis:
"""Return AA's default cache client or a stub if Redis is not available."""
redis = get_redis_client()
try:
if not redis.ping():
raise RuntimeError()
except (AttributeError, RedisError, RuntimeError):
logger.exception(
"Failed to establish a connection with Redis. "
"This EventSeries object is disabled.",
)
return _RedisStub()
return redis

View File

@@ -1,9 +1,7 @@
"""Signals for Task Statistics."""
from celery.signals import ( from celery.signals import (
task_failure, task_failure, task_internal_error, task_retry, task_success, worker_ready,
task_internal_error,
task_retry,
task_success,
worker_ready
) )
from django.conf import settings from django.conf import settings
@@ -19,6 +17,7 @@ def reset_counters():
def is_enabled() -> bool: def is_enabled() -> bool:
"""Return True if task statistics are enabled, else return False."""
return not bool( return not bool(
getattr(settings, "ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED", False) getattr(settings, "ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED", False)
) )

View File

@@ -4,29 +4,30 @@ from django.test import TestCase
from django.utils.timezone import now from django.utils.timezone import now
from allianceauth.authentication.task_statistics.counters import ( from allianceauth.authentication.task_statistics.counters import (
dashboard_results, dashboard_results, failed_tasks, retried_tasks, succeeded_tasks,
succeeded_tasks,
retried_tasks,
failed_tasks,
) )
class TestDashboardResults(TestCase): class TestDashboardResults(TestCase):
def test_should_return_counts_for_given_timeframe_only(self): def test_should_return_counts_for_given_time_frame_only(self):
# given # given
earliest_task = now() - dt.timedelta(minutes=15) earliest_task = now() - dt.timedelta(minutes=15)
succeeded_tasks.clear() succeeded_tasks.clear()
succeeded_tasks.add(now() - dt.timedelta(hours=1, seconds=1)) succeeded_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
succeeded_tasks.add(earliest_task) succeeded_tasks.add(earliest_task)
succeeded_tasks.add() succeeded_tasks.add()
succeeded_tasks.add() succeeded_tasks.add()
retried_tasks.clear() retried_tasks.clear()
retried_tasks.add(now() - dt.timedelta(hours=1, seconds=1)) retried_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
retried_tasks.add(now() - dt.timedelta(seconds=30)) retried_tasks.add(now() - dt.timedelta(seconds=30))
retried_tasks.add() retried_tasks.add()
failed_tasks.clear() failed_tasks.clear()
failed_tasks.add(now() - dt.timedelta(hours=1, seconds=1)) failed_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
failed_tasks.add() failed_tasks.add()
# when # when
results = dashboard_results(hours=1) results = dashboard_results(hours=1)
# then # then

View File

@@ -1,48 +1,19 @@
import datetime as dt import datetime as dt
from unittest.mock import patch
from pytz import utc from pytz import utc
from redis import RedisError
from django.test import TestCase from django.test import TestCase
from django.utils.timezone import now from django.utils.timezone import now
from allianceauth.authentication.task_statistics.event_series import ( from allianceauth.authentication.task_statistics.event_series import (
EventSeries, EventSeries,
_RedisStub,
) )
from allianceauth.authentication.task_statistics.helpers import _RedisStub
MODULE_PATH = "allianceauth.authentication.task_statistics.event_series" MODULE_PATH = "allianceauth.authentication.task_statistics.event_series"
class TestEventSeries(TestCase): class TestEventSeries(TestCase):
def test_should_abort_without_redis_client(self):
# when
with patch(MODULE_PATH + ".get_redis_client") as mock:
mock.return_value = None
events = EventSeries("dummy")
# then
self.assertTrue(events._redis, _RedisStub)
self.assertTrue(events.is_disabled)
def test_should_disable_itself_if_redis_not_available_1(self):
# when
with patch(MODULE_PATH + ".get_redis_client") as mock_get_master_client:
mock_get_master_client.return_value.ping.side_effect = RedisError
events = EventSeries("dummy")
# then
self.assertIsInstance(events._redis, _RedisStub)
self.assertTrue(events.is_disabled)
def test_should_disable_itself_if_redis_not_available_2(self):
# when
with patch(MODULE_PATH + ".get_redis_client") as mock_get_master_client:
mock_get_master_client.return_value.ping.return_value = False
events = EventSeries("dummy")
# then
self.assertIsInstance(events._redis, _RedisStub)
self.assertTrue(events.is_disabled)
def test_should_add_event(self): def test_should_add_event(self):
# given # given
events = EventSeries("dummy") events = EventSeries("dummy")
@@ -166,3 +137,15 @@ class TestEventSeries(TestCase):
results = events.all() results = events.all()
# then # then
self.assertEqual(len(results), 2) self.assertEqual(len(results), 2)
def test_should_not_report_as_disabled_when_initialized_normally(self):
# given
events = EventSeries("dummy")
# when/then
self.assertFalse(events.is_disabled)
def test_should_report_as_disabled_when_initialized_with_redis_stub(self):
# given
events = EventSeries("dummy", redis=_RedisStub())
# when/then
self.assertTrue(events.is_disabled)

View File

@@ -0,0 +1,28 @@
from unittest import TestCase
from unittest.mock import patch
from redis import RedisError
from allianceauth.authentication.task_statistics.helpers import (
_RedisStub, get_redis_client_or_stub,
)
MODULE_PATH = "allianceauth.authentication.task_statistics.helpers"
class TestGetRedisClient(TestCase):
def test_should_return_mock_if_redis_not_available_1(self):
# when
with patch(MODULE_PATH + ".get_redis_client") as mock_get_master_client:
mock_get_master_client.return_value.ping.side_effect = RedisError
result = get_redis_client_or_stub()
# then
self.assertIsInstance(result, _RedisStub)
def test_should_return_mock_if_redis_not_available_2(self):
# when
with patch(MODULE_PATH + ".get_redis_client") as mock_get_master_client:
mock_get_master_client.return_value.ping.return_value = False
result = get_redis_client_or_stub()
# then
self.assertIsInstance(result, _RedisStub)

View File

@@ -17,16 +17,17 @@ from allianceauth.eveonline.tasks import update_character
@override_settings( @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): class TestTaskSignals(TestCase):
fixtures = ["disable_analytics"] fixtures = ["disable_analytics"]
def test_should_record_successful_task(self): def setUp(self) -> None:
# given
succeeded_tasks.clear() succeeded_tasks.clear()
retried_tasks.clear() retried_tasks.clear()
failed_tasks.clear() failed_tasks.clear()
def test_should_record_successful_task(self):
# when # when
with patch( with patch(
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character" "allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
@@ -39,10 +40,6 @@ class TestTaskSignals(TestCase):
self.assertEqual(failed_tasks.count(), 0) self.assertEqual(failed_tasks.count(), 0)
def test_should_record_retried_task(self): def test_should_record_retried_task(self):
# given
succeeded_tasks.clear()
retried_tasks.clear()
failed_tasks.clear()
# when # when
with patch( with patch(
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character" "allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
@@ -55,10 +52,6 @@ class TestTaskSignals(TestCase):
self.assertEqual(retried_tasks.count(), 1) self.assertEqual(retried_tasks.count(), 1)
def test_should_record_failed_task(self): def test_should_record_failed_task(self):
# given
succeeded_tasks.clear()
retried_tasks.clear()
failed_tasks.clear()
# when # when
with patch( with patch(
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character" "allianceauth.eveonline.tasks.EveCharacter.objects.update_character"

View File

@@ -0,0 +1,46 @@
{% load i18n %}
<div class="col-12 col-xl-8 align-self-stretch p-2 ps-0">
<div class="card">
<div class="card-body">
<div class="d-flex align-items-center">
<h4 class="ms-auto me-auto">
{% translate "Characters" %}
</h4>
</div>
<div class="card-body">
<div style="height: 300px; overflow-y:auto;">
<div class="d-flex">
<a href="{% url 'authentication:add_character' %}" class="btn btn-primary flex-fill m-1" title="{% translate 'Add Character' %}">
<span class="d-md-inline m-2">{% translate 'Add Character' %}</span>
</a>
<a href="{% url 'authentication:change_main_character' %}" class="btn btn-primary flex-fill m-1" title="{% translate 'Change Main' %}">
<span class="d-md-inline m-2">{% translate 'Change Main' %}</span>
</a>
</div>
<table class="table">
<thead>
<tr>
<th class="text-center"></th>
<th class="text-center">{% translate "Name" %}</th>
<th class="text-center">{% translate "Corp" %}</th>
<th class="text-center">{% translate "Alliance" %}</th>
</tr>
</thead>
<tbody>
{% for char in characters %}
<tr>
<td class="text-center">
<img class="ra-avatar rounded-circle" src="{{ char.portrait_url_32 }}" alt="{{ char.character_name }}">
</td>
<td class="text-center">{{ char.character_name }}</td>
<td class="text-center">{{ char.corporation_name }}</td>
<td class="text-center">{{ char.alliance_name|default_if_none:"" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,20 @@
{% load i18n %}
<div class="col-12 col-xl-4 align-self-stretch py-2 ps-2">
<div class="card h-100">
<div class="card-body">
<h4 class="card-title text-center">{% translate "Membership" %}</h4>
<div class="card-body">
<div style="height: 300px; overflow-y:auto;">
<h6 class="text-center">{% translate "State:" %} {{ request.user.profile.state }}</h6>
<table class="table">
{% for group in groups %}
<tr>
<td class="text-center">{{ group.name }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,190 +1,15 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base-bs5.html" %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Dashboard" %}{% endblock %} {% block page_title %}{% translate "Dashboard" %}{% endblock %}
{% block header_nav_brand %}
{% translate "Dashboard" %}
{% endblock %}
{% block content %} {% block content %}
<h1 class="page-header text-center">{% translate "Dashboard" %}</h1> <div class="d-flex justify-content-around align-self-center flex-wrap">
{% if user.is_staff %} {% for dash in views %}
{% include 'allianceauth/admin-status/include.html' %} {{ dash | safe }}
{% endif %} {% endfor %}
<div class="col-sm-12">
<div class="row vertical-flexbox-row2">
<div class="col-sm-6 text-center">
<div class="panel panel-primary" style="height:100%">
<div class="panel-heading">
<h3 class="panel-title">
{% blocktranslate with state=request.user.profile.state %}
Main Character (State: {{ state }})
{% endblocktranslate %}
</h3>
</div>
<div class="panel-body">
{% if request.user.profile.main_character %}
{% with request.user.profile.main_character as main %}
<div class="hidden-xs">
<div class="col-lg-4 col-sm-2">
<table class="table">
<tr>
<td class="text-center">
<img class="ra-avatar" src="{{ main.portrait_url_128 }}" alt="{{ main.character_name }}">
</td>
</tr>
<tr>
<td class="text-center">{{ main.character_name }}</td>
</tr>
</table>
</div>
<div class="col-lg-4 col-sm-2">
<table class="table">
<tr>
<td class="text-center">
<img class="ra-avatar" src="{{ main.corporation_logo_url_128 }}" alt="{{ main.corporation_name }}">
</td>
</tr>
<tr>
<td class="text-center">{{ main.corporation_name }}</td>
</tr>
</table>
</div>
<div class="col-lg-4 col-sm-2">
{% if main.alliance_id %}
<table class="table">
<tr>
<td class="text-center">
<img class="ra-avatar" src="{{ main.alliance_logo_url_128 }}" alt="{{ main.alliance_name }}">
</td>
</tr>
<tr>
<td class="text-center">{{ main.alliance_name }}</td>
<tr>
</table>
{% elif main.faction_id %}
<table class="table">
<tr>
<td class="text-center">
<img class="ra-avatar" src="{{ main.faction_logo_url_128 }}" alt="{{ main.faction_name }}">
</td>
</tr>
<tr>
<td class="text-center">{{ main.faction_name }}</td>
<tr>
</table>
{% endif %}
</div>
</div>
<div class="table visible-xs-block">
<p>
<img class="ra-avatar" src="{{ main.portrait_url_64 }}" alt="{{ main.corporation_name }}">
<img class="ra-avatar" src="{{ main.corporation_logo_url_64 }}" alt="{{ main.corporation_name }}">
{% if main.alliance_id %}
<img class="ra-avatar" src="{{ main.alliance_logo_url_64 }}" alt="{{ main.alliance_name }}">
{% endif %}
{% if main.faction_id %}
<img class="ra-avatar" src="{{ main.faction_logo_url_64 }}" alt="{{ main.faction_name }}">
{% endif %}
</p>
<p>
<strong>{{ main.character_name }}</strong><br>
{{ main.corporation_name }}<br>
{% if main.alliance_id %}
{{ main.alliance_name }}<br>
{% endif %}
{% if main.faction_id %}
{{ main.faction_name }}
{% endif %}
</p>
</div>
{% endwith %}
{% else %}
<div class="alert alert-danger" role="alert">
{% translate "No main character set." %}
</div>
{% endif %}
<div class="clearfix"></div>
<div class="row">
<div class="col-sm-6">
<p>
<a href="{% url 'authentication:add_character' %}" class="btn btn-block btn-info"
title="Add Character">{% translate 'Add Character' %}</a>
</p>
</div>
<div class="col-sm-6">
<p>
<a href="{% url 'authentication:change_main_character' %}" class="btn btn-block btn-info"
title="Change Main Character">{% translate "Change Main" %}</a>
</p>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 text-center">
<div class="panel panel-success" style="height:100%">
<div class="panel-heading">
<h3 class="panel-title">{% translate "Group Memberships" %}</h3>
</div>
<div class="panel-body">
<div style="height: 240px;overflow-y:auto;">
<table class="table table-aa">
{% for group in groups %}
<tr>
<td>{{ group.name }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
</div>
</div>
<div class="clearfix"></div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title text-center" style="text-align: center">
{% translate 'Characters' %}
</h3>
</div>
<div class="panel-body">
<table class="table table-aa hidden-xs">
<thead>
<tr>
<th class="text-center"></th>
<th class="text-center">{% translate 'Name' %}</th>
<th class="text-center">{% translate 'Corp' %}</th>
<th class="text-center">{% translate 'Alliance' %}</th>
</tr>
</thead>
<tbody>
{% for char in characters %}
<tr>
<td class="text-center">
<img class="ra-avatar img-circle" src="{{ char.portrait_url_32 }}" alt="{{ char.character_name }}">
</td>
<td class="text-center">{{ char.character_name }}</td>
<td class="text-center">{{ char.corporation_name }}</td>
<td class="text-center">{{ char.alliance_name|default:"" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<table class="table table-aa visible-xs-block" style="width: 100%">
<tbody>
{% for char in characters %}
<tr>
<td class="text-center" style="vertical-align: middle">
<img class="ra-avatar img-circle" src="{{ char.portrait_url_32 }}" alt="{{ char.character_name }}">
</td>
<td class="text-center" style="vertical-align: middle; width: 100%">
<strong>{{ char.character_name }}</strong><br>
{{ char.corporation_name }}<br>
{{ char.alliance_name|default:"" }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,16 +1,16 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base-bs5.html" %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Dashboard" %}{% endblock %} {% block page_title %}{% translate "Dashboard" %}{% endblock page_title %}
{% block content %} {% block content %}
<h1 class="page-header text-center">{% translate "Token Management" %}</h1> <h1 class="page-header text-center">{% translate "Token Management" %}</h1>
<div class="col-sm-12"> <div>
<table class="table table-aa" id="table_tokens" style="width:100%"> <table class="table table-aa" id="table_tokens" style="width: 100%;">
<thead> <thead>
<tr> <tr>
<th>{% translate "Scopes" %}</th> <th>{% translate "Scopes" %}</th>
<th class="text-right">{% translate "Actions" %}</th> <th class="text-end">{% translate "Actions" %}</th>
<th>{% translate "Character" %}</th> <th>{% translate "Character" %}</th>
</tr> </tr>
@@ -18,24 +18,27 @@
<tbody> <tbody>
{% for t in tokens %} {% for t in tokens %}
<tr> <tr>
<td styl="white-space:initial;">{% for s in t.scopes.all %}<span class="label label-default">{{s.name}}</span> {% endfor %}</td> <td style="white-space:initial;">{% for s in t.scopes.all %}<span class="badge bg-secondary">{{ s.name }}</span>{% endfor %}</td>
<td nowrap class="text-right"><a href="{% url 'authentication:token_delete' t.id %}" class="btn btn-danger"><i class="fas fa-trash"></i></a> <a href="{% url 'authentication:token_refresh' t.id %}" class="btn btn-success"><i class="fas fa-sync-alt"></i></a></td> <td nowrap class="text-end">
<td>{{t.character_name}}</td> <a href="{% url 'authentication:token_delete' t.id %}" class="btn btn-danger"><i class="fas fa-trash"></i></a>
<a href="{% url 'authentication:token_refresh' t.id %}" class="btn btn-success"><i class="fas fa-sync-alt"></i></a>
</td>
<td>{{ t.character_name }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% translate "This page is a best attempt, but backups or database logs can still contain your tokens. Always revoke tokens on https://community.eveonline.com/support/third-party-applications/ where possible."|urlize %} {% translate "This page is a best attempt, but backups or database logs can still contain your tokens. Always revoke tokens on https://community.eveonline.com/support/third-party-applications/ where possible."|urlize %}
</div> </div>
{% endblock %} {% endblock content %}
{% block extra_javascript %} {% block extra_javascript %}
{% include 'bundles/datatables-js.html' %} {% include "bundles/datatables-js-bs5.html" %}
{% endblock %} {% endblock extra_javascript %}
{% block extra_css %} {% block extra_css %}
{% include 'bundles/datatables-css.html' %} {% include "bundles/datatables-css-bs5.html" %}
{% endblock %} {% endblock extra_css %}
{% block extra_script %} {% block extra_script %}
$(document).ready(function(){ $(document).ready(function(){
@@ -59,4 +62,4 @@
"stateSave": true, "stateSave": true,
}); });
}); });
{% endblock %} {% endblock extra_script %}

View File

@@ -7,6 +7,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content=""> <meta name="description" content="">
<meta name="author" content=""> <meta name="author" content="">
<!-- TODO Bundle all the site specific stuff up into its own template for easy overide -->
<meta property="og:title" content="{{ SITE_NAME }}"> <meta property="og:title" content="{{ SITE_NAME }}">
<meta property="og:image" content="{{ SITE_URL }}{% static 'allianceauth/icons/apple-touch-icon.png' %}"> <meta property="og:image" content="{{ SITE_URL }}{% static 'allianceauth/icons/apple-touch-icon.png' %}">
<meta property="og:description" content="Alliance Auth - An auth system for EVE Online to help in-game organizations manage online service access."> <meta property="og:description" content="Alliance Auth - An auth system for EVE Online to help in-game organizations manage online service access.">
@@ -30,7 +31,7 @@
} }
.panel-transparent { .panel-transparent {
background: rgba(48, 48, 48, 0.7); background: rgba(48 48 48 / 0.7);
color: #ffffff; color: #ffffff;
padding-bottom: 21px; padding-bottom: 21px;
} }

View File

@@ -5,7 +5,7 @@
<select onchange="this.form.submit()" class="form-control" id="lang-select" name="language"> <select onchange="this.form.submit()" class="form-control" id="lang-select" name="language">
{% get_language_info_list for LANGUAGES as languages %} {% get_language_info_list for LANGUAGES as languages %}
{% for language in languages %} {% for language in languages %}
<option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected="selected"{% endif %}> <option lang="{{ language.code }}" value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected="selected"{% endif %}>
{{ language.name_local|capfirst }} ({{ language.code }}) {{ language.name_local|capfirst }} ({{ language.code }})
</option> </option>
{% endfor %} {% endfor %}

View File

@@ -6,7 +6,7 @@
{% block page_title %}{% translate "Login" %}{% endblock %} {% block page_title %}{% translate "Login" %}{% endblock %}
{% block middle_box_content %} {% block middle_box_content %}
<a href="{% url 'auth_sso_login' %}{% if request.GET.next %}?next={{request.GET.next}}{%endif%}"> <a href="{% url 'auth_sso_login' %}{% if request.GET.next %}?next={{request.GET.next | urlencode}}{%endif%}">
<img class="img-responsive center-block" src="{% static 'allianceauth/authentication/img/sso/EVE_SSO_Login_Buttons_Large_Black.png' %}" alt="{% translate 'Login with Eve SSO' %}"> <img class="img-responsive center-block" src="{% static 'allianceauth/authentication/img/sso/EVE_SSO_Login_Buttons_Large_Black.png' %}" alt="{% translate 'Login with Eve SSO' %}">
</a> </a>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,85 @@
from unittest.mock import patch
from amqp.exceptions import ChannelError
from django.test import TestCase
from allianceauth.authentication.core.celery_workers import (
active_tasks_count, queued_tasks_count,
)
MODULE_PATH = "allianceauth.authentication.core.celery_workers"
@patch(MODULE_PATH + ".current_app")
class TestActiveTasksCount(TestCase):
def test_should_return_correct_count_when_no_active_tasks(self, mock_current_app):
# given
mock_current_app.control.inspect.return_value.active.return_value = {
"queue": []
}
# when
result = active_tasks_count()
# then
self.assertEqual(result, 0)
def test_should_return_correct_task_count_for_active_tasks(self, mock_current_app):
# given
mock_current_app.control.inspect.return_value.active.return_value = {
"queue": [1, 2, 3]
}
# when
result = active_tasks_count()
# then
self.assertEqual(result, 3)
def test_should_return_correct_task_count_for_multiple_queues(
self, mock_current_app
):
# given
mock_current_app.control.inspect.return_value.active.return_value = {
"queue_1": [1, 2],
"queue_2": [3, 4],
}
# when
result = active_tasks_count()
# then
self.assertEqual(result, 4)
def test_should_return_none_when_celery_not_available(self, mock_current_app):
# given
mock_current_app.control.inspect.return_value.active.return_value = None
# when
result = active_tasks_count()
# then
self.assertIsNone(result)
@patch(MODULE_PATH + ".current_app")
class TestQueuedTasksCount(TestCase):
def test_should_return_queue_length_when_queue_exists(self, mock_current_app):
# given
mock_conn = (
mock_current_app.connection_or_acquire.return_value.__enter__.return_value
)
mock_conn.default_channel.queue_declare.return_value.message_count = 7
# when
result = queued_tasks_count()
# then
self.assertEqual(result, 7)
def test_should_return_0_when_queue_does_not_exists(self, mock_current_app):
# given
mock_current_app.connection_or_acquire.side_effect = ChannelError
# when
result = queued_tasks_count()
# then
self.assertEqual(result, 0)
def test_should_return_None_on_other_errors(self, mock_current_app):
# given
mock_current_app.connection_or_acquire.side_effect = RuntimeError
# when
result = queued_tasks_count()
# then
self.assertIsNone(result)

View File

@@ -4,16 +4,16 @@ from urllib import parse
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.http.response import HttpResponse from django.http.response import HttpResponse
from django.shortcuts import reverse
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.urls import reverse, URLPattern
from allianceauth.eveonline.models import EveCharacter from allianceauth.eveonline.models import EveCharacter
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
from ..decorators import main_character_required
from ..models import CharacterOwnership
from ..decorators import decorate_url_patterns, main_character_required
from ..models import CharacterOwnership
MODULE_PATH = 'allianceauth.authentication' MODULE_PATH = 'allianceauth.authentication'
@@ -66,3 +66,33 @@ class DecoratorTestCase(TestCase):
setattr(self.request, 'user', self.main_user) setattr(self.request, 'user', self.main_user)
response = self.dummy_view(self.request) response = self.dummy_view(self.request)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
class TestDecorateUrlPatterns(TestCase):
def test_should_add_decorator_by_default(self):
# given
decorator = mock.MagicMock(name="decorator")
view = mock.MagicMock(name="view")
path = mock.MagicMock(spec=URLPattern, name="path")
path.callback = view
path.lookup_str = "my_lookup_str"
urls = [path]
urlconf_module = urls
# when
decorate_url_patterns(urlconf_module, decorator)
# then
self.assertEqual(path.callback, decorator(view))
def test_should_not_add_decorator_when_excluded(self):
# given
decorator = mock.MagicMock(name="decorator")
view = mock.MagicMock(name="view")
path = mock.MagicMock(spec=URLPattern, name="path")
path.callback = view
path.lookup_str = "my_lookup_str"
urls = [path]
urlconf_module = urls
# when
decorate_url_patterns(urlconf_module, decorator, excluded_views=["my_lookup_str"])
# then
self.assertEqual(path.callback, view)

View File

@@ -9,12 +9,8 @@ from django.core.cache import cache
from django.test import TestCase from django.test import TestCase
from allianceauth.templatetags.admin_status import ( from allianceauth.templatetags.admin_status import (
status_overview, _current_notifications, _current_version_summary, _fetch_list_from_gitlab,
_fetch_list_from_gitlab, _fetch_notification_issues_from_gitlab, _latests_versions, status_overview,
_current_notifications,
_current_version_summary,
_fetch_notification_issues_from_gitlab,
_latests_versions
) )
MODULE_PATH = 'allianceauth.templatetags' MODULE_PATH = 'allianceauth.templatetags'
@@ -56,14 +52,10 @@ TEST_VERSION = '2.6.5'
class TestStatusOverviewTag(TestCase): class TestStatusOverviewTag(TestCase):
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION) @patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
@patch(MODULE_PATH + '.admin_status._fetch_celery_queue_length')
@patch(MODULE_PATH + '.admin_status._current_version_summary') @patch(MODULE_PATH + '.admin_status._current_version_summary')
@patch(MODULE_PATH + '.admin_status._current_notifications') @patch(MODULE_PATH + '.admin_status._current_notifications')
def test_status_overview( def test_status_overview(
self, self, mock_current_notifications, mock_current_version_info
mock_current_notifications,
mock_current_version_info,
mock_fetch_celery_queue_length
): ):
# given # given
notifications = { notifications = {
@@ -82,7 +74,6 @@ class TestStatusOverviewTag(TestCase):
'latest_beta_version': '2.4.4a1', 'latest_beta_version': '2.4.4a1',
} }
mock_current_version_info.return_value = version_info mock_current_version_info.return_value = version_info
mock_fetch_celery_queue_length.return_value = 3
# when # when
result = status_overview() result = status_overview()
# then # then
@@ -96,7 +87,6 @@ class TestStatusOverviewTag(TestCase):
self.assertEqual(result["latest_minor_version"], '2.4.0') self.assertEqual(result["latest_minor_version"], '2.4.0')
self.assertEqual(result["latest_patch_version"], '2.4.5') self.assertEqual(result["latest_patch_version"], '2.4.5')
self.assertEqual(result["latest_beta_version"], '2.4.4a1') self.assertEqual(result["latest_beta_version"], '2.4.4a1')
self.assertEqual(result["task_queue_length"], 3)
class TestNotifications(TestCase): class TestNotifications(TestCase):

View File

@@ -0,0 +1,39 @@
import json
from unittest.mock import patch
from django.test import RequestFactory, TestCase
from allianceauth.authentication.views import task_counts
from allianceauth.tests.auth_utils import AuthUtils
MODULE_PATH = "allianceauth.authentication.views"
def jsonresponse_to_dict(response) -> dict:
return json.loads(response.content)
@patch(MODULE_PATH + ".queued_tasks_count")
@patch(MODULE_PATH + ".active_tasks_count")
class TestRunningTasksCount(TestCase):
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.factory = RequestFactory()
cls.user = AuthUtils.create_user("bruce_wayne")
def test_should_return_data(
self, mock_active_tasks_count, mock_queued_tasks_count
):
# given
mock_active_tasks_count.return_value = 2
mock_queued_tasks_count.return_value = 3
request = self.factory.get("/")
request.user = self.user
# when
response = task_counts(request)
# then
self.assertEqual(response.status_code, 200)
self.assertDictEqual(
jsonresponse_to_dict(response), {"tasks_running": 2, "tasks_queued": 3}
)

View File

@@ -38,4 +38,5 @@ urlpatterns = [
name='token_refresh' name='token_refresh'
), ),
path('dashboard/', views.dashboard, name='dashboard'), path('dashboard/', views.dashboard, name='dashboard'),
path('task-counts/', views.task_counts, name='task_counts'),
] ]

View File

@@ -1,31 +1,32 @@
import logging import logging
from allianceauth.hooks import get_hooks
from django_registration.backends.activation.views import (
REGISTRATION_SALT, ActivationView as BaseActivationView,
RegistrationView as BaseRegistrationView,
)
from django_registration.signals import user_registered
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import login, authenticate from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core import signing from django.core import signing
from django.core.mail import EmailMultiAlternatives
from django.http import JsonResponse from django.http import JsonResponse
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from allianceauth.eveonline.models import EveCharacter
from esi.decorators import token_required from esi.decorators import token_required
from esi.models import Token from esi.models import Token
from django_registration.backends.activation.views import ( from allianceauth.eveonline.models import EveCharacter
RegistrationView as BaseRegistrationView,
ActivationView as BaseActivationView,
REGISTRATION_SALT
)
from django_registration.signals import user_registered
from .models import CharacterOwnership from .core.celery_workers import active_tasks_count, queued_tasks_count
from .forms import RegistrationForm from .forms import RegistrationForm
from .models import CharacterOwnership
if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS: if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS:
_has_auto_groups = True _has_auto_groups = True
@@ -42,25 +43,54 @@ def index(request):
return redirect('authentication:dashboard') return redirect('authentication:dashboard')
@login_required def dashboard_groups(request):
def dashboard(request):
groups = request.user.groups.all() groups = request.user.groups.all()
if _has_auto_groups: if _has_auto_groups:
groups = groups\ groups = groups\
.filter(managedalliancegroup__isnull=True)\ .filter(managedalliancegroup__isnull=True)\
.filter(managedcorpgroup__isnull=True) .filter(managedcorpgroup__isnull=True)
groups = groups.order_by('name') groups = groups.order_by('name')
context = {
'groups': groups,
}
return render_to_string('authentication/dashboard.groups.html', context=context, request=request)
def dashboard_characters(request):
characters = EveCharacter.objects\ characters = EveCharacter.objects\
.filter(character_ownership__user=request.user)\ .filter(character_ownership__user=request.user)\
.select_related()\ .select_related()\
.order_by('character_name') .order_by('character_name')
context = { context = {
'groups': groups,
'characters': characters 'characters': characters
} }
return render_to_string('authentication/dashboard.characters.html', context=context, request=request)
def dashboard_admin(request):
if request.user.is_superuser:
return render_to_string('allianceauth/admin-status/include.html', request=request)
else:
return ""
@login_required
def dashboard(request):
_dash_items = list()
hooks = get_hooks('dashboard_hook')
items = [fn() for fn in hooks]
items.sort(key=lambda i: i.order)
for item in items:
_dash_items.append(item.render(request))
context = {
'views': _dash_items,
}
return render(request, 'authentication/dashboard.html', context) return render(request, 'authentication/dashboard.html', context)
@login_required @login_required
def token_management(request): def token_management(request):
tokens = request.user.token_set.all() tokens = request.user.token_set.all()
@@ -70,6 +100,7 @@ def token_management(request):
} }
return render(request, 'authentication/tokens.html', context) return render(request, 'authentication/tokens.html', context)
@login_required @login_required
def token_delete(request, token_id=None): def token_delete(request, token_id=None):
try: try:
@@ -83,6 +114,7 @@ def token_delete(request, token_id=None):
messages.warning(request, "Token does not exist") messages.warning(request, "Token does not exist")
return redirect('authentication:token_management') return redirect('authentication:token_management')
@login_required @login_required
def token_refresh(request, token_id=None): def token_refresh(request, token_id=None):
try: try:
@@ -127,7 +159,7 @@ def main_character_change(request, token):
def add_character(request, token): def add_character(request, token):
if CharacterOwnership.objects.filter(character__character_id=token.character_id).filter( if CharacterOwnership.objects.filter(character__character_id=token.character_id).filter(
owner_hash=token.character_owner_hash).filter(user=request.user).exists(): owner_hash=token.character_owner_hash).filter(user=request.user).exists():
messages.success(request, _('Added %(name)s to your account.'% ({'name': token.character_name}))) messages.success(request, _('Added %(name)s to your account.' % ({'name': token.character_name})))
else: else:
messages.error(request, _('Failed to add %(name)s to your account: they already have an account.' % ({'name': token.character_name}))) messages.error(request, _('Failed to add %(name)s to your account: they already have an account.' % ({'name': token.character_name})))
return redirect('authentication:dashboard') return redirect('authentication:dashboard')
@@ -268,8 +300,11 @@ class ActivationView(BaseActivationView):
def validate_key(self, activation_key): def validate_key(self, activation_key):
try: try:
dump = signing.loads(activation_key, salt=REGISTRATION_SALT, dump = signing.loads(
max_age=settings.ACCOUNT_ACTIVATION_DAYS * 86400) activation_key,
salt=REGISTRATION_SALT,
max_age=settings.ACCOUNT_ACTIVATION_DAYS * 86400
)
return dump return dump
except signing.BadSignature: except signing.BadSignature:
return None return None
@@ -299,3 +334,12 @@ def activation_complete(request):
def registration_closed(request): def registration_closed(request):
messages.error(request, _('Registration of new accounts is not allowed at this time.')) messages.error(request, _('Registration of new accounts is not allowed at this time.'))
return redirect('authentication:login') return redirect('authentication:login')
def task_counts(request) -> JsonResponse:
"""Return task counts as JSON for an AJAX call."""
data = {
"tasks_running": active_tasks_count(),
"tasks_queued": queued_tasks_count()
}
return JsonResponse(data)

View File

@@ -12,13 +12,14 @@ class StartProject(BaseStartProject):
parser.add_argument('--python', help='The path to the python executable.') parser.add_argument('--python', help='The path to the python executable.')
parser.add_argument('--celery', help='The path to the celery executable.') parser.add_argument('--celery', help='The path to the celery executable.')
parser.add_argument('--gunicorn', help='The path to the gunicorn executable.') parser.add_argument('--gunicorn', help='The path to the gunicorn executable.')
parser.add_argument('--memmon', help='The path to the memmon executable.')
def create_project(parser, options, args): def create_project(parser, options, args):
# Validate args # Validate args
if len(args) < 2: if len(args) < 2:
parser.error("Please specify a name for your Alliance Auth installation.") parser.error("Please specify a name for your Alliance Auth installation.")
elif len(args) > 3: elif len(args) > 4:
parser.error("Too many arguments.") parser.error("Too many arguments.")
# First find the path to Alliance Auth # First find the path to Alliance Auth
@@ -32,6 +33,7 @@ def create_project(parser, options, args):
'python': shutil.which('python'), 'python': shutil.which('python'),
'gunicorn': shutil.which('gunicorn'), 'gunicorn': shutil.which('gunicorn'),
'celery': shutil.which('celery'), 'celery': shutil.which('celery'),
'memmon': shutil.which('memmon'),
'extensions': ['py', 'conf', 'json'], 'extensions': ['py', 'conf', 'json'],
} }

View File

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

View File

@@ -1,37 +1,40 @@
{% extends 'allianceauth/base.html' %} {% extends "allianceauth/base-bs5.html" %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Corporation Member Data" %}{% endblock %} {% block page_title %}
{% translate "Corporation Member Data" %}
{% endblock page_title %}
{% block content %} {% block content %}
<div class="col-lg-12"> <div>
<h1 class="page-header text-center">{% translate "Corporation Member Data" %}</h1> <h1 class="page-header text-center">{% translate "Corporation Member Data" %}</h1>
<div class="col-lg-10 col-lg-offset-1 container"> <nav class="navbar navbar-default">
<nav class="navbar navbar-default"> <div class="container-fluid">
<div class="container-fluid"> <ul class="nav navbar-nav">
<ul class="nav navbar-nav"> <li class="dropdown">
<li class="dropdown"> <a href="#" id="dLabel" class="dropdown-toggle" role="button" data-toggle="dropdown" aria-haspopup="false" aria-expanded="false">{% translate "Corporations" %}<span class="caret"></span></a>
<a href="#" id="dLabel" class="dropdown-toggle" role="button" data-toggle="dropdown" aria-haspopup="false" aria-expanded="false">{% translate "Corporations" %}<span class="caret"></span></a> <ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">
<ul class="dropdown-menu" role="menu" aria-labelledby="dLabel"> {% for corpstat in available %}
{% for corpstat in available %} <li>
<li> <a href="{% url 'corputils:view_corp' corpstat.corp.corporation_id %}">{{ corpstat.corp.corporation_name }}</a>
<a href="{% url 'corputils:view_corp' corpstat.corp.corporation_id %}">{{ corpstat.corp.corporation_name }}</a> </li>
</li> {% endfor %}
{% endfor %} </ul>
</ul> </li>
{% if perms.corputils.add_corpstats %}
<li>
<a href="{% url 'corputils:add' %}">{% translate "Add" %}</a>
</li> </li>
{% if perms.corputils.add_corpstats %} {% endif %}
<li> </ul>
<a href="{% url 'corputils:add' %}">{% translate "Add" %}</a>
</li> <form class="navbar-form navbar-right" role="search" action="{% url 'corputils:search' %}" method="GET">
{% endif %} <div class="form-group">
</ul> <input type="text" class="form-control" name="search_string" placeholder="{% if search_string %}{{ search_string }}{% else %}{% translate 'Search all corporations...' %}{% endif %}">
<form class="navbar-form navbar-right" role="search" action="{% url 'corputils:search' %}" method="GET"> </div>
<div class="form-group"> </form>
<input type="text" class="form-control" name="search_string" placeholder="{% if search_string %}{{ search_string }}{% else %}{% translate "Search all corporations..." %}{% endif %}"> </div>
</div> </nav>
</form>
</div> {% block member_data %}
</nav> {% endblock member_data %}
{% block member_data %}{% endblock %}
</div>
</div> </div>
{% endblock %} {% endblock content %}

View File

@@ -3,177 +3,175 @@
{% load humanize %} {% load humanize %}
{% block member_data %} {% block member_data %}
{% if corpstats %} {% if corpstats %}
<div class="row"> <div>
<div class="col-lg-12 text-center"> <table class="table">
<table class="table"> <tr>
<tr> <td class="text-center col-lg-6{% if corpstats.corp.alliance %}{% else %}col-lg-offset-3{% endif %}">
<td class="text-center col-lg-6{% if corpstats.corp.alliance %}{% else %}col-lg-offset-3{% endif %}"> <img class="ra-avatar" src="{{ corpstats.corp.logo_url_64 }}" alt="{{ corpstats.corp.corporation_name }}">
<img class="ra-avatar" src="{{ corpstats.corp.logo_url_64 }}" alt="{{ corpstats.corp.corporation_name }}"> </td>
</td> {% if corpstats.corp.alliance %}
{% if corpstats.corp.alliance %} <td class="text-center col-lg-6">
<td class="text-center col-lg-6"> <img class="ra-avatar" src="{{ corpstats.corp.alliance.logo_url_64 }}" alt="{{ corpstats.corp.alliance.alliance_name }}">
<img class="ra-avatar" src="{{ corpstats.corp.alliance.logo_url_64 }}" alt="{{ corpstats.corp.alliance.alliance_name }}"> </td>
</td> {% endif %}
{% endif %} </tr>
</tr> <tr>
<tr> <td class="text-center"><h4>{{ corpstats.corp.corporation_name }}</h4></td>
<td class="text-center"><h4>{{ corpstats.corp.corporation_name }}</h4></td> {% if corpstats.corp.alliance %}
{% if corpstats.corp.alliance %} <td class="text-center"><h4>{{ corpstats.corp.alliance.alliance_name }}</h4></td>
<td class="text-center"><h4>{{ corpstats.corp.alliance.alliance_name }}</h4></td> {% endif %}
{% endif %} </tr>
</tr> </table>
</table>
</div>
</div> </div>
<div class="row">
<div class="col-lg-12"> <div>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<ul class="nav nav-pills pull-left"> <ul class="nav nav-pills pull-left">
<li class="active"><a href="#mains" data-toggle="pill">{% translate 'Mains' %} ({{ total_mains }})</a></li> <li class="active"><a href="#mains" data-toggle="pill">{% translate 'Mains' %} ({{ total_mains }})</a></li>
<li><a href="#members" data-toggle="pill">{% translate 'Members' %} ({{ corpstats.member_count }})</a></li> <li><a href="#members" data-toggle="pill">{% translate 'Members' %} ({{ corpstats.member_count }})</a></li>
<li><a href="#unregistered" data-toggle="pill">{% translate 'Unregistered' %} ({{ unregistered.count }})</a></li> <li><a href="#unregistered" data-toggle="pill">{% translate 'Unregistered' %} ({{ unregistered.count }})</a></li>
</ul> </ul>
<div class="pull-right hidden-xs"> <div class="pull-right hidden-xs">
{% translate "Last update:" %} {{ corpstats.last_update|naturaltime }}&nbsp; {% translate "Last update:" %} {{ corpstats.last_update|naturaltime }}&nbsp;
<a class="btn btn-success" type="button" href="{% url 'corputils:update' corpstats.corp.corporation_id %}" title="Update Now"> <a class="btn btn-success" type="button" href="{% url 'corputils:update' corpstats.corp.corporation_id %}" title="Update Now">
<span class="glyphicon glyphicon-refresh"></span> <span class="glyphicon glyphicon-refresh"></span>
</a> </a>
</div>
<div class="clearfix"></div>
</div> </div>
<div class="panel-body"> <div class="clearfix"></div>
<div class="tab-content"> </div>
<div class="tab-pane fade in active" id="mains">
{% if mains %} <div class="panel-body">
<div class="table-responsive"> <div class="tab-content">
<table class="table table-hover" id="table-mains"> <div class="tab-pane fade in active" id="mains">
<thead> {% if mains %}
<div class="table-responsive">
<table class="table table-hover" id="table-mains">
<thead>
<tr>
<th style="height:1em;"><!-- Must have text or height to prevent clipping --></th>
<th></th>
</tr>
</thead>
<tbody>
{% for id, main in mains.items %}
<tr> <tr>
<th style="height:1em;"><!-- Must have text or height to prevent clipping --></th> <td class="text-center" style="vertical-align:middle">
<th></th> <div class="thumbnail" style="border: 0 none; box-shadow: none; background: transparent;">
</tr> <img src="{{ main.main.portrait_url_64 }}" class="img-circle" alt="{{ main.main }}">
</thead> <div class="caption text-center">
<tbody> {{ main.main }}
{% for id, main in mains.items %}
<tr>
<td class="text-center" style="vertical-align:middle">
<div class="thumbnail" style="border: 0 none; box-shadow: none; background: transparent;">
<img src="{{ main.main.portrait_url_64 }}" class="img-circle" alt="{{ main.main }}">
<div class="caption text-center">
{{ main.main }}
</div>
</div> </div>
</td> </div>
<td> </td>
<table class="table table-hover"> <td>
{% for alt in main.alts %} <table class="table table-hover">
{% if forloop.first %} {% for alt in main.alts %}
<tr> {% if forloop.first %}
<th></th>
<th class="text-center">{% translate "Character" %}</th>
<th class="text-center">{% translate "Corporation" %}</th>
<th class="text-center">{% translate "Alliance" %}</th>
<th class="text-center"></th>
</tr>
{% endif %}
<tr> <tr>
<td class="text-center" style="width:5%"> <th></th>
<div class="thumbnail" style="border: 0 none; box-shadow: none; background: transparent;"> <th class="text-center">{% translate "Character" %}</th>
<img src="{{ alt.portrait_url_32 }}" class="img-circle" alt="{{ alt.character_name }}"> <th class="text-center">{% translate "Corporation" %}</th>
</div> <th class="text-center">{% translate "Alliance" %}</th>
</td> <th class="text-center"></th>
<td class="text-center" style="width:30%">{{ alt.character_name }}</td>
<td class="text-center" style="width:30%">{{ alt.corporation_name }}</td>
<td class="text-center" style="width:30%">{{ alt.alliance_name }}</td>
<td class="text-center" style="width:5%">
<a href="https://zkillboard.com/character/{{ alt.character_id }}/" class="label label-danger" target="_blank">
{% translate "Killboard" %}
</a>
</td>
</tr> </tr>
{% endfor %} {% endif %}
</table> <tr>
</td> <td class="text-center" style="width:5%">
</tr> <div class="thumbnail" style="border: 0 none; box-shadow: none; background: transparent;">
{% endfor %} <img src="{{ alt.portrait_url_32 }}" class="img-circle" alt="{{ alt.character_name }}">
</tbody> </div>
</table> </td>
</div> <td class="text-center" style="width:30%">{{ alt.character_name }}</td>
{% endif %} <td class="text-center" style="width:30%">{{ alt.corporation_name }}</td>
</div> <td class="text-center" style="width:30%">{{ alt.alliance_name }}</td>
<div class="tab-pane fade" id="members"> <td class="text-center" style="width:5%">
{% if members %} <a href="https://zkillboard.com/character/{{ alt.character_id }}/" class="badge bg-danger" target="_blank">
<div class="table-responsive"> {% translate "Killboard" %}
<table class="table table-hover" id="table-members"> </a>
<thead> </td>
<tr> </tr>
<th></th> {% endfor %}
<th class="text-center">{% translate "Character" %}</th> </table>
<th class="text-center"></th> </td>
<th class="text-center">{% translate "Main Character" %}</th>
<th class="text-center">{% translate "Main Corporation" %}</th>
<th class="text-center">{% translate "Main Alliance" %}</th>
</tr> </tr>
</thead> {% endfor %}
<tbody> </tbody>
{% for member in members %} </table>
<tr> </div>
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member }}"></td> {% endif %}
<td class="text-center">{{ member }}</td> </div>
<td class="text-center"> <div class="tab-pane fade" id="members">
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="label label-danger" target="_blank">{% translate "Killboard" %}</a> {% if members %}
</td> <div class="table-responsive">
<td class="text-center">{{ member.character_ownership.user.profile.main_character.character_name }}</td> <table class="table table-hover" id="table-members">
<td class="text-center">{{ member.character_ownership.user.profile.main_character.corporation_name }}</td> <thead>
<td class="text-center">{{ member.character_ownership.user.profile.main_character.alliance_name }}</td> <tr>
</tr> <th></th>
{% endfor %} <th class="text-center">{% translate "Character" %}</th>
{% for member in unregistered %} <th class="text-center"></th>
<tr class="danger"> <th class="text-center">{% translate "Main Character" %}</th>
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member.character_name }}"></td> <th class="text-center">{% translate "Main Corporation" %}</th>
<td class="text-center">{{ member.character_name }}</td> <th class="text-center">{% translate "Main Alliance" %}</th>
<td class="text-center"> </tr>
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="label label-danger" target="_blank">{% translate "Killboard" %}</a> </thead>
</td> <tbody>
<td class="text-center"></td> {% for member in members %}
<td class="text-center"></td>
<td class="text-center"></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
<div class="tab-pane fade" id="unregistered">
{% if unregistered %}
<div class="table-responsive">
<table class="table table-hover" id="table-unregistered">
<thead>
<tr> <tr>
<th></th> <td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member }}"></td>
<th class="text-center">{% translate "Character" %}</th> <td class="text-center">{{ member }}</td>
<th class="text-center"></th> <td class="text-center">
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="badge bg-danger" target="_blank">{% translate "Killboard" %}</a>
</td>
<td class="text-center">{{ member.character_ownership.user.profile.main_character.character_name }}</td>
<td class="text-center">{{ member.character_ownership.user.profile.main_character.corporation_name }}</td>
<td class="text-center">{{ member.character_ownership.user.profile.main_character.alliance_name }}</td>
</tr> </tr>
</thead> {% endfor %}
<tbody> {% for member in unregistered %}
{% for member in unregistered %} <tr class="danger">
<tr class="danger"> <td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member.character_name }}"></td>
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member.character_name }}"></td> <td class="text-center">{{ member.character_name }}</td>
<td class="text-center">{{ member.character_name }}</td> <td class="text-center">
<td class="text-center"> <a href="https://zkillboard.com/character/{{ member.character_id }}/" class="badge bg-danger" target="_blank">{% translate "Killboard" %}</a>
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="label label-danger" target="_blank"> </td>
{% translate "Killboard" %} <td class="text-center"></td>
</a> <td class="text-center"></td>
</td> <td class="text-center"></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
{% endif %} {% endif %}
</div> </div>
<div class="tab-pane fade" id="unregistered">
{% if unregistered %}
<div class="table-responsive">
<table class="table table-hover" id="table-unregistered">
<thead>
<tr>
<th></th>
<th class="text-center">{% translate "Character" %}</th>
<th class="text-center"></th>
</tr>
</thead>
<tbody>
{% for member in unregistered %}
<tr class="danger">
<td><img src="{{ member.portrait_url }}" class="img-circle" alt="{{ member.character_name }}"></td>
<td class="text-center">{{ member.character_name }}</td>
<td class="text-center">
<a href="https://zkillboard.com/character/{{ member.character_id }}/" class="badge bg-danger" target="_blank">
{% translate "Killboard" %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@@ -181,12 +179,15 @@
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block extra_javascript %} {% block extra_javascript %}
{% include 'bundles/datatables-js.html' %} {% include 'bundles/datatables-js.html' %}
{% endblock %} {% endblock %}
{% block extra_css %} {% block extra_css %}
{% include 'bundles/datatables-css.html' %} {% include 'bundles/datatables-css.html' %}
{% endblock %} {% endblock %}
{% block extra_script %} {% block extra_script %}
$(document).ready(function(){ $(document).ready(function(){
$('#table-mains').DataTable({ $('#table-mains').DataTable({

View File

@@ -24,7 +24,7 @@
<td class="text-center"><img src="{{ result.1.portrait_url }}" class="img-circle" alt="{{ result.1.character_name }}"></td> <td class="text-center"><img src="{{ result.1.portrait_url }}" class="img-circle" alt="{{ result.1.character_name }}"></td>
<td class="text-center">{{ result.1.character_name }}</td> <td class="text-center">{{ result.1.character_name }}</td>
<td class="text-center">{{ result.0.corp.corporation_name }}</td> <td class="text-center">{{ result.0.corp.corporation_name }}</td>
<td class="text-center"><a href="https://zkillboard.com/character/{{ result.1.character_id }}/" class="label label-danger" target="_blank">{% translate "Killboard" %}</a></td> <td class="text-center"><a href="https://zkillboard.com/character/{{ result.1.character_id }}/" class="badge bg-danger" target="_blank">{% translate "Killboard" %}</a></td>
<td class="text-center">{{ result.1.main_character.character_name }}</td> <td class="text-center">{{ result.1.main_character.character_name }}</td>
<td class="text-center">{{ result.1.main_character.corporation_name }}</td> <td class="text-center">{{ result.1.main_character.corporation_name }}</td>
<td class="text-center">{{ result.1.main_character.alliance_name }}</td> <td class="text-center">{{ result.1.main_character.alliance_name }}</td>

View File

@@ -14,6 +14,7 @@ def sync_user_groups(modeladmin, request, queryset):
agc.update_all_states_group_membership() agc.update_all_states_group_membership()
@admin.register(AutogroupsConfig)
class AutogroupsConfigAdmin(admin.ModelAdmin): class AutogroupsConfigAdmin(admin.ModelAdmin):
formfield_overrides = { formfield_overrides = {
models.CharField: {'strip': False} models.CharField: {'strip': False}
@@ -36,6 +37,5 @@ class AutogroupsConfigAdmin(admin.ModelAdmin):
return actions return actions
admin.site.register(AutogroupsConfig, AutogroupsConfigAdmin)
admin.site.register(ManagedCorpGroup) admin.site.register(ManagedCorpGroup)
admin.site.register(ManagedAllianceGroup) admin.site.register(ManagedAllianceGroup)

View File

@@ -93,7 +93,7 @@ class AutogroupsConfigTestCase(TestCase):
group_qs = Group.objects.filter(pk=group.pk) group_qs = Group.objects.filter(pk=group.pk)
self.assertIn(group, self.member.groups.all()) self.assertIn(group, self.member.groups.all())
self.assertQuerysetEqual(self.member.groups.all(), map(repr, pre_groups | group_qs), ordered=False) self.assertQuerySetEqual(self.member.groups.all(), pre_groups | group_qs, ordered=False)
def test_update_alliance_group_membership_no_main_character(self): def test_update_alliance_group_membership_no_main_character(self):
obj = AutogroupsConfig.objects.create() obj = AutogroupsConfig.objects.create()
@@ -172,7 +172,7 @@ class AutogroupsConfigTestCase(TestCase):
group_qs = Group.objects.filter(pk=group.pk) group_qs = Group.objects.filter(pk=group.pk)
self.assertIn(group, self.member.groups.all()) self.assertIn(group, self.member.groups.all())
self.assertQuerysetEqual(self.member.groups.all(), map(repr, pre_groups | group_qs), ordered=False) self.assertQuerySetEqual(self.member.groups.all(), pre_groups | group_qs, ordered=False)
def test_update_corp_group_membership_no_state(self): def test_update_corp_group_membership_no_state(self):
obj = AutogroupsConfig.objects.create(corp_groups=True) obj = AutogroupsConfig.objects.create(corp_groups=True)

View File

@@ -8,10 +8,10 @@ Needs to be called with a context containing three objects:
--> -->
{% extends 'allianceauth/base.html' %} {% extends "allianceauth/base-bs5.html" %}
{% load evelinks %} {% load evelinks %}
{% block page_title %}Evelinks examples{% endblock %} {% block page_title %}Evelinks Examples{% endblock page_title %}
{% block content %} {% block content %}
@@ -25,60 +25,57 @@ Needs to be called with a context containing three objects:
<div class="col-md-4"> <div class="col-md-4">
<h3>evewho</h3> <h3>evewho</h3>
<p><a href="{{ my_character|evewho_character_url}}">character from character object</a></p> <p><a href="{{ my_character|evewho_character_url }}">character from character object</a></p>
<p><a href="{{ my_corporation|evewho_corporation_url}}">corporation form corporation object</a></p> <p><a href="{{ my_corporation|evewho_corporation_url }}">corporation form corporation object</a></p>
<p><a href="{{ my_character|evewho_corporation_url}}">corporation from charachter object</a></p> <p><a href="{{ my_character|evewho_corporation_url }}">corporation from charachter object</a></p>
<p><a href="{{ my_alliance|evewho_alliance_url}}">alliance from alliance object</a></p> <p><a href="{{ my_alliance|evewho_alliance_url }}">alliance from alliance object</a></p>
<p><a href="{{ my_character|evewho_alliance_url}}">alliance from character object</a></p> <p><a href="{{ my_character|evewho_alliance_url }}">alliance from character object</a></p>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<h3>dotlan</h3> <h3>dotlan</h3>
<p><a href="{{ my_character|dotlan_corporation_url}}">corporation form character object</a></p> <p><a href="{{ my_character|dotlan_corporation_url }}">corporation form character object</a></p>
<p><a href="{{ my_corporation|dotlan_corporation_url}}">corporation form corporation object</a></p> <p><a href="{{ my_corporation|dotlan_corporation_url }}">corporation form corporation object</a></p>
<p><a href="{{ my_character|dotlan_alliance_url}}">alliance from character object</a></p> <p><a href="{{ my_character|dotlan_alliance_url }}">alliance from character object</a></p>
<p><a href="{{ my_alliance|dotlan_alliance_url}}">alliance from alliance object</a></p> <p><a href="{{ my_alliance|dotlan_alliance_url }}">alliance from alliance object</a></p>
<p><a href="{{ 'Black Rise'|dotlan_region_url}}">region from name string</a></p> <p><a href="{{ 'Black Rise'|dotlan_region_url }}">region from name string</a></p>
<p><a href="{{ 'Tama'|dotlan_solar_system_url}}">solar system from name string</a></p> <p><a href="{{ 'Tama'|dotlan_solar_system_url }}">solar system from name string</a></p>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<h3>zkillboard</h3> <h3>zkillboard</h3>
<p><a href="{{ my_character|zkillboard_character_url}}">character from character object</a></p> <p><a href="{{ my_character|zkillboard_character_url }}">character from character object</a></p>
<p><a href="{{ my_character|zkillboard_corporation_url}}">corporation from character object</a></p> <p><a href="{{ my_character|zkillboard_corporation_url }}">corporation from character object</a></p>
<p><a href="{{ my_corporation|zkillboard_corporation_url}}">corporation form corporation object</a></p> <p><a href="{{ my_corporation|zkillboard_corporation_url }}">corporation form corporation object</a></p>
<p><a href="{{ my_character|zkillboard_alliance_url}}">alliance from character object</a></p> <p><a href="{{ my_character|zkillboard_alliance_url }}">alliance from character object</a></p>
<p><a href="{{ my_alliance|zkillboard_alliance_url}}">alliance from alliance object</a></p> <p><a href="{{ my_alliance|zkillboard_alliance_url }}">alliance from alliance object</a></p>
<p><a href="{{ 10000069|zkillboard_region_url}}">region from ID</a></p> <p><a href="{{ 10000069|zkillboard_region_url }}">region from ID</a></p>
<p><a href="{{ 30002813|zkillboard_solar_system_url}}">solar sytem from ID</a></p> <p><a href="{{ 30002813|zkillboard_solar_system_url }}">solar sytem from ID</a></p>
</div> </div>
</div> </div>
</div>
<h2>image URLs</h2> <h2>image URLs</h2>
<div class="rows"> <div class="rows">
<div class="col-md-4"> <div class="col-md-4">
<p>character from ID: <img src="{{ my_character.character_id|character_portrait_url:128}}"></p> <p>character from ID: <img src="{{ my_character.character_id|character_portrait_url:128 }}"></p>
<p>character from character object: <img src="{{ my_character|character_portrait_url:128}}"></p> <p>character from character object: <img src="{{ my_character|character_portrait_url:128 }}"></p>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<p>corporation from ID: <img src="{{ my_character.corporation_id|corporation_logo_url:128}}"></p> <p>corporation from ID: <img src="{{ my_character.corporation_id|corporation_logo_url:128 }}"></p>
<p>corporation from character object: <img src="{{ my_character|corporation_logo_url:128}}"></p> <p>corporation from character object: <img src="{{ my_character|corporation_logo_url:128 }}"></p>
<p>corporation from corporation object: <img src="{{ my_corporation|corporation_logo_url:128}}"></p> <p>corporation from corporation object: <img src="{{ my_corporation|corporation_logo_url:128 }}"></p>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<p>alliance from ID: <img src="{{ my_character.alliance_id|alliance_logo_url:128}}"></p> <p>alliance from ID: <img src="{{ my_character.alliance_id|alliance_logo_url:128 }}"></p>
<p>alliance from character object: <img src="{{ my_character|alliance_logo_url:128}}"></p> <p>alliance from character object: <img src="{{ my_character|alliance_logo_url:128 }}"></p>
<p>alliance from alliance object: <img src="{{ my_alliance|alliance_logo_url:128}}"></p> <p>alliance from alliance object: <img src="{{ my_alliance|alliance_logo_url:128 }}"></p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock content %}

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

View File

@@ -1,7 +1,8 @@
from allianceauth.menu.hooks import MenuItemHook
from . import urls from . import urls
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from allianceauth import hooks from allianceauth import hooks
from allianceauth.services.hooks import MenuItemHook, UrlHook from allianceauth.services.hooks import UrlHook
@hooks.register('menu_item_hook') @hooks.register('menu_item_hook')

View File

@@ -2,7 +2,6 @@
import datetime import datetime
from django.db import migrations, models from django.db import migrations, models
from django.utils.timezone import utc
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -15,6 +14,6 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='fatlink', model_name='fatlink',
name='fatdatetime', name='fatdatetime',
field=models.DateTimeField(default=datetime.datetime(2016, 9, 5, 22, 20, 2, 999041, tzinfo=utc)), field=models.DateTimeField(default=datetime.datetime(2016, 9, 5, 22, 20, 2, 999041, tzinfo=datetime.timezone.utc)),
), ),
] ]

View File

@@ -1,27 +1,28 @@
{% extends 'allianceauth/base.html' %} {% extends 'allianceauth/base-bs5.html' %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Fleet Participation" %}{% endblock %} {% block page_title %}
{% translate "Fleet Participation" %}
{% endblock %}
{% block content %} {% block content %}
<div class="col-lg-12"> <div class="col-lg-12">
<h1 class="page-header text-center">{% translate "Character not found!" %}</h1> <h1 class="page-header text-center">{% translate "Character not found!" %}</h1>
<div class="col-lg-12 container" id="example"> <div class="col-lg-12 container" id="example">
<div class="row"> <div class="row">
<div class="col-lg-12"> <div class="col-lg-12">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading">{{ character_name }}</div> <div class="panel-heading">{{ character_name }}</div>
<div class="panel-body"> <div class="panel-body">
<div class="col-lg-2 col-sm-2"> <div class="col-lg-2 col-sm-2">
<img class="ra-avatar img-responsive" src="{{ character_portrait_url }}" alt="{{ character_name }}"> <img class="ra-avatar img-responsive" src="{{ character_portrait_url }}" alt="{{ character_name }}">
</div> </div>
<div class="col-lg-10 col-sm-2"> <div class="col-lg-10 col-sm-2">
<div class="alert alert-danger" role="alert">{% translate "Character not registered!" %}</div> <div class="alert alert-danger" role="alert">{% translate "Character not registered!" %}</div>
{% translate "This character is not associated with an auth account." %} <a href=" {% url 'authentication:add_character' %}">{% translate "Add it here" %}</a> {% translate "before attempting to click fleet attendance links." %} {% translate "This character is not associated with an auth account." %} <a href=" {% url 'authentication:add_character' %}">{% translate "Add it here" %}</a> {% translate "before attempting to click fleet attendance links." %}
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
{% endblock %} {% endblock %}

View File

@@ -1,31 +1,31 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base-bs5.html" %}
{% load bootstrap %} {% load bootstrap %}
{% load i18n %} {% load i18n %}
{% block page_title %}
{% block page_title %}{% translate "Create Fatlink" %}{% endblock page_title %} {% translate "Create Fatlink" %}
{% endblock page_title %}
{% block content %} {% block content %}
<div class="col-lg-12"> <div class="col-lg-12">
<h1 class="page-header text-center">{% translate "Create Fleet Operation" %}</h1> <h1 class="page-header text-center">{% translate "Create Fleet Operation" %}</h1>
<div class="container-fluid"> <div class="container-fluid">
{% if badrequest %} {% if badrequest %}
<div class="alert alert-danger" role="alert">{% translate "Bad request!" %}</div> <div class="alert alert-danger" role="alert">{% translate "Bad request!" %}</div>
{% endif %} {% endif %}
{% for message in errormessages %} {% for message in errormessages %}<div class="alert alert-danger" role="alert">{{ message }}</div>{% endfor %}
<div class="alert alert-danger" role="alert">{{ message }}</div> <div class="col-md-4 offset-md-4">
{% endfor %}
<div class="col-md-4 col-md-offset-4">
<div class="row"> <div class="row">
<form class="form-signin" role="form" action="" method="POST"> <form class="form-signin" role="form" action="" method="POST">
{% csrf_token %} {% csrf_token %}
{{ form|bootstrap }} {{ form|bootstrap }}
<br> <br>
<button class="btn btn-lg btn-primary btn-block" type="submit" name="submit_fat">{% translate "Create fatlink" %}</button> <button class="btn btn-lg btn-primary btn-block"
</form> type="submit"
name="submit_fat">
{% translate "Create fatlink" %}
</button>
</form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock content %} {% endblock content %}

View File

@@ -1,11 +1,11 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base-bs5.html" %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Fatlink view" %}{% endblock page_title %} {% block page_title %}{% translate "Fatlink view" %}{% endblock page_title %}
{% block content %} {% block content %}
<div class="col-lg-12"> <div class="col-lg-12">
<h1 class="page-header text-center">{% translate "Edit fatlink" %} "{{ fatlink }}" <h1 class="page-header text-center">{% translate "Edit fatlink" %} "{{ fatlink }}"
<div class="text-right"> <div class="text-end">
<form> <form>
<button type="submit" onclick="return confirm('Are you sure?')" class="btn btn-danger" name="deletefat" value="True"> <button type="submit" onclick="return confirm('Are you sure?')" class="btn btn-danger" name="deletefat" value="True">
{% translate "Delete fat" %} {% translate "Delete fat" %}

View File

@@ -1,4 +1,4 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base-bs5.html" %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Personal fatlink statistics" %}{% endblock page_title %} {% block page_title %}{% translate "Personal fatlink statistics" %}{% endblock page_title %}
@@ -7,7 +7,7 @@
<div class="col-lg-12"> <div class="col-lg-12">
<h1 class="page-header text-center">{% blocktranslate %}Participation data statistics for {{ month }}, {{ year }}{% endblocktranslate %} <h1 class="page-header text-center">{% blocktranslate %}Participation data statistics for {{ month }}, {{ year }}{% endblocktranslate %}
{% if char_id %} {% if char_id %}
<div class="text-right"> <div class="text-end">
<a href="{% url 'fatlink:user_statistics_month' char_id previous_month|date:'Y' previous_month|date:'m' %}" class="btn btn-info">{% translate "Previous month" %}</a> <a href="{% url 'fatlink:user_statistics_month' char_id previous_month|date:'Y' previous_month|date:'m' %}" class="btn btn-info">{% translate "Previous month" %}</a>
<a href="{% url 'fatlink:user_statistics_month' char_id next_month|date:'Y' next_month|date:'m' %}" class="btn btn-info">{% translate "Next month" %}</a> <a href="{% url 'fatlink:user_statistics_month' char_id next_month|date:'Y' next_month|date:'m' %}" class="btn btn-info">{% translate "Next month" %}</a>
</div> </div>
@@ -33,39 +33,39 @@
{% endfor %} {% endfor %}
</table> </table>
{% if created_fats %} {% if created_fats %}
<h2> <h2>
{% blocktranslate count links=n_created_fats trimmed %} {% blocktranslate count links=n_created_fats trimmed %}
{{ user }} has created one link this month. {{ user }} has created one link this month.
{% plural %} {% plural %}
{{ user }} has created {{ links }} links this month. {{ user }} has created {{ links }} links this month.
{% endblocktranslate %} {% endblocktranslate %}
</h2> </h2>
{% if created_fats %} {% if created_fats %}
<table class="table"> <table class="table">
<tr> <tr>
<th class="text-center">{% translate "Fleet" %}</th> <th class="text-center">{% translate "Fleet" %}</th>
<th class="text-center">{% translate "Creator" %}</th> <th class="text-center">{% translate "Creator" %}</th>
<th class="text-center">{% translate "Eve Time" %}</th> <th class="text-center">{% translate "Eve Time" %}</th>
<th class="text-center">{% translate "Duration" %}</th> <th class="text-center">{% translate "Duration" %}</th>
<th class="text-center">{% translate "Edit" %}</th> <th class="text-center">{% translate "Edit" %}</th>
</tr> </tr>
{% for link in created_fats %} {% for link in created_fats %}
<tr> <tr>
<td class="text-center"><a href="{% url 'fatlink:click' link.hash %}" class="label label-primary">{{ link.fleet }}</a></td> <td class="text-center"><a href="{% url 'fatlink:click' link.hash %}" class="badge bg-primary">{{ link.fleet }}</a></td>
<td class="text-center">{{ link.creator.username }}</td> <td class="text-center">{{ link.creator.username }}</td>
<td class="text-center">{{ link.fatdatetime }}</td> <td class="text-center">{{ link.fatdatetime }}</td>
<td class="text-center">{{ link.duration }}</td> <td class="text-center">{{ link.duration }}</td>
<td class="text-center"> <td class="text-center">
<a href="{% url 'fatlink:modify' link.hash %}"> <a href="{% url 'fatlink:modify' link.hash %}">
<button type="button" class="btn btn-info"><span <button type="button" class="btn btn-info"><span
class="glyphicon glyphicon-edit"></span></button> class="glyphicon glyphicon-edit"></span></button>
</a> </a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
{% endblock content %} {% endblock content %}

View File

@@ -1,35 +1,34 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base-bs5.html" %}
{% load i18n %} {% load i18n %}
{% block page_title %}
{% block page_title %}{% translate "Personal fatlink statistics" %}{% endblock page_title %} {% translate "Personal fatlink statistics" %}
{% endblock page_title %}
{% block content %} {% block content %}
<div class="col-lg-12"> <div class="col-lg-12">
<h1 class="page-header text-center">{% blocktranslate %}Participation data statistics for {{ year }}{% endblocktranslate %} <h1 class="page-header text-center">
<div class="text-right"> {% blocktranslate %}Participation data statistics for {{ year }}{% endblocktranslate %}
<a href="{% url 'fatlink:personal_statistics_year' previous_year %}" class="btn btn-info">{% translate "Previous year" %}</a> <div class="text-end">
{% if next_year %} <a href="{% url "fatlink:personal_statistics_year" previous_year %}" class="btn btn-info"><i class="fa-solid fa-chevron-left"></i> {% translate "Previous year" %}</a>
<a href="{% url 'fatlink:personal_statistics_year' next_year %}" class="btn btn-info">{% translate "Next year" %}</a> {% if next_year %}
{% endif %} <a href="{% url "fatlink:personal_statistics_year" next_year %}" class="btn btn-info">{% translate "Next year" %} <i class="fa-solid fa-chevron-right"></i></a>
{% endif %}
</div> </div>
</h1> </h1>
<div class="col-lg-2 col-lg-offset-5"> <div class="col-lg-2 offset-lg-5">
<table class="table table-responsive"> <table class="table table-responsive">
<tr> <tr>
<th class="col-md-2 text-center">{% translate "Month" %}</th> <th scope="col" class="col-md-2 text-center">{% translate "Month" %}</th>
<th class="col-md-2 text-center">{% translate "Fats" %}</th> <th scope="col" class="col-md-2 text-center">{% translate "Fats" %}</th>
</tr> </tr>
{% for monthnr, month, n_fats in monthlystats %} {% for monthnr, month, n_fats in monthlystats %}
<tr> <tr>
<td class="text-center"> <td class="text-center">
<a href="{% url 'fatlink:personal_statistics_month' year monthnr %}"> <a href="{% url 'fatlink:personal_statistics_month' year monthnr %}">{{ month }}</a>
{{ month }} </td>
</a> <td class="text-center">{{ n_fats }}</td>
</td> </tr>
<td class="text-center">{{ n_fats }}</td> {% endfor %}
</tr> </table>
{% endfor %}
</table>
</div> </div>
</div> </div>
{% endblock content %} {% endblock content %}

View File

@@ -1,46 +1,50 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base-bs5.html" %}
{% load i18n %} {% load i18n %}
{% block page_title %}
{% block page_title %}{% translate "Fatlink Corp Statistics" %}{% endblock page_title %} {% translate "Fatlink Corp Statistics" %}
{% endblock page_title %}
{% block content %} {% block content %}
<div class="col-lg-12"> <div class="col-lg-12">
<h1 class="page-header text-center">{% blocktranslate %}Participation data statistics for {{ month }}, {{ year }}{% endblocktranslate %} <h1 class="page-header text-center">
<div class="text-right"> {% blocktranslate %}Participation data statistics for {{ month }}, {{ year }}{% endblocktranslate %}
<a href="{% url 'fatlink:statistics_corp_month' corpid previous_month|date:"Y" previous_month|date:"m" %}" class="btn btn-info">{% translate "Previous month" %}</a> <div class="text-end">
{% if next_month %} <a href="{% url "fatlink:statistics_corp_month" corpid previous_month|date:"Y" previous_month|date:"m" %}" class="btn btn-info">{% translate "Previous month" %}</a>
<a href="{% url 'fatlink:statistics_corp_month' corpid next_month|date:"Y" next_month|date:"m" %}" class="btn btn-info">{% translate "Next month" %}</a> {% if next_month %}
{% endif %} <a href="{% url "fatlink:statistics_corp_month" corpid next_month|date:"Y" next_month|date:"m" %}" class="btn btn-info">{% translate "Next month" %}</a>
{% endif %}
</div> </div>
</h1> </h1>
{% if fatStats %} {% if fatStats %}
<table class="table table-responsive"> <div class="table-responsive">
<tr> <table class="table table-striped">
<th class="col-md-1"></th> <tr>
<th class="col-md-2 text-center">{% translate "Main Character" %}</th> <th scope="col" class="col-md-1"></th>
<th class="col-md-2 text-center">{% translate "Characters" %}</th> <th scope="col" class="col-md-2 text-center">{% translate "Main Character" %}</th>
<th class="col-md-2 text-center">{% translate "Fats" %}</th> <th scope="col" class="col-md-2 text-center">{% translate "Characters" %}</th>
<th class="col-md-2 text-center">{% translate "Average fats" %} <th scope="col" class="col-md-2 text-center">{% translate "Fats" %}</th>
<i class="glyphicon glyphicon-question-sign" rel="tooltip" title="Fats ÷ Characters"></i> <th scope="col" class="col-md-2 text-center">
</th> {% translate "Average fats" %}
</tr> <i class="fa-solid fa-question" rel="tooltip" title="Fats / Characters"></i>
{% for memberStat in fatStats %} </th>
<tr> </tr>
<td> {% for memberStat in fatStats %}
<img src="{{ memberStat.mainchar.portrait_url_32 }}" class="ra-avatar img-responsive" alt="{{ memberStat.mainchar.character_name }}"> <tr>
</td> <td>
<td class="text-center">{{ memberStat.mainchar.character_name }}</td> <img src="{{ memberStat.mainchar.portrait_url_32 }}" class="ra-avatar img-responsive" alt="{{ memberStat.mainchar.character_name }}">
<td class="text-center">{{ memberStat.n_chars }}</td> </td>
<td class="text-center">{{ memberStat.n_fats }}</td> <td class="text-center">{{ memberStat.mainchar.character_name }}</td>
<td class="text-center">{{ memberStat.avg_fat }}</td> <td class="text-center">{{ memberStat.n_chars }}</td>
</tr> <td class="text-center">{{ memberStat.n_fats }}</td>
{% endfor %} <td class="text-center">{{ memberStat.avg_fat }}</td>
</table> </tr>
{% endfor %}
</table>
</div>
{% endif %} {% endif %}
</div> </div>
{% endblock content %} {% endblock content %}
{% block extra_script %} {% block extra_script %}
$(document).ready(function () { $(document).ready(function () {
$("[rel=tooltip]").tooltip(); $("[rel=tooltip]").tooltip();
}); });
{% endblock extra_script %} {% endblock extra_script %}

View File

@@ -1,48 +1,54 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base-bs5.html" %}
{% load i18n %} {% load i18n %}
{% block page_title %}
{% block page_title %}{% translate "Fatlink statistics" %}{% endblock page_title %} {% translate "Fatlink Statistics" %}
{% endblock page_title %}
{% block content %} {% block content %}
<div class="col-lg-12"> <div class="col-lg-12">
<h1 class="page-header text-center">{% blocktranslate %}Participation data statistics for {{ month }}, {{ year }}{% endblocktranslate %} <h1 class="page-header text-center">
<div class="text-right"> {% blocktranslate %}Participation data statistics for {{ month }}, {{ year }}{% endblocktranslate %}
<a href="{% url 'fatlink:statistics_month' previous_month|date:"Y" previous_month|date:"m" %}" class="btn btn-info">{% translate "Previous month" %}</a> <div class="text-end">
{% if next_month %} <a href="{% url "fatlink:statistics_month" previous_month|date:"Y" previous_month|date:"m" %}" class="btn btn-info">{% translate "Previous month" %}</a>
<a href="{% url 'fatlink:statistics_month' next_month|date:"Y" next_month|date:"m" %}" class="btn btn-info">{% translate "Next month" %}</a> {% if next_month %}
{% endif %} <a href="{% url 'fatlink:statistics_month' next_month|date:"Y" next_month|date:"m" %}" class="btn btn-info">{% translate "Next month" %}</a>
{% endif %}
</div> </div>
</h1> </h1>
{% if fatStats %} {% if fatStats %}
<table class="table table-responsive"> <div class="table-responsive">
<tr> <table class="table table-striped">
<th class="col-md-1"></th> <tr>
<th class="col-md-2 text-center">{% translate "Ticker" %}</th> <th scope="col" class="col-md-1"></th>
<th class="col-md-5 text-center">{% translate "Corp" %}</th> <th scope="col" class="col-md-2 text-center">{% translate "Ticker" %}</th>
<th class="col-md-2 text-center">{% translate "Members" %}</th> <th scope="col" class="col-md-5 text-center">{% translate "Corp" %}</th>
<th class="col-md-2 text-center">{% translate "Fats" %}</th> <th scope="col" class="col-md-2 text-center">{% translate "Members" %}</th>
<th class="col-md-2 text-center">{% translate "Average fats" %} <th scope="col" class="col-md-2 text-center">{% translate "Fats" %}</th>
<i class="glyphicon glyphicon-question-sign" rel="tooltip" title="Fats ÷ Characters"></i> <th scope="col" class="col-md-2 text-center">
</th> {% translate "Average fats" %}
</tr> <i class="fa-solid fa-question" rel="tooltip" title="Fats / Characters"></i>
{% for corpStat in fatStats %} </th>
<tr> </tr>
<td> {% for corpStat in fatStats %}
<img src="{{ corpStat.corp.logo_url_32 }}" class="ra-avatar img-responsive" alt="{{ corpStat.corp.corporation_name }}"> <tr>
</td> <td>
<td class="text-center"><a href="{% url 'fatlink:statistics_corp' corpStat.corp.corporation_id %}">[{{ corpStat.corp.corporation_ticker }}]</a></td> <img src="{{ corpStat.corp.logo_url_32 }}" class="ra-avatar img-responsive" alt="{{ corpStat.corp.corporation_name }}">
<td class="text-center">{{ corpStat.corp.corporation_name }}</td> </td>
<td class="text-center">{{ corpStat.corp.member_count }}</td> <td class="text-center">
<td class="text-center">{{ corpStat.n_fats }}</td> <a href="{% url 'fatlink:statistics_corp' corpStat.corp.corporation_id %}">[{{ corpStat.corp.corporation_ticker }}]</a>
<td class="text-center">{{ corpStat.avg_fat }}</td> </td>
</tr> <td class="text-center">{{ corpStat.corp.corporation_name }}</td>
{% endfor %} <td class="text-center">{{ corpStat.corp.member_count }}</td>
</table> <td class="text-center">{{ corpStat.n_fats }}</td>
<td class="text-center">{{ corpStat.avg_fat }}</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %} {% endif %}
</div> </div>
{% endblock content %} {% endblock content %}
{% block extra_script %} {% block extra_script %}
$(document).ready(function () { $(document).ready(function () {
$("[rel=tooltip]").tooltip(); $("[rel=tooltip]").tooltip();
}); });
{% endblock extra_script %} {% endblock extra_script %}

View File

@@ -1,99 +1,107 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base-bs5.html" %}
{% load i18n %} {% load i18n %}
{% block page_title %}
{% block page_title %}{% translate "Fatlink view" %}{% endblock page_title %} {% translate "Fatlink view" %}
{% endblock page_title %}
{% block content %} {% block content %}
<div class="col-lg-12"> <div class="col-lg-12">
<h1 class="page-header text-center">{% translate "Participation data" %}</h1> <h1 class="page-header text-center">{% translate "Participation data" %}</h1>
<table class="table"> <div class="table-responsive">
<tr> <table class="table table-striped">
<th class="col-md-11">
<h4><b>{% translate "Most recent clicked fatlinks" %}</b>
</h4>
</th>
<th class="col-md-1">
<a href="{% url 'fatlink:personal_statistics' %}" class="btn btn-info">
{% translate "Personal statistics" %}
</a>
</th>
</tr>
</table>
{% if fats %}
<table class="table table-responsive">
<tr>
<th class="text-center">{% translate "Fleet" %}</th>
<th class="text-center">{% translate "Character" %}</th>
<th class="text-center">{% translate "System" %}</th>
<th class="text-center">{% translate "Ship" %}</th>
<th class="text-center">{% translate "Eve Time" %}</th>
</tr>
{% for fat in fats %}
<tr>
<td class="text-center">{{ fat.fatlink.fleet }}</td>
<td class="text-center">{{ fat.character.character_name }}</td>
{% if fat.station != "No Station" %}
<td class="text-center">{% blocktranslate %}Docked in {% endblocktranslate %}{{ fat.system }}</td>
{% else %}
<td class="text-center">{{ fat.system }}</td>
{% endif %}
<td class="text-center">{{ fat.shiptype }}</td>
<td class="text-center">{{ fat.fatlink.fatdatetime }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<div class="alert alert-warning text-center">{% translate "No fleet activity on record." %}</div>
{% endif %}
{% if perms.auth.fleetactivitytracking%}
<table class="table">
<tr> <tr>
<th class="col-md-10"> <th class="col-md-10">
<h4><b>{% translate "Most recent fatlinks" %}</b> <h4>
<b>{% translate "Most recent clicked fatlinks" %}</b>
</h4> </h4>
</th> </th>
<th class="col-md-1"> <th class="col-md-2 align-self-end">
<a href="{% url 'fatlink:statistics' %}" class="btn btn-info"> <a href="{% url 'fatlink:personal_statistics' %}" class="btn btn-info"><i class="fa-solid fa-circle-info fa-fw"></i>{% translate "Personal statistics" %}</a>
{% translate "View statistics" %}
</a>
</th>
<th class="col-md-1">
<a href="{% url 'fatlink:create' %}" class="btn btn-success">
{% translate "Create fatlink" %}
</a>
</th> </th>
</tr> </tr>
</table> </table>
{% if fatlinks %} </div>
<table class="table"> {% if fats %}
<tr> <div class="table-responsive">
<th class="text-center">{% translate "Name" %}</th> <table class="table table-striped">
<th class="text-center">{% translate "Creator" %}</th> <tr>
<th class="text-center">{% translate "Fleet" %}</th> <th scope="col" class="text-center">{% translate "Fleet" %}</th>
<th class="text-center">{% translate "Eve Time" %}</th> <th scope="col" class="text-center">{% translate "Character" %}</th>
<th class="text-center">{% translate "Duration" %}</th> <th scope="col" class="text-center">{% translate "System" %}</th>
<th class="text-center">{% translate "Edit" %}</th> <th scope="col" class="text-center">{% translate "Ship" %}</th>
</tr> <th scope="col" class="text-center">{% translate "Eve Time" %}</th>
{% for link in fatlinks %} </tr>
<tr> {% for fat in fats %}
<td class="text-center"><a href="{% url 'fatlink:click' link.hash %}" class="label label-primary">{{ link.fleet }}</a></td> <tr>
<td class="text-center">{{ link.creator.username }}</td> <td class="text-center">{{ fat.fatlink.fleet }}</td>
<td class="text-center">{{ link.fleet }}</td> <td class="text-center">{{ fat.character.character_name }}</td>
<td class="text-center">{{ link.fatdatetime }}</td> {% if fat.station != "No Station" %}
<td class="text-center">{{ link.duration }}</td> <td class="text-center">{% translate "Docked in" %} {{ fat.system }}</td>
<td class="text-center"> {% else %}
<a href="{% url 'fatlink:modify' link.hash %}" class="btn btn-info"> <td class="text-center">{{ fat.system }}</td>
<span class="glyphicon glyphicon-edit"></span> {% endif %}
</a> <td class="text-center">{{ fat.shiptype }}</td>
</td> <td class="text-center">{{ fat.fatlink.fatdatetime }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table>
</table> </div>
{% else %} {% else %}
<div class="alert alert-warning text-center">{% translate "No created fatlinks on record." %}</div> <div class="alert alert-warning text-center">{% translate "No fleet activity on record." %}</div>
{% endif %} {% endif %}
{% if perms.auth.fleetactivitytracking %}
<div class="table-responsive">
<table class="table table-striped">
<tr>
<th class="col-md-8">
<h4>
<b>{% translate "Most recent fatlinks" %}</b>
</h4>
</th>
<th class="col-md-2 align-self-end">
<a href="{% url 'fatlink:statistics' %}" class="btn btn-info"><i class="fa-solid fa-eye fa-fw"></i> {% translate "View statistics" %}</a>
</th>
<th class="col-md-2 align-self-end">
<a href="{% url 'fatlink:create' %}" class="btn btn-success"><i class="fa-solid fa-plus fa-fw"></i> {% translate "Create fatlink" %}</a>
</th>
</tr>
</table>
</div>
{% if fatlinks %}
<div class="table-responsive">
<table class="table table-striped">
<tr>
<th scope="col" class="text-center">{% translate "Name" %}</th>
<th scope="col" class="text-center">{% translate "Creator" %}</th>
<th scope="col" class="text-center">{% translate "Fleet" %}</th>
<th scope="col" class="text-center">{% translate "Eve Time" %}</th>
<th scope="col" class="text-center">{% translate "Duration" %}</th>
<th scope="col" class="text-center">{% translate "Edit" %}</th>
</tr>
{% for link in fatlinks %}
<tr>
<td class="text-center">
<a href="{% url 'fatlink:click' link.hash %}" class="badge bg-primary">{{ link.fleet }}</a>
</td>
<td class="text-center">{{ link.creator.username }}</td>
<td class="text-center">{{ link.fleet }}</td>
<td class="text-center">{{ link.fatdatetime }}</td>
<td class="text-center">
{{ link.duration }}
</td>
<td class="text-center">
<a href="{% url 'fatlink:modify' link.hash %}" class="btn btn-info">
<i class="fa-solid fa-pen-to-square fa-fw"></i>
</a>
</td>
</tr>
{% endfor %}
</table>
</div>
{% else %}
<div class="alert alert-warning text-center">
{% translate "No created fatlinks on record." %}
</div>
{% endif %}
{% endif %} {% endif %}
</div> </div>
{% endblock content %} {% endblock content %}

View File

@@ -1,6 +1,7 @@
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from allianceauth.menu.hooks import MenuItemHook
from allianceauth.services.hooks import MenuItemHook, UrlHook from allianceauth.services.hooks import UrlHook
from allianceauth import hooks from allianceauth import hooks
from . import urls from . import urls
@@ -33,11 +34,43 @@ class GroupManagementMenuItem(MenuItemHook):
return "" return ""
"""
<li class="d-flex m-2 p-2 pt-0 pb-0 mt-0 mb-0">
<i class="fas fa-users fa-fw align-self-center me-2"></i>
<a class="nav-link flex-fill align-self-center {% navactive request 'groupmanagement:groups' %}" href="{% url 'groupmanagement:groups' %}">
{% translate "Groups" %}
</a>
</li>
"""
class GroupsMenuItem(MenuItemHook):
def __init__(self):
MenuItemHook.__init__(
self,
text=_("Groups"),
classes="fas fa-user fa-fw",
url_name="groupmanagement:groups",
order=25,
navactive=[
"groupmanagement:groups", # group list view
],
)
def render(self, request):
return MenuItemHook.render(self, request)
@hooks.register("menu_item_hook") @hooks.register("menu_item_hook")
def register_menu(): def register_manager_menu():
return GroupManagementMenuItem() return GroupManagementMenuItem()
@hooks.register("menu_item_hook")
def register_groups_menu():
return GroupsMenuItem()
@hooks.register("url_hook") @hooks.register("url_hook")
def register_urls(): def register_urls():
return UrlHook(urls, "group", r"^groups/") return UrlHook(urls, "group", r"^groups/")

View File

@@ -1,85 +1,75 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base-bs5.html" %}
{% load static %}
{% load i18n %} {% load i18n %}
{% load navactive %}
{% block page_title %}{{ group }} {% translate "Audit Log" %}{% endblock page_title %} {% block page_title %}{{ group }} {% translate "Audit Log" %}{% endblock page_title %}
{% block header_nav_brand %}{% translate "Audit Log" %} - {{ group.name }}{% endblock header_nav_brand %}
{% block header_nav_collapse_left %}
<li class="nav-item ">
<a class="nav-link {% navactive request 'groupmanagement:management' %}" href="{% url 'groupmanagement:management' %}">{% translate "Back" %}</a>
</li>
{% endblock %}
{% block content %} {% block content %}
<div class="col-lg-12"> {% if entries %}
<br> <div class="table-responsive">
{% include 'groupmanagement/menu.html' %} <table class="table table-striped" id="log-entries">
<thead>
<tr>
<th scope="col">{% translate "Date/Time" %}</th>
<th scope="col">{% translate "Requestor" %}</th>
<th scope="col">{% translate "Character" %}</th>
<th scope="col">{% translate "Corporation" %}</th>
<th scope="col">{% translate "Type" %}</th>
<th scope="col">{% translate "Action" %}</th>
<th scope="col">{% translate "Actor" %}</th>
</tr>
</thead>
<div class="panel panel-default"> <tbody>
<div class="panel-heading"> {% for entry in entries %}
{{ group }} - {% translate "Audit Log" %} <tr>
</div> <td>{{ entry.date|date:"Y-M-d, H:i" }}</td>
<td>{{ entry.requestor }}</td>
<td>{{ entry.req_char }}</td>
<td>{{ entry.req_char.corporation_name }}</td>
<td>{{ entry.type_to_str }}</td>
<div class="panel-body"> {% if entry.request_type is None %}
<p> <td>{% translate "Removed" %}</td>
<a class="btn btn-default" href="{% url 'groupmanagement:membership' %}" role="button"> {% else %}
{% translate "Back" %} <td>{{ entry.action_to_str }}</td>
</a> {% endif %}
</p>
{% if entries %} <td>{{ entry.request_actor }}</td>
<div class="table-responsive"> </tr>
<table class="table table-striped" id="log-entries"> {% endfor %}
<thead> </tbody>
<tr> </table>
<th scope="col">{% translate "Date/Time" %}</th>
<th scope="col">{% translate "Requestor" %}</th>
<th scope="col">{% translate "Character" %}</th>
<th scope="col">{% translate "Corporation" %}</th>
<th scope="col">{% translate "Type" %}</th>
<th scope="col">{% translate "Action" %}</th>
<th scope="col">{% translate "Actor" %}</th>
</tr>
</thead>
<tbody> <p class="text-muted">
{% for entry in entries %} {% translate "All times displayed are EVE/UTC." %}
<tr> </p>
<td>{{ entry.date|date:"Y-M-d, H:i" }}</td>
<td>{{ entry.requestor }}</td>
<td>{{ entry.req_char }}</td>
<td>{{ entry.req_char.corporation_name }}</td>
<td>{{ entry.type_to_str }}</td>
{% if entry.request_type is None %}
<td>{% translate "Removed" %}</td>
{% else %}
<td>{{ entry.action_to_str }}</td>
{% endif %}
<td>{{ entry.request_actor }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p class="text-muted">
{% translate "All times displayed are EVE/UTC." %}
</p>
</div>
{% else %}
<div class="clearfix"></div>
<br>
<div class="alert alert-warning text-center">
{% translate "No entries found for this group." %}
</div>
{% endif %}
</div>
</div> </div>
</div> {% else %}
<div class="clearfix"></div>
<br>
<div class="alert alert-warning text-center">
{% translate "No entries found for this group." %}
</div>
{% endif %}
{% endblock %} {% endblock %}
{% block extra_javascript %} {% block extra_javascript %}
{% include 'bundles/datatables-js.html' %} {% include 'bundles/datatables-js-bs5.html' %}
{% include 'bundles/moment-js.html' with locale=True %} {% include 'bundles/moment-js.html' with locale=True %}
{% include 'bundles/filterdropdown-js.html' %} {% include 'bundles/filterdropdown-js.html' %}
{% endblock %} {% endblock %}
{% block extra_css %} {% block extra_css %}
{% include 'bundles/datatables-css.html' %} {% include 'bundles/datatables-css-bs5.html' %}
{% endblock %} {% endblock %}
{% block extra_script %} {% block extra_script %}

View File

@@ -1,97 +1,86 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base-bs5.html" %}
{% load static %}
{% load i18n %} {% load i18n %}
{% load evelinks %} {% load evelinks %}
{% load navactive %}
{% block page_title %}{% translate "Group Members" %}{% endblock page_title %} {% block page_title %}{% translate "Group Members" %}{% endblock page_title %}
{% block header_nav_brand %}{% translate "Group Members" %} - {{ group.name }}{% endblock header_nav_brand %}
{% block header_nav_collapse_left %}
<li class="nav-item ">
<a class="nav-link {% navactive request 'groupmanagement:management' %}" href="{% url 'groupmanagement:management' %}">{% translate "Back" %}</a>
</li>
{% endblock %}
{% block content %} {% block content %}
<div class="col-lg-12"> {% if group.user_set %}
<br> <div class="table-responsive">
{% include 'groupmanagement/menu.html' %} <table class="table table-aa" id="tab_group_members">
<thead>
<tr>
<th>{% translate "Character" %}</th>
<th>{% translate "Organization" %}</th>
<th></th>
</tr>
</thead>
<div class="panel panel-default"> <tbody>
<div class="panel-heading"> {% for member in members %}
{{ group.name }} - {% translate 'Members' %} <tr>
</div> <td>
<img src="{{ member.main_char|character_portrait_url:32 }}" class="rounded-circle" style="margin-right: 1rem;" alt="{{ member.main_char.character_name }}">
{% if member.main_char %}
<a href="{{ member.main_char|evewho_character_url }}" target="_blank">
{{ member.main_char.character_name }}
</a>
{% else %}
{{ member.user.username }}
{% endif %}
<div class="panel-body"> {% if member.is_leader %}
<p> <i class="fa-solid fa-star"> title="{% translate "Group leader" %}" style="margin-left: 1rem;"></i>&nbsp;
<a class="btn btn-default" href="{% url 'groupmanagement:membership' %}" role="button"> {% endif %}
{% translate "Back" %} </td>
</a>
</p>
{% if group.user_set %} <td>
<div class="table-responsive"> {% if member.main_char %}
<table class="table table-aa" id="tab_group_members"> <a href="{{ member.main_char|dotlan_corporation_url }}" target="_blank">
<thead> {{ member.main_char.corporation_name }}
<tr> </a><br>
<th>{% translate "Character" %}</th> {{ member.main_char.alliance_name|default_if_none:"" }}
<th>{% translate "Organization" %}</th> {% else %}
<th></th> {% translate "(unknown)" %}
</tr> {% endif %}
</thead> </td>
<td class="text-end">
<tbody> <a href="{% url 'groupmanagement:membership_remove' group.id member.user.id %}" class="btn btn-danger" title="{% translate "Remove from group" %}">
{% for member in members %} <i class="fa-solid fa-xmark"></i>
<tr> </a>
<td> </td>
<img src="{{ member.main_char|character_portrait_url:32 }}" class="img-circle" style="margin-right: 1rem;" alt="{{ member.main_char.character_name }}"> </tr>
{% if member.main_char %} {% endfor %}
<a href="{{ member.main_char|evewho_character_url }}" target="_blank"> </tbody>
{{ member.main_char.character_name }} </table>
</a>
{% else %}
{{ member.user.username }}
{% endif %}
{% if member.is_leader %}
<i class="fas fa-star" title="{% translate "Group leader" %}" style="margin-left: 1rem;"></i>&nbsp;
{% endif %}
</td>
<td>
{% if member.main_char %}
<a href="{{ member.main_char|dotlan_corporation_url }}" target="_blank">
{{ member.main_char.corporation_name }}
</a><br>
{{ member.main_char.alliance_name|default_if_none:"" }}
{% else %}
{% translate "(unknown)" %}
{% endif %}
</td>
<td class="text-right">
<a href="{% url 'groupmanagement:membership_remove' group.id member.user.id %}" class="btn btn-danger" title="{% translate "Remove from group" %}">
<i class="glyphicon glyphicon-remove"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<p class="text-muted">
<i class="fas fa-star"></i>: {% translate "Group leader" %}
</p>
</div>
{% else %}
<div class="alert alert-warning text-center">
{% translate "No group members to list." %}
</div>
{% endif %}
</div>
<p class="text-muted">
<i class="fa-solid fa-star"></i>: {% translate "Group leader" %}
</p>
</div> </div>
</div> {% else %}
<div class="alert alert-warning text-center">
{% translate "No group members to list." %}
</div>
{% endif %}
{% endblock content %} {% endblock content %}
{% block extra_javascript %} {% block extra_javascript %}
{% include 'bundles/datatables-js.html' %} {% include 'bundles/datatables-js-bs5.html' %}
{% endblock %} {% endblock %}
{% block extra_css %} {% block extra_css %}
{% include 'bundles/datatables-css.html' %} {% include 'bundles/datatables-css-bs5.html' %}
{% endblock %} {% endblock %}
{% block extra_script %} {% block extra_script %}

View File

@@ -1,88 +1,86 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base-bs5.html" %}
{% load static %}
{% load i18n %} {% load i18n %}
{% load navactive %}
{% block page_title %}{% translate "Groups Membership" %}{% endblock page_title %} {% block page_title %}{% translate "Groups Membership" %}{% endblock page_title %}
{% block header_nav_brand %}{% translate "Groups Membership" %}{% endblock header_nav_brand %}
{% block extra_css %}{% endblock extra_css %} {% block extra_css %}{% endblock extra_css %}
{% block header_nav_collapse_left %}
<li class="nav-item ">
<a class="nav-link {% navactive request 'groupmanagement:management' %}" href="{% url 'groupmanagement:management' %}">{% translate "Join/Leave Requests" %}</a>
</li>
{% endblock header_nav_collapse_left %}
{% block content %} {% block content %}
<div class="col-lg-12"> {% if groups %}
<br> <div class="table-responsive">
{% include 'groupmanagement/menu.html' %} <table class="table table-aa">
<thead>
<tr>
<th>{% translate "Name" %}</th>
<th>{% translate "Description" %}</th>
<th>{% translate "Status" %}</th>
<th style="white-space: nowrap;" class="text-center">{% translate "Member Count" %}</th>
<th style="min-width: 170px;"></th>
</tr>
</thead>
<div class="panel panel-default"> <tbody class="align-middle">
<div class="panel-heading"> {% for group in groups %}
{% translate "Groups" %} <tr>
</div> <td>
<a href="{% url 'groupmanagement:membership' group.id %}">{{ group.name }}</a>
</td>
<div class="panel-body"> <td>{{ group.authgroup.description|linebreaks|urlize }}</td>
{% if groups %}
<div class="table-responsive">
<table class="table table-aa">
<thead>
<tr>
<th>{% translate "Name" %}</th>
<th>{% translate "Description" %}</th>
<th>{% translate "Status" %}</th>
<th style="white-space: nowrap;">{% translate "Member Count" %}</th>
<th style="min-width: 170px;"></th>
</tr>
</thead>
<tbody> <td>
{% for group in groups %} {% if group.authgroup.hidden %}
<tr> <span class="badge bg-info">{% translate "Hidden" %}</span>
<td> {% endif %}
<a href="{% url 'groupmanagement:membership' group.id %}">{{ group.name }}</a> {% if group.authgroup.open %}
</td> <span class="badge bg-success">{% translate "Open" %}</span>
{% else %}
<span class="badge bg-secondary">{% translate "Requestable" %}</span>
{% endif %}
</td>
<td>{{ group.authgroup.description|linebreaks|urlize }}</td> <td class="text-center">
{{ group.num_members }}
</td>
<td> <td class="text-end">
{% if group.authgroup.hidden %} <a href="{% url 'groupmanagement:membership' group.id %}" class="btn btn-primary" title="{% translate "View Members" %}">
<span class="label label-info">{% translate "Hidden" %}</span> <i class="far fa-eye"></i>
{% elif group.authgroup.open %} </a>
<span class="label label-success">{% translate "Open" %}</span>
{% else %}
<span class="label label-default">{% translate "Requestable" %}</span>
{% endif %}
</td>
<td class="text-right"> <a href="{% url "groupmanagement:audit_log" group.id %}" class="btn btn-info" title="{% translate "Audit Members" %}">
{{ group.num_members }} <i class="far fa-list-alt"></i>
</td> </a>
<td class="text-right"> <a id="clipboard-copy" data-clipboard-text="{{ request.scheme }}://{{request.get_host}}{% url 'groupmanagement:request_add' group.id %}" class="btn btn-warning" title="{% translate "Copy Direct Join Link" %}">
<a href="{% url 'groupmanagement:membership' group.id %}" class="btn btn-primary" title="{% translate "View Members" %}"> <i class="far fa-clipboard"></i>
<i class="glyphicon glyphicon-eye-open"></i> </a>
</a> </td>
</tr>
<a href="{% url "groupmanagement:audit_log" group.id %}" class="btn btn-info" title="{% translate "Audit Members" %}"> {% endfor %}
<i class="glyphicon glyphicon-list-alt"></i> </tbody>
</a> </table>
<a id="clipboard-copy" data-clipboard-text="{{ SITE_URL }}{% url 'groupmanagement:request_add' group.id %}" class="btn btn-warning" title="{% translate "Copy Direct Join Link" %}">
<i class="glyphicon glyphicon-copy"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-warning text-center">
{% translate "No groups to list." %}
</div>
{% endif %}
</div>
</div> </div>
</div> {% else %}
<div class="alert alert-warning text-center">
{% translate "No groups to list." %}
</div>
{% endif %}
{% endblock content %} {% endblock content %}
{% block extra_javascript %} {% block extra_javascript %}
{% include 'bundles/clipboard-js.html' %} {% include "bundles/clipboard-js.html" %}
<script> <script>
new ClipboardJS('#clipboard-copy'); new ClipboardJS('#clipboard-copy');
</script> </script>
{% endblock %} {% endblock extra_javascript %}

View File

@@ -1,62 +1,93 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base-bs5.html" %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Available Groups" %}{% endblock page_title %} {% block page_title %}{% translate "Available Groups" %}{% endblock page_title %}
{% block extra_css %}{% endblock extra_css %} {% block header_nav_brand %}{% translate "Available Groups" %}{% endblock header_nav_brand %}
{% if manager_perms %}
{% block header_nav_collapse_left %}
<li class="nav-item">
<a class="nav-link" href="{% url 'groupmanagement:management' %}">{% translate "Group Management" %}
{% if req_count %}
<span class="badge bg-secondary">{{ req_count }}</span>
{% endif %}
</a>
</li>
{% endblock %}
{% endif %}
{% block content %} {% block content %}
<div class="col-lg-12"> {% if groups %}
<h1 class="page-header text-center">{% translate "Available Groups" %}</h1> <table class="table" id="groupsTable" >
{% if groups %} <thead>
<table class="table table-aa"> <tr>
<thead> <th>{% translate "Name" %}</th>
<tr> <th>{% translate "Description" %}</th>
<th>{% translate "Name" %}</th> <th>{% translate "Leaders" %}<span class="m-1 fw-lighter badge bg-primary">{% translate "User" %}</span><span class="m-1 fw-lighter badge bg-secondary ">{% translate "Group" %}</span></th>
<th>{% translate "Description" %}</th> <th></th>
<th></th> </tr>
</tr> </thead>
</thead>
<tbody> <tbody class>
{% for g in groups %} {% for g in groups %}
<tr> <tr>
<td>{{ g.group.name }}</td> <td>{{ g.group.name }}</td>
<td>{{ g.group.authgroup.description|linebreaks|urlize }}</td> <td>{{ g.group.authgroup.description|linebreaks|urlize }}</td>
<td class="text-right"> <td style="max-width: 30%;">
{% if g.group in user.groups.all %} {% if g.group.authgroup.group_leaders.all.count %}
{% if not g.request %} {% for leader in g.group.authgroup.group_leaders.all %}{% if leader.profile.main_character %}<span class="m-1 badge bg-primary">{{leader.profile.main_character}}</span>{% endif %}{% endfor %}
<a href="{% url 'groupmanagement:request_leave' g.group.id %}" class="btn btn-danger"> {% endif %}
{% translate "Leave" %} {% if g.group.authgroup.group_leaders.all.count %}
</a> {% for group in g.group.authgroup.group_leader_groups.all %}<span class="badge bg-secondary">{{group.name}}</span>{% endfor %}
{% else %} {% endif %}
<button type="button" class="btn btn-primary" disabled> </td>
{% translate "Pending" %} <td class="text-end">
</button> {% if g.group in user_groups %}
{% endif %} {% if not g.request %}
{% elif not g.request %} <a href="{% url 'groupmanagement:request_leave' g.group.id %}" class="btn btn-danger">
{% if g.group.authgroup.open %} {% translate "Leave" %}
<a href="{% url 'groupmanagement:request_add' g.group.id %}" class="btn btn-success"> </a>
{% translate "Join" %}
</a>
{% else %}
<a href="{% url 'groupmanagement:request_add' g.group.id %}" class="btn btn-primary">
{% translate "Request" %}
</a>
{% endif %}
{% else %} {% else %}
<button type="button" class="btn btn-primary" disabled> <button type="button" class="btn btn-primary" disabled>
{% translate "Pending" %} {% translate "Pending" %}
</button> </button>
{% endif %} {% endif %}
</td> {% elif not g.request %}
</tr> {% if g.group.authgroup.open %}
{% endfor %} <a href="{% url 'groupmanagement:request_add' g.group.id %}" class="btn btn-success">
</tbody> {% translate "Join" %}
</table> </a>
{% else %} {% else %}
<div class="alert alert-warning text-center"> <a href="{% url 'groupmanagement:request_add' g.group.id %}" class="btn btn-primary">
{% translate "No groups available." %} {% translate "Request" %}
</div> </a>
{% endif %} {% endif %}
</div> {% else %}
<button type="button" class="btn btn-primary" disabled>
{% translate "Pending" %}
</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="alert alert-warning text-center">
{% translate "No groups available." %}
</div>
{% endif %}
{% endblock content %} {% endblock content %}
{% block extra_javascript %}
{% include 'bundles/datatables-js-bs5.html' %}
{% endblock %}
{% block extra_css %}
{% include 'bundles/datatables-css-bs5.html' %}
{% endblock %}
{% block extra_script %}
$(document).ready(function () {
$('#groupsTable').DataTable();
});
{% endblock extra_script %}

View File

@@ -1,166 +1,158 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base-bs5.html" %}
{% load static %}
{% load i18n %} {% load i18n %}
{% load evelinks %} {% load evelinks %}
{% load navactive %}
{% block page_title %}{% translate "Groups Management" %}{% endblock page_title %} {% block page_title %}{% translate "Groups Management" %}{% endblock page_title %}
{% block header_nav_brand %}{% translate "Groups Management" %}{% endblock header_nav_brand %}
{% block extra_css %} {% block extra_css %}
<style>
.nav-tabs > li.active > a {
background-color: rgb(236, 240, 241) !important;
color: rgb(44, 62, 80);
}
</style>
{% endblock extra_css %} {% endblock extra_css %}
{% block content %} {% block header_nav_collapse_left %}
<div class="col-lg-12"> <li class="active">
<br> <a class="nav-link active" id="add-tab" data-bs-toggle="tab" data-bs-target="#add" type="button" role="tab" aria-controls="addd" aria-selected="true">
{% include 'groupmanagement/menu.html' %} {% translate "Join Requests" %}
<ul class="nav nav-tabs"> {% if acceptrequests %}
<li class="active"> <span class="badge bg-secondary">{{ acceptrequests|length }}</span>
<a data-toggle="tab" href="#add"> {% endif %}
{% translate "Join Requests" %} </a>
</li>
{% if acceptrequests %} {% if not auto_leave %}
<span class="badge">{{ acceptrequests|length }}</span> <li>
{% endif %} <a class="nav-link" id="leave-tab" data-bs-toggle="tab" data-bs-target="#leave" type="button" role="tab" aria-controls="leave" aria-selected="false">
</a> {% translate "Leave Requests" %}
</li>
{% if not auto_leave %} {% if leaverequests %}
<li> <span class="badge bg-secondary">{{ leaverequests|length }}</span>
<a data-toggle="tab" href="#leave">
{% translate "Leave Requests" %}
{% if leaverequests %}
<span class="badge">{{ leaverequests|length }}</span>
{% endif %}
</a>
</li>
{% endif %} {% endif %}
</ul> </a>
</li>
{% endif %}
<li class="nav-item ">
<a class="nav-link {% navactive request 'groupmanagement:membership groupmanagement:audit_log' %}" href="{% url 'groupmanagement:membership' %}">{% translate "Group Membership" %}</a>
</li>
<div class="panel panel-default panel-tabs-aa"> {% endblock %}
<div class="panel-body">
<div class="tab-content">
<div id="add" class="tab-pane active">
{% if acceptrequests %}
<div class="table-responsive">
<table class="table table-aa">
<thead>
<tr>
<th>{% translate "Character" %}</th>
<th>{% translate "Organization" %}</th>
<th>{% translate "Group" %}</th>
<th></th>
</tr>
</thead>
<tbody> {% block content %}
{% for acceptrequest in acceptrequests %} <div class="tab-content">
<tr> <div id="add" class="tab-pane active">
<td> {% if acceptrequests %}
<img src="{{ acceptrequest.main_char|character_portrait_url:32 }}" class="img-circle" style="margin-right: 1rem;" alt="{{ acceptrequest.main_char.character_name }}"> <div class="table-responsive">
{% if acceptrequest.main_char %} <table class="table table-aa">
<a href="{{ acceptrequest.main_char|evewho_character_url }}" target="_blank"> <thead>
{{ acceptrequest.main_char.character_name }} <tr>
</a> <th>{% translate "Character" %}</th>
{% else %} <th>{% translate "Organization" %}</th>
{{ acceptrequest.user.username }} <th>{% translate "Group" %}</th>
{% endif %} <th></th>
</td> </tr>
<td> </thead>
{% if acceptrequest.main_char %}
<a href="{{ acceptrequest.main_char|dotlan_corporation_url }}" target="_blank">
{{ acceptrequest.main_char.corporation_name }}
</a><br>
{{ acceptrequest.main_char.alliance_name|default_if_none:"" }}
{% else %}
{% translate "(unknown)" %}
{% endif %}
</td>
<td>{{ acceptrequest.group.name }}</td>
<td class="text-right">
<a href="{% url 'groupmanagement:accept_request' acceptrequest.id %}" class="btn btn-success">
{% translate "Accept" %}
</a>
<a href="{% url 'groupmanagement:reject_request' acceptrequest.id %}" class="btn btn-danger"> <tbody class="align-middle">
{% translate "Reject" %} {% for acceptrequest in acceptrequests %}
</a> <tr>
</td> <td>
</tr> <img src="{{ acceptrequest.main_char|character_portrait_url:32 }}" class="rounded-circle" style="margin-right: 1rem;" alt="{{ acceptrequest.main_char.character_name }}">
{% endfor %} {% if acceptrequest.main_char %}
</tbody> <a href="{{ acceptrequest.main_char|evewho_character_url }}" target="_blank">
</table> {{ acceptrequest.main_char.character_name }}
</div> </a>
{% else %} {% else %}
<div class="alert alert-warning text-center">{% translate "No group add requests." %}</div> {{ acceptrequest.user.username }}
{% endif %} {% endif %}
</div> </td>
<td>
{% if acceptrequest.main_char %}
<a href="{{ acceptrequest.main_char|dotlan_corporation_url }}" target="_blank">
{{ acceptrequest.main_char.corporation_name }}
</a><br>
{{ acceptrequest.main_char.alliance_name|default_if_none:"" }}
{% else %}
{% translate "(unknown)" %}
{% endif %}
</td>
<td>{{ acceptrequest.group.name }}</td>
<td class="text-end">
<a href="{% url 'groupmanagement:accept_request' acceptrequest.id %}" class="btn btn-success">
{% translate "Accept" %}
</a>
{% if not auto_leave %} <a href="{% url 'groupmanagement:reject_request' acceptrequest.id %}" class="btn btn-danger">
<div id="leave" class="tab-pane"> {% translate "Reject" %}
{% if leaverequests %} </a>
<div class="table-responsive"> </td>
<table class="table table-aa"> </tr>
<thead> {% endfor %}
<tr> </tbody>
<th>{% translate "Character" %}</th> </table>
<th>{% translate "Organization" %}</th>
<th>{% translate "Group" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for leaverequest in leaverequests %}
<tr>
<td>
<img src="{{ leaverequest.main_char|character_portrait_url:32 }}" class="img-circle" style="margin-right: 1rem;" alt="{{ leaverequest.main_char.character_name }}">
{% if leaverequest.main_char %}
<a href="{{ leaverequest.main_char|evewho_character_url }}" target="_blank">
{{ leaverequest.main_char.character_name }}
</a>
{% else %}
{{ leaverequest.user.username }}
{% endif %}
</td>
<td>
{% if leaverequest.main_char %}
<a href="{{ leaverequest.main_char|dotlan_corporation_url }}" target="_blank">
{{ leaverequest.main_char.corporation_name }}
</a><br>
{{ leaverequest.main_char.alliance_name|default_if_none:"" }}
{% else %}
{% translate "(unknown)" %}
{% endif %}
</td>
<td>{{ leaverequest.group.name }}</td>
<td class="text-right">
<a href="{% url 'groupmanagement:leave_accept_request' leaverequest.id %}" class="btn btn-success">
{% translate "Accept" %}
</a>
<a href="{% url 'groupmanagement:leave_reject_request' leaverequest.id %}" class="btn btn-danger">
{% translate "Reject" %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-warning text-center">{% translate "No group leave requests." %}</div>
{% endif %}
</div>
{% endif %}
</div>
</div> </div>
</div> {% else %}
<div class="alert alert-warning text-center">{% translate "No group add requests." %}</div>
{% endif %}
</div> </div>
{% if not auto_leave %}
<div id="leave" class="tab-pane">
{% if leaverequests %}
<div class="table-responsive">
<table class="table table-aa">
<thead>
<tr>
<th>{% translate "Character" %}</th>
<th>{% translate "Organization" %}</th>
<th>{% translate "Group" %}</th>
<th></th>
</tr>
</thead>
<tbody class="align-middle">
{% for leaverequest in leaverequests %}
<tr>
<td>
<img src="{{ leaverequest.main_char|character_portrait_url:32 }}" class="rounded-circle" style="margin-right: 1rem;" alt="{{ leaverequest.main_char.character_name }}">
{% if leaverequest.main_char %}
<a href="{{ leaverequest.main_char|evewho_character_url }}" target="_blank">
{{ leaverequest.main_char.character_name }}
</a>
{% else %}
{{ leaverequest.user.username }}
{% endif %}
</td>
<td>
{% if leaverequest.main_char %}
<a href="{{ leaverequest.main_char|dotlan_corporation_url }}" target="_blank">
{{ leaverequest.main_char.corporation_name }}
</a><br>
{{ leaverequest.main_char.alliance_name|default_if_none:"" }}
{% else %}
{% translate "(unknown)" %}
{% endif %}
</td>
<td>{{ leaverequest.group.name }}</td>
<td class="text-end">
<a href="{% url 'groupmanagement:leave_accept_request' leaverequest.id %}" class="btn btn-success">
{% translate "Accept" %}
</a>
<a href="{% url 'groupmanagement:leave_reject_request' leaverequest.id %}" class="btn btn-danger">
{% translate "Reject" %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-warning text-center">{% translate "No group leave requests." %}</div>
{% endif %}
</div>
{% endif %}
</div>
{% endblock content %} {% endblock content %}

View File

@@ -1,27 +1,10 @@
{% load i18n %} {% load i18n %}
{% load navactive %} {% load navactive %}
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">{% translate "Toggle navigation" %}</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{% url 'groupmanagement:management' %}">{% translate "Group Management" %}</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <li class="nav-item ">
<ul class="nav navbar-nav"> <a class="nav-link {% navactive request 'groupmanagement:management' %}" href="{% url 'groupmanagement:management' %}">{% translate "Group Requests" %}</a>
<li class="{% navactive request 'groupmanagement:management' %}"> </li>
<a href="{% url 'groupmanagement:management' %}">{% translate "Group Requests" %}</a> <li class="nav-item ">
</li> <a class="nav-link {% navactive request 'groupmanagement:membership groupmanagement:audit_log' %}" href="{% url 'groupmanagement:membership' %}">{% translate "Group Membership" %}</a>
<li class="{% navactive request 'groupmanagement:membership groupmanagement:audit_log' %}"> </li>
<a href="{% url 'groupmanagement:membership' %}">{% translate "Group Membership" %}</a>
</li>
</ul>
</div>
</div>
</nav>

View File

@@ -64,7 +64,7 @@ class TestViews(TestCase):
content = response_content_to_str(response) content = response_content_to_str(response)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertIn('<a data-toggle="tab" href="#leave">', content) self.assertIn('id="leave-tab" data-bs-toggle="tab" data-bs-target="#leave"', content)
self.assertIn('<div id="leave" class="tab-pane">', content) self.assertIn('<div id="leave" class="tab-pane">', content)
@override_settings(GROUPMANAGEMENT_AUTO_LEAVE=True) @override_settings(GROUPMANAGEMENT_AUTO_LEAVE=True)
@@ -81,5 +81,5 @@ class TestViews(TestCase):
content = response_content_to_str(response) content = response_content_to_str(response)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertNotIn('<a data-toggle="tab" href="#leave">', content) self.assertNotIn('id="leave-tab" data-bs-toggle="tab" data-bs-target="#leave"', content)
self.assertNotIn('<div id="leave" class="tab-pane">', content) self.assertNotIn('<div id="leave" class="tab-pane">', content)

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

@@ -87,7 +87,7 @@ def group_membership_audit(request, group_id):
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise Http404("Group does not exist") raise Http404("Group does not exist")
render_items = {'group': group.name} render_items = {'group': group}
entries = RequestLog.objects.filter(group=group).order_by('-date') entries = RequestLog.objects.filter(group=group).order_by('-date')
render_items['entries'] = entries render_items['entries'] = entries
@@ -311,8 +311,10 @@ def groups_view(request):
groups_qs = GroupManager.get_joinable_groups_for_user( groups_qs = GroupManager.get_joinable_groups_for_user(
request.user, include_hidden=False request.user, include_hidden=False
) )
groups_qs = groups_qs.order_by('name') groups_qs = groups_qs.order_by('name').select_related("authgroup").prefetch_related('authgroup__group_leaders', 'authgroup__group_leaders__profile__main_character', 'authgroup__group_leader_groups')
groups = [] groups = []
## TODO see about making this faster
for group in groups_qs: for group in groups_qs:
group_request = GroupRequest.objects\ group_request = GroupRequest.objects\
.filter(user=request.user)\ .filter(user=request.user)\
@@ -322,7 +324,14 @@ def groups_view(request):
'request': group_request[0] if group_request else None 'request': group_request[0] if group_request else None
}) })
context = {'groups': groups} count = 0
perms = GroupManager.can_manage_groups(request.user)
if perms:
count = GroupManager.pending_requests_count_for_user(request.user)
user_groups_list = list(request.user.groups.all())
context = {'groups': groups, "manager_perms": perms, "req_count":count, "user_groups": user_groups_list}
return render(request, 'groupmanagement/groups.html', context=context) return render(request, 'groupmanagement/groups.html', context=context)

View File

@@ -91,7 +91,7 @@ def get_app_modules():
def get_app_submodules(module_name): def get_app_submodules(module_name):
""" """pyt
Get a specific sub module of the app Get a specific sub module of the app
:param module_name: module name to get :param module_name: module name to get
:return: name, module tuple :return: name, module tuple
@@ -122,3 +122,17 @@ def get_hooks(name):
""" """
register_all_hooks() register_all_hooks()
return _hooks.get(name, []) return _hooks.get(name, [])
class DashboardItemHook:
def __init__(self, view_function, order:int=10):
self.view_function = view_function
self.order = order
def render(self, request):
try:
logger.debug(f"Rendering {self.view_function} to dashboard")
return self.view_function(request)
except Exception as e:
logger.exception(f"Rendering {self.view_function} failed!")
return ""

2
allianceauth/hrapplications/admin.py Executable file → Normal file
View File

@@ -10,6 +10,7 @@ class ChoiceInline(admin.TabularInline):
verbose_name_plural = 'Choices (optional)' verbose_name_plural = 'Choices (optional)'
verbose_name= 'Choice' verbose_name= 'Choice'
@admin.register(ApplicationQuestion)
class QuestionAdmin(admin.ModelAdmin): class QuestionAdmin(admin.ModelAdmin):
fieldsets = [ fieldsets = [
(None, {'fields': ['title', 'help_text', 'multi_select']}), (None, {'fields': ['title', 'help_text', 'multi_select']}),
@@ -18,6 +19,5 @@ class QuestionAdmin(admin.ModelAdmin):
admin.site.register(Application) admin.site.register(Application)
admin.site.register(ApplicationComment) admin.site.register(ApplicationComment)
admin.site.register(ApplicationQuestion, QuestionAdmin)
admin.site.register(ApplicationForm) admin.site.register(ApplicationForm)
admin.site.register(ApplicationResponse) admin.site.register(ApplicationResponse)

View File

@@ -1,7 +1,8 @@
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from allianceauth import hooks from allianceauth import hooks
from allianceauth.services.hooks import MenuItemHook, UrlHook from allianceauth.menu.hooks import MenuItemHook
from allianceauth.services.hooks import UrlHook
from . import urls from . import urls
from .models import Application from .models import Application

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

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

View File

@@ -1,4 +1,4 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base-bs5.html" %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Choose a Corp" %}{% endblock page_title %} {% block page_title %}{% translate "Choose a Corp" %}{% endblock page_title %}

View File

@@ -1,4 +1,4 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base-bs5.html" %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Apply To" %} {{ corp.corporation_name }}{% endblock page_title %} {% block page_title %}{% translate "Apply To" %} {{ corp.corporation_name }}{% endblock page_title %}

View File

@@ -8,7 +8,7 @@
{% block content %} {% block content %}
<div class="col-lg-12"> <div class="col-lg-12">
<h1 class="page-header text-center">{% translate "Personal Applications" %} <h1 class="page-header text-center">{% translate "Personal Applications" %}
<div class="text-right"> <div class="text-end">
{% if create %} {% if create %}
<a href="{% url 'hrapplications:create_view' %}"> <a href="{% url 'hrapplications:create_view' %}">
<button type="button" class="btn btn-success">{% translate "Create Application" %}</button> <button type="button" class="btn btn-success">{% translate "Create Application" %}</button>
@@ -33,11 +33,11 @@
<td class="text-center">{{ personal_app.form.corp.corporation_name }}</td> <td class="text-center">{{ personal_app.form.corp.corporation_name }}</td>
<td class="text-center"> <td class="text-center">
{% if personal_app.approved == None %} {% if personal_app.approved == None %}
<div class="label label-warning">{% translate "Pending" %}</div> <div class="badge bg-warning">{% translate "Pending" %}</div>
{% elif personal_app.approved == True %} {% elif personal_app.approved == True %}
<div class="label label-success">{% translate "Approved" %}</div> <div class="badge bg-success">{% translate "Approved" %}</div>
{% else %} {% else %}
<div class="label label-danger">{% translate "Rejected" %}</div> <div class="badge bg-danger">{% translate "Rejected" %}</div>
{% endif %} {% endif %}
</td> </td>
<td class="text-center"> <td class="text-center">
@@ -58,7 +58,7 @@
{% endif %} {% endif %}
{% if perms.auth.human_resources %} {% if perms.auth.human_resources %}
<h1 class="page-header text-center">{% translate "Application Management" %} <h1 class="page-header text-center">{% translate "Application Management" %}
<div class="text-right"> <div class="text-end">
<!-- Button trigger modal --> <!-- Button trigger modal -->
<button type="button" class="btn btn-primary btn-sm" data-toggle="modal" data-target="#myModal"> <button type="button" class="btn btn-primary btn-sm" data-toggle="modal" data-target="#myModal">
{% translate "Search Applications" %} {% translate "Search Applications" %}
@@ -91,14 +91,14 @@
<td class="text-center"> <td class="text-center">
{% if app.approved == None %} {% if app.approved == None %}
{% if app.reviewer_str %} {% if app.reviewer_str %}
<div class="label label-info">{% translate "Reviewer:" %} {{ app.reviewer_str }}</div> <div class="badge bg-info">{% translate "Reviewer:" %} {{ app.reviewer_str }}</div>
{% else %} {% else %}
<div class="label label-warning">{% translate "Pending" %}</div> <div class="badge bg-warning">{% translate "Pending" %}</div>
{% endif %} {% endif %}
{% elif app.approved == True %} {% elif app.approved == True %}
<div class="label label-success">{% translate "Approved" %}</div> <div class="badge bg-success">{% translate "Approved" %}</div>
{% else %} {% else %}
<div class="label label-danger">{% translate "Rejected" %}</div> <div class="badge bg-danger">{% translate "Rejected" %}</div>
{% endif %} {% endif %}
</td> </td>
<td class="text-center"> <td class="text-center">
@@ -135,14 +135,14 @@
<td class="text-center"> <td class="text-center">
{% if app.approved == None %} {% if app.approved == None %}
{% if app.reviewer_str %} {% if app.reviewer_str %}
<div class="label label-info">{% translate "Reviewer:" %} {{ app.reviewer_str }}</div> <div class="badge bg-info">{% translate "Reviewer:" %} {{ app.reviewer_str }}</div>
{% else %} {% else %}
<div class="label label-warning">{% translate "Pending" %}</div> <div class="badge bg-warning">{% translate "Pending" %}</div>
{% endif %} {% endif %}
{% elif app.approved == True %} {% elif app.approved == True %}
<div class="label label-success">{% translate "Approved" %}</div> <div class="badge bg-success">{% translate "Approved" %}</div>
{% else %} {% else %}
<div class="label label-danger">{% translate "Rejected" %}</div> <div class="badge bg-danger">{% translate "Rejected" %}</div>
{% endif %} {% endif %}
</td> </td>
<td class="text-center"> <td class="text-center">

View File

@@ -9,7 +9,7 @@
<div class="col-lg-12"> <div class="col-lg-12">
{% if perms.auth.human_resources %} {% if perms.auth.human_resources %}
<h1 class="page-header text-center">{% translate "Application Search Results" %} <h1 class="page-header text-center">{% translate "Application Search Results" %}
<div class="text-right"> <div class="text-end">
<!-- Button trigger modal --> <!-- Button trigger modal -->
<button type="button" class="btn btn-primary btn-sm" data-toggle="modal" data-target="#myModal"> <button type="button" class="btn btn-primary btn-sm" data-toggle="modal" data-target="#myModal">
{% translate "Search Applications" %} {% translate "Search Applications" %}
@@ -34,11 +34,11 @@
<td class="text-center">{{ app.form.corp }}</td> <td class="text-center">{{ app.form.corp }}</td>
<td class="text-center"> <td class="text-center">
{% if app.approved == None %} {% if app.approved == None %}
<div class="label label-warning">{% translate "Pending" %}</div> <div class="badge bg-warning">{% translate "Pending" %}</div>
{% elif app.approved == True %} {% elif app.approved == True %}
<div class="label label-success">{% translate "Approved" %}</div> <div class="badge bg-success">{% translate "Approved" %}</div>
{% else %} {% else %}
<div class="label label-danger">{% translate "Rejected" %}</div> <div class="badge bg-danger">{% translate "Rejected" %}</div>
{% endif %} {% endif %}
</td> </td>
<td class="text-center"> <td class="text-center">

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

@@ -57,7 +57,7 @@ def hr_application_create_view(request, form_id=None):
app_form = get_object_or_404(ApplicationForm, id=form_id) app_form = get_object_or_404(ApplicationForm, id=form_id)
if request.method == "POST": if request.method == "POST":
if Application.objects.filter(user=request.user).filter(form=app_form).exists(): if Application.objects.filter(user=request.user).filter(form=app_form).exists():
logger.warn(f"User {request.user} attempting to duplicate application to {app_form.corp}") logger.warning(f"User {request.user} attempting to duplicate application to {app_form.corp}")
else: else:
application = Application(user=request.user, form=app_form) application = Application(user=request.user, form=app_form)
application.save() application.save()
@@ -92,7 +92,7 @@ def hr_application_personal_view(request, app_id):
} }
return render(request, 'hrapplications/view.html', context=context) return render(request, 'hrapplications/view.html', context=context)
else: else:
logger.warn(f"User {request.user} not authorized to view {app}") logger.warning(f"User {request.user} not authorized to view {app}")
return redirect('hrapplications:personal_view') return redirect('hrapplications:personal_view')
@@ -105,9 +105,9 @@ def hr_application_personal_removal(request, app_id):
logger.info(f"User {request.user} deleting {app}") logger.info(f"User {request.user} deleting {app}")
app.delete() app.delete()
else: else:
logger.warn(f"User {request.user} attempting to delete reviewed app {app}") logger.warning(f"User {request.user} attempting to delete reviewed app {app}")
else: else:
logger.warn(f"User {request.user} not authorized to delete {app}") logger.warning(f"User {request.user} not authorized to delete {app}")
return redirect('hrapplications:index') return redirect('hrapplications:index')
@@ -132,7 +132,7 @@ def hr_application_view(request, app_id):
logger.info(f"Saved comment by user {request.user} to {app}") logger.info(f"Saved comment by user {request.user} to {app}")
return redirect('hrapplications:view', app_id) return redirect('hrapplications:view', app_id)
else: else:
logger.warn("User %s does not have permission to add ApplicationComments" % request.user) logger.warning("User %s does not have permission to add ApplicationComments" % request.user)
return redirect('hrapplications:view', app_id) return redirect('hrapplications:view', app_id)
else: else:
logger.debug("Returning blank HRApplication comment form.") logger.debug("Returning blank HRApplication comment form.")
@@ -171,7 +171,7 @@ def hr_application_approve(request, app_id):
app.save() app.save()
notify(app.user, "Application Accepted", message="Your application to %s has been approved." % app.form.corp, level="success") notify(app.user, "Application Accepted", message="Your application to %s has been approved." % app.form.corp, level="success")
else: else:
logger.warn(f"User {request.user} not authorized to approve {app}") logger.warning(f"User {request.user} not authorized to approve {app}")
return redirect('hrapplications:index') return redirect('hrapplications:index')
@@ -187,7 +187,7 @@ def hr_application_reject(request, app_id):
app.save() app.save()
notify(app.user, "Application Rejected", message="Your application to %s has been rejected." % app.form.corp, level="danger") notify(app.user, "Application Rejected", message="Your application to %s has been rejected." % app.form.corp, level="danger")
else: else:
logger.warn(f"User {request.user} not authorized to reject {app}") logger.warning(f"User {request.user} not authorized to reject {app}")
return redirect('hrapplications:index') return redirect('hrapplications:index')
@@ -208,7 +208,7 @@ def hr_application_search(request):
app_list = app_list.filter( app_list = app_list.filter(
form__corp__corporation_id=request.user.profile.main_character.corporation_id) form__corp__corporation_id=request.user.profile.main_character.corporation_id)
except AttributeError: except AttributeError:
logger.warn( logger.warning(
"User %s missing main character model: unable to filter applications to search" % request.user) "User %s missing main character model: unable to filter applications to search" % request.user)
applications = app_list.filter( applications = app_list.filter(
@@ -246,6 +246,6 @@ def hr_application_mark_in_progress(request, app_id):
app.save() app.save()
notify(app.user, "Application In Progress", message=f"Your application to {app.form.corp} is being reviewed by {app.reviewer_str}") notify(app.user, "Application In Progress", message=f"Your application to {app.form.corp} is being reviewed by {app.reviewer_str}")
else: else:
logger.warn( logger.warning(
f"User {request.user} unable to mark {app} in progress: already being reviewed by {app.reviewer}") f"User {request.user} unable to mark {app} in progress: already being reviewed by {app.reviewer}")
return redirect("hrapplications:view", app_id) return redirect("hrapplications:view", app_id)

View File

View File

@@ -0,0 +1,9 @@
from django.contrib import admin
from . import models
@admin.register(models.MenuItem)
class MenuItemAdmin(admin.ModelAdmin):
list_display = ['text', 'hide', 'parent', 'url', 'icon_classes', 'rank']
ordering = ('rank',)

19
allianceauth/menu/apps.py Normal file
View File

@@ -0,0 +1,19 @@
import logging
from django.apps import AppConfig
from django.db.utils import ProgrammingError, OperationalError
logger = logging.getLogger(__name__)
class MenuConfig(AppConfig):
name = "allianceauth.menu"
label = "menu"
def ready(self):
try:
logger.debug("Syncing MenuItem Hooks")
from allianceauth.menu.providers import MenuItem
MenuItem.sync_hook_models()
except (ProgrammingError, OperationalError):
logger.warning("Migrations not completed for MenuItems")

View File

@@ -0,0 +1,42 @@
from django.template.loader import render_to_string
from typing import List, Optional
class MenuItemHook:
"""
Auth Hook for generating Side Menu Items
"""
def __init__(self, text: str, classes: str, url_name: str, order: Optional[int] = None, navactive: List = []):
"""
:param text: The text shown as menu item, e.g. usually the name of the app.
:type text: str
:param classes: The classes that should be applied to the menu item icon
:type classes: List[str]
:param url_name: The name of the Django URL to use
:type url_name: str
:param order: An integer which specifies the order of the menu item, lowest to highest. Community apps are free to use any order above `1000`. Numbers below are served for Auth.
:type order: Optional[int], optional
:param navactive: A list of views or namespaces the link should be highlighted on. See [django-navhelper](https://github.com/geelweb/django-navhelper#navactive) for usage. Defaults to the supplied `url_name`.
:type navactive: List, optional
"""
self.text = text
self.classes = classes
self.url_name = url_name
self.template = 'public/menuitem.html'
self.order = order if order is not None else 9999
# count is an integer shown next to the menu item as badge when count != None
# apps need to set the count in their child class, e.g. in render() method
self.count = None
navactive = navactive or []
navactive.append(url_name)
self.navactive = navactive
def render(self, request):
return render_to_string(self.template,
{'item': self},
request=request)

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