Compare commits

...

261 Commits

Author SHA1 Message Date
Basraah
2741a92d31 Version bump to v2.2.0 2019-04-14 03:07:22 +00:00
Basraah
d809902d1e Merge branch 'issue-templates' into 'master'
Create Issue Templates

See merge request allianceauth/allianceauth!1111
2019-03-12 22:16:00 +00:00
Ariel Rin
ec4232c00a Create Issue Templates 2019-03-12 22:16:00 +00:00
Basraah
dec793bfac Merge branch 'dependencies' into 'master'
Depencies fix

Closes #1150

See merge request allianceauth/allianceauth!1127
2019-03-12 20:50:00 +10:00
Basraah
fe3fe0527a Merge branch 'timer-change' into 'master'
Rename Citadels and FLEX Structures

See merge request allianceauth/allianceauth!1126
2019-03-12 10:06:53 +00:00
colcrunch
a8855e86ed Change Type [SIZE] notation for Citadels/EC/Refineries to structure names to reduce ambiguity.
Also, Added "Brand names" for the Beacon, Jammer, and Gate.
2019-03-12 10:06:53 +00:00
Basraah
179d1c38e6 Merge branch 'dj-2.1' into 'master'
Django 2.1 Compatibility

See merge request allianceauth/allianceauth!1124
2019-03-12 10:05:05 +00:00
colcrunch
287da73a4f Update StateBackend.authenticate to match ModelBackend
Also, change setup to no longer include Django 1.11
2019-03-12 10:05:05 +00:00
Basraah
e9ed917888 Merge branch 'fix_discourse_usernames' into 'master'
Send usernames as string instead of array

Closes #1149

See merge request allianceauth/allianceauth!1123
2019-03-12 10:00:52 +00:00
Basraah
70d842c971 Merge branch 'jokke_ilujo/Issue1146' into 'master'
Precent encode mumble username for connect link

See merge request allianceauth/allianceauth!1121
2019-03-12 09:58:07 +00:00
Basraah
bcda228e05 Merge branch 'mysql-docs-fix' into 'master'
Add Time Zone table instructions to Install Docs

See merge request allianceauth/allianceauth!1119
2019-03-12 09:56:20 +00:00
colcrunch
000dafc5e6 Add Time Zone table instructions to Install Docs 2019-03-12 09:56:20 +00:00
Basraah
4ea824fe71 Merge branch 'audit-log-update' into 'master'
Add datetime to Audit Log entries.

Closes #1134

See merge request allianceauth/allianceauth!1115
2019-03-12 09:53:43 +00:00
colcrunch
f72f539516 Add datetime to auditlog entries.
Also, change ordering, add pagination, and stripe the table for increased readability.

Action column now also reads "Removed" when a user is removed from a group. Note that this does not change anything on the back-end, so if you use this data for anything else, be aware that while the template is explicit, the data isn't as explicit.
2019-03-12 09:53:43 +00:00
Stephen Shirley
1b192a184f Send usernames as string instead of array
Fixes https://gitlab.com/allianceauth/allianceauth/issues/1149
2019-02-09 11:10:34 +01:00
Erno Kuvaja
0edf896b4c Precent encode mumble username for connect link
This change wraps mumble username on connect link forming with
urllib.parse.quote() to ensure that the username does not contain
unsafe reserved characters and gets passed properly to mumble.

Fixes Issue: #1146
2019-01-21 14:12:50 +00:00
Basraah
7dec4deb70 Version bump to v2.1.1 2018-12-20 22:17:04 +00:00
Basraah
d511221899 Merge branch 'flex-structures' into 'master'
Add Flex Structures to timerboard

See merge request allianceauth/allianceauth!1112
2018-12-20 22:06:01 +00:00
Basraah
d2b7de5221 Merge branch 'master' into 'master'
Update Readme and fix GitLab announcements

See merge request allianceauth/allianceauth!1113
2018-12-20 22:04:39 +00:00
colcrunch
79c5be02e2 Update Readme and fix GitLab announcements 2018-12-20 22:04:39 +00:00
Basraah
09df37438d Add missing order_by, thanks @Fundaris 2018-12-20 22:02:25 +00:00
Basraah
8561e4c6fd Merge branch 'corpstats-fix' into 'master'
update corpstats swagger.json

Closes #1139

See merge request allianceauth/allianceauth!1116
2018-12-20 21:49:35 +00:00
Aaron Kable
976cb4d988 update corpstats swagger.json 2018-12-20 08:27:56 -05:00
soratidus999
20f7d5103c Add FLEX to past timers as well as present 2018-12-01 17:43:35 +10:00
soratidus999
d049ec2832 Add Flex Structures to timerboard
New FLEX structures as per https://support.eveonline.com/hc/en-us/articles/213021829-Upwell-Structures

Cleaned up layout slightly and removed Stations as there are no longer any conquerable stations left
2018-12-01 17:26:51 +10:00
Basraah
00fe2a527e Version bump to v2.1.0 2018-11-30 07:56:54 +00:00
Basraah
f53ec3b43e Merge branch 'corputils_alliance_fix' into 'master'
None is not a valid alliance ID.

Closes #1122

See merge request allianceauth/allianceauth!1106
2018-11-30 07:54:51 +00:00
colcrunch
4d0417f114 None is not a valid alliance ID. 2018-11-30 07:54:51 +00:00
Basraah
00903b64db Merge branch 'github-to-gitlab' into 'master'
Update GitHub references to Gitlab

See merge request allianceauth/allianceauth!1110
2018-11-30 03:55:19 +00:00
Ariel Rin
a3038cad00 Update GitHub references to Gitlab 2018-11-30 03:55:19 +00:00
Basraah
ef99f1afac Merge branch 'services-market-deprecate' into 'master'
replace market docs with deprecation notice

See merge request allianceauth/allianceauth!1105
2018-11-29 23:55:37 +00:00
Basraah
cc00d4bd04 Merge branch 'services-phpbb3-docsupdate' into 'master'
Set default theme in PHPBB3, explain impact in docs

See merge request allianceauth/allianceauth!1107
2018-11-29 23:51:49 +00:00
Basraah
250f26ff6f Merge branch 'master' into 'master'
django-redis-cache does not support redis 3

See merge request allianceauth/allianceauth!1108
2018-11-29 23:51:22 +00:00
Basraah
62b786ca4a Merge branch 'zkill_fix' into 'master'
Fix srp url for zkill api

See merge request allianceauth/allianceauth!1109
2018-11-29 23:51:09 +00:00
Ariel Rin
9cfb47e658 Limit redis to 2.0 due to 3.0 incompatabilities 2018-11-29 23:12:11 +00:00
Mike
ccef27b637 Fix srp url for zkill api 2018-11-17 20:26:16 -05:00
soratidus999
8dee61fd39 Set default theme in PHPBB3, explain impact in docs
The default theme in PHPBB3 needs to be set or users cannot see the forum, this is needed because AA creates users without a theme set.

Docs explain impact and how to avoid when changing theme later on.
2018-11-15 17:45:33 +10:00
soratidus999
ae64bd0e19 replace docs with deprecation notice 2018-11-14 21:59:05 +10:00
Basraah
a75e93dbfc Version bump to v2.1b1 2018-11-14 10:55:04 +00:00
Basraah
0aa66c5729 Merge branch 'patch-1' into 'master'
limit django, django-celery-beat to compatible versions

See merge request allianceauth/allianceauth!1104
2018-11-11 08:06:39 +00:00
Ariel Rin
4c2434219d limit django, django-celery-beat to compatible versions 2018-11-11 01:55:13 +00:00
Basraah
8c65fda33b Fix incorrect hasattr 2018-10-09 23:58:35 +00:00
Basraah
14f2751bbb Fix typo 2018-10-09 19:44:11 +10:00
Basraah
d37a543c39 Update admin status to work with gitlab 2018-10-09 19:43:44 +10:00
Basraah
4947e0c483 Merge branch 'group_application_fix' into 'master'
Group Application Tweaks

See merge request allianceauth/allianceauth!1096
2018-10-09 05:15:30 +00:00
Basraah
f87c630b86 Merge branch 'fleetup-template-bootstrap-fix' into 'master'
Fixes for FleetUp templates

See merge request allianceauth/allianceauth!1100
2018-10-09 05:15:03 +00:00
Basraah
73789ea734 Prevent multiple migration leaf nodes 2018-10-09 02:15:01 +00:00
Basraah
5a16c9c495 Merge branch 'restrict_group_states' into 'master'
Restrict groups by state.

See merge request allianceauth/allianceauth!1095
2018-10-09 02:06:44 +00:00
colcrunch
9dd8357f67 Restrict groups by state. 2018-10-09 02:06:44 +00:00
Basraah
623e77a268 Avoid Dj2.1 until 1.11 depreciation issues are fixed 2018-10-07 22:45:12 +00:00
Basraah
73403b98df Merge branch 'srp_api_fix' into 'master'
Update SRP module for zKill API changes.

See merge request allianceauth/allianceauth!1102
2018-10-07 22:08:03 +00:00
colcrunch
7aa1a2f336 Update SRP module for zKill API changes. 2018-10-07 22:08:03 +00:00
Peter Pfeufer
08e42d2f56 Serving pilot avatar in fleetup character view from the right host 2018-10-06 13:02:26 +02:00
Peter Pfeufer
69248fd7bb Formatting additional informations text in operations 2018-09-29 10:34:41 +02:00
colcrunch
0af188c6ab Disallow applying to groups the user is already a member of 2018-08-29 21:28:14 -04:00
Peter Pfeufer
8b6d32d0d1 Removed unnecessary HTML comment 2018-08-11 11:09:16 +02:00
Peter Pfeufer
3c11c25d69 Corrected usage of Bootstrap classes in FleetUp templates 2018-08-11 11:03:36 +02:00
colcrunch
12e6cc63e8 Refine auto_leave check. 2018-08-05 03:34:16 -04:00
Basraah
d429c8b59a Grant srp.add_srpfleetmain access to create SRP request
See merge request allianceauth/allianceauth!1098
2018-08-05 02:24:27 +00:00
Basraah
ddd7a3551b Add Audit Log to Group Management
See merge request allianceauth/allianceauth!1089
2018-08-05 02:19:51 +00:00
Basraah
49ede92e06 Skip Teamspeak server admin groups and template groups
See merge request allianceauth/allianceauth!1093
2018-08-05 01:34:34 +00:00
Basraah
b813213328 Fix discourse group sync
See merge request allianceauth/allianceauth!1097
2018-08-04 00:06:30 +00:00
Loïc LEUILLIOT
14065b3ca9 Fix discourse group sync 2018-08-04 00:06:30 +00:00
Basraah
41f9dc490a Merge branch 'fix-fat-ship' into 'master'
Extend Ship Type field on FAT link.

See merge request allianceauth/allianceauth!1099
2018-08-03 04:57:24 +00:00
colcrunch
48d25ca73f Extend Ship Type field on FAT link.
Was not previously long enough for gold pods. Extended further for future-proofing.
2018-08-03 00:32:23 -04:00
Col Crunch
e49e04034c Imports are hard
Note to self: Read before commit
2018-08-01 23:00:31 -04:00
Col Crunch
c7860f8e5c oops 2018-08-01 22:50:44 -04:00
Col Crunch
adb982114a Actually use srp.add_srpfleetmain permission
Also adds a new @permissions_required decorator.
2018-08-01 22:38:54 -04:00
colcrunch
5b8983deac Rename Auditable group.
Its better to be explicit.
2018-07-22 20:08:16 -04:00
colcrunch
1730bc3b98 Add check for auditable groups.
To ensure functionality with other possible changes to group management.
2018-07-22 19:38:56 -04:00
Col Crunch
4374064d98 Case matters 2018-07-11 00:48:51 -04:00
Col Crunch
c1d7994045 Add setting to allow for unrestricted leaving of all groups. 2018-07-11 00:47:04 -04:00
Col Crunch
7bda367cc8 No need to allow more than one request. 2018-07-11 00:11:36 -04:00
Basraah
3de7a2ccd2 Version bump to v2.0.5 2018-07-10 02:27:32 +00:00
Basraah
9cc278df31 Merge branch 'corp_stat_fix' into 'master'
Corp Stats update to fix removal of character name endpoints

See merge request allianceauth/allianceauth!1092
2018-07-10 02:01:26 +00:00
Jamie McMillan
a0bab07e2f Fix indentation 2018-06-26 10:25:29 +01:00
randomic
149bbd92ca Skip server admin groups and template groups 2018-06-26 00:03:02 +01:00
Mike
1de3c989d7 fix tests with new endpoints (i think) 2018-06-24 17:41:27 -04:00
Mike
2e547945e2 Corp Stats update to fix removal of character name endpoints 2018-06-24 17:23:08 -04:00
Col Crunch
4d4a9a27af Merge remote-tracking branch 'allianceauth/master' 2018-06-20 16:14:02 -04:00
Basraah
5b41d0995f Update README.md badges 2018-06-06 07:12:10 +00:00
colcrunch
ab98d72022 Fix migration dependencies. 2018-06-06 07:03:57 +00:00
Unknown
8a7cd3f74d Merge remote-tracking branch 'allianceauth/master' 2018-06-06 03:00:03 -04:00
colcrunch
35cb56d6e9 Update 0009_requestlog.py 2018-06-06 06:41:56 +00:00
Basraah
a7a2ffd16b Add .gitlab-ci.yml 2018-06-06 05:46:35 +00:00
Basraah
dbeda324e0 Update tox.ini for GitLab CI 2018-06-06 05:45:23 +00:00
colcrunch
bf1fe99d98 Add Audit Log to Group Management 2018-06-04 01:45:44 -04:00
colcrunch
41429ec7c7 Merge pull request #3 from allianceauth/master
Sync
2018-06-03 20:59:17 -04:00
Adarnof
ee9ed13a66 Remove reference to depreciated bad_gateway model.
Addresses #1078

I too enjoy breaking changes with no warning. Round two.
2018-05-28 17:16:58 -04:00
Stephen Shirley
490ce286ff Add missing <tr> tags for discourse service template 2018-05-26 13:21:39 -04:00
Adarnof
099c2c0a21 Remove reference to depreciated x-user-agent header.
Addresses #1073

