Compare commits

...

402 Commits

Author SHA1 Message Date
Ariel Rin
90f6777a7a Version Bump 2.11.1 2022-03-20 14:42:39 +10:00
Ariel Rin
a8d890abaf Merge branch 'improve_task_statistics' into 'master'
Improve task statistics

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

See merge request allianceauth/allianceauth!1406
2022-03-09 10:02:01 +00:00
Kevin McKernan
5d6128e9ea remove collectstatic command from dockerfile 2022-03-01 13:23:49 -07:00
Ariel Rin
131cc5ed0a Version Bump 2.11.0 2022-02-26 17:26:55 +10:00
Ariel Rin
9297bed43f Version Bump 2.10.2 2022-02-26 16:37:20 +10:00
Ariel Rin
b2fddc683a Merge branch 'master' of https://gitlab.com/allianceauth/allianceauth into v2.10.x 2022-02-26 16:32:45 +10:00
Ariel Rin
9af634d16a Merge branch 'fix_show_available_groups_for_user_only' into 'master'
Fix: Users can be assigned to groups depite not matching state restrictions

See merge request allianceauth/allianceauth!1402
2022-02-26 05:19:45 +00:00
Erik Kalkoken
a68163caa3 Fix: Users can be assigned to groups depite not matching state restrictions 2022-02-26 05:19:45 +00:00
Ariel Rin
00770fd034 Merge branch 'improve_celery_info_on_dashboard' into 'master'
Improve celery infos on Dashboard

See merge request allianceauth/allianceauth!1384
2022-02-26 05:15:30 +00:00
Erik Kalkoken
01164777ed Improve celery infos on Dashboard 2022-02-26 05:15:30 +00:00
Ariel Rin
00f5e3e1e0 Version Bump 2.10.1 2022-02-21 00:02:12 +10:00
Ariel Rin
8b2527f408 Merge branch 'capsleekxmpp' into 'master'
Cap sleekxmpp to 1.3.2

See merge request allianceauth/allianceauth!1401
2022-02-20 13:44:27 +00:00
Ariel Rin
b7500e4e4e Cap sleekxmpp to 1.3.2 2022-02-20 13:44:27 +00:00
Kevin McKernan
4f4bd0c419 add note to docker README about Apple M1 support 2022-02-20 23:41:12 +10:00
Ariel Rin
8ae4e02012 Merge branch 'docker-bump-version' into 'v2.10.x'
Bump version for Docker deployment to v2.10.x.

See merge request allianceauth/allianceauth!1396
2022-02-02 13:26:33 +00:00
Weyland
cc9a07197d Bump version for Docker deployment to v2.10.x. 2022-02-02 13:30:05 +01:00
Ariel Rin
f18dd1029b Version Bump v2.10.0 2022-01-31 20:58:09 +10:00
Ariel Rin
fd8d43571a Merge branch 'analytics' into 'master'
Analytics - Extra Ignore Path

See merge request allianceauth/allianceauth!1347
2022-01-31 09:23:43 +00:00
Ariel Rin
13e88492f1 Analytics - Extra Ignore Path 2022-01-31 09:23:43 +00:00
Ariel Rin
38df580a56 Merge branch 'analytics_update' into 'master'
Add setting to disable analytics

See merge request allianceauth/allianceauth!1373
2022-01-27 05:14:12 +00:00
Erik Kalkoken
ba39318313 Add setting to disable analytics 2022-01-27 05:14:11 +00:00
Ariel Rin
d8c6035405 Merge branch 'ts3_reserved_groups' into 'master'
Implement reserved group names in Teamspeak3 service module.

See merge request allianceauth/allianceauth!1380
2022-01-27 05:10:22 +00:00
Ariel Rin
2ef3da916b Merge branch 'datatablessavestate' into 'master'
Add DataTables stateSave feature

See merge request allianceauth/allianceauth!1374
2022-01-27 05:05:37 +00:00
Ariel Rin
d32d8b26ce Merge branch 'delete_characters' into 'master'
Fix: Can not update biomassed characters

See merge request allianceauth/allianceauth!1381
2022-01-27 05:02:57 +00:00
Erik Kalkoken
f348b1a34c Fix: Can not update biomassed characters 2022-01-27 05:02:57 +00:00
Ariel Rin
86aaa3edda Merge branch 'fix-grafana-image-2' into 'master'
fix grafana image again, thanks grafana for not tagging your new images properly

See merge request allianceauth/allianceauth!1393
2022-01-27 04:57:40 +00:00
Ariel Rin
26017056c7 Merge branch 'evetime-js-update' into 'master'
Evetime js update

See merge request allianceauth/allianceauth!1395
2022-01-27 04:35:15 +00:00
Peter Pfeufer
e39a3c072b Evetime js update 2022-01-27 04:35:15 +00:00
Kevin McKernan
827291dda4 fix grafana image again, thanks grafana for not tagging your new images properly 2022-01-07 10:48:50 -07:00
Ariel Rin
ea8958ccc3 Version Bump v2.9.4 2021-12-28 21:56:46 +10:00
Ariel Rin
20554df857 Merge branch 'jqueryui' into 'master'
Missing jQuery-UI Images

See merge request allianceauth/allianceauth!1391
2021-12-28 11:55:37 +00:00
Ariel Rin
750f43eaf0 Missing jQuery-UI Images 2021-12-28 11:55:37 +00:00
Ariel Rin
09cf28ec9f Merge branch 'jqueryui' into 'master'
Removing Themeing from jQuery UI

See merge request allianceauth/allianceauth!1386
2021-12-28 11:30:05 +00:00
Ariel Rin
b61746b3cb Removing Themeing from jQuery UI 2021-12-28 11:30:05 +00:00
Ariel Rin
22c22fafeb Merge branch 'py3.11' into 'master'
Python 3.11 Testing

See merge request allianceauth/allianceauth!1388
2021-12-28 11:29:33 +00:00
Ariel Rin
577c4395c4 Python 3.11 Testing 2021-12-28 11:29:33 +00:00
Ariel Rin
d241f476f7 Merge branch 'fix-grafana-image' into 'master'
update grafana image

See merge request allianceauth/allianceauth!1389
2021-12-28 10:43:43 +00:00
Kevin McKernan
5832ed0c30 update grafana image 2021-12-28 10:43:43 +00:00
Ariel Rin
bd9ea225be Rogue comment annoying MRs 2021-12-24 05:02:46 +00:00
Ariel Rin
4a575dd70c Merge branch 'groupmanagement-auto-leave-improvements' into 'master'
Hide "Leave Requests" tab when GROUPMANAGEMENT_AUTO_LEAVE = True

See merge request allianceauth/allianceauth!1378
2021-12-24 04:54:55 +00:00
Ariel Rin
b80ee16a7c Merge branch 'fix_max_notifications_warning' into 'master'
Fix: NOTIFICATIONS_MAX_PER_USER warning when not set

See merge request allianceauth/allianceauth!1383
2021-12-24 04:53:40 +00:00
Ariel Rin
c888371e6c Merge branch 'fix_esi_spec' into 'master'
Add missing ESI operation to eveonline swagger spec file.

See merge request allianceauth/allianceauth!1382
2021-12-24 04:51:49 +00:00
Adarnof
8de2c3bfcb Update name of serverquery IP file changed in TS3 v3.13.0
Changelog indicates old filenames are still accepted, but newly installed servers come with the new file names.
Closes #1298
2021-12-16 22:23:15 -05:00
Adarnof
6688f73565 Use integer teamspeak group IDs when filtering. 2021-12-15 23:54:53 -05:00
ErikKalkoken
7d929cb6e2 Fix: NOTIFICATIONS_MAX_PER_USER warning when not set 2021-12-09 18:12:18 +01:00
Adarnof
72740b9e4d Prevent assignment of reserved groups to AuthTSgroup mappings.
Implemented in TS group updates to prevent their creation / delete once
reserved, and the admin site for when a reserved group name is created
but before the TS group sync occurs.
2021-12-08 23:41:10 -05:00
Adarnof
f7d279fa16 Add missing ESI operation to minimized spec file. My bad. 2021-12-07 23:42:02 -05:00
Ariel Rin
ff7c9c48f3 Merge branch 'v2.9.x' of https://gitlab.com/allianceauth/allianceauth 2021-12-02 02:26:33 +10:00
Adarnof
d11832913d Implement reserved group names in Teamspeak3 service module.
Closes #1302
2021-12-01 00:50:29 -05:00
Ariel Rin
724e0e83f2 Merge branch 'fix-docker-script' into 'v2.9.x'
fix download script

See merge request allianceauth/allianceauth!1379
2021-11-30 11:44:35 +00:00
Kevin McKernan
333f091f1a fix download script 2021-11-29 13:43:32 -07:00
Peter Pfeufer
cfbb0b993a Behavior added to documentation 2021-11-28 19:07:14 +01:00
Peter Pfeufer
582b6754a4 Hide "Leave Requests" tab when GROUPMANAGEMENT_AUTO_LEAVE = True 2021-11-28 18:12:02 +01:00
Ariel Rin
7767c46bf4 Version Bump v2.9.3 2021-11-28 15:37:17 +00:00
Ariel Rin
bf34cef896 Merge branch 'transifex' into 'v2.9.x'
Update from Transifex

See merge request allianceauth/allianceauth!1377
2021-11-28 15:36:14 +00:00
Ariel Rin
c085ec6860 Update from Transifex 2021-11-29 01:34:13 +10:00
Ariel Rin
5f5d0316b2 Merge branch 'transifex' into 'v2.9.x'
Update from Transifex

See merge request allianceauth/allianceauth!1376
2021-11-28 15:25:50 +00:00
Ariel Rin
f9ec64c3ad Update from Transifex 2021-11-28 15:25:50 +00:00
Ariel Rin
0dfd0ad4b0 Django support for Python 3.10 was added to django 3.2.9, missed this in MR !1367 2021-11-28 15:17:53 +00:00
Ariel Rin
e88e11b9ba Merge branch 'jqueryui' into 'v2.9.x'
Add jqueryui Bundles

See merge request allianceauth/allianceauth!1372
2021-11-28 14:50:22 +00:00
Ariel Rin
7a2a79ca7b Merge branch 'groups_blacklist' into 'v2.9.x'
Add blacklist for groups and ignore blacklisted roles in Discord service

See merge request allianceauth/allianceauth!1371
2021-11-28 14:48:49 +00:00
Erik Kalkoken
4c0683c484 Add blacklist for groups and ignore blacklisted roles in Discord service 2021-11-28 14:48:49 +00:00
Ariel Rin
dfe62db8ee add datatables savestate feature 2021-11-27 23:02:33 +10:00
Ariel Rin
025c824fbb Merge branch 'build-docker-image' into 'v2.9.x'
Build docker image in gitlab, add docs for turn key docker setup

See merge request allianceauth/allianceauth!1356
2021-11-27 04:45:21 +00:00
Kevin McKernan
930c5d7c7a Build docker image in gitlab, add docs for turn key docker setup 2021-11-27 04:45:21 +00:00
Ariel Rin
8b8dcc0127 Merge branch 'group-auto-leave-enhancements' into 'v2.9.x'
Group auto leave enhancements

See merge request allianceauth/allianceauth!1369
2021-11-27 03:45:31 +00:00
Peter Pfeufer
4ad8e88bd8 Old reference to AUTO_LEAVE removed 2021-11-27 04:40:23 +01:00
Ariel Rin
89d4640e92 Merge branch 'py310' into 'v2.9.x'
Python 3.10 Support

See merge request allianceauth/allianceauth!1367
2021-11-27 03:35:40 +00:00
Ariel Rin
60b12bad61 Python 3.10 Support 2021-11-27 03:35:40 +00:00
Ariel Rin
2595fa5c51 Merge branch 'gitlab-api-request-error-handling' into 'v2.9.x'
Added error handling when fetching from GitLab API

See merge request allianceauth/allianceauth!1370
2021-11-27 03:31:39 +00:00
Ariel Rin
3e487e5f13 Add jqueryui bundles 2021-11-24 17:56:15 +10:00
Peter Pfeufer
b6d6c68e54 Fix tests
Thanks to @ErikKalkoken for helping here
2021-11-23 02:31:18 +01:00
Peter Pfeufer
49548d6f9f test_can_handle_connection_timeout added 2021-11-23 01:23:34 +01:00
Peter Pfeufer
8faadc23b0 Set logger to warning to not trigger a notification to admins every time 2021-11-23 01:02:08 +01:00
Peter Pfeufer
20da1ebfab request.raise_for_status() re-added 2021-11-22 18:56:33 +01:00
Peter Pfeufer
03305c72c7 Added error handling when fetching from GitLab API 2021-11-22 18:10:02 +01:00
Peter Pfeufer
a636fd1cf0 Add it to the docu 2021-11-21 18:24:04 +01:00
Peter Pfeufer
d8797a8dc6 Prefix the setting so it is clear where it belongs to 2021-11-21 18:18:05 +01:00
Ariel Rin
363e18e15d Merge branch 'more_group_notifications' into 'v2.9.x'
Add option to notify approvers about new group requests

See merge request allianceauth/allianceauth!1363
2021-11-20 01:32:20 +00:00
Erik Kalkoken
982cac8c43 Add option to notify approvers about new group requests 2021-11-20 01:32:20 +00:00
Ariel Rin
fabf64b838 Merge branch 'cherry-pick-f17ebbed' into 'v2.9.x'
Use danger and error message tags to render correctly on admin site.

Closes #1305

See merge request allianceauth/allianceauth!1366
2021-11-20 01:27:26 +00:00
Ariel Rin
e3e6ebe953 Merge branch 'improve_menu_bar' into 'v2.9.x'
Remove seconds from eve clock in menu bar

See merge request allianceauth/allianceauth!1365
2021-11-20 01:26:24 +00:00
Ariel Rin
7ad9b52546 Merge branch 'fix_tox_ini' into 'v2.9.x'
Fix tox environment config

See merge request allianceauth/allianceauth!1364
2021-11-20 01:16:13 +00:00
Adarnof
abb5090d63 Use danger and error message tags to render correctly on admin site.
Closes #1305


(cherry picked from commit f17ebbede6)
2021-11-20 01:14:07 +00:00
Ariel Rin
52ae05d057 Merge branch 'messages' into 'master'
Use danger and error tags to render messages correctly on admin site.

See merge request allianceauth/allianceauth!1362
2021-11-20 01:13:27 +00:00
ErikKalkoken
3cd216d119 Remove seconds from eve clock in menu bar 2021-11-11 21:14:48 +01:00
ErikKalkoken
581edc0a38 Fix environment config in tox.ini 2021-11-11 17:37:27 +01:00
Adarnof
f17ebbede6 Use danger and error message tags to render correctly on admin site.
Closes #1305
2021-11-03 21:02:03 -04:00
Ariel Rin
2bd2c09c23 Version Bump 2.9.2 2021-10-30 05:23:48 +00:00
Ariel Rin
c1cf859ec9 Merge branch 'srp-provider' into 'v2.9.x'
Follow up test fixes missed from !1358

See merge request allianceauth/allianceauth!1361
2021-10-30 05:22:38 +00:00
Ariel Rin
1ebf864998 adapt tests 2021-10-30 05:22:38 +00:00
Ariel Rin
0948e34e48 Merge branch 'transifex' into 'v2.9.x'
Update from Transifex

See merge request allianceauth/allianceauth!1360
2021-10-30 04:54:00 +00:00
Ariel Rin
f9a1ea9c83 Update from Transifex 2021-10-30 04:54:00 +00:00
Ariel Rin
d02a8ebc1b Merge branch 'dependencies' into 'v2.9.x'
Dependencies, CDN imports, CSS optimization

See merge request allianceauth/allianceauth!1350
2021-10-30 04:35:55 +00:00
Ariel Rin
5c2625b648 Dependencies, CDN imports, CSS optimization 2021-10-30 04:35:55 +00:00
Ariel Rin
13174d006e Merge branch 'issue/1211-feature-request-optimer-labels' into 'v2.9.x'
Add types and description to optimers

Closes #1211

See merge request allianceauth/allianceauth!1357
2021-10-30 04:34:49 +00:00
Peter Pfeufer
2c59cc4cc3 Add types and description to optimers 2021-10-30 04:34:49 +00:00
Ariel Rin
41c81d3226 Merge branch 'chunkupdateerrortype' into 'v2.9.x'
Correct logging of affiliation endpoint to Info not Error

See merge request allianceauth/allianceauth!1359
2021-10-30 04:32:12 +00:00
Ariel Rin
2ec7d3b4d9 Merge branch 'srp-provider' into 'v2.9.x'
Decouple allianceauth.srp ESI Provider from allianceauth.eveonline

See merge request allianceauth/allianceauth!1358
2021-10-30 04:29:28 +00:00
Ariel Rin
b607b73598 chunk updates to be info, not error 2021-10-30 12:43:49 +10:00
Ariel Rin
f52791cd1f document swagger spec operations 2021-10-27 18:18:23 +10:00
Ariel Rin
d829facbd4 use own esi provider for itemtypes 2021-10-27 18:14:18 +10:00
Ariel Rin
44ac3a9ff2 decouple srp provider from eveonline 2021-10-27 17:54:14 +10:00
Ariel Rin
2054a76353 Version Bump 2.9.1 2021-10-26 09:45:37 +00:00
Ariel Rin
4b073c3161 Migration Merge for !1338 !1352 2021-10-26 19:33:43 +10:00
Ariel Rin
527c016c72 Merge branch 'faction' into 'v2.9.x'
Allow assigning factions to states.

See merge request allianceauth/allianceauth!1338
2021-10-26 09:16:29 +00:00
Adarnof
23849b9f42 Allow assigning factions to states. 2021-10-26 09:16:28 +00:00
Ariel Rin
5d4277adf0 Revert "Change Permissions"
This reverts commit 2fe1de1c97.
2021-10-26 18:46:50 +10:00
Ariel Rin
0f479d525e Merge branch 'transifex' into 'v2.9.x'
Transifex

See merge request allianceauth/allianceauth!1346
2021-10-26 08:45:37 +00:00
Ariel Rin
4aae85a70f Transifex 2021-10-26 08:45:37 +00:00
Ariel Rin
f0ec16be4e Merge branch 'versions-versions-versions' into 'v2.9.x'
Fix versions shown on the dashboard

Closes #1277

See merge request allianceauth/allianceauth!1355
2021-10-26 08:24:11 +00:00
Ariel Rin
92f6d8b98d Merge branch 'issue/1200-activation-link-shown-trunctated' into 'v2.9.x'
Send proper multipart email

Closes #1200

See merge request allianceauth/allianceauth!1354
2021-10-26 08:20:58 +00:00
Ariel Rin
0a48436937 Merge branch 'issue/1287-add-shieldarmorhull-toggle-to-timer' into 'v2.9.x'
Adding timer type to the timer board

Closes #1287

See merge request allianceauth/allianceauth!1353
2021-10-26 08:19:17 +00:00
Ariel Rin
f4c5dc77d8 Merge branch 'issue-1299' into 'v2.9.x'
Extend State name to 32 chars

Closes #1299

See merge request allianceauth/allianceauth!1352
2021-10-26 08:03:28 +00:00
Peter Pfeufer
8656d1c972 Let's compare what we actually want to compare
- "major" and "minor" is not used at all, so these can be removed
- "base_version" can be removed as well, since we are comparing full version strings, not just parts of them
2021-10-25 21:54:10 +02:00
Peter Pfeufer
1b81f5cfe8 send proper multipart email 2021-10-20 17:46:51 +02:00
Peter Pfeufer
a4723c5942 timer type should always be in its own line 2021-10-20 15:34:57 +02:00
Peter Pfeufer
b31521c758 Adding timer type to the timer board
See issue #1287
2021-10-20 15:30:45 +02:00
Ariel Rin
6dc96bb348 Extend State name to 32 chars 2021-10-20 15:24:54 +10:00
Ariel Rin
2da78f7793 Merge branch 'master' into 'v2.9.x'
Merge Chunking Updates from Master

See merge request allianceauth/allianceauth!1351
2021-10-20 05:00:57 +00:00
Ariel Rin
e8d54a1d11 Merge Chunking Updates from Master 2021-10-20 05:00:57 +00:00
Ariel Rin
f79e764439 Merge branch 'issue/1308-groups-without-assigned-group-leade' into 'v2.9.x'
[FIX] Pending Count for superuser for groups without group lead assigned

Closes #1308

See merge request allianceauth/allianceauth!1349
2021-10-20 05:00:10 +00:00
Peter Pfeufer
803b659fce [FIX] Pending Count for superuser for groups without group lead assigned 2021-10-20 05:00:09 +00:00
Ariel Rin
537987398d Merge branch 'make-SITE_NAME-great-again' into 'v2.9.x'
Make SITE_NAME great again

See merge request allianceauth/allianceauth!1348
2021-10-20 04:49:54 +00:00
Peter Pfeufer
cdcf65cb52 fixed old Django 2 translation template tags 2021-10-19 12:05:45 +02:00
Peter Pfeufer
5fcb092151 use SITE_NAME constant as page title 2021-10-19 11:50:42 +02:00
Ariel Rin
1fd708e765 Merge branch 'code-update-to-python-3.7' into 'v2.9.x'
Update codebase to reflect the new minimum python version 3.7

See merge request allianceauth/allianceauth!1345
2021-10-19 00:56:39 +00:00
Ariel Rin
a19302babc Merge branch 'chunking_updates' into 'master'
Breakout Model updates into separate tasks

See merge request allianceauth/allianceauth!1343
2021-10-19 00:49:39 +00:00
Aaron Kable
18a627b01e Breakout Model updates into separate tasks 2021-10-19 00:49:39 +00:00
Peter Pfeufer
a6b340c179 update code to reflect the new minimum python version 3.7
- update string format method
- remove redundant default arguments from function  calls
- remove unused imports
- remove unicode identifier from strings, it's default in py3 (see: https://stackoverflow.com/a/4182635/12201331)
2021-10-18 11:59:05 +02:00
Joel Falknau
2fe1de1c97 Change Permissions 2021-10-18 12:06:41 +10:00
Ariel Rin
0072983b4f Merge branch 'fix-setup-classifiers' into 'v2.9.x'
Fix setup.py classifiers

See merge request allianceauth/allianceauth!1342
2021-10-18 01:59:25 +00:00
Ariel Rin
e1e87edc74 Merge branch 'add-missing-analytics-migration' into 'v2.9.x'
Adding missing analytics migration

See merge request allianceauth/allianceauth!1341
2021-10-18 01:59:15 +00:00
Ariel Rin
eddb5480e9 Merge branch 'v2.9.x' into 'master'
Bring up Master to 2.9.0

See merge request allianceauth/allianceauth!1344
2021-10-18 01:58:24 +00:00
Peter Pfeufer
f3c28ea933 No Django 3.2 in AA 2.9.x 2021-10-18 00:32:46 +02:00
Peter Pfeufer
829f8ba0a4 missing analytics migration 2021-10-17 18:26:40 +02:00
Ariel Rin
f49ca2bccb Version Bump 2.9.0 2021-10-17 10:15:20 +00:00
Ariel Rin
9c71a8d9a3 Merge branch 'transifex' into 'v2.9.x'
Update from Transifex

See merge request allianceauth/allianceauth!1319
2021-10-17 09:28:37 +00:00
Ariel Rin
0032f91525 Update from Transifex 2021-10-17 09:28:37 +00:00
Ariel Rin
7f3492f978 Merge branch 'analytics' into 'v2.9.x'
Analytics Improvements

See merge request allianceauth/allianceauth!1340
2021-10-17 09:26:11 +00:00
Ariel Rin
2b9110e417 Analytics Improvements 2021-10-17 09:26:11 +00:00
Joel Falknau
98619a0eb8 Require Django-ESI 3.x 2021-10-17 19:13:43 +10:00
Joel Falknau
7a4aa05c39 Merge commit '8486b95917a48d760bb6ba05795a40c19a2440d0' into v2.9.x 2021-10-17 19:11:09 +10:00
Ariel Rin
5b26757662 Merge branch 'features/kick-from-discord-admin' into 'master'
Add override to delete the user from discord itself

See merge request allianceauth/allianceauth!1337
2021-10-17 07:34:22 +00:00
Ariel Rin
ba597cd2ad Merge branch 'case-sensitive-filter-fix' into 'v2.9.x'
[FIX] This filter is case sensitive

See merge request allianceauth/allianceauth!1339
2021-10-17 07:28:55 +00:00
Peter Pfeufer
5992ddd48e This filter is case sensitive 2021-10-15 06:06:02 +02:00
Crashtec
8486b95917 Add override to delete the user from discord itself 2021-10-10 15:19:46 -04:00
Ariel Rin
564f4fb5f9 Merge branch 'use-django-3.2' into 'v2.9.x'
Bumped minimum Django version to 3.2.7

See merge request allianceauth/allianceauth!1336
2021-09-22 13:43:51 +00:00
Peter Pfeufer
688c11ff18 bumped min Django version to 3.2.7 2021-09-19 18:05:39 +02:00
Ariel Rin
4a9a2a670c Version Bump 2.9.0b1 2021-09-14 05:08:15 +00:00
Ariel Rin
5b25637de5 Debian Bullseye now Stable, libmariadbclient-dev name deprecated, now removed 2021-09-14 05:02:15 +00:00
Ariel Rin
bb15de6d1a Version Bump v2.8.7 2021-09-14 04:32:37 +00:00
Ariel Rin
cf2e368978 Merge branch 'docs-updates' into 'v2.9.x'
Docs Updates

See merge request allianceauth/allianceauth!1334
2021-09-14 04:21:23 +00:00
Peter Pfeufer
0d67673542 Docs Updates 2021-09-14 04:21:22 +00:00
Ariel Rin
d7e58fb557 Merge branch 'django-admin-favicons' into 'v2.9.x'
Add favicon support to django admin backend

See merge request allianceauth/allianceauth!1335
2021-09-14 04:19:33 +00:00
Peter Pfeufer
f8cffb64a1 add favicon support to django admin backend 2021-08-25 22:37:37 +02:00
Joel Falknau
dc97fb1cc5 Forgot pre-commit for master > 2.9.x 2021-08-20 14:50:44 +10:00
Joel Falknau
392a0c4dcb Merge remote-tracking branch 'upstream/master' into v2.9.x 2021-08-20 14:41:38 +10:00
Ariel Rin
970a111386 Merge branch 'analytics' into 'v2.9.x'
Analytics Enable migration typo

See merge request allianceauth/allianceauth!1331
2021-08-20 04:04:33 +00:00
Ariel Rin
cafa7cbf17 Merge branch 'top-menu-redesign' into 'v2.9.x'
Top menu redesign

See merge request allianceauth/allianceauth!1329
2021-08-20 04:04:23 +00:00
Ariel Rin
0f0c0441a9 Merge branch 'fix_logging_notifications' into 'master'
Fix logging notifications

See merge request allianceauth/allianceauth!1332
2021-08-20 04:03:48 +00:00
Erik Kalkoken
a0db8e8e2c Fix logging notifications 2021-08-20 04:03:48 +00:00
Peter Pfeufer
641356f31d css properly formatted 2021-08-18 07:40:47 +02:00
Peter Pfeufer
191b238a04 using "public/lang_select.html" 2021-08-18 07:39:36 +02:00
Joel Falknau
f6936c5f33 Typo in token enable flag 2021-08-18 11:43:05 +10:00
Ariel Rin
e8f8cb8aa3 Merge branch 'fix-for-the-fix' into 'v2.9.x'
(Hotfix) Missing import added

See merge request allianceauth/allianceauth!1330
2021-08-18 01:39:15 +00:00
Peter Pfeufer
96170a668f missing import added 2021-08-17 23:47:15 +02:00
Peter Pfeufer
4a3e807066 menu item text for mobile view 2021-08-14 14:02:25 +02:00
Peter Pfeufer
ab369d9aac [FIX] text color 2021-08-14 13:58:01 +02:00
Peter Pfeufer
742864fe6c adding eve time to top menu 2021-08-11 16:19:13 +02:00
Peter Pfeufer
c3df1c4d1f adding user menu 2021-08-11 16:18:59 +02:00
Ariel Rin
63d7fb80e6 Merge branch 'srp_evetools_killboard' into 'v2.9.x'
Add EveTools kill board as accepted source for srp request kill mails

See merge request allianceauth/allianceauth!1326
2021-08-11 13:06:19 +00:00
Ariel Rin
a7f468efd1 Merge branch 'fix_group_creation' into 'master'
Fix Group Creation

Closes #1161

See merge request allianceauth/allianceauth!1327
2021-08-11 06:07:24 +00:00
Ariel Rin
9f4ab9540b Merge branch 'improve_notifications' into 'master'
Improve notifications

See merge request allianceauth/allianceauth!1324
2021-08-11 06:06:24 +00:00
Erik Kalkoken
1e133b7c5d Improve notifications 2021-08-11 06:06:23 +00:00
Ariel Rin
4aa7530bbc Merge branch 'fix_hasgroupleader' into 'master'
Fix `Has Leader` column for groups that have group leader groups.

