Compare commits

...

76 Commits

Author SHA1 Message Date
Basraah
3de7a2ccd2 Version bump to v2.0.5 2018-07-10 02:27:32 +00:00
Basraah
9cc278df31 Merge branch 'corp_stat_fix' into 'master'
Corp Stats update to fix removal of character name endpoints

See merge request allianceauth/allianceauth!1092
2018-07-10 02:01:26 +00:00
Mike
1de3c989d7 fix tests with new endpoints (i think) 2018-06-24 17:41:27 -04:00
Mike
2e547945e2 Corp Stats update to fix removal of character name endpoints 2018-06-24 17:23:08 -04:00
Basraah
5b41d0995f Update README.md badges 2018-06-06 07:12:10 +00:00
Basraah
a7a2ffd16b Add .gitlab-ci.yml 2018-06-06 05:46:35 +00:00
Basraah
dbeda324e0 Update tox.ini for GitLab CI 2018-06-06 05:45:23 +00:00
Adarnof
ee9ed13a66 Remove reference to depreciated bad_gateway model.
Addresses #1078

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

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

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

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

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

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

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

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

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

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

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

Prevent including task_self repr in key name.

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

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

Closes #751

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

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

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

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

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

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

Apparently patch has issues.

Thanks @TargetZ3R0 and Discord devs <3
2018-03-19 18:08:24 -04:00
Adarnof
b4c395f116 Don't force token updates on main character checks. 2018-03-15 19:41:11 -04:00
Adarnof
a38116014d PyCharm defaults to venv 2018-03-15 19:38:52 -04:00
Adarnof
54223db1e9 Merge remote-tracking branch 'origin/patch1' 2018-03-15 19:36:49 -04:00
Adarnof
8a897abc7b Ensure service URL has protocol.
Thanks @jdrc
2018-03-11 01:08:02 -05:00
Adarnof
fe7b078ec8 Wait until token is deleted before assessing ownerships.
Hopefully this will fix the infinite recursion.
Elevate logging messages to Info so they appear in logs with the default configuration.
2018-03-09 11:47:28 -05:00
121 changed files with 3503 additions and 1036 deletions

2
.gitignore vendored
View File

@@ -8,6 +8,7 @@ __pycache__/
# Distribution / packaging
.Python
env/
venv/
build/
develop-eggs/
dist/
@@ -41,7 +42,6 @@ nosetests.xml
coverage.xml
# Translations
*.mo
*.pot
# Django stuff:

41
.gitlab-ci.yml Normal file
View File

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

View File

