Compare commits

...

234 Commits

Author SHA1 Message Date
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
ghoti
2c98cbd020 Sort completed HR apps by create date (most recent first) (#930) 2017-12-20 18:03:27 -05:00
Adarnof
1fe9d18a1a Include confusable_homoglyphs data files.
django-registration currently requires confusable_homoglyphs~=2.0 which has problems retrieving these data files. Version 2.3.1 is expected to update the dependency to 3.0 which includes these with the package.
2017-12-15 12:43:31 -05:00
Adarnof
02a75a931a Correct corp_id argument 2017-12-15 11:28:54 -05:00
Adarnof
af7a432f29 Include basic app installation instructions. 2017-12-14 18:01:47 -05:00
Adarnof
770aca923f Reorder migrations to allow updating from v1.15.6 2017-12-14 17:48:54 -05:00
Adarnof
d50a74c7c6 Correct project template celery usage. 2017-12-14 10:20:34 -05:00
Adarnof
fcd8554ea7 Define additional arguments for startproject
Allows project creation with Django 2.0
2017-12-14 00:24:22 -05:00
Adarnof
98da0723fc Correct install procedure for gunicorn.
Correct docs index links.
2017-12-14 00:24:10 -05:00
Basraah
f037d7fea6 Change celery tasks to shared task decorator 2017-12-11 00:26:07 +10:00
Basraah
676e68a2bb Update travis config 2017-12-04 13:30:23 +10:00
Basraah
c82d9c7722 Fix migration issue 2017-12-04 13:21:29 +10:00
Basraah
70c4b17518 [v2] Alliance and Corp Autogroups (#902)
* Add pseudo foreign keys to EveCharacter model

* Ensure populate alliance is called on create

* Add unit tests for model

* Add extra signal for state removal/addition

* Add unit tests for signals

* Add tests for manager

* Add migrations

* Add sync command to admin control

* Prevent whitespace being stripped from group names

* Add documentation
2017-12-04 12:52:05 +10:00
Adarnof
995a44de0a Case-insensitive group name to ID translation
Seems Discourse won't let you create `Group` if `group` already exists (`422 Name has already been taken`).

Thanks @huberfe
2017-11-17 18:13:18 -05:00
Adarnof
4f5fc18c66 Merge branch 'v2-dev-master-merge' of https://github.com/allianceauth/allianceauth into v2-dev 2017-11-17 18:00:30 -05:00
Adarnof
4a94f379b4 Correct issues with the SRP merge from v1 2017-11-17 17:29:01 -05:00
mmolitor87
fb4651b11f Change name of location of gunicorn log. (#920) 2017-11-15 13:51:47 -05:00
phaynu
f961db3130 Extending Choices for Questions in hrapplications to Allow Multiselect (#911)
An additional field at the question level defines whether the choices for the question are multi-select or not. The template will render the choices with radio buttons or checkboxes depending on multi-select. Multiple selected choices are saved with a line break between them.
2017-11-10 12:07:17 -05:00
Adarnof
c63464c4c9 Handle new zKillboard API format 2017-11-10 11:57:51 -05:00
Adarnof
cca8b26375 Handle FAT ZeroDivisionErrors 2017-11-10 11:52:22 -05:00
Adarnof
cd0afeca15 Disable SeAT accounts instead of deleting. (#915)
See eveseat/web@1abb402
2017-11-03 19:29:39 -04:00
Basraah
86362bb0dd Refactor mumble service (#914)
* Added in_organisation check to EveCharacter model

* Basic name formatter

* Switch mumble service to use name formatter

* Squash services migrations

* Add name to example service to allow it to be used in tests

* Add name formatter to services

* Add abstract views, model, form for services modules

* Refactor mumble service to new style

* Don't set credentials if setting a provided password

* Add success message to set password view
2017-11-03 16:52:45 -04:00
Adarnof
c4979a22dd Merge pull request #913 from Adarnof/sso_registration
Determine paths to commands using shutil
2017-10-25 19:20:36 -04:00
Adarnof
859c5b3fa1 Determine paths to commands using shutil
Change context names to avoid colliding with base command pythonpath option
2017-10-24 12:48:23 -04:00
Basraah
57a26b90cb Admin status panel for dashboard (#903) 2017-10-18 15:46:58 +10:00
Basraah
7fa45fa471 Add favicons and logo 2017-10-12 13:18:51 +10:00
Basraah
47b5b286d8 Uniform page titles 2017-10-12 10:20:40 +10:00
Basraah
ef37cb3ea5 Name generator/formatter (#897)
* Squash services migrations

* Add name to example service to allow it to be used in tests

* Add name formatter to services

* Add documentation
2017-10-11 12:34:31 +10:00
Adarnof
b95bb9aa6a Merge pull request #891 from Adarnof/sso_registration
Install using project template
2017-10-10 17:09:08 -04:00
Adarnof
cfad4fa8a6 Pycharm refactor failed me. 2017-10-10 10:41:25 -04:00
Adarnof
3f33485ca9 Template supervisor conf.
Update docs to reflect simple installation.

Ensure allianceauth celery workers can find config file for development (doesn't work as celeryapp.py).
2017-10-10 00:29:57 -04:00
Adarnof
1c1dfde2c4 Mention database migrations in update docs. 2017-10-05 15:06:34 -04:00
Adarnof
86e92941df Move default logging directory. 2017-10-05 14:35:25 -04:00
Adarnof
09b788fef6 Remove legacy settings documentation.
Settings are self-documented in the new install template.

Services install docs should be updated to indicate settings need to be added and how to configure them.
2017-10-05 12:50:51 -04:00
Adarnof
d54d80b0b8 Include updating instructions.
Tweak wording of install sections.
2017-10-05 12:48:33 -04:00
Adarnof
3ff29ba3b0 Do not check coverage of bin commands. 2017-10-05 11:08:43 -04:00
Adarnof
2efb45943c Do not check coverage of project template. 2017-10-05 11:00:32 -04:00
Adarnof
d43b8cf0e5 Update install docs to reflect new magical commands. 2017-10-05 03:10:27 -04:00
Basraah
5e8b5e1a15 Add files required for confusable_homoglyphs
Recreating them in CI was causing intermittent build stalls that were
almost impossible to debug.
2017-10-05 15:54:49 +10:00
Adarnof
03447abf5c Update base settings with command. 2017-10-05 01:23:33 -04:00
Adarnof
c22d3a9967 Command line utility to create project.
Shamelessly stolen from wagtail.
2017-10-05 00:52:55 -04:00
Adarnof
5df3672f3b Merge branch 'v2-dev' of https://github.com/allianceauth/allianceauth into sso_registration 2017-10-05 00:36:37 -04:00
Adarnof
c68574efc3 Create project template for deployment. 2017-10-05 00:33:18 -04:00
Basraah
e5d76cbce8 Add night mode (#890)
* Split base template menus out

* Add toggle for night mode setting

* Add vertical flexbox row

Makes each col-* expand to fill the space created by the largest column.

* Use flexbox on main character and group panels

* Fix data table sort control clipping

* Build new css
2017-10-05 14:32:58 +10:00
Adarnof
f121ed4062 Merge branch 'v2-dev' of https://github.com/allianceauth/allianceauth into sso_registration 2017-10-04 23:43:57 -04:00
Adarnof
111105d48b Restructure settings (#882)
* Refactor settings into importable base.

User deployment should use 'from allianceauth.settings.base import *' in their project settings.

* Restructure tests folder.
Remove DOMAIN setting.
Use localhost for services revoked emails.

* Add missing python3.4 typing requirement
typing is included python3.5+

* Remove legacy setup comments
Remove legacy update script
2017-10-05 13:12:49 +10:00
Basraah
d7cb1a2fab Use django-webtest for hanging tests instead of TestCase 2017-10-02 00:06:06 +10:00
Basraah
01d34b54eb N+1 query fixes 2017-10-01 20:55:15 +10:00
Basraah
f33f796421 Remove obsolete template tag 2017-10-01 19:23:43 +10:00
Basraah
0504be2441 Fix redirect URLs 2017-10-01 19:23:25 +10:00
Adarnof
93eca76bf8 Remove legacy setup comments
Remove legacy update script
2017-09-28 23:05:43 -04:00
Adarnof
b3d02b0c37 Add missing python3.4 typing requirement
typing is included python3.5+
2017-09-28 22:33:17 -04:00
Adarnof
67ff9eb379 Merge branch 'v2-dev' of https://github.com/allianceauth/allianceauth into sso_registration
Conflicts:
	alliance_auth/settings.py.example
	test_allianceauth/settings.py
2017-09-28 20:40:02 -04:00
Adarnof
cd6963daa6 Restructure tests folder.
Remove DOMAIN setting.
Use localhost for services revoked emails.
2017-09-28 20:37:33 -04:00
Adarnof
44de49cbb0 Refactor settings into importable base.
User deployment should use 'from allianceauth.settings.base import *' in their project settings.
2017-09-28 17:28:02 -04:00
Basraah
8864afd784 Resolve Dj20 compatibility issues
Temporarily use latest snapshot for esi for dj20

    Correct admin urls include

    Manually correct old ts3 table migration

    Remove obsolete services migrations

    Remove bootstrap pagination package

    Fix url namespacing
2017-09-28 14:47:06 +10:00
Basraah
51e4db73f0 Add latest django-celery-beat snapshot for Dj2.0 testing 2017-09-28 12:33:46 +10:00
Basraah
11fca74fec Add required on_delete parameters for Dj2.0 compatibility 2017-09-28 12:32:33 +10:00
Basraah
fff2cd32d5 Update timerboard views
Adds basic unit tests
Changed to class based views
2017-09-27 19:12:07 +10:00
Basraah
d993a299ef Add pseudo foreign keys to EveCharacter model 2017-09-26 15:13:41 +10:00
Basraah
650408f61c Unit test tweaks 2017-09-26 15:13:37 +10:00
Basraah
ef26cdbbee Improve support for milliseconds backoff 2017-09-26 09:14:58 +10:00
Adarnof
168ab569b9 Increase tested retry after
Apparently tests take longer than 200ms to evaluate here.
2017-09-26 09:14:51 +10:00
Adarnof
b418abc7c8 Retry after in milliseconds
Closes #874
2017-09-26 09:14:32 +10:00
Basraah
280ddb336e Change to use tox to manage unit tests 2017-09-25 17:25:28 +10:00
Basraah
d93f36a180 Ensure populate alliance is called on create 2017-09-25 10:15:13 +10:00
Basraah
941bcd3cd1 Fix allow failures format 2017-09-23 18:47:06 +10:00
Basraah
7591db3168 Add experimental support for Dj2.0 2017-09-23 18:40:25 +10:00
Basraah
e68c840dad CSS Fixes and cleanup
Improves bootstrap compatibility, allowing for dropping in theme CSS.
Also improves responsive design but still has a ways to go.
2017-09-23 18:03:43 +10:00
Basraah
45f7b7d962 Fix broken query 2017-09-23 17:50:53 +10:00
Adarnof
1bf8b6079d Load receivers on ready 2017-09-22 22:49:14 -04:00
Basraah
c59565c038 Removed unused context processor vars 2017-09-23 11:33:06 +10:00
Basraah
53d7916772 Move context processor 2017-09-23 11:32:32 +10:00
Basraah
7beec38881 Move templates and fix namespacing
Move base template

    Refactor services urls and templates

    Refactor groupmanagement urls and templates

    Refactor notifications urls and templates
2017-09-23 06:48:51 +10:00
Basraah
b130cc6c8e Update install docs 2017-09-22 09:57:58 +10:00
Basraah
f8488208fb Documentation updates 2017-09-21 18:07:01 +10:00
Basraah
2efaf40370 Update readme 2017-09-21 16:21:25 +10:00
Basraah
75b99148c6 Remove vagrant config
It's very badly out of date making it very evident it' no longer being
used.
2017-09-21 16:16:39 +10:00
Basraah
f36b038010 Remove EveManager, refactor into model managers
Lots of unused methods removed.
Unit tests added for those that are left.
2017-09-21 14:56:40 +10:00
Basraah
f84de28338 Remove seat API sync 2017-09-21 14:56:21 +10:00
Adarnof
2d92cd6cb2 Minimize swagger spec files. 2017-09-20 01:29:16 -04:00
Adarnof
1a8d163d45 Remove XML provider.
Remove caching provider - ESI calls are cached according to expiry headers now.
Remove evelink as a requirement.
Initialize API response objects using kwargs.

I won't miss XML.
2017-09-19 20:55:26 -04:00
Basraah
954f36dae6 Remove services migrations 2017-09-20 08:11:51 +10:00
Basraah
76ab835347 Add base app, remove explicit template loading 2017-09-20 07:42:47 +10:00
Adarnof
5f4e873f4a Merge pull request #870 from ghoti/v2-dev
settings.py.example fixes
2017-09-19 10:29:39 -04:00
Mike
889fcfd0e0 settings.py.example fixes 2017-09-19 05:00:05 -04:00
Basraah
0326836544 Migrate mumble authenticator to its own repo (#869) 2017-09-19 12:27:14 +10:00
Basraah
18f64b0357 Remove IPBoard3 support (#868) 2017-09-19 11:35:35 +10:00
Basraah
786859294d Restructure Alliance Auth package (#867)
* Refactor allianceauth into its own package

* Add setup

* Add missing default_app_config declarations

* Fix timerboard namespacing

* Remove obsolete future imports

* Remove py2 mock support

* Remove six

* Add experimental 3.7 support and multiple Dj versions

* Remove python_2_unicode_compatible

* Add navhelper as local package

* Update requirements
2017-09-19 09:46:40 +10:00
Basraah
d10580b56b Remove py2 CI 2017-09-18 09:08:54 +10:00
Adarnof
2b51a98d27 Return missing requirement 2017-09-17 19:04:24 -04:00
Adarnof
96c4309230 Add unit tests to CorpUtils 2017-09-17 18:57:08 -04:00
Adarnof
5ce5f37867 Periodic checks of character ownership hashes.
Fix migration naming from merge.
2017-09-17 02:31:46 -04:00
Adarnof
02f2968ee5 Merge branch 'master' of https://github.com/Adarnof/allianceauth into sso_registration
# Conflicts:
#	alliance_auth/__init__.py
#	corputils/models.py
#	corputils/views.py
#	eveonline/tasks.py
#	fleetactivitytracking/views.py
#	hrapplications/admin.py
#	requirements.txt
2017-09-17 01:36:05 -04:00
Adarnof
c1547cab8b Fix that state deletion bug. Thanks @basraah
Also sets state to guest on inactivation, and reassesses on reactivation. With a unit test to boot!
2017-09-06 23:00:12 -04:00
Adarnof
5f20efb0c7 Correct permissions_tool menu item display unit test 2017-08-11 19:49:53 -04:00
Adarnof
3c90868338 Correct available states query for character models missing field data
Ensure state_changed signals are sent
Speed up authentication unit tests
Add authentication unit test for state public availability change
2017-08-11 17:34:43 -04:00
Adarnof
84b1ddc5d9 Enhance admin site.
Regroup auth models.
2017-06-30 17:57:29 -04:00
Adarnof
a235254e48 A whole bunch of unit tests. 2017-06-15 23:01:15 -04:00
Adarnof
40aac6c2dd Fix discord tests 2017-06-14 21:52:32 -04:00
Adarnof
70c496e9fe Remove pycharm .idea folder 2017-06-14 21:03:36 -04:00
Adarnof
4bc91f2381 Fix unit tests.
Discord still failing.
2017-06-14 16:12:56 -04:00
Adarnof
dd3a3e1081 Fix service migration 2017-06-14 00:32:39 -04:00
Adarnof
23bfc3d34a Correct main character reset on token invalidation.
Correct interpretation of missing SERVICES_MIGRATED setting.
Remove legacy api sync celerybeat scheduled task.
2017-06-13 20:34:39 -04:00
Adarnof
7ab88dd663 Alter task to update all eveonline models 2017-06-12 22:20:42 -04:00
Adarnof
ebb44773c2 New view_state_corpstats permission
Cleanup CorpStats permissions
2017-06-12 22:14:35 -04:00
Adarnof
122ab148a4 Move DataTables source to cdnjs 2017-06-08 21:31:19 -04:00
Adarnof
0deb60ac2c Update CorpStats documentation.
Internalize all doc images.
2017-06-08 20:48:44 -04:00
Adarnof
fd8ab2688e Implement DataTables in CorpStats
Site header is broken on pages with DataTables.
2017-06-08 17:28:05 -04:00
Adarnof
b7062c8dda Remove legacy HRApplication models.
Closes #655
2017-06-07 23:12:07 -04:00
Adarnof
97fe2ddfd0 Move templates and urls to apps.
Implement url hooks.
Many apps are now removable.
Default to assuming services have been migrated.
2017-06-07 22:49:46 -04:00
Adarnof
9cc9a36766 Merge branch 'master' of https://github.com/Adarnof/allianceauth into custom_user 2017-06-06 21:48:02 -04:00
Adarnof
d51848aa50 Minor corrections from merge. 2017-06-06 21:47:51 -04:00
Adarnof
00cc89d71c Merge branch 'master' of https://github.com/Adarnof/allianceauth into custom_user
# Conflicts:
#	alliance_auth/settings.py.example
#	eveonline/views.py

Fix some tests.
2017-05-27 18:34:59 -04:00
Adarnof
971ce294ad Merge branch 'master' of https://github.com/Adarnof/allianceauth into custom_user
# Conflicts:
#	alliance_auth/settings.py.example
#	eveonline/views.py

Fix some tests.
2017-05-27 17:25:15 -04:00
Adarnof
ccc7412947 Update test settings 2017-05-27 17:02:59 -04:00
Adarnof
3e4fc7ceb4 Merge branch 'master' of https://github.com/Adarnof/allianceauth into custom_user
# Conflicts:
#	alliance_auth/settings.py.example
#	eveonline/views.py
2017-04-07 00:26:59 -04:00
Adarnof
87973951b8 Correct translation issues.
Correct py3 str model methods.
2017-04-07 00:23:25 -04:00
Adarnof
96b04a269d Merge master.
Fix FAT statistics generation.
2017-04-07 00:21:14 -04:00
Adarnof
f0f1b21226 Begin fixing tests.
Use custom django-navhelper
2017-04-05 00:10:05 -04:00
Adarnof
26405985a2 Change background image.
Remove legacy index.html link images.
2017-03-27 16:17:35 -04:00
Adarnof
3063355eb7 Correct display of non-default CorpStats
Correct display of application comments
Beautify notifications panel
2017-03-27 12:48:24 -04:00
Adarnof
04053c8465 Tweak appearance of CorpStats
Start fixing unit tests
2017-03-27 09:37:20 -04:00
Adarnof
6f36a26694 Allow ordering of ApplicationForm questions
Closes #775
2017-03-26 23:31:56 -04:00
Adarnof
aaf196b477 Apply username sanitizing upon creation
Prevent purging of character ownerships when logging in
Listen to state permission changes for service access verification
2017-03-26 17:45:32 -04:00
Adarnof
06f78a7518 Corpstats views for mains and unregistered
Remove json blob from corpstats model and replace with discrete member models
2017-03-26 17:37:00 -04:00
Adarnof
c6699686ad Prevent altering user states on admin site 2017-03-25 20:19:44 -04:00
Adarnof
64e7c6093e Remove references to legacy is_blue attributes
Provide simple EVE model creation forms with ID, populate the rest from providers
Correct handling of non-main characters in signals
2017-03-25 17:59:38 -04:00
Adarnof
54262a850d Include states in permission auditing 2017-03-25 16:48:21 -04:00
Adarnof
13b0dbc960 Restructure settings.py.example
Add help text to State model
Remove navbar group headings
Fix registration email pluralization
Group memberships on state admin page
Attempt to prevent resetting of state if set on profile admin manually
Embed readthedocs on help page
Rename CorpStats API Index to Registration Index
Default corputils view to main character's corp if available
Correct Application characters listing
Correct string coercion of optimers
Improve readability of SRP values with intcomma
Beautify tables by embeding in panels
Replace slugify with py3-friendly python-slugify
2017-03-25 16:28:26 -04:00
Adarnof
963cecb365 Fix registration.
Fix state assignment.
Fix character ownership transfer.
Disable non-staff passwords.
Fix dashboard groups panel placement.
Fix corpstats viewmodel retrieval.
2017-03-24 19:50:50 -04:00
Adarnof
ab10f062f7 Correct migration syntax. 2017-03-24 10:03:56 -04:00
Adarnof
e15d79b834 Bulk of the profile work is done. 2017-03-23 22:54:25 -04:00
Adarnof
bb87fdd958 temp commit 2017-02-27 18:34:16 -05:00
769 changed files with 13055 additions and 15308 deletions

View File

@@ -1,24 +1,13 @@
[run]
branch = True
source =
alliance_auth
authentication
corputils
eveonline
fleetactivitytracking
fleetup
groupmanagement
hrapplications
notifications
optimer
permissions_tool
services
srp
timerboard
allianceauth
omit =
*/migrations/*
*/example/*
*/project_template/*
*/bin/*
[report]
exclude_lines =

1
.gitattributes vendored
View File

@@ -1 +0,0 @@
*/*.py.example linguist-language=Python

7
.gitignore vendored
View File

@@ -53,17 +53,12 @@ docs/_build/
# PyBuilder
target/
.vagrant/
alliance_auth/settings.py
*Thumbs.db
nginx_config.txt
# custom staticfiles
static/*
#celerybeat
*.pid
celerybeat-schedule
#pycharm
.idea/*
.idea/*

View File

@@ -1,14 +1,25 @@
language: python
python:
- "2.7"
- "3.5"
# command to install dependencies
install:
- pip install requests
- pip install -r requirements.txt
- pip install -r testing-requirements.txt
# command to run tests
script: coverage run runtests.py
sudo: false
cache: pip
dist: trusty
install:
- pip install coveralls>=1.1 tox
# command to run tests
script:
- tox
after_success:
coveralls
matrix:
include:
- env: TOXENV=py35-dj111
python: '3.5'
- env: TOXENV=py36-dj111
python: '3.6'
- env: TOXENV=py35-dj20
python: '3.5'
- env: TOXENV=py36-dj20
python: '3.6'
- env: TOXENV=py37-dj20
python: '3.7-dev'
allow_failures:
- env: TOXENV=py37-dj20

7
MANIFEST.in Normal file
View File

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

View File

@@ -7,49 +7,27 @@ Alliance Auth
[![Coverage Status](https://coveralls.io/repos/github/allianceauth/allianceauth/badge.svg?branch=master)](https://coveralls.io/github/allianceauth/allianceauth?branch=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)
Special Permissions In Admin:
[Get help on gitter](https://gitter.im/R4stl1n/allianceauth) or submit an Issue.
auth | user | group_management ( Access to add members to groups within the alliance )
auth | user | jabber_broadcast ( Access to broadcast a message over jabber to own groups )
auth | user | jabber_broadcast_all ( Can choose from all groups and the 'all' option when broadcasting )
auth | user | corp_apis ( View APIs, and jackKnife, of all members in user's corp. )
auth | user | alliance_apis ( View APIs, and jackKnife, of all member in user's alliance member corps. )
auth | user | timer_management ( Access to create and remove timers )
auth | user | timer_view ( Access to timerboard to view timers )
auth | user | srp_management ( Allows for an individual to create and remove srp fleets and fleet data )
auth | user | sigtracker_management ( Allows for an individual to create and remove signitures )
auth | user | sigtracker_view ( Allows for an individual view signitures )
auth | user | optimer_management ( Allows for an individual to create and remove fleet operations )
auth | user | optimer_view ( Allows for an individual view fleet operations )
auth | user | logging_notifications ( Generate notifications from logging )
auth | user | human_resources ( View applications to user's corp )
hrapplications | application | delete_application ( Can delete applications )
hrapplications | application | accept_application ( Can accept applications )
hrapplications | application | reject_application ( Can reject applications )
hrapplications | application | view_apis ( Can see applicant's API keys )
hrapplications | applicationcomment | add_applicationcomment ( Can comment on applications )
Vagrant Instructions:
Copy the scripts to the root directory before running
Active Developers:
Adarnof
basraah
- [Adarnof](https://github.com/adarnof/)
- [Basraah](https://github.com/basraah/)
Beta Testers/ Bug Fixers:
Beta Testers / Bug Fixers:
TrentBartlem ( Testing and Bug Fixes )
IskFiend ( Bug Fixes and Server Configuration )
Mr McClain (Bug Fixes and server configuration )
- [ghoti](https://github.com/ghoti/)
- [mmolitor87](https://github.com/mmolitor87/)
- [kaezon](https://github.com/kaezon/)
- [orbitroom](https://github.com/orbitroom/)
- [tehfiend](https://github.com/tehfiend/)
Special Thanks:
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.
Thanks to Nikdoof, without his old auth implementation this project wouldn't be as far as it is now.
### 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.

View File

@@ -1,2 +0,0 @@
/settings.py
!/*.example

View File

@@ -1,708 +0,0 @@
# -*- coding: UTF-8 -*-
"""
Django settings for alliance_auth project.
Generated by 'django-admin startproject' using Django 1.10.1.
For more information on this file, see
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
from django.contrib import messages
from celery.schedules import crontab
# Celery configuration
BROKER_URL = 'redis://localhost:6379/0'
CELERYBEAT_SCHEDULER = "django_celery_beat.schedulers.DatabaseScheduler"
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = ''
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = 'True' == os.environ.get('AA_DEBUG','True')
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.humanize',
'django_celery_beat',
'bootstrapform',
'authentication',
'services',
'eveonline',
'groupmanagement',
'hrapplications',
'timerboard',
'srp',
'optimer',
'corputils',
'fleetactivitytracking',
'fleetup',
'notifications',
'esi',
'permissions_tool',
'geelweb.django.navhelper',
'bootstrap_pagination',
'captcha',
# Services
'services.modules.mumble',
'services.modules.discord',
'services.modules.discourse',
'services.modules.ipboard',
'services.modules.ips4',
'services.modules.market',
'services.modules.openfire',
'services.modules.seat',
'services.modules.smf',
'services.modules.phpbb3',
'services.modules.xenforo',
'services.modules.teamspeak3',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.locale.LocaleMiddleware',
]
ROOT_URLCONF = 'alliance_auth.urls'
LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale/'),
)
ugettext = lambda s: s
LANGUAGES = (
('en', ugettext('English')),
('de', ugettext('German')),
)
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(BASE_DIR, 'customization/templates'),
os.path.join(BASE_DIR, 'stock/templates'),
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'django.template.context_processors.i18n',
'django.template.context_processors.media',
'django.template.context_processors.static',
'django.template.context_processors.tz',
'services.context_processors.auth_settings',
'notifications.context_processors.user_notification_count',
'authentication.context_processors.states',
'authentication.context_processors.membership_state',
'groupmanagement.context_processors.can_manage_groups',
],
},
},
]
WSGI_APPLICATION = 'alliance_auth.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'alliance_auth',
'USER': os.environ.get('AA_DB_DEFAULT_USER', 'allianceserver'),
'PASSWORD': os.environ.get('AA_DB_DEFAULT_PASSWORD', 'password'),
'HOST': os.environ.get('AA_DB_DEFAULT_HOST', '127.0.0.1'),
'PORT': os.environ.get('AA_DB_DEFAULT_PORT', '3306'),
},
}
# If you have run the authentication.0013_service_modules migration
# you will need to set this to True in order to install service modules
# which were involved in that migration after it has been run.
# If you are on a fresh install with no existing database you can safely
# set this to True
# If you have not run the authentication.0013_service_modules migration
# leave this set to False.
SERVICES_MIGRATED = False
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
LOGIN_URL = 'auth_login_user'
SUPERUSER_STATE_BYPASS = 'True' == os.environ.get('AA_SUPERUSER_STATE_BYPASS', 'True')
# Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/
LANGUAGE_CODE = os.environ.get('AA_LANGUAGE_CODE', 'en-us')
TIME_ZONE = os.environ.get('AA_TIME_ZONE', 'UTC')
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, "static")
STATICFILES_DIRS = (
os.path.join(BASE_DIR, "customization/static"),
os.path.join(BASE_DIR, "stock/static"),
)
# Bootstrap messaging css workaround
MESSAGE_TAGS = {
messages.ERROR: 'danger'
}
CACHES = {
"default": {
"BACKEND": "redis_cache.RedisCache",
"LOCATION": "localhost:6379",
"OPTIONS": {
"DB": 1,
}
}
}
# Google Recaptcha
CAPTCHA_ENABLED = False
RECAPTCHA_PUBLIC_KEY = 'MyRecaptchaKey'
RECAPTCHA_PRIVATE_KEY = 'MyRecaptchaPrivateKey'
NOCAPTCHA = True
#####################################################
##
## Auth configuration starts here
##
#####################################################
#########################
# CELERY SCHEDULED TASKS
#########################
CELERYBEAT_SCHEDULE = {
'run_api_refresh': {
'task': 'eveonline.tasks.run_api_refresh',
'schedule': crontab(minute=0, hour="*/3"),
},
'run_corp_update': {
'task': 'eveonline.tasks.run_corp_update',
'schedule': crontab(minute=0, hour="*/2"),
},
'update_all_corpstats': {
'task': 'corputils.tasks.update_all_corpstats',
'schedule': crontab(minute=0, hour="*/6"),
},
}
#################
# EMAIL SETTINGS
#################
# DOMAIN - The alliance auth domain_url
# EMAIL_HOST - SMTP Server URL
# EMAIL_PORT - SMTP Server PORT
# EMAIL_HOST_USER - Email Username (for gmail, the entire address)
# EMAIL_HOST_PASSWORD - Email Password
# EMAIL_USE_TLS - Set to use TLS encryption
#################
DOMAIN = os.environ.get('AA_DOMAIN', 'https://example.com')
EMAIL_HOST = os.environ.get('AA_EMAIL_HOST', 'smtp.gmail.com')
EMAIL_PORT = int(os.environ.get('AA_EMAIL_PORT', '587'))
EMAIL_HOST_USER = os.environ.get('AA_EMAIL_HOST_USER', '')
EMAIL_HOST_PASSWORD = os.environ.get('AA_EMAIL_HOST_PASSWORD', '')
EMAIL_USE_TLS = 'True' == os.environ.get('AA_EMAIL_USE_TLS', 'True')
####################
# Front Page Links
####################
# KILLBOARD_URL - URL for your killboard. Blank to hide link
# MEDIA_URL - URL for your media page (youtube etc). Blank to hide link
# FORUM_URL - URL for your forums. Blank to hide link
# SITE_NAME - Name of the auth site.
####################
KILLBOARD_URL = os.environ.get('AA_KILLBOARD_URL', '')
EXTERNAL_MEDIA_URL = os.environ.get('AA_EXTERNAL_MEDIA_URL', '')
FORUM_URL = os.environ.get('AA_FORUM_URL', '')
SITE_NAME = os.environ.get('AA_SITE_NAME', 'Alliance Auth')
###################
# SSO Settings
###################
# Get client ID and client secret from registering an app at
# https://developers.eveonline.com/
# Callback URL should be https://example.com/sso/callback
###################
ESI_SSO_CLIENT_ID = os.environ.get('AA_ESI_SSO_CLIENT_ID', '')
ESI_SSO_CLIENT_SECRET = os.environ.get('AA_ESI_SSO_CLIENT_SECRET', '')
ESI_SSO_CALLBACK_URL = os.environ.get('AA_ESI_SSO_CALLBACK_URL', '')
#########################
# Default Group Settings
#########################
# DEFAULT_AUTH_GROUP - Default group members are put in
# DEFAULT_BLUE_GROUP - Default group for blue members
# MEMBER_CORP_GROUPS - Assign members to a group representing their main corp
# BLUE_CORP_GROUPS - Assign blues to a group representing their main corp
#########################
DEFAULT_AUTH_GROUP = os.environ.get('AA_DEFAULT_ALLIANCE_GROUP', 'Member')
DEFAULT_BLUE_GROUP = os.environ.get('AA_DEFAULT_BLUE_GROUP', 'Blue')
MEMBER_CORP_GROUPS = 'True' == os.environ.get('AA_MEMBER_CORP_GROUPS', 'True')
MEMBER_ALLIANCE_GROUPS = 'True' == os.environ.get('AA_MEMBER_ALLIANCE_GROUPS', 'False')
BLUE_CORP_GROUPS = 'True' == os.environ.get('AA_BLUE_CORP_GROUPS', 'False')
BLUE_ALLIANCE_GROUPS = 'True' == os.environ.get('AA_BLUE_ALLIANCE_GROUPS', 'False')
#########################
# Tenant Configuration
#########################
# CORP_IDS - A list of corporation IDs to treat as members.
# ALLIANCE_IDS - A list of alliance IDs to treat as members.
# Any corps in a specified alliance will be treated as members, so do not include them in CORP_IDS
#########################
CORP_IDS = []
ALLIANCE_IDS = []
#########################
# Standings Configuration
#########################
# Add a corp API key to add blue standings to grant access.
# CORP_API_ID - Set this to the api id for the corp API key
# CORP_API_VCODE - Set this to the api vcode for the corp API key
# BLUE_STANDING - The lowest standings value to consider blue
# STANDING_LEVEL - The level of standings to query. Accepted values are 'corp' and 'alliance'.
# BLUE_CORP_IDS - A list of corps to remain blue regardless of in-game standings
# BLUE_ALLIANCE_IDS - A list of alliances to remain blue regardless of in-game standings
########################
CORP_API_ID = os.environ.get('AA_CORP_API_ID', '')
CORP_API_VCODE = os.environ.get('AA_CORP_API_VCODE', '')
BLUE_STANDING = float(os.environ.get('AA_BLUE_STANDING', '5.0'))
STANDING_LEVEL = os.environ.get('AA_STANDING_LEVEL', 'corp')
BLUE_CORP_IDS = []
BLUE_ALLIANCE_IDS = []
########################
# API Configuration
########################
# MEMBER_API_MASK - Numeric value of minimum API mask required for members
# MEMBER_API_ACCOUNT - Require API to be for Account and not character restricted
# BLUE_API_MASK - Numeric value of minimum API mask required for blues
# BLUE_API_ACCOUNT - Require API to be for Account and not character restricted
# REJECT_OLD_APIS - Require each submitted API be newer than the latest submitted API
# REJECT_OLD_APIS_MARGIN - Margin from latest submitted API ID within which a newly submitted API is still accepted
# API_SSO_VALIDATION - Require users to prove ownership of newly entered API keys via SSO
# Requires SSO to be configured.
#######################
MEMBER_API_MASK = os.environ.get('AA_MEMBER_API_MASK', 268435455)
MEMBER_API_ACCOUNT = 'True' == os.environ.get('AA_MEMBER_API_ACCOUNT', 'True')
BLUE_API_MASK = os.environ.get('AA_BLUE_API_MASK', 8388608)
BLUE_API_ACCOUNT = 'True' == os.environ.get('AA_BLUE_API_ACCOUNT', 'False')
REJECT_OLD_APIS = 'True' == os.environ.get('AA_REJECT_OLD_APIS', 'False')
REJECT_OLD_APIS_MARGIN = os.environ.get('AA_REJECT_OLD_APIS_MARGIN', 50)
API_SSO_VALIDATION = 'True' == os.environ.get('AA_API_SSO_VALIDATION', 'False')
#######################
# EVE Provider Settings
#######################
# EVEONLINE_CHARACTER_PROVIDER - Name of default data source for getting eve character data
# EVEONLINE_CORP_PROVIDER - Name of default data source for getting eve corporation data
# EVEONLINE_ALLIANCE_PROVIDER - Name of default data source for getting eve alliance data
# EVEONLINE_ITEMTYPE_PROVIDER - Name of default data source for getting eve item type data
#
# Available sources are 'esi' and 'xml'. Leaving blank results in the default 'esi' being used.
#######################
EVEONLINE_CHARACTER_PROVIDER = os.environ.get('AA_EVEONLINE_CHARACTER_PROVIDER', '')
EVEONLINE_CORP_PROVIDER = os.environ.get('AA_EVEONLINE_CORP_PROVIDER', '')
EVEONLINE_ALLIANCE_PROVIDER = os.environ.get('AA_EVEONLINE_ALLIANCE_PROVIDER', '')
EVEONLINE_ITEMTYPE_PROVIDER = os.environ.get('AA_EVEONLINE_ITEMTYPE_PROVIDER', '')
#####################
# Alliance Market
#####################
MARKET_URL = os.environ.get('AA_MARKET_URL', 'http://example.com/market')
MARKET_DB = {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'alliance_market',
'USER': os.environ.get('AA_DB_MARKET_USER', 'allianceserver-market'),
'PASSWORD': os.environ.get('AA_DB_MARKET_PASSWORD', 'password'),
'HOST': os.environ.get('AA_DB_MARKET_HOST', '127.0.0.1'),
'PORT': os.environ.get('AA_DB_MARKET_PORT', '3306'),
}
#####################
# HR Configuration
#####################
# API_KEY_AUDIT_URL - URL for viewing API keys.
# Other URLs are supported. Use string formatting notation {} with parameter names api_id, vcode, pk
# Example URL formats are shown below:
# Old Jacknife: 'https://example.com/jacknife/eveapi/audit.php?usid={api_id}&apik={vcode}'
# New Jacknife: 'https://example.com/jacknife/?usid={api_id}&apik={vcode}'
# SeAT: 'https://seat.example.com/api-key/detail/{api_id}'
# Django Admin: '/admin/eveonline/eveapikeypair/{pk}/change'
# Leave blank for the verification code to be shown in a popup on click.
#####################
API_KEY_AUDIT_URL = os.environ.get('AA_API_KEY_AUDIT_URL', '')
#####################
# Forum Configuration
#####################
# IPBOARD_ENDPOINT - Api endpoint if using ipboard
# IPBOARD_APIKEY - Api key to interact with ipboard
# IPBOARD_APIMODULE - Module for alliance auth *leave alone*
#####################
IPBOARD_ENDPOINT = os.environ.get('AA_IPBOARD_ENDPOINT', 'example.com/interface/board/index.php')
IPBOARD_APIKEY = os.environ.get('AA_IPBOARD_APIKEY', 'somekeyhere')
IPBOARD_APIMODULE = 'aa'
########################
# XenForo Configuration
########################
XENFORO_ENDPOINT = os.environ.get('AA_XENFORO_ENDPOINT', 'example.com/api.php')
XENFORO_DEFAULT_GROUP = os.environ.get('AA_XENFORO_DEFAULT_GROUP', 0)
XENFORO_APIKEY = os.environ.get('AA_XENFORO_APIKEY', 'yourapikey')
#####################
######################
# Jabber Configuration
######################
# JABBER_URL - Jabber address url
# JABBER_PORT - Jabber service portal
# JABBER_SERVER - Jabber server url
# OPENFIRE_ADDRESS - Address of the openfire admin console including port
# Please use http with 9090 or https with 9091
# OPENFIRE_SECRET_KEY - Openfire REST API secret key
# BROADCAST_USER - Broadcast user JID
# BROADCAST_USER_PASSWORD - Broadcast user password
######################
JABBER_URL = os.environ.get('AA_JABBER_URL', "example.com")
JABBER_PORT = int(os.environ.get('AA_JABBER_PORT', '5223'))
JABBER_SERVER = os.environ.get('AA_JABBER_SERVER', "example.com")
OPENFIRE_ADDRESS = os.environ.get('AA_OPENFIRE_ADDRESS', "http://example.com:9090")
OPENFIRE_SECRET_KEY = os.environ.get('AA_OPENFIRE_SECRET_KEY', "somekey")
BROADCAST_USER = os.environ.get('AA_BROADCAST_USER', "broadcast@") + JABBER_URL
BROADCAST_USER_PASSWORD = os.environ.get('AA_BROADCAST_USER_PASSWORD', "somepassword")
BROADCAST_SERVICE_NAME = os.environ.get('AA_BROADCAST_SERVICE_NAME', "broadcast")
######################################
# Mumble Configuration
######################################
# MUMBLE_URL - Mumble server host
# Do not include leading http:// or mumble://
######################################
MUMBLE_URL = os.environ.get('AA_MUMBLE_URL', "example.com")
######################################
# PHPBB3 Configuration
######################################
PHPBB3_DB = {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'alliance_forum',
'USER': os.environ.get('AA_DB_PHPBB3_USER', 'allianceserver-phpbb3'),
'PASSWORD': os.environ.get('AA_DB_PHPBB3_PASSWORD', 'password'),
'HOST': os.environ.get('AA_DB_PHPBB3_HOST', '127.0.0.1'),
'PORT': os.environ.get('AA_DB_PHPBB3_PORT', '3306'),
}
######################################
# Teamspeak3 Configuration
######################################
# TEAMSPEAK3_SERVER_IP - Teamspeak3 server ip
# TEAMSPEAK3_SERVER_PORT - Teamspeak3 server port
# TEAMSPEAK3_SERVERQUERY_USER - Teamspeak3 serverquery username
# TEAMSPEAK3_SERVERQUERY_PASSWORD - Teamspeak3 serverquery password
# TEAMSPEAK3_VIRTUAL_SERVER - Virtual server id
# TEAMSPEAK3_AUTHED_GROUP_ID - Default authed group id
# TEAMSPEAK3_PUBLIC_URL - teamspeak3 public url used for link creation
######################################
TEAMSPEAK3_SERVER_IP = os.environ.get('AA_TEAMSPEAK3_SERVER_IP', '127.0.0.1')
TEAMSPEAK3_SERVER_PORT = int(os.environ.get('AA_TEAMSPEAK3_SERVER_PORT', '10011'))
TEAMSPEAK3_SERVERQUERY_USER = os.environ.get('AA_TEAMSPEAK3_SERVERQUERY_USER', 'serveradmin')
TEAMSPEAK3_SERVERQUERY_PASSWORD = os.environ.get('AA_TEAMSPEAK3_SERVERQUERY_PASSWORD', 'passwordhere')
TEAMSPEAK3_VIRTUAL_SERVER = int(os.environ.get('AA_TEAMSPEAK3_VIRTUAL_SERVER', '1'))
TEAMSPEAK3_PUBLIC_URL = os.environ.get('AA_TEAMSPEAK3_PUBLIC_URL', 'example.com')
######################################
# Discord Configuration
######################################
# DISCORD_GUILD_ID - ID of the guild to manage
# DISCORD_BOT_TOKEN - oauth token of the app bot user
# DISCORD_INVITE_CODE - invite code to the server
# DISCORD_APP_ID - oauth app client ID
# DISCORD_APP_SECRET - oauth app secret
# DISCORD_CALLBACK_URL - oauth callback url
# DISCORD_SYNC_NAMES - enable to force discord nicknames to be set to eve char name (bot needs Manage Nicknames permission)
######################################
DISCORD_GUILD_ID = os.environ.get('AA_DISCORD_GUILD_ID', '')
DISCORD_BOT_TOKEN = os.environ.get('AA_DISCORD_BOT_TOKEN', '')
DISCORD_INVITE_CODE = os.environ.get('AA_DISCORD_INVITE_CODE', '')
DISCORD_APP_ID = os.environ.get('AA_DISCORD_APP_ID', '')
DISCORD_APP_SECRET = os.environ.get('AA_DISCORD_APP_SECRET', '')
DISCORD_CALLBACK_URL = os.environ.get('AA_DISCORD_CALLBACK_URL', 'http://example.com/discord/callback')
DISCORD_SYNC_NAMES = 'True' == os.environ.get('AA_DISCORD_SYNC_NAMES', 'False')
######################################
# Discourse Configuration
######################################
# DISCOURSE_URL - Web address of the forums (no trailing slash)
# DISCOURSE_API_USERNAME - API account username
# DISCOURSE_API_KEY - API Key
# DISCOURSE_SSO_SECRET - SSO secret key
######################################
DISCOURSE_URL = os.environ.get('AA_DISCOURSE_URL', '')
DISCOURSE_API_USERNAME = os.environ.get('AA_DISCOURSE_API_USERNAME', '')
DISCOURSE_API_KEY = os.environ.get('AA_DISCOURSE_API_KEY', '')
DISCOURSE_SSO_SECRET = os.environ.get('AA_DISCOURSE_SSO_SECRET', '')
#####################################
# IPS4 Configuration
#####################################
# IPS4_URL - base url of the IPS4 install (no trailing slash)
# IPS4_API_KEY - API key provided by IPS4
#####################################
IPS4_URL = os.environ.get('AA_IPS4_URL', 'http://example.com/ips4')
IPS4_API_KEY = os.environ.get('AA_IPS4_API_KEY', '')
IPS4_DB = {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'alliance_ips4',
'USER': os.environ.get('AA_DB_IPS4_USER', 'allianceserver-ips4'),
'PASSWORD': os.environ.get('AA_DB_IPS4_PASSWORD', 'password'),
'HOST': os.environ.get('AA_DB_IPS4_HOST', '127.0.0.1'),
'PORT': os.environ.get('AA_DB_IPS4_PORT', '3306'),
}
#####################################
# SEAT Configuration
#####################################
# SEAT_URL - base url of the seat install (no trailing slash)
# SEAT_XTOKEN - API key X-Token provided by SeAT
#####################################
SEAT_URL = os.environ.get('AA_SEAT_URL', 'http://example.com/seat')
SEAT_XTOKEN = os.environ.get('AA_SEAT_XTOKEN', '')
######################################
# SMF Configuration
######################################
SMF_URL = os.environ.get('AA_SMF_URL', 'https://example.com')
SMF_DB = {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'alliance_smf',
'USER': os.environ.get('AA_DB_SMF_USER', 'allianceserver-smf'),
'PASSWORD': os.environ.get('AA_DB_SMF_PASSWORD', 'password'),
'HOST': os.environ.get('AA_DB_SMF_HOST', '127.0.0.1'),
'PORT': os.environ.get('AA_DB_SMF_PORT', '3306'),
}
######################################
# Fleet-Up Configuration
######################################
# FLEETUP_APP_KEY - The app key from http://fleet-up.com/Api/MyApps
# FLEETUP_USER_ID - The user id from http://fleet-up.com/Api/MyKeys
# FLEETUP_API_ID - The API id from http://fleet-up.com/Api/MyKeys
# FLEETUP_GROUP_ID - The id of the group you want to pull data from, see http://fleet-up.com/Api/Endpoints#groups_mygroupmemberships
######################################
FLEETUP_APP_KEY = os.environ.get('AA_FLEETUP_APP_KEY', '')
FLEETUP_USER_ID = os.environ.get('AA_FLEETUP_USER_ID', '')
FLEETUP_API_ID = os.environ.get('AA_FLEETUP_API_ID', '')
FLEETUP_GROUP_ID = os.environ.get('AA_FLEETUP_GROUP_ID', '')
#####################################
# 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 = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format' : "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s",
'datefmt' : "%d/%b/%Y %H:%M:%S"
},
'simple': {
'format': '%(levelname)s %(message)s'
},
},
'handlers': {
'log_file': {
'level': 'INFO', # edit this line to change logging level to file
'class': 'logging.handlers.RotatingFileHandler',
'filename': os.path.join(BASE_DIR,'log/allianceauth.log'),
'formatter': 'verbose',
'maxBytes': 1024*1024*5, # edit this line to change max log file size
'backupCount': 5, # edit this line to change number of log backups
},
'console': {
'level': 'DEBUG', # edit this line to change logging level to console
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
'notifications': { # creates notifications for users with logging_notifications permission
'level': 'ERROR', # edit this line to change logging level to notifications
'class': 'notifications.handlers.NotificationHandler',
'formatter': 'verbose',
},
},
'loggers': {
'authentication': {
'handlers': ['log_file', 'console', 'notifications'],
'level': 'DEBUG',
},
'celerytask': {
'handlers': ['log_file', 'console', 'notifications'],
'level': 'DEBUG',
},
'eveonline': {
'handlers': ['log_file', 'console', 'notifications'],
'level': 'DEBUG',
},
'groupmanagement': {
'handlers': ['log_file', 'console', 'notifications'],
'level': 'DEBUG',
},
'hrapplications': {
'handlers': ['log_file', 'console', 'notifications'],
'level': 'DEBUG',
},
'portal': {
'handlers': ['log_file', 'console', 'notifications'],
'level': 'DEBUG',
},
'registration': {
'handlers': ['log_file', 'console', 'notifications'],
'level': 'DEBUG',
},
'services': {
'handlers': ['log_file', 'console', 'notifications'],
'level': 'DEBUG',
},
'srp': {
'handlers': ['log_file', 'console', 'notifications'],
'level': 'DEBUG',
},
'timerboard': {
'handlers': ['log_file', 'console', 'notifications'],
'level': 'DEBUG',
},
'sigtracker': {
'handlers': ['log_file', 'console', 'notifications'],
'level': 'DEBUG',
},
'optimer': {
'handlers': ['log_file', 'console', 'notifications'],
'level': 'DEBUG',
},
'corputils': {
'handlers': ['log_file', 'console', 'notifications'],
'level': 'DEBUG',
},
'fleetactivitytracking': {
'handlers': ['log_file', 'console', 'notifications'],
'level': 'ERROR',
},
'fleetup': {
'handlers': ['log_file', 'console', 'notifications'],
'level': 'DEBUG',
},
'util': {
'handlers': ['log_file', 'console', 'notifications'],
'level': 'DEBUG',
},
'django': {
'handlers': ['log_file', 'console', 'notifications'],
'level': 'ERROR',
},
}
}
# Conditionally add databases only if configured
if 'services.modules.phpbb3' in INSTALLED_APPS:
DATABASES['phpbb3'] = PHPBB3_DB
if 'services.modules.smf' in INSTALLED_APPS:
DATABASES['smf'] = SMF_DB
if 'services.modules.market' in INSTALLED_APPS:
DATABASES['market'] = MARKET_DB
if 'services.modules.ips4' in INSTALLED_APPS:
DATABASES['ips4'] = IPS4_DB
# Ensure corp/alliance IDs are expected types
STR_CORP_IDS = [str(id) for id in CORP_IDS]
STR_ALLIANCE_IDS = [str(id) for id in ALLIANCE_IDS]
STR_BLUE_CORP_IDS = [str(id) for id in BLUE_CORP_IDS]
STR_BLUE_ALLIANCE_IDS = [str(id) for id in BLUE_ALLIANCE_IDS]
# Conditionally add periodic tasks for services if installed
if 'services.modules.teamspeak3' in INSTALLED_APPS:
CELERYBEAT_SCHEDULE['run_ts3_group_update'] = {
'task': 'services.modules.teamspeak3.tasks.run_ts3_group_update',
'schedule': crontab(minute='*/30'),
}
if 'services.modules.seat' in INSTALLED_APPS:
CELERYBEAT_SCHEDULE['run_seat_api_sync'] = {
'task': 'services.modules.seat.tasks.run_api_sync',
'schedule': crontab(minute='*/30'),
}

View File

@@ -1,98 +0,0 @@
from __future__ import unicode_literals
from django.db.models.signals import m2m_changed, pre_save
from django.contrib.auth.models import User, Group, Permission
from services.signals import m2m_changed_user_groups, pre_save_user
from services.signals import m2m_changed_group_permissions, m2m_changed_user_permissions
from authentication.signals import pre_save_auth_state
from authentication.tasks import make_member, make_blue
from authentication.models import AuthServicesInfo
from authentication.states import MEMBER_STATE, BLUE_STATE, NONE_STATE
from eveonline.models import EveCharacter
class AuthUtils:
def __init__(self):
pass
@staticmethod
def _create_user(username):
return User.objects.create(username=username)
@classmethod
def create_user(cls, username, disconnect_signals=False):
if disconnect_signals:
cls.disconnect_signals()
user = cls._create_user(username)
user.authservicesinfo.state = NONE_STATE
user.authservicesinfo.save()
if disconnect_signals:
cls.connect_signals()
return user
@classmethod
def create_member(cls, username):
cls.disconnect_signals()
user = cls._create_user(username)
user.authservicesinfo.state = MEMBER_STATE
user.authservicesinfo.save()
make_member(user.authservicesinfo)
cls.connect_signals()
return user
@classmethod
def create_blue(cls, username):
cls.disconnect_signals()
user = cls._create_user(username)
user.authservicesinfo.state = BLUE_STATE
user.authservicesinfo.save()
make_blue(user.authservicesinfo)
cls.connect_signals()
return user
@classmethod
def disconnect_signals(cls):
m2m_changed.disconnect(m2m_changed_user_groups, sender=User.groups.through)
m2m_changed.disconnect(m2m_changed_group_permissions, sender=Group.permissions.through)
m2m_changed.disconnect(m2m_changed_user_permissions, sender=User.user_permissions.through)
pre_save.disconnect(pre_save_user, sender=User)
pre_save.disconnect(pre_save_auth_state, sender=AuthServicesInfo)
@classmethod
def connect_signals(cls):
m2m_changed.connect(m2m_changed_user_groups, sender=User.groups.through)
m2m_changed.connect(m2m_changed_group_permissions, sender=Group.permissions.through)
m2m_changed.connect(m2m_changed_user_permissions, sender=User.user_permissions.through)
pre_save.connect(pre_save_user, sender=User)
pre_save.connect(pre_save_auth_state, sender=AuthServicesInfo)
@classmethod
def add_main_character(cls, user, name, character_id, corp_id='', corp_name='', corp_ticker='', alliance_id='',
alliance_name=''):
EveCharacter.objects.create(
character_id=character_id,
character_name=name,
corporation_id=corp_id,
corporation_name=corp_name,
corporation_ticker=corp_ticker,
alliance_id=alliance_id,
alliance_name=alliance_name,
api_id='1234',
user=user
)
AuthServicesInfo.objects.update_or_create(user=user, defaults={'main_char_id': character_id})
@classmethod
def add_permissions_to_groups(cls, perms, groups, disconnect_signals=True):
if disconnect_signals:
cls.disconnect_signals()
for group in groups:
for perm in perms:
group.permissions.add(perm)
if disconnect_signals:
cls.connect_signals()

View File

@@ -1,556 +0,0 @@
"""
Alliance Auth Test Suite Django settings.
"""
import os
from django.contrib import messages
import alliance_auth
# Use nose to run all tests
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
NOSE_ARGS = [
#'--with-coverage',
#'--cover-package=',
#'--exe', # If your tests need this to be found/run, check they py files are not chmodded +x
]
# Celery configuration
CELERY_ALWAYS_EAGER = True # Forces celery to run locally for testing
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(alliance_auth.__file__)))
SECRET_KEY = 'testing only'
DEBUG = True
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.humanize',
'django_celery_beat',
'bootstrapform',
'authentication',
'services',
'eveonline',
'groupmanagement',
'hrapplications',
'timerboard',
'srp',
'optimer',
'corputils',
'fleetactivitytracking',
'fleetup',
'notifications',
'esi',
'permissions_tool',
'geelweb.django.navhelper',
'bootstrap_pagination',
'services.modules.mumble',
'services.modules.discord',
'services.modules.discourse',
'services.modules.ipboard',
'services.modules.ips4',
'services.modules.market',
'services.modules.openfire',
'services.modules.seat',
'services.modules.smf',
'services.modules.phpbb3',
'services.modules.xenforo',
'services.modules.teamspeak3',
'django_nose',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.locale.LocaleMiddleware',
]
ROOT_URLCONF = 'alliance_auth.urls'
LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale/'),
)
ugettext = lambda s: s
LANGUAGES = (
('en', ugettext('English')),
('de', ugettext('German')),
)
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(BASE_DIR, 'customization/templates'),
os.path.join(BASE_DIR, 'stock/templates'),
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'django.template.context_processors.i18n',
'django.template.context_processors.media',
'django.template.context_processors.static',
'django.template.context_processors.tz',
'services.context_processors.auth_settings',
'notifications.context_processors.user_notification_count',
'authentication.context_processors.states',
'authentication.context_processors.membership_state',
'groupmanagement.context_processors.can_manage_groups',
],
},
},
]
# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'alliance_auth',
'USER': os.environ.get('AA_DB_DEFAULT_USER', None),
'PASSWORD': os.environ.get('AA_DB_DEFAULT_PASSWORD', None),
'HOST': os.environ.get('AA_DB_DEFAULT_HOST', None)
},
}
LOGIN_URL = 'auth_login_user'
SUPERUSER_STATE_BYPASS = 'True' == os.environ.get('AA_SUPERUSER_STATE_BYPASS', 'True')
# Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/
LANGUAGE_CODE = os.environ.get('AA_LANGUAGE_CODE', 'en')
TIME_ZONE = os.environ.get('AA_TIME_ZONE', 'UTC')
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, "static")
STATICFILES_DIRS = (
os.path.join(BASE_DIR, "customization/static"),
os.path.join(BASE_DIR, "stock/static"),
)
# Bootstrap messaging css workaround
MESSAGE_TAGS = {
messages.ERROR: 'danger'
}
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
}
}
#####################################################
##
## Auth configuration starts here
##
#####################################################
###########################
# ALLIANCE / CORP TOGGLE
###########################
# Specifies to run membership checks against corp or alliance
# Set to FALSE for alliance
# Set to TRUE for corp
###########################
IS_CORP = 'True' == os.environ.get('AA_IS_CORP', 'True')
#################
# EMAIL SETTINGS
#################
# DOMAIN - The alliance auth domain_url
# EMAIL_HOST - SMTP Server URL
# EMAIL_PORT - SMTP Server PORT
# EMAIL_HOST_USER - Email Username (for gmail, the entire address)
# EMAIL_HOST_PASSWORD - Email Password
# EMAIL_USE_TLS - Set to use TLS encryption
#################
DOMAIN = os.environ.get('AA_DOMAIN', 'https://example.com')
EMAIL_HOST = os.environ.get('AA_EMAIL_HOST', 'smtp.example.com')
EMAIL_PORT = int(os.environ.get('AA_EMAIL_PORT', '587'))
EMAIL_HOST_USER = os.environ.get('AA_EMAIL_HOST_USER', '')
EMAIL_HOST_PASSWORD = os.environ.get('AA_EMAIL_HOST_PASSWORD', '')
EMAIL_USE_TLS = 'True' == os.environ.get('AA_EMAIL_USE_TLS', 'True')
####################
# Front Page Links
####################
# KILLBOARD_URL - URL for your killboard. Blank to hide link
# MEDIA_URL - URL for your media page (youtube etc). Blank to hide link
# FORUM_URL - URL for your forums. Blank to hide link
# SITE_NAME - Name of the auth site.
####################
KILLBOARD_URL = os.environ.get('AA_KILLBOARD_URL', '')
EXTERNAL_MEDIA_URL = os.environ.get('AA_EXTERNAL_MEDIA_URL', '')
FORUM_URL = os.environ.get('AA_FORUM_URL', '')
SITE_NAME = os.environ.get('AA_SITE_NAME', 'Test Alliance Auth')
###################
# SSO Settings
###################
# Optional SSO.
# Get client ID and client secret from registering an app at
# https://developers.eveonline.com/
# Callback URL should be http://mydomain.com/sso/callback
# Leave callback blank to hide SSO button on login page
###################
ESI_SSO_CLIENT_ID = os.environ.get('AA_ESI_SSO_CLIENT_ID', '')
ESI_SSO_CLIENT_SECRET = os.environ.get('AA_ESI_SSO_CLIENT_SECRET', '')
ESI_SSO_CALLBACK_URL = os.environ.get('AA_ESI_SSO_CALLBACK_URL', '')
#########################
# Default Group Settings
#########################
# DEFAULT_AUTH_GROUP - Default group members are put in
# DEFAULT_BLUE_GROUP - Default group for blue members
# MEMBER_CORP_GROUPS - Assign members to a group representing their main corp
# BLUE_CORP_GROUPS - Assign blues to a group representing their main corp
#########################
DEFAULT_AUTH_GROUP = os.environ.get('AA_DEFAULT_ALLIANCE_GROUP', 'Member')
DEFAULT_BLUE_GROUP = os.environ.get('AA_DEFAULT_BLUE_GROUP', 'Blue')
MEMBER_CORP_GROUPS = 'True' == os.environ.get('AA_MEMBER_CORP_GROUPS', 'True')
MEMBER_ALLIANCE_GROUPS = 'True' == os.environ.get('AA_MEMBER_ALLIANCE_GROUPS', 'False')
BLUE_CORP_GROUPS = 'True' == os.environ.get('AA_BLUE_CORP_GROUPS', 'False')
BLUE_ALLIANCE_GROUPS = 'True' == os.environ.get('AA_BLUE_ALLIANCE_GROUPS', 'False')
#########################
# Corp Configuration
#########################
# If running in alliance mode, the following should be for the executor corp#
# CORP_ID - Set this to your corp ID (get this from https://zkillboard.com/corporation/#######)
# CORP_NAME - Set this to your Corporation Name
# CORP_API_ID - Set this to the api id for the corp API key
# CORP_API_VCODE - Set this to the api vcode for the corp API key
########################
CORP_ID = os.environ.get('AA_CORP_ID', '1234')
CORP_NAME = os.environ.get('AA_CORP_NAME', 'Alliance Auth Test Corp')
CORP_API_ID = os.environ.get('AA_CORP_API_ID', '')
CORP_API_VCODE = os.environ.get('AA_CORP_API_VCODE', '')
#########################
# Alliance Configuration
#########################
# ALLIANCE_ID - Set this to your Alliance ID (get this from https://zkillboard.com/alliance/#######)
# ALLIANCE_NAME - Set this to your Alliance Name
########################
ALLIANCE_ID = os.environ.get('AA_ALLIANCE_ID', '12345')
ALLIANCE_NAME = os.environ.get('AA_ALLIANCE_NAME', 'Alliance Auth Test Alliance')
########################
# API Configuration
########################
# MEMBER_API_MASK - Numeric value of minimum API mask required for members
# MEMBER_API_ACCOUNT - Require API to be for Account and not character restricted
# BLUE_API_MASK - Numeric value of minimum API mask required for blues
# BLUE_API_ACCOUNT - Require API to be for Account and not character restricted
# REJECT_OLD_APIS - Require each submitted API be newer than the latest submitted API
# REJECT_OLD_APIS_MARGIN - Margin from latest submitted API ID within which a newly submitted API is still accepted
# API_SSO_VALIDATION - Require users to prove ownership of newly entered API keys via SSO
# Requires SSO to be configured.
#######################
MEMBER_API_MASK = os.environ.get('AA_MEMBER_API_MASK', 268435455)
MEMBER_API_ACCOUNT = 'True' == os.environ.get('AA_MEMBER_API_ACCOUNT', 'True')
BLUE_API_MASK = os.environ.get('AA_BLUE_API_MASK', 8388608)
BLUE_API_ACCOUNT = 'True' == os.environ.get('AA_BLUE_API_ACCOUNT', 'True')
REJECT_OLD_APIS = 'True' == os.environ.get('AA_REJECT_OLD_APIS', 'False')
REJECT_OLD_APIS_MARGIN = os.environ.get('AA_REJECT_OLD_APIS_MARGIN', 50)
API_SSO_VALIDATION = 'True' == os.environ.get('AA_API_SSO_VALIDATION', 'False')
#######################
# EVE Provider Settings
#######################
# EVEONLINE_CHARACTER_PROVIDER - Name of default data source for getting eve character data
# EVEONLINE_CORP_PROVIDER - Name of default data source for getting eve corporation data
# EVEONLINE_ALLIANCE_PROVIDER - Name of default data source for getting eve alliance data
# EVEONLINE_ITEMTYPE_PROVIDER - Name of default data source for getting eve item type data
#
# Available sources are 'esi' and 'xml'. Leaving blank results in the default 'esi' being used.
#######################
EVEONLINE_CHARACTER_PROVIDER = os.environ.get('AA_EVEONLINE_CHARACTER_PROVIDER', 'xml')
EVEONLINE_CORP_PROVIDER = os.environ.get('AA_EVEONLINE_CORP_PROVIDER', 'xml')
EVEONLINE_ALLIANCE_PROVIDER = os.environ.get('AA_EVEONLINE_ALLIANCE_PROVIDER', 'xml')
EVEONLINE_ITEMTYPE_PROVIDER = os.environ.get('AA_EVEONLINE_ITEMTYPE_PROVIDER', 'xml')
#####################
# Alliance Market
#####################
MARKET_URL = os.environ.get('AA_MARKET_URL', 'http://yourdomain.com/market')
#####################
# HR Configuration
#####################
# JACK_KNIFE_URL - Url for the audit page of API Jack knife
# Should seriously replace with your own.
#####################
JACK_KNIFE_URL = os.environ.get('AA_JACK_KNIFE_URL', 'http://example.com/eveapi/audit.php')
#####################
# Forum Configuration
#####################
# IPBOARD_ENDPOINT - Api endpoint if using ipboard
# IPBOARD_APIKEY - Api key to interact with ipboard
# IPBOARD_APIMODULE - Module for alliance auth *leave alone*
#####################
IPBOARD_ENDPOINT = os.environ.get('AA_IPBOARD_ENDPOINT', 'example.com/interface/board/index.php')
IPBOARD_APIKEY = os.environ.get('AA_IPBOARD_APIKEY', 'somekeyhere')
IPBOARD_APIMODULE = 'aa'
########################
# XenForo Configuration
########################
XENFORO_ENDPOINT = os.environ.get('AA_XENFORO_ENDPOINT', 'example.com/api.php')
XENFORO_DEFAULT_GROUP = os.environ.get('AA_XENFORO_DEFAULT_GROUP', 0)
XENFORO_APIKEY = os.environ.get('AA_XENFORO_APIKEY', 'yourapikey')
#####################
######################
# Jabber Configuration
######################
# JABBER_URL - Jabber address url
# JABBER_PORT - Jabber service portal
# JABBER_SERVER - Jabber server url
# OPENFIRE_ADDRESS - Address of the openfire admin console including port
# Please use http with 9090 or https with 9091
# OPENFIRE_SECRET_KEY - Openfire REST API secret key
# BROADCAST_USER - Broadcast user JID
# BROADCAST_USER_PASSWORD - Broadcast user password
######################
JABBER_URL = os.environ.get('AA_JABBER_URL', "example.com")
JABBER_PORT = int(os.environ.get('AA_JABBER_PORT', '5223'))
JABBER_SERVER = os.environ.get('AA_JABBER_SERVER', "example.com")
OPENFIRE_ADDRESS = os.environ.get('AA_OPENFIRE_ADDRESS', "http://example.com:9090")
OPENFIRE_SECRET_KEY = os.environ.get('AA_OPENFIRE_SECRET_KEY', "somekey")
BROADCAST_USER = os.environ.get('AA_BROADCAST_USER', "broadcast@") + JABBER_URL
BROADCAST_USER_PASSWORD = os.environ.get('AA_BROADCAST_USER_PASSWORD', "somepassword")
BROADCAST_SERVICE_NAME = os.environ.get('AA_BROADCAST_SERVICE_NAME', "broadcast")
######################################
# Mumble Configuration
######################################
# MUMBLE_URL - Mumble server url
# MUMBLE_SERVER_ID - Mumble server id
######################################
MUMBLE_URL = os.environ.get('AA_MUMBLE_URL', "example.com")
MUMBLE_SERVER_ID = int(os.environ.get('AA_MUMBLE_SERVER_ID', '1'))
######################################
# PHPBB3 Configuration
######################################
######################################
# Teamspeak3 Configuration
######################################
# TEAMSPEAK3_SERVER_IP - Teamspeak3 server ip
# TEAMSPEAK3_SERVER_PORT - Teamspeak3 server port
# TEAMSPEAK3_SERVERQUERY_USER - Teamspeak3 serverquery username
# TEAMSPEAK3_SERVERQUERY_PASSWORD - Teamspeak3 serverquery password
# TEAMSPEAK3_VIRTUAL_SERVER - Virtual server id
# TEAMSPEAK3_AUTHED_GROUP_ID - Default authed group id
# TEAMSPEAK3_PUBLIC_URL - teamspeak3 public url used for link creation
######################################
TEAMSPEAK3_SERVER_IP = os.environ.get('AA_TEAMSPEAK3_SERVER_IP', '127.0.0.1')
TEAMSPEAK3_SERVER_PORT = int(os.environ.get('AA_TEAMSPEAK3_SERVER_PORT', '10011'))
TEAMSPEAK3_SERVERQUERY_USER = os.environ.get('AA_TEAMSPEAK3_SERVERQUERY_USER', 'serveradmin')
TEAMSPEAK3_SERVERQUERY_PASSWORD = os.environ.get('AA_TEAMSPEAK3_SERVERQUERY_PASSWORD', 'passwordhere')
TEAMSPEAK3_VIRTUAL_SERVER = int(os.environ.get('AA_TEAMSPEAK3_VIRTUAL_SERVER', '1'))
TEAMSPEAK3_PUBLIC_URL = os.environ.get('AA_TEAMSPEAK3_PUBLIC_URL', 'example.com')
######################################
# Discord Configuration
######################################
# DISCORD_GUILD_ID - ID of the guild to manage
# DISCORD_BOT_TOKEN - oauth token of the app bot user
# DISCORD_INVITE_CODE - invite code to the server
# DISCORD_APP_ID - oauth app client ID
# DISCORD_APP_SECRET - oauth app secret
# DISCORD_CALLBACK_URL - oauth callback url
# DISCORD_SYNC_NAMES - enable to force discord nicknames to be set to eve char name (bot needs Manage Nicknames permission)
######################################
DISCORD_GUILD_ID = os.environ.get('AA_DISCORD_GUILD_ID', '0118999')
DISCORD_BOT_TOKEN = os.environ.get('AA_DISCORD_BOT_TOKEN', 'bottoken')
DISCORD_INVITE_CODE = os.environ.get('AA_DISCORD_INVITE_CODE', 'invitecode')
DISCORD_APP_ID = os.environ.get('AA_DISCORD_APP_ID', 'appid')
DISCORD_APP_SECRET = os.environ.get('AA_DISCORD_APP_SECRET', 'secret')
DISCORD_CALLBACK_URL = os.environ.get('AA_DISCORD_CALLBACK_URL', 'http://example.com/discord/callback')
DISCORD_SYNC_NAMES = 'True' == os.environ.get('AA_DISCORD_SYNC_NAMES', 'False')
######################################
# Discourse Configuration
######################################
# DISCOURSE_URL - Web address of the forums (no trailing slash)
# DISCOURSE_API_USERNAME - API account username
# DISCOURSE_API_KEY - API Key
# DISCOURSE_SSO_SECRET - SSO secret key
######################################
DISCOURSE_URL = os.environ.get('AA_DISCOURSE_URL', 'https://example.com')
DISCOURSE_API_USERNAME = os.environ.get('AA_DISCOURSE_API_USERNAME', '')
DISCOURSE_API_KEY = os.environ.get('AA_DISCOURSE_API_KEY', '')
DISCOURSE_SSO_SECRET = 'd836444a9e4084d5b224a60c208dce14'
# Example secret from https://meta.discourse.org/t/official-single-sign-on-for-discourse/13045
#####################################
# IPS4 Configuration
#####################################
# IPS4_URL - base url of the IPS4 install (no trailing slash)
# IPS4_API_KEY - API key provided by IPS4
#####################################
IPS4_URL = os.environ.get('AA_IPS4_URL', 'http://example.com/ips4')
IPS4_API_KEY = os.environ.get('AA_IPS4_API_KEY', '')
#####################################
# SEAT Configuration
#####################################
# SEAT_URL - base url of the seat install (no trailing slash)
# SEAT_XTOKEN - API key X-Token provided by SeAT
#####################################
SEAT_URL = os.environ.get('AA_SEAT_URL', 'http://example.com/seat')
SEAT_XTOKEN = os.environ.get('AA_SEAT_XTOKEN', 'tokentokentoken')
######################################
# SMF Configuration
######################################
SMF_URL = os.environ.get('AA_SMF_URL', '')
######################################
# Fleet-Up Configuration
######################################
# FLEETUP_APP_KEY - The app key from http://fleet-up.com/Api/MyApps
# FLEETUP_USER_ID - The user id from http://fleet-up.com/Api/MyKeys
# FLEETUP_API_ID - The API id from http://fleet-up.com/Api/MyKeys
# FLEETUP_GROUP_ID - The id of the group you want to pull data from, see http://fleet-up.com/Api/Endpoints#groups_mygroupmemberships
######################################
FLEETUP_APP_KEY = os.environ.get('AA_FLEETUP_APP_KEY', '')
FLEETUP_USER_ID = os.environ.get('AA_FLEETUP_USER_ID', '')
FLEETUP_API_ID = os.environ.get('AA_FLEETUP_API_ID', '')
FLEETUP_GROUP_ID = os.environ.get('AA_FLEETUP_GROUP_ID', '')
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.MD5PasswordHasher',
]
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format' : "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s",
'datefmt' : "%d/%b/%Y %H:%M:%S"
},
'simple': {
'format': '%(levelname)s %(message)s'
},
},
'handlers': {
'console': {
'level': 'DEBUG', # edit this line to change logging level to console
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
'notifications': { # creates notifications for users with logging_notifications permission
'level': 'ERROR', # edit this line to change logging level to notifications
'class': 'notifications.handlers.NotificationHandler',
'formatter': 'verbose',
},
},
'loggers': {
'authentication': {
'handlers': ['console', 'notifications'],
'level': 'DEBUG',
},
'celerytask': {
'handlers': ['console', 'notifications'],
'level': 'DEBUG',
},
'eveonline': {
'handlers': ['console', 'notifications'],
'level': 'DEBUG',
},
'groupmanagement': {
'handlers': ['console', 'notifications'],
'level': 'DEBUG',
},
'hrapplications': {
'handlers': ['console', 'notifications'],
'level': 'DEBUG',
},
'portal': {
'handlers': ['console', 'notifications'],
'level': 'DEBUG',
},
'registration': {
'handlers': ['console', 'notifications'],
'level': 'DEBUG',
},
'services': {
'handlers': ['console', 'notifications'],
'level': 'DEBUG',
},
'srp': {
'handlers': ['console', 'notifications'],
'level': 'DEBUG',
},
'timerboard': {
'handlers': ['console', 'notifications'],
'level': 'DEBUG',
},
'sigtracker': {
'handlers': ['console', 'notifications'],
'level': 'DEBUG',
},
'optimer': {
'handlers': ['console', 'notifications'],
'level': 'DEBUG',
},
'corputils': {
'handlers': ['console', 'notifications'],
'level': 'DEBUG',
},
'fleetactivitytracking': {
'handlers': ['console', 'notifications'],
'level': 'ERROR',
},
'util': {
'handlers': ['console', 'notifications'],
'level': 'DEBUG',
},
'django': {
'handlers': ['console', 'notifications'],
'level': 'ERROR',
},
}
}
LOGGING = None # Comment out to enable logging for debugging