See merge request allianceauth/allianceauth!1328
2021-08-11 06:06:13 +00:00
colcrunch
2e0ddf2e7a has_leader should return true when a group has a group_leader_group 2021-07-13 18:00:21 -04:00
colcrunch
e24bc2a05d Revert "Update tests."
This reverts commit 7c1d1074f9.
2021-07-13 11:35:19 -04:00
colcrunch
a8c0db3fd7 Revert "Update autogroups."
This reverts commit eaa1cde01a.
2021-07-13 11:35:06 -04:00
colcrunch
7b77a6cd40 Revert "Add authutil for creating groups with authgroups."
This reverts commit 15db817382.
2021-07-13 11:34:55 -04:00
colcrunch
b8b8e470f2 Revert "Update tests to use the create_group util."
This reverts commit 0897383e41.
2021-07-13 11:34:46 -04:00
colcrunch
ad92ea243d Revert "Fix missed test."
This reverts commit 37005b1c68.
2021-07-13 11:34:35 -04:00
colcrunch
489a8456f7 Revert "More test fixes."
This reverts commit 6c3650d9f2.
2021-07-13 11:34:19 -04:00
colcrunch
122e389c38 Add signals back. 2021-07-13 11:33:51 -04:00
colcrunch
8318add6d5 Update test_admin.py 2021-07-13 10:18:39 -04:00
colcrunch
6c3650d9f2 More test fixes. 2021-07-13 09:28:31 -04:00
colcrunch
37005b1c68 Fix missed test. 2021-07-13 09:18:53 -04:00
colcrunch
0897383e41 Update tests to use the create_group util. 2021-07-13 09:13:47 -04:00
colcrunch
15db817382 Add authutil for creating groups with authgroups. 2021-07-13 09:13:17 -04:00
colcrunch
eaa1cde01a Update autogroups. 2021-07-13 08:41:36 -04:00
colcrunch
7c1d1074f9 Update tests. 2021-07-13 08:41:25 -04:00
colcrunch
0f0f9b6062 Fix group creation ignoring AuthGroup settings. (Fixes #1161) 2021-07-13 08:09:40 -04:00
Peter Pfeufer
839232dc98 add evetools killboard as accepted source for srp requests 2021-07-04 15:04:25 +02:00
Ariel Rin
356df45583 Version Bump v2.9.0a4 2021-07-03 04:44:17 +00:00
Ariel Rin
6b78992f07 Merge branch 'googleanalytics' into 'v2.9.x'
Analytics Module

See merge request allianceauth/allianceauth!1286
2021-07-03 04:43:17 +00:00
Ariel Rin
5af33facb6 Analytics Module 2021-07-03 04:43:16 +00:00
Ariel Rin
a7b9bcf3e8 Merge branch 'bump-celery-version' into 'v2.9.x'
allow celery 5 since it is pulled in by celery-beat>2.2.0

See merge request allianceauth/allianceauth!1325
2021-07-03 04:25:54 +00:00
Ariel Rin
eafc6074c1 Merge branch 'purge_state_cache' into 'v2.9.x'
Refresh user profile on state change to force permission changes.

See merge request allianceauth/allianceauth!1296
2021-07-03 04:25:34 +00:00
Peter Pfeufer
99383a482b allow celery 5 since it is pulled in by celery-beat
This commit is so that celery 5 can be tested
2021-07-02 18:56:54 +02:00
Ariel Rin
6f2807cba2 Version Bump v2.8.6 2021-07-02 16:47:06 +00:00
Ariel Rin
39a40a8c43 Limit Django-Celery-Beat to 2.2.0 for Celery 4.x 2021-07-02 16:42:07 +00:00
Ariel Rin
31123f7e97 Merge branch 'v2.9.x' of https://gitlab.com/allianceauth/allianceauth into v2.9.x 2021-06-29 14:02:00 +10:00
Ariel Rin
96300a012a Merge branch 'master' of https://gitlab.com/allianceauth/allianceauth into v2.9.x 2021-06-29 14:01:52 +10:00
Ariel Rin
5f98b5350e Version Bump v2.8.5 2021-06-29 03:27:30 +00:00
Ariel Rin
8874eceb5f Merge branch 'name-formatting' into 'v2.9.x'
Name formatter fixes

See merge request allianceauth/allianceauth!1321
2021-06-29 03:15:02 +00:00
Ariel Rin
9de4d557e3 Merge branch 'discord-existing' into 'master'
Update member when they are already in Discord server.

See merge request allianceauth/allianceauth!1322
2021-06-29 03:10:23 +00:00
Aaron Kable
1d5f2634f1 Update member when they are already in Discord server. 2021-06-29 03:10:23 +00:00
Ariel Rin
710d26d36d Merge branch 'message-icons-fix' into 'master'
[FIX] icons in alert messages

See merge request allianceauth/allianceauth!1323
2021-06-29 03:10:01 +00:00
Peter Pfeufer
47793e6198 [FIX] icons in alert messages
This way they don't interfere with possible HTML in the message itself.
2021-06-28 21:20:59 +02:00
Peter Pfeufer
3c799eb40b None is neither an allince, a corp or a character 2021-06-23 12:29:33 +02:00
Ariel Rin
0d7b8e315f tidy up missed pre-commits from master merge 2021-06-23 10:03:57 +10:00
Ariel Rin
ae7cfbff35 Merge branch 'master' of https://gitlab.com/allianceauth/allianceauth into v2.9.x 2021-06-23 09:58:25 +10:00
Ariel Rin
a0a497ab33 Merge branch 'sanity-checks-and-editorconfig' into 'v2.9.x'
Basic code sanity checks and editorconfig

See merge request allianceauth/allianceauth!1316
2021-06-22 23:38:54 +00:00
Peter Pfeufer
fd86471a0f exluded .po, .mo and swagger.json from checks
For demonstration, the last empty line has been removed from swagger.json. Usually it would have been added again through pre-commit, but not with the exclusion here.
2021-06-22 16:42:10 +02:00
Ariel Rin
5fcb56a087 Merge branch 'notification_with_icons' into 'master'
Improve messages

See merge request allianceauth/allianceauth!1320
2021-06-21 10:39:56 +00:00
Erik Kalkoken
808080d5ee Improve messages 2021-06-21 10:39:56 +00:00
Ariel Rin
e6037f1680 Typo (Allince<Alliance) Credit @Thalimet 2021-06-21 10:23:40 +00:00
Ariel Rin
5c3ded6b07 Merge branch 'admin_performance_tuning' into 'master'
Admin performance tuning for group and state

See merge request allianceauth/allianceauth!1318
2021-06-01 11:40:32 +00:00
Erik Kalkoken
0c14e35d15 Admin performance tuning for group and state 2021-06-01 11:40:32 +00:00
Ariel Rin
c13be5d39a Merge branch 'ts3_update_button' into 'master'
Add button to update TS3 groups to admin page

See merge request allianceauth/allianceauth!1313
2021-06-01 11:38:24 +00:00
Erik Kalkoken
e4b515c1d5 Add button to update TS3 groups to admin page 2021-06-01 11:38:24 +00:00
Ariel Rin
65e2c87e8f Merge branch 'improve_eveonline_manager' into 'master'
Improve performance in EveCharacter manager

See merge request allianceauth/allianceauth!1314
2021-06-01 11:36:29 +00:00
Ariel Rin
68ce25854a Merge branch 'quick-syntax-fix-in-docu' into 'master'
quick fix in documentation

See merge request allianceauth/allianceauth!1317
2021-06-01 11:35:20 +00:00
Peter Pfeufer
1f80a02be9 version updated 2021-05-17 23:27:17 +02:00
Peter Pfeufer
3df6643513 this is a shell command ... 2021-05-17 17:41:28 +02:00
Peter Pfeufer
e6a4cea4de editorconfig applied 2021-05-17 11:42:28 +02:00
Peter Pfeufer
bad36a69e8 added editorconfig 2021-05-17 09:52:29 +02:00
Peter Pfeufer
2697fb5317 remove coding pragma. it's not needed since python 3.x 2021-05-17 09:51:09 +02:00
Peter Pfeufer
b47392ba7f check for LF as line ending 2021-05-17 09:49:58 +02:00
Peter Pfeufer
a99a375375 one and only one empty line at the end of the file 2021-05-17 09:48:57 +02:00
Peter Pfeufer
8c3df89d52 remove trailing whitespaces 2021-05-17 09:46:11 +02:00
Ariel Rin
6ea0ebc9f9 Merge branch 'db-install-docs-update' into 'master'
Added a quick hint that it's ok to leave the SQL shell, and some commas

See merge request allianceauth/allianceauth!1315
2021-05-16 02:38:43 +00:00
Peter Pfeufer
26236f5886 Added a quick hint that it's ok to leave the SQL shell, and some commas 2021-05-15 15:27:50 +02:00
ErikKalkoken
1420c71ec5 Improve get_character_by_id() 2021-05-15 13:58:23 +02:00
Ariel Rin
10bd77d761 Version Bump v2.9.0a2 2021-05-13 21:43:24 +10:00
Ariel Rin
192d286cf2 Merge branch 'master' of https://gitlab.com/allianceauth/allianceauth into v2.9.x 2021-05-13 21:38:22 +10:00
Ariel Rin
8ab9d2d5ad Merge branch 'revert-to-autofield' into 'v2.9.x'
revert migrations & set DEFAULT_AUTO_FIELD to django.db.models.AutoField

See merge request allianceauth/allianceauth!1312
2021-05-10 11:53:47 +00:00
Peter Pfeufer
3630812b92 revert migrations & set DEFAULT_AUTO_FIELD to django.db.models.AutoField 2021-05-10 13:35:40 +02:00
Ariel Rin
2f320bc256 Version Bump 2.9.0a1 2021-05-09 06:45:17 +00:00
Ariel Rin
45b8d42b8e Merge branch 'javascript-modernization' into 'v2.9.x'
Cleanup and modernize JS

See merge request allianceauth/allianceauth!1305
2021-05-09 06:43:58 +00:00
Peter Pfeufer
bd2d19f867 Cleanup and modernize JS 2021-05-09 06:43:58 +00:00
Ariel Rin
0be404baca Merge branch 'py36remove' into 'v2.9.x'
Remove Python36 Testing and Support

See merge request allianceauth/allianceauth!1301
2021-05-09 06:42:56 +00:00
Ariel Rin
e6cee9ac83 Remove Python36 Testing and Support 2021-05-09 06:42:56 +00:00
Ariel Rin
5a2c9243c4 Version Bump v2.8.4 2021-05-09 05:52:20 +00:00
Ariel Rin
fecd748198 Merge branch 'mumble-version' into 'master'
Model updates for Mumble Authenticator 1.1

See merge request allianceauth/allianceauth!1303
2021-05-09 05:41:05 +00:00
Ariel Rin
85351b2c66 Model updates for Mumble Authenticator 1.1 2021-05-09 05:41:05 +00:00
Ariel Rin
8b3e5b6462 Merge branch 'improve_eveonline_admin' into 'master'
Improve admin pages for eveonline models

See merge request allianceauth/allianceauth!1306
2021-05-09 05:39:46 +00:00
Erik Kalkoken
93af920b8f Improve admin pages for eveonline models 2021-05-09 05:39:46 +00:00
Ariel Rin
b1248d9845 Merge branch 'improve_groups_admin' into 'master'
Improve group management admin pages

See merge request allianceauth/allianceauth!1308
2021-05-09 05:38:28 +00:00
Erik Kalkoken
c86abef07d Improve group management admin pages 2021-05-09 05:38:28 +00:00
Ariel Rin
17ad5dfe33 Merge branch 'improve_authentication_admin' into 'master'
Improve authentication admin

See merge request allianceauth/allianceauth!1307
2021-05-09 05:31:45 +00:00
Erik Kalkoken
e1843fe1f1 Improve authentication admin 2021-05-09 05:31:45 +00:00
Ariel Rin
256a21f058 Merge branch 'fix_table_styles_for_dark_mode' into 'master'
Fix table styles and nav tabs for dark mode

See merge request allianceauth/allianceauth!1310
2021-05-09 05:25:45 +00:00
Erik Kalkoken
1473259e41 Fix table styles and nav tabs for dark mode 2021-05-09 05:25:45 +00:00
Ariel Rin
e935b91e93 Merge branch 'improve_gitlab_error_handling' into 'master'
Improve handling of gitlab HTTP errors

See merge request allianceauth/allianceauth!1311
2021-05-09 05:17:56 +00:00
ErikKalkoken
07258a6914 Log HTTP errors from gitlab as warning instead of error 2021-05-08 13:20:57 +02:00
Ariel Rin
cb35808508 Merge branch 'improve_ci_script' into 'master'
Remove redis ping and consolidate before steps

See merge request allianceauth/allianceauth!1309
2021-05-05 09:04:43 +00:00
Erik Kalkoken
937d656227 Remove redis ping and consolidate before steps 2021-05-05 09:04:43 +00:00
Ariel Rin
d173a59441 Merge branch 'add-robots-txt' into 'v2.9.x'
Adding robots.txt to fix random 404 message

See merge request allianceauth/allianceauth!1302
2021-04-27 14:11:14 +00:00
Ariel Rin
75a3adb2c9 Merge branch 'cron-fix' into 'master'
[Docs] Fix Discord Task Schedule

See merge request allianceauth/allianceauth!1304
2021-04-27 13:53:37 +00:00
colcrunch
148e208476 Update cron schedule for discord.update_all_usernames task in docs. 2021-04-18 22:14:52 -04:00
Peter Pfeufer
8f59f2549a robots.txt added
This fixes the randomly appearing 404 error in Auth  (issue #1232)
2021-04-15 20:59:25 +02:00
Ariel Rin
630400fee4 Merge branch 'django-3-2-update' into 'v2.9.x'
Django 3.2 compatibility update

See merge request allianceauth/allianceauth!1300
2021-04-08 05:07:01 +00:00
Ariel Rin
1ec6929e91 Merge branch 'python39' into 'v2.9.x'
Add  Python3.9 Testing

See merge request allianceauth/allianceauth!1284
2021-04-08 04:49:15 +00:00
Ariel Rin
8c6bdd8ae2 Add Python3.9 Testing 2021-04-08 04:49:15 +00:00
Peter Pfeufer
e04138bced migrations 2021-04-07 18:40:39 +02:00
Peter Pfeufer
db5ad85811 add new default for PKs 2021-04-07 18:16:33 +02:00
Peter Pfeufer
5b44fd376d revert version cap on Django, so we get 3.2 again 2021-04-07 18:11:01 +02:00
Ariel Rin
0036e8b280 Version Bump v2.8.3 2021-04-07 15:18:08 +00:00
Ariel Rin
ea2b5bfecf Merge branch 'srp-killmail-link-fix-issue-1282' into 'master'
Proper error message on non kill mail zkb links

See merge request allianceauth/allianceauth!1297
2021-04-07 15:12:56 +00:00
Peter Pfeufer
aa7495fa60 Proper error message on non kill mail zkb links 2021-04-07 15:12:56 +00:00
Ariel Rin
162ec1bd86 Merge branch 'docs-permissioncleanup' into 'master'
Permissions Cleanup Docs

See merge request allianceauth/allianceauth!1293
2021-04-07 13:23:28 +00:00
Ariel Rin
2668884008 Merge branch 'srp-copypaste' into 'master'
SRP Character Name Copy-Paste

See merge request allianceauth/allianceauth!1295
2021-04-07 13:22:24 +00:00
Ariel Rin
abdc3f3485 SRP Character Name Copy-Paste 2021-04-07 13:22:23 +00:00
Ariel Rin
911f21ee7c Merge branch 'add-support-discord-link' into 'master'
support discord link added to admin notifications on dashboard

See merge request allianceauth/allianceauth!1298
2021-04-07 13:21:26 +00:00
Ariel Rin
2387c40254 Limit Django to 3.1.x 2021-04-07 13:12:15 +00:00
Peter Pfeufer
76e18a79b3 support discord link added to admin notifications on dashboard 2021-04-06 10:44:12 +02:00
Ariel Rin
9725c9c947 Update .gitlab-ci.yml file 2021-03-27 08:38:40 +00:00
Aaron Kable
b1b79d1245 Refresh user profile on state change to force permision changes. 2021-03-26 18:20:39 +08:00
Ariel Rin
247ed7cc64 Merge branch 'nootification_log_spam' into 'master'
Use default by default in Notification system

See merge request allianceauth/allianceauth!1294
2021-02-25 11:04:18 +00:00
Aaron Kable
2fd2af793e Use default by defaut in notification system 2021-02-21 18:14:42 +08:00
Ariel Rin
3fc36b9ce1 Permissions Cleanup Tool 2021-02-18 13:33:23 +10:00
Ariel Rin
c12fd2d7bc Merge branch 'improve_developer_docs' into 'master'
Improve developer docs

See merge request allianceauth/allianceauth!1292
2021-02-18 01:52:15 +00:00
ErikKalkoken
7fe1ba2fb2 Improve docs for WSL setup 2021-02-17 09:50:01 +01:00
ErikKalkoken
ab7eb3e5df Fix sphinx issues in docs 2021-02-17 09:35:18 +01:00
Ariel Rin
3452c3acd1 Version Bump v2.8.2 2021-02-05 12:19:25 +00:00
Ariel Rin
3c305fbf37 Merge branch 'wheel' into 'master'
Build wheel

See merge request allianceauth/allianceauth!1290
2021-01-26 12:59:56 +00:00
Rebecca Claire Murphy
a5e0721ec1 Build wheel 2021-01-26 12:59:56 +00:00
Ariel Rin
164a0d5376 Merge branch 'gitlab-announcements-fix' into 'master'
Only show open announcements

See merge request allianceauth/allianceauth!1291
2021-01-26 12:55:11 +00:00
Ariel Rin
2bcf6ec39a Merge branch 'master' into 'master'
Squash mumble migrations

See merge request allianceauth/allianceauth!1289
2021-01-26 12:30:35 +00:00
Peter Pfeufer
40824156bf Only show open announcements 2021-01-25 21:13:43 +01:00
Rebecca Claire Murphy
cec4495034 Merge branch 'master' of gitlab.com:allianceauth/allianceauth 2021-01-15 19:43:39 -05:00
Ariel Rin
74eb8621d9 Merge branch 'notifications_refresh' into 'master'
Notifications refresh v2 and session caching

See merge request allianceauth/allianceauth!1218
2021-01-16 00:09:49 +00:00
Erik Kalkoken
4394d25961 Notifications refresh v2 and session caching 2021-01-16 00:09:49 +00:00
Rebecca Claire Murphy
8113327d31 Squash mumble migrations 2021-01-08 12:28:12 -05:00
Ariel Rin
aeeb35bc60 Merge branch 'ceo_id' into 'master'
Add CEO ID to Corp Eve Model

See merge request allianceauth/allianceauth!1287
2021-01-08 10:37:33 +00:00
colcrunch
630aa3f320 Merge branch 'doc-fixes' into 'master'
It's TeamSpeak not Discord here

See merge request allianceauth/allianceauth!1288
2021-01-07 19:16:22 +00:00
Peter Pfeufer
3487a945c2 It's TeamSpeak not Discord here 2021-01-06 10:42:01 +01:00
Ariel Rin
1936ae44a3 Merge branch 'opengroupauditlogs' into 'master'
Add Open Group Audit Logs

Closes #1262

See merge request allianceauth/allianceauth!1279
2021-01-06 02:38:48 +00:00
Ariel Rin
3d7a84e786 Revert "Add Audit Logs to Open Groups and Autoleaves"
This reverts commit f64d81292e8b6c0531d610799098c3e3cf105c87.
2021-01-06 02:38:47 +00:00
Ariel Rin
a4d6730cb0 Merge branch 'docs' into 'master'
Document Permissions and other Docs Improvments

Closes #1253

See merge request allianceauth/allianceauth!1283
2021-01-06 02:37:29 +00:00
Ariel Rin
b724227a29 Merge branch 'dependencies' into 'master'
Bump Django-Redis-Cache to 3.x branch

Closes #1264

See merge request allianceauth/allianceauth!1280
2021-01-06 02:36:52 +00:00
AaronKable
d72964fd7c Add CEO Id to Corp Eve Model 2021-01-05 22:14:52 +08:00
Ariel Rin
d4a41cfb60 Merge branch 'transifex' into 'master'
Update from Transifex

See merge request allianceauth/allianceauth!1285
2020-12-28 07:11:50 +00:00
Ariel Rin
05859453df Update from Transifex 2020-12-28 07:11:50 +00:00
Ariel Rin
240d910c9b Document Permissions and other Docs Improvments 2020-12-26 16:10:42 +10:00
Ariel Rin
0c31bce7d0 Bump Django-Redis-Cache up to 3.x branch 2020-12-23 17:59:36 +10:00
Ariel Rin
f4c4ae36ed Merge branch 'fix_celeryonce_dependency' into 'master'
Prevent autoretry_for bug in celery_once

See merge request allianceauth/allianceauth!1278
2020-12-19 02:08:36 +00:00
Ariel Rin
b9931b2ceb Merge branch 'improve_groups_state_binding' into 'master'
Enable group state restrictions to work with internal groups

See merge request allianceauth/allianceauth!1276
2020-12-19 02:05:05 +00:00
Erik Kalkoken
4fd6f06c0b Enable group state restrictions to work with internal groups 2020-12-19 02:05:05 +00:00
ErikKalkoken
09573ba7ef fix dependency for celery_once 2020-12-17 22:53:27 +01:00
Ariel Rin
08e9676760 Version Bump v2.8.1 2020-12-07 04:46:55 +00:00
Ariel Rin
15ae737522 Merge branch 'user-agent' into 'master'
Bump minimum django-esi and add User Agent header.

See merge request allianceauth/allianceauth!1275
2020-12-07 04:32:10 +00:00
Ariel Rin
50ec9e563e Merge branch 'celery_result_logger' into 'master'
Remove result from default log message on task success

See merge request allianceauth/allianceauth!1273
2020-12-07 04:30:52 +00:00
colcrunch
cb40649f8b Add note to local.py on ESI_CONTACT_EMAIL setting 2020-12-06 22:54:31 -05:00
colcrunch
bccead0881 Add mention of ESI_USER_CONTACT_EMAIL to install documentation. 2020-12-02 17:42:03 -05:00
colcrunch
35ae710624 Add ESI_USER_CONTACT_EMAIL setting to local.py 2020-12-01 23:25:48 -05:00
colcrunch
75de4518f2 Use app_info_text in EveSwaggerProvider to set the User-Agent header on ESI requests. 2020-12-01 22:40:09 -05:00
colcrunch
bfcdfea6c8 Update minimum django-esi version to ensure support for user-agent header. 2020-12-01 17:17:55 -05:00
Ariel Rin
471553fa88 Add a minimal set of django deps needed to build docs 2020-12-01 07:14:22 +00:00
Ariel Rin
db59f5f69b Add Django to docs requirements 2020-11-30 14:41:01 +00:00
Ariel Rin
1b5413646e Correct Typos in Docs Requirements 2020-11-30 14:37:42 +00:00
Ariel Rin
0337d2517c use limited set of python packages for docs 2020-11-30 14:30:23 +00:00
ErikKalkoken
87e6eb9688 Remove result from default log message on task success 2020-11-28 22:10:41 +01:00
Ariel Rin
7b8c246ef8 Merge branch 'master' into 'master'
[Quickfix] Missing ^ added

See merge request allianceauth/allianceauth!1271
2020-11-19 13:26:53 +00:00
Peter Pfeufer
a268a8980a Missing ^ added 2020-11-18 22:50:27 +01:00
Ariel Rin
3b516c338e Merge branch 'groupmanagement-revamp' into 'master'
Group management improvements

See merge request allianceauth/allianceauth!1270
2020-11-17 11:56:16 +00:00
Peter Pfeufer
9952685805 textfield should not be Null=True 2020-11-16 18:04:57 +01:00
Peter Pfeufer
2f59c8df22 And of course I missed something ... 2020-10-26 01:10:41 +01:00
Peter Pfeufer
6cd0a42791 reverted code formatting for Aaron :-) 2020-10-26 01:06:58 +01:00
Peter Pfeufer
4c42153bfd stop member count header from line breaking 2020-10-25 13:53:17 +01:00
Peter Pfeufer
603bd9c37c make description a real description
textfield instead of charfield. a bit more user friendly in the backend
2020-10-25 12:40:10 +01:00
Peter Pfeufer
87c0c3ac73 tables formatted properly
align center is horrible to read ....
2020-10-25 11:55:03 +01:00
Peter Pfeufer
8a91e7f6ac navactive status fixed in left menu and top menu 2020-10-25 11:22:10 +01:00
Peter Pfeufer
281dbdbb01 proper URL structure established 2020-10-25 11:21:35 +01:00
Ariel Rin
8980d8d32f Version bump to 2.8.0 Stable 2020-10-13 05:33:54 +00:00
Ariel Rin
9d6cf9a62e Merge branch 'transifex' into 'master'
Add French and Japanese Translations + QoL Translation Fixes

See merge request allianceauth/allianceauth!1263
2020-10-13 04:17:37 +00:00
Ariel Rin
0fabb2b368 Add French and Japanese Translations + QoL Translation Fixes 2020-10-13 04:17:37 +00:00
Ariel Rin
9801ca0314 Merge branch 'emailoverrideredirect' into 'master'
Fix Redirect to dashboard if not verifying email

See merge request allianceauth/allianceauth!1268
2020-10-13 04:08:15 +00:00
Ariel Rin
fd86b26b39 Redirect to dashboard if not verifying email 2020-10-13 13:25:00 +10:00
Ariel Rin
b0dbef1587 Merge branch 'master' into 'master'
Expand mumble pwhash field to allow for passlib 1.7.3+

See merge request allianceauth/allianceauth!1267
2020-10-11 10:34:44 +00:00
Ariel Rin
a6e60bc23b expand mumble certhash to allow for hmac-sha-256 2020-10-11 20:11:56 +10:00
Ariel Rin
137e8a876d Merge branch 'add-corp-and-alliance-tags-to-srp' into 'master'
Add alliance and corp ticker to pilot name

See merge request allianceauth/allianceauth!1265
2020-10-11 03:42:56 +00:00
Ariel Rin
fe9538253f Merge branch 'highlight-active-menu-item' into 'master'
Highlight active menu item

See merge request allianceauth/allianceauth!1266
2020-10-11 03:41:17 +00:00
Peter Pfeufer
edda2c248e highlight active menu item 2020-10-02 20:12:21 +02:00
Peter Pfeufer
62275639e3 Added alliance and corp ticker to pilot name
Takes care of https://gitlab.com/allianceauth/allianceauth/-/issues/1228
2020-10-01 20:14:54 +02:00
Ariel Rin
d0c68b82f4 Merge branch 'docs' into 'master'
Docs Sphinx Upgrades

See merge request allianceauth/allianceauth!1264
2020-10-01 09:17:36 +00:00
Ariel Rin
78b5953bdf Merge branch 'master' of https://gitlab.com/allianceauth/allianceauth into docs 2020-10-01 18:59:14 +10:00
Ariel Rin
25e565b099 Docs Requirement upgrades 2020-10-01 18:54:04 +10:00
Ariel Rin
1fe0f78ad7 Exclude django-redis-cache 2.1.3 2020-10-01 01:59:48 +00:00
Ariel Rin
fc8b68156f Merge branch 'patch-4' into 'master'
Fixing FA icon

See merge request allianceauth/allianceauth!1262
2020-09-29 05:10:30 +00:00
Peter Pfeufer
b47cd197ce Fixing FA icon 2020-09-28 21:07:08 +00:00
Ariel Rin
3943426c4c Version Bump 2.8.0a2 2020-09-21 06:25:16 +00:00
Ariel Rin
08a9bd42a3 Merge branch 'django3' into 'master'
Django 3.1.1 bring up

See merge request allianceauth/allianceauth!1256
2020-09-21 06:16:44 +00:00
Ariel Rin
b2ff339efe Merge branch 'gitlabci2' into 'master'
Update Gitlab Deploy Python Version, Debian Distro to Stable

See merge request allianceauth/allianceauth!1260
2020-09-21 06:15:40 +00:00
Ariel Rin
c604131e04 Update Deploy Runner 2020-09-21 12:12:02 +10:00
Ariel Rin
f17607f126 Merge branch 'transifex' into 'master'
Update From Transifex

See merge request allianceauth/allianceauth!1258
2020-09-21 01:45:16 +00:00
Ariel Rin
94e455a57b Update From Transifex 2020-09-21 01:45:16 +00:00
Ariel Rin
3c9149db4a Merge branch 'improve_authtuilts_add_permission' into 'master'
Improve auth utils for permissions

See merge request allianceauth/allianceauth!1255
2020-09-21 01:09:03 +00:00
Ariel Rin
96cc615c07 Merge branch 'docs_mumbleavatars' into 'master'
Docs: Mumble Avatars Feature

See merge request allianceauth/allianceauth!1250
2020-09-21 00:01:35 +00:00
Ariel Rin
cd1f4a1c2b Docs: Mumble Avatars Feature 2020-09-21 00:01:35 +00:00
Ariel Rin
e0f99a42db Merge branch 'exiom-srp-update' into 'master'
SRP Module - Added Datatables & Sorting, Standardized Date/Time for Overall AA Consistency

See merge request allianceauth/allianceauth!1254
2020-09-20 23:55:22 +00:00
Exiom
3506e417d4 SRP Module - Added Datatables & Sorting, Standardized Date/Time for Overall AA Consistency 2020-09-20 23:55:22 +00:00
AaronKable
36197e2212 swap to reverse_lazy 2020-09-18 23:32:36 +08:00
AaronKable
fc5f42d01e remove whitespace in setup.py 2020-09-18 22:16:49 +08:00
AaronKable
e26d3767e0 update models as NullBooleanField is deprecated. 2020-09-18 22:16:24 +08:00
AaronKable
046b37c76a update auth and group model admins for django 3.1 2020-09-18 22:04:59 +08:00
AaronKable
925ff3e116 remove django req's from tox, they are managed in setup.py 2020-09-18 22:04:05 +08:00
AaronKable
e5ede4f7b6 Cant Reference what is already deleted 2020-09-18 22:03:16 +08:00
AaronKable
ded9301527 initial django3 bringup 2020-09-18 11:45:37 +08:00
ErikKalkoken
bd1ed6ff73 Add user as return value to add permission methods 2020-09-15 12:35:58 +02:00
Ariel Rin
feb65980d4 Merge branch 'fix_group_count_badge' into 'master'
Fix group count badge showing at zero

Closes #1258

See merge request allianceauth/allianceauth!1253
2020-09-12 02:05:22 +00:00
Ariel Rin
e81d75a782 Merge branch 'docs_settings_fix' into 'master'
Remove erroneous indents from settings in service module docs

See merge request allianceauth/allianceauth!1252
2020-09-12 02:04:37 +00:00
ErikKalkoken
ff305d13ae Fix group count badge showing at zero 2020-09-11 23:54:56 +02:00
colcrunch
8bcbc1a779 Remove erroneous indents from settings in service module docs. (Checked other docs, and there do not appear to be any more errors of this type) 2020-09-11 12:51:17 -04:00
Ariel Rin
3874aa6fee Version Bump 2.8.0a1 2020-09-11 11:52:20 +00:00
Ariel Rin
103e9f3a11 Merge branch 'discourse_beta' into 'master'
Discourse API with external package

See merge request allianceauth/allianceauth!1251
2020-09-11 11:33:19 +00:00
Ariel Rin
d02c25f421 Merge branch 'feature_menu_item_badges' into 'master'
Add menu item badge feature and update group icons

See merge request allianceauth/allianceauth!1240
2020-09-11 11:33:14 +00:00
Erik Kalkoken
228af38a4a Add menu item badge feature and update group icons 2020-09-11 11:33:14 +00:00
AaronKable
051a48885c discourse API with external package 2020-09-11 17:19:54 +08:00
Ariel Rin
6bcdc6052f Merge branch 'local-delivery' into 'master'
JS/CSS/Font Refactoring for use with AA-GDPR Package

Closes #1217

See merge request allianceauth/allianceauth!1247
2020-09-11 04:13:01 +00:00
Ariel Rin
af3527e64f Revert "load bootswatch less locally #1217"
This reverts commit 3a9a7267ea8734ba0a5cf1d0fea632eb2276d45c.
2020-09-11 04:13:01 +00:00
Ariel Rin
17ef3dd07a Merge branch 'srp_fix' into 'master'
Use request.scheme to get the http/https for the site

See merge request allianceauth/allianceauth!1249
2020-09-11 03:05:06 +00:00
AaronKable
1f165ecd2a use request.scheme to get the http/https for the site 2020-09-07 19:10:58 +08:00
Ariel Rin
70d1d450a9 Add Korean and Russian as features 2020-09-03 12:24:23 +00:00
Ariel Rin
b667892698 Version Bump 2.7.5 2020-09-01 02:09:05 +00:00
Ariel Rin
dc11add0e9 Merge branch 'discordapp.com-deprecation' into 'master'
discord.com replaces discordapp.com

See merge request allianceauth/allianceauth!1248
2020-09-01 01:53:25 +00:00
Ariel Rin
cb429a0b88 discord.com replaces discordapp.com 2020-09-01 11:20:57 +10:00
Ariel Rin
b51039cfc0 Merge branch 'docs' into 'master'
Add Optimizing Mumble to Services docs

See merge request allianceauth/allianceauth!1243
2020-09-01 01:11:49 +00:00
Ariel Rin
eadd959d95 Merge branch 'fix_celery_once_backend' into 'master'
Fix celery once not working properly

See merge request allianceauth/allianceauth!1246
2020-08-25 06:13:59 +00:00
Erik Kalkoken
1856e03d88 Fix celery once not working properly 2020-08-25 06:13:59 +00:00
Ariel Rin
7dcfa622a3 Merge branch 'issue_1234_exiom' into 'master'
Month Ordering Fix for Group Management Audit Logs

See merge request allianceauth/allianceauth!1245
2020-08-25 06:07:45 +00:00
Exiom
64251b9b3c Fix Date/Time Month Ordering #1234 2020-08-21 03:57:24 +00:00
Exiom
6b073dd5fc Fix Date/Time Month Ordering #1234 2020-08-21 03:33:35 +00:00
Ariel Rin
0911fabfb2 Merge branch 'issue_1234' into 'master'
Correct Month Ordering in Group Management Audit Logs

Closes #1234

See merge request allianceauth/allianceauth!1244
2020-08-20 07:11:47 +00:00
Ariel Rin
050d3f5e63 use month numerical 2020-08-20 16:55:18 +10:00
Ariel Rin
bbe3f78ad1 Grammar and Spelling Corrections 2020-08-20 15:19:50 +10:00
Ariel Rin
8204c18895 Add Optimzing Mumble 2020-08-20 15:09:07 +10:00
Ariel Rin
b91c788897 Merge branch 'bulk-affiliations' into 'master'
Reduce run time for eve online model updates

See merge request allianceauth/allianceauth!1227
2020-08-20 03:14:40 +00:00
Erik Kalkoken
1d20a3029f Only update characters if they have changed corp or alliance by bulk calling affiliations before calling character tasks. 2020-08-20 03:14:40 +00:00
577 changed files with 24067 additions and 8432 deletions

28
.editorconfig Normal file
View File

@@ -0,0 +1,28 @@
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 4
tab_width = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.{yaml,yml,less}]
indent_size = 2
[*.md]
indent_size = 2
# Makefiles always use tabs for indentation
[Makefile]
indent_style = tab
[*.bat]
indent_style = tab
[{Dockerfile,*.dockerfile}]
indent_style = space
indent_size = 4