@@ -3,8 +3,9 @@ Alliance Auth
[![Join the chat at https://gitter.im/R4stl1n/allianceauth](https://badges.gitter.im/R4stl1n/allianceauth.svg)](https://gitter.im/R4stl1n/allianceauth?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Documentation Status](https://readthedocs.org/projects/allianceauth/badge/?version=latest)](http://allianceauth.readthedocs.io/?badge=latest)
[![Build Status](https://travis-ci.org/allianceauth/allianceauth.svg?branch=master)](https://travis-ci.org/allianceauth/allianceauth)
[![Coverage Status](https://coveralls.io/repos/github/allianceauth/allianceauth/badge.svg?branch=master)](https://coveralls.io/github/allianceauth/allianceauth?branch=master)
[![pipeline status](https://gitlab.com/allianceauth/allianceauth/badges/master/pipeline.svg)](https://gitlab.com/allianceauth/allianceauth/commits/master)
[![coverage report](https://gitlab.com/allianceauth/allianceauth/badges/master/coverage.svg)](https://gitlab.com/allianceauth/allianceauth/commits/master)
An auth system for EVE Online to help in-game organizations manage online service access.
@@ -23,6 +24,7 @@ Beta Testers / Bug Fixers:
- [ghoti](https://github.com/ghoti/)
- [mmolitor87](https://github.com/mmolitor87/)
- [TargetZ3R0](https://github.com/TargetZ3R0)
- [kaezon](https://github.com/kaezon/)
- [orbitroom](https://github.com/orbitroom/)
- [tehfiend](https://github.com/tehfiend/)

View File

@@ -1,7 +1,6 @@
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
__version__ = '2.0b3'
__version__ = '2.0.5'
NAME = 'Alliance Auth v%s' % __version__
default_app_config = 'allianceauth.apps.AllianceAuthConfig'

View File

@@ -6,7 +6,7 @@ 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.authentication.models import State, get_guest_state, CharacterOwnership, UserProfile, OwnershipRecord
from allianceauth.hooks import get_hooks
from allianceauth.eveonline.models import EveCharacter
from django.forms import ModelForm
@@ -160,12 +160,23 @@ class StateAdmin(admin.ModelAdmin):
return obj.userprofile_set.all().count()
@admin.register(CharacterOwnership)
class CharacterOwnershipAdmin(admin.ModelAdmin):
class BaseOwnershipAdmin(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 get_readonly_fields(self, request, obj=None):
if obj and obj.pk:
return 'owner_hash', 'character'
return tuple()
@admin.register(OwnershipRecord)
class OwnershipRecordAdmin(BaseOwnershipAdmin):
list_display = BaseOwnershipAdmin.list_display + ('created',)
@admin.register(CharacterOwnership)
class CharacterOwnershipAdmin(BaseOwnershipAdmin):
def has_add_permission(self, request):
return False

View File

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

View File

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

View File

@@ -43,7 +43,7 @@ def create_member_group(apps, schema_editor):
member_state_name = getattr(settings, 'DEFAULT_AUTH_GROUP', 'Member')
try:
g = Group.objects.get(name=member_state_name)
g, _ = Group.objects.get_or_create(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()]
@@ -51,7 +51,7 @@ def create_member_group(apps, schema_editor):
# move users back
for profile in state.userprofile_set.all().select_related('user'):
profile.user.groups.add(g.pk)
except (Group.DoesNotExist, State.DoesNotExist):
except State.DoesNotExist:
pass
@@ -67,7 +67,7 @@ def create_blue_state(apps, schema_editor):
# 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()
g.delete()
except Group.DoesNotExist:
pass
@@ -84,7 +84,7 @@ def create_blue_group(apps, schema_editor):
blue_state_name = getattr(settings, 'DEFAULT_BLUE_GROUP', 'Blue')
try:
g = Group.objects.get(name=blue_state_name)
g, _ = Group.objects.get_or_create(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()]
@@ -92,10 +92,15 @@ def create_blue_group(apps, schema_editor):
# move users back
for profile in state.userprofile_set.all().select_related('user'):
profile.user.groups.add(g.pk)
except (Group.DoesNotExist, State.DoesNotExist):
except State.DoesNotExist:
pass
def purge_tokens(apps, schema_editor):
Token = apps.get_model('esi', 'Token')
Token.objects.filter(refresh_token__isnull=True).delete()
def populate_ownerships(apps, schema_editor):
Token = apps.get_model('esi', 'Token')
CharacterOwnership = apps.get_model('authentication', 'CharacterOwnership')
@@ -128,15 +133,24 @@ def create_profiles(apps, schema_editor):
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')
blue_state_name = getattr(settings, 'DEFAULT_BLUE_GROUP', 'Blue')
member_state_name = getattr(settings, 'DEFAULT_AUTH_GROUP', 'Member')
states = {
'Member': State.objects.get(name=member_state_name),
'Blue': State.objects.get(name=blue_state_name),
}
guest_state = State.objects.get(name='Guest')
for auth in auths:
# carry states and mains forward
state = State.objects.get(name=auth.state if auth.state else 'Guest')
state = states.get(auth.state, guest_state)
char = EveCharacter.objects.get(character_id=auth.main_char_id)
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)
UserProfile.objects.create(user=auth.user, state=guest_state)
def recreate_authservicesinfo(apps, schema_editor):
@@ -144,6 +158,14 @@ def recreate_authservicesinfo(apps, schema_editor):
UserProfile = apps.get_model('authentication', 'UserProfile')
User = apps.get_model('auth', 'User')
blue_state_name = getattr(settings, 'DEFAULT_BLUE_GROUP', 'Blue')
member_state_name = getattr(settings, 'DEFAULT_AUTH_GROUP', 'Member')
states = {
member_state_name: 'Member',
blue_state_name: 'Blue',
}
# recreate all missing AuthServicesInfo models
AuthServicesInfo.objects.bulk_create([AuthServicesInfo(user_id=u.pk) for u in User.objects.all()])
@@ -154,8 +176,8 @@ def recreate_authservicesinfo(apps, schema_editor):
# 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})
state__name__in=[member_state_name, blue_state_name]).select_related('user', 'state'):
AuthServicesInfo.objects.update_or_create(user=profile.user, defaults={'state': states[profile.state.name]})
def disable_passwords(apps, schema_editor):
@@ -221,6 +243,7 @@ class Migration(migrations.Migration):
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(purge_tokens, migrations.RunPython.noop),
migrations.RunPython(populate_ownerships, migrations.RunPython.noop),
migrations.RunPython(create_profiles, recreate_authservicesinfo),
migrations.RemoveField(

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
import logging
from .models import CharacterOwnership, UserProfile, get_guest_state, State
from .models import CharacterOwnership, UserProfile, get_guest_state, State, OwnershipRecord
from django.contrib.auth.models import User
from django.db.models import Q
from django.db.models.signals import pre_save, post_save, pre_delete, m2m_changed
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed
from django.dispatch import receiver, Signal
from esi.models import Token
@@ -103,29 +103,23 @@ def record_character_ownership(sender, instance, created, *args, **kwargs):
@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()
try:
if instance.user.profile.main_character == instance.character:
logger.info("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()
except UserProfile.DoesNotExist:
# a user is being deleted
pass
@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(post_delete, sender=Token)
def validate_ownership(sender, instance, *args, **kwargs):
if not Token.objects.filter(character_owner_hash=instance.character_owner_hash).filter(refresh_token__isnull=False).exists():
logger.info("No remaining tokens to validate ownership of character {0}. Revoking ownership.".format(instance.character_name))
CharacterOwnership.objects.filter(owner_hash=instance.character_owner_hash).delete()
@receiver(pre_save, sender=User)
@@ -153,3 +147,15 @@ def check_state_on_character_update(sender, instance, *args, **kwargs):
except UserProfile.DoesNotExist:
logger.debug("Character {0} is not a main character. No state assessment required.".format(instance))
pass
@receiver(post_save, sender=CharacterOwnership)
def ownership_record_creation(sender, instance, created, *args, **kwargs):
if created:
records = OwnershipRecord.objects.filter(owner_hash=instance.owner_hash).filter(character=instance.character)
if records.exists():
if records[0].user == instance.user: # most recent record is sorted first
logger.debug("Already have ownership record of {0} by user {1}".format(instance.character, instance.user))
return
logger.info("Character {0} has a new owner {1}. Creating ownership record.".format(instance.character, instance.user))
OwnershipRecord.objects.create(user=instance.user, character=instance.character, owner_hash=instance.owner_hash)

View File

@@ -1,6 +1,6 @@
import logging
from esi.errors import TokenExpiredError, TokenInvalidError
from esi.errors import TokenExpiredError, TokenInvalidError, IncompleteResponseError
from esi.models import Token
from celery import shared_task
@@ -20,13 +20,19 @@ def check_character_ownership(owner_hash):
except (TokenExpiredError, TokenInvalidError):
t.delete()
continue
if t.character_owner_hash == old_hash:
except (KeyError, IncompleteResponseError):
# We can't validate the hash hasn't changed but also can't assume it has. Abort for now.
logger.warning("Failed to validate owner hash of {0} due to problems contacting SSO servers.".format(
tokens[0].character_name))
break
else:
logger.info('Character %s has changed ownership. Revoking %s tokens.' % (t.character_name, tokens.count()))
if not t.character_owner_hash == old_hash:
logger.info(
'Character %s has changed ownership. Revoking %s tokens.' % (t.character_name, tokens.count()))
tokens.delete()
else:
break
if not Token.objects.filter(character_owner_hash=owner_hash).exists():
logger.info('No tokens found with owner hash %s. Revoking ownership.' % owner_hash)
CharacterOwnership.objects.filter(owner_hash=owner_hash).delete()

View File

@@ -1,19 +1,21 @@
from unittest import mock
from io import StringIO
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 .models import CharacterOwnership, UserProfile, State, get_guest_state, OwnershipRecord
from .backends import StateBackend
from .tasks import check_character_ownership
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo
from esi.models import Token
from esi.errors import IncompleteResponseError
from allianceauth.authentication.decorators import main_character_required
from django.test.client import RequestFactory
from django.http.response import HttpResponse
from django.contrib.auth.models import AnonymousUser
from django.conf import settings
from django.shortcuts import reverse
from django.core.management import call_command
from urllib import parse
MODULE_PATH = 'allianceauth.authentication'
@@ -90,6 +92,7 @@ class BackendTestCase(TestCase):
corporation_ticker='CORP',
)
cls.user = AuthUtils.create_user('test_user', disconnect_signals=True)
cls.old_user = AuthUtils.create_user('old_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')
@@ -113,6 +116,14 @@ class BackendTestCase(TestCase):
self.assertEqual(user.username, 'Unclaimed_Character')
self.assertEqual(user.profile.main_character, self.unclaimed_character)
def test_authenticate_character_record(self):
t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='4')
record = OwnershipRecord.objects.create(user=self.old_user, character=self.unclaimed_character, owner_hash='4')
user = StateBackend().authenticate(t)
self.assertEqual(user, self.old_user)
self.assertTrue(CharacterOwnership.objects.filter(owner_hash='4', user=self.old_user).exists())
self.assertTrue(user.profile.main_character)
def test_iterate_username(self):
t = Token(character_id=self.unclaimed_character.character_id,
character_name=self.unclaimed_character.character_name, character_owner_hash='3')
@@ -185,28 +196,6 @@ class CharacterOwnershipTestCase(TestCase):
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
@@ -341,3 +330,73 @@ class StateTestCase(TestCase):
self.user.save()
self._refresh_user()
self.assertEquals(self.user.profile.state, self.member_state)
class CharacterOwnershipCheckTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = AuthUtils.create_user('test_user', disconnect_signals=True)
AuthUtils.add_main_character(cls.user, 'Test Character', '1', corp_id='1', alliance_id='1',
corp_name='Test Corp', alliance_name='Test Alliance')
cls.character = EveCharacter.objects.get(character_id='1')
cls.token = Token.objects.create(
user=cls.user,
character_id='1',
character_name='Test',
character_owner_hash='1',
)
cls.ownership = CharacterOwnership.objects.get(character=cls.character)
@mock.patch(MODULE_PATH + '.tasks.Token.update_token_data')
def test_no_change_owner_hash(self, update_token_data):
# makes sure the ownership isn't delete if owner hash hasn't changed
check_character_ownership(self.ownership)
self.assertTrue(CharacterOwnership.objects.filter(user=self.user).filter(character=self.character).exists())
@mock.patch(MODULE_PATH + '.tasks.Token.update_token_data')
def test_unable_to_update_token_data(self, update_token_data):
# makes sure ownerships and tokens aren't hellpurged when there's problems with the SSO servers
update_token_data.side_effect = IncompleteResponseError()
check_character_ownership(self.ownership)
self.assertTrue(CharacterOwnership.objects.filter(user=self.user).filter(character=self.character).exists())
update_token_data.side_effect = KeyError()
check_character_ownership(self.ownership)
self.assertTrue(CharacterOwnership.objects.filter(user=self.user).filter(character=self.character).exists())
@mock.patch(MODULE_PATH + '.tasks.Token.update_token_data')
@mock.patch(MODULE_PATH + '.tasks.Token.delete')
@mock.patch(MODULE_PATH + '.tasks.Token.objects.exists')
@mock.patch(MODULE_PATH + '.tasks.CharacterOwnership.objects.filter')
def test_owner_hash_changed(self, filter, exists, delete, update_token_data):
# makes sure the ownership is revoked when owner hash changes
filter.return_value.exists.return_value = False
check_character_ownership(self.ownership)
self.assertTrue(filter.return_value.delete.called)
class ManagementCommandTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = AuthUtils.create_user('test user', disconnect_signals=True)
AuthUtils.add_main_character(cls.user, 'test character', '1', '2', 'test corp', 'test')
character = UserProfile.objects.get(user=cls.user).main_character
CharacterOwnership.objects.create(user=cls.user, character=character, owner_hash='test')
def setUp(self):
self.stdout = StringIO()
def test_ownership(self):
call_command('checkmains', stdout=self.stdout)
self.assertFalse(UserProfile.objects.filter(main_character__isnull=True).count())
self.assertNotIn(self.user.username, self.stdout.getvalue())
self.assertIn('All main characters', self.stdout.getvalue())
def test_no_ownership(self):
user = AuthUtils.create_user('v1 user', disconnect_signals=True)
AuthUtils.add_main_character(user, 'v1 character', '10', '20', 'test corp', 'test')
self.assertFalse(UserProfile.objects.filter(main_character__isnull=True).count())
call_command('checkmains', stdout=self.stdout)
self.assertEqual(UserProfile.objects.filter(main_character__isnull=True).count(), 1)
self.assertIn(user.username, self.stdout.getvalue())

View File

@@ -95,20 +95,33 @@ class RegistrationView(BaseRegistrationView):
form_class = RegistrationForm
success_url = 'authentication:dashboard'
def dispatch(self, *args, **kwargs):
def get_success_url(self, user):
if not getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True):
return 'authentication:dashboard', (), {}
return super().get_success_url(user)
def dispatch(self, request, *args, **kwargs):
# We're storing a key in the session to pass user information from OAuth response. Make sure it's there.
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)
if not getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True):
# Keep the request so the user can be automagically logged in.
setattr(self, 'request', request)
return super(RegistrationView, self).dispatch(request, *args, **kwargs)
def register(self, form):
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)
if getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True):
# Go to Step 3
self.send_activation_email(user)
else:
user.is_active = True
user.save()
login(self.request, user, 'allianceauth.authentication.backends.StateBackend')
return user
def get_activation_key(self, user):

View File

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

View File

@@ -12,6 +12,16 @@ 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')
"""
Swagger spec operations:
Character
get_characters_character_id
get_corporations_corporation_id_members
Universe
post_universe_names
"""
logger = logging.getLogger(__name__)
@@ -40,19 +50,18 @@ class CorpStats(models.Model):
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(
member_ids = 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_name_chunks = [c.Universe.post_universe_names(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})
member_list.update({m['id']: m['name'] for m in name_chunk})
# bulk create new member models
missing_members = [m_id for m_id in member_ids if

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -11,7 +11,7 @@
{% 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 class="text-center col-lg-6"><img class="ra-avatar" src="{{ corpstats.corp.alliance.logo_url_128 }}">
</td>
{% endif %}
</tr>
@@ -202,4 +202,4 @@
});
});
{% endblock %}
{% endblock %}

View File

@@ -85,8 +85,8 @@ class CorpStatsUpdateTestCase(TestCase):
@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'}]
SwaggerClient.from_spec.return_value.Corporation.get_corporations_corporation_id_members.return_value.result.return_value = [1]
SwaggerClient.from_spec.return_value.Universe.post_universe_names.return_value.result.return_value = [{'id': 1, 'name': 'test character'}]
self.corpstats.update()
self.assertTrue(CorpMember.objects.filter(character_id='1', character_name='test character', corpstats=self.corpstats).exists())
@@ -94,8 +94,8 @@ class CorpStatsUpdateTestCase(TestCase):
def test_update_remove_member(self, SwaggerClient):
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'}]
SwaggerClient.from_spec.return_value.Corporation.get_corporations_corporation_id_members.return_value.result.return_value = [1]
SwaggerClient.from_spec.return_value.Universe.post_universe_names.return_value.result.return_value = [{'id': 1, 'name': 'test character'}]
self.corpstats.update()
self.assertFalse(CorpMember.objects.filter(character_id='2', corpstats=self.corpstats).exists())

View File

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

View File

@@ -11,7 +11,7 @@ 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)
.filter(profile__state_id=state.pk)
class AutogroupsConfigManager(models.Manager):
@@ -39,7 +39,12 @@ class AutogroupsConfigManager(models.Manager):
if state is None:
state = user.profile.state
for config in self.filter(states=state):
config.update_group_membership_for_user(user)
# grant user new groups for their state
config.update_group_membership_for_user(user)
for config in self.exclude(states=state):
# ensure user does not have groups from previous state
config.remove_user_from_alliance_groups(user)
config.remove_user_from_corp_groups(user)
class AutogroupsConfig(models.Model):
@@ -119,8 +124,9 @@ class AutogroupsConfig(models.Model):
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))
logger.debug('User {} main characters alliance does not exist in the database. Creating.'.format(user))
alliance = EveAllianceInfo.objects.create_alliance(user.profile.main_character.alliance_id)
group = self.get_alliance_group(alliance)
except AttributeError:
logger.warning('User {} does not have a main character. Group membership not updated'.format(user))
finally:
@@ -139,8 +145,9 @@ class AutogroupsConfig(models.Model):
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))
logger.debug('User {} main characters corporation does not exist in the database. Creating.'.format(user))
corp = EveCorporationInfo.objects.create_corporation(user.profile.main_character.corporation_id)
group = self.get_corp_group(corp)
except AttributeError:
logger.warning('User {} does not have a main character. Group membership not updated'.format(user))
finally:

View File

@@ -2,6 +2,7 @@ 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 allianceauth.eveonline.models import EveCharacter
from .models import AutogroupsConfig
@@ -45,9 +46,7 @@ def check_groups_on_profile_update(sender, instance, created, *args, **kwargs):
"""
Trigger check when main character or state changes.
"""
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)
AutogroupsConfig.objects.update_groups_for_user(instance.user)
@receiver(m2m_changed, sender=AutogroupsConfig.states.through)
@@ -64,3 +63,13 @@ def autogroups_states_changed(sender, instance, action, reverse, model, pk_set,
except State.DoesNotExist:
# Deleted States handled by the profile state change
pass
@receiver(post_save, sender=EveCharacter)
def check_groups_on_character_update(sender, instance, created, *args, **kwargs):
if not created:
try:
profile = UserProfile.objects.prefetch_related('user').get(main_character_id=instance.pk)
AutogroupsConfig.objects.update_groups_for_user(profile.user)
except UserProfile.DoesNotExist:
pass

View File

@@ -1,8 +1,7 @@
from unittest import mock
from 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 allianceauth.authentication.signals import reassess_on_profile_save
from .. import signals
from ..models import AutogroupsConfig
@@ -14,6 +13,7 @@ def patch(target, *args, **kwargs):
def connect_signals():
post_save.connect(receiver=reassess_on_profile_save, sender=UserProfile)
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)
@@ -21,6 +21,7 @@ def connect_signals():
def disconnect_signals():
post_save.disconnect(receiver=reassess_on_profile_save, sender=UserProfile)
pre_save.disconnect(receiver=signals.pre_save_config, sender=AutogroupsConfig)
pre_delete.disconnect(receiver=signals.pre_delete_config, sender=AutogroupsConfig)
post_save.disconnect(receiver=signals.check_groups_on_profile_update, sender=UserProfile)

View File

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

View File

@@ -16,8 +16,6 @@ class AutogroupsConfigTestCase(TestCase):
# Disconnect signals
disconnect_signals()
self.member = AuthUtils.create_member('test user')
state = AuthUtils.get_member_state()
self.alliance = EveAllianceInfo.objects.create(
@@ -38,6 +36,8 @@ class AutogroupsConfigTestCase(TestCase):
state.member_alliances.add(self.alliance)
state.member_corporations.add(self.corp)
self.member = AuthUtils.create_member('test user')
def tearDown(self):
# Reconnect signals
connect_signals()

View File

@@ -1,11 +1,11 @@
from django.test import TestCase
from django.contrib.auth.models import Group, User
from django.contrib.auth.models import User
from allianceauth.tests.auth_utils import AuthUtils
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo
from ..models import AutogroupsConfig, ManagedAllianceGroup
from ..models import AutogroupsConfig
from . import patch, disconnect_signals, connect_signals
@@ -13,8 +13,6 @@ 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(
@@ -27,9 +25,6 @@ class SignalsTestCase(TestCase):
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',
@@ -47,13 +42,17 @@ class SignalsTestCase(TestCase):
state.member_alliances.add(self.alliance)
state.member_corporations.add(self.corp)
self.member = AuthUtils.create_member('test user')
self.member.profile.main_character = self.char
self.member.profile.save()
connect_signals()
@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.member.profile.assign_state(state=AuthUtils.get_guest_state())
self.assertTrue(update_groups_for_user.called)
self.assertEqual(update_groups_for_user.call_count, 1)
@@ -71,10 +70,10 @@ class SignalsTestCase(TestCase):
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

View File

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

View File

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

View File

@@ -84,9 +84,10 @@ class EveCharacter(models.Model):
character_name = models.CharField(max_length=254, unique=True)
corporation_id = models.CharField(max_length=254)
corporation_name = models.CharField(max_length=254)
corporation_ticker = models.CharField(max_length=254)
corporation_ticker = models.CharField(max_length=5)
alliance_id = models.CharField(max_length=254, blank=True, null=True, default='')
alliance_name = models.CharField(max_length=254, blank=True, null=True, default='')
alliance_ticker = models.CharField(max_length=5, blank=True, null=True, default='')
objects = EveCharacterManager()
provider = EveCharacterProviderManager()
@@ -120,6 +121,7 @@ class EveCharacter(models.Model):
self.corporation_ticker = character.corp.ticker
self.alliance_id = character.alliance.id
self.alliance_name = character.alliance.name
self.alliance_ticker = getattr(character.alliance, 'ticker', None)
self.save()
return self

View File

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

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -22,6 +22,16 @@ from allianceauth.eveonline.models import EveCharacter
from allianceauth.eveonline.models import EveCorporationInfo
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json')
"""
Swagger spec operations:
get_characters_character_id_location
get_characters_character_id_ship
get_universe_systems_system_id
get_universe_stations_station_id
get_universe_structures_structure_id
"""
logger = logging.getLogger(__name__)

View File

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

View File

@@ -155,7 +155,7 @@
<span class="glyphicon glyphicon-eye-open"></span>
</a>
{% if perms.hrapplications.delete_application %}
<a href="(% url 'hrapplications:remove' app.id %}"
<a href="{% url 'hrapplications:remove' app.id %}"
class="btn btn-danger">
<span class="glyphicon glyphicon-remove"></span>
</a>

View File

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

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -41,11 +41,11 @@ CELERYBEAT_SCHEDULER = "django_celery_beat.schedulers.DatabaseScheduler"
CELERYBEAT_SCHEDULE = {
'esi_cleanup_callbackredirect': {
'task': 'esi.tasks.cleanup_callbackredirect',
'schedule': crontab(hour='*/4'),
'schedule': crontab(minute=0, hour='*/4'),
},
'esi_cleanup_token': {
'task': 'esi.tasks.cleanup_token',
'schedule': crontab(day_of_month='*/1'),
'schedule': crontab(minute=0, hour=0),
},
'run_model_update': {
'task': 'allianceauth.eveonline.tasks.run_model_update',
@@ -53,7 +53,7 @@ CELERYBEAT_SCHEDULE = {
},
'check_all_character_ownership': {
'task': 'allianceauth.authentication.tasks.check_all_character_ownership',
'schedule': crontab(hour='*/4'),
'schedule': crontab(minute=0, hour='*/4'),
}
}
@@ -82,6 +82,7 @@ ugettext = lambda s: s
LANGUAGES = (
('en', ugettext('English')),
('de', ugettext('German')),
('es', ugettext('Spanish')),
)
TEMPLATES = [
@@ -166,7 +167,6 @@ CACHES = {
}
}
SECRET_KEY = 'this is a very bad secret key you should change'
DEBUG = True
ALLOWED_HOSTS = ['*']
DATABASES = {
@@ -194,6 +194,8 @@ LOGIN_TOKEN_SCOPES = ['publicData']
# number of days email verification links are valid for
ACCOUNT_ACTIVATION_DAYS = 1
ESI_API_URL = 'https://esi.evetech.net/'
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
@@ -235,5 +237,9 @@ LOGGING = {
'handlers': ['log_file', 'console'],
'level': 'ERROR',
},
'esi': {
'handlers': ['log_file', 'console'],
'level': 'DEBUG',
},
}
}

View File

@@ -41,10 +41,13 @@ ESI_SSO_CLIENT_ID = ''
ESI_SSO_CLIENT_SECRET = ''
ESI_SSO_CALLBACK_URL = ''
# Emails are validated before new users can log in.
# It's recommended to use a free service like SparkPost or Mailgun to send email.
# By default emails are validated before new users can log in.
# It's recommended to use a free service like SparkPost or Elastic Email to send email.
# https://www.sparkpost.com/docs/integrations/django/
# https://elasticemail.com/resources/settings/smtp-api/
# Set the default from email to something like 'noreply@example.com'
# Email validation can be turned off by uncommenting the line below. This can break some services.
# REGISTRATION_VERIFY_EMAIL = False
EMAIL_HOST = ''
EMAIL_PORT = 587
EMAIL_HOST_USER = ''

View File

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

View File

@@ -32,7 +32,7 @@ SCOPES = [
'guilds.join',
]
GROUP_CACHE_MAX_AGE = int(getattr(settings, 'DISCORD_GROUP_CACHE_MAX_AGE', 2 * 60 * 60)) # 2 hours default
GROUP_CACHE_MAX_AGE = getattr(settings, 'DISCORD_GROUP_CACHE_MAX_AGE', 2 * 60 * 60) # 2 hours default
class DiscordApiException(Exception):
@@ -204,7 +204,7 @@ class DiscordOAuthManager:
'access_token': token,
}
if nickname:
data['nick'] = nickname
data['nick'] = DiscordOAuthManager._sanitize_name(nickname)
custom_headers['authorization'] = 'Bot ' + settings.DISCORD_BOT_TOKEN
r = requests.put(path, headers=custom_headers, json=data)
logger.debug("Got status code %s after joining Discord server" % r.status_code)
@@ -219,22 +219,18 @@ class DiscordOAuthManager:
@staticmethod
@api_backoff
def update_nickname(user_id, nickname):
try:
nickname = DiscordOAuthManager._sanitize_name(nickname)
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
data = {'nick': nickname}
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id)
r = requests.patch(path, headers=custom_headers, json=data)
logger.debug("Got status code %s after setting nickname for Discord user ID %s (%s)" % (
r.status_code, user_id, nickname))
if r.status_code == 404:
logger.warn("Discord user ID %s could not be found in server." % user_id)
return True
r.raise_for_status()
nickname = DiscordOAuthManager._sanitize_name(nickname)
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
data = {'nick': nickname}
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id)
r = requests.patch(path, headers=custom_headers, json=data)
logger.debug("Got status code %s after setting nickname for Discord user ID %s (%s)" % (
r.status_code, user_id, nickname))
if r.status_code == 404:
logger.warn("Discord user ID %s could not be found in server." % user_id)
return True
except:
logger.exception("Failed to set nickname for Discord user ID %s (%s)" % (user_id, nickname))
return False
r.raise_for_status()
return True
@staticmethod
def delete_user(user_id):
@@ -301,13 +297,38 @@ class DiscordOAuthManager:
def _create_group(name):
return DiscordOAuthManager.__generate_role(name)
@staticmethod
def _get_user(user_id):
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id)
r = requests.get(path, headers=custom_headers)
r.raise_for_status()
return r.json()
@staticmethod
def _get_user_roles(user_id):
user = DiscordOAuthManager._get_user(user_id)
return user['roles']
@staticmethod
def _modify_user_role(user_id, role_id, method):
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id) + "/roles/" + str(
role_id)
r = getattr(requests, method)(path, headers=custom_headers)
r.raise_for_status()
logger.debug("%s role %s for user %s" % (method, role_id, user_id))
@staticmethod
@api_backoff
def update_groups(user_id, groups):
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
group_ids = [DiscordOAuthManager._group_name_to_id(DiscordOAuthManager._sanitize_group_name(g)) for g in groups]
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id)
data = {'roles': group_ids}
r = requests.patch(path, headers=custom_headers, json=data)
logger.debug("Received status code %s after setting user roles" % r.status_code)
r.raise_for_status()
user_group_ids = DiscordOAuthManager._get_user_roles(user_id)
for g in group_ids:
if g not in user_group_ids:
DiscordOAuthManager._modify_user_role(user_id, g, 'put')
time.sleep(1) # we're gonna be hammering the API here
for g in user_group_ids:
if g not in group_ids:
DiscordOAuthManager._modify_user_role(user_id, g, 'delete')
time.sleep(1)