I too enjoy breaking changes with no warning.
2018-05-23 22:58:41 -04:00
Peter Pfeufer
46e15f7fa1 German translation corrected
At least the most obvious mistakes ...
2018-05-16 11:20:29 -04:00
Adarnof
6677e63e08 Correct resetting of permission key.
Thanks @Alf-Life
2018-05-11 10:55:56 -04:00
Adarnof
6d6a3a3d6b Allow viewing corpstats added by the user.
Order corpstats by corp name.
2018-05-10 14:25:57 -04:00
colcrunch
5006246cf1 Build TS perm key using State Information (#1044)
Build permkey with state group id
Pass user object to add_user instead of just username

Fixes #1043
2018-05-09 20:39:14 -04:00
Basraah
6187fb9b86 Timer JS fixes (#1054)
Add months to duration output
Update momentjs
Move EVE time generation function to shared source

Fixes timerboard showing EVE time as local time.
Changed to show 24 hour time.
2018-05-09 20:31:02 -04:00
Adarnof
86f57ccd56 Allow reversing service migrations.
This is probably the wrong way as we should really take care of removing the permission we added, but I don't see a reason anyone would need to migrate back that far as auth wouldn't work anymore without XML api (and even so newer installs don't have the settings referenced so permissions are not automagically added by the migration). So noop is bad but acceptable to me.

Thanks @mmolitor87
2018-05-08 10:06:58 -04:00
colcrunch
854096bac7 fix alliancelogo on corp stats page 2018-05-07 23:26:37 -04:00
Adarnof
9d2b3bb157 Include compiled messages.
It doesn't work without these if DEBUG is False. And users can't compile them outside the allianceauth source directory.

When editing translations, compile with: django-admin compilemessages --settings=allianceauth.project_template.project_name.settings.base
2018-05-02 21:42:26 -04:00
Adarnof
22bda62e59 Spanish translations courtesy of @frank1210
Fixed a few problems with translating the menu links - they had leading spaces.
2018-05-02 20:49:21 -04:00
colcrunch
c8ad1dcc7a Merge pull request #2 from allianceauth/master
sync
2018-05-02 20:01:42 -04:00
Adarnof
7212a7a328 Example supervisor config for authenticator. Ensure ICE is active in config. 2018-05-01 16:40:37 -04:00
Adarnof
f6b1b7b6bb Do not check mains when user has no profile.
This can occur when a user is being deleted: Django deletes the UserProfile, followed by the CharacterOwnerships which triggers the main check. As the user doesn't have a profile it explodes.

Thanks @Slevinator
2018-04-30 17:29:06 -04:00
Adarnof
53a9d72c4a Correct reversing states back to groups. 2018-04-30 17:24:31 -04:00
Adarnof
ca10fbcde5 Translate Member/Blue to custom state names.
Closes #1037
2018-04-25 17:20:28 -04:00
randomic
b4d33e5dfc Fix retry logic being suppressed by try block (#1035) 2018-04-24 11:53:13 -04:00
Adarnof
37bed989f1 Requires mariadb-shared for mysqlclient on centos.
Thanks @rlayne
2018-04-22 12:50:22 -04:00
Adarnof
507eda8a7d Version bump to 2.0 2018-04-21 20:44:15 -04:00
Adarnof
cbe67e9ebc Command to reset unverifiable main characters.
Include section in upgrade docs to run this command and the service account validation one.
2018-04-21 20:28:27 -04:00
Adarnof
cd38200506 Section for adding and removing apps.
People know how to add, but tend not to migrate to zero when removing leading to integrity errors.
2018-04-21 19:49:46 -04:00
Adarnof
5d5cf92a19 Remove sudo from docs.
Include section on logging DEBUG messages.
Cleanup some formatting.
2018-04-21 17:00:18 -04:00
Adarnof
98230d0ee3 Log but don't deal with problems refreshing tokens. 2018-04-20 14:15:19 -04:00
Adarnof
e47c04a0b0 Deactivate services when user loses main character.
This will prevent issues with service username formatting when access permissions are granted to the guest state. While users without mains cannot activate a service they could still retain an active account and it's possible to schedule a nickname update task which would subsequently error out.

Also it seems like a security issue if someone has a service account but their EVE character isn't known. cc8a7a18d2 prevented accessing the services page without a main, now this ensures users don't have an account to manage.
2018-04-20 13:28:41 -04:00
Adarnof
b65ccac58f Revoke CharacterOwnership on token deletion.
I'm pretty sure this is what I meant to do initially. I created the OwnershipRecord system under the assumption that CharacterOwnership models were being deleted when they could no longer be validated. That turned out not to be the case - only main characters were rest. This ensures they are deleted when they can no longer be validated.
2018-04-19 17:13:07 -04:00
Adarnof
bee69cc250 User is created inactive.
When users were created they started active, then were changed to inactive and saved. This triggered service account validation logic which is silly to be running on brand new users. I hated seeing those logging messages so now it doesn't happen.

At the same time I do love logging messages so I added some to the authentication process.
2018-04-19 17:10:38 -04:00
Adarnof
a350e175c7 Update to latest ESI routes. 2018-04-18 20:49:05 -04:00
Adarnof
2cd8188ffb Include a functional market nginx config.
Addresses #1021

Thanks @mmolitor87
2018-04-17 21:37:39 -04:00
Adarnof
b8a2d65a1d Create a separate doc page for upgrade from v1. 2018-04-17 20:59:08 -04:00
Ariel Rin
95f72c854d Minor Documentation Update (#1019)
Gunicorn needs to be run whilist in the folder for context, folder path is not enough
Correct static path, note to check nginx user
Capitalization of services and small typos
Service examples updated to their latest versions and download links
Expanded /var/www chowns for Nginx and Apache examples
Add in a troubleshooting note for no images being displayed (permissions issue) and gunicorn not execting (file path context)
Correct formatting. Reword a few parts. Remove "new in 1.15".
2018-04-17 18:55:18 -04:00
Adarnof
cd8bcfbbb5 Build from github to fix tests in py37-dj20
py37-dj111 still fails for some reason. The only difference in the problematic method between 1.11.12 and 2.0 is whitespace. It's fine in py37-dj20 which is all we really care about so I'm inclined to ignore that issue. py37-dj111 isn't even tested on Travis CI so its failure won't be a problem.

Both django-celery-beat and adarnauth-esi have put out releases supporting dj20 so it's not necessary to build from their source.
2018-04-17 17:40:08 -04:00
Adarnof
08f89d2844 Stop using task_self in bound tasks. 2018-04-17 16:21:43 -04:00
Adarnof
f3f156bf57 Use Django's cache framework for task keys.
Remove depreciated only_one decorator.

Prevent including task_self repr in key name.

Because some tasks are nested in a class, they use a task_self argument instead of the normal self which the celery_once package doesn't recognize to strip out.
2018-04-17 16:21:54 -04:00
Adarnof
73e6f576f4 Use celery_once to prevent repeat task queueing.
Prevent group updates from being queued multiple times per user.

Default graceful to prevent raising exceptions.
2018-04-17 16:21:43 -04:00
Adarnof
20236cab8a Use alliance ticker stored in character table. 2018-04-17 16:18:16 -04:00
Adarnof
6c7b65edad Record alliance ticker in character model.
Closes #1018
2018-04-17 16:18:16 -04:00
Adarnof
21782293ea Create missing Corp/Alliance models.
Thanks @Lof79
2018-04-17 12:08:39 -04:00
Adarnof
e52478c9aa Correct URL template tag.
Thanks @Peggle2K
2018-04-16 19:36:41 -04:00
Adarnof
319cba8653 Allow reconnecting characters to old users.
Addresses #1007
2018-04-14 15:13:42 -04:00
Adarnof
df3acccc50 Correct matching start of URL patterns.
Thanks @Peggle2k
2018-04-14 14:12:39 -04:00
Adarnof
19282cac60 Log messages from esi package. 2018-04-14 13:53:41 -04:00
Adarnof
933c12b48d Increase telnet timeout
Should help tolerate slower responses from remote servers.

Closes #751

Thanks @namenmalkav
2018-04-09 22:14:21 -04:00
Adarnof
8a73890646 Ensure ticker is fetched if alliance_or_corp used.
Closes #1011
2018-04-09 21:53:41 -04:00
Ariel Rin
d6df5184a6 Set minute for Cron to stop mass task creation (#1010) 2018-04-08 23:22:06 -04:00
Adarnof
91e1a374b4 Merge pull request #1009 from kormat/corpstats_comma
Remove trailing , from CELERYBEAT_SCHEDULE example.
2018-04-08 10:39:59 -04:00
Stephen Shirley
c725de7b5b Remove trailing , from CELERYBEAT_SCHEDULE example.
If a user copies the example verbatim, celery logs this error:
```
[2018-04-07 14:57:29,930: ERROR/MainProcess] Cannot add entry 'update_all_corpstats' to database schedule: TypeE rror('from_entry() argument after ** must be a mapping, not tuple',). Contents: ({'task': 'allianceauth.corputil s.tasks.update_all_corpstats', 'schedule': <crontab: 0 */6 * * * (m/h/d/dM/MY)>},)
```
2018-04-08 11:42:18 +02:00
Adarnof
ad1fd633b1 Ensure autogroups are removed if new state has config 2018-04-07 20:59:45 -04:00
Adarnof
ef9284030b Remove autogroups if no config for state. 2018-04-07 20:59:45 -04:00
Adarnof
89e5740027 Update autogroups on main character save
Closes #997
2018-04-07 20:59:45 -04:00
Adarnof
106f6bbcea Fix test user creation. 2018-04-07 20:59:45 -04:00
Adarnof
b53c7a624b Use queryset delete to purge non-refreshable tokens. 2018-04-07 20:49:14 -04:00
Adarnof
6fa788a8f9 Use libmysqlclient-dev on Ubuntu
`libmariadbclient-dev` is unavailable on Xenial (and the suggested replacement `libmariadb-client-lgpl-dev-compat` doesn't have the `mysql_config` symlink patch for whatever reason).

https://bugs.launchpad.net/ubuntu/+source/mariadb-client-lgpl/+bug/1575968
https://bugs.launchpad.net/ubuntu/+source/mariadb-client-lgpl/+bug/1546923
https://anonscm.debian.org/cgit/pkg-mysql/mariadb-client-lgpl.git/commit/debian/libmariadb-dev-compat.links?id=0bbbb8ea0bbeab4a6ebb1e62b92c1ca347061be4

Thanks @kormat
2018-04-07 13:39:20 -04:00
Adarnof
19f0788f47 Merge pull request #1002 from randomic/verify-email-option
Add setting for skipping email requirement
2018-04-03 21:15:38 -04:00
Adarnof
7767226000 Still collect emails from newly registered users.
Log in users immediately if no validation required.
Document new optional setting in project template settings file.
2018-04-03 21:09:43 -04:00
randomic
4eb6b73903 Nameformat configs which default to corp where alliance is None (#1003)
Add nameconfig format for alliance_or_corp_ticker
Add nameconfig format for alliance_or_corp_name
Update docs for new nameformats

Correct missing dict key if no alliance.
2018-04-03 19:25:47 -04:00
Adarnof
cb46ecb002 Correct mysql packages to mariadb on Ubuntu
Thanks @kormat
2018-04-03 15:11:05 -04:00
Adarnof
e694921fe6 Include mandatory DB package notice.
Thanks @zuiji
2018-04-03 13:57:26 -04:00
Adarnof
8266661855 Sanitize username on Discord user join.
Thanks @iakopo
2018-04-02 20:38:12 -04:00
Adarnof
cf7ddbe0e1 Set hostname to domain, not localhost.
Stop using sudo commands. Trust the user to handle permissions.

Closes #994
2018-03-23 11:16:59 -04:00
Adarnof
bdb3ab366f Group list API endpoint has moved.
Allow infinite group cache age.

Thanks @TargetZ3R0
2018-03-22 17:59:24 -04:00
colcrunch
1fc71a0738 Fix celerybeat task in ts3 config. (#998) 2018-03-22 15:43:50 -04:00
colcrunch
0b7520e3b1 Fix celerybeat task in ts3 config. 2018-03-22 15:23:52 -04:00
colcrunch
48c8ccfe97 Merge pull request #1 from allianceauth/master
sync
2018-03-22 15:20:48 -04:00
Adarnof
ad79b4f77c Correct logging string formatting. 2018-03-20 15:51:37 -04:00
Adarnof
fd876b8636 Correct model import.
Thanks @TargetZ3R0
2018-03-19 18:25:47 -04:00
Adarnof
21e896642a Stop using the patch method for setting roles.
Switch to dedicated add/remove endpoints.
Allow setting max cache age to None for infinite.

Apparently patch has issues.

Thanks @TargetZ3R0 and Discord devs <3
2018-03-19 18:08:24 -04:00
Adarnof
b4c395f116 Don't force token updates on main character checks. 2018-03-15 19:41:11 -04:00
Adarnof
a38116014d PyCharm defaults to venv 2018-03-15 19:38:52 -04:00
Adarnof
54223db1e9 Merge remote-tracking branch 'origin/patch1' 2018-03-15 19:36:49 -04:00
Adarnof
8a897abc7b Ensure service URL has protocol.
Thanks @jdrc
2018-03-11 01:08:02 -05:00
Adarnof
fe7b078ec8 Wait until token is deleted before assessing ownerships.
Hopefully this will fix the infinite recursion.
Elevate logging messages to Info so they appear in logs with the default configuration.
2018-03-09 11:47:28 -05:00
Adarnof
ce66bdcbd4 Copy v1 database after creating new one for v2 if updating. 2018-03-02 20:18:23 -05:00
Adarnof
f65e563c0c Update project setup description to match repo and docs.
Thanks @soratidus999
2018-03-02 11:26:35 -05:00
Adarnof
e860ba6c22 Remove pre-v1.13 changelog. It's on the wiki. 2018-03-02 04:03:01 -05:00
Adarnof
50b6605a43 Set folder permissions once user is created.
Remove redundant gunicorn webserver config.

Closes #984
2018-03-02 03:37:14 -05:00
Adarnof
d181200642 Mention Discord bot will never come online.
Update phpbb3 version.
2018-03-02 03:12:36 -05:00
Adarnof
386ba25a44 Add explanation of auth project structure.
Update troubleshooting guide for v2.
Remove homoglyph data files included with confusable-homoglyphs>=3.0
2018-03-02 02:58:55 -05:00
Adarnof
5331d194df Instruct selection of only necessary SSO scopes.
Standardize instructions of adding app to settings.
2018-03-02 01:52:16 -05:00
Adarnof
814ecd233e Most apps use menu item hooks now. 2018-02-28 16:49:39 -05:00
Adarnof
f9a8ac4e9b Bump version to b3 in anticipation of next release. 2018-02-28 13:20:36 -05:00
Adarnof
1bd5eecd54 Correct old template URLs.
Remove redundant name from fatlink.
Remove optimer app dependency.
And other general cleanup.

Thanks @TargetZ3R0
2018-02-28 13:16:54 -05:00
Adarnof
2fa1d9998d Handle custom table prefixes on service databases.
Closes #987

Thanks @Ric878
2018-02-28 10:56:30 -05:00
Adarnof
9d9cfebd9e Specify character set in database to avoid key length errors.
Default database setting uncommented.

Addresses #985
2018-02-27 19:56:19 -05:00
Adarnof
cc8a7a18d2 Hook URLs require logged in user with a main character.
Should prevent anything else like #983

Heavily inspired by https://gist.github.com/garrypolley/3762045#gistcomment-2089316
2018-02-26 22:50:58 -05:00
Adarnof
552c795041 Update project description. 2018-02-24 01:46:53 -05:00
Adarnof
3d757e8d90 Make sure wheel is in venv.
It's hit-and-miss when venvs are created it seems. Doesn't hut to install even if it's already there.
2018-02-24 01:26:31 -05:00
Adarnof
1b5ecaed80 Requirement to sign license agreement for contributing.
Restructure user lists.
Remove reference to long-dead alliance in description.
2018-02-24 00:13:47 -05:00
Adarnof
77c93ed96b Correct broken template tags. 2018-02-23 22:23:42 -05:00
Adarnof
3eeed99af2 Basic fleetup instructions including settings. 2018-02-23 21:47:55 -05:00
Adarnof
a143dfbb37 Add Timerboard Structures, step 2 (#976)
Added additional labels for added structure types
(cherry picked from commit d8f4d56dd8)
2018-02-23 21:36:57 -05:00
Adarnof
6b1da3b18a Briefly document the state system.
Ensure add and delete permissions are created. Not sure why I prevented them - maybe a holdover from an earlier iteration of the state system?
2018-02-23 21:34:48 -05:00
Adarnof
f0894f3415 Update group management docs showing merged admin pages.
Give groupmanagement app a more friendly display name.
2018-02-23 21:01:34 -05:00
Adarnof
539295c1b7 Remove unpopulated first/last name fields from list display.
Include only useful information in list display.
2018-02-23 20:33:07 -05:00
Adarnof
54f91a5bfb Simplify admin inline titles.
More descriptive name format config admin list.
2018-02-23 20:25:59 -05:00
Adarnof
f3c0d05c39 Embed authgroup into group admin.
Mirror authgroup admin permissions from group model.
Delete authgroup permissions.
2018-02-23 18:21:58 -05:00
Adarnof
9f9cc7ed42 Embed profile into user admin.
Restrict main character choices to non-main characters or current main.
If superuser, allow choosing any non-main character.
Proxy user permissions to base model.
Allow all staff to see permission list but not edit.
2018-02-23 17:58:14 -05:00
Adarnof
814b2da0ca Redirect all signals from admin proxy models. 2018-02-23 14:44:12 -05:00
Adarnof
7a9bb0c84b Centralize portrait/logo URL creation. 2018-02-23 12:54:21 -05:00
Adarnof
36ae2af29b Deduplicate login tokens. 2018-02-23 12:25:06 -05:00
Adarnof
d192f23e6e Require exactly django-registration==2.4
This is the newest version which allows installation on Django 2.0 (and indeed does work) that also provides the scheme context to emails.
2018-02-23 11:57:06 -05:00
Adarnof
67cd0cd55c Reassess user groups on state change. 2018-02-23 01:50:03 -05:00
Adarnof
9e53d8b429 Correct migration dependency.
I have no idea what 0016 is, but I'm nuking my dev env to be safe.
2018-02-23 01:26:59 -05:00
Adarnof
f5abf82b95 Allow mapping of states to Teamspeak3 groups.
Addresses #950

Happy now, @colcrunch ?
2018-02-23 01:22:51 -05:00
Adarnof
8dd3a25b52 Remove mentions of no longer used invite code. 2018-02-22 18:50:59 -05:00
Adarnof
d0aa46db08 Accept any ordering of groups in test.
I have no idea why the order is reversing itself. Doesn't matter for functionality which of them comes first. This just checks they're both in there and a comma to separate them.
2018-02-22 18:10:24 -05:00
Adarnof
f0ff70566b Include expected state group in test. 2018-02-22 18:01:14 -05:00
Adarnof
efecf5113b Correct celery eagerness during tests.
I have no idea why this setting name has to be changed. The docs for Celery 4.1.0 (installed) indicate it should be called CELERY_TASK_ALWAYS_EAGER - even with namespace removal TASK_ALWAYS_EAGER doesn't work, but the "old" name of CELERY_ALWAYS_EAGER does.
2018-02-22 18:00:03 -05:00
Adarnof
980569de68 Do not attempt to serialize User models 2018-02-22 17:54:35 -05:00
Adarnof
9c74952607 Correct CorpStats tests. 2018-02-22 17:44:59 -05:00
Adarnof
70c2a4a6e4 Use new endpoint for adding Discord users.
Closes #974
2018-02-22 17:41:38 -05:00
Adarnof
99b136b824 Delete Discord users if they've left the server.
Closes #968
2018-02-22 15:37:29 -05:00
Adarnof
ae4116c0f6 Create new role with desired attributes in one call. 2018-02-22 15:22:35 -05:00
Adarnof
3080d7d868 Prevent new roles from being sorted separately.
Addresses #969
2018-02-22 14:43:59 -05:00
Adarnof
08cf8ae1d6 Capture permission changes from proxy model on admin 2018-02-22 14:28:23 -05:00
Adarnof
3ed0f873f3 Capture signals sent by admin proxy models.
This will prevent those weird missing UserProfile and AuthGroup errors.
Add logging to authentication signals.
Correct reverse migration authservicesinfo creation.
Rename proxy models so they look better on the admin site.
2018-02-22 14:25:43 -05:00
Adarnof
5060d3f408 Ensure login tokens always get attached to the user. 2018-02-22 13:40:02 -05:00
Adarnof
ef24bea562 Put missing logout redirect setting back.
Not sure what I did with it.
2018-02-22 13:31:50 -05:00
Adarnof
c18efaa33d Default login scopes to publicData
We need a refresh token to monitor character ownership but don't need any scopes explicitly. publicData provides no private information but grants a refresh token.

https://github.com/ccpgames/sso-issues/issues/17

Rumor has it this scope isn't going away with CREST.

adarnauth-esi will automatically create a new scope model when it encounters one it doesn't recognize.
2018-02-22 13:27:47 -05:00
Adarnof
b6b14f6f1c Ensure all columns are perfectly aligned.
Prevent sorting/searching portraits and killboard links.
Default sorting to character names.
2018-02-22 13:06:07 -05:00
Adarnof
a90a52f426 Ensure api backoff returns result of decorated function
(cherry picked from commit 91ec924acc)
2018-02-22 02:10:27 -05:00
Adarnof
bd5ea38446 Add a warning against editing base.py
Beautify local.py by removing big block comments.
Move some settings back to base.py which don't need to be in local.py
2018-02-21 22:32:23 -05:00
Adarnof
f8248f46e5 Update docs to reflect refreshing changes 2018-02-21 22:08:45 -05:00
Adarnof
b09c454bf0 Can be updated by any user who can view
Thanks @ghoti
2018-02-21 22:02:46 -05:00
colcrunch
d825689da4 Add settings section to service docs. Remove references to settings.py. (#942)
Standardized the addition of settings instructions.
Changed all references of local.py to a more generic 'auth project settings file'.
Included basic apache and nginx configs.
Include database creation steps.
Instruct users to restart gunicorn and celery after altering settings.
Include missing TS3 celerybeat schedule.
2018-02-21 20:39:54 -05:00
Adarnof
a64dda2a2e Handle HTTP429 on nickname API endpoint
Closes #971
2018-02-21 17:47:20 -05:00
colcrunch
8ce8789631 Discord Sanitization Removal (#947)
No need to sanitize, just prune.
2018-02-21 17:40:41 -05:00
Adarnof
2b2f367c30 Updated Strucure Choices
Added Refineries, and a Moon Mining Option
Also changed spacing to be consistent and be easier to read
(cherry picked from commit 0474fa6d17)
2018-02-21 17:23:36 -05:00
Adarnof
4d194457d8 Include state in service group sync.
The "empty" group will never appear as all users have a state so it has been removed.

I haven't yet found a good way to apply this to Teamspeak - perhaps go back to the token generation logic and create one with a user's state instead of "Member" and exempt those names from group sync?

Addresses #950
2018-02-21 17:11:22 -05:00
Adarnof
6f7cf8805d Correct background resizing on Firefox
https://stackoverflow.com/a/24104710
2018-02-20 15:29:56 -05:00
Adarnof
36e39503c8 Use symbolic links for supervisor conf 2018-02-20 14:46:28 -05:00
Adarnof
e7a24c9cd4 Explicitly forbid logging in as allianceserver 2018-02-20 13:07:16 -05:00
Adarnof
bd8a8922cc Detailed superuser main character instructions 2018-02-20 12:52:00 -05:00
Adarnof
396b2e0fb6 Select all esi scopes when registering application
Thanks @RacerX330
2018-02-20 12:35:11 -05:00
Adarnof
36e382fadb Move SSL header instructions to SSL block 2018-02-20 12:29:12 -05:00
Adarnof
d2666f2440 Instructions for accessing superuser account 2018-02-20 12:20:07 -05:00
Adarnof
397ca97f0f Add missing context to teamspeak join template.
Closes #967
2018-02-13 18:07:34 -05:00
Adarnof
631bb439a4 Remove celery setting namespace.
Somehow it prevents celerybeat tasks from being registered.
Doesn't work with or without the namespace prefix on CELERYBEAT_SCHEDULE

Thanks @warlof
2018-02-12 21:55:19 -05:00
Adarnof
a4003e188e Correct string formatting format
Thanks @warlof
2018-02-12 18:53:23 -05:00
Adarnof
f4a9ba2db8 Remove reference to deleted function. 2018-02-09 01:16:27 -05:00
Adarnof
895a62c475 Include leading http(s) on activation link.
Closes #961
2018-02-09 01:11:35 -05:00
Basraah
ac5a0d9dcb Remove obsolete function call 2018-02-04 19:15:10 +10:00
Adarnof
b8644d5c93 Remove unnecessary next URL from lang select.
This is automatically determined by the lang select view if not specified.
https://docs.djangoproject.com/en/2.0/topics/i18n/translation/#the-set-language-redirect-view
Closes #958
2018-02-02 19:55:59 -05:00
ghoti
4d8baf1af0 V2 Fix redirect issues in HRApps (#951)
Fix redirect issues in HRApps
Allow HR managers to delete reviewed apps
2018-01-11 19:06:10 -05:00
Basraah
f70987de09 Version bump 2018-01-09 12:38:49 +10:00
Basraah
9d02b1530c Update contributors 2018-01-09 12:34:21 +10:00
Basraah
3d532dae01 Fix celery in tests
There's actually a better way to structure tests involving celery since
4.0, but that can wait for some time in the future.
2018-01-09 12:11:54 +10:00
Adarnof
02247b067f Include INSTALLED_APPS setting
Clean up section headers and add a few more.
2018-01-08 10:50:24 -05:00
Basraah
63c2668171 Change static path to match default install 2018-01-08 21:15:19 +10:00
Basraah
5575039126 Remove obsolete section on automatic groups 2018-01-08 21:07:29 +10:00
Basraah
f97c8f2ce4 Fix issue causing queue length query to hang 2018-01-08 18:20:29 +10:00
Adarnof
6baab1d006 New apache guide for proxying to wsgi worker 2018-01-08 00:52:10 -05:00
Adarnof
17adf04860 Correct CentOS supervisor config folder.
Install gunicorn before starting auth project.
2018-01-08 00:14:17 -05:00
Adarnof
f871ecb425 Namespace celery settings to correct broker issues 2018-01-07 19:50:34 -05:00
Adarnof
4a425cde78 Set the email from address for SparkPost
Thanks @colcrunch
2018-01-07 19:39:27 -05:00
Basraah
f56252b0cc Fix celery broker url config 2018-01-06 12:16:30 +10:00
Basraah
7ae6c66beb Fix registration of services signals 2018-01-06 12:08:53 +10:00
Basraah
be90fb96ea Remove unnecessary param 2017-12-28 10:18:50 +10:00
Basraah
dd3350b169 Fix field name 2017-12-28 09:20:11 +10:00
Basraah
cdd1ba1fe3 Use coerce to allow PEP440 to partially work 2017-12-23 15:05:51 +10:00
Basraah
10ea12c867 Prevent error on bad version 2017-12-23 14:44:26 +10:00
Basraah
e6358d948a Fix bug preventing users being added to alliance autogroups
Will need `python manage.py migrate eve_autogroups zero` before updating
2017-12-23 14:43:05 +10:00
Adarnof
1101572f78 Set token user to allow ownership creation 2017-12-22 12:20:55 -05:00
Adarnof
0cf8836832 Use primary key for FK assignment 2017-12-22 11:32:45 -05:00
Adarnof
6e4562b0e6 Don't rely on manager in migration 2017-12-22 11:19:53 -05:00
Basraah
856f1e176a Fix copy paste error, add extra debug output 2017-12-22 11:35:49 +10:00
Adarnof
1653a57e7b Do not set request main character
Thanks @GhostMB
2017-12-21 19:28:39 -05:00
Adarnof
5f03e580c2 Make manager available in migrations
Thanks @mmolitor87
2017-12-21 15:37:54 -05:00
Adarnof
d370ae48a2 Full import path
Python doesn't want to play nice.
2017-12-21 15:32:42 -05:00
Adarnof
38baeba254 Load signals with app 2017-12-21 15:25:53 -05:00
Adarnof
478f9b9390 Fix typo
Thanks @mmolitor87
2017-12-21 15:20:20 -05:00
Adarnof
82ad3821c4 Avoid using model property in migration
https://stackoverflow.com/a/3315547

Thanks @mmolitor87
2017-12-21 15:15:01 -05:00
Adarnof
07afaf12d5 Fix bracket.
Thanks @mmolitor87
2017-12-21 14:13:16 -05:00
206 changed files with 5600 additions and 2394 deletions

6
.gitignore vendored
View File

@@ -8,6 +8,7 @@ __pycache__/
# Distribution / packaging # Distribution / packaging
.Python .Python
env/ env/
venv/
build/ build/
develop-eggs/ develop-eggs/
dist/ dist/
@@ -41,7 +42,6 @@ nosetests.xml
coverage.xml coverage.xml
# Translations # Translations
*.mo
*.pot *.pot
# Django stuff: # Django stuff:
@@ -62,3 +62,7 @@ celerybeat-schedule
#pycharm #pycharm
.idea/* .idea/*
/nbproject/
#gitlab configs
.gitlab/

33
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,33 @@
# Official language image. Look for the different tagged releases at:
# https://hub.docker.com/r/library/python/tags/
.job_template: &job_definition
# Change pip's cache directory to be inside the project directory since we can
# only cache local items.
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache"
# Pip's cache doesn't store the python packages
# https://pip.pypa.io/en/stable/reference/pip_install/#caching
#
# If you want to also cache the installed packages, you have to install
# them in a virtualenv and cache it as well.
cache:
paths:
- .cache/pip
- venv/
before_script:
- python -V # Print out python version for debugging
- pip install virtualenv tox
- virtualenv venv
- source venv/bin/activate
coverage: '/TOTAL.+ ([0-9]{1,3}%)/'
py36-dj20:
<<: *job_definition
image: python:3.6-stretch
script:
- export TOXENV=py36-dj20
- tox

View File

@@ -0,0 +1,14 @@
# Bug
- 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 Operating System:
- Version of other components relevant to issue eg. Service, Database:
Please include a brief description of your issue here.
Please include steps to reproduce the issue
Please include any tracebacks or logs
Please include the results of the command `pip list`

View File

@@ -0,0 +1,7 @@
# Feature Request
- Describe the feature are you requesting.
- 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.

View File

@@ -1,37 +1,35 @@
Alliance Auth Alliance Auth
============ ============
[![Join the chat at https://gitter.im/R4stl1n/allianceauth](https://badges.gitter.im/R4stl1n/allianceauth.svg)](https://gitter.im/R4stl1n/allianceauth?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Chat on Discord](https://img.shields.io/discord/399006117012832262.svg)](https://discord.gg/fjnHAmk)
[![Documentation Status](https://readthedocs.org/projects/allianceauth/badge/?version=latest)](http://allianceauth.readthedocs.io/?badge=latest) [![Documentation Status](https://readthedocs.org/projects/allianceauth/badge/?version=latest)](http://allianceauth.readthedocs.io/?badge=latest)
[![Build Status](https://travis-ci.org/allianceauth/allianceauth.svg?branch=master)](https://travis-ci.org/allianceauth/allianceauth) [![pipeline status](https://gitlab.com/allianceauth/allianceauth/badges/master/pipeline.svg)](https://gitlab.com/allianceauth/allianceauth/commits/master)
[![Coverage Status](https://coveralls.io/repos/github/allianceauth/allianceauth/badge.svg?branch=master)](https://coveralls.io/github/allianceauth/allianceauth?branch=master) [![coverage report](https://gitlab.com/allianceauth/allianceauth/badges/master/coverage.svg)](https://gitlab.com/allianceauth/allianceauth/commits/master)
EVE service auth to help corps, alliances, and coalitions manage services.
Built for "The 99 Percent" open for anyone to use. An auth system for EVE Online to help in-game organizations manage online service access.
[Read the docs here.](http://allianceauth.rtfd.io) [Read the docs here.](http://allianceauth.rtfd.io)
[Get help on gitter](https://gitter.im/R4stl1n/allianceauth) or submit an Issue. [Get help on Discord](https://discord.gg/fjnHAmk) or submit an Issue.
Active Developers: Active Developers:
- [Adarnof](https://github.com/Adarnof) - [Adarnof](https://gitlab.com/adarnof/)
- [Basraah](https://github.com/basraah) - [Basraah](https://gitlab.com/basraah/)
Beta Testers / Bug Fixers: Beta Testers / Bug Fixers:
- [ghoti](https://github.com/ghoti) - [ghoti](https://gitlab.com/ChainsawMcGinny/)
- [mmolitor87](https://gitlab.com/mmolitor87/)
- [TargetZ3R0](https://github.com/TargetZ3R0)
- [kaezon](https://github.com/kaezon/)
- [orbitroom](https://github.com/orbitroom/)
- [tehfiend](https://github.com/tehfiend/)
Special thanks to [Nikdoof](https://github.com/nikdoof/), as his [auth](https://github.com/nikdoof/test-auth) was the foundation for the original work on this project.
Past Beta Testers / Bug Fixers: ### Contributing
Make sure you have signed the [License Agreement](https://developers.eveonline.com/resource/license-agreement) by logging in at [https://developers.eveonline.com](https://developers.eveonline.com) before submitting any pull requests.
- TrentBartlem (Testing and Bug Fixes)
- IskFiend (Bug Fixes and Server Configuration)
- Mr McClain (Bug Fixes and server configuration)
Special Thanks:
- Thanks to Nikdoof, without his old auth implementation this project wouldn't be as far as it is now.

View File

@@ -1,7 +1,6 @@
# 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.0-dev' __version__ = '2.2.0'
NAME = 'Alliance Auth v%s' % __version__ NAME = 'Alliance Auth v%s' % __version__
default_app_config = 'allianceauth.apps.AllianceAuthConfig' default_app_config = 'allianceauth.apps.AllianceAuthConfig'

View File

@@ -1,11 +1,15 @@
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, Permission from django.contrib.auth.models import User as BaseUser, Permission as BasePermission
from django.utils.text import slugify from django.utils.text import slugify
from django.db.models import 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, post_delete, m2m_changed
from allianceauth.authentication.models import State, get_guest_state, CharacterOwnership, UserProfile from django.dispatch import receiver
from allianceauth.authentication.models import State, get_guest_state, CharacterOwnership, UserProfile, OwnershipRecord
from allianceauth.hooks import get_hooks from allianceauth.hooks import get_hooks
from allianceauth.eveonline.models import EveCharacter
from django.forms import ModelForm
def make_service_hooks_update_groups_action(service): def make_service_hooks_update_groups_action(service):
@@ -38,6 +42,47 @@ def make_service_hooks_sync_nickname_action(service):
return sync_nickname return sync_nickname
class QuerysetModelForm(ModelForm):
# allows specifying FK querysets through kwarg
def __init__(self, querysets=None, *args, **kwargs):
querysets = querysets or {}
super().__init__(*args, **kwargs)
for field, qs in querysets.items():
self.fields[field].queryset = qs
class UserProfileInline(admin.StackedInline):
model = UserProfile
readonly_fields = ('state',)
form = QuerysetModelForm
verbose_name = ''
verbose_name_plural = 'Profile'
def get_formset(self, request, obj=None, **kwargs):
# main_character field can only show current value or unclaimed alts
# if superuser, allow selecting from any unclaimed main
query = Q()
if obj and obj.profile.main_character:
query |= Q(pk=obj.profile.main_character_id)
if request.user.is_superuser:
query |= Q(userprofile__isnull=True)
else:
query |= Q(character_ownership__user=obj)
qs = EveCharacter.objects.filter(query)
formset = super().get_formset(request, obj=obj, **kwargs)
def get_kwargs(self, index):
return {'querysets': {'main_character': EveCharacter.objects.filter(query)}}
formset.get_form_kwargs = get_kwargs
return formset
def has_add_permission(self, request):
return False
def has_delete_permission(self, request, obj=None):
return False
class UserAdmin(BaseUserAdmin): class UserAdmin(BaseUserAdmin):
""" """
Extending Django's UserAdmin model Extending Django's UserAdmin model
@@ -62,6 +107,25 @@ class UserAdmin(BaseUserAdmin):
return actions return actions
list_filter = BaseUserAdmin.list_filter + ('profile__state',) list_filter = BaseUserAdmin.list_filter + ('profile__state',)
inlines = BaseUserAdmin.inlines + [UserProfileInline]
list_display = ('username', 'email', 'get_main_character', 'get_state', 'is_active')
def get_main_character(self, obj):
return obj.profile.main_character
get_main_character.short_description = "Main Character"
def get_state(self, obj):
return obj.profile.state
get_state.short_description = "State"
def has_change_permission(self, request, obj=None):
return request.user.has_perm('auth.change_user')
def has_add_permission(self, request, obj=None):
return request.user.has_perm('auth.add_user')
def has_delete_permission(self, request, obj=None):
return request.user.has_perm('auth.delete_user')
@admin.register(State) @admin.register(State)
@@ -96,31 +160,30 @@ class StateAdmin(admin.ModelAdmin):
return obj.userprofile_set.all().count() return obj.userprofile_set.all().count()
@admin.register(UserProfile) class BaseOwnershipAdmin(admin.ModelAdmin):
class UserProfileAdmin(admin.ModelAdmin): list_display = ('user', 'character')
readonly_fields = ('user', 'state') search_fields = ('user__username', 'character__character_name', 'character__corporation_name', 'character__alliance_name')
search_fields = ('user__username', 'main_character__character_name')
list_filter = ('state',)
list_display = ('user', 'main_character')
actions = None
def has_add_permission(self, request): def get_readonly_fields(self, request, obj=None):
return False if obj and obj.pk:
return 'owner_hash', 'character'
return tuple()
def has_delete_permission(self, request, obj=None):
return False @admin.register(OwnershipRecord)
class OwnershipRecordAdmin(BaseOwnershipAdmin):
list_display = BaseOwnershipAdmin.list_display + ('created',)
@admin.register(CharacterOwnership) @admin.register(CharacterOwnership)
class CharacterOwnershipAdmin(admin.ModelAdmin): class CharacterOwnershipAdmin(BaseOwnershipAdmin):
list_display = ('user', 'character') def has_add_permission(self, request):
search_fields = ('user__username', 'character__character_name', 'character__corporation_name', 'character__alliance_name') return False
readonly_fields = ('owner_hash', 'character')
class PermissionAdmin(admin.ModelAdmin): class PermissionAdmin(admin.ModelAdmin):
actions = None actions = None
readonly_fields = [field.name for field in Permission._meta.fields] readonly_fields = [field.name for field in BasePermission._meta.fields]
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',)
@@ -134,23 +197,61 @@ class PermissionAdmin(admin.ModelAdmin):
def has_delete_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None):
return False return False
def has_module_permission(self, request):
return True
def has_change_permission(self, request, obj=None):
# can see list but not edit it
return not obj
# Hack to allow registration of django.contrib.auth models in our authentication app # Hack to allow registration of django.contrib.auth models in our authentication app
class ProxyUser(User): class User(BaseUser):
class Meta: class Meta:
proxy = True proxy = True
verbose_name = User._meta.verbose_name verbose_name = BaseUser._meta.verbose_name
verbose_name_plural = User._meta.verbose_name_plural verbose_name_plural = BaseUser._meta.verbose_name_plural
class ProxyPermission(Permission): class Permission(BasePermission):
class Meta: class Meta:
proxy = True proxy = True
verbose_name = Permission._meta.verbose_name verbose_name = BasePermission._meta.verbose_name
verbose_name_plural = Permission._meta.verbose_name_plural verbose_name_plural = BasePermission._meta.verbose_name_plural
try: try:
admin.site.unregister(User) admin.site.unregister(BaseUser)
finally: finally:
admin.site.register(ProxyUser, UserAdmin) admin.site.register(User, UserAdmin)
admin.site.register(ProxyPermission, PermissionAdmin) admin.site.register(Permission, PermissionAdmin)
@receiver(pre_save, sender=User)
def redirect_pre_save(sender, signal=None, *args, **kwargs):
pre_save.send(BaseUser, *args, **kwargs)
@receiver(post_save, sender=User)
def redirect_post_save(sender, signal=None, *args, **kwargs):
post_save.send(BaseUser, *args, **kwargs)
@receiver(pre_delete, sender=User)
def redirect_pre_delete(sender, signal=None, *args, **kwargs):
pre_delete.send(BaseUser, *args, **kwargs)
@receiver(post_delete, sender=User)
def redirect_post_delete(sender, signal=None, *args, **kwargs):
post_delete.send(BaseUser, *args, **kwargs)
@receiver(m2m_changed, sender=User.groups.through)
def redirect_m2m_changed_groups(sender, signal=None, *args, **kwargs):
m2m_changed.send(BaseUser, *args, **kwargs)
@receiver(m2m_changed, sender=User.user_permissions.through)
def redirect_m2m_changed_permissions(sender, signal=None, *args, **kwargs):
m2m_changed.send(BaseUser, *args, **kwargs)

View File

@@ -1,8 +1,11 @@
from django.contrib.auth.backends import ModelBackend from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.contrib.auth.models import User from django.contrib.auth.models import User
import logging
from .models import UserProfile, CharacterOwnership, OwnershipRecord
from .models import UserProfile, CharacterOwnership
logger = logging.getLogger(__name__)
class StateBackend(ModelBackend): class StateBackend(ModelBackend):
@@ -24,37 +27,54 @@ class StateBackend(ModelBackend):
user_obj._perm_cache.update(self.get_state_permissions(user_obj)) user_obj._perm_cache.update(self.get_state_permissions(user_obj))
return user_obj._perm_cache return user_obj._perm_cache
def authenticate(self, token=None): def authenticate(self, request=None, token=None, **credentials):
if not token: if not token:
return None return None
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))
return ownership.user return ownership.user
else: else:
logger.debug('{0} has changed ownership. Creating new user account.'.format(token.character_name))
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))
# attach an ownership # attach an ownership
token.user = profile.user
CharacterOwnership.objects.create_by_token(token) CharacterOwnership.objects.create_by_token(token)
return profile.user return profile.user
except UserProfile.DoesNotExist: except UserProfile.DoesNotExist:
pass # now we check historical records to see if this is a returning user
records = OwnershipRecord.objects.filter(owner_hash=token.character_owner_hash).filter(character__character_id=token.character_id)
if records.exists():
# we've seen this character owner before. Re-attach to their old user account
user = records[0].user
token.user = user
co = CharacterOwnership.objects.create_by_token(token)
logger.debug('Authenticating {0} by matching owner hash record of character {1}'.format(user, co.character))
if not user.profile.main_character:
# set this as their main by default if they have none
user.profile.main_character = co.character
user.profile.save()
return user
logger.debug('Unable to authenticate character {0}. Creating new user.'.format(token.character_name))
return self.create_user(token) return self.create_user(token)
def create_user(self, token): def create_user(self, token):
username = self.iterate_username(token.character_name) # build unique username off character name username = self.iterate_username(token.character_name) # build unique username off character name
user = User.objects.create_user(username) user = User.objects.create_user(username, is_active=False) # prevent login until email set
user.set_unusable_password() # prevent login via password user.set_unusable_password() # prevent login via password
user.is_active = False # prevent login until email set
user.save() user.save()
token.user = user token.user = user
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))
return user return user
@staticmethod @staticmethod

View File

@@ -0,0 +1,68 @@
from django.conf.urls import include
from django.contrib.auth.decorators import user_passes_test
from django.core.exceptions import PermissionDenied
from functools import wraps
from django.shortcuts import redirect
from django.contrib import messages
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.decorators import login_required
def user_has_main_character(user):
return bool(user.profile.main_character)
def decorate_url_patterns(urls, decorator):
url_list, app_name, namespace = include(urls)
def process_patterns(url_patterns):
for pattern in url_patterns:
if hasattr(pattern, 'url_patterns'):
# this is an include - apply to all nested patterns
process_patterns(pattern.url_patterns)
else:
# this is a pattern
pattern.callback = decorator(pattern.callback)
process_patterns(url_list)
return url_list, app_name, namespace
def main_character_required(view_func):
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
if user_has_main_character(request.user):
return view_func(request, *args, **kwargs)
messages.error(request, _('A main character is required to perform that action. Add one below.'))
return redirect('authentication:dashboard')
return login_required(_wrapped_view)
def permissions_required(perm, login_url=None, raise_exception=False):
"""
Decorator for views that checks whether a user has a particular permission
enabled, redirecting to the log-in page if necessary.
If the raise_exception parameter is given the PermissionDenied exception
is raised.
This decorator is identical to the django permission_required except it
allows for passing a tuple/list of perms that will return true if any one
of them is present.
"""
def check_perms(user):
if isinstance(perm, str):
perms = (perm,)
else:
perms = perm
# First check if the user has the permission (even anon users)
for perm_ in perms:
perm_ = (perm_,)
if user.has_perms(perm_):
return True
# In case the 403 handler should be called raise the exception
if raise_exception:
raise PermissionDenied
# As the last resort, show the login form
return False
return user_passes_test(check_perms, login_url=login_url)

View File

@@ -0,0 +1,20 @@
from django.core.management.base import BaseCommand
from allianceauth.authentication.models import UserProfile
class Command(BaseCommand):
help = 'Ensures all main characters have an active ownership'
def handle(self, *args, **options):
profiles = UserProfile.objects.filter(main_character__isnull=False).filter(
main_character__character_ownership__isnull=True)
if profiles.exists():
for profile in profiles:
self.stdout.write(self.style.ERROR(
'{0} does not have an ownership. Resetting user {1} main character.'.format(profile.main_character,
profile.user)))
profile.main_character = None
profile.save()
self.stdout.write(self.style.WARNING('Reset {0} main characters.'.format(profiles.count())))
else:
self.stdout.write(self.style.SUCCESS('All main characters have active ownership.'))

View File

@@ -43,7 +43,7 @@ def create_member_group(apps, schema_editor):
member_state_name = getattr(settings, 'DEFAULT_AUTH_GROUP', 'Member') member_state_name = getattr(settings, 'DEFAULT_AUTH_GROUP', 'Member')
try: try:
g = Group.objects.get(name=member_state_name) g, _ = Group.objects.get_or_create(name=member_state_name)
# move permissions back # move permissions back
state = State.objects.get(name=member_state_name) state = State.objects.get(name=member_state_name)
[g.permissions.add(p.pk) for p in state.permissions.all()] [g.permissions.add(p.pk) for p in state.permissions.all()]
@@ -51,7 +51,7 @@ def create_member_group(apps, schema_editor):
# move users back # move users back
for profile in state.userprofile_set.all().select_related('user'): for profile in state.userprofile_set.all().select_related('user'):
profile.user.groups.add(g.pk) profile.user.groups.add(g.pk)
except (Group.DoesNotExist, State.DoesNotExist): except State.DoesNotExist:
pass pass
@@ -67,7 +67,7 @@ def create_blue_state(apps, schema_editor):
# move group permissions to state # move group permissions to state
g = Group.objects.get(name=blue_state_name) g = Group.objects.get(name=blue_state_name)
[s.permissions.add(p.pk) for p in g.permissions.all()] [s.permissions.add(p.pk) for p in g.permissions.all()]
g.permissions.clear() g.delete()
except Group.DoesNotExist: except Group.DoesNotExist:
pass pass
@@ -84,7 +84,7 @@ def create_blue_group(apps, schema_editor):
blue_state_name = getattr(settings, 'DEFAULT_BLUE_GROUP', 'Blue') blue_state_name = getattr(settings, 'DEFAULT_BLUE_GROUP', 'Blue')
try: try:
g = Group.objects.get(name=blue_state_name) g, _ = Group.objects.get_or_create(name=blue_state_name)
# move permissions back # move permissions back
state = State.objects.get(name=blue_state_name) state = State.objects.get(name=blue_state_name)
[g.permissions.add(p.pk) for p in state.permissions.all()] [g.permissions.add(p.pk) for p in state.permissions.all()]
@@ -92,10 +92,15 @@ def create_blue_group(apps, schema_editor):
# move users back # move users back
for profile in state.userprofile_set.all().select_related('user'): for profile in state.userprofile_set.all().select_related('user'):
profile.user.groups.add(g.pk) profile.user.groups.add(g.pk)
except (Group.DoesNotExist, State.DoesNotExist): except State.DoesNotExist:
pass pass
def purge_tokens(apps, schema_editor):
Token = apps.get_model('esi', 'Token')
Token.objects.filter(refresh_token__isnull=True).delete()
def populate_ownerships(apps, schema_editor): def populate_ownerships(apps, schema_editor):
Token = apps.get_model('esi', 'Token') Token = apps.get_model('esi', 'Token')
CharacterOwnership = apps.get_model('authentication', 'CharacterOwnership') CharacterOwnership = apps.get_model('authentication', 'CharacterOwnership')
@@ -103,16 +108,16 @@ def populate_ownerships(apps, schema_editor):
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:
ts = tokens.filter(character_id=c_id).order_by('created') # find newest refreshable token and use it as basis for CharacterOwnership
for t in ts: ts = tokens.filter(character_id=c_id).exclude(refresh_token__isnull=True).order_by('created')
if t.can_refresh: if ts.exists():
# find newest refreshable token and use it as basis for CharacterOwnership token = ts[0]
CharacterOwnership.objecs.create_by_token(t) char = EveCharacter.objects.get(character_id=token.character_id)
break CharacterOwnership.objects.create(user_id=token.user_id, character_id=char.id, owner_hash=token.character_owner_hash)
def create_profiles(apps, schema_editor): def create_profiles(apps, schema_editor):
@@ -128,15 +133,24 @@ def create_profiles(apps, schema_editor):
auth['n'] == 1 and EveCharacter.objects.filter(character_id=auth['main_char_id']).exists()] auth['n'] == 1 and EveCharacter.objects.filter(character_id=auth['main_char_id']).exists()]
auths = AuthServicesInfo.objects.filter(main_char_id__in=unique_mains).select_related('user') auths = AuthServicesInfo.objects.filter(main_char_id__in=unique_mains).select_related('user')
blue_state_name = getattr(settings, 'DEFAULT_BLUE_GROUP', 'Blue')
member_state_name = getattr(settings, 'DEFAULT_AUTH_GROUP', 'Member')
states = {
'Member': State.objects.get(name=member_state_name),
'Blue': State.objects.get(name=blue_state_name),
}
guest_state = State.objects.get(name='Guest')
for auth in auths: for auth in auths:
# carry states and mains forward # carry states and mains forward
state = State.objects.get(name=auth.state if auth.state else 'Guest') state = states.get(auth.state, guest_state)
char = EveCharacter.objects.get(character_id=auth.main_char_id) char = EveCharacter.objects.get(character_id=auth.main_char_id)
UserProfile.objects.create(user=auth.user, state=state, main_character=char) UserProfile.objects.create(user=auth.user, state=state, main_character=char)
for auth in AuthServicesInfo.objects.exclude(main_char_id__in=unique_mains).select_related('user'): for auth in AuthServicesInfo.objects.exclude(main_char_id__in=unique_mains).select_related('user'):
# prepare empty profiles # prepare empty profiles
state = State.objects.get(name='Guest') UserProfile.objects.create(user=auth.user, state=guest_state)
UserProfile.objects.create(user=auth.user, state=state)
def recreate_authservicesinfo(apps, schema_editor): def recreate_authservicesinfo(apps, schema_editor):
@@ -144,8 +158,16 @@ def recreate_authservicesinfo(apps, schema_editor):
UserProfile = apps.get_model('authentication', 'UserProfile') UserProfile = apps.get_model('authentication', 'UserProfile')
User = apps.get_model('auth', 'User') User = apps.get_model('auth', 'User')
blue_state_name = getattr(settings, 'DEFAULT_BLUE_GROUP', 'Blue')
member_state_name = getattr(settings, 'DEFAULT_AUTH_GROUP', 'Member')
states = {
member_state_name: 'Member',
blue_state_name: 'Blue',
}
# recreate all missing AuthServicesInfo models # recreate all missing AuthServicesInfo models
AuthServicesInfo.objects.bulk_create([AuthServicesInfo(user=u.pk) for u in User.objects.all()]) AuthServicesInfo.objects.bulk_create([AuthServicesInfo(user_id=u.pk) for u in User.objects.all()])
# 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'):
@@ -154,8 +176,8 @@ def recreate_authservicesinfo(apps, schema_editor):
# 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(
state__name__in=['Member', 'Blue']).select_related('user', 'state'): state__name__in=[member_state_name, blue_state_name]).select_related('user', 'state'):
AuthServicesInfo.objects.update_or_create(user=profile.user, defaults={'state': profile.state.name}) AuthServicesInfo.objects.update_or_create(user=profile.user, defaults={'state': states[profile.state.name]})
def disable_passwords(apps, schema_editor): def disable_passwords(apps, schema_editor):
@@ -203,7 +225,6 @@ class Migration(migrations.Migration):
('permissions', models.ManyToManyField(blank=True, to='auth.Permission')), ('permissions', models.ManyToManyField(blank=True, to='auth.Permission')),
], ],
options={ options={
'default_permissions': ('change',),
'ordering': ['-priority'], 'ordering': ['-priority'],
}, },
), ),
@@ -222,6 +243,7 @@ class Migration(migrations.Migration):
migrations.RunPython(create_guest_state, migrations.RunPython.noop), migrations.RunPython(create_guest_state, migrations.RunPython.noop),
migrations.RunPython(create_member_state, create_member_group), migrations.RunPython(create_member_state, create_member_group),
migrations.RunPython(create_blue_state, create_blue_group), migrations.RunPython(create_blue_state, create_blue_group),
migrations.RunPython(purge_tokens, migrations.RunPython.noop),
migrations.RunPython(populate_ownerships, migrations.RunPython.noop), migrations.RunPython(populate_ownerships, migrations.RunPython.noop),
migrations.RunPython(create_profiles, recreate_authservicesinfo), migrations.RunPython(create_profiles, recreate_authservicesinfo),
migrations.RemoveField( migrations.RemoveField(
@@ -233,7 +255,7 @@ class Migration(migrations.Migration):
), ),
migrations.RunPython(disable_passwords, migrations.RunPython.noop), migrations.RunPython(disable_passwords, migrations.RunPython.noop),
migrations.CreateModel( migrations.CreateModel(
name='ProxyPermission', name='Permission',
fields=[ fields=[
], ],
options={ options={
@@ -247,7 +269,7 @@ class Migration(migrations.Migration):
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='ProxyUser', name='User',
fields=[ fields=[
], ],
options={ options={

View File

@@ -0,0 +1,40 @@
# Generated by Django 2.0.4 on 2018-04-14 18:28
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
def create_initial_records(apps, schema_editor):
OwnershipRecord = apps.get_model('authentication', 'OwnershipRecord')
CharacterOwnership = apps.get_model('authentication', 'CharacterOwnership')
OwnershipRecord.objects.bulk_create([
OwnershipRecord(user=o.user, character=o.character, owner_hash=o.owner_hash) for o in CharacterOwnership.objects.all()
])
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('eveonline', '0009_on_delete'),
('authentication', '0015_user_profiles'),
]
operations = [
migrations.CreateModel(
name='OwnershipRecord',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('owner_hash', models.CharField(db_index=True, max_length=28)),
('created', models.DateTimeField(auto_now=True)),
('character', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ownership_records', to='eveonline.EveCharacter')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ownership_records', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-created'],
},
),
migrations.RunPython(create_initial_records, migrations.RunPython.noop)
]

View File

@@ -96,3 +96,16 @@ class CharacterOwnership(models.Model):
def __str__(self): def __str__(self):
return "%s: %s" % (self.user, self.character) return "%s: %s" % (self.user, self.character)
class OwnershipRecord(models.Model):
character = models.ForeignKey(EveCharacter, on_delete=models.CASCADE, related_name='ownership_records')
owner_hash = models.CharField(max_length=28, db_index=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='ownership_records')
created = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created']
def __str__(self):
return "%s: %s on %s" % (self.user, self.character, self.created)

View File

@@ -1,9 +1,9 @@
import logging import logging
from .models import CharacterOwnership, UserProfile, get_guest_state, State from .models import CharacterOwnership, UserProfile, get_guest_state, State, OwnershipRecord
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import Q from django.db.models import Q
from django.db.models.signals import post_save, pre_delete, m2m_changed, pre_save from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed
from django.dispatch import receiver, Signal from django.dispatch import receiver, Signal
from esi.models import Token from esi.models import Token
@@ -11,7 +11,6 @@ from allianceauth.eveonline.models import EveCharacter
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
state_changed = Signal(providing_args=['user', 'state']) state_changed = Signal(providing_args=['user', 'state'])
@@ -32,23 +31,27 @@ 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))
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))
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))
trigger_state_check(instance) 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))
trigger_state_check(instance) trigger_state_check(instance)
@@ -59,6 +62,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))
instance.assign_state() instance.assign_state()
@@ -66,12 +70,15 @@ 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))
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,
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:
@@ -80,10 +87,15 @@ 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,
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(character__character_id=instance.character_id).exists(): if instance.user and not CharacterOwnership.objects.filter(
character__character_id=instance.character_id).exists():
logger.debug("Character {0} is not yet owned. Assigning ownership to {1}".format(instance.character_name,
instance.user))
CharacterOwnership.objects.update_or_create(character=char, CharacterOwnership.objects.update_or_create(character=char,
defaults={'owner_hash': instance.character_owner_hash, defaults={'owner_hash': instance.character_owner_hash,
'user': instance.user}) 'user': instance.user})
@@ -91,20 +103,23 @@ def record_character_ownership(sender, instance, created, *args, **kwargs):
@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):
if instance.user.profile.main_character == instance.character: try:
# clear main character as user no longer owns them if instance.user.profile.main_character == instance.character:
instance.user.profile.main_character = None logger.info("Ownership of a main character {0} has been revoked. Resetting {1} main character.".format(
instance.user.profile.save() instance.character, instance.user))
# clear main character as user no longer owns them
instance.user.profile.main_character = None
instance.user.profile.save()
except UserProfile.DoesNotExist:
# a user is being deleted
pass
@receiver(pre_delete, sender=Token) @receiver(post_delete, sender=Token)
def validate_main_character_token(sender, instance, *args, **kwargs): def validate_ownership(sender, instance, *args, **kwargs):
if UserProfile.objects.filter(main_character__character_id=instance.character_id).exists(): if not Token.objects.filter(character_owner_hash=instance.character_owner_hash).filter(refresh_token__isnull=False).exists():
profile = UserProfile.objects.get(main_character__character_id=instance.character_id) logger.info("No remaining tokens to validate ownership of character {0}. Revoking ownership.".format(instance.character_name))
if not Token.objects.filter(character_id=instance.character_id).filter(user=profile.user).exclude(pk=instance.pk).exists(): CharacterOwnership.objects.filter(owner_hash=instance.character_owner_hash).delete()
# clear main character as we can no longer verify ownership
profile.main_character = None
profile.save()
@receiver(pre_save, sender=User) @receiver(pre_save, sender=User)
@@ -114,8 +129,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))
instance.profile.assign_state() instance.profile.assign_state()
else: else:
logger.debug(
"User {0} has been deactivated. Revoking state and assigning to guest state.".format(instance))
instance.profile.state = get_guest_state() instance.profile.state = get_guest_state()
instance.profile.save(update_fields=['state']) instance.profile.save(update_fields=['state'])
@@ -124,6 +142,20 @@ 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))
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))
pass pass
@receiver(post_save, sender=CharacterOwnership)
def ownership_record_creation(sender, instance, created, *args, **kwargs):
if created:
records = OwnershipRecord.objects.filter(owner_hash=instance.owner_hash).filter(character=instance.character)
if records.exists():
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))
return
logger.info("Character {0} has a new owner {1}. Creating ownership record.".format(instance.character, instance.user))
OwnershipRecord.objects.create(user=instance.user, character=instance.character, owner_hash=instance.owner_hash)

View File

@@ -1,6 +1,6 @@
import logging import logging
from esi.errors import TokenExpiredError, TokenInvalidError from esi.errors import TokenExpiredError, TokenInvalidError, IncompleteResponseError
from esi.models import Token from esi.models import Token
from celery import shared_task from celery import shared_task
@@ -20,13 +20,19 @@ def check_character_ownership(owner_hash):
except (TokenExpiredError, TokenInvalidError): except (TokenExpiredError, TokenInvalidError):
t.delete() t.delete()
continue continue
except (KeyError, IncompleteResponseError):
if t.character_owner_hash == old_hash: # 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(
tokens[0].character_name))
break break
else:
logger.info('Character %s has changed ownership. Revoking %s tokens.' % (t.character_name, tokens.count())) if not t.character_owner_hash == old_hash:
logger.info(
'Character %s has changed ownership. Revoking %s tokens.' % (t.character_name, tokens.count()))
tokens.delete() tokens.delete()
else: break
if not Token.objects.filter(character_owner_hash=owner_hash).exists():
logger.info('No tokens found with owner hash %s. Revoking ownership.' % owner_hash) logger.info('No tokens found with owner hash %s. Revoking ownership.' % owner_hash)
CharacterOwnership.objects.filter(owner_hash=owner_hash).delete() CharacterOwnership.objects.filter(owner_hash=owner_hash).delete()

View File

@@ -21,7 +21,7 @@
<table class="table"> <table class="table">
<tr> <tr>
<td class="text-center"><img class="ra-avatar" <td class="text-center"><img class="ra-avatar"
src="https://image.eveonline.com/Character/{{ main.character_id }}_128.jpg"> src="{{ main.portrait_url_128 }}">
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -57,7 +57,7 @@
</div> </div>
{% endwith %} {% endwith %}
{% else %} {% else %}
<div class="alert alert-danger" role="alert">{% trans "Missing main character model." %}</div> <div class="alert alert-danger" role="alert">{% trans "No main character set." %}</div>
{% endif %} {% endif %}
<div class="clearfix"></div> <div class="clearfix"></div>
<div class="col-xs-6"> <div class="col-xs-6">
@@ -102,8 +102,7 @@
{% for ownership in request.user.character_ownerships.all %} {% for ownership in request.user.character_ownerships.all %}
{% with ownership.character as char %} {% with ownership.character as char %}
<tr> <tr>
<td class="text-center"><img class="ra-avatar img-circle" <td class="text-center"><img class="ra-avatar img-circle" src="{{ char.portrait_url_32 }}">
src="https://image.eveonline.com/Character/{{ char.character_id }}_32.jpg">
</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>

View File

@@ -17,7 +17,7 @@
<style> <style>
body { body {
background: url('{% static 'authentication/img/background.jpg' %}') no-repeat scroll; background: url('{% static 'authentication/img/background.jpg' %}') no-repeat center center fixed;
-webkit-background-size: cover; -webkit-background-size: cover;
-moz-background-size: cover; -moz-background-size: cover;
-o-background-size: cover; -o-background-size: cover;
@@ -48,4 +48,4 @@
{% endblock %} {% endblock %}
</div> </div>
</body> </body>
</html> </html>

View File

@@ -2,7 +2,6 @@
<div class="dropdown"> <div class="dropdown">
<form action="{% url 'set_language' %}" method="post"> <form action="{% url 'set_language' %}" method="post">
{% csrf_token %} {% csrf_token %}
<input name="next" type="hidden" value="{{ request.get_full_path|slice:'3:' }}" />
<select onchange="this.form.submit()" class="form-control" id="lang-select" name="language"> <select onchange="this.form.submit()" class="form-control" id="lang-select" name="language">
{% get_language_info_list for LANGUAGES as languages %} {% get_language_info_list for LANGUAGES as languages %}
{% for language in languages %} {% for language in languages %}
@@ -12,4 +11,4 @@
{% endfor %} {% endfor %}
</select> </select>
</form> </form>
</div> </div>

View File

@@ -2,7 +2,7 @@ You're receiving this email because someone has entered this email address while
If this was you, please go to the following URL to confirm your email address: If this was you, please go to the following URL to confirm your email address:
{{ url }} {{ scheme }}://{{ url }}
This link will expire in {{ expiration_days }} day(s). This link will expire in {{ expiration_days }} day(s).

View File

@@ -1,13 +1,70 @@
from unittest import mock from unittest import mock
from io import StringIO
from django.test import TestCase from django.test import TestCase
from django.contrib.auth.models import User from django.contrib.auth.models import User
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
from .models import CharacterOwnership, UserProfile, State, get_guest_state from .models import CharacterOwnership, UserProfile, State, get_guest_state, OwnershipRecord
from .backends import StateBackend from .backends import StateBackend
from .tasks import check_character_ownership from .tasks import check_character_ownership
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo
from esi.models import Token from esi.models import Token
from esi.errors import IncompleteResponseError
from allianceauth.authentication.decorators import main_character_required
from django.test.client import RequestFactory
from django.http.response import HttpResponse
from django.contrib.auth.models import AnonymousUser
from django.conf import settings
from django.shortcuts import reverse
from django.core.management import call_command
from urllib import parse
MODULE_PATH = 'allianceauth.authentication'
class DecoratorTestCase(TestCase):
@staticmethod
@main_character_required
def dummy_view(*args, **kwargs):
return HttpResponse(status=200)
@classmethod
def setUpTestData(cls):
cls.main_user = AuthUtils.create_user('main_user', disconnect_signals=True)
cls.no_main_user = AuthUtils.create_user('no_main_user', disconnect_signals=True)
main_character = EveCharacter.objects.create(
character_id=1,
character_name='Main Character',
corporation_id=1,
corporation_name='Corp',
corporation_ticker='CORP',
)
CharacterOwnership.objects.create(user=cls.main_user, character=main_character, owner_hash='1')
cls.main_user.profile.main_character = main_character
def setUp(self):
self.request = RequestFactory().get('/test/')
@mock.patch(MODULE_PATH + '.decorators.messages')
def test_login_redirect(self, m):
setattr(self.request, 'user', AnonymousUser())
response = self.dummy_view(self.request)
self.assertEqual(response.status_code, 302)
url = getattr(response, 'url', None)
self.assertEqual(parse.urlparse(url).path, reverse(settings.LOGIN_URL))
@mock.patch(MODULE_PATH + '.decorators.messages')
def test_main_character_redirect(self, m):
setattr(self.request, 'user', self.no_main_user)
response = self.dummy_view(self.request)
self.assertEqual(response.status_code, 302)
url = getattr(response, 'url', None)
self.assertEqual(url, reverse('authentication:dashboard'))
@mock.patch(MODULE_PATH + '.decorators.messages')
def test_successful_request(self, m):
setattr(self.request, 'user', self.main_user)
response = self.dummy_view(self.request)
self.assertEqual(response.status_code, 200)
class BackendTestCase(TestCase): class BackendTestCase(TestCase):
@@ -35,6 +92,7 @@ class BackendTestCase(TestCase):
corporation_ticker='CORP', corporation_ticker='CORP',
) )
cls.user = AuthUtils.create_user('test_user', disconnect_signals=True) cls.user = AuthUtils.create_user('test_user', disconnect_signals=True)
cls.old_user = AuthUtils.create_user('old_user', disconnect_signals=True)
AuthUtils.disconnect_signals() AuthUtils.disconnect_signals()
CharacterOwnership.objects.create(user=cls.user, character=cls.main_character, owner_hash='1') CharacterOwnership.objects.create(user=cls.user, character=cls.main_character, owner_hash='1')
CharacterOwnership.objects.create(user=cls.user, character=cls.alt_character, owner_hash='2') CharacterOwnership.objects.create(user=cls.user, character=cls.alt_character, owner_hash='2')
@@ -58,6 +116,14 @@ class BackendTestCase(TestCase):
self.assertEqual(user.username, 'Unclaimed_Character') self.assertEqual(user.username, 'Unclaimed_Character')
self.assertEqual(user.profile.main_character, self.unclaimed_character) self.assertEqual(user.profile.main_character, self.unclaimed_character)
def test_authenticate_character_record(self):
t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='4')
record = OwnershipRecord.objects.create(user=self.old_user, character=self.unclaimed_character, owner_hash='4')
user = StateBackend().authenticate(token=t)
self.assertEqual(user, self.old_user)
self.assertTrue(CharacterOwnership.objects.filter(owner_hash='4', user=self.old_user).exists())
self.assertTrue(user.profile.main_character)
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')
@@ -130,28 +196,6 @@ class CharacterOwnershipTestCase(TestCase):
self.user = User.objects.get(pk=self.user.pk) self.user = User.objects.get(pk=self.user.pk)
self.assertIsNone(self.user.profile.main_character) self.assertIsNone(self.user.profile.main_character)
@mock.patch('esi.models.Token.update_token_data')
def test_character_ownership_check(self, update_token_data):
t = Token.objects.create(
user=self.user,
character_id=self.character.character_id,
character_name=self.character.character_name,
character_owner_hash='1',
)
co = CharacterOwnership.objects.get(owner_hash='1')
check_character_ownership(co.owner_hash)
self.assertTrue(CharacterOwnership.objects.filter(owner_hash='1').exists())
t.character_owner_hash = '2'
t.save()
check_character_ownership(co.owner_hash)
self.assertFalse(CharacterOwnership.objects.filter(owner_hash='1').exists())
t.delete()
co = CharacterOwnership.objects.create(user=self.user, character=self.character, owner_hash='3')
check_character_ownership(co.owner_hash)
self.assertFalse(CharacterOwnership.objects.filter(owner_hash='3').exists())
class StateTestCase(TestCase): class StateTestCase(TestCase):
@classmethod @classmethod
@@ -286,3 +330,73 @@ class StateTestCase(TestCase):
self.user.save() self.user.save()
self._refresh_user() self._refresh_user()
self.assertEquals(self.user.profile.state, self.member_state) self.assertEquals(self.user.profile.state, self.member_state)
class CharacterOwnershipCheckTestCase(TestCase):
@classmethod
def setUpTestData(cls):
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',
corp_name='Test Corp', alliance_name='Test Alliance')
cls.character = EveCharacter.objects.get(character_id='1')
cls.token = Token.objects.create(
user=cls.user,
character_id='1',
character_name='Test',
character_owner_hash='1',
)
cls.ownership = CharacterOwnership.objects.get(character=cls.character)
@mock.patch(MODULE_PATH + '.tasks.Token.update_token_data')
def test_no_change_owner_hash(self, update_token_data):
# makes sure the ownership isn't delete if owner hash hasn't changed
check_character_ownership(self.ownership)
self.assertTrue(CharacterOwnership.objects.filter(user=self.user).filter(character=self.character).exists())
@mock.patch(MODULE_PATH + '.tasks.Token.update_token_data')
def test_unable_to_update_token_data(self, update_token_data):
# makes sure ownerships and tokens aren't hellpurged when there's problems with the SSO servers
update_token_data.side_effect = IncompleteResponseError()
check_character_ownership(self.ownership)
self.assertTrue(CharacterOwnership.objects.filter(user=self.user).filter(character=self.character).exists())
update_token_data.side_effect = KeyError()
check_character_ownership(self.ownership)
self.assertTrue(CharacterOwnership.objects.filter(user=self.user).filter(character=self.character).exists())
@mock.patch(MODULE_PATH + '.tasks.Token.update_token_data')
@mock.patch(MODULE_PATH + '.tasks.Token.delete')
@mock.patch(MODULE_PATH + '.tasks.Token.objects.exists')
@mock.patch(MODULE_PATH + '.tasks.CharacterOwnership.objects.filter')
def test_owner_hash_changed(self, filter, exists, delete, update_token_data):
# makes sure the ownership is revoked when owner hash changes
filter.return_value.exists.return_value = False
check_character_ownership(self.ownership)
self.assertTrue(filter.return_value.delete.called)
class ManagementCommandTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = AuthUtils.create_user('test user', disconnect_signals=True)
AuthUtils.add_main_character(cls.user, 'test character', '1', '2', 'test corp', 'test')
character = UserProfile.objects.get(user=cls.user).main_character
CharacterOwnership.objects.create(user=cls.user, character=character, owner_hash='test')
def setUp(self):
self.stdout = StringIO()
def test_ownership(self):
call_command('checkmains', stdout=self.stdout)
self.assertFalse(UserProfile.objects.filter(main_character__isnull=True).count())
self.assertNotIn(self.user.username, self.stdout.getvalue())
self.assertIn('All main characters', self.stdout.getvalue())
def test_no_ownership(self):
user = AuthUtils.create_user('v1 user', disconnect_signals=True)
AuthUtils.add_main_character(user, 'v1 character', '10', '20', 'test corp', 'test')
self.assertFalse(UserProfile.objects.filter(main_character__isnull=True).count())
call_command('checkmains', stdout=self.stdout)
self.assertEqual(UserProfile.objects.filter(main_character__isnull=True).count(), 1)
self.assertIn(user.username, self.stdout.getvalue())

View File

@@ -10,6 +10,7 @@ from django.urls import reverse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from esi.decorators import token_required from esi.decorators import token_required
from esi.models import Token
from registration.backends.hmac.views import RegistrationView as BaseRegistrationView, \ from registration.backends.hmac.views import RegistrationView as BaseRegistrationView, \
ActivationView as BaseActivationView, REGISTRATION_SALT ActivationView as BaseActivationView, REGISTRATION_SALT
from registration.signals import user_registered from registration.signals import user_registered
@@ -71,17 +72,22 @@ have the email address embedded much like the username. Key creation and decodin
@token_required(new=True, scopes=settings.LOGIN_TOKEN_SCOPES) @token_required(new=True, scopes=settings.LOGIN_TOKEN_SCOPES)
def sso_login(request, token): def sso_login(request, token):
user = authenticate(token=token) user = authenticate(token=token)
if user and user.is_active: if user:
login(request, user) token.user = user
return redirect(request.POST.get('next', request.GET.get('next', 'authentication:dashboard'))) if Token.objects.exclude(pk=token.pk).equivalent_to(token).require_valid().exists():
elif user and not user.email: token.delete()
# Store the new user PK in the session to enable us to identify the registering user in Step 2 else:
request.session['registration_uid'] = user.pk token.save()
# Go to Step 2 if user.is_active:
return redirect('registration_register') login(request, user)
else: return redirect(request.POST.get('next', request.GET.get('next', 'authentication:dashboard')))
messages.error(request, _('Unable to authenticate as the selected character.')) elif not user.email:
return redirect(settings.LOGIN_URL) # Store the new user PK in the session to enable us to identify the registering user in Step 2
request.session['registration_uid'] = user.pk
# Go to Step 2
return redirect('registration_register')
messages.error(request, _('Unable to authenticate as the selected character.'))
return redirect(settings.LOGIN_URL)
# Step 2 # Step 2
@@ -89,20 +95,33 @@ class RegistrationView(BaseRegistrationView):
form_class = RegistrationForm form_class = RegistrationForm
success_url = 'authentication:dashboard' success_url = 'authentication:dashboard'
def dispatch(self, *args, **kwargs): def get_success_url(self, user):
if not getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True):
return 'authentication:dashboard', (), {}
return super().get_success_url(user)
def dispatch(self, request, *args, **kwargs):
# We're storing a key in the session to pass user information from OAuth response. Make sure it's there. # We're storing a key in the session to pass user information from OAuth response. Make sure it's there.
if not self.request.session.get('registration_uid', None) or not User.objects.filter( if not self.request.session.get('registration_uid', None) or not User.objects.filter(
pk=self.request.session.get('registration_uid')).exists(): pk=self.request.session.get('registration_uid')).exists():
messages.error(self.request, _('Registration token has expired.')) messages.error(self.request, _('Registration token has expired.'))
return redirect(settings.LOGIN_URL) return redirect(settings.LOGIN_URL)
return super(RegistrationView, self).dispatch(*args, **kwargs) if not getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True):
# Keep the request so the user can be automagically logged in.
setattr(self, 'request', request)
return super(RegistrationView, self).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'))
user.email = form.cleaned_data['email'] user.email = form.cleaned_data['email']
user_registered.send(self.__class__, user=user, request=self.request) user_registered.send(self.__class__, user=user, request=self.request)
# Go to Step 3 if getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True):
self.send_activation_email(user) # Go to Step 3
self.send_activation_email(user)
else:
user.is_active = True
user.save()
login(self.request, user, 'allianceauth.authentication.backends.StateBackend')
return user return user
def get_activation_key(self, user): def get_activation_key(self, user):

View File

@@ -1,5 +1,5 @@
from allianceauth.services.hooks import MenuItemHook, UrlHook from allianceauth.services.hooks import MenuItemHook, UrlHook
from django.utils.translation import ugettext_lazy as _
from allianceauth import hooks from allianceauth import hooks
from allianceauth.corputils import urls from allianceauth.corputils import urls
@@ -7,7 +7,7 @@ from allianceauth.corputils import urls
class CorpStats(MenuItemHook): class CorpStats(MenuItemHook):
def __init__(self): def __init__(self):
MenuItemHook.__init__(self, MenuItemHook.__init__(self,
'Corporation Stats', _('Corporation Stats'),
'fa fa-share-alt fa-fw', 'fa fa-share-alt fa-fw',
'corputils:view', 'corputils:view',
navactive=['corputils:']) navactive=['corputils:'])

View File

@@ -16,10 +16,16 @@ class CorpStatsQuerySet(models.QuerySet):
assert char assert char
# build all accepted queries # build all accepted queries
queries = [models.Q(token__user=user)] queries = [models.Q(token__user=user)]
if user.has_perm('corputils.view_corp_corpstats'):
queries.append(models.Q(corp__corporation_id=char.corporation_id))
if user.has_perm('corputils.view_alliance_corpstats'): if user.has_perm('corputils.view_alliance_corpstats'):
queries.append(models.Q(corp__alliance__alliance_id=char.alliance_id)) if char.alliance_id is not None:
queries.append(models.Q(corp__alliance__alliance_id=char.alliance_id))
else:
queries.append(models.Q(corp__corporation_id=char.corporation_id))
if user.has_perm('corputils.view_corp_corpstats'):
if user.has_perm('corputils.view_alliance_corpstats'):
pass
else:
queries.append(models.Q(corp__corporation_id=char.corporation_id))
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()))

View File

@@ -12,6 +12,16 @@ from allianceauth.notifications import notify
from allianceauth.corputils.managers import CorpStatsManager from allianceauth.corputils.managers import CorpStatsManager
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json') SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json')
"""
Swagger spec operations:
Character
get_characters_character_id
get_corporations_corporation_id_members
Universe
post_universe_names
"""
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -40,19 +50,18 @@ class CorpStats(models.Model):
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)
members = 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()
member_ids = [m['character_id'] for m in members]
# requesting too many ids per call results in a HTTP400 # requesting too many ids per call results in a HTTP400
# 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.Character.get_characters_names(character_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['character_id']: m['character_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
@@ -121,8 +130,11 @@ class CorpStats(models.Model):
m.main_character and int(m.main_character.character_id) == int( m.main_character and int(m.main_character.character_id) == int(
m.character_id)]) m.character_id)])
def visible_to(self, user):
return CorpStats.objects.filter(pk=self.pk).visible_to(user).exists()
def can_update(self, user): def can_update(self, user):
return user.is_superuser or user == self.token.user return self.token.user == user or self.visible_to(user)
def corp_logo(self, size=128): def corp_logo(self, size=128):
return "https://image.eveonline.com/Corporation/%s_%s.png" % (self.corp.corporation_id, size) return "https://image.eveonline.com/Corporation/%s_%s.png" % (self.corp.corporation_id, size)
@@ -179,4 +191,4 @@ class CorpMember(models.Model):
if item.startswith('portrait_url_'): if item.startswith('portrait_url_'):
size = item.strip('portrait_url_') size = item.strip('portrait_url_')
return self.portrait_url(size) return self.portrait_url(size)
return super(CorpMember, self).__getattr__(item) return self.__getattribute__(item)

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
from celery import shared_task from celery import shared_task
from allianceauth.corputils import CorpStats from allianceauth.corputils.models import CorpStats
@shared_task @shared_task

View File

@@ -9,9 +9,9 @@
<tr> <tr>
<td class="text-center col-lg-6 <td class="text-center col-lg-6
{% if corpstats.corp.alliance %}{% else %}col-lg-offset-3{% endif %}"><img {% if corpstats.corp.alliance %}{% else %}col-lg-offset-3{% endif %}"><img
class="ra-avatar" src="{{ corpstats.corp_logo }}"></td> class="ra-avatar" src="{{ corpstats.corp.logo_url_128 }}"></td>
{% if corpstats.corp.alliance %} {% if corpstats.corp.alliance %}
<td class="text-center col-lg-6"><img class="ra-avatar" src="{{ corpstats.alliance_logo }}"> <td class="text-center col-lg-6"><img class="ra-avatar" src="{{ corpstats.corp.alliance.logo_url_128 }}">
</td> </td>
{% endif %} {% endif %}
</tr> </tr>
@@ -70,6 +70,7 @@
{% for alt in main.alts %} {% for alt in main.alts %}
{% if forloop.first %} {% if forloop.first %}
<tr> <tr>
<th></th>
<th class="text-center">{% trans "Character" %}</th> <th class="text-center">{% trans "Character" %}</th>
<th class="text-center">{% trans "Corporation" %}</th> <th class="text-center">{% trans "Corporation" %}</th>
<th class="text-center">{% trans "Alliance" %}</th> <th class="text-center">{% trans "Alliance" %}</th>
@@ -77,10 +78,15 @@
</tr> </tr>
{% endif %} {% endif %}
<tr> <tr>
<td class="text-center">{{ alt.character_name }}</td> <td class="text-center" style="width:5%">
<td class="text-center">{{ alt.corporation_name }}</td> <div class="thumbnail" style="border: 0 none; box-shadow: none; background: transparent;">
<td class="text-center">{{ alt.alliance_name }}</td> <img src="https://image.eveonline.com/Character/{{ alt.character_id }}_32.jpg" class="img-circle">
<td class="text-center"> </div>
</td>
<td class="text-center" style="width:30%">{{ alt.character_name }}</td>
<td class="text-center" style="width:30%">{{ alt.corporation_name }}</td>
<td class="text-center" style="width:30%">{{ alt.alliance_name }}</td>
<td class="text-center" style="width:5%">
<a href="https://zkillboard.com/character/{{ alt.character_id }}/" <a href="https://zkillboard.com/character/{{ alt.character_id }}/"
class="label label-danger" target="_blank"> class="label label-danger" target="_blank">
{% trans "Killboard" %} {% trans "Killboard" %}
@@ -175,9 +181,25 @@
{% endblock %} {% endblock %}
{% block extra_script %} {% block extra_script %}
$(document).ready(function(){ $(document).ready(function(){
$('#table-mains').DataTable(); $('#table-mains').DataTable({
$('#table-members').DataTable(); "columnDefs": [
$('#table-unregistered').DataTable(); { "sortable": false, "targets": [1] },
],
});
$('#table-members').DataTable({
"columnDefs": [
{ "searchable": false, "targets": [0, 2] },
{ "sortable": false, "targets": [0, 2] },
],
"order": [[ 1, "asc" ]],
});
$('#table-unregistered').DataTable({
"columnDefs": [
{ "searchable": false, "targets": [0, 2] },
{ "sortable": false, "targets": [0, 2] },
],
"order": [[ 1, "asc" ]],
});
}); });
{% endblock %} {% endblock %}

View File

@@ -85,8 +85,8 @@ class CorpStatsUpdateTestCase(TestCase):
@mock.patch('esi.clients.SwaggerClient') @mock.patch('esi.clients.SwaggerClient')
def test_update_add_member(self, SwaggerClient): def test_update_add_member(self, SwaggerClient):
SwaggerClient.from_spec.return_value.Character.get_characters_character_id.return_value.result.return_value = {'corporation_id': 2} SwaggerClient.from_spec.return_value.Character.get_characters_character_id.return_value.result.return_value = {'corporation_id': 2}
SwaggerClient.from_spec.return_value.Corporation.get_corporations_corporation_id_members.return_value.result.return_value = [{'character_id': 1}] SwaggerClient.from_spec.return_value.Corporation.get_corporations_corporation_id_members.return_value.result.return_value = [1]
SwaggerClient.from_spec.return_value.Character.get_characters_names.return_value.result.return_value = [{'character_id': 1, 'character_name': 'test character'}] SwaggerClient.from_spec.return_value.Universe.post_universe_names.return_value.result.return_value = [{'id': 1, 'name': 'test character'}]
self.corpstats.update() self.corpstats.update()
self.assertTrue(CorpMember.objects.filter(character_id='1', character_name='test character', corpstats=self.corpstats).exists()) self.assertTrue(CorpMember.objects.filter(character_id='1', character_name='test character', corpstats=self.corpstats).exists())
@@ -94,8 +94,8 @@ class CorpStatsUpdateTestCase(TestCase):
def test_update_remove_member(self, SwaggerClient): def test_update_remove_member(self, SwaggerClient):
CorpMember.objects.create(character_id='2', character_name='old test character', corpstats=self.corpstats) CorpMember.objects.create(character_id='2', character_name='old test character', corpstats=self.corpstats)
SwaggerClient.from_spec.return_value.Character.get_characters_character_id.return_value.result.return_value = {'corporation_id': 2} SwaggerClient.from_spec.return_value.Character.get_characters_character_id.return_value.result.return_value = {'corporation_id': 2}
SwaggerClient.from_spec.return_value.Corporation.get_corporations_corporation_id_members.return_value.result.return_value = [{'character_id': 1}] SwaggerClient.from_spec.return_value.Corporation.get_corporations_corporation_id_members.return_value.result.return_value = [1]
SwaggerClient.from_spec.return_value.Character.get_characters_names.return_value.result.return_value = [{'character_id': 1, 'character_name': 'test character'}] SwaggerClient.from_spec.return_value.Universe.post_universe_names.return_value.result.return_value = [{'id': 1, 'name': 'test character'}]
self.corpstats.update() self.corpstats.update()
self.assertFalse(CorpMember.objects.filter(character_id='2', corpstats=self.corpstats).exists()) self.assertFalse(CorpMember.objects.filter(character_id='2', corpstats=self.corpstats).exists())

View File

@@ -13,11 +13,17 @@ from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo
from .models import CorpStats from .models import CorpStats
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json') SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json')
"""
Swagger spec operations:
get_characters_character_id
"""
def access_corpstats_test(user): def access_corpstats_test(user):
return user.has_perm('corputils.view_corp_corpstats') or user.has_perm( return user.has_perm('corputils.view_corp_corpstats') or user.has_perm(
'corputils.view_alliance_corpstats') or user.has_perm('corputils.view_state_corpstats') 'corputils.view_alliance_corpstats') or user.has_perm('corputils.view_state_corpstats') or user.has_perm(
'corputils.add_corpstats')
@login_required @login_required
@@ -62,7 +68,7 @@ def corpstats_view(request, corp_id=None):
corpstats = get_object_or_404(CorpStats, corp=corp) corpstats = get_object_or_404(CorpStats, corp=corp)
# get available models # get available models
available = CorpStats.objects.visible_to(request.user) available = CorpStats.objects.visible_to(request.user).order_by('corp__corporation_name')
# ensure we can see the requested model # ensure we can see the requested model
if corpstats and corpstats not in available: if corpstats and corpstats not in available:

View File

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

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Generated by Django 1.11.5 on 2017-09-29 14:44 # Generated by Django 1.11.6 on 2017-12-23 04:30
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
@@ -29,40 +29,31 @@ class Migration(migrations.Migration):
('alliance_name_source', models.CharField(choices=[('ticker', 'Ticker'), ('name', 'Full name')], default='name', max_length=20)), ('alliance_name_source', models.CharField(choices=[('ticker', 'Ticker'), ('name', 'Full name')], default='name', max_length=20)),
('replace_spaces', models.BooleanField(default=False)), ('replace_spaces', models.BooleanField(default=False)),
('replace_spaces_with', models.CharField(blank=True, default='', help_text='Any spaces in the group name will be replaced with this.', max_length=10)), ('replace_spaces_with', models.CharField(blank=True, default='', help_text='Any spaces in the group name will be replaced with this.', max_length=10)),
('states', models.ManyToManyField(related_name='autogroups', to='authentication.State')),
],
),
migrations.CreateModel(
name='ManagedGroup',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='ManagedAllianceGroup', name='ManagedAllianceGroup',
fields=[ fields=[
('managedgroup_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='eve_autogroups.ManagedGroup')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('alliance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eveonline.EveAllianceInfo')), ('alliance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eveonline.EveAllianceInfo')),
('config', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eve_autogroups.AutogroupsConfig')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.Group')),
], ],
bases=('eve_autogroups.managedgroup',), options={
'abstract': False,
},
), ),
migrations.CreateModel( migrations.CreateModel(
name='ManagedCorpGroup', name='ManagedCorpGroup',
fields=[ fields=[
('managedgroup_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='eve_autogroups.ManagedGroup')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('config', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eve_autogroups.AutogroupsConfig')),
('corp', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eveonline.EveCorporationInfo')), ('corp', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eveonline.EveCorporationInfo')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.Group')),
], ],
bases=('eve_autogroups.managedgroup',), options={
), 'abstract': False,
migrations.AddField( },
model_name='managedgroup',
name='config',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eve_autogroups.AutogroupsConfig'),
),
migrations.AddField(
model_name='managedgroup',
name='group',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.Group'),
), ),
migrations.AddField( migrations.AddField(
model_name='autogroupsconfig', model_name='autogroupsconfig',
@@ -74,4 +65,9 @@ class Migration(migrations.Migration):
name='corp_managed_groups', name='corp_managed_groups',
field=models.ManyToManyField(help_text="A list of corporation groups created and maintained by this AutogroupConfig. You should not edit this list unless you know what you're doing.", related_name='corp_managed_config', through='eve_autogroups.ManagedCorpGroup', to='auth.Group'), field=models.ManyToManyField(help_text="A list of corporation groups created and maintained by this AutogroupConfig. You should not edit this list unless you know what you're doing.", related_name='corp_managed_config', through='eve_autogroups.ManagedCorpGroup', to='auth.Group'),
), ),
migrations.AddField(
model_name='autogroupsconfig',
name='states',
field=models.ManyToManyField(related_name='autogroups', to='authentication.State'),
),
] ]

View File

@@ -11,7 +11,7 @@ logger = logging.getLogger(__name__)
def get_users_for_state(state: State): def get_users_for_state(state: State):
return User.objects.select_related('profile').prefetch_related('profile__main_character')\ return User.objects.select_related('profile').prefetch_related('profile__main_character')\
.filter(profile__state__pk=state.pk) .filter(profile__state_id=state.pk)
class AutogroupsConfigManager(models.Manager): class AutogroupsConfigManager(models.Manager):
@@ -39,7 +39,12 @@ class AutogroupsConfigManager(models.Manager):
if state is None: if state is None:
state = user.profile.state state = user.profile.state
for config in self.filter(states=state): for config in self.filter(states=state):
config.update_group_membership_for_user(user) # grant user new groups for their state
config.update_group_membership_for_user(user)
for config in self.exclude(states=state):
# ensure user does not have groups from previous state
config.remove_user_from_alliance_groups(user)
config.remove_user_from_corp_groups(user)
class AutogroupsConfig(models.Model): class AutogroupsConfig(models.Model):
@@ -110,21 +115,24 @@ class AutogroupsConfig(models.Model):
group = None group = None
try: try:
if not self.alliance_groups or not self.user_entitled_to_groups(user): if not self.alliance_groups or not self.user_entitled_to_groups(user):
logger.debug('User {} does not have required state'.format(user)) logger.debug('User {} does not have required state for alliance group membership'.format(user))
return return
else: else:
alliance = user.profile.main_character.alliance alliance = user.profile.main_character.alliance
if alliance is None: if alliance is None:
logger.debug('User {} alliance is None, cannot update group membership'.format(user))
return return
group = self.get_alliance_group(alliance) group = self.get_alliance_group(alliance)
except EveAllianceInfo.DoesNotExist: except EveAllianceInfo.DoesNotExist:
logger.warning('User {} main characters alliance does not exist in the database.' logger.debug('User {} main characters alliance does not exist in the database. Creating.'.format(user))
' Group membership not updated'.format(user)) alliance = EveAllianceInfo.objects.create_alliance(user.profile.main_character.alliance_id)
group = self.get_alliance_group(alliance)
except AttributeError: except AttributeError:
logger.warning('User {} does not have a main character. Group membership not updated'.format(user)) logger.warning('User {} does not have a main character. Group membership not updated'.format(user))
finally: finally:
self.remove_user_from_corp_groups(user, except_group=group) self.remove_user_from_alliance_groups(user, except_group=group)
if group is not None: if group is not None:
logger.debug('Adding user {} to alliance group {}'.format(user, group))
user.groups.add(group) user.groups.add(group)
@transaction.atomic @transaction.atomic
@@ -132,18 +140,20 @@ class AutogroupsConfig(models.Model):
group = None group = None
try: try:
if not self.corp_groups or not self.user_entitled_to_groups(user): if not self.corp_groups or not self.user_entitled_to_groups(user):
logger.debug('User {} does not have required state'.format(user)) logger.debug('User {} does not have required state for corp group membership'.format(user))
else: else:
corp = user.profile.main_character.corporation corp = user.profile.main_character.corporation
group = self.get_corp_group(corp) group = self.get_corp_group(corp)
except EveCorporationInfo.DoesNotExist: except EveCorporationInfo.DoesNotExist:
logger.warning('User {} main characters corporation does not exist in the database.' logger.debug('User {} main characters corporation does not exist in the database. Creating.'.format(user))
' Group membership not updated'.format(user)) corp = EveCorporationInfo.objects.create_corporation(user.profile.main_character.corporation_id)
group = self.get_corp_group(corp)
except AttributeError: except AttributeError:
logger.warning('User {} does not have a main character. Group membership not updated'.format(user)) logger.warning('User {} does not have a main character. Group membership not updated'.format(user))
finally: finally:
self.remove_user_from_corp_groups(user, except_group=group) self.remove_user_from_corp_groups(user, except_group=group)
if group is not None: if group is not None:
logger.debug('Adding user {} to corp group {}'.format(user, group))
user.groups.add(group) user.groups.add(group)
@transaction.atomic @transaction.atomic
@@ -184,13 +194,15 @@ class AutogroupsConfig(models.Model):
""" """
Deletes ALL managed alliance groups Deletes ALL managed alliance groups
""" """
self.alliance_managed_groups.all().delete() for g in self.alliance_managed_groups.all():
g.delete()
def delete_corp_managed_groups(self): def delete_corp_managed_groups(self):
""" """
Deletes ALL managed corp groups Deletes ALL managed corp groups
""" """
self.corp_managed_groups.all().delete() for g in self.corp_managed_groups.all():
g.delete()
def get_alliance_group_name(self, alliance: EveAllianceInfo) -> str: def get_alliance_group_name(self, alliance: EveAllianceInfo) -> str:
if self.alliance_name_source == self.OPT_TICKER: if self.alliance_name_source == self.OPT_TICKER:
@@ -225,6 +237,9 @@ class ManagedGroup(models.Model):
group = models.ForeignKey(Group, on_delete=models.CASCADE) group = models.ForeignKey(Group, on_delete=models.CASCADE)
config = models.ForeignKey(AutogroupsConfig, on_delete=models.CASCADE) config = models.ForeignKey(AutogroupsConfig, on_delete=models.CASCADE)
class Meta:
abstract = True
class ManagedCorpGroup(ManagedGroup): class ManagedCorpGroup(ManagedGroup):
corp = models.ForeignKey(EveCorporationInfo, on_delete=models.CASCADE) corp = models.ForeignKey(EveCorporationInfo, on_delete=models.CASCADE)

View File

@@ -2,6 +2,7 @@ import logging
from django.dispatch import receiver from django.dispatch import receiver
from django.db.models.signals import pre_save, post_save, pre_delete, m2m_changed from django.db.models.signals import pre_save, post_save, pre_delete, m2m_changed
from allianceauth.authentication.models import UserProfile, State from allianceauth.authentication.models import UserProfile, State
from allianceauth.eveonline.models import EveCharacter
from .models import AutogroupsConfig from .models import AutogroupsConfig
@@ -45,9 +46,7 @@ def check_groups_on_profile_update(sender, instance, created, *args, **kwargs):
""" """
Trigger check when main character or state changes. Trigger check when main character or state changes.
""" """
update_fields = kwargs.pop('update_fields', []) or [] AutogroupsConfig.objects.update_groups_for_user(instance.user)
if 'main_character' in update_fields or 'state' in update_fields:
AutogroupsConfig.objects.update_groups_for_user(instance.user)
@receiver(m2m_changed, sender=AutogroupsConfig.states.through) @receiver(m2m_changed, sender=AutogroupsConfig.states.through)
@@ -64,3 +63,13 @@ def autogroups_states_changed(sender, instance, action, reverse, model, pk_set,
except State.DoesNotExist: except State.DoesNotExist:
# Deleted States handled by the profile state change # Deleted States handled by the profile state change
pass pass
@receiver(post_save, sender=EveCharacter)
def check_groups_on_character_update(sender, instance, created, *args, **kwargs):
if not created:
try:
profile = UserProfile.objects.prefetch_related('user').get(main_character_id=instance.pk)
AutogroupsConfig.objects.update_groups_for_user(profile.user)
except UserProfile.DoesNotExist:
pass

View File

@@ -1,8 +1,7 @@
from unittest import mock from unittest import mock
from django.db.models.signals import pre_save, post_save, pre_delete, m2m_changed from django.db.models.signals import pre_save, post_save, pre_delete, m2m_changed
from allianceauth.authentication.models import UserProfile from allianceauth.authentication.models import UserProfile
from allianceauth.authentication.signals import state_changed from allianceauth.authentication.signals import reassess_on_profile_save
from allianceauth.eveonline.models import EveCharacter
from .. import signals from .. import signals
from ..models import AutogroupsConfig from ..models import AutogroupsConfig
@@ -14,6 +13,7 @@ def patch(target, *args, **kwargs):
def connect_signals(): def connect_signals():
post_save.connect(receiver=reassess_on_profile_save, sender=UserProfile)
pre_save.connect(receiver=signals.pre_save_config, sender=AutogroupsConfig) pre_save.connect(receiver=signals.pre_save_config, sender=AutogroupsConfig)
pre_delete.connect(receiver=signals.pre_delete_config, sender=AutogroupsConfig) pre_delete.connect(receiver=signals.pre_delete_config, sender=AutogroupsConfig)
post_save.connect(receiver=signals.check_groups_on_profile_update, sender=UserProfile) post_save.connect(receiver=signals.check_groups_on_profile_update, sender=UserProfile)
@@ -21,6 +21,7 @@ def connect_signals():
def disconnect_signals(): def disconnect_signals():
post_save.disconnect(receiver=reassess_on_profile_save, sender=UserProfile)
pre_save.disconnect(receiver=signals.pre_save_config, sender=AutogroupsConfig) pre_save.disconnect(receiver=signals.pre_save_config, sender=AutogroupsConfig)
pre_delete.disconnect(receiver=signals.pre_delete_config, sender=AutogroupsConfig) pre_delete.disconnect(receiver=signals.pre_delete_config, sender=AutogroupsConfig)
post_save.disconnect(receiver=signals.check_groups_on_profile_update, sender=UserProfile) post_save.disconnect(receiver=signals.check_groups_on_profile_update, sender=UserProfile)

View File

@@ -7,7 +7,7 @@ from . import patch
class AutogroupsConfigManagerTestCase(TestCase): class AutogroupsConfigManagerTestCase(TestCase):
def test_update_groups_for_state(self, ): def test_update_groups_for_state(self):
member = AuthUtils.create_member('test member') member = AuthUtils.create_member('test member')
obj = AutogroupsConfig.objects.create() obj = AutogroupsConfig.objects.create()
obj.states.add(member.profile.state) obj.states.add(member.profile.state)
@@ -32,3 +32,23 @@ class AutogroupsConfigManagerTestCase(TestCase):
self.assertEqual(update_group_membership_for_user.call_count, 1) self.assertEqual(update_group_membership_for_user.call_count, 1)
args, kwargs = update_group_membership_for_user.call_args args, kwargs = update_group_membership_for_user.call_args
self.assertEqual(args[0], member) self.assertEqual(args[0], member)
@patch('.models.AutogroupsConfig.update_group_membership_for_user')
@patch('.models.AutogroupsConfig.remove_user_from_alliance_groups')
@patch('.models.AutogroupsConfig.remove_user_from_corp_groups')
def test_update_groups_no_config(self, remove_corp, remove_alliance, update_groups):
member = AuthUtils.create_member('test member')
obj = AutogroupsConfig.objects.create()
# Corp and alliance groups should be removed from users if their state has no config
AutogroupsConfig.objects.update_groups_for_user(member)
self.assertFalse(update_groups.called)
self.assertTrue(remove_alliance.called)
self.assertTrue(remove_corp.called)
# The normal group assignment should occur if there state has a config
obj.states.add(member.profile.state)
AutogroupsConfig.objects.update_groups_for_user(member)
self.assertTrue(update_groups.called)

View File

@@ -16,8 +16,6 @@ class AutogroupsConfigTestCase(TestCase):
# Disconnect signals # Disconnect signals
disconnect_signals() disconnect_signals()
self.member = AuthUtils.create_member('test user')
state = AuthUtils.get_member_state() state = AuthUtils.get_member_state()
self.alliance = EveAllianceInfo.objects.create( self.alliance = EveAllianceInfo.objects.create(
@@ -38,6 +36,8 @@ class AutogroupsConfigTestCase(TestCase):
state.member_alliances.add(self.alliance) state.member_alliances.add(self.alliance)
state.member_corporations.add(self.corp) state.member_corporations.add(self.corp)
self.member = AuthUtils.create_member('test user')
def tearDown(self): def tearDown(self):
# Reconnect signals # Reconnect signals
connect_signals() connect_signals()
@@ -82,6 +82,7 @@ class AutogroupsConfigTestCase(TestCase):
# Act # Act
obj.update_alliance_group_membership(self.member) obj.update_alliance_group_membership(self.member)
obj.update_corp_group_membership(self.member) # check for no side effects
group = obj.create_alliance_group(self.alliance) group = obj.create_alliance_group(self.alliance)
group_qs = Group.objects.filter(pk=group.pk) group_qs = Group.objects.filter(pk=group.pk)

View File

@@ -1,11 +1,11 @@
from django.test import TestCase from django.test import TestCase
from django.contrib.auth.models import Group, User from django.contrib.auth.models import User
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo
from ..models import AutogroupsConfig, ManagedAllianceGroup from ..models import AutogroupsConfig
from . import patch, disconnect_signals, connect_signals from . import patch, disconnect_signals, connect_signals
@@ -13,8 +13,6 @@ from . import patch, disconnect_signals, connect_signals
class SignalsTestCase(TestCase): class SignalsTestCase(TestCase):
def setUp(self): def setUp(self):
disconnect_signals() disconnect_signals()
self.member = AuthUtils.create_member('test user')
state = AuthUtils.get_member_state() state = AuthUtils.get_member_state()
self.char = EveCharacter.objects.create( self.char = EveCharacter.objects.create(
@@ -27,9 +25,6 @@ class SignalsTestCase(TestCase):
alliance_name='alliance name', alliance_name='alliance name',
) )
self.member.profile.main_character = self.char
self.member.profile.save()
self.alliance = EveAllianceInfo.objects.create( self.alliance = EveAllianceInfo.objects.create(
alliance_id='3456', alliance_id='3456',
alliance_name='alliance name', alliance_name='alliance name',
@@ -47,13 +42,17 @@ class SignalsTestCase(TestCase):
state.member_alliances.add(self.alliance) state.member_alliances.add(self.alliance)
state.member_corporations.add(self.corp) state.member_corporations.add(self.corp)
self.member = AuthUtils.create_member('test user')
self.member.profile.main_character = self.char
self.member.profile.save()
connect_signals() connect_signals()
@patch('.models.AutogroupsConfigManager.update_groups_for_user') @patch('.models.AutogroupsConfigManager.update_groups_for_user')
def test_check_groups_on_profile_update_state(self, update_groups_for_user): def test_check_groups_on_profile_update_state(self, update_groups_for_user):
# Trigger signal # Trigger signal
self.member.profile.state = AuthUtils.get_guest_state() self.member.profile.assign_state(state=AuthUtils.get_guest_state())
self.member.profile.save()
self.assertTrue(update_groups_for_user.called) self.assertTrue(update_groups_for_user.called)
self.assertEqual(update_groups_for_user.call_count, 1) self.assertEqual(update_groups_for_user.call_count, 1)
@@ -71,10 +70,10 @@ class SignalsTestCase(TestCase):
alliance_id='3456', alliance_id='3456',
alliance_name='alliance name', alliance_name='alliance name',
) )
# Trigger signal # Trigger signal
self.member.profile.main_character = char self.member.profile.main_character = char
self.member.profile.save() self.member.profile.save()
self.assertTrue(update_groups_for_user.called) self.assertTrue(update_groups_for_user.called)
self.assertEqual(update_groups_for_user.call_count, 1) self.assertEqual(update_groups_for_user.call_count, 1)
args, kwargs = update_groups_for_user.call_args args, kwargs = update_groups_for_user.call_args

View File

@@ -26,6 +26,7 @@ class EveCharacterManager(models.Manager):
corporation_ticker=character.corp.ticker, corporation_ticker=character.corp.ticker,
alliance_id=character.alliance.id, alliance_id=character.alliance.id,
alliance_name=character.alliance.name, alliance_name=character.alliance.name,
alliance_ticker=getattr(character.alliance, 'ticker', None),
) )
def update_character(self, character_id): def update_character(self, character_id):

View File

@@ -0,0 +1,23 @@
# Generated by Django 2.0.4 on 2018-04-17 20:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('eveonline', '0009_on_delete'),
]
operations = [
migrations.AddField(
model_name='evecharacter',
name='alliance_ticker',
field=models.CharField(blank=True, default='', max_length=5, null=True),
),
migrations.AlterField(
model_name='evecharacter',
name='corporation_ticker',
field=models.CharField(max_length=5),
),
]

View File

@@ -35,6 +35,15 @@ class EveAllianceInfo(models.Model):
def __str__(self): def __str__(self):
return self.alliance_name return self.alliance_name
def logo_url(self, size=32):
return "https://image.eveonline.com/Alliance/%s_%s.png" % (self.alliance_id, size)
def __getattr__(self, item):
if item.startswith('logo_url_'):
size = item.strip('logo_url_')
return self.logo_url(size)
return self.__getattribute__(item)
class EveCorporationInfo(models.Model): class EveCorporationInfo(models.Model):
corporation_id = models.CharField(max_length=254, unique=True) corporation_id = models.CharField(max_length=254, unique=True)
@@ -60,15 +69,25 @@ class EveCorporationInfo(models.Model):
def __str__(self): def __str__(self):
return self.corporation_name return self.corporation_name
def logo_url(self, size=32):
return "https://image.eveonline.com/Corporation/%s_%s.png" % (self.corporation_id, size)
def __getattr__(self, item):
if item.startswith('logo_url_'):
size = item.strip('logo_url_')
return self.logo_url(size)
return self.__getattribute__(item)
class EveCharacter(models.Model): class EveCharacter(models.Model):
character_id = models.CharField(max_length=254, unique=True) character_id = models.CharField(max_length=254, unique=True)
character_name = models.CharField(max_length=254, unique=True) character_name = models.CharField(max_length=254, unique=True)
corporation_id = models.CharField(max_length=254) corporation_id = models.CharField(max_length=254)
corporation_name = models.CharField(max_length=254) corporation_name = models.CharField(max_length=254)
corporation_ticker = models.CharField(max_length=254) corporation_ticker = models.CharField(max_length=5)
alliance_id = models.CharField(max_length=254, blank=True, null=True, default='') alliance_id = models.CharField(max_length=254, blank=True, null=True, default='')
alliance_name = models.CharField(max_length=254, blank=True, null=True, default='') alliance_name = models.CharField(max_length=254, blank=True, null=True, default='')
alliance_ticker = models.CharField(max_length=5, blank=True, null=True, default='')
objects = EveCharacterManager() objects = EveCharacterManager()
provider = EveCharacterProviderManager() provider = EveCharacterProviderManager()
@@ -102,8 +121,18 @@ class EveCharacter(models.Model):
self.corporation_ticker = character.corp.ticker self.corporation_ticker = character.corp.ticker
self.alliance_id = character.alliance.id self.alliance_id = character.alliance.id
self.alliance_name = character.alliance.name self.alliance_name = character.alliance.name
self.alliance_ticker = getattr(character.alliance, 'ticker', None)
self.save() self.save()
return self return self
def __str__(self): def __str__(self):
return self.character_name return self.character_name
def portrait_url(self, size=32):
return "https://image.eveonline.com/Character/%s_%s.jpg" % (self.character_id, size)
def __getattr__(self, item):
if item.startswith('portrait_url_'):
size = item.strip('portrait_url_')
return self.portrait_url(size)
return self.__getattribute__(item)

View File

@@ -4,6 +4,16 @@ import logging
import os import os
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json') SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json')
"""
Swagger spec operations:
get_alliances_alliance_id
get_alliances_alliance_id_corporations
get_corporations_corporation_id
get_characters_character_id
get_universe_types_type_id
"""
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -81,7 +91,9 @@ class Alliance(Entity):
@property @property
def executor_corp(self): def executor_corp(self):
return self.corp(self.executor_corp_id) if self.executor_corp_id:
return self.corp(self.executor_corp_id)
return Entity(None, None)
class Character(Entity): class Character(Entity):
@@ -150,10 +162,10 @@ class EveSwaggerProvider(EveProvider):
corps = self.client.Alliance.get_alliances_alliance_id_corporations(alliance_id=alliance_id).result() corps = self.client.Alliance.get_alliances_alliance_id_corporations(alliance_id=alliance_id).result()
model = Alliance( model = Alliance(
id=alliance_id, id=alliance_id,
name=data['alliance_name'], name=data['name'],
ticker=data['ticker'], ticker=data['ticker'],
corp_ids=corps, corp_ids=corps,
executor_corp_id=data['executor_corp'], executor_corp_id=data['executor_corporation_id'] if 'executor_corporation_id' in data else None,
) )
return model return model
except HTTPNotFound: except HTTPNotFound:
@@ -164,7 +176,7 @@ class EveSwaggerProvider(EveProvider):
data = self.client.Corporation.get_corporations_corporation_id(corporation_id=corp_id).result() data = self.client.Corporation.get_corporations_corporation_id(corporation_id=corp_id).result()
model = Corporation( model = Corporation(
id=corp_id, id=corp_id,
name=data['corporation_name'], name=data['name'],
ticker=data['ticker'], ticker=data['ticker'],
ceo_id=data['ceo_id'], ceo_id=data['ceo_id'],
members=data['member_count'], members=data['member_count'],
@@ -177,12 +189,11 @@ class EveSwaggerProvider(EveProvider):
def get_character(self, character_id): def get_character(self, character_id):
try: try:
data = self.client.Character.get_characters_character_id(character_id=character_id).result() data = self.client.Character.get_characters_character_id(character_id=character_id).result()
alliance_id = self.adapter.get_corp(data['corporation_id']).alliance_id
model = Character( model = Character(
id=character_id, id=character_id,
name=data['name'], name=data['name'],
corp_id=data['corporation_id'], corp_id=data['corporation_id'],
alliance_id=alliance_id, alliance_id=data['alliance_id'] if 'alliance_id' in data else None,
) )
return model return model
except (HTTPNotFound, HTTPUnprocessableEntity): except (HTTPNotFound, HTTPUnprocessableEntity):

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,12 @@
from . import urls from . import urls
from django.utils.translation import ugettext_lazy as _
from allianceauth import hooks from allianceauth import hooks
from allianceauth.services.hooks import MenuItemHook, UrlHook from allianceauth.services.hooks import MenuItemHook, UrlHook
@hooks.register('menu_item_hook') @hooks.register('menu_item_hook')
def register_menu(): def register_menu():
return MenuItemHook('Fleet Activity Tracking', 'fa fa-users fa-lightbulb-o fa-fw', 'fatlink:view', return MenuItemHook(_('Fleet Activity Tracking'), 'fa fa-users fa-lightbulb-o fa-fw', 'fatlink:view',
navactive=['fatlink:']) navactive=['fatlink:'])

View File

@@ -2,11 +2,9 @@
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.optimer.models import OpTimer
class FatlinkForm(forms.Form): class FatlinkForm(forms.Form):
fatname = forms.CharField(label=_('Name of fat-link'), required=True) fleet = forms.CharField(label=_("Fleet Name"), max_length=50)
duration = forms.IntegerField(label=_("Duration of fat-link"), required=True, initial=30, min_value=1, duration = forms.IntegerField(label=_("Duration of fat-link"), required=True, initial=30, min_value=1,
max_value=2147483647) max_value=2147483647, help_text=_('minutes'))
fleet = forms.ModelChoiceField(label=_("Fleet"), queryset=OpTimer.objects.all().order_by('operation_name'))

View File

@@ -2,12 +2,10 @@
# Generated by Django 1.10.1 on 2016-09-05 21:39 # Generated by Django 1.10.1 on 2016-09-05 21:39
from __future__ import unicode_literals from __future__ import unicode_literals
import datetime
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
from django.utils.timezone import utc from django.utils import timezone
import allianceauth.fleetactivitytracking.models import allianceauth.fleetactivitytracking.models
@@ -36,9 +34,9 @@ class Migration(migrations.Migration):
name='Fatlink', name='Fatlink',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('fatdatetime', models.DateTimeField(default=datetime.datetime(2016, 9, 5, 21, 39, 17, 307954, tzinfo=utc))), ('fatdatetime', models.DateTimeField(default=timezone.now)),
('duration', models.PositiveIntegerField()), ('duration', models.PositiveIntegerField()),
('fleet', models.CharField(default=b'', max_length=254)), ('fleet', models.CharField(default='', max_length=254)),
('name', models.CharField(max_length=254)), ('name', models.CharField(max_length=254)),
('hash', models.CharField(max_length=254, unique=True)), ('hash', models.CharField(max_length=254, unique=True)),
('creator', models.ForeignKey(on_delete=models.SET( ('creator', models.ForeignKey(on_delete=models.SET(

View File

@@ -0,0 +1,22 @@
# Generated by Django 2.0.2 on 2018-02-28 18:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fleetactivitytracking', '0004_make_strings_more_stringy'),
]
operations = [
migrations.RemoveField(
model_name='fatlink',
name='name',
),
migrations.AlterField(
model_name='fatlink',
name='fleet',
field=models.CharField(max_length=254),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.0.6 on 2018-08-03 04:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fleetactivitytracking', '0005_remove_fat_name'),
]
operations = [
migrations.AlterField(
model_name='fat',
name='shiptype',
field=models.CharField(max_length=100),
),
]

View File

@@ -12,20 +12,19 @@ def get_sentinel_user():
class Fatlink(models.Model): class Fatlink(models.Model):
fatdatetime = models.DateTimeField(default=timezone.now) fatdatetime = models.DateTimeField(default=timezone.now)
duration = models.PositiveIntegerField() duration = models.PositiveIntegerField()
fleet = models.CharField(max_length=254, default="") fleet = models.CharField(max_length=254)
name = models.CharField(max_length=254)
hash = models.CharField(max_length=254, unique=True) hash = models.CharField(max_length=254, unique=True)
creator = models.ForeignKey(User, on_delete=models.SET(get_sentinel_user)) creator = models.ForeignKey(User, on_delete=models.SET(get_sentinel_user))
def __str__(self): def __str__(self):
return self.name return self.fleet
class Fat(models.Model): class Fat(models.Model):
character = models.ForeignKey(EveCharacter, on_delete=models.CASCADE) character = models.ForeignKey(EveCharacter, on_delete=models.CASCADE)
fatlink = models.ForeignKey(Fatlink, on_delete=models.CASCADE) fatlink = models.ForeignKey(Fatlink, on_delete=models.CASCADE)
system = models.CharField(max_length=30) system = models.CharField(max_length=30)
shiptype = models.CharField(max_length=30) shiptype = models.CharField(max_length=100)
station = models.CharField(max_length=125) station = models.CharField(max_length=125)
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)

File diff suppressed because one or more lines are too long

View File

@@ -16,7 +16,7 @@
</div> </div>
<div class="col-lg-10 col-sm-2"> <div class="col-lg-10 col-sm-2">
<div class="alert alert-danger" role="alert">{% trans "Character not registered!" %}</div> <div class="alert alert-danger" role="alert">{% trans "Character not registered!" %}</div>
{% trans "This character is not part of any registered API-key. You must go to" %} <a href=" {% url 'auth_api_key_management' %}">{% trans "API key management</a> and add an API with the character on before being able to click fleet attendance links." %} {% trans "This character is not associated with an auth account." %} <a href=" {% url 'authentication:add_character' %}">{% trans "Add it here" %}</a> {% trans "before attempting to click fleet attendance links." %}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -10,12 +10,12 @@
<h1 class="page-header text-center">{% blocktrans %}Participation data statistics for {{ month }}, {{ year }}{% endblocktrans %} <h1 class="page-header text-center">{% blocktrans %}Participation data statistics for {{ month }}, {{ year }}{% endblocktrans %}
{% if char_id %} {% if char_id %}
<div class="text-right"> <div class="text-right">
<a href="{% url 'fatlink:user_statistics_month' char_id previous_month|date:"Y" previous_month|date:"m" %}" class="btn btn-info">{% trans "Previous month" %}</a> <a href="{% url 'fatlink:user_statistics_month' char_id previous_month|date:'Y' previous_month|date:'m' %}" class="btn btn-info">{% trans "Previous month" %}</a>
<a href="{% url 'fatlink:user_statistics_month' char_id next_month|date:"Y" next_month|date:"m" %}" class="btn btn-info">{% trans "Next month" %}</a> <a href="{% url 'fatlink:user_statistics_month' char_id next_month|date:'Y' next_month|date:'m' %}" class="btn btn-info">{% trans "Next month" %}</a>
</div> </div>
{% endif %} {% endif %}
</h1> </h1>
<h2>{% blocktrans %}{{ user }} has collected {{ n_fats }} links this month.{% endblocktrans %}</h2> <h2>{% blocktrans %}{{ user }} has collected {{ n_fats }} link{{ n_fats|pluralize }} this month.{% endblocktrans %}</h2>
<table class="table table-responsive"> <table class="table table-responsive">
<tr> <tr>
<th class="col-md-2 text-center">{% trans "Ship" %}</th> <th class="col-md-2 text-center">{% trans "Ship" %}</th>
@@ -29,26 +29,24 @@
{% endfor %} {% endfor %}
</table> </table>
{% if created_fats %} {% if created_fats %}
<h2>{% blocktrans %}{{ user }} has created {{ n_created_fats }} links this month.{% endblocktrans %}</h2> <h2>{% blocktrans %}{{ user }} has created {{ n_created_fats }} link{{ n_created_fats|pluralize }} this month.{% endblocktrans %}</h2>
{% if created_fats %} {% if created_fats %}
<table class="table"> <table class="table">
<tr> <tr>
<th class="text-center">{% trans "Name" %}</th>
<th class="text-center">{% trans "Creator" %}</th>
<th class="text-center">{% trans "Fleet" %}</th> <th class="text-center">{% trans "Fleet" %}</th>
<th class="text-center">{% trans "Creator" %}</th>
<th class="text-center">{% trans "Eve Time" %}</th> <th class="text-center">{% trans "Eve Time" %}</th>
<th class="text-center">{% trans "Duration" %}</th> <th class="text-center">{% trans "Duration" %}</th>
<th class="text-center">{% trans "Edit" %}</th> <th class="text-center">{% trans "Edit" %}</th>
</tr> </tr>
{% for link in created_fats %} {% for link in created_fats %}
<tr> <tr>
<td class="text-center"><a href="{% url 'auth_click_fatlink_view' %}{{ link.hash }}/{{ link.name }}">{{ link.name }}</a></td> <td class="text-center"><a href="{% url 'fatlink:click' link.hash %}" class="label label-primary">{{ link.fleet }}</a></td>
<td class="text-center">{{ link.creator.username }}</td> <td class="text-center">{{ link.creator.username }}</td>
<td class="text-center">{{ link.fleet }}</td>
<td class="text-center">{{ link.fatdatetime }}</td> <td class="text-center">{{ link.fatdatetime }}</td>
<td class="text-center">{{ link.duration }}</td> <td class="text-center">{{ link.duration }}</td>
<td class="text-center"> <td class="text-center">
<a href="{% url 'auth_modify_fatlink_view' %}{{ link.hash }}/{{ link.name }}"> <a href="{% url 'fatlink:modify' link.hash %}">
<button type="button" class="btn btn-info"><span <button type="button" class="btn btn-info"><span
class="glyphicon glyphicon-edit"></span></button> class="glyphicon glyphicon-edit"></span></button>
</a> </a>

View File

@@ -24,7 +24,7 @@
{% if fats %} {% if fats %}
<table class="table table-responsive"> <table class="table table-responsive">
<tr> <tr>
<th class="text-center">{% trans "fatname" %}</th> <th class="text-center">{% trans "Fleet" %}</th>
<th class="text-center">{% trans "Character" %}</th> <th class="text-center">{% trans "Character" %}</th>
<th class="text-center">{% trans "System" %}</th> <th class="text-center">{% trans "System" %}</th>
<th class="text-center">{% trans "Ship" %}</th> <th class="text-center">{% trans "Ship" %}</th>
@@ -32,7 +32,7 @@
</tr> </tr>
{% for fat in fats %} {% for fat in fats %}
<tr> <tr>
<td class="text-center">{{ fat.fatlink.name }}</td> <td class="text-center">{{ fat.fatlink.fleet }}</td>
<td class="text-center">{{ fat.character.character_name }}</td> <td class="text-center">{{ fat.character.character_name }}</td>
{% if fat.station != "No Station" %} {% if fat.station != "No Station" %}
<td class="text-center">{% blocktrans %}Docked in {% endblocktrans %}{{ fat.system }}</td> <td class="text-center">{% blocktrans %}Docked in {% endblocktrans %}{{ fat.system }}</td>
@@ -79,13 +79,13 @@
</tr> </tr>
{% for link in fatlinks %} {% for link in fatlinks %}
<tr> <tr>
<td class="text-center"><a href="{% url 'fatlink:click_fatlink' %}{{ link.hash }}/{{ link.name }}">{{ link.name }}</a></td> <td class="text-center"><a href="{% url 'fatlink:click' link.hash %}" class="label label-primary">{{ link.fleet }}</a></td>
<td class="text-center">{{ link.creator.username }}</td> <td class="text-center">{{ link.creator.username }}</td>
<td class="text-center">{{ link.fleet }}</td> <td class="text-center">{{ link.fleet }}</td>
<td class="text-center">{{ link.fatdatetime }}</td> <td class="text-center">{{ link.fatdatetime }}</td>
<td class="text-center">{{ link.duration }}</td> <td class="text-center">{{ link.duration }}</td>
<td class="text-center"> <td class="text-center">
<a href="{% url 'fatlink:modify' %}{{ link.hash }}/{{ link.name }}" class="btn btn-info"> <a href="{% url 'fatlink:modify' link.hash %}" class="btn btn-info">
<span class="glyphicon glyphicon-edit"></span> <span class="glyphicon glyphicon-edit"></span>
</a> </a>
</td> </td>

View File

@@ -25,10 +25,6 @@ urlpatterns = [
views.fatlink_monthly_personal_statistics_view, views.fatlink_monthly_personal_statistics_view,
name='user_statistics_month'), name='user_statistics_month'),
url(r'^create/$', views.create_fatlink_view, name='create'), url(r'^create/$', views.create_fatlink_view, name='create'),
url(r'^modify/$', views.modify_fatlink_view, name='modify'), url(r'^modify/(?P<fat_hash>[a-zA-Z0-9_-]+)/$', views.modify_fatlink_view, name='modify'),
url(r'^modify/(?P<hash>[a-zA-Z0-9_-]+)/([a-z0-9_-]+)$', url(r'^link/(?P<fat_hash>[a-zA-Z0-9]+)/$', views.click_fatlink_view, name='click'),
views.modify_fatlink_view),
url(r'^link/$', views.fatlink_view, name='click_fatlink'),
url(r'^link/(?P<hash>[a-zA-Z0-9]+)/(?P<fatname>[a-z0-9_-]+)/$',
views.click_fatlink_view),
] ]

View File

@@ -1,8 +1,6 @@
import datetime import datetime
import logging import logging
import os import os
import random
import string
from allianceauth.authentication.models import CharacterOwnership from allianceauth.authentication.models import CharacterOwnership
from django.contrib import messages from django.contrib import messages
@@ -17,13 +15,23 @@ from esi.decorators import token_required
from allianceauth.eveonline.providers import provider from allianceauth.eveonline.providers import provider
from .forms import FatlinkForm from .forms import FatlinkForm
from .models import Fatlink, Fat from .models import Fatlink, Fat
from slugify import slugify from django.utils.crypto import get_random_string
from allianceauth.eveonline.models import EveAllianceInfo from allianceauth.eveonline.models import EveAllianceInfo
from allianceauth.eveonline.models import EveCharacter from allianceauth.eveonline.models import EveCharacter
from allianceauth.eveonline.models import EveCorporationInfo from allianceauth.eveonline.models import EveCorporationInfo
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json') SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json')
"""
Swagger spec operations:
get_characters_character_id_location
get_characters_character_id_ship
get_universe_systems_system_id
get_universe_stations_station_id
get_universe_structures_structure_id
"""
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -114,7 +122,7 @@ def fatlink_statistics_corp_view(request, corpid, year=None, month=None):
start_of_next_month = first_day_of_next_month(year, month) start_of_next_month = first_day_of_next_month(year, month)
start_of_previous_month = first_day_of_previous_month(year, month) start_of_previous_month = first_day_of_previous_month(year, month)
fat_stats = {} fat_stats = {}
corp_members = CharacterOwnership.objects.filter(character__corporation_id=corpid).values('user_id').distinct() corp_members = CharacterOwnership.objects.filter(character__corporation_id=corpid).order_by('user_id').values('user_id').distinct()
for member in corp_members: for member in corp_members:
try: try:
@@ -181,7 +189,7 @@ def fatlink_personal_statistics_view(request, year=datetime.date.today().year):
personal_fats = Fat.objects.select_related('fatlink').filter(user=user).order_by('id') personal_fats = Fat.objects.select_related('fatlink').filter(user=user).order_by('id')
monthlystats = [0 for month in range(1, 13)] monthlystats = [0 for i in range(1, 13)]
for fat in personal_fats: for fat in personal_fats:
fatdate = fat.fatlink.fatdatetime fatdate = fat.fatlink.fatdatetime
@@ -236,8 +244,8 @@ def fatlink_monthly_personal_statistics_view(request, year, month, char_id=None)
@login_required @login_required
@token_required( @token_required(
scopes=['esi-location.read_location.v1', 'esi-location.read_ship_type.v1', 'esi-universe.read_structures.v1']) scopes=['esi-location.read_location.v1', 'esi-location.read_ship_type.v1', 'esi-universe.read_structures.v1'])
def click_fatlink_view(request, token, hash, fatname): def click_fatlink_view(request, token, fat_hash=None):
fatlink = get_object_or_404(Fatlink, hash=hash, name=fatname) fatlink = get_object_or_404(Fatlink, hash=fat_hash)
if (timezone.now() - fatlink.fatdatetime) < datetime.timedelta(seconds=(fatlink.duration * 60)): if (timezone.now() - fatlink.fatdatetime) < datetime.timedelta(seconds=(fatlink.duration * 60)):
@@ -298,12 +306,11 @@ def create_fatlink_view(request):
logger.debug("Submitting fleetactivitytracking by user %s" % request.user) logger.debug("Submitting fleetactivitytracking by user %s" % request.user)
if form.is_valid(): if form.is_valid():
fatlink = Fatlink() fatlink = Fatlink()
fatlink.name = slugify(form.cleaned_data["fatname"])
fatlink.fleet = form.cleaned_data["fleet"] fatlink.fleet = form.cleaned_data["fleet"]
fatlink.duration = form.cleaned_data["duration"] fatlink.duration = form.cleaned_data["duration"]
fatlink.fatdatetime = timezone.now() fatlink.fatdatetime = timezone.now()
fatlink.creator = request.user fatlink.creator = request.user
fatlink.hash = ''.join(random.choice(string.ascii_letters + string.digits) for i in range(10)) fatlink.hash = get_random_string(length=15)
try: try:
fatlink.full_clean() fatlink.full_clean()
fatlink.save() fatlink.save()
@@ -331,25 +338,19 @@ def create_fatlink_view(request):
@login_required @login_required
@permission_required('auth.fleetactivitytracking') @permission_required('auth.fleetactivitytracking')
def modify_fatlink_view(request, hash=""): def modify_fatlink_view(request, fat_hash=None):
logger.debug("modify_fatlink_view called by user %s" % request.user) logger.debug("modify_fatlink_view called by user %s" % request.user)
if not hash: fatlink = get_object_or_404(Fatlink, hash=fat_hash)
return redirect('fatlink:view')
try:
fatlink = Fatlink.objects.get(hash=hash)
except Fatlink.DoesNotExist:
raise Http404
if request.GET.get('removechar', None): if request.GET.get('removechar', None):
character_id = request.GET.get('removechar') character_id = request.GET.get('removechar')
character = EveCharacter.objects.get(character_id=character_id) character = EveCharacter.objects.get(character_id=character_id)
logger.debug("Removing character %s from fleetactivitytracking %s" % (character.character_name, fatlink.name)) logger.debug("Removing character %s from fleetactivitytracking %s" % (character.character_name, fatlink))
Fat.objects.filter(fatlink=fatlink).filter(character=character).delete() Fat.objects.filter(fatlink=fatlink).filter(character=character).delete()
if request.GET.get('deletefat', None): if request.GET.get('deletefat', None):
logger.debug("Removing fleetactivitytracking %s" % fatlink.name) logger.debug("Removing fleetactivitytracking %s" % fatlink)
fatlink.delete() fatlink.delete()
return redirect('fatlink:view') return redirect('fatlink:view')

View File

@@ -15,7 +15,7 @@
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div class="col-lg-6"> <div class="col-lg-6">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-condensed table-hover table-striped"> <table class="table table-condensed table-hover table-striped">
<tr> <tr>
<th class="col-md-1"></th> <th class="col-md-1"></th>
@@ -26,7 +26,7 @@
{% for char_name, user_id in member_list %} {% for char_name, user_id in member_list %}
<tr> <tr>
<td> <td>
<img src="http://image.eveonline.com/Character/{{ user_id.char_id }}_32.jpg" class="img-circle"> <img src="https://imageserver.eveonline.com/Character/{{ user_id.char_id }}_32.jpg" class="img-circle">
</td> </td>
<td> <td>
<p>{{ user_id.char_name }}</p> <p>{{ user_id.char_name }}</p>

View File

@@ -8,12 +8,12 @@
{% block content %} {% block content %}
<div class="col-lg-12"> <div class="col-lg-12">
{% include "fleetup/menu.html" %} {% include "fleetup/menu.html" %}
<div class="panel"> <div>
{% for a, j in doctrine.items %} {% for a, j in doctrine.items %}
{% regroup j.Data|dictsort:"Role" by Role as role_list %} {% regroup j.Data|dictsort:"Role" by Role as role_list %}
{% for Role in role_list %} {% for Role in role_list %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title"><b>{{ Role.grouper }}</b></h3> <h3 class="panel-title"><b>{{ Role.grouper }}</b></h3>
@@ -50,7 +50,7 @@
{% load humanize %}{{ item.EstPrice|intword }} {% load humanize %}{{ item.EstPrice|intword }}
</td> </td>
<td> <td>
{% for categories in item.Categories %} {% for categories in item.Categories %}
{{ categories }}, {{ categories }},
{% endfor %} {% endfor %}
</td> </td>

View File

@@ -8,12 +8,12 @@
{% block content %} {% block content %}
<div class="col-lg-12"> <div class="col-lg-12">
{% include "fleetup/menu.html" %} {% include "fleetup/menu.html" %}
<div class="panel"> <div>
{% if doctrines_list %} {% if doctrines_list %}
{% for a, j in doctrines_list.items %} {% for a, j in doctrines_list.items %}
{% regroup j|dictsort:"FolderName" by FolderName as folder_list %} {% regroup j|dictsort:"FolderName" by FolderName as folder_list %}
{% for FolderName in folder_list %} {% for FolderName in folder_list %}
<div class="col-lg-8"> <div>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title"><b>{{ FolderName.grouper }}</b></h3> <h3 class="panel-title"><b>{{ FolderName.grouper }}</b></h3>
@@ -29,11 +29,11 @@
<th class="col-lg-2">Note</th>--> <th class="col-lg-2">Note</th>-->
</tr> </tr>
{% for item in FolderName.list %} {% for item in FolderName.list %}
<tr> <tr>
<td> <td>
<a href="{% url 'fleetup:doctrine' item.DoctrineId %}"><img src="https://image.eveonline.com/InventoryType/{{ item.IconId }}_32.png"></a> <a href="{% url 'fleetup:doctrine' item.DoctrineId %}"><img src="https://image.eveonline.com/InventoryType/{{ item.IconId }}_32.png"></a>
</td> </td>
<td> <td>
{{ item.Name }} {{ item.Name }}
</td> </td>

View File

@@ -8,9 +8,9 @@
{% block content %} {% block content %}
<div class="col-lg-12"> <div class="col-lg-12">
{% include "fleetup/menu.html" %} {% include "fleetup/menu.html" %}
<div class="tab-content"> <div class="tab-content row">
<div id="fit" class="tab-pane fade in active"> <div id="fit" class="tab-pane fade in active">
<div class="col-lg-3"> <div class="col-lg-4">
{% for x, y in fitting_data.items %} {% for x, y in fitting_data.items %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
@@ -18,22 +18,24 @@
</div> </div>
<div class="panel-body"> <div class="panel-body">
{% for doctrin in y.Doctrines %} {% for doctrin in y.Doctrines %}
<h4>{{ doctrin.Name }}</h4> <div class="clearfix">
<div class="col-lg-12"> <h4>{{ doctrin.Name }}</h4>
<p>{% trans "Role in doctrine:" %} {{ doctrin.Role }}</p> <div class="col-lg-12">
</div> <p>{% trans "Role in doctrine:" %} {{ doctrin.Role }}</p>
<div class="col-lg-4"> </div>
<p>{% trans "Priority:" %}</p> <div class="col-lg-4">
</div> <p>{% trans "Priority:" %}</p>
<div class="col-lg-8"> </div>
<div class="progress"> <div class="col-lg-8">
<div class="progress-bar progress-bar-striped" role="progressbar" aria-valuenow="{{ doctrin.Priority }}" aria-valuemin="0" aria-valuemax="5" style="width: {% widthratio doctrin.Priority 5 100 %}%;"> <div class="progress">
{{ doctrin.Priority }}/5 <div class="progress-bar progress-bar-striped" role="progressbar" aria-valuenow="{{ doctrin.Priority }}" aria-valuemin="0" aria-valuemax="5" style="width: {% widthratio doctrin.Priority 5 100 %}%;">
{{ doctrin.Priority }}/5
</div>
</div> </div>
</div> </div>
</div> <div class="pull-right">
<div class="pull-right"> <a class="btn btn-primary" href="{% url 'fleetup:doctrine' doctrin.DoctrineId %}">{% trans "See doctrine" %}</a>
<a class="btn btn-primary" href="{% url 'fleetup:doctrine' doctrin.DoctrineId %}">{% trans "See doctrine" %}</a> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
@@ -56,10 +58,10 @@
<div class="panel-body"> <div class="panel-body">
<div class="list-group"> <div class="list-group">
{% for arbit, orbit in doctrines_list.items %} {% for arbit, orbit in doctrines_list.items %}
{% for fitting in orbit.Data %} {% for fitting in orbit.Data %}
<a href="{% url 'fleetup:fitting' fitting.FittingId %}" class="list-group-item"> <a href="{% url 'fleetup:fitting' fitting.FittingId %}" class="list-group-item">
<h4 class="list-group-item-heading">{{ fitting.Name }}<span class="pull-right"><img src="https://image.eveonline.com/InventoryType/{{ fitting.EveTypeId }}_32.png" class="img-circle"></span></h4> <h4 class="list-group-item-heading">{{ fitting.Name }}<span class="pull-right"><img src="https://image.eveonline.com/InventoryType/{{ fitting.EveTypeId }}_32.png" class="img-circle"></span></h4>
<p class="list-group-item-heading">{{ fitting.Role }} - {{ fitting.ShipType }}</p> <p class="list-group-item-heading">{{ fitting.Role }} - {{ fitting.ShipType }}</p>
</a> </a>
@@ -107,8 +109,8 @@
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<div class="col-lg-3"> <div class="col-lg-4">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title">{% trans "EFT/Export" %}</h3> <h3 class="panel-title">{% trans "EFT/Export" %}</h3>

View File

@@ -20,11 +20,11 @@
<th class="col-md-2">{% trans "Categories" %}</th> <th class="col-md-2">{% trans "Categories" %}</th>
</tr> </tr>
{% for id, fittings in fitting_list %} {% for id, fittings in fitting_list %}
<tr> <tr>
<td> <td>
<a href="{% url 'fleetup:fitting' fittings.fitting_id %}"><img src="https://image.eveonline.com/InventoryType/{{ fittings.icon_id }}_32.png"></a> <a href="{% url 'fleetup:fitting' fittings.fitting_id %}"><img src="https://image.eveonline.com/InventoryType/{{ fittings.icon_id }}_32.png"></a>
</td> </td>
<td> <td>
{{ fittings.name }} {{ fittings.name }}
</td> </td>
@@ -38,12 +38,12 @@
{% load humanize %}{{ fittings.estimated|intword }} {% load humanize %}{{ fittings.estimated|intword }}
</td> </td>
<td> <td>
{% for categories in fittings.categories %} {% for categories in fittings.categories %}
{{ categories }}, {{ categories }},
{% endfor %} {% endfor %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
{% else %} {% else %}

View File

@@ -8,15 +8,15 @@
{% block content %} {% block content %}
<div class="col-lg-12"> <div class="col-lg-12">
{% include "fleetup/menu.html" %} {% include "fleetup/menu.html" %}
<div class="panel"> <div>
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
<li class="active"><a data-toggle="tab" href="#operations">{% trans "Operations" %}</a></li> <li class="active"><a data-toggle="tab" href="#operations">{% trans "Operations" %}</a></li>
<li><a data-toggle="tab" href="#timers">{% trans "Timers" %}</a></li> <li><a data-toggle="tab" href="#timers">{% trans "Timers" %}</a></li>
</ul> </ul>
<div class="tab-content"> <div class="tab-content row">
<div id="operations" class="tab-pane fade in active"> <div id="operations" class="tab-pane fade in active">
<div class="col-lg-7"> <div class="col-lg-8">
{% if operations_list %} {% if operations_list %}
{% for subject, start in operations_list %} {% for subject, start in operations_list %}
<div class="panel panel-default"> <div class="panel panel-default">
@@ -31,7 +31,7 @@
</tr> </tr>
<tr> <tr>
<td class="col-md-6">{{ start.start|date:"l d M H:i" }} <span class="label label-success">{% trans "Eve Time" %}</span></td> <td class="col-md-6">{{ start.start|date:"l d M H:i" }} <span class="label label-success">{% trans "Eve Time" %}</span></td>
<td class="col-md-6">{{ start.end|date:"l d M H:i" }} <span class="label label-success">{% trans "Eve Time" %}</span></td> <td class="col-md-6">{{ start.end|date:"l d M H:i" }} <span class="label label-success">{% trans "Eve Time" %}</span></td>
</tr> </tr>
<tr> <tr>
@@ -39,13 +39,12 @@
<span id="localtime{{ start.operation_id }}"></span>&nbsp;<span class='label label-success'>Local time</span><br> <span id="localtime{{ start.operation_id }}"></span>&nbsp;<span class='label label-success'>Local time</span><br>
<div id="countdown{{ start.operation_id }}"></div> <div id="countdown{{ start.operation_id }}"></div>
</td> </td>
<td class="col-md-6"></td> <td class="col-md-6"></td>
</tr> </tr>
</table> </table>
<p>{{ start.details }}</p> {{ start.details|linebreaks }}
<div class="col-lg-12">
<table class="table table-condensed table-striped"> <table class="table table-condensed table-striped">
<tr> <tr>
<th class="col-md-4">{% trans "Location" %}</th> <th class="col-md-4">{% trans "Location" %}</th>
@@ -60,11 +59,11 @@
<td> <td>
{% if start.doctrine %} {% if start.doctrine %}
{% for doctrine in start.doctrine %} {% for doctrine in start.doctrine %}
<a href="{% url 'fleetup:doctrine' doctrine.Id %}" class="label label-success">{{ doctrine.Name }}</a> <a href="{% url 'fleetup:doctrine' doctrine.Id %}" class="label label-success">{{ doctrine.Name }}</a>
{% endfor %} {% endfor %}
{% else %} {% else %}
<span class="label label-danger">{% trans "TBA" %}</span> <span class="label label-danger">{% trans "TBA" %}</span>
{% endif %} {% endif %}
@@ -81,7 +80,6 @@
</td> </td>
</tr> </tr>
</table> </table>
</div>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
@@ -89,7 +87,7 @@
<h3>{% trans "There seems to be no Operations in the near future." %}</h3> <h3>{% trans "There seems to be no Operations in the near future." %}</h3>
{% endif %} {% endif %}
</div> </div>
<div class="col-lg-3"> <div class="col-lg-4">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h2 class="panel-title">{% trans "Current Eve Time:" %}</h2> <h2 class="panel-title">{% trans "Current Eve Time:" %}</h2>
@@ -158,7 +156,7 @@
<td> <td>
{{ type.owner }} {{ type.owner }}
</td> </td>
<td> <td>
{{ type.notes }} {{ type.notes }}
</td> </td>
</tr> </tr>

View File

@@ -1,28 +1,68 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.models import Group from django.contrib.auth.models import Group as BaseGroup
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed
from django.dispatch import receiver
from .models import AuthGroup from .models import AuthGroup
from .models import GroupRequest from .models import GroupRequest
class AuthGroupAdmin(admin.ModelAdmin): class AuthGroupInlineAdmin(admin.StackedInline):
""" model = AuthGroup
Admin model for AuthGroup filter_horizontal = ('group_leaders', 'states',)
""" fields = ('description', 'group_leaders', 'states', 'internal', 'hidden', 'open', 'public')
filter_horizontal = ('group_leaders',) verbose_name_plural = 'Auth Settings'
verbose_name = ''
def has_add_permission(self, request):
return False
def has_delete_permission(self, request, obj=None):
return False
def has_change_permission(self, request, obj=None):
return request.user.has_perm('auth.change_group')
class ProxyGroup(Group): class GroupAdmin(admin.ModelAdmin):
filter_horizontal = ('permissions',)
inlines = (AuthGroupInlineAdmin,)
class Group(BaseGroup):
class Meta: class Meta:
proxy = True proxy = True
verbose_name = Group._meta.verbose_name verbose_name = BaseGroup._meta.verbose_name
verbose_name_plural = Group._meta.verbose_name_plural verbose_name_plural = BaseGroup._meta.verbose_name_plural
try: try:
admin.site.unregister(Group) admin.site.unregister(BaseGroup)
finally: finally:
admin.site.register(ProxyGroup) admin.site.register(Group, GroupAdmin)
admin.site.register(GroupRequest) admin.site.register(GroupRequest)
admin.site.register(AuthGroup, AuthGroupAdmin)
@receiver(pre_save, sender=Group)
def redirect_pre_save(sender, signal=None, *args, **kwargs):
pre_save.send(BaseGroup, *args, **kwargs)
@receiver(post_save, sender=Group)
def redirect_post_save(sender, signal=None, *args, **kwargs):
post_save.send(BaseGroup, *args, **kwargs)
@receiver(pre_delete, sender=Group)
def redirect_pre_delete(sender, signal=None, *args, **kwargs):
pre_delete.send(BaseGroup, *args, **kwargs)
@receiver(post_delete, sender=Group)
def redirect_post_delete(sender, signal=None, *args, **kwargs):
post_delete.send(BaseGroup, *args, **kwargs)
@receiver(m2m_changed, sender=Group.permissions.through)
def redirect_m2m_changed_permissions(sender, signal=None, *args, **kwargs):
m2m_changed.send(BaseGroup, *args, **kwargs)

View File

@@ -4,3 +4,4 @@ from django.apps import AppConfig
class GroupManagementConfig(AppConfig): class GroupManagementConfig(AppConfig):
name = 'allianceauth.groupmanagement' name = 'allianceauth.groupmanagement'
label = 'groupmanagement' label = 'groupmanagement'
verbose_name = 'Group Management'

View File

@@ -1,4 +1,5 @@
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.db.models import Q
class GroupManager: class GroupManager:
@@ -6,7 +7,12 @@ class GroupManager:
pass pass
@staticmethod @staticmethod
def get_joinable_groups(): def get_joinable_groups(state):
return Group.objects.select_related('authgroup').exclude(authgroup__internal=True)\
.filter(Q(authgroup__states=state) | Q(authgroup__states=None))
@staticmethod
def get_all_non_internal_groups():
return Group.objects.select_related('authgroup').exclude(authgroup__internal=True) return Group.objects.select_related('authgroup').exclude(authgroup__internal=True)
@staticmethod @staticmethod
@@ -14,13 +20,35 @@ class GroupManager:
return Group.objects.select_related('authgroup').filter(authgroup__group_leaders__in=[user]) return Group.objects.select_related('authgroup').filter(authgroup__group_leaders__in=[user])
@staticmethod @staticmethod
def joinable_group(group): def joinable_group(group, state):
""" """
Check if a group is a user joinable group, i.e. Check if a group is a user/state joinable group, i.e.
not an internal group for Corp, Alliance, Members etc not an internal group for Corp, Alliance, Members etc,
or restricted from the user's current state.
:param group: django.contrib.auth.models.Group object :param group: django.contrib.auth.models.Group object
:param state: allianceauth.authentication.State object
:return: bool True if its joinable, False otherwise :return: bool True if its joinable, False otherwise
""" """
if len(group.authgroup.states.all()) != 0 and state not in group.authgroup.states.all():
return False
return not group.authgroup.internal
@staticmethod
def check_internal_group(group):
"""
Check if a group is auditable, i.e not an internal group
:param group: django.contrib.auth.models.Group object
:return: bool True if it is auditable, false otherwise
"""
return not group.authgroup.internal
@staticmethod
def check_internal_group(group):
"""
Check if a group is auditable, i.e not an internal group
:param group: django.contrib.auth.models.Group object
:return: bool True if it is auditable, false otherwise
"""
return not group.authgroup.internal return not group.authgroup.internal
@staticmethod @staticmethod

View File

@@ -15,7 +15,7 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='ProxyGroup', name='Group',
fields=[ fields=[
], ],
options={ options={

View File

@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.10 on 2018-02-23 23:09
from __future__ import unicode_literals
from django.db import migrations
def delete_permissions(apps, schema_editor):
AuthGroup = apps.get_model('groupmanagement', 'AuthGroup')
ContentType = apps.get_model('contenttypes', 'ContentType')
Permission = apps.get_model('auth', 'Permission')
ct = ContentType.objects.get_for_model(AuthGroup)
Permission.objects.filter(content_type=ct).delete()
def recreate_permissions(apps, schema_editor):
AuthGroup = apps.get_model('groupmanagement', 'AuthGroup')
ContentType = apps.get_model('contenttypes', 'ContentType')
Permission = apps.get_model('auth', 'Permission')
ct = ContentType.objects.get_for_model(AuthGroup)
Permission.objects.create(content_type=ct, name='Can add auth group', codename='add_authgroup')
Permission.objects.create(content_type=ct, name='Can delete auth group', codename='delete_authgroup')
Permission.objects.create(content_type=ct, name='Can change auth group', codename='change_authgroup')
class Migration(migrations.Migration):
dependencies = [
('groupmanagement', '0007_on_delete'),
]
operations = [
migrations.AlterModelOptions(
name='authgroup',
options={'default_permissions': (), 'permissions': (('request_groups', 'Can request non-public groups'),)},
),
migrations.RunPython(delete_permissions, recreate_permissions)
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 2.0.6 on 2018-06-04 02:45
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('auth', '0008_alter_user_username_max_length'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('groupmanagement', '0008_remove_authgroup_permissions'),
]
operations = [
migrations.CreateModel(
name='RequestLog',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('request_type', models.NullBooleanField(default=0)),
('request_info', models.CharField(max_length=254)),
('action', models.BooleanField(default=0)),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.Group')),
('request_actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 2.0.6 on 2018-07-11 00:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0016_ownershiprecord'),
('groupmanagement', '0009_requestlog'),
]
operations = [
migrations.AddField(
model_name='authgroup',
name='states',
field=models.ManyToManyField(blank=True, help_text='States listed here will have the ability to join this group provided they have the proper permissions.', related_name='valid_states', to='authentication.State'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 2.0.8 on 2018-12-07 08:56
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('groupmanagement', '0010_authgroup_states'),
]
operations = [
migrations.AddField(
model_name='requestlog',
name='date',
field=models.DateTimeField(default=datetime.datetime(2018, 12, 7, 8, 56, 33, 846342)),
),
]

View File

@@ -3,8 +3,8 @@ from django.contrib.auth.models import User
from django.db import models from django.db import models
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from allianceauth.authentication.models import State
from allianceauth.eveonline.models import EveCharacter from datetime import datetime
class GroupRequest(models.Model): class GroupRequest(models.Model):
@@ -25,6 +25,38 @@ class GroupRequest(models.Model):
return self.user.username + ":" + self.group.name return self.user.username + ":" + self.group.name
class RequestLog(models.Model):
request_type = models.NullBooleanField(default=0)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
request_info = models.CharField(max_length=254)
action = models.BooleanField(default=0)
request_actor = models.ForeignKey(User, on_delete=models.CASCADE)
date = models.DateTimeField(auto_now_add=datetime.utcnow())
def requestor(self):
return self.request_info.split(":")[0]
def type_to_str(self):
if self.request_type is None:
return "Removed"
elif self.request_type is True:
return "Leave"
elif self.request_type is False:
return "Join"
def action_to_str(self):
if self.action is True:
return "Accept"
elif self.action is False:
return "Reject"
def req_char(self):
usr = self.requestor()
user = User.objects.get(username=usr)
return user.profile.main_character
class AuthGroup(models.Model): class AuthGroup(models.Model):
""" """
Extends Django Group model with a one-to-one field Extends Django Group model with a one-to-one field
@@ -67,6 +99,10 @@ class AuthGroup(models.Model):
"specifically. Use the auth.group_management permission to allow " "specifically. Use the auth.group_management permission to allow "
"a user to manage all groups.") "a user to manage all groups.")
states = models.ManyToManyField(State, related_name='valid_states', blank=True,
help_text="States listed here will have the ability to join this group provided "
"they have the proper permissions.")
description = models.CharField(max_length=512, blank=True, help_text="Description of the group shown to users.") description = models.CharField(max_length=512, blank=True, help_text="Description of the group shown to users.")
def __str__(self): def __str__(self):
@@ -76,6 +112,7 @@ class AuthGroup(models.Model):
permissions = ( permissions = (
("request_groups", u"Can request non-public groups"), ("request_groups", u"Can request non-public groups"),
) )
default_permissions = tuple()
@receiver(post_save, sender=Group) @receiver(post_save, sender=Group)

View File

@@ -0,0 +1,62 @@
{% extends "allianceauth/base.html" %}
{% load staticfiles %}
{% load i18n %}
{% block page_title %}{{ group }} {% trans "Audit Log" %}{% endblock page_title %}
{% block content %}
<div class="col-lg-12">
<br>
{% include 'groupmanagement/menu.html' %}
<div>
<h3>{{ group }} Audit Log</h3>
<p> All times displayed are EVE/UTC.</p>
{% if entries %}
<div class="table-responsive">
<table class="table table-striped" id="log-entries">
<thead>
<th class="text-center" scope="col">{% trans "Date/Time" %}</th>
<th class="text-center" scope="col">{% trans "Requestor" %}</th>
<th class="text-center" scope="col">{% trans "Main Character" %}</th>
<th class="text-center" scope="col">{% trans "Group" %}</th>
<th class="text-center" scope="col">{% trans "Type" %}</th>
<th class="text-center" scope="col">{% trans "Action" %}</th>
<th class="text-center" scope="col">{% trans "Actor" %}</th>
</thead>
<tbody>
{% for entry in entries %}
<tr>
<td class="text-center">{{ entry.date }}</td>
<td class="text-center">{{ entry.requestor }}</td>
<td class="text-center">{{ entry.req_char }}</td>
<td class="text-center">{{ entry.group }}</td>
<td class="text-center">{{ entry.type_to_str }}</td>
{% if entry.request_type is None %}
<td class="text-center"> Removed</td>
{% else %}
<td class="text-center">{{ entry.action_to_str }}</td>
{% endif %}
<td class="text-center">{{ entry.request_actor }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-warning text-center">{% trans "No entries found for this group." %}</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block extra_javascript %}
{% include 'bundles/datatables-js.html' %}
{% endblock %}
{% block extra_css %}
{% include 'bundles/datatables-css.html' %}
{% endblock %}
{% block extra_script %}
$(document).ready(function(){
$('#log-entries').DataTable();
});
{% endblock %}

View File

@@ -41,6 +41,9 @@
title="{% trans "View Members" %}"> title="{% trans "View Members" %}">
<i class="glyphicon glyphicon-eye-open"></i> <i class="glyphicon glyphicon-eye-open"></i>
</a> </a>
<a href="{% url "groupmanagement:audit_log" group.id %}" class="btn btn-info" title="{% trans "Audit Members" %}">
<i class="glyphicon glyphicon-list-alt"></i>
</a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@@ -12,6 +12,7 @@ urlpatterns = [
name='membership'), name='membership'),
url(r'^membership/(\w+)/$', views.group_membership_list, url(r'^membership/(\w+)/$', views.group_membership_list,
name='membership_list'), name='membership_list'),
url(r'^membership/(\w+)/audit/$', views.group_membership_audit, name="audit_log"),
url(r'^membership/(\w+)/remove/(\w+)/$', views.group_membership_remove, url(r'^membership/(\w+)/remove/(\w+)/$', views.group_membership_remove,
name='membership_remove'), name='membership_remove'),
url(r'^request_add/(\w+)', views.group_request_add, url(r'^request_add/(\w+)', views.group_request_add,

View File

@@ -5,15 +5,18 @@ from django.contrib.auth.decorators import login_required
from django.contrib.auth.decorators import user_passes_test from django.contrib.auth.decorators import user_passes_test
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.core.paginator import Paginator, EmptyPage
from django.db.models import Count from django.db.models import Count
from django.http import Http404 from django.http import Http404
from django.shortcuts import render, redirect, get_object_or_404 from django.shortcuts import render, redirect, get_object_or_404
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from .managers import GroupManager from .managers import GroupManager
from .models import GroupRequest from .models import GroupRequest, RequestLog
from allianceauth.notifications import notify from allianceauth.notifications import notify
from django.conf import settings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -53,7 +56,7 @@ def group_membership(request):
# Get all open and closed groups # Get all open and closed groups
if GroupManager.has_management_permission(request.user): if GroupManager.has_management_permission(request.user):
# Full access # Full access
groups = GroupManager.get_joinable_groups() groups = GroupManager.get_all_non_internal_groups()
else: else:
# Group leader specific # Group leader specific
groups = GroupManager.get_group_leaders_groups(request.user) groups = GroupManager.get_group_leaders_groups(request.user)
@@ -65,6 +68,31 @@ def group_membership(request):
return render(request, 'groupmanagement/groupmembership.html', context=render_items) return render(request, 'groupmanagement/groupmembership.html', context=render_items)
@login_required
@user_passes_test(GroupManager.can_manage_groups)
def group_membership_audit(request, group_id):
logger.debug("group_management_audit called by user %s" % request.user)
group = get_object_or_404(Group, id=group_id)
try:
# Check its a joinable group i.e. not corp or internal
# And the user has permission to manage it
if not GroupManager.check_internal_group(group) or not GroupManager.can_manage_group(request.user, group):
logger.warning("User %s attempted to view the membership of group %s but permission was denied" %
(request.user, group_id))
raise PermissionDenied
except ObjectDoesNotExist:
raise Http404("Group does not exist")
render_items = {'group': group.name}
entries = RequestLog.objects.filter(group=group).order_by('-date')
render_items['entries'] = entries
return render(request, 'groupmanagement/audit.html', context=render_items)
@login_required @login_required
@user_passes_test(GroupManager.can_manage_groups) @user_passes_test(GroupManager.can_manage_groups)
def group_membership_list(request, group_id): def group_membership_list(request, group_id):
@@ -74,7 +102,7 @@ def group_membership_list(request, group_id):
# Check its a joinable group i.e. not corp or internal # Check its a joinable group i.e. not corp or internal
# And the user has permission to manage it # And the user has permission to manage it
if not GroupManager.joinable_group(group) or not GroupManager.can_manage_group(request.user, group): if not GroupManager.check_internal_group(group) or not GroupManager.can_manage_group(request.user, group):
logger.warning("User %s attempted to view the membership of group %s but permission was denied" % logger.warning("User %s attempted to view the membership of group %s but permission was denied" %
(request.user, group_id)) (request.user, group_id))
raise PermissionDenied raise PermissionDenied
@@ -105,13 +133,16 @@ def group_membership_remove(request, group_id, user_id):
try: try:
# Check its a joinable group i.e. not corp or internal # Check its a joinable group i.e. not corp or internal
# And the user has permission to manage it # And the user has permission to manage it
if not GroupManager.joinable_group(group) or not GroupManager.can_manage_group(request.user, group): if not GroupManager.check_internal_group(group) or not GroupManager.can_manage_group(request.user, group):
logger.warning("User %s attempted to remove a user from group %s but permission was denied" % (request.user, logger.warning("User %s attempted to remove a user from group %s but permission was denied" % (request.user,
group_id)) group_id))
raise PermissionDenied raise PermissionDenied
try: try:
user = group.user_set.get(id=user_id) user = group.user_set.get(id=user_id)
request_info = user.username + ":" + group.name
log = RequestLog(request_type=None,group=group,request_info=request_info,action=1,request_actor=request.user)
log.save()
# Remove group from user # Remove group from user
user.groups.remove(group) user.groups.remove(group)
logger.info("User %s removed user %s from group %s" % (request.user, user, group)) logger.info("User %s removed user %s from group %s" % (request.user, user, group))
@@ -133,12 +164,14 @@ def group_accept_request(request, group_request_id):
try: try:
group, created = Group.objects.get_or_create(name=group_request.group.name) group, created = Group.objects.get_or_create(name=group_request.group.name)
if not GroupManager.joinable_group(group_request.group) or \ if not GroupManager.joinable_group(group_request.group, group_request.user.profile.state) or \
not GroupManager.can_manage_group(request.user, group_request.group): not GroupManager.can_manage_group(request.user, group_request.group):
raise PermissionDenied raise PermissionDenied
group_request.user.groups.add(group) group_request.user.groups.add(group)
group_request.user.save() group_request.user.save()
log = RequestLog(request_type=group_request.leave_request,group=group,request_info=group_request.__str__(),action=1,request_actor=request.user)
log.save()
group_request.delete() group_request.delete()
logger.info("User %s accepted group request from user %s to group %s" % ( logger.info("User %s accepted group request from user %s to group %s" % (
request.user, group_request.user, group_request.group.name)) request.user, group_request.user, group_request.group.name))
@@ -172,6 +205,8 @@ def group_reject_request(request, group_request_id):
if group_request: if group_request:
logger.info("User %s rejected group request from user %s to group %s" % ( logger.info("User %s rejected group request from user %s to group %s" % (
request.user, group_request.user, group_request.group.name)) request.user, group_request.user, group_request.group.name))
log = RequestLog(request_type=group_request.leave_request,group=group_request.group,request_info=group_request.__str__(),action=0,request_actor=request.user)
log.save()
group_request.delete() group_request.delete()
notify(group_request.user, "Group Application Rejected", level="danger", notify(group_request.user, "Group Application Rejected", level="danger",
message="Your application to %s has been rejected." % group_request.group) message="Your application to %s has been rejected." % group_request.group)
@@ -204,6 +239,8 @@ def group_leave_accept_request(request, group_request_id):
group, created = Group.objects.get_or_create(name=group_request.group.name) group, created = Group.objects.get_or_create(name=group_request.group.name)
group_request.user.groups.remove(group) group_request.user.groups.remove(group)
group_request.user.save() group_request.user.save()
log = RequestLog(request_type=group_request.leave_request,group=group_request.group,request_info=group_request.__str__(),action=1,request_actor=request.user)
log.save()
group_request.delete() group_request.delete()
logger.info("User %s accepted group leave request from user %s to group %s" % ( logger.info("User %s accepted group leave request from user %s to group %s" % (
request.user, group_request.user, group_request.group.name)) request.user, group_request.user, group_request.group.name))
@@ -236,6 +273,8 @@ def group_leave_reject_request(request, group_request_id):
raise PermissionDenied raise PermissionDenied
if group_request: if group_request:
log = RequestLog(request_type=group_request.leave_request,group=group_request.group,request_info=group_request.__str__(),action=0,request_actor=request.user)
log.save()
group_request.delete() group_request.delete()
logger.info("User %s rejected group leave request from user %s for group %s" % ( logger.info("User %s rejected group leave request from user %s for group %s" % (
request.user, group_request.user, group_request.group.name)) request.user, group_request.user, group_request.group.name))
@@ -262,7 +301,7 @@ def groups_view(request):
logger.debug("groups_view called by user %s" % request.user) logger.debug("groups_view called by user %s" % request.user)
groups = [] groups = []
group_query = GroupManager.get_joinable_groups() group_query = GroupManager.get_joinable_groups(request.user.profile.state)
if not request.user.has_perm('groupmanagement.request_groups'): if not request.user.has_perm('groupmanagement.request_groups'):
# Filter down to public groups only for non-members # Filter down to public groups only for non-members
@@ -284,11 +323,18 @@ def groups_view(request):
def group_request_add(request, group_id): def group_request_add(request, group_id):
logger.debug("group_request_add called by user %s for group id %s" % (request.user, group_id)) logger.debug("group_request_add called by user %s for group id %s" % (request.user, group_id))
group = Group.objects.get(id=group_id) group = Group.objects.get(id=group_id)
if not GroupManager.joinable_group(group): state = request.user.profile.state
if not GroupManager.joinable_group(group, state):
logger.warning("User %s attempted to join group id %s but it is not a joinable group" % logger.warning("User %s attempted to join group id %s but it is not a joinable group" %
(request.user, group_id)) (request.user, group_id))
messages.warning(request, _("You cannot join that group")) messages.warning(request, _("You cannot join that group"))
return redirect('groupmanagement:groups') return redirect('groupmanagement:groups')
if group in request.user.groups.all():
# User is already a member of this group.
logger.warning("User %s attempted to join group id %s but they are already a member." %
(request.user, group_id))
messages.warning(request, "You are already a member of that group.")
return redirect('groupmanagement:groups')
if not request.user.has_perm('groupmanagement.request_groups') and not group.authgroup.public: if not request.user.has_perm('groupmanagement.request_groups') and not group.authgroup.public:
# Does not have the required permission, trying to join a non-public group # Does not have the required permission, trying to join a non-public group
logger.warning("User %s attempted to join group id %s but it is not a public group" % logger.warning("User %s attempted to join group id %s but it is not a public group" %
@@ -299,11 +345,15 @@ def group_request_add(request, group_id):
logger.info("%s joining %s as is an open group" % (request.user, group)) logger.info("%s joining %s as is an open group" % (request.user, group))
request.user.groups.add(group) request.user.groups.add(group)
return redirect("groupmanagement:groups") return redirect("groupmanagement:groups")
req = GroupRequest.objects.filter(user=request.user, group=group)
if len(req) > 0:
logger.info("%s attempted to join %s but already has an open application" % (request.user, group))
messages.warning(request, "You already have a pending application for that group.")
return redirect("groupmanagement:groups")
grouprequest = GroupRequest() grouprequest = GroupRequest()
grouprequest.status = _('Pending') grouprequest.status = _('Pending')
grouprequest.group = group grouprequest.group = group
grouprequest.user = request.user grouprequest.user = request.user
grouprequest.main_char = request.user.profile.main_character
grouprequest.leave_request = False grouprequest.leave_request = False
grouprequest.save() grouprequest.save()
logger.info("Created group request for user %s to group %s" % (request.user, Group.objects.get(id=group_id))) logger.info("Created group request for user %s to group %s" % (request.user, Group.objects.get(id=group_id)))
@@ -315,7 +365,7 @@ def group_request_add(request, group_id):
def group_request_leave(request, group_id): def group_request_leave(request, group_id):
logger.debug("group_request_leave called by user %s for group id %s" % (request.user, group_id)) logger.debug("group_request_leave called by user %s for group id %s" % (request.user, group_id))
group = Group.objects.get(id=group_id) group = Group.objects.get(id=group_id)
if not GroupManager.joinable_group(group): if not GroupManager.check_internal_group(group):
logger.warning("User %s attempted to leave group id %s but it is not a joinable group" % logger.warning("User %s attempted to leave group id %s but it is not a joinable group" %
(request.user, group_id)) (request.user, group_id))
messages.warning(request, _("You cannot leave that group")) messages.warning(request, _("You cannot leave that group"))
@@ -329,11 +379,19 @@ def group_request_leave(request, group_id):
logger.info("%s leaving %s as is an open group" % (request.user, group)) logger.info("%s leaving %s as is an open group" % (request.user, group))
request.user.groups.remove(group) request.user.groups.remove(group)
return redirect("groupmanagement:groups") return redirect("groupmanagement:groups")
req = GroupRequest.objects.filter(user=request.user, group=group)
if len(req) > 0:
logger.info("%s attempted to leave %s but already has an pending leave request." % (request.user, group))
messages.warning(request, "You already have a pending leave request for that group.")
return redirect("groupmanagement:groups")
if getattr(settings, 'AUTO_LEAVE', False):
logger.info("%s leaving joinable group %s due to auto_leave" % (request.user, group))
request.user.groups.remove(group)
return redirect('groupmanagement:groups')
grouprequest = GroupRequest() grouprequest = GroupRequest()
grouprequest.status = _('Pending') grouprequest.status = _('Pending')
grouprequest.group = group grouprequest.group = group
grouprequest.user = request.user grouprequest.user = request.user
grouprequest.main_char = request.user.profile.main_character
grouprequest.leave_request = True grouprequest.leave_request = True
grouprequest.save() grouprequest.save()
logger.info("Created group leave request for user %s to group %s" % (request.user, Group.objects.get(id=group_id))) logger.info("Created group leave request for user %s to group %s" % (request.user, Group.objects.get(id=group_id)))

View File

@@ -1,5 +1,5 @@
from allianceauth.services.hooks import MenuItemHook, UrlHook from allianceauth.services.hooks import MenuItemHook, UrlHook
from django.utils.translation import ugettext_lazy as _
from allianceauth import hooks from allianceauth import hooks
from allianceauth.hrapplications import urls from allianceauth.hrapplications import urls
@@ -7,7 +7,7 @@ from allianceauth.hrapplications import urls
class ApplicationsMenu(MenuItemHook): class ApplicationsMenu(MenuItemHook):
def __init__(self): def __init__(self):
MenuItemHook.__init__(self, MenuItemHook.__init__(self,
'Applications', _('Applications'),
'fa fa-file-o fa-fw', 'fa fa-file-o fa-fw',
'hrapplications:index', 'hrapplications:index',
navactive=['hrapplications:']) navactive=['hrapplications:'])

View File

@@ -154,6 +154,12 @@
class="btn btn-primary"> class="btn btn-primary">
<span class="glyphicon glyphicon-eye-open"></span> <span class="glyphicon glyphicon-eye-open"></span>
</a> </a>
{% if perms.hrapplications.delete_application %}
<a href="{% url 'hrapplications:remove' app.id %}"
class="btn btn-danger">
<span class="glyphicon glyphicon-remove"></span>
</a>
{% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@@ -13,19 +13,19 @@ urlpatterns = [
name="create_view"), name="create_view"),
url(r'^remove/(\w+)', views.hr_application_remove, url(r'^remove/(\w+)', views.hr_application_remove,
name="remove"), name="remove"),
url(r'view/(\w+)', views.hr_application_view, url(r'^view/(\w+)', views.hr_application_view,
name="view"), name="view"),
url(r'personal/view/(\w+)', views.hr_application_personal_view, url(r'^personal/view/(\w+)', views.hr_application_personal_view,
name="personal_view"), name="personal_view"),
url(r'personal/removal/(\w+)', url(r'^personal/removal/(\w+)',
views.hr_application_personal_removal, views.hr_application_personal_removal,
name="personal_removal"), name="personal_removal"),
url(r'approve/(\w+)', views.hr_application_approve, url(r'^approve/(\w+)', views.hr_application_approve,
name="approve"), name="approve"),
url(r'reject/(\w+)', views.hr_application_reject, url(r'^reject/(\w+)', views.hr_application_reject,
name="reject"), name="reject"),
url(r'search/', views.hr_application_search, url(r'^search/', views.hr_application_search,
name="search"), name="search"),
url(r'mark_in_progress/(\w+)', views.hr_application_mark_in_progress, url(r'^mark_in_progress/(\w+)', views.hr_application_mark_in_progress,
name="mark_in_progress"), name="mark_in_progress"),
] ]

View File

@@ -67,7 +67,7 @@ def hr_application_create_view(request, form_id=None):
"")) ""))
response.save() response.save()
logger.info("%s created %s" % (request.user, application)) logger.info("%s created %s" % (request.user, application))
return redirect('hrapplications:view') return redirect('hrapplications:personal_view', application.id)
else: else:
questions = app_form.questions.all() questions = app_form.questions.all()
return render(request, 'hrapplications/create.html', return render(request, 'hrapplications/create.html',
@@ -95,7 +95,7 @@ def hr_application_personal_view(request, app_id):
return render(request, 'hrapplications/view.html', context=context) return render(request, 'hrapplications/view.html', context=context)
else: else:
logger.warn("User %s not authorized to view %s" % (request.user, app)) logger.warn("User %s not authorized to view %s" % (request.user, app))
return redirect('hrapplications:view') return redirect('hrapplications:personal_view')
@login_required @login_required
@@ -110,7 +110,7 @@ def hr_application_personal_removal(request, app_id):
logger.warn("User %s attempting to delete reviewed app %s" % (request.user, app)) logger.warn("User %s attempting to delete reviewed app %s" % (request.user, app))
else: else:
logger.warn("User %s not authorized to delete %s" % (request.user, app)) logger.warn("User %s not authorized to delete %s" % (request.user, app))
return redirect('hrapplications:view') return redirect('hrapplications:index')
@login_required @login_required
@@ -158,7 +158,7 @@ def hr_application_remove(request, app_id):
logger.info("User %s deleting %s" % (request.user, app)) logger.info("User %s deleting %s" % (request.user, app))
app.delete() app.delete()
notify(app.user, "Application Deleted", message="Your application to %s was deleted." % app.form.corp) notify(app.user, "Application Deleted", message="Your application to %s was deleted." % app.form.corp)
return redirect('hrapplications:view') return redirect('hrapplications:index')
@login_required @login_required
@@ -175,7 +175,7 @@ def hr_application_approve(request, app_id):
level="success") level="success")
else: else:
logger.warn("User %s not authorized to approve %s" % (request.user, app)) logger.warn("User %s not authorized to approve %s" % (request.user, app))
return redirect('hrapplications:view') return redirect('hrapplications:index')
@login_required @login_required
@@ -192,7 +192,7 @@ def hr_application_reject(request, app_id):
level="danger") level="danger")
else: else:
logger.warn("User %s not authorized to reject %s" % (request.user, app)) logger.warn("User %s not authorized to reject %s" % (request.user, app))
return redirect('hrapplications:view') return redirect('hrapplications:index')
@login_required @login_required

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,12 @@
from allianceauth.services.hooks import MenuItemHook, UrlHook from allianceauth.services.hooks import MenuItemHook, UrlHook
from django.utils.translation import ugettext_lazy as _
from allianceauth import hooks from allianceauth import hooks
from . import urls from . import urls
class OpTimerboardMenu(MenuItemHook): class OpTimerboardMenu(MenuItemHook):
def __init__(self): def __init__(self):
MenuItemHook.__init__(self, 'Fleet Operations', MenuItemHook.__init__(self, _('Fleet Operations'),
'fa fa-exclamation fa-fw', 'fa fa-exclamation fa-fw',
'optimer:view', 'optimer:view',
navactive=['optimer:']) navactive=['optimer:'])

View File

@@ -19,7 +19,8 @@
<div class="col-lg-12 text-center row"> <div class="col-lg-12 text-center row">
<div class="label label-info text-left"> <div class="label label-info text-left">
<b>{% trans "Current Eve Time:" %} </b> <b>{% trans "Current Eve Time:" %} </b>
</div><div class="label label-info text-left" id="current-time"></div> </div>
<strong class="label label-info text-left" id="current-time"></strong>
<br /> <br />
</div> </div>
@@ -111,7 +112,7 @@
} }
function updateClock() { function updateClock() {
document.getElementById("current-time").innerHTML = "<b>" + moment.utc().format('LLLL') + "</b>"; document.getElementById("current-time").innerHTML = getCurrentEveTimeString();
} }
</script> </script>
{% endblock content %} {% endblock content %}

View File

@@ -8,6 +8,7 @@ from allianceauth.tests.auth_utils import AuthUtils
class PermissionsToolViewsTestCase(WebTest): class PermissionsToolViewsTestCase(WebTest):
def setUp(self): def setUp(self):
self.member = AuthUtils.create_member('auth_member') self.member = AuthUtils.create_member('auth_member')
AuthUtils.add_main_character(self.member, 'test character', '1234', '2345', 'test corp', 'testc')
self.member.email = 'auth_member@example.com' self.member.email = 'auth_member@example.com'
self.member.save() self.member.save()
self.none_user = AuthUtils.create_user('none_user', disconnect_signals=True) self.none_user = AuthUtils.create_user('none_user', disconnect_signals=True)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
from .celery import app as celery_app

View File

@@ -11,6 +11,10 @@ app = Celery('{{ project_name }}')
# Using a string here means the worker don't have to serialize # Using a string here means the worker don't have to serialize
# the configuration object to child processes. # the configuration object to child processes.
app.config_from_object('django.conf:settings') app.config_from_object('django.conf:settings')
app.conf.ONCE = {
'backend': 'allianceauth.services.tasks.DjangoBackend',
'settings': {}
}
# Load task modules from all registered Django app configs. # Load task modules from all registered Django app configs.
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)

View File

@@ -1,14 +1,10 @@
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
""" """
Django settings for alliance_auth project. DO NOT EDIT THIS FILE
Generated by 'django-admin startproject' using Django 1.10.1. This settings file contains everything needed for Alliance Auth projects to function.
It gets overwritten by the 'allianceauth update' command.
For more information on this file, see If you wish to make changes, overload the setting in your project's settings file (local.py).
https://docs.djangoproject.com/en/1.10/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
""" """
import os import os
@@ -17,7 +13,6 @@ from django.contrib import messages
from celery.schedules import crontab from celery.schedules import crontab
INSTALLED_APPS = [ INSTALLED_APPS = [
# Core apps - required to function
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
@@ -38,17 +33,19 @@ INSTALLED_APPS = [
'allianceauth.thirdparty.navhelper', 'allianceauth.thirdparty.navhelper',
] ]
SECRET_KEY = "wow I'm a really bad default secret key"
# Celery configuration # Celery configuration
BROKER_URL = 'redis://localhost:6379/0' BROKER_URL = 'redis://localhost:6379/0'
CELERYBEAT_SCHEDULER = "django_celery_beat.schedulers.DatabaseScheduler" CELERYBEAT_SCHEDULER = "django_celery_beat.schedulers.DatabaseScheduler"
CELERYBEAT_SCHEDULE = { CELERYBEAT_SCHEDULE = {
'esi_cleanup_callbackredirect': { 'esi_cleanup_callbackredirect': {
'task': 'esi.tasks.cleanup_callbackredirect', 'task': 'esi.tasks.cleanup_callbackredirect',
'schedule': crontab(hour='*/4'), 'schedule': crontab(minute=0, hour='*/4'),
}, },
'esi_cleanup_token': { 'esi_cleanup_token': {
'task': 'esi.tasks.cleanup_token', 'task': 'esi.tasks.cleanup_token',
'schedule': crontab(day_of_month='*/1'), 'schedule': crontab(minute=0, hour=0),
}, },
'run_model_update': { 'run_model_update': {
'task': 'allianceauth.eveonline.tasks.run_model_update', 'task': 'allianceauth.eveonline.tasks.run_model_update',
@@ -56,7 +53,7 @@ CELERYBEAT_SCHEDULE = {
}, },
'check_all_character_ownership': { 'check_all_character_ownership': {
'task': 'allianceauth.authentication.tasks.check_all_character_ownership', 'task': 'allianceauth.authentication.tasks.check_all_character_ownership',
'schedule': crontab(hour='*/4'), 'schedule': crontab(minute=0, hour='*/4'),
} }
} }
@@ -85,12 +82,13 @@ ugettext = lambda s: s
LANGUAGES = ( LANGUAGES = (
('en', ugettext('English')), ('en', ugettext('English')),
('de', ugettext('German')), ('de', ugettext('German')),
('es', ugettext('Spanish')),
) )
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [], 'DIRS': [os.path.join(PROJECT_DIR, 'templates')],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
@@ -148,8 +146,11 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/ # https://docs.djangoproject.com/en/1.10/howto/static-files/
STATIC_URL = '/static/' STATIC_URL = '/static/'
STATICFILES_DIRS = [
os.path.join(PROJECT_DIR, 'static'),
]
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
# Bootstrap messaging css workaround # Bootstrap messaging css workaround
MESSAGE_TAGS = { MESSAGE_TAGS = {
@@ -166,7 +167,6 @@ CACHES = {
} }
} }
SECRET_KEY = 'this is a very bad secret key you should change'
DEBUG = True DEBUG = True
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = ['*']
DATABASES = { DATABASES = {
@@ -178,40 +178,24 @@ DATABASES = {
SITE_NAME = 'Alliance Auth' SITE_NAME = 'Alliance Auth'
################# LOGIN_URL = 'auth_login_user' # view that handles login logic
# Login Settings
################# LOGIN_REDIRECT_URL = 'authentication:dashboard' # default destination when logging in if no redirect specified
# LOGIN_REDIRECT_URL - default destination when logging in if no redirect specified LOGOUT_REDIRECT_URL = 'authentication:dashboard' # destination after logging out
# LOGOUT_REDIRECT_URL - destination after logging out
# Both of these redirects accept values as per the django redirect shortcut # Both of these redirects accept values as per the django redirect shortcut
# https://docs.djangoproject.com/en/1.11/topics/http/shortcuts/#redirect # https://docs.djangoproject.com/en/1.11/topics/http/shortcuts/#redirect
# - url names eg 'authentication:dashboard' # - url names eg 'authentication:dashboard'
# - relative urls eg '/dashboard' # - relative urls eg '/dashboard'
# - absolute urls eg 'http://example.com/dashboard' # - absolute urls eg 'http://example.com/dashboard'
# LOGIN_TOKEN_SCOPES - scopes required on new tokens when logging in. Cannot be blank.
# ACCOUNT_ACTIVATION_DAYS - number of days email verification tokens are valid for # scopes required on new tokens when logging in. Cannot be blank.
################## LOGIN_TOKEN_SCOPES = ['publicData']
LOGIN_URL = 'auth_login_user'
LOGIN_REDIRECT_URL = 'authentication:dashboard' # number of days email verification links are valid for
LOGOUT_REDIRECT_URL = 'authentication:dashboard'
LOGIN_TOKEN_SCOPES = ['esi-characters.read_opportunities.v1']
ACCOUNT_ACTIVATION_DAYS = 1 ACCOUNT_ACTIVATION_DAYS = 1
##################################################### ESI_API_URL = 'https://esi.evetech.net/'
##
## Logging Configuration
##
#####################################################
# Set log_file and console level to desired state:
# DEBUG - basically stack trace, explains every step
# INFO - model creation, deletion, updates, etc
# WARN - unexpected function outcomes that do not impact user
# ERROR - unexcpeted function outcomes which prevent user from achieving desired outcome
# EXCEPTION - something critical went wrong, unhandled
#####################################
# Recommended level for log_file is INFO, console is DEBUG
# Change log level of individual apps below to narrow your debugging
#####################################
LOGGING = { LOGGING = {
'version': 1, 'version': 1,
'disable_existing_loggers': False, 'disable_existing_loggers': False,
@@ -253,5 +237,9 @@ LOGGING = {
'handlers': ['log_file', 'console'], 'handlers': ['log_file', 'console'],
'level': 'ERROR', 'level': 'ERROR',
}, },
'esi': {
'handlers': ['log_file', 'console'],
'level': 'DEBUG',
},
} }
} }

View File

@@ -1,68 +1,60 @@
# Every setting in base.py can be overloaded by redefining it here.
from .base import * from .base import *
# These are required for Django to function properly # These are required for Django to function properly. Don't touch.
ROOT_URLCONF = '{{ project_name }}.urls' ROOT_URLCONF = '{{ project_name }}.urls'
WSGI_APPLICATION = '{{ project_name }}.wsgi.application' WSGI_APPLICATION = '{{ project_name }}.wsgi.application'
STATICFILES_DIRS = [
os.path.join(PROJECT_DIR, 'static'),
]
STATIC_ROOT = "/var/www/{{ project_name }}/static/"
TEMPLATES[0]['DIRS'] += [os.path.join(PROJECT_DIR, 'templates')]
SECRET_KEY = '{{ secret_key }}' SECRET_KEY = '{{ secret_key }}'
# Change this to change the name of the auth site # This is where css/images will be placed for your webserver to read
STATIC_ROOT = "/var/www/{{ project_name }}/static/"
# Change this to change the name of the auth site displayed
# in page titles and the site header.
SITE_NAME = '{{ project_name }}' SITE_NAME = '{{ project_name }}'
# Change this to enable/disable debug mode # Change this to enable/disable debug mode, which displays
# useful error messages but can leak sensitive data.
DEBUG = False DEBUG = False
####################################### # Add any additional apps to this list.
# Database Settings # INSTALLED_APPS += [
#######################################
# Uncomment and change the database name ]
# and credentials to use MySQL/MariaDB.
# Leave commented to use sqlite3 # Enter credentials to use MySQL/MariaDB. Comment out to use sqlite3
#######################################
"""
DATABASES['default'] = { DATABASES['default'] = {
'ENGINE': 'django.db.backends.mysql', 'ENGINE': 'django.db.backends.mysql',
'NAME': 'alliance_auth', 'NAME': 'alliance_auth',
'USER': os.environ.get('AA_DB_DEFAULT_USER', ''), 'USER': '',
'PASSWORD': os.environ.get('AA_DB_DEFAULT_PASSWORD', ''), 'PASSWORD': '',
'HOST': os.environ.get('AA_DB_DEFAULT_HOST', '127.0.0.1'), 'HOST': '127.0.0.1',
'PORT': os.environ.get('AA_DB_DEFAULT_PORT', '3306'), 'PORT': '3306',
} }
"""
###################################### # Register an application at https://developers.eveonline.com for Authentication
# SSO Settings # # & API Access and fill out these settings. Be sure to set the callback URL
###################################### # to https://example.com/sso/callback substituting your domain for example.com
# Register an application at # Logging in to auth requires the publicData scope (can be overridden through the
# https://developers.eveonline.com # LOGIN_TOKEN_SCOPES setting). Other apps may require more (see their docs).
# and fill out these settings.
# Be sure to set the callback URL to
# https://example.com/sso/callback
# substituting your domain for example.com
######################################
ESI_SSO_CLIENT_ID = '' ESI_SSO_CLIENT_ID = ''
ESI_SSO_CLIENT_SECRET = '' ESI_SSO_CLIENT_SECRET = ''
ESI_SSO_CALLBACK_URL = '' ESI_SSO_CALLBACK_URL = ''
###################################### # By default emails are validated before new users can log in.
# Email Settings # # It's recommended to use a free service like SparkPost or Elastic Email to send email.
######################################
# Alliance Auth validates emails before
# new users can log in.
# It's recommended to use a free service
# like SparkPost or Mailgun to send email.
# https://www.sparkpost.com/docs/integrations/django/ # https://www.sparkpost.com/docs/integrations/django/
################# # https://elasticemail.com/resources/settings/smtp-api/
# Set the default from email to something like 'noreply@example.com'
# Email validation can be turned off by uncommenting the line below. This can break some services.
# REGISTRATION_VERIFY_EMAIL = False
EMAIL_HOST = '' EMAIL_HOST = ''
EMAIL_PORT = 587 EMAIL_PORT = 587
EMAIL_HOST_USER = '' EMAIL_HOST_USER = ''
EMAIL_HOST_PASSWORD = '' EMAIL_HOST_PASSWORD = ''
EMAIL_USE_TLS = True EMAIL_USE_TLS = True
DEFAULT_FROM_EMAIL = ''
###################################### #######################################
# Add any custom settings below here # # Add any custom settings below here. #
###################################### #######################################

View File

@@ -9,12 +9,19 @@ class NameFormatConfigForm(forms.ModelForm):
super(NameFormatConfigForm, self).__init__(*args, **kwargs) super(NameFormatConfigForm, self).__init__(*args, **kwargs)
SERVICE_CHOICES = [(s.name, s.name) for h in hooks.get_hooks('services_hook') for s in [h()]] SERVICE_CHOICES = [(s.name, s.name) for h in hooks.get_hooks('services_hook') for s in [h()]]
if self.instance.id: if self.instance.id:
SERVICE_CHOICES.append((self.instance.field, self.instance.field)) current_choice = (self.instance.service_name, self.instance.service_name)
if current_choice not in SERVICE_CHOICES:
SERVICE_CHOICES.append(current_choice)
self.fields['service_name'] = forms.ChoiceField(choices=SERVICE_CHOICES) self.fields['service_name'] = forms.ChoiceField(choices=SERVICE_CHOICES)
class NameFormatConfigAdmin(admin.ModelAdmin): class NameFormatConfigAdmin(admin.ModelAdmin):
form = NameFormatConfigForm form = NameFormatConfigForm
list_display = ('service_name', 'get_state_display_string')
def get_state_display_string(self, obj):
return ', '.join([state.name for state in obj.states.all()])
get_state_display_string.short_description = 'States'
admin.site.register(NameFormatConfig, NameFormatConfigAdmin) admin.site.register(NameFormatConfig, NameFormatConfigAdmin)

View File

@@ -6,4 +6,4 @@ class ServicesConfig(AppConfig):
label = 'services' label = 'services'
def ready(self): def ready(self):
pass from . import signals

View File

@@ -1,7 +1,6 @@
from django.conf.urls import include, url from django.conf.urls import include, url
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.core.exceptions import ObjectDoesNotExist
from django.conf import settings from django.conf import settings
from string import Formatter from string import Formatter
@@ -160,15 +159,12 @@ class NameFormatter:
'corp_id': getattr(main_char, 'corporation_id', None), 'corp_id': getattr(main_char, 'corporation_id', None),
'alliance_name': getattr(main_char, 'alliance_name', None), 'alliance_name': getattr(main_char, 'alliance_name', None),
'alliance_id': getattr(main_char, 'alliance_id', None), 'alliance_id': getattr(main_char, 'alliance_id', None),
'alliance_ticker': getattr(main_char, 'alliance_ticker', None),
'username': self.user.username, 'username': self.user.username,
} }
if main_char is not None and 'alliance_ticker' in self.string_formatter: format_data['alliance_or_corp_name'] = format_data['alliance_name'] or format_data['corp_name']
# Reduces db lookups format_data['alliance_or_corp_ticker'] = format_data['alliance_ticker'] or format_data['corp_ticker']
try:
format_data['alliance_ticker'] = getattr(getattr(main_char, 'alliance', None), 'alliance_ticker', None)
except ObjectDoesNotExist:
format_data['alliance_ticker'] = None
return format_data return format_data
@cached_property @cached_property

View File

@@ -9,5 +9,5 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
for u in User.objects.all(): for u in User.objects.all():
validate_services(u) validate_services(u.pk)
self.stdout.write(self.style.SUCCESS('Verified all user service accounts.')) self.stdout.write(self.style.SUCCESS('Verified all user service accounts.'))

View File

@@ -11,6 +11,7 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
('services', '0001_squashed_0003_delete_groupcache'), ('services', '0001_squashed_0003_delete_groupcache'),
('authentication', '0015_user_profiles'),
] ]
operations = [ operations = [

View File

@@ -1,6 +1,4 @@
import requests import requests
import json
import re
import math import math
from django.conf import settings from django.conf import settings
from requests_oauthlib import OAuth2Session from requests_oauthlib import OAuth2Session
@@ -24,8 +22,8 @@ Previously all we asked for was permission to kick members, manage roles, and ma
Users have reported weird unauthorized errors we don't understand. So now we ask for full server admin. Users have reported weird unauthorized errors we don't understand. So now we ask for full server admin.
It's almost fixed the problem. It's almost fixed the problem.
""" """
# kick members, manage roles, manage nicknames # kick members, manage roles, manage nicknames, create instant invite
# BOT_PERMISSIONS = 0x00000002 + 0x10000000 + 0x08000000 # BOT_PERMISSIONS = 0x00000002 + 0x10000000 + 0x08000000 + 0x00000001
BOT_PERMISSIONS = 0x00000008 BOT_PERMISSIONS = 0x00000008
# get user ID, accept invite # get user ID, accept invite
@@ -34,7 +32,7 @@ SCOPES = [
'guilds.join', 'guilds.join',
] ]
GROUP_CACHE_MAX_AGE = int(getattr(settings, 'DISCORD_GROUP_CACHE_MAX_AGE', 2 * 60 * 60)) # 2 hours default GROUP_CACHE_MAX_AGE = getattr(settings, 'DISCORD_GROUP_CACHE_MAX_AGE', 2 * 60 * 60) # 2 hours default
class DiscordApiException(Exception): class DiscordApiException(Exception):
@@ -109,7 +107,7 @@ def api_backoff(func):
backoff_timer = datetime.datetime.strptime(existing_backoff, cache_time_format) backoff_timer = datetime.datetime.strptime(existing_backoff, cache_time_format)
if backoff_timer > datetime.datetime.utcnow(): if backoff_timer > datetime.datetime.utcnow():
backoff_seconds = (backoff_timer - datetime.datetime.utcnow()).total_seconds() backoff_seconds = (backoff_timer - datetime.datetime.utcnow()).total_seconds()
logger.debug("Still under backoff for {} seconds, backing off" % backoff_seconds) logger.debug("Still under backoff for %s seconds, backing off" % backoff_seconds)
# Still under backoff # Still under backoff
raise PerformBackoff( raise PerformBackoff(
retry_after=backoff_seconds, retry_after=backoff_seconds,
@@ -117,8 +115,7 @@ def api_backoff(func):
global_ratelimit=bool(existing_global_backoff) global_ratelimit=bool(existing_global_backoff)
) )
logger.debug("Calling API calling function") logger.debug("Calling API calling function")
func(*args, **kwargs) return func(*args, **kwargs)
break
except requests.HTTPError as e: except requests.HTTPError as e:
if e.response.status_code == 429: if e.response.status_code == 429:
try: try:
@@ -163,12 +160,11 @@ class DiscordOAuthManager:
@staticmethod @staticmethod
def _sanitize_name(name): def _sanitize_name(name):
return re.sub('[^\w.-]', '', name)[:32] return name[:32]
@staticmethod @staticmethod
def _sanitize_groupname(name): def _sanitize_group_name(name):
name = name.strip(' _') return name[:100]
return DiscordOAuthManager._sanitize_name(name)
@staticmethod @staticmethod
def generate_bot_add_url(): def generate_bot_add_url():
@@ -187,23 +183,33 @@ class DiscordOAuthManager:
return token return token
@staticmethod @staticmethod
def add_user(code): def add_user(code, groups, nickname=None):
try: try:
token = DiscordOAuthManager._process_callback_code(code)['access_token'] token = DiscordOAuthManager._process_callback_code(code)['access_token']
logger.debug("Received token from OAuth") logger.debug("Received token from OAuth")
custom_headers = {'accept': 'application/json', 'authorization': 'Bearer ' + token} custom_headers = {'accept': 'application/json', 'authorization': 'Bearer ' + token}
path = DISCORD_URL + "/invites/" + str(settings.DISCORD_INVITE_CODE)
r = requests.post(path, headers=custom_headers)
logger.debug("Got status code %s after accepting Discord invite" % r.status_code)
r.raise_for_status()
path = DISCORD_URL + "/users/@me" path = DISCORD_URL + "/users/@me"
r = requests.get(path, headers=custom_headers) r = requests.get(path, headers=custom_headers)
logger.debug("Got status code %s after retrieving Discord profile" % r.status_code) logger.debug("Got status code %s after retrieving Discord profile" % r.status_code)
r.raise_for_status() r.raise_for_status()
user_id = r.json()['id'] user_id = r.json()['id']
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id)
group_ids = [DiscordOAuthManager._group_name_to_id(DiscordOAuthManager._sanitize_group_name(g)) for g in
groups]
data = {
'roles': group_ids,
'access_token': token,
}
if nickname:
data['nick'] = DiscordOAuthManager._sanitize_name(nickname)
custom_headers['authorization'] = 'Bot ' + settings.DISCORD_BOT_TOKEN
r = requests.put(path, headers=custom_headers, json=data)
logger.debug("Got status code %s after joining Discord server" % r.status_code)
r.raise_for_status()
logger.info("Added Discord user ID %s to server." % user_id) logger.info("Added Discord user ID %s to server." % user_id)
return user_id return user_id
except: except:
@@ -211,23 +217,20 @@ class DiscordOAuthManager:
return None return None
@staticmethod @staticmethod
@api_backoff
def update_nickname(user_id, nickname): def update_nickname(user_id, nickname):
try: nickname = DiscordOAuthManager._sanitize_name(nickname)
nickname = DiscordOAuthManager._sanitize_name(nickname) custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN} data = {'nick': nickname}
data = {'nick': nickname} path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id)
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id) r = requests.patch(path, headers=custom_headers, json=data)
r = requests.patch(path, headers=custom_headers, json=data) logger.debug("Got status code %s after setting nickname for Discord user ID %s (%s)" % (
logger.debug("Got status code %s after setting nickname for Discord user ID %s (%s)" % ( r.status_code, user_id, nickname))
r.status_code, user_id, nickname)) if r.status_code == 404:
if r.status_code == 404: logger.warn("Discord user ID %s could not be found in server." % user_id)
logger.warn("Discord user ID %s could not be found in server." % user_id)
return True
r.raise_for_status()
return True return True
except: r.raise_for_status()
logger.exception("Failed to set nickname for Discord user ID %s (%s)" % (user_id, nickname)) return True
return False
@staticmethod @staticmethod
def delete_user(user_id): def delete_user(user_id):
@@ -260,7 +263,7 @@ class DiscordOAuthManager:
@staticmethod @staticmethod
def _group_name_to_id(name): def _group_name_to_id(name):
name = DiscordOAuthManager._sanitize_groupname(name) name = DiscordOAuthManager._sanitize_group_name(name)
def get_or_make_role(): def get_or_make_role():
groups = DiscordOAuthManager._get_groups() groups = DiscordOAuthManager._get_groups()
@@ -271,42 +274,61 @@ class DiscordOAuthManager:
return cache.get_or_set(DiscordOAuthManager._generate_cache_role_key(name), get_or_make_role, GROUP_CACHE_MAX_AGE) return cache.get_or_set(DiscordOAuthManager._generate_cache_role_key(name), get_or_make_role, GROUP_CACHE_MAX_AGE)
@staticmethod @staticmethod
def __generate_role(): def __generate_role(name, **kwargs):
custom_headers = {'accept': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN} custom_headers = {'accept': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/roles" path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/roles"
r = requests.post(path, headers=custom_headers) data = {'name': name}
data.update(kwargs)
r = requests.post(path, headers=custom_headers, json=data)
logger.debug("Received status code %s after generating new role." % r.status_code) logger.debug("Received status code %s after generating new role." % r.status_code)
r.raise_for_status() r.raise_for_status()
return r.json() return r.json()
@staticmethod @staticmethod
def __edit_role(role_id, name, color=0, hoist=True, permissions=36785152): def __edit_role(role_id, **kwargs):
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN} custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
data = {
'color': color,
'hoist': hoist,
'name': name,
'permissions': permissions,
}
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/roles/" + str(role_id) path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/roles/" + str(role_id)
r = requests.patch(path, headers=custom_headers, data=json.dumps(data)) r = requests.patch(path, headers=custom_headers, json=kwargs)
logger.debug("Received status code %s after editing role id %s" % (r.status_code, role_id)) logger.debug("Received status code %s after editing role id %s" % (r.status_code, role_id))
r.raise_for_status() r.raise_for_status()
return r.json() return r.json()
@staticmethod @staticmethod
def _create_group(name): def _create_group(name):
role = DiscordOAuthManager.__generate_role() return DiscordOAuthManager.__generate_role(name)
return DiscordOAuthManager.__edit_role(role['id'], name)
@staticmethod
def _get_user(user_id):
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id)
r = requests.get(path, headers=custom_headers)
r.raise_for_status()
return r.json()
@staticmethod
def _get_user_roles(user_id):
user = DiscordOAuthManager._get_user(user_id)
return user['roles']
@staticmethod
def _modify_user_role(user_id, role_id, method):
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id) + "/roles/" + str(
role_id)
r = getattr(requests, method)(path, headers=custom_headers)
r.raise_for_status()
logger.debug("%s role %s for user %s" % (method, role_id, user_id))
@staticmethod @staticmethod
@api_backoff @api_backoff
def update_groups(user_id, groups): def update_groups(user_id, groups):
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN} group_ids = [DiscordOAuthManager._group_name_to_id(DiscordOAuthManager._sanitize_group_name(g)) for g in groups]
group_ids = [DiscordOAuthManager._group_name_to_id(DiscordOAuthManager._sanitize_groupname(g)) for g in groups] user_group_ids = DiscordOAuthManager._get_user_roles(user_id)
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id) for g in group_ids:
data = {'roles': group_ids} if g not in user_group_ids:
r = requests.patch(path, headers=custom_headers, json=data) DiscordOAuthManager._modify_user_role(user_id, g, 'put')
logger.debug("Received status code %s after setting user roles" % r.status_code) time.sleep(1) # we're gonna be hammering the API here
r.raise_for_status() for g in user_group_ids:
if g not in group_ids:
DiscordOAuthManager._modify_user_role(user_id, g, 'delete')
time.sleep(1)