2
.gitignore vendored
View File

@@ -38,7 +38,6 @@ htmlcov/
.tox/ .tox/
.coverage .coverage
.cache .cache
nosetests.xml
coverage.xml coverage.xml
# Translations # Translations
@@ -77,3 +76,4 @@ celerybeat-schedule
.flake8 .flake8
.pylintrc .pylintrc
Makefile Makefile
.isort.cfg

View File

@@ -1,54 +1,225 @@
.only-default: &only-default
only:
- master
- branches
- merge_requests
stages: stages:
- pre-commit
- gitlab
- test - test
- deploy - deploy
- docker
include:
- template: Dependency-Scanning.gitlab-ci.yml
- template: Security/SAST.gitlab-ci.yml
before_script: before_script:
- apt-get update && apt-get install redis-server -y - apt-get update && apt-get install redis-server -y
- redis-server --daemonize yes - redis-server --daemonize yes
- redis-cli ping - python -V
- python -V - pip install wheel tox
- pip install wheel tox
test-3.6-core: pre-commit-check:
<<: *only-default
stage: pre-commit
image: python:3.6-buster image: python:3.6-buster
script: variables:
- tox -e py36-core PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
cache:
paths:
- ${PRE_COMMIT_HOME}
script:
- pip install pre-commit
- pre-commit run --all-files
sast:
stage: gitlab
before_script: []
dependency_scanning:
stage: gitlab
before_script:
- apt-get update && apt-get install redis-server libmariadb-dev -y
- redis-server --daemonize yes
- python -V
- pip install wheel tox
test-3.7-core: test-3.7-core:
image: python:3.7-buster <<: *only-default
script: image: python:3.7-bullseye
script:
- tox -e py37-core - tox -e py37-core
artifacts:
when: always
reports:
cobertura: coverage.xml
test-3.8-core: test-3.8-core:
image: python:3.8-buster <<: *only-default
script: image: python:3.8-bullseye
script:
- tox -e py38-core - tox -e py38-core
artifacts:
when: always
reports:
cobertura: coverage.xml
test-3.6-all: test-3.9-core:
image: python:3.6-buster <<: *only-default
script: image: python:3.9-bullseye
- tox -e py36-all script:
- tox -e py39-core
artifacts:
when: always
reports:
cobertura: coverage.xml
test-3.10-core:
<<: *only-default
image: python:3.10-bullseye
script:
- tox -e py310-core
artifacts:
when: always
reports:
cobertura: coverage.xml
test-3.11-core:
<<: *only-default
image: python:3.11-rc-bullseye
script:
- tox -e py311-core
artifacts:
when: always
reports:
cobertura: coverage.xml
allow_failure: true
test-3.7-all: test-3.7-all:
image: python:3.7-buster <<: *only-default
script: image: python:3.7-bullseye
script:
- tox -e py37-all - tox -e py37-all
artifacts:
when: always
reports:
cobertura: coverage.xml
test-3.8-all: test-3.8-all:
image: python:3.8-buster <<: *only-default
script: image: python:3.8-bullseye
script:
- tox -e py38-all - tox -e py38-all
artifacts:
when: always
reports:
cobertura: coverage.xml
test-3.9-all:
<<: *only-default
image: python:3.9-bullseye
script:
- tox -e py39-all
artifacts:
when: always
reports:
cobertura: coverage.xml
test-3.10-all:
<<: *only-default
image: python:3.10-bullseye
script:
- tox -e py310-all
artifacts:
when: always
reports:
cobertura: coverage.xml
test-3.11-all:
<<: *only-default
image: python:3.11-rc-bullseye
script:
- tox -e py311-all
artifacts:
when: always
reports:
cobertura: coverage.xml
allow_failure: true
deploy_production: deploy_production:
stage: deploy stage: deploy
image: python:3.6-stretch image: python:3.10-bullseye
before_script: before_script:
- pip install twine - pip install twine wheel
script: script:
- python setup.py sdist - python setup.py sdist bdist_wheel
- twine upload dist/* - twine upload dist/*
rules: rules:
- if: $CI_COMMIT_TAG - if: $CI_COMMIT_TAG
build-image:
before_script: []
image: docker:20.10.10
stage: docker
services:
- docker:20.10.10-dind
script: |
CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -)
IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_COMMIT_SHORT_SHA
CURRENT_TAG=$CI_REGISTRY_IMAGE/auth:$CI_COMMIT_TAG
MINOR_TAG=$CI_REGISTRY_IMAGE/auth:$(echo $CI_COMMIT_TAG | cut -d '.' -f 1-2)
MAJOR_TAG=$CI_REGISTRY_IMAGE/auth:$(echo $CI_COMMIT_TAG | cut -d '.' -f 1)
LATEST_TAG=$CI_REGISTRY_IMAGE/auth:latest
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 tag $IMAGE_TAG $CURRENT_TAG
docker tag $IMAGE_TAG $MINOR_TAG
docker tag $IMAGE_TAG $MAJOR_TAG
docker tag $IMAGE_TAG $LATEST_TAG
docker image push --all-tags $CI_REGISTRY_IMAGE/auth
rules:
- if: $CI_COMMIT_TAG
build-image-dev:
before_script: []
image: docker:20.10.10
stage: docker
services:
- docker:20.10.10-dind
script: |
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
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 push $IMAGE_TAG
rules:
- if: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == ""'
when: manual
- if: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME != ""'
when: never
build-image-mr:
before_script: []
image: docker:20.10.10
stage: docker
services:
- docker:20.10.10-dind
script: |
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
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 push $IMAGE_TAG
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: manual
- if: '$CI_PIPELINE_SOURCE != "merge_request_event"'
when: never

View File

@@ -1,8 +1,8 @@
# Bug # Bug
- I have searched [issues](https://gitlab.com/allianceauth/allianceauth/issues?scope=all&utf8=%E2%9C%93&state=all) (Y/N): - I have searched [issues](https://gitlab.com/allianceauth/allianceauth/issues?scope=all&utf8=%E2%9C%93&state=all) (Y/N):
- What Version of Alliance Auth: - What Version of Alliance Auth:
- What Operating System: - What Operating System:
- Version of other components relevant to issue eg. Service, Database: - Version of other components relevant to issue eg. Service, Database:
Please include a brief description of your issue here. Please include a brief description of your issue here.
@@ -11,4 +11,4 @@ Please include steps to reproduce the issue
Please include any tracebacks or logs Please include any tracebacks or logs
Please include the results of the command `pip list` Please include the results of the command `pip list`

View File

@@ -4,4 +4,4 @@
- Is this a Service (external integration), a Module (Alliance Auth extension) or an enhancement to an existing service/module. - Is this a Service (external integration), a Module (Alliance Auth extension) or an enhancement to an existing service/module.
- Describe why its useful to you or others. - Describe why its useful to you or others.

34
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,34 @@
# Apply to all files without committing:
# pre-commit run --all-files
# Update this file:
# pre-commit autoupdate
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
hooks:
- id: check-case-conflict
- id: check-json
- id: check-xml
- id: check-yaml
- id: fix-byte-order-marker
- id: trailing-whitespace
exclude: (\.min\.css|\.min\.js|\.mo|\.po|swagger\.json)$
- id: end-of-file-fixer
exclude: (\.min\.css|\.min\.js|\.mo|\.po|swagger\.json)$
- id: mixed-line-ending
args: [ '--fix=lf' ]
- id: fix-encoding-pragma
args: [ '--remove' ]
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python
rev: 2.3.54
hooks:
- id: editorconfig-checker
exclude: ^(LICENSE|allianceauth\/static\/css\/themes\/bootstrap-locals.less|allianceauth\/eveonline\/swagger.json|(.*.po)|(.*.mo))
- repo: https://github.com/asottile/pyupgrade
rev: v2.29.0
hooks:
- id: pyupgrade
args: [ --py37-plus ]

View File

@@ -18,10 +18,6 @@ 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:
version: 3.7 version: 3.7
install: install:
- method: pip - requirements: docs/requirements.txt
path: .
extra_requirements:
- testing
system_packages: true

View File

@@ -337,4 +337,3 @@ proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. Public License instead of this License.

View File

@@ -36,7 +36,7 @@ Main features:
- Can be easily extended with additional services and apps. Many are provided by the community and can be found here: [Community Creations](https://gitlab.com/allianceauth/community-creations) - Can be easily extended with additional services and apps. Many are provided by the community and can be found here: [Community Creations](https://gitlab.com/allianceauth/community-creations)
- Chinese :cn:, English :us:, German :de: and Spanish :es: localization - English :flag_gb:, Chinese :flag_cn:, German :flag_de:, Spanish :flag_es:, Korean :flag_kr: and Russian :flag_ru: localization
For further details about AA - including an installation guide and a full list of included services and plugin apps - please see the [official documentation](http://allianceauth.rtfd.io). For further details about AA - including an installation guide and a full list of included services and plugin apps - please see the [official documentation](http://allianceauth.rtfd.io).

View File

@@ -1,8 +1,8 @@
# 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__ = '2.7.4' __version__ = '2.11.1'
__title__ = 'Alliance Auth' __title__ = 'Alliance Auth'
__url__ = 'https://gitlab.com/allianceauth/allianceauth' __url__ = 'https://gitlab.com/allianceauth/allianceauth'
NAME = '%s v%s' % (__title__, __version__) NAME = f'{__title__} v{__version__}'
default_app_config = 'allianceauth.apps.AllianceAuthConfig' default_app_config = 'allianceauth.apps.AllianceAuthConfig'

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,21 @@
[
{
"model": "analytics.AnalyticsTokens",
"pk": 1,
"fields": {
"name": "AA Team Public Google Analytics (Universal)",
"type": "GA-V4",
"token": "UA-186249766-2",
"send_page_views": "False",
"send_celery_tasks": "False",
"send_stats": "False"
}
},
{
"model": "analytics.AnalyticsIdentifier",
"pk": 1,
"fields": {
"identifier": "ab33e241fbf042b6aa77c7655a768af7"
}
}
]

View File

@@ -0,0 +1,52 @@
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,42 @@
# Generated by Django 3.1.4 on 2020-12-30 13:11
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='AnalyticsIdentifier',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('identifier', models.UUIDField(default=uuid.uuid4, editable=False)),
],
),
migrations.CreateModel(
name='AnalyticsPath',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ignore_path', models.CharField(default='/example/', max_length=254)),
],
),
migrations.CreateModel(
name='AnalyticsTokens',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=254)),
('type', models.CharField(choices=[('GA-U', 'Google Analytics Universal'), ('GA-V4', 'Google Analytics V4')], max_length=254)),
('token', models.CharField(max_length=254)),
('send_page_views', models.BooleanField(default=False)),
('send_celery_tasks', models.BooleanField(default=False)),
('send_stats', models.BooleanField(default=False)),
('ignore_paths', models.ManyToManyField(blank=True, to='analytics.AnalyticsPath')),
],
),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 3.1.4 on 2020-12-30 08:53
from django.db import migrations
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')
token = Tokens()
token.type = 'GA-U'
token.token = 'UA-186249766-2'
token.send_page_views = True
token.send_celery_tasks = True
token.send_stats = True
token.name = 'AA Team Public Google Analytics (Universal)'
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="UA-186249766-2").delete()
class Migration(migrations.Migration):
dependencies = [
('analytics', '0001_initial'),
]
operations = [migrations.RunPython(add_aa_team_token, remove_aa_team_token)
]

View File

@@ -0,0 +1,30 @@
# Generated by Django 3.1.4 on 2020-12-30 08:53
from uuid import uuid4
from django.db import migrations
def generate_identifier(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.
Identifier = apps.get_model('analytics', 'AnalyticsIdentifier')
identifier = Identifier()
identifier.id = 1
identifier.save()
def zero_identifier(apps, schema_editor):
# Have to define some code to remove this identifier
# In case of migration rollback?
Identifier = apps.get_model('analytics', 'AnalyticsIdentifier')
Identifier.objects.filter(id=1).delete()
class Migration(migrations.Migration):
dependencies = [
('analytics', '0002_add_AA_Team_Token'),
]
operations = [migrations.RunPython(generate_identifier, zero_identifier)
]

View File

@@ -0,0 +1,42 @@
# Generated by Django 3.1.13 on 2021-10-15 05:02
from django.core.exceptions import ObjectDoesNotExist
from django.db import migrations
def modify_aa_team_token_add_page_ignore_paths(apps, schema_editor):
# Add /admin/ and /user_notifications_count/ path to ignore
AnalyticsPath = apps.get_model('analytics', 'AnalyticsPath')
admin = AnalyticsPath.objects.create(ignore_path=r"^\/admin\/.*")
user_notifications_count = AnalyticsPath.objects.create(ignore_path=r"^\/user_notifications_count\/.*")
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
token = Tokens.objects.get(token="UA-186249766-2")
token.ignore_paths.add(admin, user_notifications_count)
def undo_modify_aa_team_token_add_page_ignore_paths(apps, schema_editor):
#
AnalyticsPath = apps.get_model('analytics', 'AnalyticsPath')
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
token = Tokens.objects.get(token="UA-186249766-2")
try:
admin = AnalyticsPath.objects.get(ignore_path=r"^\/admin\/.*", analyticstokens=token)
user_notifications_count = AnalyticsPath.objects.get(ignore_path=r"^\/user_notifications_count\/.*", analyticstokens=token)
admin.delete()
user_notifications_count.delete()
except ObjectDoesNotExist:
# Its fine if it doesnt exist, we just dont want them building up when re-migrating
pass
class Migration(migrations.Migration):
dependencies = [
('analytics', '0003_Generate_Identifier'),
]
operations = [migrations.RunPython(modify_aa_team_token_add_page_ignore_paths, undo_modify_aa_team_token_add_page_ignore_paths)
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.8 on 2021-10-17 16:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('analytics', '0004_auto_20211015_0502'),
]
operations = [
migrations.AlterField(
model_name='analyticspath',
name='ignore_path',
field=models.CharField(default='/example/', help_text='Regex Expression, If matched no Analytics Page View is sent', max_length=254),
),
]

View File

@@ -0,0 +1,40 @@
# Generated by Django 3.2.8 on 2021-10-19 01:47
from django.core.exceptions import ObjectDoesNotExist
from django.db import migrations
def modify_aa_team_token_add_page_ignore_paths(apps, schema_editor):
# Add the /account/activate path to ignore
AnalyticsPath = apps.get_model('analytics', 'AnalyticsPath')
account_activate = AnalyticsPath.objects.create(ignore_path=r"^\/account\/activate\/.*")
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
token = Tokens.objects.get(token="UA-186249766-2")
token.ignore_paths.add(account_activate)
def undo_modify_aa_team_token_add_page_ignore_paths(apps, schema_editor):
#
AnalyticsPath = apps.get_model('analytics', 'AnalyticsPath')
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
token = Tokens.objects.get(token="UA-186249766-2")
try:
account_activate = AnalyticsPath.objects.get(ignore_path=r"^\/account\/activate\/.*", analyticstokens=token)
account_activate.delete()
except ObjectDoesNotExist:
# Its fine if it doesnt exist, we just dont want them building up when re-migrating
pass
class Migration(migrations.Migration):
dependencies = [
('analytics', '0005_alter_analyticspath_ignore_path'),
]
operations = [
migrations.RunPython(modify_aa_team_token_add_page_ignore_paths, undo_modify_aa_team_token_add_page_ignore_paths)
]

View File

@@ -0,0 +1,38 @@
from django.db import models
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from uuid import uuid4
class AnalyticsIdentifier(models.Model):
identifier = models.UUIDField(default=uuid4,
editable=False)
def save(self, *args, **kwargs):
if not self.pk and AnalyticsIdentifier.objects.exists():
# Force a single object
raise ValidationError('There is can be only one \
AnalyticsIdentifier instance')
self.pk = self.id = 1 # If this happens to be deleted and recreated, force it to be 1
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 Analytics_Type(models.TextChoices):
GA_U = 'GA-U', _('Google Analytics Universal')
GA_V4 = 'GA-V4', _('Google Analytics V4')
name = models.CharField(max_length=254)
type = models.CharField(max_length=254, choices=Analytics_Type.choices)
token = models.CharField(max_length=254, blank=False)
send_page_views = models.BooleanField(default=False)
send_celery_tasks = models.BooleanField(default=False)
send_stats = models.BooleanField(default=False)
ignore_paths = models.ManyToManyField(AnalyticsPath, blank=True)

View File

@@ -0,0 +1,55 @@
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

@@ -0,0 +1,207 @@
import requests
import logging
from django.conf import settings
from django.apps import apps
from celery import shared_task
from allianceauth import __version__
from .models import AnalyticsTokens, AnalyticsIdentifier
from .utils import (
install_stat_addons,
install_stat_tokens,
install_stat_users)
logger = logging.getLogger(__name__)
BASE_URL = "https://www.google-analytics.com/"
DEBUG_URL = f"{BASE_URL}debug/collect"
COLLECTION_URL = f"{BASE_URL}collect"
if getattr(settings, "ANALYTICS_ENABLE_DEBUG", False) and settings.DEBUG:
# Force sending of analytics data during in a debug/test environemt
# Usefull for developers working on this feature.
logger.warning(
"You have 'ANALYTICS_ENABLE_DEBUG' Enabled! "
"This debug instance will send analytics data!")
DEBUG_URL = COLLECTION_URL
ANALYTICS_URL = COLLECTION_URL
if settings.DEBUG is True:
ANALYTICS_URL = DEBUG_URL
def analytics_event(category: str,
action: str,
label: str,
value: int = 0,
event_type: str = 'Celery'):
"""
Send a Google Analytics Event for each token stored
Includes check for if its enabled/disabled
Args:
`category` (str): Celery Namespace
`action` (str): Task Name
`label` (str): Optional, Task Success/Exception
`value` (int): Optional, If bulk, Query size, can be a binary True/False
`event_type` (str): Optional, Celery or Stats only, Default to Celery
"""
analyticstokens = AnalyticsTokens.objects.all()
client_id = AnalyticsIdentifier.objects.get(id=1).identifier.hex
for token in analyticstokens:
if event_type == 'Celery':
allowed = token.send_celery_tasks
elif event_type == 'Stats':
allowed = token.send_stats
else:
allowed = False
if allowed is True:
tracking_id = token.token
send_ga_tracking_celery_event.s(
tracking_id=tracking_id,
client_id=client_id,
category=category,
action=action,
label=label,
value=value).apply_async(priority=9)
@shared_task()
def analytics_daily_stats():
"""Celery Task: Do not call directly
Gathers a series of daily statistics and sends analytics events containing them
"""
users = install_stat_users()
tokens = install_stat_tokens()
addons = install_stat_addons()
logger.debug("Running Daily Analytics Upload")
analytics_event(category='allianceauth.analytics',
action='send_install_stats',
label='existence',
value=1,
event_type='Stats')
analytics_event(category='allianceauth.analytics',
action='send_install_stats',
label='users',
value=users,
event_type='Stats')
analytics_event(category='allianceauth.analytics',
action='send_install_stats',
label='tokens',
value=tokens,
event_type='Stats')
analytics_event(category='allianceauth.analytics',
action='send_install_stats',
label='addons',
value=addons,
event_type='Stats')
for appconfig in apps.get_app_configs():
analytics_event(category='allianceauth.analytics',
action='send_extension_stats',
label=appconfig.label,
value=1,
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()
def send_ga_tracking_celery_event(
tracking_id: str,
client_id: str,
category: str,
action: str,
label: str,
value: int) -> 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
`category` (str): Celery Namespace
`action` (str): Task Name
`label` (str): Optional, Task Success/Exception
`value` (int): Optional, If bulk, Query size, can be a binary True/False
Returns
-------
requests.Reponse Object
"""
headers = {
"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"}
payload = {
'v': '1',
'tid': tracking_id,
'cid': client_id,
't': 'event',
'ec': category,
'ea': action,
'el': label,
'ev': value,
'aip': 1,
'an': "allianceauth",
'av': __version__
}
response = requests.post(
ANALYTICS_URL, data=payload,
timeout=5, headers=headers)
logger.debug(f"Analytics Celery/Stats Event HTTP{response.status_code}")
return response

View File

View File

@@ -0,0 +1,108 @@
from unittest.mock import patch
from urllib.parse import parse_qs
import requests_mock
from django.test import TestCase, override_settings
from allianceauth.analytics.tasks import ANALYTICS_URL
from allianceauth.eveonline.tasks import update_character
from allianceauth.tests.auth_utils import AuthUtils
@override_settings(CELERY_ALWAYS_EAGER=True)
@requests_mock.mock()
class TestAnalyticsForViews(TestCase):
@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(TestCase):
@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

@@ -0,0 +1,23 @@
from allianceauth.analytics.middleware import AnalyticsMiddleware
from unittest.mock import Mock
from django.test.testcases import TestCase
class TestAnalyticsMiddleware(TestCase):
def setUp(self):
self.middleware = AnalyticsMiddleware()
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

@@ -0,0 +1,26 @@
from allianceauth.analytics.models import AnalyticsIdentifier
from django.core.exceptions import ValidationError
from django.test.testcases import TestCase
from uuid import UUID, uuid4
# Identifiers
uuid_1 = "ab33e241fbf042b6aa77c7655a768af7"
uuid_2 = "7aa6bd70701f44729af5e3095ff4b55c"
class TestAnalyticsIdentifier(TestCase):
def test_identifier_random(self):
self.assertNotEqual(AnalyticsIdentifier.objects.get(), uuid4)
def test_identifier_singular(self):
AnalyticsIdentifier.objects.all().delete()
AnalyticsIdentifier.objects.create(identifier=uuid_1)
# Yeah i have multiple asserts here, they all do the same thing
with self.assertRaises(ValidationError):
AnalyticsIdentifier.objects.create(identifier=uuid_2)
self.assertEqual(AnalyticsIdentifier.objects.count(), 1)
self.assertEqual(AnalyticsIdentifier.objects.get(pk=1).identifier, UUID(uuid_1))

View File

@@ -0,0 +1,119 @@
from allianceauth.analytics.tasks import (
analytics_event,
send_ga_tracking_celery_event,
send_ga_tracking_web_view)
from django.test.testcases import TestCase
class TestAnalyticsTasks(TestCase):
def test_analytics_event(self):
analytics_event(
category='allianceauth.analytics',
action='send_tests',
label='test',
value=1,
event_type='Stats')
def test_send_ga_tracking_web_view_sent(self):
# This test sends if the event SENDS to google
# Not if it was successful
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"
response = send_ga_tracking_web_view(
tracking_id,
client_id,
page,
title,
locale,
useragent)
self.assertEqual(response.status_code, 200)
def test_send_ga_tracking_web_view_success(self):
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"
json_response = send_ga_tracking_web_view(
tracking_id,
client_id,
page,
title,
locale,
useragent).json()
self.assertTrue(json_response["hitParsingResult"][0]["valid"])
def test_send_ga_tracking_web_view_invalid_token(self):
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"
json_response = send_ga_tracking_web_view(
tracking_id,
client_id,
page,
title,
locale,
useragent).json()
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):
tracking_id = 'UA-186249766-2'
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
category = 'test'
action = 'test'
label = 'test'
value = '1'
response = send_ga_tracking_celery_event(
tracking_id,
client_id,
category,
action,
label,
value)
self.assertEqual(response.status_code, 200)
def test_send_ga_tracking_celery_event_success(self):
tracking_id = 'UA-186249766-2'
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
category = 'test'
action = 'test'
label = 'test'
value = '1'
json_response = send_ga_tracking_celery_event(
tracking_id,
client_id,
category,
action,
label,
value).json()
self.assertTrue(json_response["hitParsingResult"][0]["valid"])
def test_send_ga_tracking_celery_event_invalid_token(self):
tracking_id = 'UA-IntentionallyBadTrackingID-2'
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
category = 'test'
action = 'test'
label = 'test'
value = '1'
json_response = send_ga_tracking_celery_event(
tracking_id,
client_id,
category,
action,
label,
value).json()
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'}]

View File

@@ -0,0 +1,55 @@
from django.apps import apps
from allianceauth.authentication.models import User
from esi.models import Token
from allianceauth.analytics.utils import install_stat_users, install_stat_tokens, install_stat_addons
from django.test.testcases import TestCase
def create_testdata():
User.objects.all().delete()
User.objects.create_user(
'user_1'
'abc@example.com',
'password'
)
User.objects.create_user(
'user_2'
'abc@example.com',
'password'
)
#Token.objects.all().delete()
#Token.objects.create(
# character_id=101,
# character_name='character1',
# access_token='my_access_token'
#)
#Token.objects.create(
# character_id=102,
# character_name='character2',
# access_token='my_access_token'
#)
class TestAnalyticsUtils(TestCase):
def test_install_stat_users(self):
create_testdata()
expected = 2
users = install_stat_users()
self.assertEqual(users, expected)
#def test_install_stat_tokens(self):
# create_testdata()
# expected = 2
#
# tokens = install_stat_tokens()
# self.assertEqual(tokens, expected)
def test_install_stat_addons(self):
# this test does what its testing...
# but helpful for existing as a sanity check
expected = len(list(apps.get_app_configs()))
addons = install_stat_addons()
self.assertEqual(addons, expected)

View File

@@ -0,0 +1,36 @@
from django.apps import apps
from allianceauth.authentication.models import User
from esi.models import Token
def install_stat_users() -> int:
"""Count and Return the number of User accounts
Returns
-------
int
The Number of User objects"""
users = User.objects.count()
return users
def install_stat_tokens() -> int:
"""Count and Return the number of ESI Tokens Stored
Returns
-------
int
The Number of Token Objects"""
tokens = Token.objects.count()
return tokens
def install_stat_addons() -> int:
"""Count and Return the number of Django Applications Installed
Returns
-------
int
The Number of Installed Apps"""
addons = len(list(apps.get_app_configs()))
return addons

View File

@@ -1,10 +1,8 @@
from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import User as BaseUser, \ from django.contrib.auth.models import User as BaseUser, \
Permission as BasePermission, Group Permission as BasePermission, Group
from django.db.models import Q, F from django.db.models import Count, Q
from allianceauth.services.hooks import ServicesHook from allianceauth.services.hooks import ServicesHook
from django.db.models.signals import pre_save, post_save, pre_delete, \ from django.db.models.signals import pre_save, post_save, pre_delete, \
post_delete, m2m_changed post_delete, m2m_changed
@@ -19,16 +17,11 @@ from allianceauth.authentication.models import State, get_guest_state,\
CharacterOwnership, UserProfile, OwnershipRecord CharacterOwnership, UserProfile, OwnershipRecord
from allianceauth.hooks import get_hooks from allianceauth.hooks import get_hooks
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo,\ from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo,\
EveAllianceInfo EveAllianceInfo, EveFactionInfo
from allianceauth.eveonline.tasks import update_character from allianceauth.eveonline.tasks import update_character
from .app_settings import AUTHENTICATION_ADMIN_USERS_MAX_GROUPS, \ from .app_settings import AUTHENTICATION_ADMIN_USERS_MAX_GROUPS, \
AUTHENTICATION_ADMIN_USERS_MAX_CHARS AUTHENTICATION_ADMIN_USERS_MAX_CHARS
if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS:
_has_auto_groups = True
else:
_has_auto_groups = False
def make_service_hooks_update_groups_action(service): def make_service_hooks_update_groups_action(service):
""" """
@@ -43,8 +36,8 @@ def make_service_hooks_update_groups_action(service):
for user in queryset: # queryset filtering doesn't work here? for user in queryset: # queryset filtering doesn't work here?
service.update_groups(user) service.update_groups(user)
update_service_groups.__name__ = str('update_{}_groups'.format(slugify(service.name))) update_service_groups.__name__ = str(f'update_{slugify(service.name)}_groups')
update_service_groups.short_description = "Sync groups for selected {} accounts".format(service.title) update_service_groups.short_description = f"Sync groups for selected {service.title} accounts"
return update_service_groups return update_service_groups
@@ -61,8 +54,8 @@ def make_service_hooks_sync_nickname_action(service):
for user in queryset: # queryset filtering doesn't work here? for user in queryset: # queryset filtering doesn't work here?
service.sync_nickname(user) service.sync_nickname(user)
sync_nickname.__name__ = str('sync_{}_nickname'.format(slugify(service.name))) sync_nickname.__name__ = str(f'sync_{slugify(service.name)}_nickname')
sync_nickname.short_description = "Sync nicknames for selected {} accounts".format(service.title) sync_nickname.short_description = f"Sync nicknames for selected {service.title} accounts"
return sync_nickname return sync_nickname
@@ -92,7 +85,6 @@ class UserProfileInline(admin.StackedInline):
query |= Q(userprofile__isnull=True) query |= Q(userprofile__isnull=True)
else: else:
query |= Q(character_ownership__user=obj) query |= Q(character_ownership__user=obj)
qs = EveCharacter.objects.filter(query)
formset = super().get_formset(request, obj=obj, **kwargs) formset = super().get_formset(request, obj=obj, **kwargs)
def get_kwargs(self, index): def get_kwargs(self, index):
@@ -100,7 +92,7 @@ class UserProfileInline(admin.StackedInline):
formset.get_form_kwargs = get_kwargs formset.get_form_kwargs = get_kwargs
return formset return formset
def has_add_permission(self, request): def has_add_permission(self, request, obj=None):
return False return False
def has_delete_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None):
@@ -121,6 +113,8 @@ def user_profile_pic(obj):
) )
else: else:
return None return None
user_profile_pic.short_description = '' user_profile_pic.short_description = ''
@@ -129,29 +123,30 @@ def user_username(obj):
works for both User objects and objects with `user` as FK to User works for both User objects and objects with `user` as FK to User
To be used for all user based admin lists To be used for all user based admin lists
""" """
link = reverse( link = reverse(
'admin:{}_{}_change'.format( 'admin:{}_{}_change'.format(
obj._meta.app_label, obj._meta.app_label,
type(obj).__name__.lower() type(obj).__name__.lower()
), ),
args=(obj.pk,) args=(obj.pk,)
) )
user_obj = obj.user if hasattr(obj, 'user') else obj user_obj = obj.user if hasattr(obj, 'user') else obj
if user_obj.profile.main_character: if user_obj.profile.main_character:
return format_html( return format_html(
'<strong><a href="{}">{}</a></strong><br>{}', '<strong><a href="{}">{}</a></strong><br>{}',
link, link,
user_obj.username, user_obj.username,
user_obj.profile.main_character.character_name user_obj.profile.main_character.character_name
) )
else: else:
return format_html( return format_html(
'<strong><a href="{}">{}</a></strong>', '<strong><a href="{}">{}</a></strong>',
link, link,
user_obj.username, user_obj.username,
) )
user_username.short_description = 'user / main' user_username.short_description = 'user / main'
user_username.admin_order_field = 'username' user_username.admin_order_field = 'username'
@@ -164,17 +159,15 @@ def user_main_organization(obj):
""" """
user_obj = obj.user if hasattr(obj, 'user') else obj user_obj = obj.user if hasattr(obj, 'user') else obj
if not user_obj.profile.main_character: if not user_obj.profile.main_character:
result = None result = ''
else: else:
corporation = user_obj.profile.main_character.corporation_name result = user_obj.profile.main_character.corporation_name
if user_obj.profile.main_character.alliance_id: if user_obj.profile.main_character.alliance_id:
result = format_html('{}<br>{}', result += f'<br>{user_obj.profile.main_character.alliance_name}'
corporation, elif user_obj.profile.main_character.faction_name:
user_obj.profile.main_character.alliance_name result += f'<br>{user_obj.profile.main_character.faction_name}'
) return format_html(result)
else:
result = corporation
return result
user_main_organization.short_description = 'Corporation / Alliance (Main)' user_main_organization.short_description = 'Corporation / Alliance (Main)'
user_main_organization.admin_order_field = \ user_main_organization.admin_order_field = \
@@ -197,22 +190,22 @@ class MainCorporationsFilter(admin.SimpleListFilter):
.distinct()\ .distinct()\
.order_by(Lower('corporation_name')) .order_by(Lower('corporation_name'))
return tuple( return tuple(
[(x['corporation_id'], x['corporation_name']) for x in qs] (x['corporation_id'], x['corporation_name']) for x in qs
) )
def queryset(self, request, qs): def queryset(self, request, qs):
if self.value() is None: if self.value() is None:
return qs.all() return qs.all()
else: else:
if qs.model == User: if qs.model == User:
return qs\ return qs.filter(
.filter(profile__main_character__corporation_id=\ profile__main_character__corporation_id=self.value()
self.value()) )
else: else:
return qs\ return qs.filter(
.filter(user__profile__main_character__corporation_id=\ user__profile__main_character__corporation_id=self.value()
self.value()) )
class MainAllianceFilter(admin.SimpleListFilter): class MainAllianceFilter(admin.SimpleListFilter):
"""Custom filter to filter on alliances from mains only """Custom filter to filter on alliances from mains only
@@ -231,23 +224,54 @@ class MainAllianceFilter(admin.SimpleListFilter):
.distinct()\ .distinct()\
.order_by(Lower('alliance_name')) .order_by(Lower('alliance_name'))
return tuple( return tuple(
[(x['alliance_id'], x['alliance_name']) for x in qs] (x['alliance_id'], x['alliance_name']) for x in qs
) )
def queryset(self, request, qs): def queryset(self, request, qs):
if self.value() is None: if self.value() is None:
return qs.all() return qs.all()
else: else:
if qs.model == User: if qs.model == User:
return qs\ return qs.filter(profile__main_character__alliance_id=self.value())
.filter(profile__main_character__alliance_id=self.value())
else: else:
return qs\ return qs.filter(
.filter(user__profile__main_character__alliance_id=\ user__profile__main_character__alliance_id=self.value()
self.value()) )
def update_main_character_model(modeladmin, request, queryset):
class MainFactionFilter(admin.SimpleListFilter):
"""Custom filter to filter on factions from mains only
works for both User objects and objects with `user` as FK to User
To be used for all user based admin lists
"""
title = 'faction'
parameter_name = 'main_faction_id__exact'
def lookups(self, request, model_admin):
qs = EveCharacter.objects\
.exclude(faction_id=None)\
.exclude(userprofile=None)\
.values('faction_id', 'faction_name')\
.distinct()\
.order_by(Lower('faction_name'))
return tuple(
(x['faction_id'], x['faction_name']) for x in qs
)
def queryset(self, request, qs):
if self.value() is None:
return qs.all()
else:
if qs.model == User:
return qs.filter(profile__main_character__faction_id=self.value())
else:
return qs.filter(
user__profile__main_character__faction_id=self.value()
)
def update_main_character_model(modeladmin, request, queryset):
tasks_count = 0 tasks_count = 0
for obj in queryset: for obj in queryset:
if obj.profile.main_character: if obj.profile.main_character:
@@ -255,51 +279,36 @@ def update_main_character_model(modeladmin, request, queryset):
tasks_count += 1 tasks_count += 1
modeladmin.message_user( modeladmin.message_user(
request, request,
'Update from ESI started for {} characters'.format(tasks_count) f'Update from ESI started for {tasks_count} characters'
) )
update_main_character_model.short_description = \ update_main_character_model.short_description = \
'Update main character model from ESI' 'Update main character model from ESI'
class UserAdmin(BaseUserAdmin): class UserAdmin(BaseUserAdmin):
"""Extending Django's UserAdmin model """Extending Django's UserAdmin model
Behavior of groups and characters columns can be configured via settings
Behavior of groups and characters columns can be configured via settings
""" """
class Media: class Media:
css = { css = {
"all": ("authentication/css/admin.css",) "all": ("authentication/css/admin.css",)
} }
class RealGroupsFilter(admin.SimpleListFilter):
"""Custom filter to get groups w/o Autogroups"""
title = 'group'
parameter_name = 'group_id__exact'
def lookups(self, request, model_admin): def get_queryset(self, request):
qs = Group.objects.all().order_by(Lower('name')) qs = super().get_queryset(request)
if _has_auto_groups: return qs.prefetch_related("character_ownerships__character", "groups")
qs = qs\
.filter(managedalliancegroup__isnull=True)\
.filter(managedcorpgroup__isnull=True)
return tuple([(x.pk, x.name) for x in qs])
def queryset(self, request, queryset):
if self.value() is None:
return queryset.all()
else:
return queryset.filter(groups__pk=self.value())
def get_actions(self, request): def get_actions(self, request):
actions = super(BaseUserAdmin, self).get_actions(request) actions = super(BaseUserAdmin, self).get_actions(request)
actions[update_main_character_model.__name__] = ( actions[update_main_character_model.__name__] = (
update_main_character_model, update_main_character_model,
update_main_character_model.__name__, update_main_character_model.__name__,
update_main_character_model.short_description update_main_character_model.short_description
) )
@@ -309,21 +318,21 @@ class UserAdmin(BaseUserAdmin):
if svc.update_groups.__module__ != ServicesHook.update_groups.__module__: if svc.update_groups.__module__ != ServicesHook.update_groups.__module__:
action = make_service_hooks_update_groups_action(svc) action = make_service_hooks_update_groups_action(svc)
actions[action.__name__] = ( actions[action.__name__] = (
action, action,
action.__name__, action.__name__,
action.short_description action.short_description
) )
# Create sync nickname action if service implements it # Create sync nickname action if service implements it
if svc.sync_nickname.__module__ != ServicesHook.sync_nickname.__module__: if svc.sync_nickname.__module__ != ServicesHook.sync_nickname.__module__:
action = make_service_hooks_sync_nickname_action(svc) action = make_service_hooks_sync_nickname_action(svc)
actions[action.__name__] = ( actions[action.__name__] = (
action, action.__name__, action, action.__name__,
action.short_description action.short_description
) )
return actions return actions
def _list_2_html_w_tooltips(self, my_items: list, max_items: int) -> str: def _list_2_html_w_tooltips(self, my_items: list, max_items: int) -> str:
"""converts list of strings into HTML with cutoff and tooltip""" """converts list of strings into HTML with cutoff and tooltip"""
items_truncated_str = ', '.join(my_items[:max_items]) items_truncated_str = ', '.join(my_items[:max_items])
if not my_items: if not my_items:
@@ -339,79 +348,62 @@ class UserAdmin(BaseUserAdmin):
items_truncated_str items_truncated_str
) )
return result return result
inlines = BaseUserAdmin.inlines + [UserProfileInline]
inlines = BaseUserAdmin.inlines + [UserProfileInline]
ordering = ('username', ) ordering = ('username', )
list_select_related = True list_select_related = ('profile__state', 'profile__main_character')
show_full_result_count = True show_full_result_count = True
list_display = ( list_display = (
user_profile_pic, user_profile_pic,
user_username, user_username,
'_state', '_state',
'_groups', '_groups',
user_main_organization, user_main_organization,
'_characters', '_characters',
'is_active', 'is_active',
'date_joined', 'date_joined',
'_role' '_role'
) )
list_display_links = None list_display_links = None
list_filter = (
list_filter = (
'profile__state', 'profile__state',
RealGroupsFilter, 'groups',
MainCorporationsFilter, MainCorporationsFilter,
MainAllianceFilter, MainAllianceFilter,
MainFactionFilter,
'is_active', 'is_active',
'date_joined', 'date_joined',
'is_staff', 'is_staff',
'is_superuser' 'is_superuser'
) )
search_fields = ( search_fields = (
'username', 'username',
'character_ownerships__character__character_name' 'character_ownerships__character__character_name'
) )
readonly_fields = ('date_joined', 'last_login')
def _characters(self, obj): def _characters(self, obj):
my_characters = [ character_ownerships = list(obj.character_ownerships.all())
x.character.character_name characters = [obj.character.character_name for obj in character_ownerships]
for x in CharacterOwnership.objects\
.filter(user=obj)\
.order_by('character__character_name')\
.select_related()
]
return self._list_2_html_w_tooltips( return self._list_2_html_w_tooltips(
my_characters, sorted(characters),
AUTHENTICATION_ADMIN_USERS_MAX_CHARS AUTHENTICATION_ADMIN_USERS_MAX_CHARS
) )
_characters.short_description = 'characters' _characters.short_description = 'characters'
def _state(self, obj): def _state(self, obj):
return obj.profile.state.name return obj.profile.state.name
_state.short_description = 'state' _state.short_description = 'state'
_state.admin_order_field = 'profile__state' _state.admin_order_field = 'profile__state'
def _groups(self, obj): def _groups(self, obj):
if not _has_auto_groups: my_groups = sorted(group.name for group in list(obj.groups.all()))
my_groups = [x.name for x in obj.groups.order_by('name')]
else:
my_groups = [
x.name for x in obj.groups\
.filter(managedalliancegroup__isnull=True)\
.filter(managedcorpgroup__isnull=True)\
.order_by('name')
]
return self._list_2_html_w_tooltips( return self._list_2_html_w_tooltips(
my_groups, my_groups, AUTHENTICATION_ADMIN_USERS_MAX_GROUPS
AUTHENTICATION_ADMIN_USERS_MAX_GROUPS
) )
_groups.short_description = 'groups' _groups.short_description = 'groups'
def _role(self, obj): def _role(self, obj):
@@ -420,11 +412,11 @@ class UserAdmin(BaseUserAdmin):
elif obj.is_staff: elif obj.is_staff:
role = 'Staff' role = 'Staff'
else: else:
role = 'User' role = 'User'
return role return role
_role.short_description = 'role' _role.short_description = 'role'
def has_change_permission(self, request, obj=None): def has_change_permission(self, request, obj=None):
return request.user.has_perm('auth.change_user') return request.user.has_perm('auth.change_user')
@@ -434,21 +426,35 @@ class UserAdmin(BaseUserAdmin):
def has_delete_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None):
return request.user.has_perm('auth.delete_user') return request.user.has_perm('auth.delete_user')
def get_object(self, *args , **kwargs):
obj = super().get_object(*args , **kwargs)
self.obj = obj # storing current object for use in formfield_for_manytomany
return obj
def formfield_for_manytomany(self, db_field, request, **kwargs): def formfield_for_manytomany(self, db_field, request, **kwargs):
"""overriding this formfield to have sorted lists in the form"""
if db_field.name == "groups": if db_field.name == "groups":
kwargs["queryset"] = Group.objects.all().order_by(Lower('name')) groups_qs = Group.objects.filter(authgroup__states__isnull=True)
obj_state = self.obj.profile.state
if obj_state:
matching_groups_qs = Group.objects.filter(authgroup__states=obj_state)
groups_qs = groups_qs | matching_groups_qs
kwargs["queryset"] = groups_qs.order_by(Lower('name'))
return super().formfield_for_manytomany(db_field, request, **kwargs) return super().formfield_for_manytomany(db_field, request, **kwargs)
@admin.register(State) @admin.register(State)
class StateAdmin(admin.ModelAdmin): class StateAdmin(admin.ModelAdmin):
list_select_related = True list_select_related = True
list_display = ('name', 'priority', '_user_count') list_display = ('name', 'priority', '_user_count')
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.annotate(user_count=Count("userprofile__id"))
def _user_count(self, obj): def _user_count(self, obj):
return obj.userprofile_set.all().count() return obj.user_count
_user_count.short_description = 'Users' _user_count.short_description = 'Users'
_user_count.admin_order_field = 'user_count'
fieldsets = ( fieldsets = (
(None, { (None, {
@@ -456,17 +462,19 @@ class StateAdmin(admin.ModelAdmin):
}), }),
('Membership', { ('Membership', {
'fields': ( 'fields': (
'public', 'public',
'member_characters', 'member_characters',
'member_corporations', 'member_corporations',
'member_alliances' 'member_alliances',
'member_factions'
), ),
}) })
) )
filter_horizontal = [ filter_horizontal = [
'member_characters', 'member_characters',
'member_corporations', 'member_corporations',
'member_alliances', 'member_alliances',
'member_factions',
'permissions' 'permissions'
] ]
@@ -481,12 +489,17 @@ class StateAdmin(admin.ModelAdmin):
elif db_field.name == "member_alliances": elif db_field.name == "member_alliances":
kwargs["queryset"] = EveAllianceInfo.objects.all()\ kwargs["queryset"] = EveAllianceInfo.objects.all()\
.order_by(Lower('alliance_name')) .order_by(Lower('alliance_name'))
elif db_field.name == "member_factions":
kwargs["queryset"] = EveFactionInfo.objects.all()\
.order_by(Lower('faction_name'))
elif db_field.name == "permissions":
kwargs["queryset"] = Permission.objects.select_related("content_type").all()
return super().formfield_for_manytomany(db_field, request, **kwargs) return super().formfield_for_manytomany(db_field, request, **kwargs)
def has_delete_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None):
if obj == get_guest_state(): if obj == get_guest_state():
return False return False
return super(StateAdmin, self).has_delete_permission(request, obj=obj) return super().has_delete_permission(request, obj=obj)
def get_fieldsets(self, request, obj=None): def get_fieldsets(self, request, obj=None):
if obj == get_guest_state(): if obj == get_guest_state():
@@ -495,16 +508,17 @@ class StateAdmin(admin.ModelAdmin):
'fields': ('permissions', 'priority'), 'fields': ('permissions', 'priority'),
}), }),
) )
return super(StateAdmin, self).get_fieldsets(request, obj=obj) return super().get_fieldsets(request, obj=obj)
class BaseOwnershipAdmin(admin.ModelAdmin): class BaseOwnershipAdmin(admin.ModelAdmin):
class Media: class Media:
css = { css = {
"all": ("authentication/css/admin.css",) "all": ("authentication/css/admin.css",)
} }
list_select_related = True list_select_related = (
'user__profile__state', 'user__profile__main_character', 'character')
list_display = ( list_display = (
user_profile_pic, user_profile_pic,
user_username, user_username,
@@ -512,13 +526,14 @@ class BaseOwnershipAdmin(admin.ModelAdmin):
'character', 'character',
) )
search_fields = ( search_fields = (
'user__username', 'user__username',
'character__character_name', 'character__character_name',
'character__corporation_name', 'character__corporation_name',
'character__alliance_name' 'character__alliance_name',
'character__faction_name'
) )
list_filter = ( list_filter = (
MainCorporationsFilter, MainCorporationsFilter,
MainAllianceFilter, MainAllianceFilter,
) )
@@ -542,6 +557,7 @@ class CharacterOwnershipAdmin(BaseOwnershipAdmin):
class PermissionAdmin(admin.ModelAdmin): class PermissionAdmin(admin.ModelAdmin):
actions = None actions = None
readonly_fields = [field.name for field in BasePermission._meta.fields] readonly_fields = [field.name for field in BasePermission._meta.fields]
search_fields = ('codename', )
list_display = ('admin_name', 'name', 'codename', 'content_type') list_display = ('admin_name', 'name', 'codename', 'content_type')
list_filter = ('content_type__app_label',) list_filter = ('content_type__app_label',)
@@ -549,7 +565,7 @@ class PermissionAdmin(admin.ModelAdmin):
def admin_name(obj): def admin_name(obj):
return str(obj) return str(obj)
def has_add_permission(self, request): def has_add_permission(self, request, obj=None):
return False return False
def has_delete_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None):

View File

@@ -2,14 +2,14 @@ from django.conf import settings
def _clean_setting( def _clean_setting(
name: str, name: str,
default_value: object, default_value: object,
min_value: int = None, min_value: int = None,
max_value: int = None, max_value: int = None,
required_type: type = None required_type: type = None
): ):
"""cleans the input for a custom setting """cleans the input for a custom setting
Will use `default_value` if settings does not exit or has the wrong type Will use `default_value` if settings does not exit or has the wrong type
or is outside define boundaries (for int only) or is outside define boundaries (for int only)
@@ -18,22 +18,22 @@ def _clean_setting(
Will assume `min_value` of 0 for int (can be overriden) Will assume `min_value` of 0 for int (can be overriden)
Returns cleaned value for setting Returns cleaned value for setting
""" """
if default_value is None and not required_type: if default_value is None and not required_type:
raise ValueError('You must specify a required_type for None defaults') raise ValueError('You must specify a required_type for None defaults')
if not required_type: if not required_type:
required_type = type(default_value) required_type = type(default_value)
if min_value is None and required_type == int: if min_value is None and required_type == int:
min_value = 0 min_value = 0
if (hasattr(settings, name) if (hasattr(settings, name)
and isinstance(getattr(settings, name), required_type) and isinstance(getattr(settings, name), required_type)
and (min_value is None or getattr(settings, name) >= min_value) and (min_value is None or getattr(settings, name) >= min_value)
and (max_value is None or getattr(settings, name) <= max_value) and (max_value is None or getattr(settings, name) <= max_value)
): ):
return getattr(settings, name) return getattr(settings, name)
else: else:
return default_value return default_value
@@ -43,4 +43,3 @@ AUTHENTICATION_ADMIN_USERS_MAX_GROUPS = \
AUTHENTICATION_ADMIN_USERS_MAX_CHARS = \ AUTHENTICATION_ADMIN_USERS_MAX_CHARS = \
_clean_setting('AUTHENTICATION_ADMIN_USERS_MAX_CHARS', 5) _clean_setting('AUTHENTICATION_ADMIN_USERS_MAX_CHARS', 5)

View File

@@ -3,10 +3,14 @@ from django.core.checks import register, Tags
class AuthenticationConfig(AppConfig): class AuthenticationConfig(AppConfig):
name = 'allianceauth.authentication' name = "allianceauth.authentication"
label = 'authentication' label = "authentication"
def ready(self): def ready(self):
super(AuthenticationConfig, self).ready() from allianceauth.authentication import checks, signals # noqa: F401
from allianceauth.authentication import checks, signals from allianceauth.authentication.task_statistics import (
signals as celery_signals,
)
register(Tags.security)(checks.check_login_scopes_setting) register(Tags.security)(checks.check_login_scopes_setting)
celery_signals.reset_counters()

View File

@@ -12,9 +12,9 @@ logger = logging.getLogger(__name__)
class StateBackend(ModelBackend): class StateBackend(ModelBackend):
@staticmethod @staticmethod
def _get_state_permissions(user_obj): def _get_state_permissions(user_obj):
"""returns permissions for state of given user object""" """returns permissions for state of given user object"""
if hasattr(user_obj, "profile") and user_obj.profile: if hasattr(user_obj, "profile") and user_obj.profile:
return Permission.objects.filter(state=user_obj.profile.state) return Permission.objects.filter(state=user_obj.profile.state)
else: else:
return Permission.objects.none() return Permission.objects.none()
@@ -36,17 +36,17 @@ class StateBackend(ModelBackend):
try: try:
ownership = CharacterOwnership.objects.get(character__character_id=token.character_id) ownership = CharacterOwnership.objects.get(character__character_id=token.character_id)
if ownership.owner_hash == token.character_owner_hash: if ownership.owner_hash == token.character_owner_hash:
logger.debug('Authenticating {0} by ownership of character {1}'.format(ownership.user, token.character_name)) logger.debug(f'Authenticating {ownership.user} by ownership of character {token.character_name}')
return ownership.user return ownership.user
else: else:
logger.debug('{0} has changed ownership. Creating new user account.'.format(token.character_name)) logger.debug(f'{token.character_name} has changed ownership. Creating new user account.')
ownership.delete() ownership.delete()
return self.create_user(token) return self.create_user(token)
except CharacterOwnership.DoesNotExist: except CharacterOwnership.DoesNotExist:
try: try:
# insecure legacy main check for pre-sso registration auth installs # insecure legacy main check for pre-sso registration auth installs
profile = UserProfile.objects.get(main_character__character_id=token.character_id) profile = UserProfile.objects.get(main_character__character_id=token.character_id)
logger.debug('Authenticating {0} by their main character {1} without active ownership.'.format(profile.user, profile.main_character)) logger.debug(f'Authenticating {profile.user} by their main character {profile.main_character} without active ownership.')
# attach an ownership # attach an ownership
token.user = profile.user token.user = profile.user
CharacterOwnership.objects.create_by_token(token) CharacterOwnership.objects.create_by_token(token)
@@ -59,13 +59,13 @@ class StateBackend(ModelBackend):
user = records[0].user user = records[0].user
token.user = user token.user = user
co = CharacterOwnership.objects.create_by_token(token) co = CharacterOwnership.objects.create_by_token(token)
logger.debug('Authenticating {0} by matching owner hash record of character {1}'.format(user, co.character)) logger.debug(f'Authenticating {user} by matching owner hash record of character {co.character}')
if not user.profile.main_character: if not user.profile.main_character:
# set this as their main by default if they have none # set this as their main by default if they have none
user.profile.main_character = co.character user.profile.main_character = co.character
user.profile.save() user.profile.save()
return user return user
logger.debug('Unable to authenticate character {0}. Creating new user.'.format(token.character_name)) logger.debug(f'Unable to authenticate character {token.character_name}. Creating new user.')
return self.create_user(token) return self.create_user(token)
def create_user(self, token): def create_user(self, token):
@@ -77,7 +77,7 @@ class StateBackend(ModelBackend):
co = CharacterOwnership.objects.create_by_token(token) # assign ownership to this user co = CharacterOwnership.objects.create_by_token(token) # assign ownership to this user
user.profile.main_character = co.character # assign main character as token character user.profile.main_character = co.character # assign main character as token character
user.profile.save() user.profile.save()
logger.debug('Created new user {0}'.format(user)) logger.debug(f'Created new user {user}')
return user return user
@staticmethod @staticmethod
@@ -87,10 +87,10 @@ class StateBackend(ModelBackend):
if User.objects.filter(username__startswith=name).exists(): if User.objects.filter(username__startswith=name).exists():
u = User.objects.filter(username__startswith=name) u = User.objects.filter(username__startswith=name)
num = len(u) num = len(u)
username = "%s_%s" % (name, num) username = f"{name}_{num}"
while u.filter(username=username).exists(): while u.filter(username=username).exists():
num += 1 num += 1
username = "%s_%s" % (name, num) username = f"{name}_{num}"
else: else:
username = name username = name
return username return username

View File

@@ -1,6 +1,8 @@
from django import forms from django import forms
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from allianceauth.authentication.models import User
class RegistrationForm(forms.Form): class RegistrationForm(forms.Form):
email = forms.EmailField(label=_('Email'), max_length=254, required=True) email = forms.EmailField(label=_('Email'), max_length=254, required=True)
class _meta:
model = User

View File

@@ -10,5 +10,5 @@ urlpatterns = [
url(r'^register/$', views.RegistrationView.as_view(), name='registration_register'), url(r'^register/$', views.RegistrationView.as_view(), name='registration_register'),
url(r'^register/complete/$', views.registration_complete, name='registration_complete'), url(r'^register/complete/$', views.registration_complete, name='registration_complete'),
url(r'^register/closed/$', views.registration_closed, name='registration_disallowed'), url(r'^register/closed/$', views.registration_closed, name='registration_disallowed'),
url(r'', include('registration.auth_urls')), url(r'', include('django.contrib.auth.urls')),
] ]

View File

@@ -11,10 +11,10 @@ class Command(BaseCommand):
if profiles.exists(): if profiles.exists():
for profile in profiles: for profile in profiles:
self.stdout.write(self.style.ERROR( self.stdout.write(self.style.ERROR(
'{0} does not have an ownership. Resetting user {1} main character.'.format(profile.main_character, '{} does not have an ownership. Resetting user {} main character.'.format(profile.main_character,
profile.user))) profile.user)))
profile.main_character = None profile.main_character = None
profile.save() profile.save()
self.stdout.write(self.style.WARNING('Reset {0} main characters.'.format(profiles.count()))) self.stdout.write(self.style.WARNING(f'Reset {profiles.count()} main characters.'))
else: else:
self.stdout.write(self.style.SUCCESS('All main characters have active ownership.')) self.stdout.write(self.style.SUCCESS('All main characters have active ownership.'))

View File

@@ -16,6 +16,8 @@ def available_states_query(character):
query |= Q(member_corporations__corporation_id=character.corporation_id) query |= Q(member_corporations__corporation_id=character.corporation_id)
if character.alliance_id: if character.alliance_id:
query |= Q(member_alliances__alliance_id=character.alliance_id) query |= Q(member_alliances__alliance_id=character.alliance_id)
if character.faction_id:
query |= Q(member_factions__faction_id=character.faction_id)
return query return query
@@ -23,8 +25,7 @@ class CharacterOwnershipManager(Manager):
def create_by_token(self, token): def create_by_token(self, token):
if not EveCharacter.objects.filter(character_id=token.character_id).exists(): if not EveCharacter.objects.filter(character_id=token.character_id).exists():
EveCharacter.objects.create_character(token.character_id) EveCharacter.objects.create_character(token.character_id)
return self.create(character=EveCharacter.objects.get(character_id=token.character_id), user=token.user, return self.create(character=EveCharacter.objects.get(character_id=token.character_id), user=token.user, owner_hash=token.character_owner_hash)
owner_hash=token.character_owner_hash)
class StateQuerySet(QuerySet): class StateQuerySet(QuerySet):
@@ -50,7 +51,7 @@ class StateQuerySet(QuerySet):
for state in self: for state in self:
for profile in state.userprofile_set.all(): for profile in state.userprofile_set.all():
profile.assign_state(state=self.model.objects.exclude(pk=state.pk).get_for_user(profile.user)) profile.assign_state(state=self.model.objects.exclude(pk=state.pk).get_for_user(profile.user))
super(StateQuerySet, self).delete() super().delete()
class StateManager(Manager): class StateManager(Manager):

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-05 21:38 # Generated by Django 1.10.1 on 2016-09-05 21:38
from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-07 19:14 # Generated by Django 1.10.1 on 2016-09-07 19:14
from __future__ import unicode_literals
from django.db import migrations from django.db import migrations

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-09 20:29 # Generated by Django 1.10.1 on 2016-09-09 20:29
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-09 23:19 # Generated by Django 1.10.1 on 2016-09-09 23:19
from __future__ import unicode_literals
from django.db import migrations from django.db import migrations

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-09 23:11 # Generated by Django 1.10.1 on 2016-09-09 23:11
from __future__ import unicode_literals
from django.db import migrations from django.db import migrations
@@ -17,7 +15,7 @@ def create_permissions(apps, schema_editor):
Permission = apps.get_model('auth', 'Permission') Permission = apps.get_model('auth', 'Permission')
ct = ContentType.objects.get_for_model(User) ct = ContentType.objects.get_for_model(User)
Permission.objects.get_or_create(codename="member", content_type=ct, name="member") Permission.objects.get_or_create(codename="member", content_type=ct, name="member")
Permission.objects.get_or_create(codename="blue_member", content_type=ct, name="blue_member") Permission.objects.get_or_create(codename="blue_member", content_type=ct, name="blue_member")
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-10 05:42 # Generated by Django 1.10.1 on 2016-09-10 05:42
from __future__ import unicode_literals
from django.db import migrations from django.db import migrations

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-10 21:50 # Generated by Django 1.10.1 on 2016-09-10 21:50
from __future__ import unicode_literals
from django.db import migrations from django.db import migrations

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-12 13:04 # Generated by Django 1.10.1 on 2016-09-12 13:04
from __future__ import unicode_literals
from django.db import migrations from django.db import migrations

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2016-10-21 02:28 # Generated by Django 1.10.2 on 2016-10-21 02:28
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2017-01-07 06:47 # Generated by Django 1.10.1 on 2017-01-07 06:47
from __future__ import unicode_literals
from django.db import migrations from django.db import migrations
@@ -10,7 +8,7 @@ def count_completed_fields(model):
def forward(apps, schema_editor): def forward(apps, schema_editor):
# this ensures only one model exists per user # this ensures only one model exists per user
AuthServicesInfo = apps.get_model('authentication', 'AuthServicesInfo') AuthServicesInfo = apps.get_model('authentication', 'AuthServicesInfo')
users = set([a.user for a in AuthServicesInfo.objects.all()]) users = {a.user for a in AuthServicesInfo.objects.all()}
for u in users: for u in users:
auths = AuthServicesInfo.objects.filter(user=u) auths = AuthServicesInfo.objects.filter(user=u)
if auths.count() > 1: if auths.count() > 1:

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2017-01-07 07:11 # Generated by Django 1.10.1 on 2017-01-07 07:11
from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-01-12 00:59 # Generated by Django 1.10.5 on 2017-01-12 00:59
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2016-12-11 23:14 # Generated by Django 1.10.2 on 2016-12-11 23:14
from __future__ import unicode_literals
from django.db import migrations from django.db import migrations

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-09 23:19 # Generated by Django 1.10.1 on 2016-09-09 23:19
from __future__ import unicode_literals
from django.db import migrations from django.db import migrations

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-03-22 23:09 # Generated by Django 1.10.5 on 2017-03-22 23:09
from __future__ import unicode_literals
import allianceauth.authentication.models import allianceauth.authentication.models
import django.db.models.deletion import django.db.models.deletion
@@ -107,8 +105,8 @@ def populate_ownerships(apps, schema_editor):
EveCharacter = apps.get_model('eveonline', 'EveCharacter') EveCharacter = apps.get_model('eveonline', 'EveCharacter')
unique_character_owners = [t['character_id'] for t in unique_character_owners = [t['character_id'] for t in
Token.objects.all().values('character_id').annotate(n=models.Count('user')) if Token.objects.all().values('character_id').annotate(n=models.Count('user')) if
t['n'] == 1 and EveCharacter.objects.filter(character_id=t['character_id']).exists()] t['n'] == 1 and EveCharacter.objects.filter(character_id=t['character_id']).exists()]
tokens = Token.objects.filter(character_id__in=unique_character_owners) tokens = Token.objects.filter(character_id__in=unique_character_owners)
for c_id in unique_character_owners: for c_id in unique_character_owners:
@@ -171,8 +169,7 @@ def recreate_authservicesinfo(apps, schema_editor):
# repopulate main characters # repopulate main characters
for profile in UserProfile.objects.exclude(main_character__isnull=True).select_related('user', 'main_character'): for profile in UserProfile.objects.exclude(main_character__isnull=True).select_related('user', 'main_character'):
AuthServicesInfo.objects.update_or_create(user=profile.user, AuthServicesInfo.objects.update_or_create(user=profile.user, defaults={'main_char_id': profile.main_character.character_id})
defaults={'main_char_id': profile.main_character.character_id})
# repopulate states we understand # repopulate states we understand
for profile in UserProfile.objects.exclude(state__name='Guest').filter( for profile in UserProfile.objects.exclude(state__name='Guest').filter(

View File

@@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations from django.db import migrations

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.8 on 2021-10-20 05:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0017_remove_fleetup_permission'),
]
operations = [
migrations.AlterField(
model_name='state',
name='name',
field=models.CharField(max_length=32, unique=True),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.1.13 on 2021-10-12 20:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('eveonline', '0015_factions'),
('authentication', '0017_remove_fleetup_permission'),
]
operations = [
migrations.AddField(
model_name='state',
name='member_factions',
field=models.ManyToManyField(blank=True, help_text='Factions to whose members this state is available.', to='eveonline.EveFactionInfo'),
),
]

View File

@@ -0,0 +1,14 @@
# Generated by Django 3.2.8 on 2021-10-26 09:19
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('authentication', '0018_alter_state_name_length'),
('authentication', '0018_state_member_factions'),
]
operations = [
]

View File

@@ -3,7 +3,7 @@ import logging
from django.contrib.auth.models import User, Permission from django.contrib.auth.models import User, Permission
from django.db import models, transaction from django.db import models, transaction
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo, EveFactionInfo
from allianceauth.notifications import notify from allianceauth.notifications import notify
from .managers import CharacterOwnershipManager, StateManager from .managers import CharacterOwnershipManager, StateManager
@@ -12,10 +12,9 @@ logger = logging.getLogger(__name__)
class State(models.Model): class State(models.Model):
name = models.CharField(max_length=20, unique=True) name = models.CharField(max_length=32, unique=True)
permissions = models.ManyToManyField(Permission, blank=True) permissions = models.ManyToManyField(Permission, blank=True)
priority = models.IntegerField(unique=True, priority = models.IntegerField(unique=True, help_text="Users get assigned the state with the highest priority available to them.")
help_text="Users get assigned the state with the highest priority available to them.")
member_characters = models.ManyToManyField(EveCharacter, blank=True, member_characters = models.ManyToManyField(EveCharacter, blank=True,
help_text="Characters to which this state is available.") help_text="Characters to which this state is available.")
@@ -23,6 +22,8 @@ class State(models.Model):
help_text="Corporations to whose members this state is available.") help_text="Corporations to whose members this state is available.")
member_alliances = models.ManyToManyField(EveAllianceInfo, blank=True, member_alliances = models.ManyToManyField(EveAllianceInfo, blank=True,
help_text="Alliances to whose members this state is available.") help_text="Alliances to whose members this state is available.")
member_factions = models.ManyToManyField(EveFactionInfo, blank=True,
help_text="Factions to whose members this state is available.")
public = models.BooleanField(default=False, help_text="Make this state available to any character.") public = models.BooleanField(default=False, help_text="Make this state available to any character.")
objects = StateManager() objects = StateManager()
@@ -43,7 +44,7 @@ class State(models.Model):
with transaction.atomic(): with transaction.atomic():
for profile in self.userprofile_set.all(): for profile in self.userprofile_set.all():
profile.assign_state(state=State.objects.exclude(pk=self.pk).get_for_user(profile.user)) profile.assign_state(state=State.objects.exclude(pk=self.pk).get_for_user(profile.user))
super(State, self).delete(**kwargs) super().delete(**kwargs)
def get_guest_state(): def get_guest_state():
@@ -71,16 +72,21 @@ class UserProfile(models.Model):
if self.state != state: if self.state != state:
self.state = state self.state = state
if commit: if commit:
logger.info('Updating {} state to {}'.format(self.user, self.state)) logger.info(f'Updating {self.user} state to {self.state}')
self.save(update_fields=['state']) self.save(update_fields=['state'])
notify( notify(
self.user, self.user,
_('State changed to: %s' % state), _('State changed to: %s' % state),
_('Your user\'s state is now: %(state)s') _('Your user\'s state is now: %(state)s')
% ({'state': state}), % ({'state': state}),
'info' 'info'
) )
from allianceauth.authentication.signals import state_changed from allianceauth.authentication.signals import state_changed
# We need to ensure we get up to date perms here as they will have just changed.
# Clear all attribute caches and reload the model that will get passed to the signals!
self.refresh_from_db()
state_changed.send( state_changed.send(
sender=self.__class__, user=self.user, state=self.state sender=self.__class__, user=self.user, state=self.state
) )
@@ -101,7 +107,7 @@ class CharacterOwnership(models.Model):
objects = CharacterOwnershipManager() objects = CharacterOwnershipManager()
def __str__(self): def __str__(self):
return "%s: %s" % (self.user, self.character) return f"{self.user}: {self.character}"
class OwnershipRecord(models.Model): class OwnershipRecord(models.Model):
@@ -114,4 +120,4 @@ class OwnershipRecord(models.Model):
ordering = ['-created'] ordering = ['-created']
def __str__(self): def __str__(self):
return "%s: %s on %s" % (self.user, self.character, self.created) return f"{self.user}: {self.character} on {self.created}"

View File

@@ -29,27 +29,32 @@ def trigger_state_check(state):
@receiver(m2m_changed, sender=State.member_characters.through) @receiver(m2m_changed, sender=State.member_characters.through)
def state_member_characters_changed(sender, instance, action, *args, **kwargs): def state_member_characters_changed(sender, instance, action, *args, **kwargs):
if action.startswith('post_'): if action.startswith('post_'):
logger.debug('State {} member characters changed. Re-evaluating membership.'.format(instance)) logger.debug(f'State {instance} member characters changed. Re-evaluating membership.')
trigger_state_check(instance) trigger_state_check(instance)
@receiver(m2m_changed, sender=State.member_corporations.through) @receiver(m2m_changed, sender=State.member_corporations.through)
def state_member_corporations_changed(sender, instance, action, *args, **kwargs): def state_member_corporations_changed(sender, instance, action, *args, **kwargs):
if action.startswith('post_'): if action.startswith('post_'):
logger.debug('State {} member corporations changed. Re-evaluating membership.'.format(instance)) logger.debug(f'State {instance} member corporations changed. Re-evaluating membership.')
trigger_state_check(instance) trigger_state_check(instance)
@receiver(m2m_changed, sender=State.member_alliances.through) @receiver(m2m_changed, sender=State.member_alliances.through)
def state_member_alliances_changed(sender, instance, action, *args, **kwargs): def state_member_alliances_changed(sender, instance, action, *args, **kwargs):
if action.startswith('post_'): if action.startswith('post_'):
logger.debug('State {} member alliances changed. Re-evaluating membership.'.format(instance)) logger.debug(f'State {instance} member alliances changed. Re-evaluating membership.')
trigger_state_check(instance) trigger_state_check(instance)
@receiver(m2m_changed, sender=State.member_factions.through)
def state_member_factions_changed(sender, instance, action, *args, **kwargs):
if action.startswith('post_'):
logger.debug(f'State {instance} member factions changed. Re-evaluating membership.')
trigger_state_check(instance)
@receiver(post_save, sender=State) @receiver(post_save, sender=State)
def state_saved(sender, instance, *args, **kwargs): def state_saved(sender, instance, *args, **kwargs):
logger.debug('State {} saved. Re-evaluating membership.'.format(instance)) logger.debug(f'State {instance} saved. Re-evaluating membership.')
trigger_state_check(instance) trigger_state_check(instance)
@@ -60,7 +65,7 @@ def reassess_on_profile_save(sender, instance, created, *args, **kwargs):
if not created: if not created:
update_fields = kwargs.pop('update_fields', []) or [] update_fields = kwargs.pop('update_fields', []) or []
if 'state' not in update_fields: if 'state' not in update_fields:
logger.debug('Profile for {} saved without state change. Re-evaluating state.'.format(instance.user)) logger.debug(f'Profile for {instance.user} saved without state change. Re-evaluating state.')
instance.assign_state() instance.assign_state()
@@ -68,15 +73,14 @@ def reassess_on_profile_save(sender, instance, created, *args, **kwargs):
def create_required_models(sender, instance, created, *args, **kwargs): def create_required_models(sender, instance, created, *args, **kwargs):
# ensure all users have a model # ensure all users have a model
if created: if created:
logger.debug('User {} created. Creating default UserProfile.'.format(instance)) logger.debug(f'User {instance} created. Creating default UserProfile.')
UserProfile.objects.get_or_create(user=instance) UserProfile.objects.get_or_create(user=instance)
@receiver(post_save, sender=Token) @receiver(post_save, sender=Token)
def record_character_ownership(sender, instance, created, *args, **kwargs): def record_character_ownership(sender, instance, created, *args, **kwargs):
if created: if created:
logger.debug('New token for {0} character {1} saved. Evaluating ownership.'.format(instance.user, logger.debug(f'New token for {instance.user} character {instance.character_name} saved. Evaluating ownership.')
instance.character_name))
if instance.user: if instance.user:
query = Q(owner_hash=instance.character_owner_hash) & Q(user=instance.user) query = Q(owner_hash=instance.character_owner_hash) & Q(user=instance.user)
else: else:
@@ -85,25 +89,21 @@ def record_character_ownership(sender, instance, created, *args, **kwargs):
CharacterOwnership.objects.filter(character__character_id=instance.character_id).exclude(query).delete() CharacterOwnership.objects.filter(character__character_id=instance.character_id).exclude(query).delete()
# create character if needed # create character if needed
if EveCharacter.objects.filter(character_id=instance.character_id).exists() is False: if EveCharacter.objects.filter(character_id=instance.character_id).exists() is False:
logger.debug('Token is for a new character. Creating model for {0} ({1})'.format(instance.character_name, logger.debug(f'Token is for a new character. Creating model for {instance.character_name} ({instance.character_id})')
instance.character_id))
EveCharacter.objects.create_character(instance.character_id) EveCharacter.objects.create_character(instance.character_id)
char = EveCharacter.objects.get(character_id=instance.character_id) char = EveCharacter.objects.get(character_id=instance.character_id)
# check if we need to create ownership # check if we need to create ownership
if instance.user and not CharacterOwnership.objects.filter( if instance.user and not CharacterOwnership.objects.filter(
character__character_id=instance.character_id).exists(): character__character_id=instance.character_id).exists():
logger.debug("Character {0} is not yet owned. Assigning ownership to {1}".format(instance.character_name, logger.debug(f"Character {instance.character_name} is not yet owned. Assigning ownership to {instance.user}")
instance.user)) CharacterOwnership.objects.update_or_create(character=char, defaults={'owner_hash': instance.character_owner_hash, 'user': instance.user})
CharacterOwnership.objects.update_or_create(character=char,
defaults={'owner_hash': instance.character_owner_hash,
'user': instance.user})
@receiver(pre_delete, sender=CharacterOwnership) @receiver(pre_delete, sender=CharacterOwnership)
def validate_main_character(sender, instance, *args, **kwargs): def validate_main_character(sender, instance, *args, **kwargs):
try: try:
if instance.user.profile.main_character == instance.character: if instance.user.profile.main_character == instance.character:
logger.info("Ownership of a main character {0} has been revoked. Resetting {1} main character.".format( logger.info("Ownership of a main character {} has been revoked. Resetting {} main character.".format(
instance.character, instance.user)) instance.character, instance.user))
# clear main character as user no longer owns them # clear main character as user no longer owns them
instance.user.profile.main_character = None instance.user.profile.main_character = None
@@ -116,7 +116,7 @@ def validate_main_character(sender, instance, *args, **kwargs):
@receiver(post_delete, sender=Token) @receiver(post_delete, sender=Token)
def validate_ownership(sender, instance, *args, **kwargs): def validate_ownership(sender, instance, *args, **kwargs):
if not Token.objects.filter(character_owner_hash=instance.character_owner_hash).filter(refresh_token__isnull=False).exists(): if not Token.objects.filter(character_owner_hash=instance.character_owner_hash).filter(refresh_token__isnull=False).exists():
logger.info("No remaining tokens to validate ownership of character {0}. Revoking ownership.".format(instance.character_name)) logger.info(f"No remaining tokens to validate ownership of character {instance.character_name}. Revoking ownership.")
CharacterOwnership.objects.filter(owner_hash=instance.character_owner_hash).delete() CharacterOwnership.objects.filter(owner_hash=instance.character_owner_hash).delete()
@@ -127,11 +127,11 @@ def assign_state_on_active_change(sender, instance, *args, **kwargs):
old_instance = User.objects.get(pk=instance.pk) old_instance = User.objects.get(pk=instance.pk)
if old_instance.is_active != instance.is_active: if old_instance.is_active != instance.is_active:
if instance.is_active: if instance.is_active:
logger.debug("User {0} has been activated. Assigning state.".format(instance)) logger.debug(f"User {instance} has been activated. Assigning state.")
instance.profile.assign_state() instance.profile.assign_state()
else: else:
logger.debug( logger.debug(
"User {0} has been deactivated. Revoking state and assigning to guest state.".format(instance)) f"User {instance} has been deactivated. Revoking state and assigning to guest state.")
instance.profile.state = get_guest_state() instance.profile.state = get_guest_state()
instance.profile.save(update_fields=['state']) instance.profile.save(update_fields=['state'])
@@ -140,10 +140,10 @@ def assign_state_on_active_change(sender, instance, *args, **kwargs):
def check_state_on_character_update(sender, instance, *args, **kwargs): def check_state_on_character_update(sender, instance, *args, **kwargs):
# if this is a main character updating, check that user's state # if this is a main character updating, check that user's state
try: try:
logger.debug("Character {0} has been saved. Assessing owner's state for changes.".format(instance)) logger.debug(f"Character {instance} has been saved. Assessing owner's state for changes.")
instance.userprofile.assign_state() instance.userprofile.assign_state()
except UserProfile.DoesNotExist: except UserProfile.DoesNotExist:
logger.debug("Character {0} is not a main character. No state assessment required.".format(instance)) logger.debug(f"Character {instance} is not a main character. No state assessment required.")
pass pass
@@ -153,7 +153,7 @@ def ownership_record_creation(sender, instance, created, *args, **kwargs):
records = OwnershipRecord.objects.filter(owner_hash=instance.owner_hash).filter(character=instance.character) records = OwnershipRecord.objects.filter(owner_hash=instance.owner_hash).filter(character=instance.character)
if records.exists(): if records.exists():
if records[0].user == instance.user: # most recent record is sorted first if records[0].user == instance.user: # most recent record is sorted first
logger.debug("Already have ownership record of {0} by user {1}".format(instance.character, instance.user)) logger.debug(f"Already have ownership record of {instance.character} by user {instance.user}")
return return
logger.info("Character {0} has a new owner {1}. Creating ownership record.".format(instance.character, instance.user)) logger.info(f"Character {instance.character} has a new owner {instance.user}. Creating ownership record.")
OwnershipRecord.objects.create(user=instance.user, character=instance.character, owner_hash=instance.owner_hash) OwnershipRecord.objects.create(user=instance.user, character=instance.character, owner_hash=instance.owner_hash)

View File

@@ -1,12 +1,12 @@
/* /*
CSS for allianceauth admin site CSS for allianceauth admin site
*/ */
/* styling for profile pic */ /* styling for profile pic */
.img-circle { .img-circle {
border-radius: 50%; border-radius: 50%;
} }
.column-user_profile_pic { .column-user_profile_pic {
width: 1px; width: 1px;
white-space: nowrap; white-space: nowrap;
} }
@@ -26,4 +26,4 @@ CSS for allianceauth admin site
color: black ; color: black ;
background-color: rgb(255, 255, 204) ; background-color: rgb(255, 255, 204) ;
z-index: 1 ; z-index: 1 ;
} }