View File

@@ -1,222 +0,0 @@
from django.conf.urls import include, url
from django.conf.urls.i18n import i18n_patterns
from django.utils.translation import ugettext_lazy as _
from django.contrib import admin
import django.contrib.auth.views
import authentication.views
import eveonline.views
import services.views
import groupmanagement.views
import optimer.views
import timerboard.views
import fleetactivitytracking.views
import fleetup.urls
import srp.views
import notifications.views
import hrapplications.views
import corputils.urls
import esi.urls
import permissions_tool.urls
from alliance_auth import NAME
admin.site.site_header = NAME
from alliance_auth.hooks import get_hooks
# Functional/Untranslated URL's
urlpatterns = [
# Locale
url(r'^i18n/', include('django.conf.urls.i18n')),
# Admin urls
url(r'^admin/', include(admin.site.urls)),
# SSO
url(r'^sso/', include(esi.urls, namespace='esi')),
url(r'^sso/login$', authentication.views.sso_login, name='auth_sso_login'),
# Index
url(_(r'^$'), authentication.views.index_view, name='auth_index'),
# Authentication
url(r'^logout_user/', authentication.views.logout_user, name='auth_logout_user'),
# Eve Online
url(r'^main_character_change/(\w+)/$', eveonline.views.main_character_change,
name='auth_main_character_change'),
url(r'^api_verify_owner/(\w+)/$', eveonline.views.api_sso_validate, name='auth_api_sso'),
# SRP URLS
url(r'^srp_fleet_remove/(\w+)$', srp.views.srp_fleet_remove, name='auth_srp_fleet_remove'),
url(r'^srp_fleet_disable/(\w+)$', srp.views.srp_fleet_disable, name='auth_srp_fleet_disable'),
url(r'^srp_fleet_enable/(\w+)$', srp.views.srp_fleet_enable, name='auth_srp_fleet_enable'),
url(r'^srp_fleet_mark_completed/(\w+)', srp.views.srp_fleet_mark_completed,
name='auth_srp_fleet_mark_completed'),
url(r'^srp_fleet_mark_uncompleted/(\w+)', srp.views.srp_fleet_mark_uncompleted,
name='auth_srp_fleet_mark_uncompleted'),
url(r'^srp_request_remove/', srp.views.srp_request_remove,
name="auth_srp_request_remove"),
url(r'srp_request_approve/', srp.views.srp_request_approve,
name='auth_srp_request_approve'),
url(r'srp_request_reject/', srp.views.srp_request_reject,
name='auth_srp_request_reject'),
url(_(r'srp_request_amount_update/(\w+)'), srp.views.srp_request_update_amount,
name="auth_srp_request_update_amount"),
# Notifications
url(r'^remove_notifications/(\w+)/$', notifications.views.remove_notification, name='auth_remove_notification'),
url(r'^notifications/mark_all_read/$', notifications.views.mark_all_read, name='auth_mark_all_notifications_read'),
url(r'^notifications/delete_all_read/$', notifications.views.delete_all_read,
name='auth_delete_all_read_notifications'),
]
# User viewed/translated URLS
urlpatterns += i18n_patterns(
# Fleetup
url(r'^fleetup/', include(fleetup.urls.urlpatterns)),
# Authentication
url(_(r'^login_user/'), authentication.views.login_user, name='auth_login_user'),
url(_(r'^register_user/'), authentication.views.register_user_view, name='auth_register_user'),
url(_(r'^user/password/$'), django.contrib.auth.views.password_change, name='password_change'),
url(_(r'^user/password/done/$'), django.contrib.auth.views.password_change_done,
name='password_change_done'),
url(_(r'^user/password/reset/$'), django.contrib.auth.views.password_reset,
name='password_reset'),
url(_(r'^user/password/password/reset/done/$'), django.contrib.auth.views.password_reset_done,
name='password_reset_done'),
url(_(r'^user/password/reset/complete/$'), django.contrib.auth.views.password_reset_complete,
name='password_reset_complete'),
url(_(r'^user/password/reset/confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>.+)/$'),
django.contrib.auth.views.password_reset_confirm, name='password_reset_confirm'),
# Portal Urls
url(_(r'^dashboard/$'), eveonline.views.dashboard_view, name='auth_dashboard'),
url(_(r'^help/$'), authentication.views.help_view, name='auth_help'),
# Eveonline Urls
url(_(r'^add_api_key/'), eveonline.views.add_api_key, name='auth_add_api_key'),
url(_(r'^refresh_api_pair/([0-9]+)/$'), eveonline.views.user_refresh_api, name='auth_user_refresh_api'),
url(_(r'^delete_api_pair/(\w+)/$'), eveonline.views.api_key_removal, name='auth_api_key_removal'),
url(_(r'^characters/'), eveonline.views.characters_view, name='auth_characters'),
# Corputils
url(_(r'^corpstats/'), include(corputils.urls, namespace='corputils')),
# Group management
url(_(r'^groups/'), groupmanagement.views.groups_view, name='auth_groups'),
url(_(r'^group/management/'), groupmanagement.views.group_management,
name='auth_group_management'),
url(_(r'^group/membership/$'), groupmanagement.views.group_membership,
name='auth_group_membership'),
url(_(r'^group/membership/(\w+)/$'), groupmanagement.views.group_membership_list,
name='auth_group_membership_list'),
url(_(r'^group/membership/(\w+)/remove/(\w+)/$'), groupmanagement.views.group_membership_remove,
name='auth_group_membership_remove'),
url(_(r'^group/request_add/(\w+)'), groupmanagement.views.group_request_add,
name='auth_group_request_add'),
url(_(r'^group/request/accept/(\w+)'), groupmanagement.views.group_accept_request,
name='auth_group_accept_request'),
url(_(r'^group/request/reject/(\w+)'), groupmanagement.views.group_reject_request,
name='auth_group_reject_request'),
url(_(r'^group/request_leave/(\w+)'), groupmanagement.views.group_request_leave,
name='auth_group_request_leave'),
url(_(r'group/leave_request/accept/(\w+)'), groupmanagement.views.group_leave_accept_request,
name='auth_group_leave_accept_request'),
url(_(r'^group/leave_request/reject/(\w+)'), groupmanagement.views.group_leave_reject_request,
name='auth_group_leave_reject_request'),
# HR Application Management
url(_(r'^hr_application_management/'), hrapplications.views.hr_application_management_view,
name="auth_hrapplications_view"),
url(_(r'^hr_application_create/$'), hrapplications.views.hr_application_create_view,
name="auth_hrapplication_create_view"),
url(_(r'^hr_application_create/(\d+)'), hrapplications.views.hr_application_create_view,
name="auth_hrapplication_create_view"),
url(_(r'^hr_application_remove/(\w+)'), hrapplications.views.hr_application_remove,
name="auth_hrapplication_remove"),
url(_(r'hr_application_view/(\w+)'), hrapplications.views.hr_application_view,
name="auth_hrapplication_view"),
url(_(r'hr_application_personal_view/(\w+)'), hrapplications.views.hr_application_personal_view,
name="auth_hrapplication_personal_view"),
url(_(r'hr_application_personal_removal/(\w+)'),
hrapplications.views.hr_application_personal_removal,
name="auth_hrapplication_personal_removal"),
url(_(r'hr_application_approve/(\w+)'), hrapplications.views.hr_application_approve,
name="auth_hrapplication_approve"),
url(_(r'hr_application_reject/(\w+)'), hrapplications.views.hr_application_reject,
name="auth_hrapplication_reject"),
url(_(r'hr_application_search/'), hrapplications.views.hr_application_search,
name="auth_hrapplication_search"),
url(_(r'hr_mark_in_progress/(\w+)'), hrapplications.views.hr_application_mark_in_progress,
name="auth_hrapplication_mark_in_progress"),
# Fleet Operations Timers
url(_(r'^optimer/$'), optimer.views.optimer_view, name='auth_optimer_view'),
url(_(r'^add_optimer/$'), optimer.views.add_optimer_view, name='auth_add_optimer_view'),
url(_(r'^remove_optimer/(\w+)'), optimer.views.remove_optimer, name='auth_remove_optimer'),
url(_(r'^edit_optimer/(\w+)$'), optimer.views.edit_optimer, name='auth_edit_optimer'),
# Service Urls
url(_(r'^services/$'), services.views.services_view, name='auth_services'),
# Timer URLS
url(_(r'^timers/$'), timerboard.views.timer_view, name='auth_timer_view'),
url(_(r'^add_timer/$'), timerboard.views.add_timer_view, name='auth_add_timer_view'),
url(_(r'^remove_timer/(\w+)'), timerboard.views.remove_timer, name='auth_remove_timer'),
url(_(r'^edit_timer/(\w+)$'), timerboard.views.edit_timer, name='auth_edit_timer'),
# SRP URLS
url(_(r'^srp/$'), srp.views.srp_management, name='auth_srp_management_view'),
url(_(r'^srp_all/$'), srp.views.srp_management_all, name='auth_srp_management_all_view'),
url(_(r'^srp_fleet_view/(\w+)$'), srp.views.srp_fleet_view, name='auth_srp_fleet_view'),
url(_(r'^srp_fleet_add_view/$'), srp.views.srp_fleet_add_view, name='auth_srp_fleet_add_view'),
url(_(r'^srp_fleet_edit/(\w+)$'), srp.views.srp_fleet_edit_view, name='auth_srp_fleet_edit_view'),
url(_(r'^srp_request/(\w+)'), srp.views.srp_request_view, name='auth_srp_request_view'),
# Tools
url(_(r'^tool/fleet_formatter_tool/$'), services.views.fleet_formatter_view,
name='auth_fleet_format_tool_view'),
# Notifications
url(_(r'^notifications/$'), notifications.views.notification_list, name='auth_notification_list'),
url(_(r'^notifications/(\w+)/$'), notifications.views.notification_view, name='auth_notification_view'),
# FleetActivityTracking (FAT)
url(r'^fat/$', fleetactivitytracking.views.fatlink_view, name='auth_fatlink_view'),
url(r'^fat/statistics/$', fleetactivitytracking.views.fatlink_statistics_view, name='auth_fatlink_view_statistics'),
url(r'^fat/statistics/corp/(\w+)$', fleetactivitytracking.views.fatlink_statistics_corp_view, name='auth_fatlink_view_statistics_corp'),
url(r'^fat/statistics/corp/(?P<corpid>\w+)/(?P<year>[0-9]+)/(?P<month>[0-9]+)/', fleetactivitytracking.views.fatlink_statistics_corp_view,
name='auth_fatlink_view_statistics_corp_month'),
url(r'^fat/statistics/(?P<year>[0-9]+)/(?P<month>[0-9]+)/$', fleetactivitytracking.views.fatlink_statistics_view,
name='auth_fatlink_view_statistics_month'),
url(r'^fat/user/statistics/$', fleetactivitytracking.views.fatlink_personal_statistics_view,
name='auth_fatlink_view_personal_statistics'),
url(r'^fat/user/statistics/(?P<year>[0-9]+)/$', fleetactivitytracking.views.fatlink_personal_statistics_view,
name='auth_fatlink_view_personal_statistics_year'),
url(r'^fat/user/statistics/(?P<year>[0-9]+)/(?P<month>[0-9]+)/$',
fleetactivitytracking.views.fatlink_monthly_personal_statistics_view,
name='auth_fatlink_view_personal_statistics_month'),
url(r'^fat/user/(?P<char_id>[0-9]+)/statistics/(?P<year>[0-9]+)/(?P<month>[0-9]+)/$',
fleetactivitytracking.views.fatlink_monthly_personal_statistics_view,
name='auth_fatlink_view_user_statistics_month'),
url(r'^fat/create/$', fleetactivitytracking.views.create_fatlink_view, name='auth_create_fatlink_view'),
url(r'^fat/modify/$', fleetactivitytracking.views.modify_fatlink_view, name='auth_modify_fatlink_view'),
url(r'^fat/modify/(?P<hash>[a-zA-Z0-9_-]+)/([a-z0-9_-]+)$',
fleetactivitytracking.views.modify_fatlink_view),
url(r'^fat/link/$', fleetactivitytracking.views.fatlink_view, name='auth_click_fatlink_view'),
url(r'^fat/link/(?P<hash>[a-zA-Z0-9]+)/(?P<fatname>[a-z0-9_-]+)/$',
fleetactivitytracking.views.click_fatlink_view),
url(r'^permissions/', include(permissions_tool.urls))
)
# Append hooked service urls
services = get_hooks('services_hook')
for svc in services:
urlpatterns += svc().urlpatterns