View File

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

View File

@@ -9,6 +9,7 @@ from requests.exceptions import HTTPError
from allianceauth.services.hooks import NameFormatter
from .manager import DiscordOAuthManager, DiscordApiBackoff
from .models import DiscordUser
from allianceauth.services.tasks import QueueOnce
logger = logging.getLogger(__name__)
@@ -58,8 +59,8 @@ class DiscordTasks:
return True
@staticmethod
@shared_task(bind=True, name='discord.update_groups')
def update_groups(task_self, pk):
@shared_task(bind=True, name='discord.update_groups', base=QueueOnce)
def update_groups(self, pk):
user = User.objects.get(pk=pk)
logger.debug("Updating discord groups for user %s" % user)
if DiscordTasks.has_account(user):
@@ -70,7 +71,7 @@ class DiscordTasks:
except DiscordApiBackoff as bo:
logger.info("Discord group sync API back off for %s, "
"retrying in %s seconds" % (user, bo.retry_after_seconds))
raise task_self.retry(countdown=bo.retry_after_seconds)
raise self.retry(countdown=bo.retry_after_seconds)
except HTTPError as e:
if e.response.status_code == 404:
try:
@@ -81,9 +82,9 @@ class DiscordTasks:
finally:
raise e
except Exception as e:
if task_self:
if self:
logger.exception("Discord group sync failed for %s, retrying in 10 mins" % user)
raise task_self.retry(countdown=60 * 10)
raise self.retry(countdown=60 * 10)
else:
# Rethrow
raise e
@@ -99,8 +100,8 @@ class DiscordTasks:
DiscordTasks.update_groups.delay(discord_user.user.pk)
@staticmethod
@shared_task(bind=True, name='discord.update_nickname')
def update_nickname(task_self, pk):
@shared_task(bind=True, name='discord.update_nickname', base=QueueOnce)
def update_nickname(self, pk):
user = User.objects.get(pk=pk)
logger.debug("Updating discord nickname for user %s" % user)
if DiscordTasks.has_account(user):
@@ -112,11 +113,11 @@ class DiscordTasks:
except DiscordApiBackoff as bo:
logger.info("Discord nickname update API back off for %s, "
"retrying in %s seconds" % (user, bo.retry_after_seconds))
raise task_self.retry(countdown=bo.retry_after_seconds)
raise self.retry(countdown=bo.retry_after_seconds)
except Exception as e:
if task_self:
if self:
logger.exception("Discord nickname sync failed for %s, retrying in 10 mins" % user)
raise task_self.retry(countdown=60 * 10)
raise self.retry(countdown=60 * 10)
else:
# Rethrow
raise e