View File

@@ -0,0 +1,40 @@
from collections import namedtuple
import datetime as dt
from .event_series import EventSeries
"""Global series for counting task events."""
succeeded_tasks = EventSeries("SUCCEEDED_TASKS")
retried_tasks = EventSeries("RETRIED_TASKS")
failed_tasks = EventSeries("FAILED_TASKS")
_TaskCounts = namedtuple(
"_TaskCounts", ["succeeded", "retried", "failed", "total", "earliest_task", "hours"]
)
def dashboard_results(hours: int) -> _TaskCounts:
"""Counts of all task events within the given timeframe."""
def earliest_if_exists(events: EventSeries, earliest: dt.datetime) -> list:
my_earliest = events.first_event(earliest=earliest)
return [my_earliest] if my_earliest else []
earliest = dt.datetime.utcnow() - dt.timedelta(hours=hours)
earliest_events = list()
succeeded_count = succeeded_tasks.count(earliest=earliest)
earliest_events += earliest_if_exists(succeeded_tasks, earliest)
retried_count = retried_tasks.count(earliest=earliest)
earliest_events += earliest_if_exists(retried_tasks, earliest)
failed_count = failed_tasks.count(earliest=earliest)
earliest_events += earliest_if_exists(failed_tasks, earliest)
return _TaskCounts(
succeeded=succeeded_count,
retried=retried_count,
failed=failed_count,
total=succeeded_count + retried_count + failed_count,
earliest_task=min(earliest_events) if earliest_events else None,
hours=hours,
)