View File

@@ -1,20 +0,0 @@
"""
WSGI config for alliance_auth project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "alliance_auth.settings")
# virtualenv wrapper, uncomment below to activate
# activate_env=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'env/bin/activate_this.py')
# execfile(activate_env, dict(__file__=activate_env))
application = get_wsgi_application()

View File

@@ -1,8 +1,7 @@
from __future__ import absolute_import, unicode_literals
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celeryapp import app as celery_app # noqa
__version__ = '1.15.4'
__version__ = '2.0b3'
NAME = 'Alliance Auth v%s' % __version__
default_app_config = 'allianceauth.apps.AllianceAuthConfig'

5
allianceauth/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class AllianceAuthConfig(AppConfig):
name = 'allianceauth'

View File

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

View File

@@ -0,0 +1,246 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import User as BaseUser, Permission as BasePermission
from django.utils.text import slugify
from django.db.models import Q
from allianceauth.services.hooks import ServicesHook
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed
from django.dispatch import receiver
from allianceauth.authentication.models import State, get_guest_state, CharacterOwnership, UserProfile
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):
"""
Make a admin action for the given service
:param service: services.hooks.ServicesHook
:return: fn to update services groups for the selected users
"""
def update_service_groups(modeladmin, request, queryset):
for user in queryset: # queryset filtering doesn't work here?
service.update_groups(user)
update_service_groups.__name__ = str('update_{}_groups'.format(slugify(service.name)))
update_service_groups.short_description = "Sync groups for selected {} accounts".format(service.title)
return update_service_groups
def make_service_hooks_sync_nickname_action(service):
"""
Make a sync_nickname admin action for the given service
:param service: services.hooks.ServicesHook
:return: fn to sync nickname for the selected users
"""
def sync_nickname(modeladmin, request, queryset):
for user in queryset: # queryset filtering doesn't work here?
service.sync_nickname(user)
sync_nickname.__name__ = str('sync_{}_nickname'.format(slugify(service.name)))
sync_nickname.short_description = "Sync nicknames for selected {} accounts".format(service.title)
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):
"""
Extending Django's UserAdmin model
"""
def get_actions(self, request):
actions = super(BaseUserAdmin, self).get_actions(request)
for hook in get_hooks('services_hook'):
svc = hook()
# Check update_groups is redefined/overloaded
if svc.update_groups.__module__ != ServicesHook.update_groups.__module__:
action = make_service_hooks_update_groups_action(svc)
actions[action.__name__] = (action,
action.__name__,
action.short_description)
# Create sync nickname action if service implements it
if svc.sync_nickname.__module__ != ServicesHook.sync_nickname.__module__:
action = make_service_hooks_sync_nickname_action(svc)
actions[action.__name__] = (action,
action.__name__,
action.short_description)
return actions
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)
class StateAdmin(admin.ModelAdmin):
fieldsets = (
(None, {
'fields': ('name', 'permissions', 'priority'),
}),
('Membership', {
'fields': ('public', 'member_characters', 'member_corporations', 'member_alliances'),
})
)
filter_horizontal = ['member_characters', 'member_corporations', 'member_alliances', 'permissions']
list_display = ('name', 'priority', 'user_count')
def has_delete_permission(self, request, obj=None):
if obj == get_guest_state():
return False
return super(StateAdmin, self).has_delete_permission(request, obj=obj)
def get_fieldsets(self, request, obj=None):
if obj == get_guest_state():
return (
(None, {
'fields': ('permissions', 'priority'),
}),
)
return super(StateAdmin, self).get_fieldsets(request, obj=obj)
@staticmethod
def user_count(obj):
return obj.userprofile_set.all().count()
@admin.register(CharacterOwnership)
class CharacterOwnershipAdmin(admin.ModelAdmin):
list_display = ('user', 'character')
search_fields = ('user__username', 'character__character_name', 'character__corporation_name', 'character__alliance_name')
readonly_fields = ('owner_hash', 'character')
def has_add_permission(self, request):
return False
class PermissionAdmin(admin.ModelAdmin):
actions = None
readonly_fields = [field.name for field in BasePermission._meta.fields]
list_display = ('admin_name', 'name', 'codename', 'content_type')
list_filter = ('content_type__app_label',)
@staticmethod
def admin_name(obj):
return str(obj)
def has_add_permission(self, request):
return False
def has_delete_permission(self, request, obj=None):
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
class User(BaseUser):
class Meta:
proxy = True
verbose_name = BaseUser._meta.verbose_name
verbose_name_plural = BaseUser._meta.verbose_name_plural
class Permission(BasePermission):
class Meta:
proxy = True
verbose_name = BasePermission._meta.verbose_name
verbose_name_plural = BasePermission._meta.verbose_name_plural
try:
admin.site.unregister(BaseUser)
finally:
admin.site.register(User, UserAdmin)
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

@@ -0,0 +1,12 @@
from django.apps import AppConfig
from django.core.checks import register, Tags
class AuthenticationConfig(AppConfig):
name = 'allianceauth.authentication'
label = 'authentication'
def ready(self):
super(AuthenticationConfig, self).ready()
from allianceauth.authentication import checks, signals
register(Tags.security)(checks.check_login_scopes_setting)

View File

@@ -0,0 +1,74 @@
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from .models import UserProfile, CharacterOwnership
class StateBackend(ModelBackend):
@staticmethod
def _get_state_permissions(user_obj):
profile_state_field = UserProfile._meta.get_field('state')
user_state_query = 'state__%s__user' % profile_state_field.related_query_name()
return Permission.objects.filter(**{user_state_query: user_obj})
def get_state_permissions(self, user_obj, obj=None):
return self._get_permissions(user_obj, obj, 'state')
def get_all_permissions(self, user_obj, obj=None):
if not user_obj.is_active or user_obj.is_anonymous or obj is not None:
return set()
if not hasattr(user_obj, '_perm_cache'):
user_obj._perm_cache = self.get_user_permissions(user_obj)
user_obj._perm_cache.update(self.get_group_permissions(user_obj))
user_obj._perm_cache.update(self.get_state_permissions(user_obj))
return user_obj._perm_cache
def authenticate(self, token=None):
if not token:
return None
try:
ownership = CharacterOwnership.objects.get(character__character_id=token.character_id)
if ownership.owner_hash == token.character_owner_hash:
return ownership.user
else:
ownership.delete()
return self.create_user(token)
except CharacterOwnership.DoesNotExist:
try:
# insecure legacy main check for pre-sso registration auth installs
profile = UserProfile.objects.get(main_character__character_id=token.character_id)
# attach an ownership
token.user = profile.user
CharacterOwnership.objects.create_by_token(token)
return profile.user
except UserProfile.DoesNotExist:
pass
return self.create_user(token)
def create_user(self, token):
username = self.iterate_username(token.character_name) # build unique username off character name
user = User.objects.create_user(username)
user.set_unusable_password() # prevent login via password
user.is_active = False # prevent login until email set
user.save()
token.user = 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.save()
return user
@staticmethod
def iterate_username(name):
name = str.replace(name, "'", "")
name = str.replace(name, ' ', '_')
if User.objects.filter(username__startswith=name).exists():
u = User.objects.filter(username__startswith=name)
num = len(u)
username = "%s_%s" % (name, num)
while u.filter(username=username).exists():
num += 1
username = "%s_%s" % (name, num)
else:
username = name
return username

View File

@@ -0,0 +1,12 @@
from django.core.checks import Error
from django.conf import settings
def check_login_scopes_setting(*args, **kwargs):
errors = []
try:
assert settings.LOGIN_TOKEN_SCOPES
except (AssertionError, AttributeError):
errors.append(Error('LOGIN_TOKEN_SCOPES setting cannot be blank.',
hint='SSO tokens used for logging in must require scopes to be refreshable.'))
return errors

View File

@@ -0,0 +1,37 @@
from django.conf.urls import include
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)

View File

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

View File

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

View File

@@ -0,0 +1,75 @@
import logging
from django.db import transaction
from django.db.models import Manager, QuerySet, Q
from allianceauth.eveonline.models import EveCharacter
logger = logging.getLogger(__name__)
def available_states_query(character):
query = Q(public=True)
if character.character_id:
query |= Q(member_characters__character_id=character.character_id)
if character.corporation_id:
query |= Q(member_corporations__corporation_id=character.corporation_id)
if character.alliance_id:
query |= Q(member_alliances__alliance_id=character.alliance_id)
return query
class CharacterOwnershipManager(Manager):
def create_by_token(self, token):
if not EveCharacter.objects.filter(character_id=token.character_id).exists():
EveCharacter.objects.create_character(token.character_id)
return self.create(character=EveCharacter.objects.get(character_id=token.character_id), user=token.user,
owner_hash=token.character_owner_hash)
class StateQuerySet(QuerySet):
def available_to_character(self, character):
return self.filter(available_states_query(character))
def available_to_user(self, user):
if user.profile.main_character:
return self.available_to_character(user.profile.main_character)
else:
return self.none()
def get_for_user(self, user):
states = self.available_to_user(user)
if states.exists():
return states[0]
else:
from allianceauth.authentication.models import get_guest_state
return get_guest_state()
def delete(self):
with transaction.atomic():
for state in self:
for profile in state.userprofile_set.all():
profile.assign_state(state=self.model.objects.exclude(pk=state.pk).get_for_user(profile.user))
super(StateQuerySet, self).delete()
class StateManager(Manager):
def get_queryset(self):
return StateQuerySet(self.model, using=self._db)
def available_to_character(self, character):
return self.get_queryset().available_to_character(character)
def available_to_user(self, user):
return self.get_queryset().available_to_user(user)
def get_for_character(self, character):
states = self.get_queryset().available_to_character(character)
if states.exists():
return states[0]
else:
from allianceauth.authentication.models import get_guest_state
return get_guest_state()
def get_for_user(self, user):
return self.get_queryset().get_for_user(user)

View File

@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-12 13:04
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('authentication', '0007_remove_authservicesinfo_is_blue'),
('eveonline', '0001_initial'),
('auth', '0001_initial'),
]
operations = [
]

View File

@@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2016-12-11 23:14
from __future__ import unicode_literals
from django.db import migrations
import logging
logger = logging.getLogger(__name__)
class Migration(migrations.Migration):
dependencies = [
('authentication', '0012_remove_add_delete_authservicesinfo_permissions'),
]
operations = [
# Remove fields
migrations.RemoveField(
model_name='authservicesinfo',
name='discord_uid',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='discourse_enabled',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='forum_username',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='ipboard_username',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='ips4_id',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='ips4_username',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='jabber_username',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='market_username',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='mumble_username',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='smf_username',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='teamspeak3_perm_key',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='teamspeak3_uid',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='xenforo_username',
),
]

View File

@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-09 23:19
from __future__ import unicode_literals
from django.db import migrations
def create_permission(apps, schema_editor):
User = apps.get_model('auth', 'User')
ContentType = apps.get_model('contenttypes', 'ContentType')
Permission = apps.get_model('auth', 'Permission')
ct = ContentType.objects.get_for_model(User)
Permission.objects.get_or_create(codename="view_fleetup", content_type=ct, name="view_fleetup")
class Migration(migrations.Migration):
dependencies = [
('authentication', '0013_service_modules'),
]
operations = [
migrations.RunPython(create_permission, migrations.RunPython.noop)
]

View File

@@ -0,0 +1,262 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-03-22 23:09
from __future__ import unicode_literals
import allianceauth.authentication.models
import django.db.models.deletion
from django.conf import settings
from django.contrib.auth.hashers import make_password
from django.db import migrations, models
def create_guest_state(apps, schema_editor):
State = apps.get_model('authentication', 'State')
State.objects.update_or_create(name='Guest', defaults={'priority': 0, 'public': True})
def create_member_state(apps, schema_editor):
Group = apps.get_model('auth', 'Group')
State = apps.get_model('authentication', 'State')
EveAllianceInfo = apps.get_model('eveonline', 'EveAllianceInfo')
EveCorporationInfo = apps.get_model('eveonline', 'EveCorporationInfo')
member_state_name = getattr(settings, 'DEFAULT_AUTH_GROUP', 'Member')
s = State.objects.update_or_create(name=member_state_name, defaults={'priority': 100, 'public': False})[0]
try:
# move group permissions to state
g = Group.objects.get(name=member_state_name)
[s.permissions.add(p.pk) for p in g.permissions.all()]
g.delete()
except Group.DoesNotExist:
pass
# auto-populate member IDs
CORP_IDS = getattr(settings, 'CORP_IDS', [])
ALLIANCE_IDS = getattr(settings, 'ALLIANCE_IDS', [])
[s.member_corporations.add(c.pk) for c in EveCorporationInfo.objects.filter(corporation_id__in=CORP_IDS)]
[s.member_alliances.add(a.pk) for a in EveAllianceInfo.objects.filter(alliance_id__in=ALLIANCE_IDS)]
def create_member_group(apps, schema_editor):
Group = apps.get_model('auth', 'Group')
State = apps.get_model('authentication', 'State')
member_state_name = getattr(settings, 'DEFAULT_AUTH_GROUP', 'Member')
try:
g = Group.objects.get(name=member_state_name)
# move permissions back
state = State.objects.get(name=member_state_name)
[g.permissions.add(p.pk) for p in state.permissions.all()]
# move users back
for profile in state.userprofile_set.all().select_related('user'):
profile.user.groups.add(g.pk)
except (Group.DoesNotExist, State.DoesNotExist):
pass
def create_blue_state(apps, schema_editor):
Group = apps.get_model('auth', 'Group')
State = apps.get_model('authentication', 'State')
EveAllianceInfo = apps.get_model('eveonline', 'EveAllianceInfo')
EveCorporationInfo = apps.get_model('eveonline', 'EveCorporationInfo')
blue_state_name = getattr(settings, 'DEFAULT_BLUE_GROUP', 'Blue')
s = State.objects.update_or_create(name=blue_state_name, defaults={'priority': 50, 'public': False})[0]
try:
# move group permissions to state
g = Group.objects.get(name=blue_state_name)
[s.permissions.add(p.pk) for p in g.permissions.all()]
g.permissions.clear()
except Group.DoesNotExist:
pass
# auto-populate blue member IDs
BLUE_CORP_IDS = getattr(settings, 'BLUE_CORP_IDS', [])
BLUE_ALLIANCE_IDS = getattr(settings, 'BLUE_ALLIANCE_IDS', [])
[s.member_corporations.add(c.pk) for c in EveCorporationInfo.objects.filter(corporation_id__in=BLUE_CORP_IDS)]
[s.member_alliances.add(a.pk) for a in EveAllianceInfo.objects.filter(alliance_id__in=BLUE_ALLIANCE_IDS)]
def create_blue_group(apps, schema_editor):
Group = apps.get_model('auth', 'Group')
State = apps.get_model('authentication', 'State')
blue_state_name = getattr(settings, 'DEFAULT_BLUE_GROUP', 'Blue')
try:
g = Group.objects.get(name=blue_state_name)
# move permissions back
state = State.objects.get(name=blue_state_name)
[g.permissions.add(p.pk) for p in state.permissions.all()]
# move users back
for profile in state.userprofile_set.all().select_related('user'):
profile.user.groups.add(g.pk)
except (Group.DoesNotExist, State.DoesNotExist):
pass
def populate_ownerships(apps, schema_editor):
Token = apps.get_model('esi', 'Token')
CharacterOwnership = apps.get_model('authentication', 'CharacterOwnership')
EveCharacter = apps.get_model('eveonline', 'EveCharacter')
unique_character_owners = [t['character_id'] for t in
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()]
tokens = Token.objects.filter(character_id__in=unique_character_owners)
for c_id in unique_character_owners:
# find newest refreshable token and use it as basis for CharacterOwnership
ts = tokens.filter(character_id=c_id).exclude(refresh_token__isnull=True).order_by('created')
if ts.exists():
token = ts[0]
char = EveCharacter.objects.get(character_id=token.character_id)
CharacterOwnership.objects.create(user_id=token.user_id, character_id=char.id, owner_hash=token.character_owner_hash)
def create_profiles(apps, schema_editor):
AuthServicesInfo = apps.get_model('authentication', 'AuthServicesInfo')
State = apps.get_model('authentication', 'State')
UserProfile = apps.get_model('authentication', 'UserProfile')
EveCharacter = apps.get_model('eveonline', 'EveCharacter')
# grab AuthServicesInfo if they have a unique main_char_id and the EveCharacter exists
unique_mains = [auth['main_char_id'] for auth in
AuthServicesInfo.objects.exclude(main_char_id='').values('main_char_id').annotate(
n=models.Count('main_char_id')) if
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')
for auth in auths:
# carry states and mains forward
state = State.objects.get(name=auth.state if auth.state else 'Guest')
char = EveCharacter.objects.get(character_id=auth.main_char_id)
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'):
# prepare empty profiles
state = State.objects.get(name='Guest')
UserProfile.objects.create(user=auth.user, state=state)
def recreate_authservicesinfo(apps, schema_editor):
AuthServicesInfo = apps.get_model('authentication', 'AuthServicesInfo')
UserProfile = apps.get_model('authentication', 'UserProfile')
User = apps.get_model('auth', 'User')
# recreate all missing AuthServicesInfo models
AuthServicesInfo.objects.bulk_create([AuthServicesInfo(user_id=u.pk) for u in User.objects.all()])
# repopulate main characters
for profile in UserProfile.objects.exclude(main_character__isnull=True).select_related('user', 'main_character'):
AuthServicesInfo.objects.update_or_create(user=profile.user,
defaults={'main_char_id': profile.main_character.character_id})
# repopulate states we understand
for profile in UserProfile.objects.exclude(state__name='Guest').filter(
state__name__in=['Member', 'Blue']).select_related('user', 'state'):
AuthServicesInfo.objects.update_or_create(user=profile.user, defaults={'state': profile.state.name})
def disable_passwords(apps, schema_editor):
User = apps.get_model('auth', 'User')
for u in User.objects.exclude(is_staff=True):
# remove passwords for non-staff users to prevent password-based authentication
# set_unusable_password is unavailable in migrations because :reasons:
u.password = make_password(None)
u.save()
class Migration(migrations.Migration):
dependencies = [
('auth', '0008_alter_user_username_max_length'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('eveonline', '0008_remove_apikeys'),
('authentication', '0014_fleetup_permission'),
('esi', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='CharacterOwnership',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('owner_hash', models.CharField(max_length=28, unique=True)),
('character', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='character_ownership', to='eveonline.EveCharacter')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='character_ownerships', to=settings.AUTH_USER_MODEL)),
],
options={
'default_permissions': ('change', 'delete'),
'ordering': ['user', 'character__character_name'],
},
),
migrations.CreateModel(
name='State',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=20, unique=True)),
('priority', models.IntegerField(help_text='Users get assigned the state with the highest priority available to them.', unique=True)),
('public', models.BooleanField(default=False, help_text='Make this state available to any character.')),
('member_alliances', models.ManyToManyField(blank=True, help_text='Alliances to whose members this state is available.', to='eveonline.EveAllianceInfo')),
('member_characters', models.ManyToManyField(blank=True, help_text='Characters to which this state is available.', to='eveonline.EveCharacter')),
('member_corporations', models.ManyToManyField(blank=True, help_text='Corporations to whose members this state is available.', to='eveonline.EveCorporationInfo')),
('permissions', models.ManyToManyField(blank=True, to='auth.Permission')),
],
options={
'ordering': ['-priority'],
},
),
migrations.CreateModel(
name='UserProfile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('main_character', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='eveonline.EveCharacter')),
('state', models.ForeignKey(default=allianceauth.authentication.models.get_guest_state_pk, on_delete=django.db.models.deletion.SET_DEFAULT, to='authentication.State')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
],
options={
'default_permissions': ('change',),
},
),
migrations.RunPython(create_guest_state, migrations.RunPython.noop),
migrations.RunPython(create_member_state, create_member_group),
migrations.RunPython(create_blue_state, create_blue_group),
migrations.RunPython(populate_ownerships, migrations.RunPython.noop),
migrations.RunPython(create_profiles, recreate_authservicesinfo),
migrations.RemoveField(
model_name='authservicesinfo',
name='user',
),
migrations.DeleteModel(
name='AuthServicesInfo',
),
migrations.RunPython(disable_passwords, migrations.RunPython.noop),
migrations.CreateModel(
name='Permission',
fields=[
],
options={
'proxy': True,
'verbose_name': 'permission',
'verbose_name_plural': 'permissions',
},
bases=('auth.permission',),
managers=[
('objects', django.contrib.auth.models.PermissionManager()),
],
),
migrations.CreateModel(
name='User',
fields=[
],
options={
'proxy': True,
'verbose_name': 'user',
'verbose_name_plural': 'users',
},
bases=('auth.user',),
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

View File

@@ -0,0 +1,98 @@
import logging
from django.contrib.auth.models import User, Permission
from django.db import models, transaction
from django.utils.translation import ugettext_lazy as _
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo
from allianceauth.notifications import notify
from .managers import CharacterOwnershipManager, StateManager
logger = logging.getLogger(__name__)
class State(models.Model):
name = models.CharField(max_length=20, unique=True)
permissions = models.ManyToManyField(Permission, blank=True)
priority = models.IntegerField(unique=True,
help_text="Users get assigned the state with the highest priority available to them.")
member_characters = models.ManyToManyField(EveCharacter, blank=True,
help_text="Characters to which this state is available.")
member_corporations = models.ManyToManyField(EveCorporationInfo, blank=True,
help_text="Corporations to whose members this state is available.")
member_alliances = models.ManyToManyField(EveAllianceInfo, blank=True,
help_text="Alliances to whose members this state is available.")
public = models.BooleanField(default=False, help_text="Make this state available to any character.")
objects = StateManager()
class Meta:
ordering = ['-priority']
def __str__(self):
return self.name
def available_to_character(self, character):
return self in State.objects.available_to_character(character)
def available_to_user(self, user):
return self in State.objects.available_to_user(user)
def delete(self, **kwargs):
with transaction.atomic():
for profile in self.userprofile_set.all():
profile.assign_state(state=State.objects.exclude(pk=self.pk).get_for_user(profile.user))
super(State, self).delete(**kwargs)
def get_guest_state():
try:
return State.objects.get(name='Guest')
except State.DoesNotExist:
return State.objects.create(name='Guest', priority=0, public=True)
def get_guest_state_pk():
return get_guest_state().pk
class UserProfile(models.Model):
class Meta:
default_permissions = ('change',)
user = models.OneToOneField(User, related_name='profile', on_delete=models.CASCADE)
main_character = models.OneToOneField(EveCharacter, blank=True, null=True, on_delete=models.SET_NULL)
state = models.ForeignKey(State, on_delete=models.SET_DEFAULT, default=get_guest_state_pk)
def assign_state(self, state=None, commit=True):
if not state:
state = State.objects.get_for_user(self.user)
if self.state != state:
self.state = state
if commit:
logger.info('Updating {} state to {}'.format(self.user, self.state))
self.save(update_fields=['state'])
notify(self.user, _('State Changed'),
_('Your user state has been changed to %(state)s') % ({'state': state}),
'info')
from allianceauth.authentication.signals import state_changed
state_changed.send(sender=self.__class__, user=self.user, state=self.state)
def __str__(self):
return str(self.user)
class CharacterOwnership(models.Model):
class Meta:
default_permissions = ('change', 'delete')
ordering = ['user', 'character__character_name']
character = models.OneToOneField(EveCharacter, on_delete=models.CASCADE, related_name='character_ownership')
owner_hash = models.CharField(max_length=28, unique=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='character_ownerships')
objects = CharacterOwnershipManager()
def __str__(self):
return "%s: %s" % (self.user, self.character)

View File

@@ -0,0 +1,155 @@
import logging
from .models import CharacterOwnership, UserProfile, get_guest_state, State
from django.contrib.auth.models import User
from django.db.models import Q
from django.db.models.signals import pre_save, post_save, pre_delete, m2m_changed
from django.dispatch import receiver, Signal
from esi.models import Token
from allianceauth.eveonline.models import EveCharacter
logger = logging.getLogger(__name__)
state_changed = Signal(providing_args=['user', 'state'])
def trigger_state_check(state):
# evaluate all current members to ensure they still have access
for profile in state.userprofile_set.all():
profile.assign_state()
# we may now be available to others with lower states
check_states = State.objects.filter(priority__lt=state.priority)
for profile in UserProfile.objects.filter(state__in=check_states):
if state.available_to_user(profile.user):
profile.state = state
profile.save(update_fields=['state'])
state_changed.send(sender=state.__class__, user=profile.user, state=state)
@receiver(m2m_changed, sender=State.member_characters.through)
def state_member_characters_changed(sender, instance, action, *args, **kwargs):
if action.startswith('post_'):
logger.debug('State {} member characters changed. Re-evaluating membership.'.format(instance))
trigger_state_check(instance)
@receiver(m2m_changed, sender=State.member_corporations.through)
def state_member_corporations_changed(sender, instance, action, *args, **kwargs):
if action.startswith('post_'):
logger.debug('State {} member corporations changed. Re-evaluating membership.'.format(instance))
trigger_state_check(instance)
@receiver(m2m_changed, sender=State.member_alliances.through)
def state_member_alliances_changed(sender, instance, action, *args, **kwargs):
if action.startswith('post_'):
logger.debug('State {} member alliances changed. Re-evaluating membership.'.format(instance))
trigger_state_check(instance)
@receiver(post_save, sender=State)
def state_saved(sender, instance, *args, **kwargs):
logger.debug('State {} saved. Re-evaluating membership.'.format(instance))
trigger_state_check(instance)
# Is there a smarter way to intercept pre_save with a diff main_character or state?
@receiver(post_save, sender=UserProfile)
def reassess_on_profile_save(sender, instance, created, *args, **kwargs):
# catches post_save from profiles to trigger necessary service and state checks
if not created:
update_fields = kwargs.pop('update_fields', []) or []
if 'state' not in update_fields:
logger.debug('Profile for {} saved without state change. Re-evaluating state.'.format(instance.user))
instance.assign_state()
@receiver(post_save, sender=User)
def create_required_models(sender, instance, created, *args, **kwargs):
# ensure all users have a model
if created:
logger.debug('User {} created. Creating default UserProfile.'.format(instance))
UserProfile.objects.get_or_create(user=instance)
@receiver(post_save, sender=Token)
def record_character_ownership(sender, instance, created, *args, **kwargs):
if created:
logger.debug('New token for {0} character {1} saved. Evaluating ownership.'.format(instance.user,
instance.character_name))
if instance.user:
query = Q(owner_hash=instance.character_owner_hash) & Q(user=instance.user)
else:
query = Q(owner_hash=instance.character_owner_hash)
# purge ownership records if the hash or auth user account has changed
CharacterOwnership.objects.filter(character__character_id=instance.character_id).exclude(query).delete()
# create character if needed
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)
char = EveCharacter.objects.get(character_id=instance.character_id)
# check if we need to create ownership
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,
defaults={'owner_hash': instance.character_owner_hash,
'user': instance.user})
@receiver(pre_delete, sender=CharacterOwnership)
def validate_main_character(sender, instance, *args, **kwargs):
if instance.user.profile.main_character == instance.character:
logger.debug("Ownership of a main character {0} has been revoked. Resetting {1} main character.".format(
instance.character, instance.user))
# clear main character as user no longer owns them
instance.user.profile.main_character = None
instance.user.profile.save()
@receiver(pre_delete, sender=Token)
def validate_main_character_token(sender, instance, *args, **kwargs):
if UserProfile.objects.filter(main_character__character_id=instance.character_id).exists():
logger.debug(
"Token for a main character {0} is being deleted. Ensuring there are valid tokens to refresh.".format(
instance.character_name))
profile = UserProfile.objects.get(main_character__character_id=instance.character_id)
if not Token.objects.filter(character_id=instance.character_id).filter(user=profile.user).exclude(
pk=instance.pk).require_valid().exists():
logger.debug(
"No remaining tokens to validate {0} ownership of main character {1}. Resetting main character.".format(
profile.user, profile.main_character))
# clear main character as we can no longer verify ownership
profile.main_character = None
profile.save()
@receiver(pre_save, sender=User)
def assign_state_on_active_change(sender, instance, *args, **kwargs):
# set to guest state if inactive, assign proper state if reactivated
if instance.pk:
old_instance = User.objects.get(pk=instance.pk)
if old_instance.is_active != instance.is_active:
if instance.is_active:
logger.debug("User {0} has been activated. Assigning state.".format(instance))
instance.profile.assign_state()
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.save(update_fields=['state'])
@receiver(post_save, sender=EveCharacter)
def check_state_on_character_update(sender, instance, *args, **kwargs):
# if this is a main character updating, check that user's state
try:
logger.debug("Character {0} has been saved. Assessing owner's state for changes.".format(instance))
instance.userprofile.assign_state()
except UserProfile.DoesNotExist:
logger.debug("Character {0} is not a main character. No state assessment required.".format(instance))
pass

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,37 @@
import logging
from esi.errors import TokenExpiredError, TokenInvalidError
from esi.models import Token
from celery import shared_task
from allianceauth.authentication.models import CharacterOwnership
logger = logging.getLogger(__name__)
@shared_task
def check_character_ownership(owner_hash):
tokens = Token.objects.filter(character_owner_hash=owner_hash)
if tokens:
for t in tokens:
old_hash = t.character_owner_hash
try:
t.update_token_data(commit=False)
except (TokenExpiredError, TokenInvalidError):
t.delete()
continue
if t.character_owner_hash == old_hash:
break
else:
logger.info('Character %s has changed ownership. Revoking %s tokens.' % (t.character_name, tokens.count()))
tokens.delete()
else:
logger.info('No tokens found with owner hash %s. Revoking ownership.' % owner_hash)
CharacterOwnership.objects.filter(owner_hash=owner_hash).delete()
@shared_task
def check_all_character_ownership():
for c in CharacterOwnership.objects.all().only('owner_hash'):
check_character_ownership.delay(c.owner_hash)

View File

@@ -0,0 +1,117 @@
{% extends "allianceauth/base.html" %}
{% load staticfiles %}
{% load i18n %}
{% block page_title %}{% trans "Dashboard" %}{% endblock %}
{% block content %}
<h1 class="page-header text-center">{% trans "Dashboard" %}</h1>
{% if user.is_staff %}
{% include 'allianceauth/admin-status/include.html' %}
{% endif %}
<div class="col-sm-12">
<div class="row vertical-flexbox-row">
<div class="col-sm-6 text-center">
<div class="panel panel-primary" style="height:100%">
<div class="panel-heading"><h3 class="panel-title">{% trans "Main Character" %}</h3></div>
<div class="panel-body">
{% if request.user.profile.main_character %}
{% with request.user.profile.main_character as main %}
<div class="col-lg-4 col-sm-2">
<table class="table">
<tr>
<td class="text-center"><img class="ra-avatar"
src="{{ main.portrait_url_128 }}">
</td>
</tr>
<tr>
<td class="text-center">{{ main.character_name }}</td>
</tr>
</table>
</div>
<div class="col-lg-4 col-sm-2">
<table class="table">
<tr>
<td class="text-center"><img class="ra-avatar"
src="https://image.eveonline.com/Corporation/{{ main.corporation_id }}_128.png">
</td>
</tr>
<tr>
<td class="text-center">{{ main.corporation_name }}</td>
</tr>
</table>
</div>
<div class="col-lg-4 col-sm-2">
{% if main.alliance_id %}
<table class="table">
<tr>
<td class="text-center"><img class="ra-avatar"
src="https://image.eveonline.com/Alliance/{{ main.alliance_id }}_128.png">
</td>
</tr>
<tr>
<td class="text-center">{{ main.alliance_name }}</td>
<tr>
</table>
{% endif %}
</div>
{% endwith %}
{% else %}
<div class="alert alert-danger" role="alert">{% trans "No main character set." %}</div>
{% endif %}
<div class="clearfix"></div>
<div class="col-xs-6">
<a href="{% url 'authentication:add_character' %}" class="btn btn-block btn-info"
title="Add Character">{% trans 'Add Character' %}</a>
</div>
<div class="col-xs-6">
<a href="{% url 'authentication:change_main_character' %}" class="btn btn-block btn-info"
title="Change Main Character">{% trans "Change Main" %}</a>
</div>
</div>
</div>
</div>
<div class="col-sm-6 text-center">
<div class="panel panel-success" style="height:100%">
<div class="panel-heading"><h3 class="panel-title">{% trans "Groups" %}</h3></div>
<div class="panel-body">
<div style="height: 240px;overflow:-moz-scrollbars-vertical;overflow-y:auto;">
<table class="table table-striped">
{% for group in user.groups.all %}
<tr>
<td>{{ group.name }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
</div>
</div>
<div class="clearfix"></div>
<div class="panel panel-default">
<div class="panel-heading" style="display:flex;"><h3 class="panel-title">{% trans 'Characters' %}</h3></div>
<div class="panel-body">
<table class="table table-hover">
<tr>
<th class="text-center"></th>
<th class="text-center">{% trans 'Name' %}</th>
<th class="text-center">{% trans 'Corp' %}</th>
<th class="text-center">{% trans 'Alliance' %}</th>
</tr>
{% for ownership in request.user.character_ownerships.all %}
{% with ownership.character as char %}
<tr>
<td class="text-center"><img class="ra-avatar img-circle" src="{{ char.portrait_url_32 }}">
</td>
<td class="text-center">{{ char.character_name }}</td>
<td class="text-center">{{ char.corporation_name }}</td>
<td class="text-center">{{ char.alliance_name }}</td>
</tr>
{% endwith %}
{% endfor %}
</table>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,51 @@
{% load static %}
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">
{% include 'allianceauth/icons.html' %}
<title>{% block title %}{{ SITE_NAME }}{% endblock %}</title>
{% include 'bundles/bootstrap-css.html' %}
{% include 'bundles/fontawesome.html' %}
{% block extra_include %}
{% endblock %}
<style>
body {
background: url('{% static 'authentication/img/background.jpg' %}') no-repeat center center fixed;
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
background-size: cover;
}
.panel-transparent {
background: rgba(48, 48, 48, 0.7);
color: #ffffff;
}
.panel-body {
}
#lang-select {
width: 40%;
margin-left: auto;
margin-right: auto;
}
{% block extra_style %}
{% endblock %}
</style>
</head>
<body>
<div class="container" style="margin-top:150px">
{% block content %}
{% endblock %}
</div>
</body>
</html>

View File

@@ -0,0 +1,14 @@
{% load i18n %}
<div class="dropdown">
<form action="{% url 'set_language' %}" method="post">
{% csrf_token %}
<select onchange="this.form.submit()" class="form-control" id="lang-select" name="language">
{% get_language_info_list for LANGUAGES as languages %}
{% for language in languages %}
<option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected="selected"{% endif %}>
{{ language.name_local }} ({{ language.code }})
</option>
{% endfor %}
</select>
</form>
</div>

View File

@@ -0,0 +1,10 @@
{% extends 'public/middle_box.html' %}
{% load static %}
{% block page_title %}Login{% endblock %}
{% block middle_box_content %}
<p style="text-align:center">
<a href="{% url 'auth_sso_login' %}">
<img src="{% static 'img/sso/EVE_SSO_Login_Buttons_Large_Black.png' %}" border=0>
</a>
</p>
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% extends 'public/base.html' %}
{% load static %}
{% block title %}Login{% endblock %}
{% block content %}
<div class="col-md-4 col-md-offset-4">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.level_tag}}">{{ message }}</div>
{% endfor %}
{% endif %}
<div class="panel panel-default panel-transparent">
<div class="panel-body">
<div class="col-md-12">
{% block middle_box_content %}
{% endblock %}
</div>
</div>
{% include 'public/lang_select.html' %}
</div>
</div>
{% endblock %}
{% block extra_include %}
{% include 'bundles/bootstrap-js.html' %}
{% endblock %}

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
You're receiving this email because someone has entered this email address while registering for an account on {{ site.domain }}
If this was you, please go to the following URL to confirm your email address:
{{ scheme }}://{{ url }}
This link will expire in {{ expiration_days }} day(s).
If this was not you, it is safe to ignore this email.

View File

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

View File

@@ -0,0 +1,14 @@
{% extends 'public/middle_box.html' %}
{% load bootstrap %}
{% load i18n %}
{% load static %}
{% block page_title %}Register{% endblock %}
{% block middle_box_content %}
<form class="form-signin" role="form" action="" method="POST">
{% csrf_token %}
{{ form|bootstrap }}
<br/>
<button class="btn btn-lg btn-primary btn-block" type="submit">{% trans "Submit" %}</button>
<br/>
</form>
{% endblock %}

View File

@@ -0,0 +1,343 @@
from unittest import mock
from django.test import TestCase
from django.contrib.auth.models import User
from allianceauth.tests.auth_utils import AuthUtils
from .models import CharacterOwnership, UserProfile, State, get_guest_state
from .backends import StateBackend
from .tasks import check_character_ownership
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo
from esi.models import Token
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 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):
@classmethod
def setUpTestData(cls):
cls.main_character = EveCharacter.objects.create(
character_id=1,
character_name='Main Character',
corporation_id=1,
corporation_name='Corp',
corporation_ticker='CORP',
)
cls.alt_character = EveCharacter.objects.create(
character_id=2,
character_name='Alt Character',
corporation_id=1,
corporation_name='Corp',
corporation_ticker='CORP',
)
cls.unclaimed_character = EveCharacter.objects.create(
character_id=3,
character_name='Unclaimed Character',
corporation_id=1,
corporation_name='Corp',
corporation_ticker='CORP',
)
cls.user = AuthUtils.create_user('test_user', disconnect_signals=True)
AuthUtils.disconnect_signals()
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')
UserProfile.objects.update_or_create(user=cls.user, defaults={'main_character': cls.main_character})
AuthUtils.connect_signals()
def test_authenticate_main_character(self):
t = Token(character_id=self.main_character.character_id, character_owner_hash='1')
user = StateBackend().authenticate(token=t)
self.assertEquals(user, self.user)
def test_authenticate_alt_character(self):
t = Token(character_id=self.alt_character.character_id, character_owner_hash='2')
user = StateBackend().authenticate(token=t)
self.assertEquals(user, self.user)
def test_authenticate_unclaimed_character(self):
t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='3')
user = StateBackend().authenticate(token=t)
self.assertNotEqual(user, self.user)
self.assertEqual(user.username, 'Unclaimed_Character')
self.assertEqual(user.profile.main_character, self.unclaimed_character)
def test_iterate_username(self):
t = Token(character_id=self.unclaimed_character.character_id,
character_name=self.unclaimed_character.character_name, character_owner_hash='3')
username = StateBackend().authenticate(token=t).username
t.character_owner_hash = '4'
username_1 = StateBackend().authenticate(token=t).username
t.character_owner_hash = '5'
username_2 = StateBackend().authenticate(token=t).username
self.assertNotEqual(username, username_1, username_2)
self.assertTrue(username_1.endswith('_1'))
self.assertTrue(username_2.endswith('_2'))
class CharacterOwnershipTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = AuthUtils.create_user('user', disconnect_signals=True)
cls.alt_user = AuthUtils.create_user('alt_user', disconnect_signals=True)
cls.character = EveCharacter.objects.create(
character_id=1,
character_name='Main Character',
corporation_id=1,
corporation_name='Corp',
corporation_ticker='CORP',
)
def test_create_ownership(self):
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(character=self.character)
self.assertEquals(co.user, self.user)
self.assertEquals(co.owner_hash, '1')
def test_transfer_ownership(self):
Token.objects.create(
user=self.user,
character_id=self.character.character_id,
character_name=self.character.character_name,
character_owner_hash='1',
)
Token.objects.create(
user=self.alt_user,
character_id=self.character.character_id,
character_name=self.character.character_name,
character_owner_hash='2',
)
co = CharacterOwnership.objects.get(character=self.character)
self.assertNotEqual(self.user, co.user)
self.assertEquals(self.alt_user, co.user)
def test_clear_main_character(self):
Token.objects.create(
user=self.user,
character_id=self.character.character_id,
character_name=self.character.character_name,
character_owner_hash='1',
)
self.user.profile.main_character = self.character
self.user.profile.save()
Token.objects.create(
user=self.alt_user,
character_id=self.character.character_id,
character_name=self.character.character_name,
character_owner_hash='2',
)
self.user = User.objects.get(pk=self.user.pk)
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):
@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.guest_state = get_guest_state()
cls.test_character = EveCharacter.objects.get(character_id='1')
cls.test_corporation = EveCorporationInfo.objects.create(corporation_id='1', corporation_name='Test Corp',
corporation_ticker='TEST', member_count=1)
cls.test_alliance = EveAllianceInfo.objects.create(alliance_id='1', alliance_name='Test Alliance',
alliance_ticker='TEST', executor_corp_id='1')
cls.member_state = State.objects.create(
name='Test Member',
priority=150,
)
def _refresh_user(self):
self.user = User.objects.get(pk=self.user.pk)
def test_state_assignment_on_character_change(self):
self.member_state.member_characters.add(self.test_character)
self._refresh_user()
self.assertEquals(self.user.profile.state, self.member_state)
self.member_state.member_characters.remove(self.test_character)
self._refresh_user()
self.assertEquals(self.user.profile.state, self.guest_state)
def test_state_assignment_on_corporation_change(self):
self.member_state.member_corporations.add(self.test_corporation)
self._refresh_user()
self.assertEquals(self.user.profile.state, self.member_state)
self.member_state.member_corporations.remove(self.test_corporation)
self._refresh_user()
self.assertEquals(self.user.profile.state, self.guest_state)
def test_state_assignment_on_alliance_addition(self):
self.member_state.member_alliances.add(self.test_alliance)
self._refresh_user()
self.assertEquals(self.user.profile.state, self.member_state)
self.member_state.member_alliances.remove(self.test_alliance)
self._refresh_user()
self.assertEquals(self.user.profile.state, self.guest_state)
def test_state_assignment_on_higher_priority_state_creation(self):
self.member_state.member_characters.add(self.test_character)
higher_state = State.objects.create(
name='Higher State',
priority=200,
)
higher_state.member_characters.add(self.test_character)
self._refresh_user()
self.assertEquals(higher_state, self.user.profile.state)
higher_state.member_characters.clear()
self._refresh_user()
self.assertEquals(self.member_state, self.user.profile.state)
self.member_state.member_characters.clear()
def test_state_assignment_on_lower_priority_state_creation(self):
self.member_state.member_characters.add(self.test_character)
lower_state = State.objects.create(
name='Lower State',
priority=125,
)
lower_state.member_characters.add(self.test_character)
self._refresh_user()
self.assertEquals(self.member_state, self.user.profile.state)
lower_state.member_characters.clear()
self._refresh_user()
self.assertEquals(self.member_state, self.user.profile.state)
self.member_state.member_characters.clear()
def test_state_assignment_on_priority_change(self):
self.member_state.member_characters.add(self.test_character)
lower_state = State.objects.create(
name='Lower State',
priority=125,
)
lower_state.member_characters.add(self.test_character)
self._refresh_user()
lower_state.priority = 500
lower_state.save()
self._refresh_user()
self.assertEquals(lower_state, self.user.profile.state)
lower_state.priority = 125
lower_state.save()
self._refresh_user()
self.assertEquals(self.member_state, self.user.profile.state)
def test_state_assignment_on_state_deletion(self):
self.member_state.member_characters.add(self.test_character)
higher_state = State.objects.create(
name='Higher State',
priority=200,
)
higher_state.member_characters.add(self.test_character)
self._refresh_user()
self.assertEquals(higher_state, self.user.profile.state)
higher_state.delete()
self.assertFalse(State.objects.filter(name='Higher State').count())
self._refresh_user()
self.assertEquals(self.member_state, self.user.profile.state)
def test_state_assignment_on_public_toggle(self):
self.member_state.member_characters.add(self.test_character)
higher_state = State.objects.create(
name='Higher State',
priority=200,
)
self._refresh_user()
self.assertEquals(self.member_state, self.user.profile.state)
higher_state.public = True
higher_state.save()
self._refresh_user()
self.assertEquals(higher_state, self.user.profile.state)
higher_state.public = False
higher_state.save()
self._refresh_user()
self.assertEquals(self.member_state, self.user.profile.state)
def test_state_assignment_on_active_changed(self):
self.member_state.member_characters.add(self.test_character)
self.user.is_active = False
self.user.save()
self._refresh_user()
self.assertEquals(self.user.profile.state, self.guest_state)
self.user.is_active = True
self.user.save()
self._refresh_user()
self.assertEquals(self.user.profile.state, self.member_state)

View File

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

View File

@@ -0,0 +1,157 @@
import logging
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import login, authenticate
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.core import signing
from django.urls import reverse
from django.shortcuts import redirect
from django.utils.translation import ugettext_lazy as _
from esi.decorators import token_required
from esi.models import Token
from registration.backends.hmac.views import RegistrationView as BaseRegistrationView, \
ActivationView as BaseActivationView, REGISTRATION_SALT
from registration.signals import user_registered
from .models import CharacterOwnership
from .forms import RegistrationForm
logger = logging.getLogger(__name__)
@login_required
@token_required(scopes=settings.LOGIN_TOKEN_SCOPES)
def main_character_change(request, token):
logger.debug("main_character_change called by user %s for character %s" % (request.user, token.character_name))
try:
co = CharacterOwnership.objects.get(character__character_id=token.character_id, user=request.user)
except CharacterOwnership.DoesNotExist:
if not CharacterOwnership.objects.filter(character__character_id=token.character_id).exists():
co = CharacterOwnership.objects.create_by_token(token)
else:
messages.error(request, 'Cannot change main character to %(char)s: character owned by a different account.' % ({'char': token.character_name}))
co = None
if co:
request.user.profile.main_character = co.character
request.user.profile.save(update_fields=['main_character'])
messages.success(request, _('Changed main character to %(char)s') % {"char": co.character})
logger.info('Changed user %(user)s main character to %(char)s' % ({'user': request.user, 'char': co.character}))
return redirect("authentication:dashboard")
@token_required(new=True, scopes=settings.LOGIN_TOKEN_SCOPES)
def add_character(request, token):
if CharacterOwnership.objects.filter(character__character_id=token.character_id).filter(
owner_hash=token.character_owner_hash).filter(user=request.user).exists():
messages.success(request, _('Added %(name)s to your account.'% ({'name': token.character_name})))
else:
messages.error(request, _('Failed to add %(name)s to your account: they already have an account.' % ({'name': token.character_name})))
return redirect('authentication:dashboard')
"""
Override the HMAC two-step registration view to accommodate the three-step registration required.
Step 1: OAuth token to create user and profile.
Step 2: Get email and send activation link (but do not save email).
Step 3: Get link, save email and activate.
Step 1 is necessary to automatically assign character ownership and a main character, both of which require a saved User
model - this means the ensuing registration form cannot create the user because it already exists.
Email is not saved to the user model in Step 2 as a way of differentiating users who have not yet completed registration
(is_active=False) and users who have been disabled by an admin (is_active=False, email present).
Because of this, the email address needs to be assigned in Step 3 after clicking the link, which means the link must
have the email address embedded much like the username. Key creation and decoding is overridden to support this action.
"""
# Step 1
@token_required(new=True, scopes=settings.LOGIN_TOKEN_SCOPES)
def sso_login(request, token):
user = authenticate(token=token)
if user:
token.user = user
if Token.objects.exclude(pk=token.pk).equivalent_to(token).require_valid().exists():
token.delete()
else:
token.save()
if user.is_active:
login(request, user)
return redirect(request.POST.get('next', request.GET.get('next', 'authentication:dashboard')))
elif not user.email:
# 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
class RegistrationView(BaseRegistrationView):
form_class = RegistrationForm
success_url = 'authentication:dashboard'
def dispatch(self, *args, **kwargs):
# 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(
pk=self.request.session.get('registration_uid')).exists():
messages.error(self.request, _('Registration token has expired.'))
return redirect(settings.LOGIN_URL)
return super(RegistrationView, self).dispatch(*args, **kwargs)
def register(self, form):
user = User.objects.get(pk=self.request.session.get('registration_uid'))
user.email = form.cleaned_data['email']
user_registered.send(self.__class__, user=user, request=self.request)
# Go to Step 3
self.send_activation_email(user)
return user
def get_activation_key(self, user):
return signing.dumps(obj=[getattr(user, User.USERNAME_FIELD), user.email], salt=REGISTRATION_SALT)
def get_email_context(self, activation_key):
context = super(RegistrationView, self).get_email_context(activation_key)
context['url'] = context['site'].domain + reverse('registration_activate', args=[activation_key])
return context
# Step 3
class ActivationView(BaseActivationView):
def validate_key(self, activation_key):
try:
dump = signing.loads(activation_key, salt=REGISTRATION_SALT,
max_age=settings.ACCOUNT_ACTIVATION_DAYS * 86400)
return dump
except signing.BadSignature:
return None
def activate(self, *args, **kwargs):
dump = self.validate_key(kwargs.get('activation_key'))
if dump:
user = self.get_user(dump[0])
if user:
user.email = dump[1]
user.is_active = True
user.save()
return user
return False
def registration_complete(request):
messages.success(request, _('Sent confirmation email. Please follow the link to confirm your email address.'))
return redirect('authentication:login')
def activation_complete(request):
messages.success(request, _('Confirmed your email address. Please login to continue.'))
return redirect('authentication:dashboard')
def registration_closed(request):
messages.error(request, _('Registraion of new accounts it not allowed at this time.'))
return redirect('authentication:login')

View File

@@ -0,0 +1,103 @@
#!/usr/bin/env python
import os
import shutil
from optparse import OptionParser
from django.core.management import call_command
from django.core.management.commands.startproject import Command as BaseStartProject
class StartProject(BaseStartProject):
def add_arguments(self, parser):
super().add_arguments(parser)
parser.add_argument('--python', help='The path to the python executable.')
parser.add_argument('--celery', help='The path to the celery executable.')
parser.add_argument('--gunicorn', help='The path to the gunicorn executable.')
def create_project(parser, options, args):
# Validate args
if len(args) < 2:
parser.error("Please specify a name for your Alliance Auth installation.")
elif len(args) > 3:
parser.error("Too many arguments.")
# First find the path to Alliance Auth
import allianceauth
allianceauth_path = os.path.dirname(allianceauth.__file__)
template_path = os.path.join(allianceauth_path, 'project_template')
# Determine locations of commands to render supervisor cond
command_options = {
'template': template_path,
'python': shutil.which('python'),
'gunicorn': shutil.which('gunicorn'),
'celery': shutil.which('celery'),
'extensions': ['py', 'conf', 'json'],
}
# Strip 'start' out of the arguments, leaving project name (and optionally destination dir)
args = args[1:]
# Call the command with extra context
call_command(StartProject(), *args, **command_options)
print("Success! %(project_name)s has been created." % {'project_name': args[0]}) # noqa
def update_settings(parser, options, args):
if len(args) < 2:
parser.error("Please specify the path to your Alliance Auth installation.")
elif len(args) > 2:
parser.error("Too many arguments.")
project_path = args[1]
project_name = os.path.split(project_path)[-1]
# find the target settings/base.py file, handing both the project and app as valid paths
# first check if given path is to the app
settings_path = os.path.join(project_path, 'settings/base.py')
if not os.path.exists(settings_path):
# next check if given path is to the project, so the app is within it
settings_path = os.path.join(project_path, project_name, 'settings/base.py')
if not os.path.exists(settings_path):
parser.error("Unable to locate the Alliance Auth project at %s" % project_path)
# first find the path to the Alliance Auth template settings
import allianceauth
allianceauth_path = os.path.dirname(allianceauth.__file__)
template_path = os.path.join(allianceauth_path, 'project_template')
template_settings_path = os.path.join(template_path, 'project_name/settings/base.py')
# overwrite the local project's base settings
with open(template_settings_path, 'r') as template, open(settings_path, 'w') as target:
target.write(template.read())
print("Successfully updated %(project_name)s settings." % {'project_name': project_name})
COMMANDS = {
'start': create_project,
'update': update_settings,
}
def main():
# Parse options
parser = OptionParser(usage="Usage: %prog [start|update] project_name [directory]")
(options, args) = parser.parse_args()
# Find command
try:
command = args[0]
except IndexError:
parser.print_help()
return
if command in COMMANDS:
COMMANDS[command](parser, options, args)
else:
parser.error("Unrecognised command: " + command)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,9 @@
from django.conf import settings
from .views import NightModeRedirectView
def auth_settings(request):
return {
'SITE_NAME': settings.SITE_NAME,
'NIGHT_MODE': NightModeRedirectView.night_mode_state(request),
}

View File

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

View File

@@ -0,0 +1,6 @@
from django.contrib import admin
from .models import CorpStats, CorpMember
admin.site.register(CorpStats)
admin.site.register(CorpMember)

View File

@@ -1,7 +1,6 @@
from __future__ import unicode_literals
from django.apps import AppConfig
class CorpUtilsConfig(AppConfig):
name = 'corputils'
name = 'allianceauth.corputils'
label = 'corputils'

View File

@@ -0,0 +1,30 @@
from allianceauth.services.hooks import MenuItemHook, UrlHook
from allianceauth import hooks
from allianceauth.corputils import urls
class CorpStats(MenuItemHook):
def __init__(self):
MenuItemHook.__init__(self,
'Corporation Stats',
'fa fa-share-alt fa-fw',
'corputils:view',
navactive=['corputils:'])
def render(self, request):
if request.user.has_perm('corputils.view_corp_corpstats') or request.user.has_perm(
'corputils.view_alliance_corpstats') or request.user.has_perm(
'corputils.add_corpstats') or request.user.has_perm('corputils.view_state_corpstats'):
return MenuItemHook.render(self, request)
return ''
@hooks.register('menu_item_hook')
def register_menu():
return CorpStats()
@hooks.register('url_hook')
def register_url():
return UrlHook(urls, 'corputils', r'^corpstats/')

View File

@@ -0,0 +1,42 @@
from django.db import models
import logging
logger = logging.getLogger(__name__)
class CorpStatsQuerySet(models.QuerySet):
def visible_to(self, user):
# superusers get all visible
if user.is_superuser:
logger.debug('Returning all corpstats for superuser %s.' % user)
return self
try:
char = user.profile.main_character
assert char
# build all accepted queries
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'):
queries.append(models.Q(corp__alliance__alliance_id=char.alliance_id))
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__alliance__in=user.profile.state.member_alliances.all()))
logger.debug('%s queries for user %s visible corpstats.' % (len(queries), user))
# filter based on queries
query = queries.pop()
for q in queries:
query |= q
return self.filter(query)
except AssertionError:
logger.debug('User %s has no main character. No corpstats visible.' % user)
return self.none()
class CorpStatsManager(models.Manager):
def get_queryset(self):
return CorpStatsQuerySet(self.model, using=self._db)
def visible_to(self, user):
return self.get_queryset().visible_to(user)

View File

@@ -19,6 +19,7 @@ PERMISSIONS = {
}
}
def user_permissions_dict(apps):
Permission = apps.get_model('auth', 'Permission')
ContentType = apps.get_model('contenttypes', 'ContentType')
@@ -33,14 +34,17 @@ def user_permissions_dict(apps):
'corpstats': {x: Permission.objects.get_or_create(codename=x, name=y, content_type=corpstats_ct)[0] for x, y in PERMISSIONS['corpstats'].items()},
}
def users_with_permission(apps, perm):
User = apps.get_model('auth', 'User')
return User.objects.filter(user_permissions=perm.pk)
def groups_with_permission(apps, perm):
Group = apps.get_model('auth', 'Group')
return Group.objects.filter(permissions=perm.pk)
def forward(apps, schema_editor):
perm_dict = user_permissions_dict(apps)
@@ -66,8 +70,9 @@ def forward(apps, schema_editor):
for name, perm in perm_dict['user'].items():
perm.delete()
def reverse(apps, schema_editor):
def reverse(apps, schema_editor):
perm_dict = user_permissions_dict(apps)
corp_users = users_with_permission(apps, perm_dict['corpstats']['view_corp_corpstats'])
@@ -79,7 +84,6 @@ def reverse(apps, schema_editor):
u.user_permissions.remove(perm_dict['corpstats']['view_corp_corpstats'].pk)
for u in corp_api_users:
u.user_permissions.remove(perm_dict['corpstats']['corp_apis'].pk)
alliance_users = users_with_permission(apps, perm_dict['corpstats']['view_alliance_corpstats'])
alliance_api_users = users_with_permission(apps, perm_dict['corpstats']['alliance_apis'])
@@ -93,7 +97,6 @@ def reverse(apps, schema_editor):
corp_groups = groups_with_permission(apps, perm_dict['corpstats']['view_corp_corpstats'])
corp_api_groups = groups_with_permission(apps, perm_dict['corpstats']['corp_apis'])
corp_gs = corp_groups | corp_api_groups
for g in corp_groups.distinct():
g.permissions.add(perm_dict['user']['corp_apis'].pk)
for g in corp_groups:

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-03-22 23:35
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('corputils', '0002_migrate_permissions'),
]
operations = [
migrations.AlterModelOptions(
name='corpstats',
options={'permissions': (('corp_apis', 'Can view API keys of members of their corporation.'), ('alliance_apis', 'Can view API keys of members of their alliance.'), ('blue_apis', 'Can view API keys of members of blue corporations.'), ('view_corp_corpstats', 'Can view corp stats of their corporation.'), ('view_alliance_corpstats', 'Can view corp stats of members of their alliance.'), ('view_blue_corpstats', 'Can view corp stats of blue corporations.')), 'verbose_name': 'corp stats', 'verbose_name_plural': 'corp stats'},
),
]

View File

@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-03-26 20:13
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import json
def convert_json_to_members(apps, schema_editor):
CorpStats = apps.get_model('corputils', 'CorpStats')
CorpMember = apps.get_model('corputils', 'CorpMember')
for cs in CorpStats.objects.all():
members = json.loads(cs._members)
CorpMember.objects.bulk_create(
[CorpMember(corpstats=cs, character_id=member_id, character_name=member_name) for member_id, member_name in
members.items()]
)
def convert_members_to_json(apps, schema_editor):
CorpStats = apps.get_model('corputils', 'CorpStats')
for cs in CorpStats.objects.all():
cs._members = json.dumps({m.character_id: m.character_name for m in cs.members.all()})
cs.save()
class Migration(migrations.Migration):
dependencies = [
('corputils', '0003_granular_permissions'),
]
operations = [
migrations.CreateModel(
name='CorpMember',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('character_id', models.PositiveIntegerField()),
('character_name', models.CharField(max_length=37)),
],
options={
'ordering': ['character_name'],
},
),
migrations.AddField(
model_name='corpmember',
name='corpstats',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='members',
to='corputils.CorpStats'),
),
migrations.AlterUniqueTogether(
name='corpmember',
unique_together=set([('corpstats', 'character_id')]),
),
migrations.RunPython(convert_json_to_members, convert_members_to_json),
migrations.RemoveField(
model_name='corpstats',
name='_members',
),
]

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-06-10 15:34
from __future__ import unicode_literals
from django.db import migrations
def delete_permissions(apps, schema_editor):
CorpStats = apps.get_model('corputils', 'CorpStats')
ContentType = apps.get_model('contenttypes', 'ContentType')
Permission = apps.get_model('auth', 'Permission')
ct = ContentType.objects.get_for_model(CorpStats)
perms = Permission.objects.filter(content_type=ct)
perms.filter(codename__contains='api').delete()
perms.filter(codename='view_corpstats').delete()
perms.filter(codename__contains='blue').delete()
perms.filter(codename__contains='remove').delete()
g = perms.get(codename='view_corp_corpstats')
g.name = 'Can view corp stats of their corporation.'
g.save()
g = perms.get(codename='view_alliance_corpstats')
g.name = 'Can view corp stats of members of their alliance.'
g.save()
class Migration(migrations.Migration):
dependencies = [
('corputils', '0004_member_models'),
]
operations = [
migrations.AlterModelOptions(
name='corpstats',
options={'permissions': (('view_corp_corpstats', 'Can view corp stats of their corporation.'), ('view_alliance_corpstats', 'Can view corp stats of members of their alliance.'), ('view_state_corpstats', 'Can view corp stats of members of their auth state.')), 'verbose_name': 'corp stats', 'verbose_name_plural': 'corp stats'},
),
migrations.RunPython(delete_permissions, migrations.RunPython.noop),
]

View File

@@ -0,0 +1,185 @@
import logging
import os
from allianceauth.authentication.models import CharacterOwnership, UserProfile
from bravado.exception import HTTPForbidden
from django.db import models
from esi.errors import TokenError
from esi.models import Token
from allianceauth.eveonline.models import EveCorporationInfo, EveCharacter
from allianceauth.notifications import notify
from allianceauth.corputils.managers import CorpStatsManager
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json')
logger = logging.getLogger(__name__)
class CorpStats(models.Model):
token = models.ForeignKey(Token, on_delete=models.CASCADE)
corp = models.OneToOneField(EveCorporationInfo, on_delete=models.CASCADE)
last_update = models.DateTimeField(auto_now=True)
class Meta:
permissions = (
('view_corp_corpstats', 'Can view corp stats of their corporation.'),
('view_alliance_corpstats', 'Can view corp stats of members of their alliance.'),
('view_state_corpstats', 'Can view corp stats of members of their auth state.'),
)
verbose_name = "corp stats"
verbose_name_plural = "corp stats"
objects = CorpStatsManager()
def __str__(self):
return "%s for %s" % (self.__class__.__name__, self.corp)
def update(self):
try:
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()[
'corporation_id'] == int(self.corp.corporation_id)
members = c.Corporation.get_corporations_corporation_id_members(
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
# the swagger spec doesn't have a maxItems count
# 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_name_chunks = [c.Character.get_characters_names(character_ids=id_chunk).result() for id_chunk in
member_id_chunks]
member_list = {}
for name_chunk in member_name_chunks:
member_list.update({m['character_id']: m['character_name'] for m in name_chunk})
# bulk create new member models
missing_members = [m_id for m_id in member_ids if
not CorpMember.objects.filter(corpstats=self, character_id=m_id).exists()]
CorpMember.objects.bulk_create(
[CorpMember(character_id=m_id, character_name=member_list[m_id], corpstats=self) for m_id in
missing_members])
# purge old members
self.members.exclude(character_id__in=member_ids).delete()
# update the timer
self.save()
except TokenError as e:
logger.warning("%s failed to update: %s" % (self, e))
if self.token.user:
notify(self.token.user, "%s failed to update with your ESI token." % self,
message="Your token has expired or is no longer valid. Please add a new one to create a new CorpStats.",
level="error")
self.delete()
except HTTPForbidden as e:
logger.warning("%s failed to update: %s" % (self, e))
if self.token.user:
notify(self.token.user, "%s failed to update with your ESI token." % self,
message="%s: %s" % (e.status_code, e.message), level="error")
self.delete()
except AssertionError:
logger.warning("%s token character no longer in corp." % self)
if self.token.user:
notify(self.token.user, "%s cannot update with your ESI token." % self,
message="%s cannot update with your ESI token as you have left corp." % self, level="error")
self.delete()
@property
def member_count(self):
return self.members.count()
@property
def user_count(self):
return len(set([m.main_character for m in self.members.all() if m.main_character]))
@property
def registered_member_count(self):
return len(self.registered_members)
@property
def registered_members(self):
return self.members.filter(pk__in=[m.pk for m in self.members.all() if m.registered])
@property
def unregistered_member_count(self):
return self.member_count - self.registered_member_count
@property
def unregistered_members(self):
return self.members.filter(pk__in=[m.pk for m in self.members.all() if not m.registered])
@property
def main_count(self):
return len(self.mains)
@property
def mains(self):
return self.members.filter(pk__in=[m.pk for m in self.members.all() if
m.main_character and int(m.main_character.character_id) == int(
m.character_id)])
def visible_to(self, user):
return CorpStats.objects.filter(pk=self.pk).visible_to(user).exists()
def can_update(self, user):
return self.token.user == user or self.visible_to(user)
def corp_logo(self, size=128):
return "https://image.eveonline.com/Corporation/%s_%s.png" % (self.corp.corporation_id, size)
def alliance_logo(self, size=128):
if self.corp.alliance:
return "https://image.eveonline.com/Alliance/%s_%s.png" % (self.corp.alliance.alliance_id, size)
else:
return "https://image.eveonline.com/Alliance/1_%s.png" % size
class CorpMember(models.Model):
character_id = models.PositiveIntegerField()
character_name = models.CharField(max_length=37)
corpstats = models.ForeignKey(CorpStats, on_delete=models.CASCADE, related_name='members')
class Meta:
# not making character_id unique in case a character moves between two corps while only one updates
unique_together = ('corpstats', 'character_id')
ordering = ['character_name']
def __str__(self):
return self.character_name
@property
def character(self):
try:
return EveCharacter.objects.get(character_id=self.character_id)
except EveCharacter.DoesNotExist:
return None
@property
def main_character(self):
try:
return self.character.character_ownership.user.profile.main_character
except (CharacterOwnership.DoesNotExist, UserProfile.DoesNotExist, AttributeError):
return None
@property
def alts(self):
if self.main_character:
return [co.character for co in self.main_character.character_ownership.user.character_ownerships.all()]
else:
return []
@property
def registered(self):
return CharacterOwnership.objects.filter(character__character_id=self.character_id).exists()
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)

File diff suppressed because one or more lines are too long

View File

@@ -1,14 +1,14 @@
from corputils.models import CorpStats
from alliance_auth.celeryapp import app
from celery import shared_task
from allianceauth.corputils import CorpStats
@app.task
@shared_task
def update_corpstats(pk):
cs = CorpStats.objects.get(pk=pk)
cs.update()
@app.task
@shared_task
def update_all_corpstats():
for cs in CorpStats.objects.all():
update_corpstats.delay(cs.pk)

View File

@@ -1,6 +1,5 @@
{% extends 'public/base.html' %}
{% extends 'allianceauth/base.html' %}
{% load i18n %}
{% block title %}{% trans "Corporation Member Data" %}{% endblock %}
{% block page_title %}{% trans "Corporation Member Data" %}{% endblock %}
{% block content %}
<div class="col-lg-12">
@@ -10,8 +9,8 @@
<div class="container-fluid">
<ul class="nav navbar-nav">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">{% trans "Corporations" %}<span class="caret"></span></a>
<ul class="dropdown-menu">
<a href="#" id="dLabel" class="dropdown-toggle" role="button" data-toggle="dropdown" aria-haspopup="false" aria-expanded="false">{% trans "Corporations" %}<span class="caret"></span></a>
<ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">
{% for corpstat in available %}
<li>
<a href="{% url 'corputils:view_corp' corpstat.corp.corporation_id %}">{{ corpstat.corp.corporation_name }}</a>
@@ -27,7 +26,7 @@
</ul>
<form class="navbar-form navbar-right" role="search" action="{% url 'corputils:search' %}" method="GET">
<div class="form-group">
<input type="text" class="form-control" name="search_string" placeholder="{% if search_string %}{{ search_string }}{% else %}{% trans "Search characters..." %}{% endif %}">
<input type="text" class="form-control" name="search_string" placeholder="{% if search_string %}{{ search_string }}{% else %}{% trans "Search all corporations..." %}{% endif %}">
</div>
</form>
</div>
@@ -35,5 +34,4 @@
{% block member_data %}{% endblock %}
</div>
</div>
{% endblock %}
{% endblock %}

View File

@@ -0,0 +1,205 @@
{% extends 'corputils/base.html' %}
{% load i18n %}
{% load humanize %}
{% block member_data %}
{% if corpstats %}
<div class="row">
<div class="col-lg-12 text-center">
<table class="table">
<tr>
<td class="text-center col-lg-6
{% if corpstats.corp.alliance %}{% else %}col-lg-offset-3{% endif %}"><img
class="ra-avatar" src="{{ corpstats.corp.logo_url_128 }}"></td>
{% if corpstats.corp.alliance %}
<td class="text-center col-lg-6"><img class="ra-avatar" src="{{ corpstats.alliance.logo_url_128 }}">
</td>
{% endif %}
</tr>
<tr>
<td class="text-center"><h4>{{ corpstats.corp.corporation_name }}</h4></td>
{% if corpstats.corp.alliance %}
<td class="text-center"><h4>{{ corpstats.corp.alliance.alliance_name }}</h4></td>
{% endif %}
</tr>
</table>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<div class="panel panel-default">
<div class="panel-heading">
<ul class="nav nav-pills pull-left">
<li class="active"><a href="#mains" data-toggle="pill">{% trans 'Mains' %} ({{ corpstats.main_count }})</a></li>
<li><a href="#members" data-toggle="pill">{% trans 'Members' %} ({{ corpstats.member_count }})</a></li>
<li><a href="#unregistered" data-toggle="pill">{% trans 'Unregistered' %} ({{ corpstats.unregistered_member_count }})</a></li>
</ul>
<div class="pull-right">
{% trans "Last update:" %} {{ corpstats.last_update|naturaltime }}
<a class="btn btn-success" type="button" href="{% url 'corputils:update' corpstats.corp.corporation_id %}" title="Update Now">
<span class="glyphicon glyphicon-refresh"></span>
</a>
</div>
<div class="clearfix"></div>
</div>
<div class="panel-body">
<div class="tab-content">
<div class="tab-pane fade in active" id="mains">
{% if mains %}
<div class="table-responsive">
<table class="table table-hover" id="table-mains">
<thead>
<tr>
<th style="height:1em;"><!-- Must have text or height to prevent clipping --></th>
<th></th>
</tr>
</thead>
<tbody>
{% for main in mains %}
<tr>
<td class="text-center" style="vertical-align:middle">
<div class="thumbnail"
style="border: 0 none; box-shadow: none; background: transparent;">
<img src="{{ main.portrait_url_64 }}" class="img-circle">
<div class="caption text-center">
{{ main }}
</div>
</div>
</td>
<td>
<table class="table table-hover">
{% for alt in main.alts %}
{% if forloop.first %}
<tr>
<th></th>
<th class="text-center">{% trans "Character" %}</th>
<th class="text-center">{% trans "Corporation" %}</th>
<th class="text-center">{% trans "Alliance" %}</th>
<th class="text-center"></th>
</tr>
{% endif %}
<tr>
<td class="text-center" style="width:5%">
<div class="thumbnail" style="border: 0 none; box-shadow: none; background: transparent;">
<img src="https://image.eveonline.com/Character/{{ alt.character_id }}_32.jpg" class="img-circle">
</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 }}/"
class="label label-danger" target="_blank">
{% trans "Killboard" %}
</a>
</td>
</tr>
{% endfor %}
</table>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
<div class="tab-pane fade" id="members">
{% if members %}
<div class="table-responsive">
<table class="table table-hover" id="table-members">
<thead>
<tr>
<th></th>
<th class="text-center">{% trans "Character" %}</th>
<th class="text-center"></th>
<th class="text-center">{% trans "Main Character" %}</th>
<th class="text-center">{% trans "Main Corporation" %}</th>
<th class="text-center">{% trans "Main Alliance" %}</th>
</tr>
</thead>
<tbody>
{% for member in members %}
<tr {% if not member.registered %}class="danger"{% endif %}>
<td><img src="{{ member.portrait_url }}" class="img-circle"></td>
<td class="text-center">{{ member.character_name }}</td>
<td class="text-center"><a
href="https://zkillboard.com/character/{{ member.character_id }}/"
class="label label-danger"
target="_blank">{% trans "Killboard" %}</a></td>
<td class="text-center">{{ member.main_character.character_name }}</td>
<td class="text-center">{{ member.main_character.corporation_name }}</td>
<td class="text-center">{{ member.main_character.alliance_name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
<div class="tab-pane fade" id="unregistered">
{% if unregistered %}
<div class="table-responsive">
<table class="table table-hover" id="table-unregistered">
<thead>
<tr>
<th></th>
<th class="text-center">{% trans "Character" %}</th>
<th class="text-center"></th>
</tr>
</thead>
<tbody>
{% for member in unregistered %}
<tr class="danger">
<td><img src="{{ member.portrait_url }}" class="img-circle"></td>
<td class="text-center">{{ member.character_name }}</td>
<td class="text-center">
<a href="https://zkillboard.com/character/{{ member.character_id }}/"
class="label label-danger"
target="_blank">
{% trans "Killboard" %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
{% 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(){
$('#table-mains').DataTable({
"columnDefs": [
{ "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 %}

View File

@@ -0,0 +1,48 @@
{% extends "corputils/base.html" %}
{% load i18n %}
{% block member_data %}
<div class="panel panel-default">
<div class="panel-heading clearfix">
<div class="panel-title pull-left">{% trans "Search Results" %}</div>
</div>
<div class="panel-body">
<table class="table table-hover" id="table-search">
<thead>
<tr>
<th class="text-center"></th>
<th class="text-center">{% trans "Character" %}</th>
<th class="text-center">{% trans "Corporation" %}</th>
<th class="text-center">{% trans "zKillboard" %}</th>
<th class="text-center">{% trans "Main Character" %}</th>
<th class="text-center">{% trans "Main Corporation" %}</th>
<th class="text-center">{% trans "Main Alliance" %}</th>
</tr>
</thead>
<tbody>
{% for result in results %}
<tr {% if not result.1.registered %}class="danger"{% endif %}>
<td class="text-center"><img src="{{ result.1.portrait_url }}" class="img-circle"></td>
<td class="text-center">{{ result.1.character_name }}</td>
<td class="text-center">{{ result.0.corp.corporation_name }}</td>
<td class="text-center"><a href="https://zkillboard.com/character/{{ result.1.character_id }}/" class="label label-danger" target="_blank">{% trans "Killboard" %}</a></td>
<td class="text-center">{{ result.1.main_character.character_name }}</td>
<td class="text-center">{{ result.1.main_character.corporation_name }}</td>
<td class="text-center">{{ result.1.main_character.alliance_name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</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(){
$('#table-search').DataTable();
});
{% endblock %}

View File

@@ -0,0 +1,277 @@
from unittest import mock
from django.test import TestCase
from allianceauth.tests.auth_utils import AuthUtils
from .models import CorpStats, CorpMember
from allianceauth.eveonline.models import EveCorporationInfo, EveAllianceInfo, EveCharacter
from esi.models import Token
from esi.errors import TokenError
from bravado.exception import HTTPForbidden
from django.contrib.auth.models import Permission
from allianceauth.authentication.models import CharacterOwnership
class CorpStatsManagerTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = AuthUtils.create_user('test')
AuthUtils.add_main_character(cls.user, 'test character', '1', corp_id='2', corp_name='test_corp', corp_ticker='TEST', alliance_id='3', alliance_name='TEST')
cls.user.profile.refresh_from_db()
cls.alliance = EveAllianceInfo.objects.create(alliance_id='3', alliance_name='test alliance', alliance_ticker='TEST', executor_corp_id='2')
cls.corp = EveCorporationInfo.objects.create(corporation_id='2', corporation_name='test corp', corporation_ticker='TEST', alliance=cls.alliance, member_count=1)
cls.token = Token.objects.create(user=cls.user, access_token='a', character_id='1', character_name='test character', character_owner_hash='z')
cls.corpstats = CorpStats.objects.create(corp=cls.corp, token=cls.token)
cls.view_corp_permission = Permission.objects.get_by_natural_key('view_corp_corpstats', 'corputils', 'corpstats')
cls.view_alliance_permission = Permission.objects.get_by_natural_key('view_alliance_corpstats', 'corputils', 'corpstats')
cls.view_state_permission = Permission.objects.get_by_natural_key('view_state_corpstats', 'corputils', 'corpstats')
cls.state = AuthUtils.create_state('test state', 500)
AuthUtils.assign_state(cls.user, cls.state, disconnect_signals=True)
def setUp(self):
self.user.refresh_from_db()
self.user.user_permissions.clear()
self.state.refresh_from_db()
self.state.member_corporations.clear()
self.state.member_alliances.clear()
def test_visible_superuser(self):
self.user.is_superuser = True
cs = CorpStats.objects.visible_to(self.user)
self.assertIn(self.corpstats, cs)
def test_visible_corporation(self):
self.user.user_permissions.add(self.view_corp_permission)
cs = CorpStats.objects.visible_to(self.user)
self.assertIn(self.corpstats, cs)
def test_visible_alliance(self):
self.user.user_permissions.add(self.view_alliance_permission)
cs = CorpStats.objects.visible_to(self.user)
self.assertIn(self.corpstats, cs)
def test_visible_state_corp_member(self):
self.state.member_corporations.add(self.corp)
self.user.user_permissions.add(self.view_state_permission)
cs = CorpStats.objects.visible_to(self.user)
self.assertIn(self.corpstats, cs)
def test_visible_state_alliance_member(self):
self.state.member_alliances.add(self.alliance)
self.user.user_permissions.add(self.view_state_permission)
cs = CorpStats.objects.visible_to(self.user)
self.assertIn(self.corpstats, cs)
class CorpStatsUpdateTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = AuthUtils.create_user('test')
AuthUtils.add_main_character(cls.user, 'test character', '1', corp_id='2', corp_name='test_corp', corp_ticker='TEST', alliance_id='3', alliance_name='TEST')
cls.token = Token.objects.create(user=cls.user, access_token='a', character_id='1', character_name='test character', character_owner_hash='z')
cls.corp = EveCorporationInfo.objects.create(corporation_id='2', corporation_name='test corp', corporation_ticker='TEST', member_count=1)
def setUp(self):
self.corpstats = CorpStats.objects.get_or_create(token=self.token, corp=self.corp)[0]
def test_can_update(self):
self.assertTrue(self.corpstats.can_update(self.user))
self.corpstats.token.user = None
self.assertFalse(self.corpstats.can_update(self.user))
self.user.is_superuser = True
self.assertTrue(self.corpstats.can_update(self.user))
self.user.refresh_from_db()
self.corpstats.token.refresh_from_db()
@mock.patch('esi.clients.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.Corporation.get_corporations_corporation_id_members.return_value.result.return_value = [{'character_id': 1}]
SwaggerClient.from_spec.return_value.Character.get_characters_names.return_value.result.return_value = [{'character_id': 1, 'character_name': 'test character'}]
self.corpstats.update()
self.assertTrue(CorpMember.objects.filter(character_id='1', character_name='test character', corpstats=self.corpstats).exists())
@mock.patch('esi.clients.SwaggerClient')
def test_update_remove_member(self, SwaggerClient):
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.Corporation.get_corporations_corporation_id_members.return_value.result.return_value = [{'character_id': 1}]
SwaggerClient.from_spec.return_value.Character.get_characters_names.return_value.result.return_value = [{'character_id': 1, 'character_name': 'test character'}]
self.corpstats.update()
self.assertFalse(CorpMember.objects.filter(character_id='2', corpstats=self.corpstats).exists())
@mock.patch('allianceauth.corputils.models.notify')
@mock.patch('esi.clients.SwaggerClient')
def test_update_deleted_token(self, SwaggerClient, notify):
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.side_effect = TokenError()
self.corpstats.update()
self.assertFalse(CorpStats.objects.filter(corp=self.corp).exists())
self.assertTrue(notify.called)
@mock.patch('allianceauth.corputils.models.notify')
@mock.patch('esi.clients.SwaggerClient')
def test_update_http_forbidden(self, SwaggerClient, notify):
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.side_effect = HTTPForbidden(mock.Mock())
self.corpstats.update()
self.assertFalse(CorpStats.objects.filter(corp=self.corp).exists())
self.assertTrue(notify.called)
@mock.patch('allianceauth.corputils.models.notify')
@mock.patch('esi.clients.SwaggerClient')
def test_update_token_character_corp_changed(self, SwaggerClient, notify):
SwaggerClient.from_spec.return_value.Character.get_characters_character_id.return_value.result.return_value = {'corporation_id': 3}
self.corpstats.update()
self.assertFalse(CorpStats.objects.filter(corp=self.corp).exists())
self.assertTrue(notify.called)
class CorpStatsPropertiesTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = AuthUtils.create_user('test')
AuthUtils.add_main_character(cls.user, 'test character', '1', corp_id='2', corp_name='test_corp', corp_ticker='TEST', alliance_id='3', alliance_name='TEST')
cls.user.profile.refresh_from_db()
cls.token = Token.objects.create(user=cls.user, access_token='a', character_id='1', character_name='test character', character_owner_hash='z')
cls.corp = EveCorporationInfo.objects.create(corporation_id='2', corporation_name='test corp', corporation_ticker='TEST', member_count=1)
cls.corpstats = CorpStats.objects.create(token=cls.token, corp=cls.corp)
cls.character = EveCharacter.objects.create(character_name='another test character', character_id='4', corporation_id='2', corporation_name='test corp', corporation_ticker='TEST')
def test_member_count(self):
member = CorpMember.objects.create(corpstats=self.corpstats, character_id='1', character_name='test character')
self.assertEqual(self.corpstats.member_count, 1)
member.delete()
self.assertEqual(self.corpstats.member_count, 0)
def test_user_count(self):
AuthUtils.disconnect_signals()
co = CharacterOwnership.objects.create(character=self.character, user=self.user, owner_hash='a')
AuthUtils.connect_signals()
CorpMember.objects.create(corpstats=self.corpstats, character_id='4', character_name='test character')
self.assertEqual(self.corpstats.user_count, 1)
co.delete()
self.assertEqual(self.corpstats.user_count, 0)
def test_registered_members(self):
AuthUtils.disconnect_signals()
co = CharacterOwnership.objects.create(character=self.character, user=self.user, owner_hash='a')
AuthUtils.connect_signals()
member = CorpMember.objects.create(corpstats=self.corpstats, character_id='4', character_name='test character')
self.assertIn(member, self.corpstats.registered_members)
self.assertEqual(self.corpstats.registered_member_count, 1)
co.delete()
self.assertNotIn(member, self.corpstats.registered_members)
self.assertEqual(self.corpstats.registered_member_count, 0)
def test_unregistered_members(self):
member = CorpMember.objects.create(corpstats=self.corpstats, character_id='4', character_name='test character')
self.corpstats.refresh_from_db()
self.assertIn(member, self.corpstats.unregistered_members)
self.assertEqual(self.corpstats.unregistered_member_count, 1)
AuthUtils.disconnect_signals()
CharacterOwnership.objects.create(character=self.character, user=self.user, owner_hash='a')
AuthUtils.connect_signals()
self.assertNotIn(member, self.corpstats.unregistered_members)
self.assertEqual(self.corpstats.unregistered_member_count, 0)
def test_mains(self):
# test when is a main
member = CorpMember.objects.create(corpstats=self.corpstats, character_id='1', character_name='test character')
self.assertIn(member, self.corpstats.mains)
self.assertEqual(self.corpstats.main_count, 1)
# test when is an alt
old_main = self.user.profile.main_character
character = EveCharacter.objects.create(character_name='other character', character_id=10, corporation_name='test corp', corporation_id='2', corporation_ticker='TEST')
AuthUtils.disconnect_signals()
co = CharacterOwnership.objects.create(character=character, user=self.user, owner_hash='b')
self.user.profile.main_character = character
self.user.profile.save()
AuthUtils.connect_signals()
self.assertNotIn(member, self.corpstats.mains)
self.assertEqual(self.corpstats.main_count, 0)
# test when no ownership
co.delete()
self.assertNotIn(member, self.corpstats.mains)
self.assertEqual(self.corpstats.main_count, 0)
# transaction won't roll this back
AuthUtils.disconnect_signals()
self.user.profile.main_character = old_main
self.user.profile.save()
AuthUtils.connect_signals()
def test_logos(self):
self.assertEqual(self.corpstats.corp_logo(size=128), 'https://image.eveonline.com/Corporation/2_128.png')
self.assertEqual(self.corpstats.alliance_logo(size=128), 'https://image.eveonline.com/Alliance/1_128.png')
alliance = EveAllianceInfo.objects.create(alliance_name='test alliance', alliance_id='3', alliance_ticker='TEST', executor_corp_id='2')
self.corp.alliance = alliance
self.corp.save()
self.assertEqual(self.corpstats.alliance_logo(size=128), 'https://image.eveonline.com/Alliance/3_128.png')
alliance.delete()
class CorpMemberTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = AuthUtils.create_user('test')
AuthUtils.add_main_character(cls.user, 'test character', '1', corp_id='2', corp_name='test_corp', corp_ticker='TEST', alliance_id='3', alliance_name='TEST')
cls.user.profile.refresh_from_db()
cls.token = Token.objects.create(user=cls.user, access_token='a', character_id='1', character_name='test character', character_owner_hash='a')
cls.corp = EveCorporationInfo.objects.create(corporation_id='2', corporation_name='test corp', corporation_ticker='TEST', member_count=1)
cls.corpstats = CorpStats.objects.create(token=cls.token, corp=cls.corp)
cls.member = CorpMember.objects.create(corpstats=cls.corpstats, character_id='2', character_name='other test character')
def test_character(self):
self.assertIsNone(self.member.character)
character = EveCharacter.objects.create(character_id='2', character_name='other test character', corporation_id='2', corporation_name='test corp', corporation_ticker='TEST')
self.assertEqual(self.member.character, character)
def test_main_character(self):
AuthUtils.disconnect_signals()
# test when member.character is None
self.assertIsNone(self.member.main_character)
# test when member.character is not None but also not a main
character = EveCharacter.objects.create(character_id='2', character_name='other test character', corporation_id='2', corporation_name='test corp', corporation_ticker='TEST')
CharacterOwnership.objects.create(character=character, user=self.user, owner_hash='b')
self.member.refresh_from_db()
self.assertNotEqual(self.member.main_character, self.member.character)
self.assertEquals(self.member.main_character, self.user.profile.main_character)
# test when is main
old_main = self.user.profile.main_character
self.user.profile.main_character = character
self.user.profile.save()
self.member.refresh_from_db()
self.assertEqual(self.member.main_character, self.member.character)
self.assertEqual(self.user.profile.main_character, self.member.main_character)
# transaction won't roll this back
self.user.profile.main_character = old_main
self.user.profile.save()
AuthUtils.connect_signals()
def test_alts(self):
self.assertListEqual(self.member.alts, [])
character = EveCharacter.objects.create(character_id='2', character_name='other test character', corporation_id='2', corporation_name='test corp', corporation_ticker='TEST')
CharacterOwnership.objects.create(character=character, user=self.user, owner_hash='b')
self.assertIn(character, self.member.alts)
def test_registered(self):
self.assertFalse(self.member.registered)
AuthUtils.disconnect_signals()
character = EveCharacter.objects.create(character_id='2', character_name='other test character', corporation_id='2', corporation_name='test corp', corporation_ticker='TEST')
CharacterOwnership.objects.create(character=character, user=self.user, owner_hash='b')
self.assertTrue(self.member.registered)
AuthUtils.connect_signals()
def test_portrait_url(self):
self.assertEquals(self.member.portrait_url(size=32), 'https://image.eveonline.com/Character/2_32.jpg')
self.assertEquals(self.member.portrait_url(size=32), self.member.portrait_url_32)

View File

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

View File

@@ -1,38 +1,23 @@
from __future__ import unicode_literals
from django.conf import settings
from django.contrib.auth.decorators import login_required, permission_required, user_passes_test
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.db import IntegrityError
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from eveonline.models import EveCharacter, EveCorporationInfo
from corputils.models import CorpStats
from esi.decorators import token_required
from bravado.exception import HTTPError
import os
from bravado.exception import HTTPError
from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required, user_passes_test
from django.core.exceptions import PermissionDenied
from django.db import IntegrityError
from django.shortcuts import render, redirect, get_object_or_404
from django.utils.translation import ugettext_lazy as _
from esi.decorators import token_required
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo
from .models import CorpStats
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json')
MEMBERS_PER_PAGE = int(getattr(settings, 'CORPSTATS_MEMBERS_PER_PAGE', 20))
def get_page(model_list, page_num):
p = Paginator(model_list, MEMBERS_PER_PAGE)
try:
members = p.page(page_num)
except PageNotAnInteger:
members = p.page(1)
except EmptyPage:
members = p.page(p.num_pages)
return members
def access_corpstats_test(user):
return user.has_perm('corputils.view_corp_corpstats') or user.has_perm(
'corputils.view_alliance_corpstats') or user.has_perm('corputils.view_blue_corpstats')
'corputils.view_alliance_corpstats') or user.has_perm('corputils.view_state_corpstats')
@login_required
@@ -44,9 +29,13 @@ def corpstats_add(request, token):
if EveCharacter.objects.filter(character_id=token.character_id).exists():
corp_id = EveCharacter.objects.get(character_id=token.character_id).corporation_id
else:
corp_id = token.get_esi_client(spec_file=SWAGGER_SPEC_PATH).Character.get_characters_character_id(
character_id=token.character_id).result()['corporation_id']
corp = EveCorporationInfo.objects.get(corporation_id=corp_id)
corp_id = \
token.get_esi_client(spec_file=SWAGGER_SPEC_PATH).Character.get_characters_character_id(
character_id=token.character_id).result()['corporation_id']
try:
corp = EveCorporationInfo.objects.get(corporation_id=corp_id)
except EveCorporationInfo.DoesNotExist:
corp = EveCorporationInfo.objects.create_corporation(corp_id)
cs = CorpStats.objects.create(token=token, corp=corp)
try:
cs.update()
@@ -55,8 +44,6 @@ def corpstats_add(request, token):
assert cs.pk # ensure update was successful
if CorpStats.objects.filter(pk=cs.pk).visible_to(request.user).exists():
return redirect('corputils:view_corp', corp_id=corp.corporation_id)
except EveCorporationInfo.DoesNotExist:
messages.error(request, _('Unrecognized corporation. Please ensure it is a member of the alliance or a blue.'))
except IntegrityError:
messages.error(request, _('Selected corp already has a statistics module.'))
except AssertionError:
@@ -84,21 +71,26 @@ def corpstats_view(request, corp_id=None):
# get default model if none requested
if not corp_id and available.count() == 1:
corpstats = available[0]
elif not corp_id and available.count() > 1 and request.user.profile.main_character:
# get their main corp if available
try:
corpstats = available.get(corp__corporation_id=request.user.profile.main_character.corporation_id)
except CorpStats.DoesNotExist:
pass
context = {
'available': available,
}
# paginate
members = []
if corpstats:
page = request.GET.get('page', 1)
members = get_page(corpstats.get_member_objects(request.user), page)
if corpstats:
members = corpstats.members.all()
mains = corpstats.mains.all()
unregistered = corpstats.unregistered_members.all()
context.update({
'corpstats': corpstats.get_view_model(request.user),
'corpstats': corpstats,
'members': members,
'mains': mains,
'unregistered': unregistered,
})
return render(request, 'corputils/corpstats.html', context=context)
@@ -109,14 +101,10 @@ def corpstats_view(request, corp_id=None):
def corpstats_update(request, corp_id):
corp = get_object_or_404(EveCorporationInfo, corporation_id=corp_id)
corpstats = get_object_or_404(CorpStats, corp=corp)
if corpstats.can_update(request.user):
try:
corpstats.update()
except HTTPError as e:
messages.error(request, str(e))
else:
raise PermissionDenied(
'You do not have permission to update member data for the selected corporation statistics module.')
try:
corpstats.update()
except HTTPError as e:
messages.error(request, str(e))
if corpstats.pk:
return redirect('corputils:view_corp', corp_id=corp.corporation_id)
else:
@@ -129,19 +117,16 @@ def corpstats_search(request):
results = []
search_string = request.GET.get('search_string', None)
if search_string:
has_similar = CorpStats.objects.filter(_members__icontains=search_string).visible_to(request.user)
has_similar = CorpStats.objects.filter(members__character_name__icontains=search_string).visible_to(
request.user).distinct()
for corpstats in has_similar:
similar = [(member_id, corpstats.members[member_id]) for member_id in corpstats.members if
search_string.lower() in corpstats.members[member_id].lower()]
similar = corpstats.members.filter(character_name__icontains=search_string)
for s in similar:
results.append(
(corpstats, CorpStats.MemberObject(s[0], s[1], show_apis=corpstats.show_apis(request.user))))
page = request.GET.get('page', 1)
results.append((corpstats, s))
results = sorted(results, key=lambda x: x[1].character_name)
results_page = get_page(results, page)
context = {
'available': CorpStats.objects.visible_to(request.user),
'results': results_page,
'results': results,
'search_string': search_string,
}
return render(request, 'corputils/search.html', context=context)

View File

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

View File

@@ -0,0 +1,135 @@
from django import forms
from django.contrib import admin
from django.core.exceptions import ObjectDoesNotExist
from .providers import ObjectNotFound
from .models import EveAllianceInfo
from .models import EveCharacter
from .models import EveCorporationInfo
class EveEntityExistsError(forms.ValidationError):
def __init__(self, entity_type_name, id):
super(EveEntityExistsError, self).__init__(
message='{} with ID {} already exists.'.format(entity_type_name, id))
class EveEntityNotFoundError(forms.ValidationError):
def __init__(self, entity_type_name, id):
super(EveEntityNotFoundError, self).__init__(
message='{} with ID {} not found.'.format(entity_type_name, id))
class EveEntityForm(forms.ModelForm):
id = forms.IntegerField(min_value=1)
entity_model_class = None
def clean_id(self):
raise NotImplementedError()
def save(self, commit=True):
raise NotImplementedError()
def save_m2m(self):
pass
class EveCharacterForm(EveEntityForm):
entity_type_name = 'character'
def clean_id(self):
try:
assert self.Meta.model.provider.get_character(self.cleaned_data['id'])
except (AssertionError, ObjectNotFound):
raise EveEntityNotFoundError(self.entity_type_name, self.cleaned_data['id'])
if self.Meta.model.objects.filter(character_id=self.cleaned_data['id']).exists():
raise EveEntityExistsError(self.entity_type_name, self.cleaned_data['id'])
return self.cleaned_data['id']
def save(self, commit=True):
return self.Meta.model.objects.create_character(self.cleaned_data['id'])
class Meta:
fields = ['id']
model = EveCharacter
class EveCorporationForm(EveEntityForm):
entity_type_name = 'corporation'
def clean_id(self):
try:
assert self.Meta.model.provider.get_corporation(self.cleaned_data['id'])
except (AssertionError, ObjectNotFound):
raise EveEntityNotFoundError(self.entity_type_name, self.cleaned_data['id'])
if self.Meta.model.objects.filter(corporation_id=self.cleaned_data['id']).exists():
raise EveEntityExistsError(self.entity_type_name, self.cleaned_data['id'])
return self.cleaned_data['id']
def save(self, commit=True):
return self.Meta.model.objects.create_corporation(self.cleaned_data['id'])
class Meta:
fields = ['id']
model = EveCorporationInfo
class EveAllianceForm(EveEntityForm):
entity_type_name = 'alliance'
def clean_id(self):
try:
assert self.Meta.model.provider.get_alliance(self.cleaned_data['id'])
except (AssertionError, ObjectNotFound):
raise EveEntityNotFoundError(self.entity_type_name, self.cleaned_data['id'])
if self.Meta.model.objects.filter(alliance_id=self.cleaned_data['id']).exists():
raise EveEntityExistsError(self.entity_type_name, self.cleaned_data['id'])
return self.cleaned_data['id']
def save(self, commit=True):
return self.Meta.model.objects.create_alliance(self.cleaned_data['id'])
class Meta:
fields = ['id']
model = EveAllianceInfo
@admin.register(EveCorporationInfo)
class EveCorporationInfoAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if not obj or not obj.pk:
return EveCorporationForm
return super(EveCorporationInfoAdmin, self).get_form(request, obj=obj, **kwargs)
@admin.register(EveAllianceInfo)
class EveAllianceInfoAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if not obj or not obj.pk:
return EveAllianceForm
return super(EveAllianceInfoAdmin, self).get_form(request, obj=obj, **kwargs)
@admin.register(EveCharacter)
class EveCharacterAdmin(admin.ModelAdmin):
search_fields = ['character_name', 'corporation_name', 'alliance_name', 'character_ownership__user__username']
list_display = ('character_name', 'corporation_name', 'alliance_name', 'user', 'main_character')
@staticmethod
def user(obj):
try:
return obj.character_ownership.user
except (AttributeError, ObjectDoesNotExist):
return None
@staticmethod
def main_character(obj):
try:
return obj.character_ownership.user.profile.main_character
except (AttributeError, ObjectDoesNotExist):
return None
def get_form(self, request, obj=None, **kwargs):
if not obj or not obj.pk:
return EveCharacterForm
return super(EveCharacterAdmin, self).get_form(request, obj=obj, **kwargs)

View File

@@ -1,7 +1,6 @@
from __future__ import unicode_literals
from django.apps import AppConfig
class EveonlineConfig(AppConfig):
name = 'eveonline'
name = 'allianceauth.eveonline'
label = 'eveonline'

View File

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

View File

@@ -0,0 +1,39 @@
from django.contrib import admin
from django.db import models
from .models import AutogroupsConfig
import logging
logger = logging.getLogger(__name__)
def sync_user_groups(modeladmin, request, queryset):
for agc in queryset:
logger.debug("update_all_states_group_membership for {}".format(agc))
agc.update_all_states_group_membership()
class AutogroupsConfigAdmin(admin.ModelAdmin):
formfield_overrides = {
models.CharField: {'strip': False}
}
def get_readonly_fields(self, request, obj=None):
if obj: # This is the case when obj is already created i.e. it's an edit
return [
'corp_group_prefix', 'corp_name_source', 'alliance_group_prefix', 'alliance_name_source',
'replace_spaces', 'replace_spaces_with'
]
else:
return []
def get_actions(self, request):
actions = super(AutogroupsConfigAdmin, self).get_actions(request)
actions['sync_user_groups'] = (sync_user_groups,
'sync_user_groups',
'Sync all users groups for this Autogroup Config')
return actions
admin.site.register(AutogroupsConfig, AutogroupsConfigAdmin)

View File

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

View File

@@ -0,0 +1,73 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-12-23 04:30
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('authentication', '0015_user_profiles'),
('auth', '0008_alter_user_username_max_length'),
('eveonline', '0009_on_delete'),
]
operations = [
migrations.CreateModel(
name='AutogroupsConfig',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('corp_groups', models.BooleanField(default=False, help_text='Setting this to false will delete all the created groups.')),
('corp_group_prefix', models.CharField(blank=True, default='Corp ', max_length=50)),
('corp_name_source', models.CharField(choices=[('ticker', 'Ticker'), ('name', 'Full name')], default='name', max_length=20)),
('alliance_groups', models.BooleanField(default=False, help_text='Setting this to false will delete all the created groups.')),
('alliance_group_prefix', models.CharField(blank=True, default='Alliance ', max_length=50)),
('alliance_name_source', models.CharField(choices=[('ticker', 'Ticker'), ('name', 'Full name')], default='name', max_length=20)),
('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)),
],
),
migrations.CreateModel(
name='ManagedAllianceGroup',
fields=[
('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')),
('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')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='ManagedCorpGroup',
fields=[
('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')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.Group')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='autogroupsconfig',
name='alliance_managed_groups',
field=models.ManyToManyField(help_text="A list of alliance groups created and maintained by this AutogroupConfig. You should not edit this list unless you know what you're doing.", related_name='alliance_managed_config', through='eve_autogroups.ManagedAllianceGroup', to='auth.Group'),
),
migrations.AddField(
model_name='autogroupsconfig',
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'),
),
migrations.AddField(
model_name='autogroupsconfig',
name='states',
field=models.ManyToManyField(related_name='autogroups', to='authentication.State'),
),
]

View File

@@ -0,0 +1,246 @@
import logging
from django.db import models, transaction
from django.contrib.auth.models import Group, User
from django.core.exceptions import ObjectDoesNotExist
from allianceauth.authentication.models import State
from allianceauth.eveonline.models import EveCorporationInfo, EveAllianceInfo
logger = logging.getLogger(__name__)
def get_users_for_state(state: State):
return User.objects.select_related('profile').prefetch_related('profile__main_character')\
.filter(profile__state__pk=state.pk)
class AutogroupsConfigManager(models.Manager):
def update_groups_for_state(self, state: State):
"""
Update all the Group memberships for the users
who have State
:param state: State to update for
:return:
"""
users = get_users_for_state(state)
for config in self.filter(states=state):
logger.debug("in state loop")
for user in users:
logger.debug("in user loop for {}".format(user))
config.update_group_membership_for_user(user)
def update_groups_for_user(self, user: User, state: State = None):
"""
Update the Group memberships for the given users state
:param user: User to update for
:param state: State to update user for
:return:
"""
if state is None:
state = user.profile.state
for config in self.filter(states=state):
config.update_group_membership_for_user(user)
class AutogroupsConfig(models.Model):
OPT_TICKER = 'ticker'
OPT_NAME = 'name'
NAME_OPTIONS = (
(OPT_TICKER, 'Ticker'),
(OPT_NAME, 'Full name'),
)
states = models.ManyToManyField(State, related_name='autogroups')
corp_groups = models.BooleanField(default=False,
help_text="Setting this to false will delete all the created groups.")
corp_group_prefix = models.CharField(max_length=50, default='Corp ', blank=True)
corp_name_source = models.CharField(max_length=20, choices=NAME_OPTIONS, default=OPT_NAME)
alliance_groups = models.BooleanField(default=False,
help_text="Setting this to false will delete all the created groups.")
alliance_group_prefix = models.CharField(max_length=50, default='Alliance ', blank=True)
alliance_name_source = models.CharField(max_length=20, choices=NAME_OPTIONS, default=OPT_NAME)
corp_managed_groups = models.ManyToManyField(
Group, through='ManagedCorpGroup', related_name='corp_managed_config',
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.')
alliance_managed_groups = models.ManyToManyField(
Group, through='ManagedAllianceGroup', related_name='alliance_managed_config',
help_text='A list of alliance groups created and maintained by this AutogroupConfig. '
'You should not edit this list unless you know what you\'re doing.')
replace_spaces = models.BooleanField(default=False)
replace_spaces_with = models.CharField(
max_length=10, default='', blank=True,
help_text='Any spaces in the group name will be replaced with this.')
objects = AutogroupsConfigManager()
def __init__(self, *args, **kwargs):
super(AutogroupsConfig, self).__init__(*args, **kwargs)
def __repr__(self):
return self.__class__.__name__
def __str__(self):
return 'States: ' + (' '.join(list(self.states.all().values_list('name', flat=True))) if self.pk else str(None))
def update_all_states_group_membership(self):
list(map(self.update_group_membership_for_state, self.states.all()))
def update_group_membership_for_state(self, state: State):
list(map(self.update_group_membership_for_user, get_users_for_state(state)))
@transaction.atomic
def update_group_membership_for_user(self, user: User):
self.update_alliance_group_membership(user)
self.update_corp_group_membership(user)
def user_entitled_to_groups(self, user: User) -> bool:
try:
return user.profile.state in self.states.all()
except ObjectDoesNotExist:
return False
@transaction.atomic
def update_alliance_group_membership(self, user: User):
group = None
try:
if not self.alliance_groups or not self.user_entitled_to_groups(user):
logger.debug('User {} does not have required state for alliance group membership'.format(user))
return
else:
alliance = user.profile.main_character.alliance
if alliance is None:
logger.debug('User {} alliance is None, cannot update group membership'.format(user))
return
group = self.get_alliance_group(alliance)
except EveAllianceInfo.DoesNotExist:
logger.warning('User {} main characters alliance does not exist in the database.'
' Group membership not updated'.format(user))
except AttributeError:
logger.warning('User {} does not have a main character. Group membership not updated'.format(user))
finally:
self.remove_user_from_alliance_groups(user, except_group=group)
if group is not None:
logger.debug('Adding user {} to alliance group {}'.format(user, group))
user.groups.add(group)
@transaction.atomic
def update_corp_group_membership(self, user: User):
group = None
try:
if not self.corp_groups or not self.user_entitled_to_groups(user):
logger.debug('User {} does not have required state for corp group membership'.format(user))
else:
corp = user.profile.main_character.corporation
group = self.get_corp_group(corp)
except EveCorporationInfo.DoesNotExist:
logger.warning('User {} main characters corporation does not exist in the database.'
' Group membership not updated'.format(user))
except AttributeError:
logger.warning('User {} does not have a main character. Group membership not updated'.format(user))
finally:
self.remove_user_from_corp_groups(user, except_group=group)
if group is not None:
logger.debug('Adding user {} to corp group {}'.format(user, group))
user.groups.add(group)
@transaction.atomic
def remove_user_from_alliance_groups(self, user: User, except_group: Group = None):
remove_groups = user.groups.filter(pk__in=self.alliance_managed_groups.all().values_list('pk', flat=True))
if except_group is not None:
remove_groups = remove_groups.exclude(pk=except_group.pk)
list(map(user.groups.remove, remove_groups))
@transaction.atomic
def remove_user_from_corp_groups(self, user: User, except_group: Group = None):
remove_groups = user.groups.filter(pk__in=self.corp_managed_groups.all().values_list('pk', flat=True))
if except_group is not None:
remove_groups = remove_groups.exclude(pk=except_group.pk)
list(map(user.groups.remove, remove_groups))
def get_alliance_group(self, alliance: EveAllianceInfo) -> Group:
return self.create_alliance_group(alliance)
def get_corp_group(self, corp: EveCorporationInfo) -> Group:
return self.create_corp_group(corp)
@transaction.atomic
def create_alliance_group(self, alliance: EveAllianceInfo) -> Group:
group, created = Group.objects.get_or_create(name=self.get_alliance_group_name(alliance))
if created:
ManagedAllianceGroup.objects.create(group=group, config=self, alliance=alliance)
return group
@transaction.atomic
def create_corp_group(self, corp: EveCorporationInfo) -> Group:
group, created = Group.objects.get_or_create(name=self.get_corp_group_name(corp))
if created:
ManagedCorpGroup.objects.create(group=group, config=self, corp=corp)
return group
def delete_alliance_managed_groups(self):
"""
Deletes ALL managed alliance groups
"""
for g in self.alliance_managed_groups.all():
g.delete()
def delete_corp_managed_groups(self):
"""
Deletes ALL managed corp groups
"""
for g in self.corp_managed_groups.all():
g.delete()
def get_alliance_group_name(self, alliance: EveAllianceInfo) -> str:
if self.alliance_name_source == self.OPT_TICKER:
name = alliance.alliance_ticker
elif self.alliance_name_source == self.OPT_NAME:
name = alliance.alliance_name
else:
raise NameSourceException('Not a valid name source')
return self._replace_spaces(self.alliance_group_prefix + name)
def get_corp_group_name(self, corp: EveCorporationInfo) -> str:
if self.corp_name_source == self.OPT_TICKER:
name = corp.corporation_ticker
elif self.corp_name_source == self.OPT_NAME:
name = corp.corporation_name
else:
raise NameSourceException('Not a valid name source')
return self._replace_spaces(self.corp_group_prefix + name)
def _replace_spaces(self, name: str) -> str:
"""
Replace the spaces in the given name based on the config
:param name: name to replace spaces in
:return: name with spaces replaced with the configured character(s) or unchanged if configured
"""
if self.replace_spaces:
return name.strip().replace(' ', str(self.replace_spaces_with))
return name
class ManagedGroup(models.Model):
group = models.ForeignKey(Group, on_delete=models.CASCADE)
config = models.ForeignKey(AutogroupsConfig, on_delete=models.CASCADE)
class Meta:
abstract = True
class ManagedCorpGroup(ManagedGroup):
corp = models.ForeignKey(EveCorporationInfo, on_delete=models.CASCADE)
class ManagedAllianceGroup(ManagedGroup):
alliance = models.ForeignKey(EveAllianceInfo, on_delete=models.CASCADE)
class NameSourceException(Exception):
pass

View File

@@ -0,0 +1,66 @@
import logging
from django.dispatch import receiver
from django.db.models.signals import pre_save, post_save, pre_delete, m2m_changed
from allianceauth.authentication.models import UserProfile, State
from .models import AutogroupsConfig
logger = logging.getLogger(__name__)
@receiver(pre_save, sender=AutogroupsConfig)
def pre_save_config(sender, instance, *args, **kwargs):
"""
Checks if enable was toggled on group config and
deletes groups if necessary.
"""
logger.debug("Received pre_save from {}".format(instance))
if not instance.pk:
# new model being created
return
try:
old_instance = AutogroupsConfig.objects.get(pk=instance.pk)
# Check if enable was toggled, delete groups?
if old_instance.alliance_groups is True and instance.alliance_groups is False:
instance.delete_alliance_managed_groups()
if old_instance.corp_groups is True and instance.corp_groups is False:
instance.delete_corp_managed_groups()
except AutogroupsConfig.DoesNotExist:
pass
@receiver(pre_delete, sender=AutogroupsConfig)
def pre_delete_config(sender, instance, *args, **kwargs):
"""
Delete groups on deleting config
"""
instance.delete_corp_managed_groups()
instance.delete_alliance_managed_groups()
@receiver(post_save, sender=UserProfile)
def check_groups_on_profile_update(sender, instance, created, *args, **kwargs):
"""
Trigger check when main character or state changes.
"""
update_fields = kwargs.pop('update_fields', []) or []
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)
def autogroups_states_changed(sender, instance, action, reverse, model, pk_set, *args, **kwargs):
"""
Trigger group membership update when a state is added or removed from
an autogroup config.
"""
if action.startswith('post_'):
for pk in pk_set:
try:
state = State.objects.get(pk=pk)
instance.update_group_membership_for_state(state)
except State.DoesNotExist:
# Deleted States handled by the profile state change
pass

View File

@@ -0,0 +1,27 @@
from unittest import mock
from django.db.models.signals import pre_save, post_save, pre_delete, m2m_changed
from allianceauth.authentication.models import UserProfile
from allianceauth.authentication.signals import state_changed
from allianceauth.eveonline.models import EveCharacter
from .. import signals
from ..models import AutogroupsConfig
MODULE_PATH = 'allianceauth.eveonline.autogroups'
def patch(target, *args, **kwargs):
return mock.patch('{}{}'.format(MODULE_PATH, target), *args, **kwargs)
def connect_signals():
pre_save.connect(receiver=signals.pre_save_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)
m2m_changed.connect(receiver=signals.autogroups_states_changed, sender=AutogroupsConfig.states.through)
def disconnect_signals():
pre_save.disconnect(receiver=signals.pre_save_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)
m2m_changed.disconnect(receiver=signals.autogroups_states_changed, sender=AutogroupsConfig.states.through)

View File

@@ -0,0 +1,34 @@
from django.test import TestCase
from allianceauth.tests.auth_utils import AuthUtils
from ..models import AutogroupsConfig
from . import patch
class AutogroupsConfigManagerTestCase(TestCase):
def test_update_groups_for_state(self, ):
member = AuthUtils.create_member('test member')
obj = AutogroupsConfig.objects.create()
obj.states.add(member.profile.state)
with patch('.models.AutogroupsConfig.update_group_membership_for_user') as update_group_membership_for_user:
AutogroupsConfig.objects.update_groups_for_state(member.profile.state)
self.assertTrue(update_group_membership_for_user.called)
self.assertEqual(update_group_membership_for_user.call_count, 1)
args, kwargs = update_group_membership_for_user.call_args
self.assertEqual(args[0], member)
def test_update_groups_for_user(self):
member = AuthUtils.create_member('test member')
obj = AutogroupsConfig.objects.create()
obj.states.add(member.profile.state)
with patch('.models.AutogroupsConfig.update_group_membership_for_user') as update_group_membership_for_user:
AutogroupsConfig.objects.update_groups_for_user(member)
self.assertTrue(update_group_membership_for_user.called)
self.assertEqual(update_group_membership_for_user.call_count, 1)
args, kwargs = update_group_membership_for_user.call_args
self.assertEqual(args[0], member)

View File

@@ -0,0 +1,343 @@
from django.test import TestCase
from django.contrib.auth.models import Group
from allianceauth.tests.auth_utils import AuthUtils
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo
from ..models import AutogroupsConfig, get_users_for_state
from . import patch, connect_signals, disconnect_signals
class AutogroupsConfigTestCase(TestCase):
def setUp(self):
# Disconnect signals
disconnect_signals()
self.member = AuthUtils.create_member('test user')
state = AuthUtils.get_member_state()
self.alliance = EveAllianceInfo.objects.create(
alliance_id='3456',
alliance_name='alliance name',
alliance_ticker='TIKR',
executor_corp_id='2345',
)
self.corp = EveCorporationInfo.objects.create(
corporation_id='2345',
corporation_name='corp name',
corporation_ticker='TIKK',
member_count=10,
alliance=self.alliance,
)
state.member_alliances.add(self.alliance)
state.member_corporations.add(self.corp)
def tearDown(self):
# Reconnect signals
connect_signals()
def test_get_users_for_state(self):
result = get_users_for_state(self.member.profile.state)
self.assertIn(self.member, result)
self.assertEqual(len(result), 1)
@patch('.models.AutogroupsConfig.update_alliance_group_membership')
@patch('.models.AutogroupsConfig.update_corp_group_membership')
def test_update_group_membership(self, update_corp, update_alliance):
agc = AutogroupsConfig.objects.create()
agc.update_group_membership_for_user(self.member)
self.assertTrue(update_corp.called)
self.assertTrue(update_alliance.called)
args, kwargs = update_corp.call_args
self.assertEqual(args[0], self.member)
args, kwargs = update_alliance.call_args
self.assertEqual(args[0], self.member)
def test_update_alliance_group_membership(self):
obj = AutogroupsConfig.objects.create(alliance_groups=True)
obj.states.add(AuthUtils.get_member_state())
char = EveCharacter.objects.create(
character_id='1234',
character_name='test character',
corporation_id='2345',
corporation_name='test corp',
corporation_ticker='tickr',
alliance_id='3456',
alliance_name='alliance name',
)
self.member.profile.main_character = char
self.member.profile.save()
pre_groups = self.member.groups.all()
# Act
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_qs = Group.objects.filter(pk=group.pk)
self.assertIn(group, self.member.groups.all())
self.assertQuerysetEqual(self.member.groups.all(), map(repr, pre_groups | group_qs), ordered=False)
def test_update_alliance_group_membership_no_main_character(self):
obj = AutogroupsConfig.objects.create()
obj.states.add(AuthUtils.get_member_state())
# Act
obj.update_alliance_group_membership(self.member)
group = obj.get_alliance_group(self.alliance)
self.assertNotIn(group, self.member.groups.all())
def test_update_alliance_group_membership_no_alliance_model(self):
obj = AutogroupsConfig.objects.create()
obj.states.add(AuthUtils.get_member_state())
char = EveCharacter.objects.create(
character_id='1234',
character_name='test character',
corporation_id='2345',
corporation_name='test corp',
corporation_ticker='tickr',
alliance_id='3459',
alliance_name='alliance name',
)
self.member.profile.main_character = char
self.member.profile.save()
# Act
obj.update_alliance_group_membership(self.member)
group = obj.get_alliance_group(self.alliance)
self.assertNotIn(group, self.member.groups.all())
def test_update_corp_group_membership(self):
obj = AutogroupsConfig.objects.create(corp_groups=True)
obj.states.add(AuthUtils.get_member_state())
char = EveCharacter.objects.create(
character_id='1234',
character_name='test character',
corporation_id='2345',
corporation_name='test corp',
corporation_ticker='tickr',
alliance_id='3456',
alliance_name='alliance name',
)
self.member.profile.main_character = char
self.member.profile.save()
pre_groups = self.member.groups.all()
# Act
obj.update_corp_group_membership(self.member)
group = obj.get_corp_group(self.corp)
group_qs = Group.objects.filter(pk=group.pk)
self.assertIn(group, self.member.groups.all())
self.assertQuerysetEqual(self.member.groups.all(), map(repr, pre_groups | group_qs), ordered=False)
def test_update_corp_group_membership_no_state(self):
obj = AutogroupsConfig.objects.create(corp_groups=True)
char = EveCharacter.objects.create(
character_id='1234',
character_name='test character',
corporation_id='2345',
corporation_name='test corp',
corporation_ticker='tickr',
alliance_id='3456',
alliance_name='alliance name',
)
self.member.profile.main_character = char
self.member.profile.save()
pre_groups = list(self.member.groups.all())
# Act
obj.update_corp_group_membership(self.member)
group = obj.get_corp_group(self.corp)
post_groups = list(self.member.groups.all())
self.assertNotIn(group, post_groups)
self.assertListEqual(pre_groups, post_groups)
def test_update_corp_group_membership_no_main_character(self):
obj = AutogroupsConfig.objects.create()
obj.states.add(AuthUtils.get_member_state())
# Act
obj.update_corp_group_membership(self.member)
group = obj.get_corp_group(self.corp)
self.assertNotIn(group, self.member.groups.all())
def test_update_corp_group_membership_no_corp_model(self):
obj = AutogroupsConfig.objects.create()
obj.states.add(AuthUtils.get_member_state())
char = EveCharacter.objects.create(
character_id='1234',
character_name='test character',
corporation_id='2348',
corporation_name='test corp',
corporation_ticker='tickr',
alliance_id='3456',
alliance_name='alliance name',
)
self.member.profile.main_character = char
self.member.profile.save()
# Act
obj.update_corp_group_membership(self.member)
group = obj.get_corp_group(self.corp)
self.assertNotIn(group, self.member.groups.all())
def test_remove_user_from_alliance_groups(self):
obj = AutogroupsConfig.objects.create()
result = obj.get_alliance_group(self.alliance)
result.user_set.add(self.member)
self.assertIn(result, self.member.groups.all())
# Act
obj.remove_user_from_alliance_groups(self.member)
self.assertNotIn(result, self.member.groups.all())
def test_remove_user_from_corp_groups(self):
obj = AutogroupsConfig.objects.create()
result = obj.get_corp_group(self.corp)
result.user_set.add(self.member)
self.assertIn(result, self.member.groups.all())
# Act
obj.remove_user_from_corp_groups(self.member)
self.assertNotIn(result, self.member.groups.all())
def test_get_alliance_group(self):
obj = AutogroupsConfig.objects.create()
result = obj.get_alliance_group(self.alliance)
group = Group.objects.get(name='Alliance alliance name')
self.assertEqual(result, group)
self.assertEqual(obj.get_alliance_group_name(self.alliance), result.name)
self.assertTrue(obj.alliance_managed_groups.filter(pk=result.pk).exists())
def test_get_corp_group(self):
obj = AutogroupsConfig.objects.create()
result = obj.get_corp_group(self.corp)
group = Group.objects.get(name='Corp corp name')
self.assertEqual(result, group)
self.assertEqual(obj.get_corp_group_name(self.corp), group.name)
self.assertTrue(obj.corp_managed_groups.filter(pk=group.pk).exists())
def test_create_alliance_group(self):
obj = AutogroupsConfig.objects.create()
result = obj.create_alliance_group(self.alliance)
group = Group.objects.get(name='Alliance alliance name')
self.assertEqual(result, group)
self.assertEqual(obj.get_alliance_group_name(self.alliance), group.name)
self.assertTrue(obj.alliance_managed_groups.filter(pk=group.pk).exists())
def test_create_corp_group(self):
obj = AutogroupsConfig.objects.create()
result = obj.create_corp_group(self.corp)
group = Group.objects.get(name='Corp corp name')
self.assertEqual(result, group)
self.assertEqual(obj.get_corp_group_name(self.corp), group.name)
self.assertTrue(obj.corp_managed_groups.filter(pk=group.pk).exists())
def test_delete_alliance_managed_groups(self):
obj = AutogroupsConfig.objects.create()
obj.create_alliance_group(self.alliance)
self.assertTrue(obj.alliance_managed_groups.all().exists())
obj.delete_alliance_managed_groups()
self.assertFalse(obj.alliance_managed_groups.all().exists())
def test_delete_corp_managed_groups(self):
obj = AutogroupsConfig.objects.create()
obj.create_corp_group(self.corp)
self.assertTrue(obj.corp_managed_groups.all().exists())
obj.delete_corp_managed_groups()
self.assertFalse(obj.corp_managed_groups.all().exists())
def test_get_alliance_group_name(self):
obj = AutogroupsConfig()
obj.replace_spaces = True
obj.replace_spaces_with = '_'
result = obj.get_alliance_group_name(self.alliance)
self.assertEqual(result, 'Alliance_alliance_name')
def test_get_alliance_group_name_ticker(self):
obj = AutogroupsConfig()
obj.replace_spaces = True
obj.replace_spaces_with = '_'
obj.alliance_name_source = obj.OPT_TICKER
result = obj.get_alliance_group_name(self.alliance)
self.assertEqual(result, 'Alliance_TIKR')
def test_get_corp_group_name(self):
obj = AutogroupsConfig()
obj.replace_spaces = True
obj.replace_spaces_with = '_'
result = obj.get_corp_group_name(self.corp)
self.assertEqual(result, 'Corp_corp_name')
def test_get_corp_group_name_ticker(self):
obj = AutogroupsConfig()
obj.replace_spaces = True
obj.replace_spaces_with = '_'
obj.corp_name_source = obj.OPT_TICKER
result = obj.get_corp_group_name(self.corp)
self.assertEqual(result, 'Corp_TIKK')
def test__replace_spaces(self):
obj = AutogroupsConfig()
obj.replace_spaces = True
obj.replace_spaces_with = '*'
name = ' test name '
result = obj._replace_spaces(name)
self.assertEqual(result, 'test*name')

View File

@@ -0,0 +1,208 @@
from django.test import TestCase
from django.contrib.auth.models import Group, User
from allianceauth.tests.auth_utils import AuthUtils
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo
from ..models import AutogroupsConfig, ManagedAllianceGroup
from . import patch, disconnect_signals, connect_signals
class SignalsTestCase(TestCase):
def setUp(self):
disconnect_signals()
self.member = AuthUtils.create_member('test user')
state = AuthUtils.get_member_state()
self.char = EveCharacter.objects.create(
character_id='1234',
character_name='test character',
corporation_id='2345',
corporation_name='test corp',
corporation_ticker='tickr',
alliance_id='3456',
alliance_name='alliance name',
)
self.member.profile.main_character = self.char
self.member.profile.save()
self.alliance = EveAllianceInfo.objects.create(
alliance_id='3456',
alliance_name='alliance name',
alliance_ticker='TIKR',
executor_corp_id='2345',
)
self.corp = EveCorporationInfo.objects.create(
corporation_id='2345',
corporation_name='corp name',
corporation_ticker='TIKK',
member_count=10,
alliance=self.alliance,
)
state.member_alliances.add(self.alliance)
state.member_corporations.add(self.corp)
connect_signals()
@patch('.models.AutogroupsConfigManager.update_groups_for_user')
def test_check_groups_on_profile_update_state(self, update_groups_for_user):
# Trigger signal
self.member.profile.state = AuthUtils.get_guest_state()
self.member.profile.save()
self.assertTrue(update_groups_for_user.called)
self.assertEqual(update_groups_for_user.call_count, 1)
args, kwargs = update_groups_for_user.call_args
self.assertEqual(args[0], self.member)
@patch('.models.AutogroupsConfigManager.update_groups_for_user')
def test_check_groups_on_profile_update_main_character(self, update_groups_for_user):
char = EveCharacter.objects.create(
character_id='1266',
character_name='test character2',
corporation_id='2345',
corporation_name='test corp',
corporation_ticker='tickr',
alliance_id='3456',
alliance_name='alliance name',
)
# Trigger signal
self.member.profile.main_character = char
self.member.profile.save()
self.assertTrue(update_groups_for_user.called)
self.assertEqual(update_groups_for_user.call_count, 1)
args, kwargs = update_groups_for_user.call_args
self.assertEqual(args[0], self.member)
member = User.objects.get(pk=self.member.pk)
self.assertEqual(member.profile.state, AuthUtils.get_member_state())
@patch('.models.AutogroupsConfigManager.update_groups_for_user')
def test_check_groups_on_character_update(self, update_groups_for_user):
"""
Test update_groups_for_user is called when main_character properties
are changed.
"""
# Trigger signal
self.member.profile.main_character.corporation_id = '2300'
self.member.profile.main_character.save()
self.assertTrue(update_groups_for_user.called)
self.assertEqual(update_groups_for_user.call_count, 1)
args, kwargs = update_groups_for_user.call_args
self.assertEqual(args[0], self.member)
member = User.objects.get(pk=self.member.pk)
self.assertEqual(member.profile.state, AuthUtils.get_member_state())
@patch('.models.AutogroupsConfig.delete_corp_managed_groups')
@patch('.models.AutogroupsConfig.delete_alliance_managed_groups')
def test_pre_save_config_deletes_alliance_groups(self, delete_alliance_managed_groups, delete_corp_managed_groups):
"""
Test that delete_alliance_managed_groups is called when the alliance_groups
setting is toggled to False
"""
obj = AutogroupsConfig.objects.create(alliance_groups=True)
obj.create_alliance_group(self.alliance)
# Trigger signal
obj.alliance_groups = False
obj.save()
self.assertTrue(delete_alliance_managed_groups.called)
self.assertFalse(delete_corp_managed_groups.called)
self.assertEqual(delete_alliance_managed_groups.call_count, 1)
@patch('.models.AutogroupsConfig.delete_alliance_managed_groups')
@patch('.models.AutogroupsConfig.delete_corp_managed_groups')
def test_pre_save_config_deletes_corp_groups(self, delete_corp_managed_groups, delete_alliance_managed_groups):
"""
Test that delete_corp_managed_groups is called when the corp_groups
setting is toggled to False
"""
obj = AutogroupsConfig.objects.create(corp_groups=True)
obj.create_corp_group(self.corp)
# Trigger signal
obj.corp_groups = False
obj.save()
self.assertTrue(delete_corp_managed_groups.called)
self.assertFalse(delete_alliance_managed_groups.called)
self.assertEqual(delete_corp_managed_groups.call_count, 1)
@patch('.models.AutogroupsConfig.delete_alliance_managed_groups')
@patch('.models.AutogroupsConfig.delete_corp_managed_groups')
def test_pre_save_config_does_nothing(self, delete_corp_managed_groups, delete_alliance_managed_groups):
"""
Test groups arent deleted if we arent setting the enabled params to False
"""
obj = AutogroupsConfig.objects.create(corp_groups=True)
obj.create_corp_group(self.corp)
# Trigger signal
obj.alliance_groups = True
obj.save()
self.assertFalse(delete_corp_managed_groups.called)
self.assertFalse(delete_alliance_managed_groups.called)
@patch('.models.AutogroupsConfig.delete_alliance_managed_groups')
@patch('.models.AutogroupsConfig.delete_corp_managed_groups')
def test_pre_delete_config(self, delete_corp_managed_groups, delete_alliance_managed_groups):
"""
Test groups are deleted if config is deleted
"""
obj = AutogroupsConfig.objects.create()
# Trigger signal
obj.delete()
self.assertTrue(delete_corp_managed_groups.called)
self.assertTrue(delete_alliance_managed_groups.called)
@patch('.models.AutogroupsConfig.update_group_membership_for_state')
def test_autogroups_states_changed_add(self, update_group_membership_for_state):
"""
Test update_group_membership_for_state is called when a state is added to
the AutogroupsConfig
"""
obj = AutogroupsConfig.objects.create(alliance_groups=True)
state = AuthUtils.get_member_state()
# Trigger
obj.states.add(state)
self.assertTrue(update_group_membership_for_state.called)
self.assertEqual(update_group_membership_for_state.call_count, 1)
args, kwargs = update_group_membership_for_state.call_args
self.assertEqual(args[0], state)
@patch('.models.AutogroupsConfig.update_group_membership_for_state')
def test_autogroups_states_changed_remove(self, update_group_membership_for_state):
"""
Test update_group_membership_for_state is called when a state is removed from
the AutogroupsConfig
"""
obj = AutogroupsConfig.objects.create(alliance_groups=True)
state = AuthUtils.get_member_state()
disconnect_signals()
obj.states.add(state)
connect_signals()
# Trigger
obj.states.remove(state)
self.assertTrue(update_group_membership_for_state.called)
self.assertEqual(update_group_membership_for_state.call_count, 1)
args, kwargs = update_group_membership_for_state.call_args
self.assertEqual(args[0], state)

View File

@@ -0,0 +1,91 @@
import logging
from django.db import models
from . import providers
logger = logging.getLogger(__name__)
class EveCharacterProviderManager:
def get_character(self, character_id) -> providers.Character:
return providers.provider.get_character(character_id)
class EveCharacterManager(models.Manager):
provider = EveCharacterProviderManager()
def create_character(self, character_id):
return self.create_character_obj(self.provider.get_character(character_id))
def create_character_obj(self, character: providers.Character):
return self.create(
character_id=character.id,
character_name=character.name,
corporation_id=character.corp.id,
corporation_name=character.corp.name,
corporation_ticker=character.corp.ticker,
alliance_id=character.alliance.id,
alliance_name=character.alliance.name,
)
def update_character(self, character_id):
return self.get(character_id=character_id).update_character()
def get_character_by_id(self, char_id):
if self.filter(character_id=char_id).exists():
return self.get(character_id=char_id)
return None
class EveAllianceProviderManager:
def get_alliance(self, alliance_id) -> providers.Alliance:
return providers.provider.get_alliance(alliance_id)
class EveAllianceManager(models.Manager):
provider = EveAllianceProviderManager()
def create_alliance(self, alliance_id):
obj = self.create_alliance_obj(self.provider.get_alliance(alliance_id))
obj.populate_alliance()
return obj
def create_alliance_obj(self, alliance: providers.Alliance):
return self.create(
alliance_id=alliance.id,
alliance_name=alliance.name,
alliance_ticker=alliance.ticker,
executor_corp_id=alliance.executor_corp_id,
)
def update_alliance(self, alliance_id):
return self.get(alliance_id=alliance_id).update_alliance()
class EveCorporationProviderManager:
def get_corporation(self, corp_id) -> providers.Corporation:
return providers.provider.get_corp(corp_id)
class EveCorporationManager(models.Manager):
provider = EveCorporationProviderManager()
def create_corporation(self, corp_id):
return self.create_corporation_obj(self.provider.get_corporation(corp_id))
def create_corporation_obj(self, corp: providers.Corporation):
from .models import EveAllianceInfo
try:
alliance = EveAllianceInfo.objects.get(alliance_id=corp.alliance_id)
except EveAllianceInfo.DoesNotExist:
alliance = None
return self.create(
corporation_id=corp.id,
corporation_name=corp.name,
corporation_ticker=corp.ticker,
member_count=corp.members,
alliance=alliance,
)
def update_corporation(self, corp_id):
return self.get(corporation_id=corp_id).update_corporation(self.provider.get_corporation(corp_id))

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