View File

@@ -327,54 +327,54 @@ class DiscordManagerTestCase(TestCase):
# Assert
self.assertTrue(result)
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._get_user_roles')
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._get_groups')
@requests_mock.Mocker()
def test_update_groups(self, group_cache, m):
def test_update_groups(self, group_cache, user_roles, m):
# Arrange
groups = ['Member', 'Blue', 'SpecialGroup']
group_cache.return_value = [{'id': 111, 'name': 'Member'},
{'id': 222, 'name': 'Blue'},
{'id': 333, 'name': 'SpecialGroup'},
{'id': 444, 'name': 'NotYourGroup'}]
group_cache.return_value = [{'id': '111', 'name': 'Member'},
{'id': '222', 'name': 'Blue'},
{'id': '333', 'name': 'SpecialGroup'},
{'id': '444', 'name': 'NotYourGroup'}]
user_roles.return_value = ['444']
headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
user_id = 12345
request_url = '{}/guilds/{}/members/{}'.format(manager.DISCORD_URL, settings.DISCORD_GUILD_ID, user_id)
user_request_url = '{}/guilds/{}/members/{}'.format(manager.DISCORD_URL, settings.DISCORD_GUILD_ID, user_id)
group_request_urls = ['{}/guilds/{}/members/{}/roles/{}'.format(manager.DISCORD_URL, settings.DISCORD_GUILD_ID, user_id, g['id']) for g in group_cache.return_value]
m.patch(request_url,
request_headers=headers)
m.patch(user_request_url, request_headers=headers)
[m.put(url, request_headers=headers) for url in group_request_urls[:-1]]
m.delete(group_request_urls[-1], request_headers=headers)
# Act
DiscordOAuthManager.update_groups(user_id, groups)
# Assert
self.assertEqual(len(m.request_history), 1, 'Must be one HTTP call made')
history = json.loads(m.request_history[0].text)
self.assertIn('roles', history, "'The request must send JSON object with the 'roles' key")
self.assertIn(111, history['roles'], 'The group id 111 must be added to the request')
self.assertIn(222, history['roles'], 'The group id 222 must be added to the request')
self.assertIn(333, history['roles'], 'The group id 333 must be added to the request')
self.assertNotIn(444, history['roles'], 'The group id 444 must NOT be added to the request')
self.assertEqual(len(m.request_history), 4, 'Must be 4 HTTP calls made')
@mock.patch(MODULE_PATH + '.manager.cache')
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._get_groups')
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._get_user_roles')
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._group_name_to_id')
@requests_mock.Mocker()
def test_update_groups_backoff(self, group_cache, djcache, m):
def test_update_groups_backoff(self, name_to_id, user_groups, djcache, m):
# Arrange
groups = ['Member']
group_cache.return_value = [{'id': 111, 'name': 'Member'}]
user_groups.return_value = []
name_to_id.return_value = '111'
headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
user_id = 12345
request_url = '{}/guilds/{}/members/{}'.format(manager.DISCORD_URL, settings.DISCORD_GUILD_ID, user_id)
request_url = '{}/guilds/{}/members/{}/roles/{}'.format(manager.DISCORD_URL, settings.DISCORD_GUILD_ID, user_id, name_to_id.return_value)
djcache.get.return_value = None # No existing backoffs in cache
m.patch(request_url,
request_headers=headers,
headers={'Retry-After': '200000'},
status_code=429)
m.put(request_url,
request_headers=headers,
headers={'Retry-After': '200000'},
status_code=429)
# Act & Assert
with self.assertRaises(manager.DiscordApiBackoff) as bo:
@@ -391,23 +391,25 @@ class DiscordManagerTestCase(TestCase):
self.assertTrue(datetime.datetime.strptime(args[1], manager.cache_time_format) > datetime.datetime.now())
@mock.patch(MODULE_PATH + '.manager.cache')
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._get_groups')
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._get_user_roles')
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._group_name_to_id')
@requests_mock.Mocker()
def test_update_groups_global_backoff(self, group_cache, djcache, m):
def test_update_groups_global_backoff(self, name_to_id, user_groups, djcache, m):
# Arrange
groups = ['Member']
group_cache.return_value = [{'id': 111, 'name': 'Member'}]
user_groups.return_value = []
name_to_id.return_value = '111'
headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
user_id = 12345
request_url = '{}/guilds/{}/members/{}'.format(manager.DISCORD_URL, settings.DISCORD_GUILD_ID, user_id)
request_url = '{}/guilds/{}/members/{}/roles/{}'.format(manager.DISCORD_URL, settings.DISCORD_GUILD_ID, user_id, name_to_id.return_value)
djcache.get.return_value = None # No existing backoffs in cache
m.patch(request_url,
request_headers=headers,
headers={'Retry-After': '200000', 'X-RateLimit-Global': 'true'},
status_code=429)
m.put(request_url,
request_headers=headers,
headers={'Retry-After': '200000', 'X-RateLimit-Global': 'true'},
status_code=429)
# Act & Assert
with self.assertRaises(manager.DiscordApiBackoff) as bo:

View File