View File

@@ -0,0 +1,95 @@
import datetime as dt
from typing import Optional, List
from redis import Redis
from pytz import utc
from django.core.cache import cache
class EventSeries:
"""API for recording and analysing a series of events."""
_ROOT_KEY = "ALLIANCEAUTH_EVENT_SERIES"
def __init__(self, key_id: str, redis: Redis = None) -> None:
self._redis = cache.get_master_client() if not redis else redis
if not isinstance(self._redis, Redis):
raise TypeError(
"This class requires a Redis client, but none was provided "
"and the default Django cache backend is not Redis either."
)
self._key_id = str(key_id)
self.clear()
@property
def _key_counter(self):
return f"{self._ROOT_KEY}_{self._key_id}_COUNTER"
@property
def _key_sorted_set(self):
return f"{self._ROOT_KEY}_{self._key_id}_SORTED_SET"
def add(self, event_time: dt.datetime = None) -> None:
"""Add event.
Args:
- event_time: timestamp of event. Will use current time if not specified.
"""
if not event_time:
event_time = dt.datetime.utcnow()
id = self._redis.incr(self._key_counter)
self._redis.zadd(self._key_sorted_set, {id: event_time.timestamp()})
def all(self) -> List[dt.datetime]:
"""List of all known events."""
return [
event[1]
for event in self._redis.zrangebyscore(
self._key_sorted_set,
"-inf",
"+inf",
withscores=True,
score_cast_func=self._cast_scores_to_dt,
)
]
def clear(self) -> None:
"""Clear all events."""
self._redis.delete(self._key_sorted_set)
self._redis.delete(self._key_counter)
def count(self, earliest: dt.datetime = None, latest: dt.datetime = None) -> int:
"""Count of events, can be restricted to given timeframe.
Args:
- earliest: Date of first events to count(inclusive), or -infinite if not specified
- latest: Date of last events to count(inclusive), or +infinite if not specified
"""
min = "-inf" if not earliest else earliest.timestamp()
max = "+inf" if not latest else latest.timestamp()
return self._redis.zcount(self._key_sorted_set, min=min, max=max)
def first_event(self, earliest: dt.datetime = None) -> Optional[dt.datetime]:
"""Date/Time of first event. Returns `None` if series has no events.
Args:
- earliest: Date of first events to count(inclusive), or any if not specified
"""
min = "-inf" if not earliest else earliest.timestamp()
event = self._redis.zrangebyscore(
self._key_sorted_set,
min,
"+inf",
withscores=True,
start=0,
num=1,
score_cast_func=self._cast_scores_to_dt,
)
if not event:
return None
return event[0][1]
@staticmethod
def _cast_scores_to_dt(score) -> dt.datetime:
return dt.datetime.fromtimestamp(float(score), tz=utc)

View File

@@ -0,0 +1,54 @@
from celery.signals import (
task_failure,
task_internal_error,
task_retry,
task_success,
worker_ready
)
from django.conf import settings
from .counters import failed_tasks, retried_tasks, succeeded_tasks
def reset_counters():
"""Reset all counters for the celery status."""
succeeded_tasks.clear()
failed_tasks.clear()
retried_tasks.clear()
def is_enabled() -> bool:
return not bool(
getattr(settings, "ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED", False)
)
@worker_ready.connect
def reset_counters_when_celery_restarted(*args, **kwargs):
if is_enabled():
reset_counters()
@task_success.connect
def record_task_succeeded(*args, **kwargs):
if is_enabled():
succeeded_tasks.add()
@task_retry.connect
def record_task_retried(*args, **kwargs):
if is_enabled():
retried_tasks.add()
@task_failure.connect
def record_task_failed(*args, **kwargs):
if is_enabled():
failed_tasks.add()
@task_internal_error.connect
def record_task_internal_error(*args, **kwargs):
if is_enabled():
failed_tasks.add()

View File

@@ -0,0 +1,51 @@
import datetime as dt
from django.test import TestCase
from django.utils.timezone import now
from allianceauth.authentication.task_statistics.counters import (
dashboard_results,
succeeded_tasks,
retried_tasks,
failed_tasks,
)
class TestDashboardResults(TestCase):
def test_should_return_counts_for_given_timeframe_only(self):
# given
earliest_task = now() - dt.timedelta(minutes=15)
succeeded_tasks.clear()
succeeded_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
succeeded_tasks.add(earliest_task)
succeeded_tasks.add()
succeeded_tasks.add()
retried_tasks.clear()
retried_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
retried_tasks.add(now() - dt.timedelta(seconds=30))
retried_tasks.add()
failed_tasks.clear()
failed_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
failed_tasks.add()
# when
results = dashboard_results(hours=1)
# then
self.assertEqual(results.succeeded, 3)
self.assertEqual(results.retried, 2)
self.assertEqual(results.failed, 1)
self.assertEqual(results.total, 6)
self.assertEqual(results.earliest_task, earliest_task)
def test_should_work_with_no_data(self):
# given
succeeded_tasks.clear()
retried_tasks.clear()
failed_tasks.clear()
# when
results = dashboard_results(hours=1)
# then
self.assertEqual(results.succeeded, 0)
self.assertEqual(results.retried, 0)
self.assertEqual(results.failed, 0)
self.assertEqual(results.total, 0)
self.assertIsNone(results.earliest_task)

View File