View File

@@ -57,5 +57,5 @@ class Migration(migrations.Migration):
name='discorduser', name='discorduser',
options={'permissions': (('access_discord', 'Can access the Discord service'),)}, options={'permissions': (('access_discord', 'Can access the Discord service'),)},
), ),
migrations.RunPython(migrate_service_enabled), migrations.RunPython(migrate_service_enabled, migrations.RunPython.noop),
] ]

View File

@@ -5,10 +5,11 @@ from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from allianceauth.notifications import notify from allianceauth.notifications import notify
from celery import shared_task from celery import shared_task
from requests.exceptions import HTTPError
from allianceauth.services.hooks import NameFormatter from allianceauth.services.hooks import NameFormatter
from .manager import DiscordOAuthManager, DiscordApiBackoff from .manager import DiscordOAuthManager, DiscordApiBackoff
from .models import DiscordUser from .models import DiscordUser
from allianceauth.services.tasks import QueueOnce
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -19,15 +20,16 @@ class DiscordTasks:
@classmethod @classmethod
def add_user(cls, user, code): def add_user(cls, user, code):
user_id = DiscordOAuthManager.add_user(code) groups = DiscordTasks.get_groups(user)
nickname = None
if settings.DISCORD_SYNC_NAMES:
nickname = DiscordTasks.get_nickname(user)
user_id = DiscordOAuthManager.add_user(code, groups, nickname=nickname)
if user_id: if user_id:
discord_user = DiscordUser() discord_user = DiscordUser()
discord_user.user = user discord_user.user = user
discord_user.uid = user_id discord_user.uid = user_id
discord_user.save() discord_user.save()
if settings.DISCORD_SYNC_NAMES:
cls.update_nickname.delay(user.pk)
cls.update_groups.delay(user.pk)
return True return True
return False return False
@@ -57,28 +59,32 @@ class DiscordTasks:
return True return True
@staticmethod @staticmethod
@shared_task(bind=True, name='discord.update_groups') @shared_task(bind=True, name='discord.update_groups', base=QueueOnce)
def update_groups(task_self, pk): def update_groups(self, pk):
user = User.objects.get(pk=pk) user = User.objects.get(pk=pk)
logger.debug("Updating discord groups for user %s" % user) logger.debug("Updating discord groups for user %s" % user)
if DiscordTasks.has_account(user): if DiscordTasks.has_account(user):
groups = [] groups = DiscordTasks.get_groups(user)
for group in user.groups.all():
groups.append(str(group.name))
if len(groups) == 0:
logger.debug("No syncgroups found for user. Adding empty group.")
groups.append('empty')
logger.debug("Updating user %s discord groups to %s" % (user, groups)) logger.debug("Updating user %s discord groups to %s" % (user, groups))
try: try:
DiscordOAuthManager.update_groups(user.discord.uid, groups) DiscordOAuthManager.update_groups(user.discord.uid, groups)
except DiscordApiBackoff as bo: except DiscordApiBackoff as bo:
logger.info("Discord group sync API back off for %s, " logger.info("Discord group sync API back off for %s, "
"retrying in %s seconds" % (user, bo.retry_after_seconds)) "retrying in %s seconds" % (user, bo.retry_after_seconds))
raise task_self.retry(countdown=bo.retry_after_seconds) raise self.retry(countdown=bo.retry_after_seconds)
except HTTPError as e:
if e.response.status_code == 404:
try:
if e.response.json()['code'] == 10007:
# user has left the server
DiscordTasks.delete_user(user)
return
finally:
raise e
except Exception as e: except Exception as e:
if task_self: if self:
logger.exception("Discord group sync failed for %s, retrying in 10 mins" % user) logger.exception("Discord group sync failed for %s, retrying in 10 mins" % user)
raise task_self.retry(countdown=60 * 10) raise self.retry(countdown=60 * 10)
else: else:
# Rethrow # Rethrow
raise e raise e
@@ -94,7 +100,7 @@ class DiscordTasks:
DiscordTasks.update_groups.delay(discord_user.user.pk) DiscordTasks.update_groups.delay(discord_user.user.pk)
@staticmethod @staticmethod
@shared_task(bind=True, name='discord.update_nickname') @shared_task(bind=True, name='discord.update_nickname', base=QueueOnce)
def update_nickname(self, pk): def update_nickname(self, pk):
user = User.objects.get(pk=pk) user = User.objects.get(pk=pk)
logger.debug("Updating discord nickname for user %s" % user) logger.debug("Updating discord nickname for user %s" % user)
@@ -104,6 +110,10 @@ class DiscordTasks:
logger.debug("Updating user %s discord nickname to %s" % (user, character.character_name)) logger.debug("Updating user %s discord nickname to %s" % (user, character.character_name))
try: try:
DiscordOAuthManager.update_nickname(user.discord.uid, DiscordTasks.get_nickname(user)) DiscordOAuthManager.update_nickname(user.discord.uid, DiscordTasks.get_nickname(user))
except DiscordApiBackoff as bo:
logger.info("Discord nickname update API back off for %s, "
"retrying in %s seconds" % (user, bo.retry_after_seconds))
raise self.retry(countdown=bo.retry_after_seconds)
except Exception as e: except Exception as e:
if self: if self:
logger.exception("Discord nickname sync failed for %s, retrying in 10 mins" % user) logger.exception("Discord nickname sync failed for %s, retrying in 10 mins" % user)
@@ -132,3 +142,7 @@ class DiscordTasks:
def get_nickname(user): def get_nickname(user):
from .auth_hooks import DiscordService from .auth_hooks import DiscordService
return NameFormatter(DiscordService(), user).format_name() return NameFormatter(DiscordService(), user).format_name()
@staticmethod
def get_groups(user):
return [g.name for g in user.groups.all()] + [user.profile.state.name]

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