@@ -7,7 +7,7 @@ from hashlib import md5
logger = logging.getLogger(__name__)
GROUP_CACHE_MAX_AGE = int(getattr(settings, 'DISCOURSE_GROUP_CACHE_MAX_AGE', 2 * 60 * 60)) # default 2 hours
GROUP_CACHE_MAX_AGE = getattr(settings, 'DISCOURSE_GROUP_CACHE_MAX_AGE', 2 * 60 * 60) # default 2 hours
class DiscourseError(Exception):
@@ -23,7 +23,7 @@ class DiscourseError(Exception):
ENDPOINTS = {
'groups': {
'list': {
'path': "/admin/groups.json",
'path': "/groups/search.json",
'method': 'get',
'args': {
'required': [],

View File

@@ -58,5 +58,5 @@ class Migration(migrations.Migration):
name='discourseuser',
options={'permissions': (('access_discourse', 'Can access the Discourse service'),)},
),
migrations.RunPython(migrate_service_enabled),
migrations.RunPython(migrate_service_enabled, migrations.RunPython.noop),
]

View File

@@ -6,6 +6,7 @@ from celery import shared_task
from allianceauth.notifications import notify
from allianceauth.services.hooks import NameFormatter
from allianceauth.services.tasks import QueueOnce
from .manager import DiscourseManager
from .models import DiscourseUser
@@ -40,7 +41,7 @@ class DiscourseTasks:
return False
@staticmethod
@shared_task(bind=True, name='discourse.update_groups')
@shared_task(bind=True, name='discourse.update_groups', base=QueueOnce)
def update_groups(self, pk):
user = User.objects.get(pk=pk)
logger.debug("Updating discourse groups for user %s" % user)

View File

@@ -1,8 +1,10 @@
{% load i18n %}
<td class="text-center">Discourse</td>
<td class="text-center">{{ char.character_name }}</td>
<td class="text-center"><a href="{{ DISCOURSE_URL }}">{{ DISCOURSE_URL }}</a></td>
<td class="text-center">
<a title="Go To Forums" class="btn btn-success" href="{{ DISCOURSE_URL }}"><span class="glyphicon glyphicon-arrow-right"></span></a>
</td>
<tr>
<td class="text-center">Discourse</td>
<td class="text-center">{{ char.character_name }}</td>
<td class="text-center"><a href="{{ DISCOURSE_URL }}">{{ DISCOURSE_URL }}</a></td>
<td class="text-center">
<a title="Go To Forums" class="btn btn-success" href="{{ DISCOURSE_URL }}"><span class="glyphicon glyphicon-arrow-right"></span></a>
</td>
</tr>

View File

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

View File

@@ -51,31 +51,31 @@ class MarketManager:
@classmethod
def check_username(cls, username):
logger.debug("Checking alliance market username %%s" % username)
logger.debug("Checking alliance market username %s" % username)
cursor = connections['market'].cursor()
cursor.execute(cls.SQL_CHECK_USERNAME, [cls.__santatize_username(username)])
row = cursor.fetchone()
if row:
logger.debug("Found user %%s on alliance market" % username)
logger.debug("Found user %s on alliance market" % username)
return True
logger.debug("User %%s not found on alliance market" % username)
logger.debug("User %s not found on alliance market" % username)
return False
@classmethod
def check_user_email(cls, username, email):
logger.debug("Checking if alliance market email exists for user %%s" % username)
logger.debug("Checking if alliance market email exists for user %s" % username)
cursor = connections['market'].cursor()
cursor.execute(cls.SQL_CHECK_EMAIL, [email])
row = cursor.fetchone()
if row:
logger.debug("Found user %%s email address on alliance market" % username)
logger.debug("Found user %s email address on alliance market" % username)
return True
logger.debug("User %%s email address not found on alliance market" % username)
logger.debug("User %s email address not found on alliance market" % username)
return False
@classmethod
def add_user(cls, username, email, characterid, charactername):
logger.debug("Adding new market user %%s" % username)
logger.debug("Adding new market user %s" % username)
plain_password = cls.__generate_random_pass()
hash = cls._gen_pwhash(plain_password)
salt = cls._get_salt(hash)
@@ -83,33 +83,33 @@ class MarketManager:
if not cls.check_username(username):
if not cls.check_user_email(username, email):
try:
logger.debug("Adding user %%s to alliance market" % username)
logger.debug("Adding user %s to alliance market" % username)
cursor = connections['market'].cursor()
cursor.execute(cls.SQL_ADD_USER, [username_clean, username_clean, email, email, salt,
hash, characterid, charactername])
return username_clean, plain_password
except:
logger.debug("Unsuccessful attempt to add market user %%s" % username)
logger.debug("Unsuccessful attempt to add market user %s" % username)
return "", ""
else:
logger.debug("Alliance market email %%s already exists Updating instead" % email)
logger.debug("Alliance market email %s already exists Updating instead" % email)
username_clean, password = cls.update_user_info(username)
return username_clean, password
else:
logger.debug("Alliance market username %%s already exists Updating instead" % username)
logger.debug("Alliance market username %s already exists Updating instead" % username)
username_clean, password = cls.update_user_info(username)
return username_clean, password
@classmethod
def disable_user(cls, username):
logger.debug("Disabling alliance market user %%s " % username)
logger.debug("Disabling alliance market user %s " % username)
cursor = connections['market'].cursor()
cursor.execute(cls.SQL_DISABLE_USER, [username])
return True
@classmethod
def update_custom_password(cls, username, plain_password):
logger.debug("Updating alliance market user %%s password" % username)
logger.debug("Updating alliance market user %s password" % username)
if cls.check_username(username):
username_clean = cls.__santatize_username(username)
hash = cls._gen_pwhash(plain_password)
@@ -118,12 +118,12 @@ class MarketManager:
cursor.execute(cls.SQL_UPDATE_PASSWORD, [hash, salt, username_clean])
return plain_password
else:
logger.error("Unable to update alliance market user %%s password" % username)
logger.error("Unable to update alliance market user %s password" % username)
return ""
@classmethod
def update_user_password(cls, username):
logger.debug("Updating alliance market user %%s password" % username)
logger.debug("Updating alliance market user %s password" % username)
if cls.check_username(username):
username_clean = cls.__santatize_username(username)
plain_password = cls.__generate_random_pass()
@@ -133,12 +133,12 @@ class MarketManager:
cursor.execute(cls.SQL_UPDATE_PASSWORD, [hash, salt, username_clean])
return plain_password
else:
logger.error("Unable to update alliance market user %%s password" % username)
logger.error("Unable to update alliance market user %s password" % username)
return ""
@classmethod
def update_user_info(cls, username):
logger.debug("Updating alliance market user %%s" % username)
logger.debug("Updating alliance market user %s" % username)
try:
username_clean = cls.__santatize_username(username)
plain_password = cls.__generate_random_pass()
@@ -148,5 +148,5 @@ class MarketManager:
cursor.execute(cls.SQL_UPDATE_USER, [hash, salt, username_clean])
return username_clean, plain_password
except:
logger.debug("Alliance market update user failed for %%s" % username)
logger.debug("Alliance market update user failed for %s" % username)
return "", ""

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ import logging
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from celery import shared_task
from allianceauth.services.tasks import QueueOnce
from .models import MumbleUser
logger = logging.getLogger(__name__)
@@ -26,7 +26,7 @@ class MumbleTasks:
MumbleUser.objects.all().delete()
@staticmethod
@shared_task(bind=True, name="mumble.update_groups")
@shared_task(bind=True, name="mumble.update_groups", base=QueueOnce)
def update_groups(self, pk):
user = User.objects.get(pk=pk)
logger.debug("Updating mumble groups for user %s" % user)

View File

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

View File

@@ -4,7 +4,7 @@ from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from allianceauth.notifications import notify
from celery import shared_task
from allianceauth.services.tasks import QueueOnce
from allianceauth.services.modules.openfire.manager import OpenfireManager
from allianceauth.services.hooks import NameFormatter
from .models import OpenfireUser
@@ -40,7 +40,7 @@ class OpenfireTasks:
OpenfireUser.objects.all().delete()
@staticmethod
@shared_task(bind=True, name="openfire.update_groups")
@shared_task(bind=True, name="openfire.update_groups", base=QueueOnce)
def update_groups(self, pk):
user = User.objects.get(pk=pk)
logger.debug("Updating jabber groups for user %s" % user)

View File

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

View File

@@ -3,7 +3,7 @@ import logging
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from celery import shared_task
from allianceauth.services.tasks import QueueOnce
from allianceauth.notifications import notify
from allianceauth.services.hooks import NameFormatter
from .manager import Phpbb3Manager
@@ -35,7 +35,7 @@ class Phpbb3Tasks:
return False
@staticmethod
@shared_task(bind=True, name="phpbb3.update_groups")
@shared_task(bind=True, name="phpbb3.update_groups", base=QueueOnce)
def update_groups(self, pk):
user = User.objects.get(pk=pk)
logger.debug("Updating phpbb3 groups for user %s" % user)

View File

@@ -3,7 +3,7 @@ import logging
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from celery import shared_task
from allianceauth.services.tasks import QueueOnce
from allianceauth.notifications import notify
from allianceauth.services.hooks import NameFormatter
from .manager import SeatManager
@@ -34,7 +34,7 @@ class SeatTasks:
return False
@staticmethod
@shared_task(bind=True)
@shared_task(bind=True, name='seat.update_roles', base=QueueOnce)
def update_roles(self, pk):
user = User.objects.get(pk=pk)
logger.debug("Updating SeAT roles for user %s" % user)

View File

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

View File

@@ -3,7 +3,7 @@ import logging
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from celery import shared_task
from allianceauth.services.tasks import QueueOnce
from allianceauth.notifications import notify
from allianceauth.services.hooks import NameFormatter
from .manager import SmfManager
@@ -39,7 +39,7 @@ class SmfTasks:
SmfUser.objects.all().delete()
@staticmethod
@shared_task(bind=True, name="smf.update_groups")
@shared_task(bind=True, name="smf.update_groups", base=QueueOnce)
def update_groups(self, pk):
user = User.objects.get(pk=pk)
logger.debug("Updating smf groups for user %s" % user)

View File

@@ -179,18 +179,19 @@ class Teamspeak3Manager:
except:
logger.exception("An unhandled exception has occured while syncing TS groups.")
def add_user(self, username):
username_clean = self.__santatize_username(username[:30])
def add_user(self, user, fmt_name):
username_clean = self.__santatize_username(fmt_name[:30])
logger.debug("Adding user to TS3 server with cleaned username %s" % username_clean)
server_groups = self._group_list()
if 'Member' not in server_groups:
self._create_group('Member')
state = user.profile.state.name
if state not in server_groups:
self._create_group(state)
alliance_group_id = self._group_id_by_name('Member')
state_group_id = self._group_id_by_name(state)
try:
ret = self.server.send_command('tokenadd', {'tokentype': 0, 'tokenid1': alliance_group_id, 'tokenid2': 0,
ret = self.server.send_command('tokenadd', {'tokentype': 0, 'tokenid1': state_group_id, 'tokenid2': 0,
'tokendescription': username_clean,
'tokencustomset': "ident=sso_uid value=%s" % username_clean})
except TeamspeakError as e:
@@ -244,10 +245,10 @@ class Teamspeak3Manager:
return False
def generate_new_permissionkey(self, uid, username):
def generate_new_permissionkey(self, uid, user, username):
logger.debug("Re-issuing permission key for user id %s" % uid)
self.delete_user(uid)
return self.add_user(username)
return self.add_user(user, username)
def update_groups(self, uid, ts_groups):
logger.debug("Updating uid %s TS3 groups %s" % (uid, ts_groups))

View File

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

View File

@@ -3,7 +3,7 @@ import logging
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from celery import shared_task
from allianceauth.services.tasks import QueueOnce
from allianceauth.notifications import notify
from allianceauth.services.hooks import NameFormatter
from .manager import Teamspeak3Manager
@@ -56,7 +56,7 @@ class Teamspeak3Tasks:
logger.info("Teamspeak3 disabled")
@staticmethod
@shared_task(bind=True, name="teamspeak3.update_groups")
@shared_task(bind=True, name="teamspeak3.update_groups", base=QueueOnce)
def update_groups(self, pk):
user = User.objects.get(pk=pk)
logger.debug("Updating user %s teamspeak3 groups" % user)

View File

@@ -10,11 +10,11 @@ module_urls = [
name='activate'),
url(r'^deactivate/$', views.deactivate_teamspeak3,
name='deactivate'),
url(r'reset_perm/$', views.reset_teamspeak3_perm,
url(r'^reset_perm/$', views.reset_teamspeak3_perm,
name='reset_perm'),
# Teamspeak Urls
url(r'verify/$', views.verify_teamspeak3, name='verify'),
url(r'^verify/$', views.verify_teamspeak3, name='verify'),
]
urlpatterns = [

View File

@@ -36,7 +36,7 @@ class TS3Proto:
def connect(self, ip, port):
try:
self._conn = telnetlib.Telnet(host=ip, port=port)
self._conn = telnetlib.Telnet(host=ip, port=port, timeout=5)
self._connected = True
except:
# raise ConnectionError(ip, port)

View File

@@ -22,7 +22,7 @@ def activate_teamspeak3(request):
character = request.user.profile.main_character
with Teamspeak3Manager() as ts3man:
logger.debug("Adding TS3 user for user %s with main character %s" % (request.user, character))
result = ts3man.add_user(Teamspeak3Tasks.get_username(request.user))
result = ts3man.add_user(request.user, Teamspeak3Tasks.get_username(request.user))
# if its empty we failed
if result[0] is not "":
@@ -79,13 +79,12 @@ def reset_teamspeak3_perm(request):
logger.debug("reset_teamspeak3_perm called by user %s" % request.user)
if not Teamspeak3Tasks.has_account(request.user):
return redirect("services:services")
character = request.user.profile.main_character
logger.debug("Deleting TS3 user for user %s" % request.user)
with Teamspeak3Manager() as ts3man:
ts3man.delete_user(request.user.teamspeak3.uid)
logger.debug("Generating new permission key for user %s with main character %s" % (request.user, character))
result = ts3man.generate_new_permissionkey(request.user.teamspeak3.uid, character.character_name)
logger.debug("Generating new permission key for user %s" % request.user)
result = ts3man.generate_new_permissionkey(request.user.teamspeak3.uid, request.user, Teamspeak3Tasks.get_username(request.user))
# if blank we failed
if result[0] != "":

View File

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

View File

@@ -141,7 +141,7 @@ def pre_delete_user(sender, instance, *args, **kwargs):
@receiver(pre_save, sender=User)
def pre_save_user(sender, instance, *args, **kwargs):
def disable_services_on_inactive(sender, instance, *args, **kwargs):
logger.debug("Received pre_save from %s" % instance)
# check if user is being marked active/inactive
if not instance.pk:
@@ -154,3 +154,17 @@ def pre_save_user(sender, instance, *args, **kwargs):
disable_user(instance)
except User.DoesNotExist:
pass
@receiver(pre_save, sender=UserProfile)
def disable_services_on_no_main(sender, instance, *args, **kwargs):
if not instance.pk:
# new model being created
return
try:
old_instance = UserProfile.objects.get(pk=instance.pk)
if old_instance.main_character and not instance.main_character:
logger.info("Disabling services due to loss of main character for user {0}".format(instance.user))
disable_user(instance.user)
except UserProfile.DoesNotExist:
pass

View File

@@ -1,40 +1,39 @@
import logging
import redis
from celery import shared_task
from django.contrib.auth.models import User
from .hooks import ServicesHook
from celery_once import QueueOnce as BaseTask, AlreadyQueued
from celery_once.helpers import now_unix
from django.core.cache import cache
REDIS_CLIENT = redis.Redis()
logger = logging.getLogger(__name__)
# http://loose-bits.com/2010/10/distributed-task-locking-in-celery.html
def only_one(function=None, key="", timeout=None):
"""Enforce only one celery task at a time."""
class QueueOnce(BaseTask):
once = BaseTask.once
once['graceful'] = True
def _dec(run_func):
"""Decorator."""
def _caller(*args, **kwargs):
"""Caller."""
ret_value = None
have_lock = False
lock = REDIS_CLIENT.lock(key, timeout=timeout)
try:
have_lock = lock.acquire(blocking=False)
if have_lock:
ret_value = run_func(*args, **kwargs)
finally:
if have_lock:
lock.release()
class DjangoBackend:
def __init__(self, settings):
pass
return ret_value
@staticmethod
def raise_or_lock(key, timeout):
now = now_unix()
result = cache.get(key)
if result:
remaining = int(result) - now
if remaining > 0:
raise AlreadyQueued(remaining)
else:
cache.set(key, now + timeout, timeout)
return _caller
return _dec(function) if function is not None else _dec
@staticmethod
def clear_lock(key):
return cache.delete(key)
@shared_task(bind=True)

View File

@@ -33,6 +33,7 @@ class NameFormatterTestCase(TestCase):
corporation_ticker='TIKK',
alliance_id='3456',
alliance_name='alliance name',
alliance_ticker='TIKR',
)
self.member.profile.main_character = self.char
self.member.profile.save()
@@ -83,11 +84,15 @@ class NameFormatterTestCase(TestCase):
self.assertIn('alliance_name', result)
self.assertEqual(result['alliance_name'], self.char.alliance_name)
self.assertIn('alliance_ticker', result)
self.assertEqual(result['alliance_ticker'], self.char.alliance.alliance_ticker)
self.assertEqual(result['alliance_ticker'], self.char.alliance_ticker)
self.assertIn('alliance_id', result)
self.assertEqual(result['alliance_id'], self.char.alliance_id)
self.assertIn('username', result)
self.assertEqual(result['username'], self.member.username)
self.assertIn('alliance_or_corp_name', result)
self.assertEqual(result['alliance_or_corp_name'], self.char.alliance_name)
self.assertIn('alliance_or_corp_ticker', result)
self.assertEqual(result['alliance_or_corp_ticker'], self.char.alliance_ticker)
def test_format_name(self):
config = NameFormatConfig.objects.create(

View File

@@ -9,6 +9,7 @@ from allianceauth.authentication.models import State
class ServicesSignalsTestCase(TestCase):
def setUp(self):
self.member = AuthUtils.create_user('auth_member', disconnect_signals=True)
AuthUtils.add_main_character(self.member, 'Test', '1', '2', 'Test Corp', 'TEST')
self.none_user = AuthUtils.create_user('none_user', disconnect_signals=True)
@mock.patch('allianceauth.services.signals.transaction')
@@ -67,6 +68,18 @@ class ServicesSignalsTestCase(TestCase):
args, kwargs = disable_user.call_args
self.assertEqual(self.member, args[0])
@mock.patch('allianceauth.services.signals.disable_user')
def test_disable_services_on_loss_of_main_character(self, disable_user):
"""
Test a user set inactive has disable_member called
"""
self.member.profile.main_character = None
self.member.profile.save() # Signal Trigger
self.assertTrue(disable_user.called)
args, kwargs = disable_user.call_args
self.assertEqual(self.member, args[0])
@mock.patch('allianceauth.services.signals.transaction')
@mock.patch('allianceauth.services.signals.ServicesHook')
def test_m2m_changed_group_permissions(self, services_hook, transaction):

View File

@@ -1,12 +1,12 @@
from allianceauth.services.hooks import MenuItemHook, UrlHook
from django.utils.translation import ugettext_lazy as _
from allianceauth import hooks
from . import urls
class SrpMenu(MenuItemHook):
def __init__(self):
MenuItemHook.__init__(self, 'Ship Replacement',
MenuItemHook.__init__(self, _('Ship Replacement'),
'fa fa-money fa-fw',
'srp:management',
navactive=['srp:'])

View File

@@ -23,9 +23,9 @@ urlpatterns = [
name='mark_uncompleted'),
url(r'^request/remove/', views.srp_request_remove,
name="request_remove"),
url(r'request/approve/', views.srp_request_approve,
url(r'^request/approve/', views.srp_request_approve,
name='request_approve'),
url(r'request/reject/', views.srp_request_reject,
url(r'^request/reject/', views.srp_request_reject,
name='request_reject'),
url(r'^request/(\w+)/update', views.srp_request_update_amount,
name="request_update_amount"),

View File

@@ -8,8 +8,16 @@ function getDurationString(duration) {
if (duration.years()) {
out += duration.years() + 'y ';
}
if (duration.months()) {
out += duration.months() + 'm ';
}
if (duration.days()) {
out += duration.days() + 'd ';
}
return out + duration.hours() + "h " + duration.minutes() + "m " + duration.seconds() + "s";
}
function getCurrentEveTimeString() {
return moment().utc().format('dddd LL HH:mm:ss')
}

View File

@@ -1,11 +1,12 @@
{% extends "allianceauth/base.html" %}
{% load i18n %}
{% block page_title %}Help{% endblock page_title %}
{% block page_title %}{% trans "Help" %}{% endblock page_title %}
{% block content %}
<div class="col-lg-12">
<h1 class="page-header text-center">Help</h1>
<h1 class="page-header text-center">{% trans "Help" %}</h1>
<div class="container-fluid">
<div class="embed-responsive embed-responsive-16by9">

View File

@@ -7,12 +7,12 @@
<li>
<a class="{% navactive request 'authentication:dashboard' %}"
href="{% url 'authentication:dashboard' %}">
<i class="fa fa-dashboard fa-fw"></i>{% trans " Dashboard" %}
<i class="fa fa-dashboard fa-fw"></i> {% trans "Dashboard" %}
</a>
</li>
<li>
<a class="{% navactive request 'groupmanagement:groups' %}" href="{% url 'groupmanagement:groups' %}">
<i class="fa fa-cogs fa-fw fa-sitemap"></i>{% trans " Groups" %}
<i class="fa fa-cogs fa-fw fa-sitemap"></i> {% trans "Groups" %}
</a>
</li>
@@ -20,7 +20,7 @@
<li>
<a class="{% navactive request 'groupmanagement:management groupmanagement:membership groupmanagement:membership_list' %}"
href="{% url 'groupmanagement:management' %}">
<i class="fa fa-lock fa-sitemap fa-fw"></i>{% trans " Group Management" %}
<i class="fa fa-lock fa-sitemap fa-fw"></i> {% trans "Group Management" %}
</a>
</li>
{% endif %}
@@ -30,7 +30,7 @@
<li>
<a class="{% navactive request 'authentication:help' %}"
href="{% url 'authentication:help' %}">
<i class="fa fa-question fa-fw"></i>{% trans " Help" %}
<i class="fa fa-question fa-fw"></i> {% trans "Help" %}
</a>
</li>
</ul>

View File

@@ -1,4 +1,4 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.17.1/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.1/moment.min.js"></script>
{% if locale and LANGUAGE_CODE != 'en' %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.17.1/locale/{{ LANGUAGE_CODE }}.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.1/locale/{{ LANGUAGE_CODE }}.js"></script>
{% endif %}

View File

@@ -9,7 +9,7 @@ from allianceauth.eveonline.models import EveCharacter
from allianceauth.services.signals import m2m_changed_group_permissions, m2m_changed_user_permissions, \
m2m_changed_state_permissions
from allianceauth.services.signals import m2m_changed_user_groups, pre_save_user
from allianceauth.services.signals import m2m_changed_user_groups, disable_services_on_inactive
class AuthUtils:
@@ -90,7 +90,7 @@ class AuthUtils:
m2m_changed.disconnect(m2m_changed_group_permissions, sender=Group.permissions.through)
m2m_changed.disconnect(m2m_changed_user_permissions, sender=User.user_permissions.through)
m2m_changed.disconnect(m2m_changed_state_permissions, sender=State.permissions.through)
pre_save.disconnect(pre_save_user, sender=User)
pre_save.disconnect(disable_services_on_inactive, sender=User)
m2m_changed.disconnect(state_member_corporations_changed, sender=State.member_corporations.through)
m2m_changed.disconnect(state_member_characters_changed, sender=State.member_characters.through)
m2m_changed.disconnect(state_member_alliances_changed, sender=State.member_alliances.through)
@@ -102,7 +102,7 @@ class AuthUtils:
m2m_changed.connect(m2m_changed_group_permissions, sender=Group.permissions.through)
m2m_changed.connect(m2m_changed_user_permissions, sender=User.user_permissions.through)
m2m_changed.connect(m2m_changed_state_permissions, sender=State.permissions.through)
pre_save.connect(pre_save_user, sender=User)
pre_save.connect(disable_services_on_inactive, sender=User)
m2m_changed.connect(state_member_corporations_changed, sender=State.member_corporations.through)
m2m_changed.connect(state_member_characters_changed, sender=State.member_characters.through)
m2m_changed.connect(state_member_alliances_changed, sender=State.member_alliances.through)

View File

@@ -18,7 +18,8 @@
<div class="col-lg-12 text-center">
<div class="label label-info text-left">
<b>{% trans "Current Eve Time:" %} </b>
</div><div class="label label-info text-left" id="current-time"></div>
</div>
<strong class="label label-info text-left" id="current-time"></strong>
</div>
{% if corp_timers %}
<h4><b>{% trans "Corp Timers" %}</b></h4>
@@ -555,7 +556,7 @@
}
function updateClock() {
document.getElementById("current-time").innerHTML = "<b>" + moment().format('LLLL') + "</b>";
document.getElementById("current-time").innerHTML = getCurrentEveTimeString();
}
</script>
{% endblock content %}

View File

@@ -26,7 +26,7 @@ urlpatterns = [
# Authentication
url(r'', include(allianceauth.authentication.urls)),
url(r'^account/login/$', TemplateView.as_view(template_name='public/login.html'), name='auth_login_user'),
url(r'account/', include(hmac_urls)),
url(r'^account/', include(hmac_urls)),
# Admin urls
url(r'^admin/', admin.site.urls),

View File

@@ -54,7 +54,7 @@ master_doc = 'index'
# General information about the project.
project = u'Alliance Auth'
copyright = u'2017, Alliance Auth'
copyright = u'2018, Alliance Auth'
author = u'R4stl1n'
# The version info for the project you're documenting, acts as replacement for
@@ -62,7 +62,7 @@ author = u'R4stl1n'
# built documents.
#
# The short X.Y version.
version = u'1.14'
version = u'2.0'
# The full version, including alpha/beta/rc tags.
# release = u'1.14.0'
@@ -156,7 +156,7 @@ man_pages = [
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'AllianceAuth', u'Alliance Auth Documentation',
author, 'AllianceAuth', 'Alliance service auth to help large scale alliances manage services.',
author, 'AllianceAuth', 'An auth system for EVE Online to help in-game organizations manage online service access.',
'Miscellaneous'),
]

View File

@@ -5,7 +5,7 @@ The documentation for Alliance Auth uses [Sphinx](http://www.sphinx-doc.org/) to
[readthedocs.org](https://readthedocs.org/).
Documentation was migrated from the Github wiki pages and into the repository to allow documentation changes to be
Documentation was migrated from the GitHub wiki pages and into the repository to allow documentation changes to be
included with pull requests. This means that documentation can be guaranteed to be updated when a pull request is
accepted rather than hoping documentation is updated afterwards or relying on maintainers to do the work. It also
allows for documentation to be maintained at different versions more easily.

View File

@@ -1,6 +1,6 @@
# Integrating Services
One of the primary roles of Alliance Auth is integrating with external services in order to authenticate and manage users. This is achieved through the use of service modules.
One of the primary roles of Alliance Auth is integrating with external services in order to authenticate and manage users. This is achieved through the use of service modules.
## The Service Module
@@ -29,8 +29,8 @@ The architecture looks something like this:
|
|
AllianceAuth
Where:
Module --▶ Dependency/Import
@@ -46,10 +46,10 @@ In order to integrate with Alliance Auth service modules must provide a `service
This would register the ExampleService class which would need to be a subclass of `services.hooks.ServiceHook`.
```eval_rst
.. important::
The hook **MUST** be registered in `yourservice.auth_hooks` along with any other hooks you are registering for Alliance Auth.
The hook **MUST** be registered in ``yourservice.auth_hooks`` along with any other hooks you are registering for Alliance Auth.
```
@@ -97,7 +97,7 @@ Functions:
Internal name of the module, should be unique amongst modules.
#### self.urlpatterns
You should define all of your service urls internally, usually in `urls.py`. Then you can import them and set `self.urlpatterns` to your defined urlpatterns.
You should define all of your service URLs internally, usually in `urls.py`. Then you can import them and set `self.urlpatterns` to your defined urlpatterns.
from . import urls
...
@@ -105,7 +105,7 @@ You should define all of your service urls internally, usually in `urls.py`. The
def __init__(self):
...
self.urlpatterns = urls.urlpatterns
All of your apps defined urlpatterns will then be included in the `URLconf` when the core application starts.
#### self.service_ctrl_template
@@ -147,7 +147,7 @@ If this function is defined, an admin action will be registered on the Django Us
#### update_groups
`def update_groups(self, user):`
Update the users group membership. The `user` parameter should be a Django User object.
Update the users group membership. The `user` parameter should be a Django User object.
When this is called the service should determine the groups the user is a member of and synchronise the group membership with the external service. If you service does not support groups then you are not required to define this.
If this function is defined, an admin action will be registered on the Django Users view, allowing admins to manually trigger this action for one or many users. The hook will trigger this action user by user, so you won't have to manage a list of users.
@@ -176,7 +176,7 @@ Should the service be shown for the given `user` with the given `state`? The `us
Usually you wont need to override this function.
For more information see the [render_service_ctrl](#render-service-ctrl) section.
#### render_service_ctrl
`def render_services_ctrl(self, request):`
@@ -221,7 +221,7 @@ Most services will survive with the default template. If, however, you require e
If you services needs cannot be satisfied by the Service Control row, you are free to specify extra hooks by subclassing or instantiating the `services.hooks.MenuItemHook` class.
For more information see the [Menu Hooks](menu-hooks.md) page.
For more information see the [Menu Hooks](menu-hooks.md) page.
### The Service Manager

View File

@@ -24,12 +24,12 @@ When you create an autogroup config you will be given the following options:
After creating a group you wont be able to change the Corp and Alliance group prefixes, name source and the replace spaces settings. Make sure you configure these the way you want before creating the config. If you need to change these you will have to create a new autogroup config.
```
- States selects which states will be added to automatic corp/alliance groups
- States selects which states will be added to automatic Corp/Alliance groups
- Corp/Alliance groups checkbox toggles corp/alliance autogroups on or off for this config.
- Corp/Alliance groups checkbox toggles Corp/Alliance autogroups on or off for this config.
- Corp/Alliance group prefix sets the prefix for the group name, e.g. if your corp was called `MyCorp` and your prefix was `Corp `, your autogroup name would be created as `Corp MyCorp`. This field accepts leading/trailing spaces.
- Corp/Alliance group prefix sets the prefix for the group name, e.g. if your Corp was called `MyCorp` and your prefix was `Corp `, your autogroup name would be created as `Corp MyCorp`. This field accepts leading/trailing spaces.
- Corp/Alliance name source sets the source of the corp/alliance name used in creating the group name. Currently the options are Full name and Ticker.
- Corp/Alliance name source sets the source of the Corp/Alliance name used in creating the group name. Currently the options are Full name and Ticker.
- Replace spaces allows you to replace spaces in the autogroup name with the value in the Replace spaces with field. This can be blank.

View File

@@ -1,6 +1,6 @@
# Corp Stats
This module is used to check the registration status of corp members and to determine character relationships, being mains or alts.
This module is used to check the registration status of Corp members and to determine character relationships, being mains or alts.
## Installation
@@ -10,13 +10,13 @@ Add `'allianceauth.corputils',` to your `INSTALLED_APPS` list in your auth proje
## Creating a Corp Stats
Upon initial install, nothing will be visible. For every corp, a model will have to be created before data can be viewed.
Upon initial install, nothing will be visible. For every Corp, a model will have to be created before data can be viewed.
![nothing is visible](/_static/images/features/corpstats/blank_header.png)
If you are a superuser, the add button will be immediate visible to you. If not, your user account requires the `add_corpstats` permission.
Corp Stats requires an EVE SSO token to access data from the EVE Swagger Interface. Upon pressing the Add button, you will be prompted to authenticated. Please select the character who is in the corp you want data for.
Corp Stats requires an EVE SSO token to access data from the EVE Swagger Interface. Upon pressing the Add button, you will be prompted to authenticated. Please select the character who is in the Corporation you want data for.
![authorize from the EVE site](/_static/images/features/corpstats/eve_sso_authorization.png)
@@ -33,7 +33,7 @@ If it fails an error message will be displayed.
![navigation bar](/_static/images/features/corpstats/navbar.png)
This bar contains a dropdown menu of all available corps. If the user has the `add_corpstats` permission, a button to add a Corp Stats will be shown.
This bar contains a dropdown menu of all available Corporations. If the user has the `add_corpstats` permission, a button to add a Corp Stats will be shown.
On the right of this bar is a search field. Press enter to search. It checks all characters in all Corp Stats you have view permission to and returns search results.
@@ -41,7 +41,7 @@ On the right of this bar is a search field. Press enter to search. It checks all
![last update and update button](/_static/images/features/corpstats/last_update.png)
An update can be performed immediately by pressing the update button. Anyone who can view the Corp Stats can update it.
An update can be performed immediately by pressing the update button. Anyone who can view the Corp Stats can update it.
### Character Lists
@@ -60,15 +60,15 @@ Each view contains a sortable and searchable table. The number of listings shown
![main list](/_static/images/features/corpstats/main_list.png)
This list contains all main characters in registered in the selected corporation and their alts. Each character has a link to [zKillboard](https://zkillboard.com).
This list contains all main characters in registered in the selected Corporation and their alts. Each character has a link to [zKillboard](https://zkillboard.com).
#### Member List
![member list](/_static/images/features/corpstats/member_list.png)
The list contains all characters in the corp. Red backgrounds means they are not registered in auth. A link to [zKillboard](https://zkillboard.com) is present for all characters.
If registered, the character will also have a main character, main corporation, and main alliance field.
The list contains all characters in the Corporation. Red backgrounds means they are not registered in auth. A link to [zKillboard](https://zkillboard.com) is present for all characters.
If registered, the character will also have a main character, main Corporation, and main Alliance field.
#### Unregistered List
@@ -80,7 +80,7 @@ This list contains all characters not registered on auth. Each character has a l
![search results](/_static/images/features/corpstats/search_view.png)
This view is essentially the same as the Corp Stats page, but not specific to a single corp.
This view is essentially the same as the Corp Stats page, but not specific to a single Corporation.
The search query is visible in the search box.
Characters from all Corp Stats to which the user has view access will be displayed. APIs respect permissions.
@@ -108,7 +108,7 @@ To use this feature, users will require some of the following:
```
Users who add a Corp Stats with their token will be granted permissions to view it regardless of the above permissions. View permissions are interpreted in the "OR" sense: a user can view their corp's Corp Stats without the `view_corp_corpstats` permission if they have the `view_alliance_corpstats` permission, same idea for their state. Note that these evaluate against the user's main character.
Users who add a Corp Stats with their token will be granted permissions to view it regardless of the above permissions. View permissions are interpreted in the "OR" sense: a user can view their Corporations's Corp Stats without the `view_corp_corpstats` permission if they have the `view_alliance_corpstats` permission, same idea for their state. Note that these evaluate against the user's main character.
## Automatic Updating
By default Corp Stats are only updated on demand. If you want to automatically refresh on a schedule, add an entry to your project's settings file:
@@ -116,7 +116,7 @@ By default Corp Stats are only updated on demand. If you want to automatically r
CELERYBEAT_SCHEDULE['update_all_corpstats'] = {
'task': 'allianceauth.corputils.tasks.update_all_corpstats',
'schedule': crontab(minute=0, hour="*/6"),
},
}
Adjust the crontab as desired.
@@ -126,12 +126,12 @@ Adjust the crontab as desired.
>Unrecognized corporation. Please ensure it is a member of the alliance or a blue.
Corp Stats can only be created for corporations who have a model in the database. These only exist for tenant corps,
Corp Stats can only be created for Corporations who have a model in the database. These only exist for tenant corps,
corps of tenant alliances, blue corps, and members of blue alliances.
>Selected corp already has a statistics module.
Only one Corp Stats may exist at a time for a given corporation.
Only one Corp Stats may exist at a time for a given Corporation.
>Failed to gather corporation statistics with selected token.
@@ -147,7 +147,7 @@ This occurs when the SSO token is invalid, which can occur when deleted by the u
>CorpStats for (corp name) cannot update with your ESI token as you have left corp.
The SSO token's character is no longer in the corp which the Corp Stats is for, and therefore membership data cannot be retrieved.
The SSO token's character is no longer in the Corporation which the Corp Stats is for, and therefore membership data cannot be retrieved.
>HTTPForbidden

View File

@@ -11,8 +11,8 @@ Additional settings are required. Append the following settings to the end of yo
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
Once filled out restart gunicorn and celery.
Once filled out restart Gunicorn and Celery.
## Permissions
The Fleetup module is only visible to users with the `auth | user | view_fleeup` permission.
The Fleetup module is only visible to users with the `auth | user | view_fleeup` permission.

View File

@@ -8,15 +8,15 @@ Add `'allianceauth.hrapplications',` to your `INSTALLED_APPS` list in your auth
### Creating Forms
The most common task is creating ApplicationForm models for corps. Only when such models exist will a corp be listed as a choice for applicants. This occurs in the django admin site, so only staff have access.
The most common task is creating ApplicationForm models for corps. Only when such models exist will a Corporation be listed as a choice for applicants. This occurs in the Django admin site, so only staff have access.
The first step is to create questions. This is achieved by creating ApplicationQuestion models, one for each question. Titles are not unique.
Next step is to create the actual ApplicationForm model. It requires an existing EveCorporationInfo model to which it will belong. It also requires the selection of questions. ApplicationForm models are unique per corp: only one may exist for any given corp concurrently.
Next step is to create the actual ApplicationForm model. It requires an existing EveCorporationInfo model to which it will belong. It also requires the selection of questions. ApplicationForm models are unique per Corporation: only one may exist for any given Corporation concurrently.
You can adjust these questions at any time. This is the preferred method of modifying the form: deleting and recreating will cascade the deletion to all received applications from this form which is usually not intended.
Once completed the corp will be available to receive applications.
Once completed the Corporation will be available to receive applications.
### Reviewing Applications
@@ -63,11 +63,11 @@ This is the model representation of a question. It contains a title, and a field
### ApplicationForm
This is the template for an application. It points at a corp, with only one form allowed per corp. It also points at ApplicationQuestion models. When a user creates an application, they will be prompted with each question the form includes at the given time. Modifying questions in a form after it has been created will not be reflected in existing applications, so it's perfectly fine to adjust them as you see fit. Changing corps however is not advisable, as existing applications will point at the wrong corp after they've been submitted, confusing reviewers.
This is the template for an application. It points at a Corporation, with only one form allowed per Corporation. It also points at ApplicationQuestion models. When a user creates an application, they will be prompted with each question the form includes at the given time. Modifying questions in a form after it has been created will not be reflected in existing applications, so it's perfectly fine to adjust them as you see fit. Changing Corporations however is not advisable, as existing applications will point at the wrong Corporation after they've been submitted, confusing reviewers.
### Application
This is the model representation of a completed application. It references an ApplicationForm from which it was spawned which is where the corp specificity comes from. It points at a user, contains info regarding its reviewer, and has a status. Shortcut properties also provide the applicant's main character, the applicant's APIs, and a string representation of the reviewer (for cases when the reviewer doesn't have a main character or the model gets deleted).
This is the model representation of a completed application. It references an ApplicationForm from which it was spawned which is where the Corporation specificity comes from. It points at a user, contains info regarding its reviewer, and has a status. Shortcut properties also provide the applicant's main character, the applicant's APIs, and a string representation of the reviewer (for cases when the reviewer doesn't have a main character or the model gets deleted).
### ApplicationResponse
@@ -81,7 +81,7 @@ This is a reviewer's comment on an application. Points at the application, point
### No corps accepting applications
Ensure there are ApplicationForm models in the admin site. Ensure the user does not already have an application to these corps. If the users wishes to re-apply they must first delete their completed application
Ensure there are ApplicationForm models in the admin site. Ensure the user does not already have an application to these Corporations. If the users wishes to re-apply they must first delete their completed application
### Reviewer unable to complete application

View File

@@ -5,7 +5,7 @@
New in 2.0
```
Each service's username or nickname, depending on which the service supports, can be customised through the use of the Name Formatter Config provided the service supports custom formats. This config can be found in the admin panel under **Services -> Name format config**
Each service's username or nickname, depending on which the service supports, can be customised through the use of the Name Formatter config provided the service supports custom formats. This config can be found in the admin panel under **Services -> Name format config**
Currently the following services support custom name formats:
@@ -37,7 +37,7 @@ Currently the following services support custom name formats:
```eval_rst
.. note::
It's important to note here, before we get into what you can do with a name formatter, that before the generated name is passed off to the service to create an account it will be sanitised to remove characters (the letters and numbers etc) that the service cannot support. This means that, despite what you configured, the service may display something different. It is up to you to test your formatter and understand how your format may be disrupted by a certain services sanitisation function.
It's important to note here, before we get into what you can do with a name formatter, that before the generated name is passed off to the service to create an account it will be sanitised to remove characters (the letters and numbers etc.) that the service cannot support. This means that, despite what you configured, the service may display something different. It is up to you to test your formatter and understand how your format may be disrupted by a certain services sanitisation function.
```
## Available format data
@@ -53,12 +53,14 @@ The following fields are available from a users account and main character:
- `alliance_id`
- `alliance_name`
- `alliance_ticker`
- `alliance_or_corp_name` (defaults to Corporation name if there is no Alliance)
- `alliance_or_corp_ticker` (defaults to Corporation ticker if there is no Alliance)
## Building a formatter string
The name formatter uses the advanced string formatting specified by [PEP-3101](https://www.python.org/dev/peps/pep-3101/). Anything supported by this specification is supported in a name formatter.
A more digestable documentation of string formatting in Python is available on the [PyFormat](https://pyformat.info/) website.
A more digestible documentation of string formatting in Python is available on the [PyFormat](https://pyformat.info/) website.
Some examples of strings you could use:
```eval_rst

View File

@@ -1,10 +1,5 @@
# Permissions Auditing
```eval_rst
.. note::
New in 1.15
```
Access to most of Alliance Auth's features are controlled by Django's permissions system. In order to help you secure your services, Alliance Auth provides a permissions auditing tool.
## Installation

View File

@@ -2,7 +2,7 @@
## Overview
In Alliance Auth v1 admins were able to define which corporations and alliances were to be considered "members" with full permissions and "blues" with restricted permissions. The state system is the replacement for these static definitions: admins can now create as many states as desired, as well as extend membership to specific characters.
In Alliance Auth v1 admins were able to define which Corporations and Alliances were to be considered "members" with full permissions and "blues" with restricted permissions. The state system is the replacement for these static definitions: admins can now create as many states as desired, as well as extend membership to specific characters.
## Creating a State
States are created through your installation's admin site. Upon install three states are created for you: `Member`, `Blue`, and `Guest`. New ones can be created like any other Django model by users with the appropriate permission (`authentication | state | Can add state`) or superusers.
@@ -27,10 +27,10 @@ Checking this box means this state is available to all users. There isn't much u
This lets you select which characters the state is available to. Characters can be added by selecting the green plus icon.
### Member Corporations
This lets you select which corporations the state is available to. Corporations can be added by selecting the green plus icon.
This lets you select which Corporations the state is available to. Corporations can be added by selecting the green plus icon.
### Member Alliances
This lets you select which alliances the state is available to. Alliances can be added by selecting the gree plus icon.
This lets you select which Alliances the state is available to. Alliances can be added by selecting the green plus icon.
## Determining a User's State
States are mutually exclusive, meaning a user can only be in one at a time.
@@ -45,5 +45,3 @@ Assigned states are visible in the `Users` section of the `Authentication` admin
If no states are available to a user's main character, or their account has been deactivated, they are assigned to a catch-all `Guest` state. This state cannot be deleted nor can its name be changed.
The `Guest` state allows permissions to be granted to users who would otherwise not get any. For example access to public services can be granted by giving the `Guest` state a service access permission.

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