@@ -0,0 +1,133 @@
import datetime as dt
from pytz import utc
from django.test import TestCase
from django.utils.timezone import now
from allianceauth.authentication.task_statistics.event_series import EventSeries
class TestEventSeries(TestCase):
def test_should_add_event(self):
# given
events = EventSeries("dummy")
# when
events.add()
# then
result = events.all()
self.assertEqual(len(result), 1)
self.assertAlmostEqual(result[0], now(), delta=dt.timedelta(seconds=30))
def test_should_add_event_with_specified_time(self):
# given
events = EventSeries("dummy")
my_time = dt.datetime(2021, 11, 1, 12, 15, tzinfo=utc)
# when
events.add(my_time)
# then
result = events.all()
self.assertEqual(len(result), 1)
self.assertAlmostEqual(result[0], my_time, delta=dt.timedelta(seconds=30))
def test_should_count_events(self):
# given
events = EventSeries("dummy")
events.add()
events.add()
# when
result = events.count()
# then
self.assertEqual(result, 2)
def test_should_count_zero(self):
# given
events = EventSeries("dummy")
# when
result = events.count()
# then
self.assertEqual(result, 0)
def test_should_count_events_within_timeframe_1(self):
# given
events = EventSeries("dummy")
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 30, tzinfo=utc))
# when
result = events.count(
earliest=dt.datetime(2021, 12, 1, 12, 8, tzinfo=utc),
latest=dt.datetime(2021, 12, 1, 12, 17, tzinfo=utc),
)
# then
self.assertEqual(result, 2)
def test_should_count_events_within_timeframe_2(self):
# given
events = EventSeries("dummy")
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 30, tzinfo=utc))
# when
result = events.count(earliest=dt.datetime(2021, 12, 1, 12, 8))
# then
self.assertEqual(result, 3)
def test_should_count_events_within_timeframe_3(self):
# given
events = EventSeries("dummy")
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 30, tzinfo=utc))
# when
result = events.count(latest=dt.datetime(2021, 12, 1, 12, 12))
# then
self.assertEqual(result, 2)
def test_should_clear_events(self):
# given
events = EventSeries("dummy")
events.add()
events.add()
# when
events.clear()
# then
self.assertEqual(events.count(), 0)
def test_should_return_date_of_first_event(self):
# given
events = EventSeries("dummy")
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 30, tzinfo=utc))
# when
result = events.first_event()
# then
self.assertEqual(result, dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
def test_should_return_date_of_first_event_with_range(self):
# given
events = EventSeries("dummy")
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 30, tzinfo=utc))
# when
result = events.first_event(
earliest=dt.datetime(2021, 12, 1, 12, 8, tzinfo=utc)
)
# then
self.assertEqual(result, dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
def test_should_return_all_events(self):
# given
events = EventSeries("dummy")
events.add()
events.add()
# when
results = events.all()
# then
self.assertEqual(len(results), 2)

View File

@@ -0,0 +1,93 @@
from unittest.mock import patch
from celery.exceptions import Retry
from django.test import TestCase, override_settings
from allianceauth.authentication.task_statistics.counters import (
failed_tasks,
retried_tasks,
succeeded_tasks,
)
from allianceauth.authentication.task_statistics.signals import (
reset_counters,
is_enabled,
)
from allianceauth.eveonline.tasks import update_character
@override_settings(
CELERY_ALWAYS_EAGER=True,ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED=False
)
class TestTaskSignals(TestCase):
fixtures = ["disable_analytics"]
def test_should_record_successful_task(self):
# given
succeeded_tasks.clear()
retried_tasks.clear()
failed_tasks.clear()
# when
with patch(
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
) as mock_update:
mock_update.return_value = None
update_character.delay(1)
# then
self.assertEqual(succeeded_tasks.count(), 1)
self.assertEqual(retried_tasks.count(), 0)
self.assertEqual(failed_tasks.count(), 0)
def test_should_record_retried_task(self):
# given
succeeded_tasks.clear()
retried_tasks.clear()
failed_tasks.clear()
# when
with patch(
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
) as mock_update:
mock_update.side_effect = Retry
update_character.delay(1)
# then
self.assertEqual(succeeded_tasks.count(), 0)
self.assertEqual(failed_tasks.count(), 0)
self.assertEqual(retried_tasks.count(), 1)
def test_should_record_failed_task(self):
# given
succeeded_tasks.clear()
retried_tasks.clear()
failed_tasks.clear()
# when
with patch(
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
) as mock_update:
mock_update.side_effect = RuntimeError
update_character.delay(1)
# then
self.assertEqual(succeeded_tasks.count(), 0)
self.assertEqual(retried_tasks.count(), 0)
self.assertEqual(failed_tasks.count(), 1)
def test_should_reset_counters(self):
# given
succeeded_tasks.add()
retried_tasks.add()
failed_tasks.add()
# when
reset_counters()
# then
self.assertEqual(succeeded_tasks.count(), 0)
self.assertEqual(retried_tasks.count(), 0)
self.assertEqual(failed_tasks.count(), 0)
class TestIsEnabled(TestCase):
@override_settings(ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED=False)
def test_enabled(self):
self.assertTrue(is_enabled())
@override_settings(ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED=True)
def test_disabled(self):
self.assertFalse(is_enabled())

View File

@@ -22,13 +22,13 @@ def check_character_ownership(owner_hash):
continue continue
except (KeyError, IncompleteResponseError): except (KeyError, IncompleteResponseError):
# We can't validate the hash hasn't changed but also can't assume it has. Abort for now. # We can't validate the hash hasn't changed but also can't assume it has. Abort for now.
logger.warning("Failed to validate owner hash of {0} due to problems contacting SSO servers.".format( logger.warning("Failed to validate owner hash of {} due to problems contacting SSO servers.".format(
tokens[0].character_name)) tokens[0].character_name))
break break
if not t.character_owner_hash == old_hash: if not t.character_owner_hash == old_hash:
logger.info( logger.info(
'Character %s has changed ownership. Revoking %s tokens.' % (t.character_name, tokens.count())) f'Character {t.character_name} has changed ownership. Revoking {tokens.count()} tokens.')
tokens.delete() tokens.delete()
break break

View File

@@ -1,11 +1,11 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load staticfiles %} {% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% trans "Dashboard" %}{% endblock %} {% block page_title %}{% translate "Dashboard" %}{% endblock %}
{% block content %} {% block content %}
<h1 class="page-header text-center">{% trans "Dashboard" %}</h1> <h1 class="page-header text-center">{% translate "Dashboard" %}</h1>
{% if user.is_staff %} {% if user.is_staff %}
{% include 'allianceauth/admin-status/include.html' %} {% include 'allianceauth/admin-status/include.html' %}
{% endif %} {% endif %}
@@ -60,36 +60,57 @@
<td class="text-center">{{ main.alliance_name }}</td> <td class="text-center">{{ main.alliance_name }}</td>
<tr> <tr>
</table> </table>
{% elif main.faction_id %}
<table class="table">
<tr>
<td class="text-center">
<img class="ra-avatar"src="{{ main.faction_logo_url_128 }}">
</td>
</tr>
<tr>
<td class="text-center">{{ main.faction_name }}</td>
<tr>
</table>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="table visible-xs-block"> <div class="table visible-xs-block">
<p> <p>
<img class="ra-avatar" src="{{ main.portrait_url_64 }}"> <img class="ra-avatar" src="{{ main.portrait_url_64 }}">
<img class="ra-avatar" src="{{ main.corporation_logo_url_64 }}"> <img class="ra-avatar" src="{{ main.corporation_logo_url_64 }}">
<img class="ra-avatar" src="{{ main.alliance_logo_url_64 }}"> {% if main.alliance_id %}
<img class="ra-avatar" src="{{ main.alliance_logo_url_64 }}">
{% endif %}
{% if main.faction_id %}
<img class="ra-avatar" src="{{ main.faction_logo_url_64 }}">
{% endif %}
</p> </p>
<p> <p>
<strong>{{ main.character_name }}</strong><br> <strong>{{ main.character_name }}</strong><br>
{{ main.corporation_name }}<br> {{ main.corporation_name }}<br>
{{ main.alliance_name }} {% if main.alliance_id %}
{{ main.alliance_name }}<br>
{% endif %}
{% if main.faction_id %}
{{ main.faction_name }}
{% endif %}
</p> </p>
</div> </div>
{% endwith %} {% endwith %}
{% else %} {% else %}
<div class="alert alert-danger" role="alert"> <div class="alert alert-danger" role="alert">
{% trans "No main character set." %} {% translate "No main character set." %}
</div> </div>
{% endif %} {% endif %}
<div class="clearfix"></div> <div class="clearfix"></div>
<div class="row"> <div class="row">
<div class="col-sm-6 button-wrapper"> <div class="col-sm-6 button-wrapper">
<a href="{% url 'authentication:add_character' %}" class="btn btn-block btn-info" <a href="{% url 'authentication:add_character' %}" class="btn btn-block btn-info"
title="Add Character">{% trans 'Add Character' %}</a> title="Add Character">{% translate 'Add Character' %}</a>
</div> </div>
<div class="col-sm-6 button-wrapper"> <div class="col-sm-6 button-wrapper">
<a href="{% url 'authentication:change_main_character' %}" class="btn btn-block btn-info" <a href="{% url 'authentication:change_main_character' %}" class="btn btn-block btn-info"
title="Change Main Character">{% trans "Change Main" %}</a> title="Change Main Character">{% translate "Change Main" %}</a>
</div> </div>
</div> </div>
</div> </div>
@@ -98,7 +119,7 @@
<div class="col-sm-6 text-center"> <div class="col-sm-6 text-center">
<div class="panel panel-success" style="height:100%"> <div class="panel panel-success" style="height:100%">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title">{% trans "Group Memberships" %}</h3> <h3 class="panel-title">{% translate "Group Memberships" %}</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div style="height: 240px;overflow:-moz-scrollbars-vertical;overflow-y:auto;"> <div style="height: 240px;overflow:-moz-scrollbars-vertical;overflow-y:auto;">
@@ -118,34 +139,34 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title text-center" style="text-align: center"> <h3 class="panel-title text-center" style="text-align: center">
{% trans 'Characters' %} {% translate 'Characters' %}
</h3> </h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<table class="table table-aa hidden-xs"> <table class="table table-aa hidden-xs">
<thead> <thead>
<tr> <tr>
<th class="text-center"></th> <th class="text-center"></th>
<th class="text-center">{% trans 'Name' %}</th> <th class="text-center">{% translate 'Name' %}</th>
<th class="text-center">{% trans 'Corp' %}</th> <th class="text-center">{% translate 'Corp' %}</th>
<th class="text-center">{% trans 'Alliance' %}</th> <th class="text-center">{% translate 'Alliance' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for char in characters %} {% for char in characters %}
<tr> <tr>
<td class="text-center"><img class="ra-avatar img-circle" src="{{ char.portrait_url_32 }}"> <td class="text-center"><img class="ra-avatar img-circle" src="{{ char.portrait_url_32 }}">
</td> </td>
<td class="text-center">{{ 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.corporation_name }}</td>
<td class="text-center">{{ char.alliance_name }}</td> <td class="text-center">{{ char.alliance_name }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<table class="table table-aa visible-xs-block" style="width: 100%"> <table class="table table-aa visible-xs-block" style="width: 100%">
<tbody> <tbody>
{% for char in characters %} {% for char in characters %}
<tr> <tr>
<td class="text-center" style="vertical-align: middle"> <td class="text-center" style="vertical-align: middle">
<img class="ra-avatar img-circle" src="{{ char.portrait_url_32 }}"> <img class="ra-avatar img-circle" src="{{ char.portrait_url_32 }}">
@@ -154,7 +175,7 @@
<strong>{{ char.character_name }}</strong><br> <strong>{{ char.character_name }}</strong><br>
{{ char.corporation_name }}<br> {{ char.corporation_name }}<br>
{{ char.alliance_name|default:"" }} {{ char.alliance_name|default:"" }}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@@ -12,7 +12,7 @@
{% include 'allianceauth/icons.html' %} {% include 'allianceauth/icons.html' %}
<title>{% block title %}{{ SITE_NAME }}{% endblock %}</title> <title>{% block title %}{% block page_title %}{% endblock page_title %} - {{ SITE_NAME }}{% endblock title %}</title>
{% include 'bundles/bootstrap-css.html' %} {% include 'bundles/bootstrap-css.html' %}
{% include 'bundles/fontawesome.html' %} {% include 'bundles/fontawesome.html' %}

View File

@@ -1,8 +1,12 @@
{% extends 'public/middle_box.html' %} {% extends 'public/middle_box.html' %}
{% load i18n %}
{% load static %} {% load static %}
{% block page_title %}Login{% endblock %}
{% block middle_box_content %} {% block page_title %}{% translate "Login" %}{% endblock %}
{% block middle_box_content %}
<a href="{% url 'auth_sso_login' %}{% if request.GET.next %}?next={{request.GET.next}}{%endif%}"> <a href="{% url 'auth_sso_login' %}{% if request.GET.next %}?next={{request.GET.next}}{%endif%}">
<img class="img-responsive center-block" src="{% static 'img/sso/EVE_SSO_Login_Buttons_Large_Black.png' %}" border=0> <img class="img-responsive center-block" src="{% static 'img/sso/EVE_SSO_Login_Buttons_Large_Black.png' %}" border=0>
</a> </a>
{% endblock %} {% endblock %}

View File

@@ -1,6 +1,5 @@
{% extends 'public/base.html' %} {% extends 'public/base.html' %}
{% load static %} {% load static %}
{% block title %}Login{% endblock %}
{% block content %} {% block content %}
<div class="col-md-4 col-md-offset-4"> <div class="col-md-4 col-md-offset-4">
{% if messages %} {% if messages %}
@@ -21,4 +20,4 @@
{% endblock %} {% endblock %}
{% block extra_include %} {% block extra_include %}
{% include 'bundles/bootstrap-js.html' %} {% include 'bundles/bootstrap-js.html' %}
{% endblock %} {% endblock %}

View File

@@ -1,13 +1,17 @@
{% load staticfiles %} {% extends 'public/base.html' %}
{% load static %}
{% load bootstrap %} {% load bootstrap %}
{% load i18n %} {% load i18n %}
{% extends 'public/base.html' %}
{% block page_title %}Registration{% endblock %} {% block page_title %}{% translate "Registration" %}{% endblock %}
{% block extra_include %} {% block extra_include %}
{% include 'bundles/bootstrap-css.html' %} {% include 'bundles/bootstrap-css.html' %}
{% include 'bundles/fontawesome.html' %} {% include 'bundles/fontawesome.html' %}
{% include 'bundles/bootstrap-js.html' %} {% include 'bundles/bootstrap-js.html' %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="col-md-4 col-md-offset-4"> <div class="col-md-4 col-md-offset-4">
<div class="panel panel-default panel-transparent"> <div class="panel panel-default panel-transparent">
@@ -15,7 +19,7 @@
<form method="POST"> <form method="POST">
{% csrf_token %} {% csrf_token %}
{{ form|bootstrap }} {{ form|bootstrap }}
<button class="btn btn-lg btn-primary btn-block" type="submit">{% trans "Register" %}</button> <button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Register" %}</button>
</form> </form>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
{% extends 'public/middle_box.html' %} {% extends 'public/middle_box.html' %}
{% load i18n %} {% load i18n %}
{% block middle_box_content %} {% block middle_box_content %}
<div class="alert alert-danger">{% trans 'Invalid or expired activation link.' %}</div> <div class="alert alert-danger">{% translate 'Invalid or expired activation link.' %}</div>
{% endblock %} {% endblock %}

View File

@@ -2,12 +2,8 @@ You're receiving this email because someone has entered this email address while
If this was you, please click on the link below to confirm your email address: If this was you, please click on the link below to confirm your email address:
<a href="{{ scheme }}://{{ url }}">Confirm email address</a>
Link not working? Try copy/pasting this URL into your browser:
{{ scheme }}://{{ url }} {{ scheme }}://{{ url }}
This link will expire in {{ expiration_days }} day(s). This link will expire in {{ expiration_days }} day(s).
If this was not you, it is safe to ignore this email. If this was not you, it is safe to ignore this email.

View File

@@ -0,0 +1,19 @@
<p>
You're receiving this email because someone has entered this email address while registering for an account on {{ site.domain }}
</p>
<p>
If this was you, please click on the link below to confirm your email address:
<p>
<p>
<a href="{{ scheme }}://{{ url }}">Confirm email address</a>
</p>
<p>
This link will expire in {{ expiration_days }} day(s).
</p>
<p>
If this was not you, it is safe to ignore this email.
</p>

View File

@@ -1 +1 @@
Confirm your Alliance Auth account email address Confirm your Alliance Auth account email address

View File

@@ -2,13 +2,13 @@
{% blocktrans trimmed %}You're receiving this email because you requested a password reset for your {% blocktrans trimmed %}You're receiving this email because you requested a password reset for your
user account.{% endblocktrans %} user account.{% endblocktrans %}
{% trans "Please go to the following page and choose a new password:" %} {% translate "Please go to the following page and choose a new password:" %}
{% block reset_link %} {% block reset_link %}
{{domain}}{% url 'password_reset_confirm' uidb64=uid token=token %} {{domain}}{% url 'password_reset_confirm' uidb64=uid token=token %}
{% endblock %} {% endblock %}
{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }} {% translate "Your username, in case you've forgotten:" %} {{ user.get_username }}
{% trans "Thanks for using our site!" %} {% translate "Thanks for using our site!" %}
{% blocktrans %}Your IT Team{% endblocktrans %} {% blocktrans %}Your IT Team{% endblocktrans %}

View File

@@ -2,13 +2,13 @@
{% load bootstrap %} {% load bootstrap %}
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
{% block page_title %}Register{% endblock %} {% block page_title %}{% translate "Register" %}{% endblock %}
{% block middle_box_content %} {% block middle_box_content %}
<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">{% trans "Submit" %}</button> <button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Submit" %}</button>
<br/> <br/>
</form> </form>
{% endblock %} {% endblock %}

View File

@@ -10,9 +10,10 @@ def get_admin_change_view_url(obj: object) -> str:
args=(obj.pk,) args=(obj.pk,)
) )
def get_admin_search_url(ModelClass: type) -> str: def get_admin_search_url(ModelClass: type) -> str:
"""returns URL to search URL for model of given object""" """returns URL to search URL for model of given object"""
return '{}{}/'.format( return '{}{}/'.format(
reverse('admin:app_list', args=(ModelClass._meta.app_label,)), reverse('admin:app_list', args=(ModelClass._meta.app_label,)),
ModelClass.__name__.lower() ModelClass.__name__.lower()
) )

View File

@@ -1,17 +1,16 @@
from bs4 import BeautifulSoup
from urllib.parse import quote from urllib.parse import quote
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
from django.conf import settings
from django.contrib import admin
from django.contrib.admin.sites import AdminSite from django.contrib.admin.sites import AdminSite
from django.contrib.auth.models import User as BaseUser, Group from django.contrib.auth.models import Group
from django.test import TestCase, RequestFactory, Client from django.test import TestCase, RequestFactory, Client
from allianceauth.authentication.models import ( from allianceauth.authentication.models import (
CharacterOwnership, State, OwnershipRecord CharacterOwnership, State, OwnershipRecord
) )
from allianceauth.eveonline.models import ( from allianceauth.eveonline.models import (
EveCharacter, EveCorporationInfo, EveAllianceInfo EveCharacter, EveCorporationInfo, EveAllianceInfo, EveFactionInfo
) )
from allianceauth.services.hooks import ServicesHook from allianceauth.services.hooks import ServicesHook
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
@@ -19,15 +18,15 @@ from allianceauth.tests.auth_utils import AuthUtils
from ..admin import ( from ..admin import (
BaseUserAdmin, BaseUserAdmin,
CharacterOwnershipAdmin, CharacterOwnershipAdmin,
PermissionAdmin,
StateAdmin, StateAdmin,
MainCorporationsFilter, MainCorporationsFilter,
MainAllianceFilter, MainAllianceFilter,
MainFactionFilter,
OwnershipRecordAdmin, OwnershipRecordAdmin,
User, User,
UserAdmin, UserAdmin,
user_main_organization, user_main_organization,
user_profile_pic, user_profile_pic,
user_username, user_username,
update_main_character_model, update_main_character_model,
make_service_hooks_update_groups_action, make_service_hooks_update_groups_action,
@@ -35,19 +34,15 @@ from ..admin import (
) )
from . import get_admin_change_view_url, get_admin_search_url from . import get_admin_change_view_url, get_admin_search_url
if 'allianceauth.eveonline.autogroups' in settings.INSTALLED_APPS:
_has_auto_groups = True
from allianceauth.eveonline.autogroups.models import AutogroupsConfig
else:
_has_auto_groups = False
MODULE_PATH = 'allianceauth.authentication.admin' MODULE_PATH = 'allianceauth.authentication.admin'
class MockRequest(object): class MockRequest:
def __init__(self, user=None): def __init__(self, user=None):
self.user = user self.user = user
class TestCaseWithTestData(TestCase): class TestCaseWithTestData(TestCase):
@classmethod @classmethod
@@ -58,7 +53,7 @@ class TestCaseWithTestData(TestCase):
EveAllianceInfo, EveCorporationInfo, EveCharacter, Group, User EveAllianceInfo, EveCorporationInfo, EveCharacter, Group, User
]: ]:
MyModel.objects.all().delete() MyModel.objects.all().delete()
# groups # groups
cls.group_1 = Group.objects.create( cls.group_1 = Group.objects.create(
name='Group 1' name='Group 1'
@@ -91,16 +86,16 @@ class TestCaseWithTestData(TestCase):
alliance = EveAllianceInfo.objects.create( alliance = EveAllianceInfo.objects.create(
alliance_id=3001, alliance_id=3001,
alliance_name='Wayne Enterprises', alliance_name='Wayne Enterprises',
alliance_ticker='WE', alliance_ticker='WE',
executor_corp_id=2001 executor_corp_id=2001
) )
EveCorporationInfo.objects.create( EveCorporationInfo.objects.create(
corporation_id=2001, corporation_id=2001,
corporation_name='Wayne Technologies', corporation_name='Wayne Technologies',
corporation_ticker='WT', corporation_ticker='WT',
member_count=42, member_count=42,
alliance=alliance alliance=alliance
) )
cls.user_1 = User.objects.create_user( cls.user_1 = User.objects.create_user(
character_1.character_name.replace(' ', '_'), character_1.character_name.replace(' ', '_'),
'abc@example.com', 'abc@example.com',
@@ -118,7 +113,7 @@ class TestCaseWithTestData(TestCase):
) )
cls.user_1.profile.main_character = character_1 cls.user_1.profile.main_character = character_1
cls.user_1.profile.save() cls.user_1.profile.save()
cls.user_1.groups.add(cls.group_1) cls.user_1.groups.add(cls.group_1)
# user 2 - corp only, staff # user 2 - corp only, staff
character_2 = EveCharacter.objects.create( character_2 = EveCharacter.objects.create(
@@ -132,7 +127,7 @@ class TestCaseWithTestData(TestCase):
EveCorporationInfo.objects.create( EveCorporationInfo.objects.create(
corporation_id=2002, corporation_id=2002,
corporation_name='Daily Plane', corporation_name='Daily Plane',
corporation_ticker='DP', corporation_ticker='DP',
member_count=99, member_count=99,
alliance=None alliance=None
) )
@@ -151,7 +146,7 @@ class TestCaseWithTestData(TestCase):
cls.user_2.groups.add(cls.group_2) cls.user_2.groups.add(cls.group_2)
cls.user_2.is_staff = True cls.user_2.is_staff = True
cls.user_2.save() cls.user_2.save()
# user 3 - no main, no group, superuser # user 3 - no main, no group, superuser
character_3 = EveCharacter.objects.create( character_3 = EveCharacter.objects.create(
character_id=1101, character_id=1101,
@@ -164,7 +159,7 @@ class TestCaseWithTestData(TestCase):
EveCorporationInfo.objects.create( EveCorporationInfo.objects.create(
corporation_id=2101, corporation_id=2101,
corporation_name='Lex Corp', corporation_name='Lex Corp',
corporation_ticker='LC', corporation_ticker='LC',
member_count=666, member_count=666,
alliance=None alliance=None
) )
@@ -187,31 +182,57 @@ class TestCaseWithTestData(TestCase):
cls.user_3.is_superuser = True cls.user_3.is_superuser = True
cls.user_3.save() cls.user_3.save()
# user 4 - corp and faction, no alliance
cls.character_4 = EveCharacter.objects.create(
character_id=4321,
character_name='Professor X',
corporation_id=5432,
corporation_name="Xavier's School for Gifted Youngsters",
corporation_ticker='MUTNT',
alliance_id=None,
faction_id=999,
faction_name='The X-Men',
)
cls.user_4 = User.objects.create_user(
cls.character_4.character_name.replace(' ', '_'),
'abc@example.com',
'password'
)
CharacterOwnership.objects.create(
character=cls.character_4,
owner_hash='x1' + cls.character_4.character_name,
user=cls.user_4
)
cls.user_4.profile.main_character = cls.character_4
cls.user_4.profile.save()
EveFactionInfo.objects.create(faction_id=999, faction_name='The X-Men')
def make_generic_search_request(ModelClass: type, search_term: str): def make_generic_search_request(ModelClass: type, search_term: str):
User.objects.create_superuser( User.objects.create_superuser(
username='superuser', password='secret', email='admin@example.com' username='superuser', password='secret', email='admin@example.com'
) )
c = Client() c = Client()
c.login(username='superuser', password='secret') c.login(username='superuser', password='secret')
return c.get( return c.get(
'%s?q=%s' % (get_admin_search_url(ModelClass), quote(search_term)) f'{get_admin_search_url(ModelClass)}?q={quote(search_term)}'
) )
class TestCharacterOwnershipAdmin(TestCaseWithTestData): class TestCharacterOwnershipAdmin(TestCaseWithTestData):
fixtures = ["disable_analytics"]
def setUp(self): def setUp(self):
self.modeladmin = CharacterOwnershipAdmin( self.modeladmin = CharacterOwnershipAdmin(
model=User, admin_site=AdminSite() model=User, admin_site=AdminSite()
) )
def test_change_view_loads_normally(self): def test_change_view_loads_normally(self):
User.objects.create_superuser( User.objects.create_superuser(
username='superuser', password='secret', email='admin@example.com' username='superuser', password='secret', email='admin@example.com'
) )
c = Client() c = Client()
c.login(username='superuser', password='secret') c.login(username='superuser', password='secret')
ownership = self.user_1.character_ownerships.first() ownership = self.user_1.character_ownerships.first()
response = c.get(get_admin_change_view_url(ownership)) response = c.get(get_admin_change_view_url(ownership))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@@ -226,18 +247,19 @@ class TestCharacterOwnershipAdmin(TestCaseWithTestData):
class TestOwnershipRecordAdmin(TestCaseWithTestData): class TestOwnershipRecordAdmin(TestCaseWithTestData):
fixtures = ["disable_analytics"]
def setUp(self): def setUp(self):
self.modeladmin = OwnershipRecordAdmin( self.modeladmin = OwnershipRecordAdmin(
model=User, admin_site=AdminSite() model=User, admin_site=AdminSite()
) )
def test_change_view_loads_normally(self): def test_change_view_loads_normally(self):
User.objects.create_superuser( User.objects.create_superuser(
username='superuser', password='secret', email='admin@example.com' username='superuser', password='secret', email='admin@example.com'
) )
c = Client() c = Client()
c.login(username='superuser', password='secret') c.login(username='superuser', password='secret')
ownership_record = OwnershipRecord.objects\ ownership_record = OwnershipRecord.objects\
.filter(user=self.user_1)\ .filter(user=self.user_1)\
.first() .first()
@@ -252,23 +274,24 @@ class TestOwnershipRecordAdmin(TestCaseWithTestData):
class TestStateAdmin(TestCaseWithTestData): class TestStateAdmin(TestCaseWithTestData):
fixtures = ["disable_analytics"]
def setUp(self): def setUp(self):
self.modeladmin = StateAdmin( self.modeladmin = StateAdmin(
model=User, admin_site=AdminSite() model=User, admin_site=AdminSite()
) )
def test_change_view_loads_normally(self): def test_change_view_loads_normally(self):
User.objects.create_superuser( User.objects.create_superuser(
username='superuser', password='secret', email='admin@example.com' username='superuser', password='secret', email='admin@example.com'
) )
c = Client() c = Client()
c.login(username='superuser', password='secret') c.login(username='superuser', password='secret')
guest_state = AuthUtils.get_guest_state() guest_state = AuthUtils.get_guest_state()
response = c.get(get_admin_change_view_url(guest_state)) response = c.get(get_admin_change_view_url(guest_state))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
member_state = AuthUtils.get_member_state() member_state = AuthUtils.get_member_state()
response = c.get(get_admin_change_view_url(member_state)) response = c.get(get_admin_change_view_url(member_state))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@@ -279,7 +302,9 @@ class TestStateAdmin(TestCaseWithTestData):
expected = 200 expected = 200
self.assertEqual(response.status_code, expected) self.assertEqual(response.status_code, expected)
class TestUserAdmin(TestCaseWithTestData): class TestUserAdmin(TestCaseWithTestData):
fixtures = ["disable_analytics"]
def setUp(self): def setUp(self):
self.factory = RequestFactory() self.factory = RequestFactory()
@@ -288,23 +313,11 @@ class TestUserAdmin(TestCaseWithTestData):
) )
self.character_1 = self.user_1.character_ownerships.first().character self.character_1 = self.user_1.character_ownerships.first().character
def _create_autogroups(self):
"""create autogroups for corps and alliances"""
if _has_auto_groups:
autogroups_config = AutogroupsConfig(
corp_groups = True,
alliance_groups = True
)
autogroups_config.save()
for state in State.objects.all():
autogroups_config.states.add(state)
autogroups_config.update_corp_group_membership(self.user_1)
# column rendering
def test_user_profile_pic_u1(self): def test_user_profile_pic_u1(self):
expected = ('<img src="https://images.evetech.net/characters/1001/' expected = (
'portrait?size=32" class="img-circle">') '<img src="https://images.evetech.net/characters/1001/'
'portrait?size=32" class="img-circle">'
)
self.assertEqual(user_profile_pic(self.user_1), expected) self.assertEqual(user_profile_pic(self.user_1), expected)
def test_user_profile_pic_u3(self): def test_user_profile_pic_u3(self):
@@ -333,9 +346,13 @@ class TestUserAdmin(TestCaseWithTestData):
self.assertEqual(user_main_organization(self.user_2), expected) self.assertEqual(user_main_organization(self.user_2), expected)
def test_user_main_organization_u3(self): def test_user_main_organization_u3(self):
expected = None expected = ''
self.assertEqual(user_main_organization(self.user_3), expected) self.assertEqual(user_main_organization(self.user_3), expected)
def test_user_main_organization_u4(self):
expected = "Xavier's School for Gifted Youngsters<br>The X-Men"
self.assertEqual(user_main_organization(self.user_4), expected)
def test_characters_u1(self): def test_characters_u1(self):
expected = 'Batman, Bruce Wayne' expected = 'Batman, Bruce Wayne'
result = self.modeladmin._characters(self.user_1) result = self.modeladmin._characters(self.user_1)
@@ -350,38 +367,18 @@ class TestUserAdmin(TestCaseWithTestData):
expected = 'Lex Luthor' expected = 'Lex Luthor'
result = self.modeladmin._characters(self.user_3) result = self.modeladmin._characters(self.user_3)
self.assertEqual(result, expected) self.assertEqual(result, expected)
def test_groups_u1(self): def test_groups_u1(self):
self._create_autogroups()
expected = 'Group 1' expected = 'Group 1'
result = self.modeladmin._groups(self.user_1) result = self.modeladmin._groups(self.user_1)
self.assertEqual(result, expected) self.assertEqual(result, expected)
def test_groups_u2(self): def test_groups_u2(self):
self._create_autogroups()
expected = 'Group 2' expected = 'Group 2'
result = self.modeladmin._groups(self.user_2) result = self.modeladmin._groups(self.user_2)
self.assertEqual(result, expected) self.assertEqual(result, expected)
def test_groups_u3(self):
self._create_autogroups()
result = self.modeladmin._groups(self.user_3)
self.assertIsNone(result)
@patch(MODULE_PATH + '._has_auto_groups', False) def test_groups_u3(self):
def test_groups_u1_no_autogroups(self):
expected = 'Group 1'
result = self.modeladmin._groups(self.user_1)
self.assertEqual(result, expected)
@patch(MODULE_PATH + '._has_auto_groups', False)
def test_groups_u2_no_autogroups(self):
expected = 'Group 2'
result = self.modeladmin._groups(self.user_2)
self.assertEqual(result, expected)
@patch(MODULE_PATH + '._has_auto_groups', False)
def test_groups_u3_no_autogroups(self):
result = self.modeladmin._groups(self.user_3) result = self.modeladmin._groups(self.user_3)
self.assertIsNone(result) self.assertIsNone(result)
@@ -413,8 +410,10 @@ class TestUserAdmin(TestCaseWithTestData):
def test_list_2_html_w_tooltips_w_cutoff(self): def test_list_2_html_w_tooltips_w_cutoff(self):
items = ['one', 'two', 'three'] items = ['one', 'two', 'three']
expected = ('<span data-tooltip="one, two, three" ' expected = (
'class="tooltip">one, two, (...)</span>') '<span data-tooltip="one, two, three" '
'class="tooltip">one, two, (...)</span>'
)
result = self.modeladmin._list_2_html_w_tooltips(items, 2) result = self.modeladmin._list_2_html_w_tooltips(items, 2)
self.assertEqual(expected, result) self.assertEqual(expected, result)
@@ -423,10 +422,10 @@ class TestUserAdmin(TestCaseWithTestData):
expected = None expected = None
result = self.modeladmin._list_2_html_w_tooltips(items, 5) result = self.modeladmin._list_2_html_w_tooltips(items, 5)
self.assertEqual(expected, result) self.assertEqual(expected, result)
# actions # actions
@patch(MODULE_PATH + '.UserAdmin.message_user', auto_spec=True) @patch(MODULE_PATH + '.UserAdmin.message_user', auto_spec=True, unsafe=True)
@patch(MODULE_PATH + '.update_character') @patch(MODULE_PATH + '.update_character')
def test_action_update_main_character_model( def test_action_update_main_character_model(
self, mock_task, mock_message_user self, mock_task, mock_message_user
@@ -437,70 +436,14 @@ class TestUserAdmin(TestCaseWithTestData):
) )
self.assertEqual(mock_task.delay.call_count, 2) self.assertEqual(mock_task.delay.call_count, 2)
self.assertTrue(mock_message_user.called) self.assertTrue(mock_message_user.called)
# filters # filters
def test_filter_real_groups_with_autogroups(self):
class UserAdminTest(BaseUserAdmin):
list_filter = (UserAdmin.RealGroupsFilter,)
self._create_autogroups()
my_modeladmin = UserAdminTest(User, AdminSite())
# Make sure the lookups are correct
request = self.factory.get('/')
request.user = self.user_1
changelist = my_modeladmin.get_changelist_instance(request)
filters = changelist.get_filters(request)
filterspec = filters[0][0]
expected = [
(self.group_1.pk, self.group_1.name),
(self.group_2.pk, self.group_2.name),
]
self.assertEqual(filterspec.lookup_choices, expected)
# Make sure the correct queryset is returned
request = self.factory.get('/', {'group_id__exact': self.group_1.pk})
request.user = self.user_1
changelist = my_modeladmin.get_changelist_instance(request)
queryset = changelist.get_queryset(request)
expected = User.objects.filter(groups__in=[self.group_1])
self.assertSetEqual(set(queryset), set(expected))
@patch(MODULE_PATH + '._has_auto_groups', False)
def test_filter_real_groups_no_autogroups(self):
class UserAdminTest(BaseUserAdmin):
list_filter = (UserAdmin.RealGroupsFilter,)
my_modeladmin = UserAdminTest(User, AdminSite())
# Make sure the lookups are correct
request = self.factory.get('/')
request.user = self.user_1
changelist = my_modeladmin.get_changelist_instance(request)
filters = changelist.get_filters(request)
filterspec = filters[0][0]
expected = [
(self.group_1.pk, self.group_1.name),
(self.group_2.pk, self.group_2.name),
]
self.assertEqual(filterspec.lookup_choices, expected)
# Make sure the correct queryset is returned
request = self.factory.get('/', {'group_id__exact': self.group_1.pk})
request.user = self.user_1
changelist = my_modeladmin.get_changelist_instance(request)
queryset = changelist.get_queryset(request)
expected = User.objects.filter(groups__in=[self.group_1])
self.assertSetEqual(set(queryset), set(expected))
def test_filter_main_corporations(self): def test_filter_main_corporations(self):
class UserAdminTest(BaseUserAdmin): class UserAdminTest(BaseUserAdmin):
list_filter = (MainCorporationsFilter,) list_filter = (MainCorporationsFilter,)
my_modeladmin = UserAdminTest(User, AdminSite()) my_modeladmin = UserAdminTest(User, AdminSite())
# Make sure the lookups are correct # Make sure the lookups are correct
@@ -509,28 +452,29 @@ class TestUserAdmin(TestCaseWithTestData):
changelist = my_modeladmin.get_changelist_instance(request) changelist = my_modeladmin.get_changelist_instance(request)
filters = changelist.get_filters(request) filters = changelist.get_filters(request)
filterspec = filters[0][0] filterspec = filters[0][0]
expected = [ expected = [
(2002, 'Daily Planet'), (2002, 'Daily Planet'),
(2001, 'Wayne Technologies'), (2001, 'Wayne Technologies'),
(5432, "Xavier's School for Gifted Youngsters"),
] ]
self.assertEqual(filterspec.lookup_choices, expected) self.assertEqual(filterspec.lookup_choices, expected)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
request = self.factory.get( request = self.factory.get(
'/', '/',
{'main_corporation_id__exact': self.character_1.corporation_id} {'main_corporation_id__exact': self.character_1.corporation_id}
) )
request.user = self.user_1 request.user = self.user_1
changelist = my_modeladmin.get_changelist_instance(request) changelist = my_modeladmin.get_changelist_instance(request)
queryset = changelist.get_queryset(request) queryset = changelist.get_queryset(request)
expected = [self.user_1] expected = [self.user_1]
self.assertSetEqual(set(queryset), set(expected)) self.assertSetEqual(set(queryset), set(expected))
def test_filter_main_alliances(self): def test_filter_main_alliances(self):
class UserAdminTest(BaseUserAdmin): class UserAdminTest(BaseUserAdmin):
list_filter = (MainAllianceFilter,) list_filter = (MainAllianceFilter,)
my_modeladmin = UserAdminTest(User, AdminSite()) my_modeladmin = UserAdminTest(User, AdminSite())
# Make sure the lookups are correct # Make sure the lookups are correct
@@ -539,28 +483,56 @@ class TestUserAdmin(TestCaseWithTestData):
changelist = my_modeladmin.get_changelist_instance(request) changelist = my_modeladmin.get_changelist_instance(request)
filters = changelist.get_filters(request) filters = changelist.get_filters(request)
filterspec = filters[0][0] filterspec = filters[0][0]
expected = [ expected = [
(3001, 'Wayne Enterprises'), (3001, 'Wayne Enterprises'),
] ]
self.assertEqual(filterspec.lookup_choices, expected) self.assertEqual(filterspec.lookup_choices, expected)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
request = self.factory.get( request = self.factory.get(
'/', '/',
{'main_alliance_id__exact': self.character_1.alliance_id} {'main_alliance_id__exact': self.character_1.alliance_id}
) )
request.user = self.user_1 request.user = self.user_1
changelist = my_modeladmin.get_changelist_instance(request) changelist = my_modeladmin.get_changelist_instance(request)
queryset = changelist.get_queryset(request) queryset = changelist.get_queryset(request)
expected = [self.user_1] expected = [self.user_1]
self.assertSetEqual(set(queryset), set(expected)) self.assertSetEqual(set(queryset), set(expected))
def test_filter_main_factions(self):
class UserAdminTest(BaseUserAdmin):
list_filter = (MainFactionFilter,)
my_modeladmin = UserAdminTest(User, AdminSite())
# Make sure the lookups are correct
request = self.factory.get('/')
request.user = self.user_4
changelist = my_modeladmin.get_changelist_instance(request)
filters = changelist.get_filters(request)
filterspec = filters[0][0]
expected = [
(999, 'The X-Men'),
]
self.assertEqual(filterspec.lookup_choices, expected)
# Make sure the correct queryset is returned
request = self.factory.get(
'/',
{'main_faction_id__exact': self.character_4.faction_id}
)
request.user = self.user_4
changelist = my_modeladmin.get_changelist_instance(request)
queryset = changelist.get_queryset(request)
expected = [self.user_4]
self.assertSetEqual(set(queryset), set(expected))
def test_change_view_loads_normally(self): def test_change_view_loads_normally(self):
User.objects.create_superuser( User.objects.create_superuser(
username='superuser', password='secret', email='admin@example.com' username='superuser', password='secret', email='admin@example.com'
) )
c = Client() c = Client()
c.login(username='superuser', password='secret') c.login(username='superuser', password='secret')
response = c.get(get_admin_change_view_url(self.user_1)) response = c.get(get_admin_change_view_url(self.user_1))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@@ -571,14 +543,50 @@ class TestUserAdmin(TestCaseWithTestData):
self.assertEqual(response.status_code, expected) self.assertEqual(response.status_code, expected)
class TestUserAdminChangeForm(TestCase):
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.modeladmin = UserAdmin(model=User, admin_site=AdminSite())
def test_should_show_groups_available_to_user_with_blue_state_only(self):
# given
superuser = User.objects.create_superuser("Super")
user = AuthUtils.create_user("Bruce Wayne")
character = AuthUtils.add_main_character_2(
user,
name="Bruce Wayne",
character_id=1001,
corp_id=2001,
corp_name="Wayne Technologies"
)
blue_state = State.objects.get(name="Blue")
blue_state.member_characters.add(character)
member_state = AuthUtils.get_member_state()
group_1 = Group.objects.create(name="Group 1")
group_2 = Group.objects.create(name="Group 2")
group_2.authgroup.states.add(blue_state)
group_3 = Group.objects.create(name="Group 3")
group_3.authgroup.states.add(member_state)
self.client.force_login(superuser)
# when
response = self.client.get(f"/admin/authentication/user/{user.pk}/change/")
# then
self.assertEqual(response.status_code, 200)
soup = BeautifulSoup(response.rendered_content, features="html.parser")
groups_select = soup.find("select", {"id": "id_groups"}).find_all('option')
group_ids = {int(option["value"]) for option in groups_select}
self.assertSetEqual(group_ids, {group_1.pk, group_2.pk})
class TestMakeServicesHooksActions(TestCaseWithTestData): class TestMakeServicesHooksActions(TestCaseWithTestData):
class MyServicesHookTypeA(ServicesHook): class MyServicesHookTypeA(ServicesHook):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.name = 'My Service A' self.name = 'My Service A'
def update_groups(self, user): def update_groups(self, user):
pass pass
@@ -590,7 +598,7 @@ class TestMakeServicesHooksActions(TestCaseWithTestData):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.name = 'My Service B' self.name = 'My Service B'
def update_groups(self, user): def update_groups(self, user):
pass pass
@@ -602,33 +610,32 @@ class TestMakeServicesHooksActions(TestCaseWithTestData):
def sync_nicknames_bulk(self, user): def sync_nicknames_bulk(self, user):
pass pass
def test_service_has_update_groups_only(self): def test_service_has_update_groups_only(self):
service = self.MyServicesHookTypeA() service = self.MyServicesHookTypeA()
mock_service = MagicMock(spec=service) mock_service = MagicMock(spec=service)
action = make_service_hooks_update_groups_action(mock_service) action = make_service_hooks_update_groups_action(mock_service)
action(MagicMock(), MagicMock(), [self.user_1]) action(MagicMock(), MagicMock(), [self.user_1])
self.assertTrue(mock_service.update_groups.called) self.assertTrue(mock_service.update_groups.called)
def test_service_has_update_groups_bulk(self): def test_service_has_update_groups_bulk(self):
service = self.MyServicesHookTypeB() service = self.MyServicesHookTypeB()
mock_service = MagicMock(spec=service) mock_service = MagicMock(spec=service)
action = make_service_hooks_update_groups_action(mock_service) action = make_service_hooks_update_groups_action(mock_service)
action(MagicMock(), MagicMock(), [self.user_1]) action(MagicMock(), MagicMock(), [self.user_1])
self.assertFalse(mock_service.update_groups.called) self.assertFalse(mock_service.update_groups.called)
self.assertTrue(mock_service.update_groups_bulk.called) self.assertTrue(mock_service.update_groups_bulk.called)
def test_service_has_sync_nickname_only(self): def test_service_has_sync_nickname_only(self):
service = self.MyServicesHookTypeA() service = self.MyServicesHookTypeA()
mock_service = MagicMock(spec=service) mock_service = MagicMock(spec=service)
action = make_service_hooks_sync_nickname_action(mock_service) action = make_service_hooks_sync_nickname_action(mock_service)
action(MagicMock(), MagicMock(), [self.user_1]) action(MagicMock(), MagicMock(), [self.user_1])
self.assertTrue(mock_service.sync_nickname.called) self.assertTrue(mock_service.sync_nickname.called)
def test_service_has_sync_nicknames_bulk(self): def test_service_has_sync_nicknames_bulk(self):
service = self.MyServicesHookTypeB() service = self.MyServicesHookTypeB()
mock_service = MagicMock(spec=service) mock_service = MagicMock(spec=service)
action = make_service_hooks_sync_nickname_action(mock_service) action = make_service_hooks_sync_nickname_action(mock_service)
action(MagicMock(), MagicMock(), [self.user_1]) action(MagicMock(), MagicMock(), [self.user_1])
self.assertFalse(mock_service.sync_nickname.called) self.assertFalse(mock_service.sync_nickname.called)

View File

@@ -9,19 +9,19 @@ MODULE_PATH = 'allianceauth.authentication'
class TestSetAppSetting(TestCase): class TestSetAppSetting(TestCase):
@patch(MODULE_PATH + '.app_settings.settings') @patch(MODULE_PATH + '.app_settings.settings')
def test_default_if_not_set(self, mock_settings): def test_default_if_not_set(self, mock_settings):
mock_settings.TEST_SETTING_DUMMY = Mock(spec=None) mock_settings.TEST_SETTING_DUMMY = Mock(spec=None)
result = app_settings._clean_setting( result = app_settings._clean_setting(
'TEST_SETTING_DUMMY', 'TEST_SETTING_DUMMY',
False, False,
) )
self.assertEqual(result, False) self.assertEqual(result, False)
@patch(MODULE_PATH + '.app_settings.settings') @patch(MODULE_PATH + '.app_settings.settings')
def test_default_if_not_set_for_none(self, mock_settings): def test_default_if_not_set_for_none(self, mock_settings):
mock_settings.TEST_SETTING_DUMMY = Mock(spec=None) mock_settings.TEST_SETTING_DUMMY = Mock(spec=None)
result = app_settings._clean_setting( result = app_settings._clean_setting(
'TEST_SETTING_DUMMY', 'TEST_SETTING_DUMMY',
None, None,
required_type=int required_type=int
) )
@@ -31,8 +31,8 @@ class TestSetAppSetting(TestCase):
def test_true_stays_true(self, mock_settings): def test_true_stays_true(self, mock_settings):
mock_settings.TEST_SETTING_DUMMY = True mock_settings.TEST_SETTING_DUMMY = True
result = app_settings._clean_setting( result = app_settings._clean_setting(
'TEST_SETTING_DUMMY', 'TEST_SETTING_DUMMY',
False, False,
) )
self.assertEqual(result, True) self.assertEqual(result, True)
@@ -40,7 +40,7 @@ class TestSetAppSetting(TestCase):
def test_false_stays_false(self, mock_settings): def test_false_stays_false(self, mock_settings):
mock_settings.TEST_SETTING_DUMMY = False mock_settings.TEST_SETTING_DUMMY = False
result = app_settings._clean_setting( result = app_settings._clean_setting(
'TEST_SETTING_DUMMY', 'TEST_SETTING_DUMMY',
False False
) )
self.assertEqual(result, False) self.assertEqual(result, False)
@@ -49,7 +49,7 @@ class TestSetAppSetting(TestCase):
def test_default_for_invalid_type_bool(self, mock_settings): def test_default_for_invalid_type_bool(self, mock_settings):
mock_settings.TEST_SETTING_DUMMY = 'invalid type' mock_settings.TEST_SETTING_DUMMY = 'invalid type'
result = app_settings._clean_setting( result = app_settings._clean_setting(
'TEST_SETTING_DUMMY', 'TEST_SETTING_DUMMY',
False False
) )
self.assertEqual(result, False) self.assertEqual(result, False)
@@ -58,7 +58,7 @@ class TestSetAppSetting(TestCase):
def test_default_for_invalid_type_int(self, mock_settings): def test_default_for_invalid_type_int(self, mock_settings):
mock_settings.TEST_SETTING_DUMMY = 'invalid type' mock_settings.TEST_SETTING_DUMMY = 'invalid type'
result = app_settings._clean_setting( result = app_settings._clean_setting(
'TEST_SETTING_DUMMY', 'TEST_SETTING_DUMMY',
50 50
) )
self.assertEqual(result, 50) self.assertEqual(result, 50)
@@ -67,7 +67,7 @@ class TestSetAppSetting(TestCase):
def test_default_if_below_minimum_1(self, mock_settings): def test_default_if_below_minimum_1(self, mock_settings):
mock_settings.TEST_SETTING_DUMMY = -5 mock_settings.TEST_SETTING_DUMMY = -5
result = app_settings._clean_setting( result = app_settings._clean_setting(
'TEST_SETTING_DUMMY', 'TEST_SETTING_DUMMY',
default_value=50 default_value=50
) )
self.assertEqual(result, 50) self.assertEqual(result, 50)
@@ -76,7 +76,7 @@ class TestSetAppSetting(TestCase):
def test_default_if_below_minimum_2(self, mock_settings): def test_default_if_below_minimum_2(self, mock_settings):
mock_settings.TEST_SETTING_DUMMY = -50 mock_settings.TEST_SETTING_DUMMY = -50
result = app_settings._clean_setting( result = app_settings._clean_setting(
'TEST_SETTING_DUMMY', 'TEST_SETTING_DUMMY',
default_value=50, default_value=50,
min_value=-10 min_value=-10
) )
@@ -86,7 +86,7 @@ class TestSetAppSetting(TestCase):
def test_default_for_invalid_type_int(self, mock_settings): def test_default_for_invalid_type_int(self, mock_settings):
mock_settings.TEST_SETTING_DUMMY = 1000 mock_settings.TEST_SETTING_DUMMY = 1000
result = app_settings._clean_setting( result = app_settings._clean_setting(
'TEST_SETTING_DUMMY', 'TEST_SETTING_DUMMY',
default_value=50, default_value=50,
max_value=100 max_value=100
) )
@@ -97,6 +97,6 @@ class TestSetAppSetting(TestCase):
mock_settings.TEST_SETTING_DUMMY = 'invalid type' mock_settings.TEST_SETTING_DUMMY = 'invalid type'
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
result = app_settings._clean_setting( result = app_settings._clean_setting(
'TEST_SETTING_DUMMY', 'TEST_SETTING_DUMMY',
default_value=None default_value=None
) )

View File

@@ -23,27 +23,27 @@ class TestStatePermissions(TestCase):
self.permission_2 = AuthUtils.get_permission_by_name(PERMISSION_2) self.permission_2 = AuthUtils.get_permission_by_name(PERMISSION_2)
# group # group
self.group_1 = Group.objects.create(name="Group 1") self.group_1 = Group.objects.create(name="Group 1")
self.group_2 = Group.objects.create(name="Group 2") self.group_2 = Group.objects.create(name="Group 2")
# state # state
self.state_1 = AuthUtils.get_member_state() self.state_1 = AuthUtils.get_member_state()
self.state_2 = AuthUtils.create_state("Other State", 75) self.state_2 = AuthUtils.create_state("Other State", 75)
# user # user
self.user = AuthUtils.create_user("Bruce Wayne") self.user = AuthUtils.create_user("Bruce Wayne")
self.main = AuthUtils.add_main_character_2(self.user, self.user.username, 123) self.main = AuthUtils.add_main_character_2(self.user, self.user.username, 123)
def test_user_has_user_permissions(self): def test_user_has_user_permissions(self):
self.user.user_permissions.add(self.permission_1) self.user.user_permissions.add(self.permission_1)
user = User.objects.get(pk=self.user.pk) user = User.objects.get(pk=self.user.pk)
self.assertTrue(user.has_perm(PERMISSION_1)) self.assertTrue(user.has_perm(PERMISSION_1))
def test_user_has_group_permissions(self): def test_user_has_group_permissions(self):
self.group_1.permissions.add(self.permission_1) self.group_1.permissions.add(self.permission_1)
self.user.groups.add(self.group_1) self.user.groups.add(self.group_1)
user = User.objects.get(pk=self.user.pk) user = User.objects.get(pk=self.user.pk)
self.assertTrue(user.has_perm(PERMISSION_1)) self.assertTrue(user.has_perm(PERMISSION_1))
@@ -55,7 +55,7 @@ class TestStatePermissions(TestCase):
self.assertTrue(user.has_perm(PERMISSION_1)) self.assertTrue(user.has_perm(PERMISSION_1))
def test_when_user_changes_state_perms_change_accordingly(self): def test_when_user_changes_state_perms_change_accordingly(self):
self.state_1.permissions.add(self.permission_1) self.state_1.permissions.add(self.permission_1)
self.state_1.member_characters.add(self.main) self.state_1.member_characters.add(self.main)
user = User.objects.get(pk=self.user.pk) user = User.objects.get(pk=self.user.pk)
self.assertTrue(user.has_perm(PERMISSION_1)) self.assertTrue(user.has_perm(PERMISSION_1))
@@ -68,16 +68,16 @@ class TestStatePermissions(TestCase):
self.assertTrue(user.has_perm(PERMISSION_2)) self.assertTrue(user.has_perm(PERMISSION_2))
def test_state_permissions_are_returned_for_current_user_object(self): def test_state_permissions_are_returned_for_current_user_object(self):
# verify state permissions are returns for the current user object # verify state permissions are returns for the current user object
# and not for it's instance in the database, which might be outdated # and not for it's instance in the database, which might be outdated
self.state_1.permissions.add(self.permission_1) self.state_1.permissions.add(self.permission_1)
self.state_2.permissions.add(self.permission_2) self.state_2.permissions.add(self.permission_2)
self.state_1.member_characters.add(self.main) self.state_1.member_characters.add(self.main)
user = User.objects.get(pk=self.user.pk) user = User.objects.get(pk=self.user.pk)
user.profile.state = self.state_2 user.profile.state = self.state_2
self.assertFalse(user.has_perm(PERMISSION_1)) self.assertFalse(user.has_perm(PERMISSION_1))
self.assertTrue(user.has_perm(PERMISSION_2)) self.assertTrue(user.has_perm(PERMISSION_2))
class TestAuthenticate(TestCase): class TestAuthenticate(TestCase):
@classmethod @classmethod
@@ -114,12 +114,12 @@ class TestAuthenticate(TestCase):
def test_authenticate_main_character(self): def test_authenticate_main_character(self):
t = Token(character_id=self.main_character.character_id, character_owner_hash='1') t = Token(character_id=self.main_character.character_id, character_owner_hash='1')
user = StateBackend().authenticate(token=t) user = StateBackend().authenticate(token=t)
self.assertEquals(user, self.user) self.assertEqual(user, self.user)
def test_authenticate_alt_character(self): def test_authenticate_alt_character(self):
t = Token(character_id=self.alt_character.character_id, character_owner_hash='2') t = Token(character_id=self.alt_character.character_id, character_owner_hash='2')
user = StateBackend().authenticate(token=t) user = StateBackend().authenticate(token=t)
self.assertEquals(user, self.user) self.assertEqual(user, self.user)
def test_authenticate_unclaimed_character(self): def test_authenticate_unclaimed_character(self):
t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='3') t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='3')
@@ -138,7 +138,7 @@ class TestAuthenticate(TestCase):
def test_iterate_username(self): def test_iterate_username(self):
t = Token(character_id=self.unclaimed_character.character_id, t = Token(character_id=self.unclaimed_character.character_id,
character_name=self.unclaimed_character.character_name, character_owner_hash='3') character_name=self.unclaimed_character.character_name, character_owner_hash='3')
username = StateBackend().authenticate(token=t).username username = StateBackend().authenticate(token=t).username
t.character_owner_hash = '4' t.character_owner_hash = '4'
username_1 = StateBackend().authenticate(token=t).username username_1 = StateBackend().authenticate(token=t).username

View File

@@ -4,7 +4,7 @@ from django.contrib.auth.models import User
from django.test import TestCase from django.test import TestCase
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo,\ from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo,\
EveAllianceInfo EveAllianceInfo, EveFactionInfo
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
from esi.errors import IncompleteResponseError from esi.errors import IncompleteResponseError
from esi.models import Token from esi.models import Token
@@ -36,8 +36,8 @@ class CharacterOwnershipTestCase(TestCase):
character_owner_hash='1', character_owner_hash='1',
) )
co = CharacterOwnership.objects.get(character=self.character) co = CharacterOwnership.objects.get(character=self.character)
self.assertEquals(co.user, self.user) self.assertEqual(co.user, self.user)
self.assertEquals(co.owner_hash, '1') self.assertEqual(co.owner_hash, '1')
def test_transfer_ownership(self): def test_transfer_ownership(self):
Token.objects.create( Token.objects.create(
@@ -54,7 +54,7 @@ class CharacterOwnershipTestCase(TestCase):
) )
co = CharacterOwnership.objects.get(character=self.character) co = CharacterOwnership.objects.get(character=self.character)
self.assertNotEqual(self.user, co.user) self.assertNotEqual(self.user, co.user)
self.assertEquals(self.alt_user, co.user) self.assertEqual(self.alt_user, co.user)
def test_clear_main_character(self): def test_clear_main_character(self):
Token.objects.create( Token.objects.create(
@@ -80,13 +80,15 @@ class StateTestCase(TestCase):
def setUpTestData(cls): def setUpTestData(cls):
cls.user = AuthUtils.create_user('test_user', disconnect_signals=True) cls.user = AuthUtils.create_user('test_user', disconnect_signals=True)
AuthUtils.add_main_character(cls.user, 'Test Character', '1', corp_id='1', alliance_id='1', AuthUtils.add_main_character(cls.user, 'Test Character', '1', corp_id='1', alliance_id='1',
corp_name='Test Corp', alliance_name='Test Alliance') corp_name='Test Corp', alliance_name='Test Alliance', faction_id=1337,
faction_name='Permabanned')
cls.guest_state = get_guest_state() cls.guest_state = get_guest_state()
cls.test_character = EveCharacter.objects.get(character_id='1') cls.test_character = EveCharacter.objects.get(character_id='1')
cls.test_corporation = EveCorporationInfo.objects.create(corporation_id='1', corporation_name='Test Corp', cls.test_corporation = EveCorporationInfo.objects.create(corporation_id='1', corporation_name='Test Corp',
corporation_ticker='TEST', member_count=1) corporation_ticker='TEST', member_count=1)
cls.test_alliance = EveAllianceInfo.objects.create(alliance_id='1', alliance_name='Test Alliance', cls.test_alliance = EveAllianceInfo.objects.create(alliance_id='1', alliance_name='Test Alliance',
alliance_ticker='TEST', executor_corp_id='1') alliance_ticker='TEST', executor_corp_id='1')
cls.test_faction = EveFactionInfo.objects.create(faction_id=1337, faction_name='Permabanned')
cls.member_state = State.objects.create( cls.member_state = State.objects.create(
name='Test Member', name='Test Member',
priority=150, priority=150,
@@ -98,29 +100,38 @@ class StateTestCase(TestCase):
def test_state_assignment_on_character_change(self): def test_state_assignment_on_character_change(self):
self.member_state.member_characters.add(self.test_character) self.member_state.member_characters.add(self.test_character)
self._refresh_user() self._refresh_user()
self.assertEquals(self.user.profile.state, self.member_state) self.assertEqual(self.user.profile.state, self.member_state)
self.member_state.member_characters.remove(self.test_character) self.member_state.member_characters.remove(self.test_character)
self._refresh_user() self._refresh_user()
self.assertEquals(self.user.profile.state, self.guest_state) self.assertEqual(self.user.profile.state, self.guest_state)
def test_state_assignment_on_corporation_change(self): def test_state_assignment_on_corporation_change(self):
self.member_state.member_corporations.add(self.test_corporation) self.member_state.member_corporations.add(self.test_corporation)
self._refresh_user() self._refresh_user()
self.assertEquals(self.user.profile.state, self.member_state) self.assertEqual(self.user.profile.state, self.member_state)
self.member_state.member_corporations.remove(self.test_corporation) self.member_state.member_corporations.remove(self.test_corporation)
self._refresh_user() self._refresh_user()
self.assertEquals(self.user.profile.state, self.guest_state) self.assertEqual(self.user.profile.state, self.guest_state)
def test_state_assignment_on_alliance_addition(self): def test_state_assignment_on_alliance_addition(self):
self.member_state.member_alliances.add(self.test_alliance) self.member_state.member_alliances.add(self.test_alliance)
self._refresh_user() self._refresh_user()
self.assertEquals(self.user.profile.state, self.member_state) self.assertEqual(self.user.profile.state, self.member_state)
self.member_state.member_alliances.remove(self.test_alliance) self.member_state.member_alliances.remove(self.test_alliance)
self._refresh_user() self._refresh_user()
self.assertEquals(self.user.profile.state, self.guest_state) self.assertEqual(self.user.profile.state, self.guest_state)
def test_state_assignment_on_faction_change(self):
self.member_state.member_factions.add(self.test_faction)
self._refresh_user()
self.assertEqual(self.user.profile.state, self.member_state)
self.member_state.member_factions.remove(self.test_faction)
self._refresh_user()
self.assertEqual(self.user.profile.state, self.guest_state)
def test_state_assignment_on_higher_priority_state_creation(self): def test_state_assignment_on_higher_priority_state_creation(self):
self.member_state.member_characters.add(self.test_character) self.member_state.member_characters.add(self.test_character)
@@ -130,10 +141,10 @@ class StateTestCase(TestCase):
) )
higher_state.member_characters.add(self.test_character) higher_state.member_characters.add(self.test_character)
self._refresh_user() self._refresh_user()
self.assertEquals(higher_state, self.user.profile.state) self.assertEqual(higher_state, self.user.profile.state)
higher_state.member_characters.clear() higher_state.member_characters.clear()
self._refresh_user() self._refresh_user()
self.assertEquals(self.member_state, self.user.profile.state) self.assertEqual(self.member_state, self.user.profile.state)
self.member_state.member_characters.clear() self.member_state.member_characters.clear()
def test_state_assignment_on_lower_priority_state_creation(self): def test_state_assignment_on_lower_priority_state_creation(self):
@@ -144,10 +155,10 @@ class StateTestCase(TestCase):
) )
lower_state.member_characters.add(self.test_character) lower_state.member_characters.add(self.test_character)
self._refresh_user() self._refresh_user()
self.assertEquals(self.member_state, self.user.profile.state) self.assertEqual(self.member_state, self.user.profile.state)
lower_state.member_characters.clear() lower_state.member_characters.clear()
self._refresh_user() self._refresh_user()
self.assertEquals(self.member_state, self.user.profile.state) self.assertEqual(self.member_state, self.user.profile.state)
self.member_state.member_characters.clear() self.member_state.member_characters.clear()
def test_state_assignment_on_priority_change(self): def test_state_assignment_on_priority_change(self):
@@ -161,11 +172,11 @@ class StateTestCase(TestCase):
lower_state.priority = 500 lower_state.priority = 500
lower_state.save() lower_state.save()
self._refresh_user() self._refresh_user()
self.assertEquals(lower_state, self.user.profile.state) self.assertEqual(lower_state, self.user.profile.state)
lower_state.priority = 125 lower_state.priority = 125
lower_state.save() lower_state.save()
self._refresh_user() self._refresh_user()
self.assertEquals(self.member_state, self.user.profile.state) self.assertEqual(self.member_state, self.user.profile.state)
def test_state_assignment_on_state_deletion(self): def test_state_assignment_on_state_deletion(self):
self.member_state.member_characters.add(self.test_character) self.member_state.member_characters.add(self.test_character)
@@ -175,11 +186,11 @@ class StateTestCase(TestCase):
) )
higher_state.member_characters.add(self.test_character) higher_state.member_characters.add(self.test_character)
self._refresh_user() self._refresh_user()
self.assertEquals(higher_state, self.user.profile.state) self.assertEqual(higher_state, self.user.profile.state)
higher_state.delete() higher_state.delete()
self.assertFalse(State.objects.filter(name='Higher State').count()) self.assertFalse(State.objects.filter(name='Higher State').count())
self._refresh_user() self._refresh_user()
self.assertEquals(self.member_state, self.user.profile.state) self.assertEqual(self.member_state, self.user.profile.state)
def test_state_assignment_on_public_toggle(self): def test_state_assignment_on_public_toggle(self):
self.member_state.member_characters.add(self.test_character) self.member_state.member_characters.add(self.test_character)
@@ -188,26 +199,26 @@ class StateTestCase(TestCase):
priority=200, priority=200,
) )
self._refresh_user() self._refresh_user()
self.assertEquals(self.member_state, self.user.profile.state) self.assertEqual(self.member_state, self.user.profile.state)
higher_state.public = True higher_state.public = True
higher_state.save() higher_state.save()
self._refresh_user() self._refresh_user()
self.assertEquals(higher_state, self.user.profile.state) self.assertEqual(higher_state, self.user.profile.state)
higher_state.public = False higher_state.public = False
higher_state.save() higher_state.save()
self._refresh_user() self._refresh_user()
self.assertEquals(self.member_state, self.user.profile.state) self.assertEqual(self.member_state, self.user.profile.state)
def test_state_assignment_on_active_changed(self): def test_state_assignment_on_active_changed(self):
self.member_state.member_characters.add(self.test_character) self.member_state.member_characters.add(self.test_character)
self.user.is_active = False self.user.is_active = False
self.user.save() self.user.save()
self._refresh_user() self._refresh_user()
self.assertEquals(self.user.profile.state, self.guest_state) self.assertEqual(self.user.profile.state, self.guest_state)
self.user.is_active = True self.user.is_active = True
self.user.save() self.user.save()
self._refresh_user() self._refresh_user()
self.assertEquals(self.user.profile.state, self.member_state) self.assertEqual(self.user.profile.state, self.member_state)
class CharacterOwnershipCheckTestCase(TestCase): class CharacterOwnershipCheckTestCase(TestCase):
@@ -215,7 +226,7 @@ class CharacterOwnershipCheckTestCase(TestCase):
def setUpTestData(cls): def setUpTestData(cls):
cls.user = AuthUtils.create_user('test_user', disconnect_signals=True) cls.user = AuthUtils.create_user('test_user', disconnect_signals=True)
AuthUtils.add_main_character(cls.user, 'Test Character', '1', corp_id='1', alliance_id='1', AuthUtils.add_main_character(cls.user, 'Test Character', '1', corp_id='1', alliance_id='1',
corp_name='Test Corp', alliance_name='Test Alliance') corp_name='Test Corp', alliance_name='Test Alliance')
cls.character = EveCharacter.objects.get(character_id=1) cls.character = EveCharacter.objects.get(character_id=1)
cls.token = Token.objects.create( cls.token = Token.objects.create(
user=cls.user, user=cls.user,

View File

@@ -1,19 +1,19 @@
from math import ceil from math import ceil
from unittest.mock import patch from unittest.mock import patch
from requests import RequestException import requests
import requests_mock import requests_mock
from packaging.version import Version as Pep440Version from packaging.version import Version as Pep440Version
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, status_overview,
_fetch_list_from_gitlab, _fetch_list_from_gitlab,
_current_notifications, _current_notifications,
_current_version_summary, _current_version_summary,
_fetch_notification_issues_from_gitlab, _fetch_notification_issues_from_gitlab,
_fetch_tags_from_gitlab,
_latests_versions _latests_versions
) )
@@ -55,17 +55,17 @@ 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._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_notifications,
mock_current_version_info, mock_current_version_info,
mock_fetch_celery_queue_length mock_fetch_celery_queue_length
): ):
# given
notifications = { notifications = {
'notifications': GITHUB_NOTIFICATION_ISSUES[:5] 'notifications': GITHUB_NOTIFICATION_ISSUES[:5]
} }
@@ -83,55 +83,69 @@ class TestStatusOverviewTag(TestCase):
} }
mock_current_version_info.return_value = version_info mock_current_version_info.return_value = version_info
mock_fetch_celery_queue_length.return_value = 3 mock_fetch_celery_queue_length.return_value = 3
# when
result = status_overview() result = status_overview()
expected = { # then
'notifications': GITHUB_NOTIFICATION_ISSUES[:5], self.assertEqual(result["notifications"], GITHUB_NOTIFICATION_ISSUES[:5])
'latest_major': True, self.assertTrue(result["latest_major"])
'latest_minor': True, self.assertTrue(result["latest_minor"])
'latest_patch': True, self.assertTrue(result["latest_patch"])
'latest_beta': False, self.assertFalse(result["latest_beta"])
'current_version': TEST_VERSION, self.assertEqual(result["current_version"], TEST_VERSION)
'latest_major_version': '2.4.5', self.assertEqual(result["latest_major_version"], '2.4.5')
'latest_minor_version': '2.4.0', self.assertEqual(result["latest_minor_version"], '2.4.0')
'latest_patch_version': '2.4.5', self.assertEqual(result["latest_patch_version"], '2.4.5')
'latest_beta_version': '2.4.4a1', self.assertEqual(result["latest_beta_version"], '2.4.4a1')
'task_queue_length': 3, self.assertEqual(result["task_queue_length"], 3)
}
self.assertEqual(result, expected)
class TestNotifications(TestCase): class TestNotifications(TestCase):
def setUp(self) -> None:
cache.clear()
@requests_mock.mock() @requests_mock.mock()
def test_fetch_notification_issues_from_gitlab(self, requests_mocker): def test_fetch_notification_issues_from_gitlab(self, requests_mocker):
# given
url = ( url = (
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth/issues' 'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth/issues'
'?labels=announcement' '?labels=announcement'
) )
requests_mocker.get(url, json=GITHUB_NOTIFICATION_ISSUES) requests_mocker.get(url, json=GITHUB_NOTIFICATION_ISSUES)
# when
result = _fetch_notification_issues_from_gitlab() result = _fetch_notification_issues_from_gitlab()
# then
self.assertEqual(result, GITHUB_NOTIFICATION_ISSUES) self.assertEqual(result, GITHUB_NOTIFICATION_ISSUES)
@patch(MODULE_PATH + '.admin_status.cache') @patch(MODULE_PATH + '.admin_status.cache')
def test_current_notifications_normal(self, mock_cache): def test_current_notifications_normal(self, mock_cache):
# given
mock_cache.get_or_set.return_value = GITHUB_NOTIFICATION_ISSUES mock_cache.get_or_set.return_value = GITHUB_NOTIFICATION_ISSUES
# when
result = _current_notifications() result = _current_notifications()
# then
self.assertEqual(result['notifications'], GITHUB_NOTIFICATION_ISSUES[:5]) self.assertEqual(result['notifications'], GITHUB_NOTIFICATION_ISSUES[:5])
@patch(MODULE_PATH + '.admin_status.cache') @requests_mock.mock()
def test_current_notifications_failed(self, mock_cache): def test_current_notifications_failed(self, requests_mocker):
mock_cache.get_or_set.side_effect = RequestException # given
url = (
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth/issues'
'?labels=announcement'
)
requests_mocker.get(url, status_code=404)
# when
result = _current_notifications() result = _current_notifications()
# then
self.assertEqual(result['notifications'], list()) self.assertEqual(result['notifications'], list())
@patch(MODULE_PATH + '.admin_status.cache') @patch(MODULE_PATH + '.admin_status.cache')
def test_current_notifications_is_none(self, mock_cache): def test_current_notifications_is_none(self, mock_cache):
# given
mock_cache.get_or_set.return_value = None mock_cache.get_or_set.return_value = None
# when
result = _current_notifications() result = _current_notifications()
# then
self.assertEqual(result['notifications'], list()) self.assertEqual(result['notifications'], list())
@@ -143,89 +157,93 @@ class TestCeleryQueueLength(TestCase):
class TestVersionTags(TestCase): class TestVersionTags(TestCase):
def setUp(self) -> None:
cache.clear()
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION) @patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
@patch(MODULE_PATH + '.admin_status.cache') @patch(MODULE_PATH + '.admin_status.cache')
def test_current_version_info_normal(self, mock_cache): def test_current_version_info_normal(self, mock_cache):
# given
mock_cache.get_or_set.return_value = GITHUB_TAGS mock_cache.get_or_set.return_value = GITHUB_TAGS
# when
result = _current_version_summary() result = _current_version_summary()
self.assertTrue(result['latest_major']) # then
self.assertTrue(result['latest_minor'])
self.assertTrue(result['latest_patch']) self.assertTrue(result['latest_patch'])
self.assertEqual(result['latest_major_version'], '2.0.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.6a1') self.assertEqual(result['latest_beta_version'], '2.4.6a1')
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION) @patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
@patch(MODULE_PATH + '.admin_status.cache')
def test_current_version_info_failed(self, mock_cache):
mock_cache.get_or_set.side_effect = RequestException
expected = {}
result = _current_version_summary()
self.assertEqual(result, expected)
@requests_mock.mock() @requests_mock.mock()
def test_fetch_tags_from_gitlab(self, requests_mocker): def test_current_version_info_failed(self, requests_mocker):
# given
url = ( url = (
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth' 'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth'
'/repository/tags' '/repository/tags'
) )
requests_mocker.get(url, status_code=500)
# when
result = _current_version_summary()
# then
self.assertEqual(result, {})
@requests_mock.mock()
def test_fetch_tags_from_gitlab(self, requests_mocker):
# given
url = (
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth'
'/repository/tags'
)
requests_mocker.get(url, json=GITHUB_TAGS) requests_mocker.get(url, json=GITHUB_TAGS)
result = _fetch_tags_from_gitlab() # when
self.assertEqual(result, GITHUB_TAGS) result = _current_version_summary()
# then
self.assertTrue(result)
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION) @patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
@patch(MODULE_PATH + '.admin_status.cache') @patch(MODULE_PATH + '.admin_status.cache')
def test_current_version_info_return_no_data(self, mock_cache): def test_current_version_info_return_no_data(self, mock_cache):
# given
mock_cache.get_or_set.return_value = None mock_cache.get_or_set.return_value = None
# when
expected = {}
result = _current_version_summary() result = _current_version_summary()
self.assertEqual(result, expected) # then
self.assertEqual(result, {})
class TestLatestsVersion(TestCase): class TestLatestsVersion(TestCase):
def test_all_version_types_defined(self): def test_all_version_types_defined(self):
tags = create_tags_list( tags = create_tags_list(
['2.1.1', '2.1.0', '2.0.0', '2.1.1a1', '1.1.1', '1.1.0', '1.0.0'] ['2.1.1', '2.1.0', '2.0.0', '2.1.1a1', '1.1.1', '1.1.0', '1.0.0']
) )
major, minor, patch, beta = _latests_versions(tags) patch, beta = _latests_versions(tags)
self.assertEqual(major, Pep440Version('2.0.0'))
self.assertEqual(minor, Pep440Version('2.1.0'))
self.assertEqual(patch, Pep440Version('2.1.1')) self.assertEqual(patch, Pep440Version('2.1.1'))
self.assertEqual(beta, Pep440Version('2.1.1a1')) self.assertEqual(beta, Pep440Version('2.1.1a1'))
def test_major_and_minor_not_defined_with_zero(self): def test_major_and_minor_not_defined_with_zero(self):
tags = create_tags_list( tags = create_tags_list(
['2.1.2', '2.1.1', '2.0.1', '2.1.1a1', '1.1.1', '1.1.0', '1.0.0'] ['2.1.2', '2.1.1', '2.0.1', '2.1.1a1', '1.1.1', '1.1.0', '1.0.0']
) )
major, minor, patch, beta = _latests_versions(tags) patch, beta = _latests_versions(tags)
self.assertEqual(major, Pep440Version('2.0.1'))
self.assertEqual(minor, Pep440Version('2.1.1'))
self.assertEqual(patch, Pep440Version('2.1.2')) self.assertEqual(patch, Pep440Version('2.1.2'))
self.assertEqual(beta, Pep440Version('2.1.1a1')) self.assertEqual(beta, Pep440Version('2.1.1a1'))
def test_can_ignore_invalid_versions(self): def test_can_ignore_invalid_versions(self):
tags = create_tags_list( tags = create_tags_list(
['2.1.1', '2.1.0', '2.0.0', '2.1.1a1', 'invalid'] ['2.1.1', '2.1.0', '2.0.0', '2.1.1a1', 'invalid']
) )
major, minor, patch, beta = _latests_versions(tags) patch, beta = _latests_versions(tags)
self.assertEqual(major, Pep440Version('2.0.0'))
self.assertEqual(minor, Pep440Version('2.1.0'))
self.assertEqual(patch, Pep440Version('2.1.1')) self.assertEqual(patch, Pep440Version('2.1.1'))
self.assertEqual(beta, Pep440Version('2.1.1a1')) self.assertEqual(beta, Pep440Version('2.1.1a1'))
class TestFetchListFromGitlab(TestCase): class TestFetchListFromGitlab(TestCase):
page_size = 2 page_size = 2
def setUp(self): def setUp(self):
self.url = ( self.url = (
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth' 'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth'
@@ -237,8 +255,8 @@ class TestFetchListFromGitlab(TestCase):
page = int(request.qs['page'][0]) page = int(request.qs['page'][0])
start = (page - 1) * cls.page_size start = (page - 1) * cls.page_size
end = start + cls.page_size end = start + cls.page_size
return GITHUB_TAGS[start:end] return GITHUB_TAGS[start:end]
@requests_mock.mock() @requests_mock.mock()
def test_can_fetch_one_page_with_header(self, requests_mocker): def test_can_fetch_one_page_with_header(self, requests_mocker):
headers = { headers = {
@@ -250,7 +268,7 @@ class TestFetchListFromGitlab(TestCase):
self.assertEqual(requests_mocker.call_count, 1) self.assertEqual(requests_mocker.call_count, 1)
@requests_mock.mock() @requests_mock.mock()
def test_can_fetch_one_page_wo_header(self, requests_mocker): def test_can_fetch_one_page_wo_header(self, requests_mocker):
requests_mocker.get(self.url, json=GITHUB_TAGS) requests_mocker.get(self.url, json=GITHUB_TAGS)
result = _fetch_list_from_gitlab(self.url) result = _fetch_list_from_gitlab(self.url)
self.assertEqual(result, GITHUB_TAGS) self.assertEqual(result, GITHUB_TAGS)
@@ -267,7 +285,7 @@ class TestFetchListFromGitlab(TestCase):
self.assertEqual(requests_mocker.call_count, 1) self.assertEqual(requests_mocker.call_count, 1)
@requests_mock.mock() @requests_mock.mock()
def test_can_fetch_multiple_pages(self, requests_mocker): def test_can_fetch_multiple_pages(self, requests_mocker):
total_pages = ceil(len(GITHUB_TAGS) / self.page_size) total_pages = ceil(len(GITHUB_TAGS) / self.page_size)
headers = { headers = {
'x-total-pages': str(total_pages) 'x-total-pages': str(total_pages)
@@ -278,7 +296,7 @@ class TestFetchListFromGitlab(TestCase):
self.assertEqual(requests_mocker.call_count, total_pages) self.assertEqual(requests_mocker.call_count, total_pages)
@requests_mock.mock() @requests_mock.mock()
def test_can_fetch_given_number_of_pages_only(self, requests_mocker): def test_can_fetch_given_number_of_pages_only(self, requests_mocker):
total_pages = ceil(len(GITHUB_TAGS) / self.page_size) total_pages = ceil(len(GITHUB_TAGS) / self.page_size)
headers = { headers = {
'x-total-pages': str(total_pages) 'x-total-pages': str(total_pages)
@@ -288,3 +306,25 @@ class TestFetchListFromGitlab(TestCase):
result = _fetch_list_from_gitlab(self.url, max_pages=max_pages) result = _fetch_list_from_gitlab(self.url, max_pages=max_pages)
self.assertEqual(result, GITHUB_TAGS[:4]) self.assertEqual(result, GITHUB_TAGS[:4])
self.assertEqual(requests_mocker.call_count, max_pages) self.assertEqual(requests_mocker.call_count, max_pages)
@requests_mock.mock()
@patch(MODULE_PATH + '.admin_status.logger')
def test_should_not_raise_any_exception_from_github_request_but_log_as_warning(
self, requests_mocker, mock_logger
):
for my_exception in [
requests.exceptions.ConnectionError,
requests.exceptions.HTTPError,
requests.exceptions.URLRequired,
requests.exceptions.TooManyRedirects,
requests.exceptions.ConnectTimeout,
requests.exceptions.Timeout,
]:
requests_mocker.get(self.url, exc=my_exception)
try:
result = _fetch_list_from_gitlab(self.url)
except Exception as ex:
self.fail(f"Unexpected exception raised: {ex}")
self.assertTrue(mock_logger.warning.called)
self.assertListEqual(result, [])

View File

@@ -9,8 +9,8 @@ app_name = 'authentication'
urlpatterns = [ urlpatterns = [
url(r'^$', views.index, name='index'), url(r'^$', views.index, name='index'),
url( url(
r'^account/login/$', r'^account/login/$',
TemplateView.as_view(template_name='public/login.html'), TemplateView.as_view(template_name='public/login.html'),
name='login' name='login'
), ),
url( url(
@@ -19,9 +19,9 @@ urlpatterns = [
name='change_main_character' name='change_main_character'
), ),
url( url(
r'^account/characters/add/$', r'^account/characters/add/$',
views.add_character, views.add_character,
name='add_character' name='add_character'
), ),
url(r'^dashboard/$', views.dashboard, name='dashboard'), url(r'^dashboard/$', views.dashboard, name='dashboard'),
] ]

View File

@@ -6,20 +6,23 @@ from django.contrib.auth import login, authenticate
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.urls import reverse from django.core.mail import EmailMultiAlternatives
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.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 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 registration.backends.hmac.views import ( from django_registration.backends.activation.views import (
RegistrationView as BaseRegistrationView, RegistrationView as BaseRegistrationView,
ActivationView as BaseActivationView, ActivationView as BaseActivationView,
REGISTRATION_SALT REGISTRATION_SALT
) )
from registration.signals import user_registered from django_registration.signals import user_registered
from .models import CharacterOwnership from .models import CharacterOwnership
from .forms import RegistrationForm from .forms import RegistrationForm
@@ -51,7 +54,7 @@ def dashboard(request):
.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, 'groups': groups,
'characters': characters 'characters': characters
@@ -62,7 +65,7 @@ def dashboard(request):
@login_required @login_required
@token_required(scopes=settings.LOGIN_TOKEN_SCOPES) @token_required(scopes=settings.LOGIN_TOKEN_SCOPES)
def main_character_change(request, token): def main_character_change(request, token):
logger.debug("main_character_change called by user %s for character %s" % (request.user, token.character_name)) logger.debug(f"main_character_change called by user {request.user} for character {token.character_name}")
try: try:
co = CharacterOwnership.objects.get(character__character_id=token.character_id, user=request.user) co = CharacterOwnership.objects.get(character__character_id=token.character_id, user=request.user)
except CharacterOwnership.DoesNotExist: except CharacterOwnership.DoesNotExist:
@@ -70,7 +73,7 @@ def main_character_change(request, token):
co = CharacterOwnership.objects.create_by_token(token) co = CharacterOwnership.objects.create_by_token(token)
else: else:
messages.error( messages.error(
request, request,
_('Cannot change main character to %(char)s: character owned by a different account.') % ({'char': token.character_name}) _('Cannot change main character to %(char)s: character owned by a different account.') % ({'char': token.character_name})
) )
co = None co = None
@@ -134,11 +137,57 @@ def sso_login(request, token):
# Step 2 # Step 2
class RegistrationView(BaseRegistrationView): class RegistrationView(BaseRegistrationView):
form_class = RegistrationForm form_class = RegistrationForm
success_url = 'authentication:dashboard' template_name = "public/register.html"
email_body_template = "registration/activation_email.txt"
email_body_template_html = "registration/activation_email_html.txt"
email_subject_template = "registration/activation_email_subject.txt"
success_url = reverse_lazy('registration_complete')
def send_activation_email(self, user):
"""
Implement our own way to send a mail to make sure we
send a RFC conform multipart email
:param user:
:type user:
"""
activation_key = self.get_activation_key(user)
context = self.get_email_context(activation_key)
context["user"] = user
# email subject
subject = render_to_string(
template_name=self.email_subject_template,
context=context,
request=self.request,
)
subject = "".join(subject.splitlines())
# plaintext email body part
message = render_to_string(
template_name=self.email_body_template,
context=context,
request=self.request,
)
# html email body part
message_html = render_to_string(
template_name=self.email_body_template_html,
context=context,
request=self.request,
)
# send it
user.email_user(
subject,
message,
settings.DEFAULT_FROM_EMAIL,
**{'html_message': message_html},
)
def get_success_url(self, user): def get_success_url(self, user):
if not getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True): if not getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True):
return 'authentication:dashboard', (), {} return reverse_lazy('authentication:dashboard')
return super().get_success_url(user) return super().get_success_url(user)
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
@@ -150,7 +199,7 @@ class RegistrationView(BaseRegistrationView):
if not getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True): if not getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True):
# Keep the request so the user can be automagically logged in. # Keep the request so the user can be automagically logged in.
setattr(self, 'request', request) setattr(self, 'request', request)
return super(RegistrationView, self).dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def register(self, form): def register(self, form):
user = User.objects.get(pk=self.request.session.get('registration_uid')) user = User.objects.get(pk=self.request.session.get('registration_uid'))
@@ -169,17 +218,20 @@ class RegistrationView(BaseRegistrationView):
return signing.dumps(obj=[getattr(user, User.USERNAME_FIELD), user.email], salt=REGISTRATION_SALT) return signing.dumps(obj=[getattr(user, User.USERNAME_FIELD), user.email], salt=REGISTRATION_SALT)
def get_email_context(self, activation_key): def get_email_context(self, activation_key):
context = super(RegistrationView, self).get_email_context(activation_key) context = super().get_email_context(activation_key)
context['url'] = context['site'].domain + reverse('registration_activate', args=[activation_key]) context['url'] = context['site'].domain + reverse('registration_activate', args=[activation_key])
return context return context
# Step 3 # Step 3
class ActivationView(BaseActivationView): class ActivationView(BaseActivationView):
template_name = "registration/activate.html"
success_url = reverse_lazy('registration_activation_complete')
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(activation_key, salt=REGISTRATION_SALT,
max_age=settings.ACCOUNT_ACTIVATION_DAYS * 86400) max_age=settings.ACCOUNT_ACTIVATION_DAYS * 86400)
return dump return dump
except signing.BadSignature: except signing.BadSignature:
return None return None
@@ -207,5 +259,5 @@ def activation_complete(request):
def registration_closed(request): def registration_closed(request):
messages.error(request, _('Registraion of new accounts it 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')

View File

@@ -41,7 +41,7 @@ def create_project(parser, options, args):
# Call the command with extra context # Call the command with extra context
call_command(StartProject(), *args, **command_options) call_command(StartProject(), *args, **command_options)
print("Success! %(project_name)s has been created." % {'project_name': args[0]}) # noqa print(f"Success! {args[0]} has been created.") # noqa
def update_settings(parser, options, args): def update_settings(parser, options, args):
@@ -69,10 +69,10 @@ def update_settings(parser, options, args):
template_settings_path = os.path.join(template_path, 'project_name/settings/base.py') template_settings_path = os.path.join(template_path, 'project_name/settings/base.py')
# overwrite the local project's base settings # overwrite the local project's base settings
with open(template_settings_path, 'r') as template, open(settings_path, 'w') as target: with open(template_settings_path) as template, open(settings_path, 'w') as target:
target.write(template.read()) target.write(template.read())
print("Successfully updated %(project_name)s settings." % {'project_name': project_name}) print(f"Successfully updated {project_name} settings.")
COMMANDS = { COMMANDS = {

View File

@@ -3,4 +3,4 @@ from django.contrib import admin
from .models import CorpStats, CorpMember from .models import CorpStats, CorpMember
admin.site.register(CorpStats) admin.site.register(CorpStats)
admin.site.register(CorpMember) admin.site.register(CorpMember)

View File

@@ -6,11 +6,13 @@ from allianceauth.corputils import urls
class CorpStats(MenuItemHook): class CorpStats(MenuItemHook):
def __init__(self): def __init__(self):
MenuItemHook.__init__(self, MenuItemHook.__init__(
_('Corporation Stats'), self,
'fas fa-share-alt fa-fw', _('Corporation Stats'),
'corputils:view', 'fas fa-share-alt fa-fw',
navactive=['corputils:']) 'corputils:view',
navactive=['corputils:']
)
def render(self, request): def render(self, request):
if request.user.has_perm('corputils.view_corp_corpstats') or request.user.has_perm( if request.user.has_perm('corputils.view_corp_corpstats') or request.user.has_perm(

View File

@@ -29,7 +29,7 @@ class CorpStatsQuerySet(models.QuerySet):
if user.has_perm('corputils.view_state_corpstats'): if user.has_perm('corputils.view_state_corpstats'):
queries.append(models.Q(corp__in=user.profile.state.member_corporations.all())) queries.append(models.Q(corp__in=user.profile.state.member_corporations.all()))
queries.append(models.Q(corp__alliance__in=user.profile.state.member_alliances.all())) queries.append(models.Q(corp__alliance__in=user.profile.state.member_alliances.all()))
logger.debug('%s queries for user %s visible corpstats.' % (len(queries), user)) logger.debug(f'{len(queries)} queries for user {user} visible corpstats.')
# filter based on queries # filter based on queries
query = queries.pop() query = queries.pop()
for q in queries: for q in queries:
@@ -37,7 +37,7 @@ class CorpStatsQuerySet(models.QuerySet):
return self.filter(query) return self.filter(query)
except AssertionError: except AssertionError:
logger.debug('User %s has no main character. No corpstats visible.' % user) logger.debug('User %s has no main character. No corpstats visible.' % user)
return self.none() return self.none()
class CorpStatsManager(models.Manager): class CorpStatsManager(models.Manager):

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-12-14 21:36 # Generated by Django 1.10.1 on 2016-12-14 21:36
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-12-14 21:48 # Generated by Django 1.10.1 on 2016-12-14 21:48
from __future__ import unicode_literals
from django.db import migrations from django.db import migrations

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-03-22 23:35 # Generated by Django 1.10.5 on 2017-03-22 23:35
from __future__ import unicode_literals
from django.db import migrations from django.db import migrations

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-03-26 20:13 # Generated by Django 1.10.5 on 2017-03-26 20:13
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@@ -13,8 +11,7 @@ def convert_json_to_members(apps, schema_editor):
for cs in CorpStats.objects.all(): for cs in CorpStats.objects.all():
members = json.loads(cs._members) members = json.loads(cs._members)
CorpMember.objects.bulk_create( CorpMember.objects.bulk_create(
[CorpMember(corpstats=cs, character_id=member_id, character_name=member_name) for member_id, member_name in [CorpMember(corpstats=cs, character_id=member_id, character_name=member_name) for member_id, member_name in members.items()]
members.items()]
) )
@@ -50,7 +47,7 @@ class Migration(migrations.Migration):
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='corpmember', name='corpmember',
unique_together=set([('corpstats', 'character_id')]), unique_together={('corpstats', 'character_id')},
), ),
migrations.RunPython(convert_json_to_members, convert_members_to_json), migrations.RunPython(convert_json_to_members, convert_members_to_json),
migrations.RemoveField( migrations.RemoveField(

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-06-10 15:34 # Generated by Django 1.11.2 on 2017-06-10 15:34
from __future__ import unicode_literals
from django.db import migrations from django.db import migrations

View File

@@ -6,8 +6,7 @@ from bravado.exception import HTTPForbidden
from django.db import models from django.db import models
from esi.errors import TokenError from esi.errors import TokenError
from esi.models import Token from esi.models import Token
from allianceauth.eveonline.models import EveCorporationInfo, EveCharacter,\ from allianceauth.eveonline.models import EveCorporationInfo, EveCharacter, EveAllianceInfo
EveAllianceInfo
from allianceauth.notifications import notify from allianceauth.notifications import notify
from allianceauth.corputils.managers import CorpStatsManager from allianceauth.corputils.managers import CorpStatsManager
@@ -44,13 +43,12 @@ class CorpStats(models.Model):
objects = CorpStatsManager() objects = CorpStatsManager()
def __str__(self): def __str__(self):
return "%s for %s" % (self.__class__.__name__, self.corp) return f"{self.__class__.__name__} for {self.corp}"
def update(self): def update(self):
try: try:
c = self.token.get_esi_client(spec_file=SWAGGER_SPEC_PATH) c = self.token.get_esi_client(spec_file=SWAGGER_SPEC_PATH)
assert c.Character.get_characters_character_id(character_id=self.token.character_id).result()[ assert c.Character.get_characters_character_id(character_id=self.token.character_id).result()['corporation_id'] == int(self.corp.corporation_id)
'corporation_id'] == int(self.corp.corporation_id)
member_ids = c.Corporation.get_corporations_corporation_id_members( member_ids = c.Corporation.get_corporations_corporation_id_members(
corporation_id=self.corp.corporation_id).result() corporation_id=self.corp.corporation_id).result()
@@ -58,18 +56,15 @@ class CorpStats(models.Model):
# the swagger spec doesn't have a maxItems count # the swagger spec doesn't have a maxItems count
# manual testing says we can do over 350, but let's not risk it # manual testing says we can do over 350, but let's not risk it
member_id_chunks = [member_ids[i:i + 255] for i in range(0, len(member_ids), 255)] member_id_chunks = [member_ids[i:i + 255] for i in range(0, len(member_ids), 255)]
member_name_chunks = [c.Universe.post_universe_names(ids=id_chunk).result() for id_chunk in member_name_chunks = [c.Universe.post_universe_names(ids=id_chunk).result() for id_chunk in member_id_chunks]
member_id_chunks]
member_list = {} member_list = {}
for name_chunk in member_name_chunks: for name_chunk in member_name_chunks:
member_list.update({m['id']: m['name'] for m in name_chunk}) member_list.update({m['id']: m['name'] for m in name_chunk})
# bulk create new member models # bulk create new member models
missing_members = [m_id for m_id in member_ids if missing_members = [m_id for m_id in member_ids if not CorpMember.objects.filter(corpstats=self, character_id=m_id).exists()]
not CorpMember.objects.filter(corpstats=self, character_id=m_id).exists()]
CorpMember.objects.bulk_create( CorpMember.objects.bulk_create(
[CorpMember(character_id=m_id, character_name=member_list[m_id], corpstats=self) for m_id in [CorpMember(character_id=m_id, character_name=member_list[m_id], corpstats=self) for m_id in missing_members])
missing_members])
# purge old members # purge old members
self.members.exclude(character_id__in=member_ids).delete() self.members.exclude(character_id__in=member_ids).delete()
@@ -78,23 +73,24 @@ class CorpStats(models.Model):
self.save() self.save()
except TokenError as e: except TokenError as e:
logger.warning("%s failed to update: %s" % (self, e)) logger.warning(f"{self} failed to update: {e}")
if self.token.user: if self.token.user:
notify(self.token.user, "%s failed to update with your ESI token." % self, notify(
message="Your token has expired or is no longer valid. Please add a new one to create a new CorpStats.", self.token.user, "%s failed to update with your ESI token." % self,
level="error") message="Your token has expired or is no longer valid. Please add a new one to create a new CorpStats.",
level="error")
self.delete() self.delete()
except HTTPForbidden as e: except HTTPForbidden as e:
logger.warning("%s failed to update: %s" % (self, e)) logger.warning(f"{self} failed to update: {e}")
if self.token.user: if self.token.user:
notify(self.token.user, "%s failed to update with your ESI token." % self, notify(self.token.user, "%s failed to update with your ESI token." % self, message=f"{e.status_code}: {e.message}", level="error")
message="%s: %s" % (e.status_code, e.message), level="error")
self.delete() self.delete()
except AssertionError: except AssertionError:
logger.warning("%s token character no longer in corp." % self) logger.warning("%s token character no longer in corp." % self)
if self.token.user: if self.token.user:
notify(self.token.user, "%s cannot update with your ESI token." % self, notify(
message="%s cannot update with your ESI token as you have left corp." % self, level="error") self.token.user, "%s cannot update with your ESI token." % self,
message="%s cannot update with your ESI token as you have left corp." % self, level="error")
self.delete() self.delete()
@property @property
@@ -103,7 +99,7 @@ class CorpStats(models.Model):
@property @property
def user_count(self): def user_count(self):
return len(set([m.main_character for m in self.members.all() if m.main_character])) return len({m.main_character for m in self.members.all() if m.main_character})
@property @property
def registered_member_count(self): def registered_member_count(self):
@@ -127,9 +123,7 @@ class CorpStats(models.Model):
@property @property
def mains(self): def mains(self):
return self.members.filter(pk__in=[m.pk for m in self.members.all() if return self.members.filter(pk__in=[m.pk for m in self.members.all() if m.main_character and int(m.main_character.character_id) == int(m.character_id)])
m.main_character and int(m.main_character.character_id) == int(
m.character_id)])
def visible_to(self, user): def visible_to(self, user):
return CorpStats.objects.filter(pk=self.pk).visible_to(user).exists() return CorpStats.objects.filter(pk=self.pk).visible_to(user).exists()

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