mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2026-02-04 14:16:21 +01:00
Compare commits
297 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98b799d821 | ||
|
|
02714956d8 | ||
|
|
4d435d58c5 | ||
|
|
1c2fd3be50 | ||
|
|
6222439e21 | ||
|
|
46d46ac90b | ||
|
|
a5fe61eb15 | ||
|
|
0bfec36983 | ||
|
|
11607ecf24 | ||
|
|
9970e5535b | ||
|
|
99492e9c34 | ||
|
|
1d6ecffb3b | ||
|
|
cfb2c55a4b | ||
|
|
e24d29f1d3 | ||
|
|
debd6ef2b9 | ||
|
|
58e9c21e4f | ||
|
|
c7c3083e3e | ||
|
|
63d061e9f2 | ||
|
|
1887bdb90a | ||
|
|
69addb068a | ||
|
|
a62c3ce0f9 | ||
|
|
aecc94bdb3 | ||
|
|
fcb7f2905a | ||
|
|
34c7169ca3 | ||
|
|
6e450061f4 | ||
|
|
fc3d7e9f43 | ||
|
|
514db4f9a2 | ||
|
|
23a8b65ce2 | ||
|
|
f8e6662bc8 | ||
|
|
89be2456fb | ||
|
|
bfd3451717 | ||
|
|
0b759d6a32 | ||
|
|
65e05084e6 | ||
|
|
f25a4ed386 | ||
|
|
b2a1d41829 | ||
|
|
2741a92d31 | ||
|
|
3570ce86d7 | ||
|
|
d809902d1e | ||
|
|
ec4232c00a | ||
|
|
dec793bfac | ||
|
|
fe3fe0527a | ||
|
|
a8855e86ed | ||
|
|
179d1c38e6 | ||
|
|
287da73a4f | ||
|
|
e9ed917888 | ||
|
|
70d842c971 | ||
|
|
bcda228e05 | ||
|
|
000dafc5e6 | ||
|
|
4ea824fe71 | ||
|
|
f72f539516 | ||
|
|
1b192a184f | ||
|
|
0edf896b4c | ||
|
|
7dec4deb70 | ||
|
|
d511221899 | ||
|
|
d2b7de5221 | ||
|
|
79c5be02e2 | ||
|
|
09df37438d | ||
|
|
8561e4c6fd | ||
|
|
976cb4d988 | ||
|
|
20f7d5103c | ||
|
|
d049ec2832 | ||
|
|
00fe2a527e | ||
|
|
f53ec3b43e | ||
|
|
4d0417f114 | ||
|
|
00903b64db | ||
|
|
a3038cad00 | ||
|
|
ef99f1afac | ||
|
|
cc00d4bd04 | ||
|
|
250f26ff6f | ||
|
|
62b786ca4a | ||
|
|
9cfb47e658 | ||
|
|
ccef27b637 | ||
|
|
8dee61fd39 | ||
|
|
ae64bd0e19 | ||
|
|
a75e93dbfc | ||
|
|
0aa66c5729 | ||
|
|
4c2434219d | ||
|
|
8c65fda33b | ||
|
|
14f2751bbb | ||
|
|
d37a543c39 | ||
|
|
4947e0c483 | ||
|
|
f87c630b86 | ||
|
|
73789ea734 | ||
|
|
5a16c9c495 | ||
|
|
9dd8357f67 | ||
|
|
623e77a268 | ||
|
|
73403b98df | ||
|
|
7aa1a2f336 | ||
|
|
08e42d2f56 | ||
|
|
69248fd7bb | ||
|
|
0af188c6ab | ||
|
|
8b6d32d0d1 | ||
|
|
3c11c25d69 | ||
|
|
12e6cc63e8 | ||
|
|
d429c8b59a | ||
|
|
ddd7a3551b | ||
|
|
49ede92e06 | ||
|
|
b813213328 | ||
|
|
14065b3ca9 | ||
|
|
41f9dc490a | ||
|
|
48d25ca73f | ||
|
|
e49e04034c | ||
|
|
c7860f8e5c | ||
|
|
adb982114a | ||
|
|
5b8983deac | ||
|
|
1730bc3b98 | ||
|
|
4374064d98 | ||
|
|
c1d7994045 | ||
|
|
7bda367cc8 | ||
|
|
3de7a2ccd2 | ||
|
|
9cc278df31 | ||
|
|
a0bab07e2f | ||
|
|
149bbd92ca | ||
|
|
1de3c989d7 | ||
|
|
2e547945e2 | ||
|
|
4d4a9a27af | ||
|
|
5b41d0995f | ||
|
|
ab98d72022 | ||
|
|
8a7cd3f74d | ||
|
|
35cb56d6e9 | ||
|
|
a7a2ffd16b | ||
|
|
dbeda324e0 | ||
|
|
bf1fe99d98 | ||
|
|
41429ec7c7 | ||
|
|
ee9ed13a66 | ||
|
|
490ce286ff | ||
|
|
099c2c0a21 | ||
|
|
46e15f7fa1 | ||
|
|
6677e63e08 | ||
|
|
6d6a3a3d6b | ||
|
|
5006246cf1 | ||
|
|
6187fb9b86 | ||
|
|
86f57ccd56 | ||
|
|
854096bac7 | ||
|
|
9d2b3bb157 | ||
|
|
22bda62e59 | ||
|
|
c8ad1dcc7a | ||
|
|
7212a7a328 | ||
|
|
f6b1b7b6bb | ||
|
|
53a9d72c4a | ||
|
|
ca10fbcde5 | ||
|
|
b4d33e5dfc | ||
|
|
37bed989f1 | ||
|
|
507eda8a7d | ||
|
|
cbe67e9ebc | ||
|
|
cd38200506 | ||
|
|
5d5cf92a19 | ||
|
|
98230d0ee3 | ||
|
|
e47c04a0b0 | ||
|
|
b65ccac58f | ||
|
|
bee69cc250 | ||
|
|
a350e175c7 | ||
|
|
2cd8188ffb | ||
|
|
b8a2d65a1d | ||
|
|
95f72c854d | ||
|
|
cd8bcfbbb5 | ||
|
|
08f89d2844 | ||
|
|
f3f156bf57 | ||
|
|
73e6f576f4 | ||
|
|
20236cab8a | ||
|
|
6c7b65edad | ||
|
|
21782293ea | ||
|
|
e52478c9aa | ||
|
|
319cba8653 | ||
|
|
df3acccc50 | ||
|
|
19282cac60 | ||
|
|
933c12b48d | ||
|
|
8a73890646 | ||
|
|
d6df5184a6 | ||
|
|
91e1a374b4 | ||
|
|
c725de7b5b | ||
|
|
ad1fd633b1 | ||
|
|
ef9284030b | ||
|
|
89e5740027 | ||
|
|
106f6bbcea | ||
|
|
b53c7a624b | ||
|
|
6fa788a8f9 | ||
|
|
19f0788f47 | ||
|
|
7767226000 | ||
|
|
4eb6b73903 | ||
|
|
cb46ecb002 | ||
|
|
e694921fe6 | ||
|
|
8266661855 | ||
|
|
cf7ddbe0e1 | ||
|
|
bdb3ab366f | ||
|
|
1fc71a0738 | ||
|
|
0b7520e3b1 | ||
|
|
48c8ccfe97 | ||
|
|
ad79b4f77c | ||
|
|
fd876b8636 | ||
|
|
21e896642a | ||
|
|
b4c395f116 | ||
|
|
a38116014d | ||
|
|
54223db1e9 | ||
|
|
8a897abc7b | ||
|
|
fe7b078ec8 | ||
|
|
ce66bdcbd4 | ||
|
|
f65e563c0c | ||
|
|
e860ba6c22 | ||
|
|
50b6605a43 | ||
|
|
d181200642 | ||
|
|
386ba25a44 | ||
|
|
5331d194df | ||
|
|
814ecd233e | ||
|
|
f9a8ac4e9b | ||
|
|
1bd5eecd54 | ||
|
|
2fa1d9998d | ||
|
|
9d9cfebd9e | ||
|
|
cc8a7a18d2 | ||
|
|
552c795041 | ||
|
|
3d757e8d90 | ||
|
|
1b5ecaed80 | ||
|
|
77c93ed96b | ||
|
|
3eeed99af2 | ||
|
|
a143dfbb37 | ||
|
|
6b1da3b18a | ||
|
|
f0894f3415 | ||
|
|
539295c1b7 | ||
|
|
54f91a5bfb | ||
|
|
f3c0d05c39 | ||
|
|
9f9cc7ed42 | ||
|
|
814b2da0ca | ||
|
|
7a9bb0c84b | ||
|
|
36ae2af29b | ||
|
|
d192f23e6e | ||
|
|
67cd0cd55c | ||
|
|
9e53d8b429 | ||
|
|
f5abf82b95 | ||
|
|
8dd3a25b52 | ||
|
|
d0aa46db08 | ||
|
|
f0ff70566b | ||
|
|
efecf5113b | ||
|
|
980569de68 | ||
|
|
9c74952607 | ||
|
|
70c2a4a6e4 | ||
|
|
99b136b824 | ||
|
|
ae4116c0f6 | ||
|
|
3080d7d868 | ||
|
|
08cf8ae1d6 | ||
|
|
3ed0f873f3 | ||
|
|
5060d3f408 | ||
|
|
ef24bea562 | ||
|
|
c18efaa33d | ||
|
|
b6b14f6f1c | ||
|
|
a90a52f426 | ||
|
|
bd5ea38446 | ||
|
|
f8248f46e5 | ||
|
|
b09c454bf0 | ||
|
|
d825689da4 | ||
|
|
a64dda2a2e | ||
|
|
8ce8789631 | ||
|
|
2b2f367c30 | ||
|
|
4d194457d8 | ||
|
|
6f7cf8805d | ||
|
|
36e39503c8 | ||
|
|
e7a24c9cd4 | ||
|
|
bd8a8922cc | ||
|
|
396b2e0fb6 | ||
|
|
36e382fadb | ||
|
|
d2666f2440 | ||
|
|
397ca97f0f | ||
|
|
631bb439a4 | ||
|
|
a4003e188e | ||
|
|
f4a9ba2db8 | ||
|
|
895a62c475 | ||
|
|
ac5a0d9dcb | ||
|
|
b8644d5c93 | ||
|
|
4d8baf1af0 | ||
|
|
f70987de09 | ||
|
|
9d02b1530c | ||
|
|
3d532dae01 | ||
|
|
02247b067f | ||
|
|
63c2668171 | ||
|
|
5575039126 | ||
|
|
f97c8f2ce4 | ||
|
|
6baab1d006 | ||
|
|
17adf04860 | ||
|
|
f871ecb425 | ||
|
|
4a425cde78 | ||
|
|
f56252b0cc | ||
|
|
7ae6c66beb | ||
|
|
be90fb96ea | ||
|
|
dd3350b169 | ||
|
|
cdd1ba1fe3 | ||
|
|
10ea12c867 | ||
|
|
e6358d948a | ||
|
|
1101572f78 | ||
|
|
0cf8836832 | ||
|
|
6e4562b0e6 | ||
|
|
856f1e176a | ||
|
|
1653a57e7b | ||
|
|
5f03e580c2 | ||
|
|
d370ae48a2 | ||
|
|
38baeba254 | ||
|
|
478f9b9390 | ||
|
|
82ad3821c4 | ||
|
|
07afaf12d5 |
@@ -8,6 +8,8 @@ omit =
|
||||
*/example/*
|
||||
*/project_template/*
|
||||
*/bin/*
|
||||
*/tests/*
|
||||
*/tests.py
|
||||
|
||||
[report]
|
||||
exclude_lines =
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -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:
|
||||
@@ -62,3 +62,7 @@ celerybeat-schedule
|
||||
|
||||
#pycharm
|
||||
.idea/*
|
||||
/nbproject/
|
||||
|
||||
#gitlab configs
|
||||
.gitlab/
|
||||
|
||||
36
.gitlab-ci.yml
Normal file
36
.gitlab-ci.yml
Normal file
@@ -0,0 +1,36 @@
|
||||
stages:
|
||||
- "test"
|
||||
- deploy
|
||||
|
||||
before_script:
|
||||
- python -V
|
||||
- pip install wheel tox
|
||||
|
||||
test-3.5:
|
||||
image: python:3.5-stretch
|
||||
script:
|
||||
- tox -e py35
|
||||
|
||||
test-3.6:
|
||||
image: python:3.6-stretch
|
||||
script:
|
||||
- tox -e py36
|
||||
|
||||
test-3.7:
|
||||
image: python:3.7-stretch
|
||||
script:
|
||||
- tox -e py37
|
||||
|
||||
deploy_production:
|
||||
stage: deploy
|
||||
image: python:3.6-stretch
|
||||
|
||||
before_script:
|
||||
- pip install twine
|
||||
|
||||
script:
|
||||
- python setup.py sdist
|
||||
- twine upload dist/*
|
||||
|
||||
only:
|
||||
- tags
|
||||
14
.gitlab/issue_templates/Bug.md
Normal file
14
.gitlab/issue_templates/Bug.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Bug
|
||||
|
||||
- I have searched [issues](https://gitlab.com/allianceauth/allianceauth/issues?scope=all&utf8=%E2%9C%93&state=all) (Y/N):
|
||||
- What Version of Alliance Auth:
|
||||
- What Operating System:
|
||||
- Version of other components relevant to issue eg. Service, Database:
|
||||
|
||||
Please include a brief description of your issue here.
|
||||
|
||||
Please include steps to reproduce the issue
|
||||
|
||||
Please include any tracebacks or logs
|
||||
|
||||
Please include the results of the command `pip list`
|
||||
7
.gitlab/issue_templates/Feature Request.md
Normal file
7
.gitlab/issue_templates/Feature Request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Feature Request
|
||||
|
||||
- Describe the feature are you requesting.
|
||||
|
||||
- Is this a Service (external integration), a Module (Alliance Auth extension) or an enhancement to an existing service/module.
|
||||
|
||||
- Describe why its useful to you or others.
|
||||
38
README.md
38
README.md
@@ -1,37 +1,37 @@
|
||||
Alliance Auth
|
||||
============
|
||||
|
||||
[](https://gitter.im/R4stl1n/allianceauth?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](https://discord.gg/fjnHAmk)
|
||||
[](http://allianceauth.readthedocs.io/?badge=latest)
|
||||
[](https://travis-ci.org/allianceauth/allianceauth)
|
||||
[](https://coveralls.io/github/allianceauth/allianceauth?branch=master)
|
||||
[](https://gitlab.com/allianceauth/allianceauth/commits/master)
|
||||
[](https://gitlab.com/allianceauth/allianceauth/commits/master)
|
||||
|
||||
|
||||
EVE service auth to help corps, alliances, and coalitions manage services.
|
||||
Built for "The 99 Percent" open for anyone to use.
|
||||
|
||||
An auth system for EVE Online to help in-game organizations manage online service access.
|
||||
|
||||
[Read the docs here.](http://allianceauth.rtfd.io)
|
||||
|
||||
[Get help on gitter](https://gitter.im/R4stl1n/allianceauth) or submit an Issue.
|
||||
[Get help on Discord](https://discord.gg/fjnHAmk) or submit an Issue.
|
||||
|
||||
|
||||
Active Developers:
|
||||
|
||||
- [Adarnof](https://github.com/Adarnof)
|
||||
- [Basraah](https://github.com/basraah)
|
||||
|
||||
- [Adarnof](https://gitlab.com/adarnof/)
|
||||
- [Basraah](https://gitlab.com/basraah/)
|
||||
- [Ariel Rin](https://gitlab.com/soratidus999/)
|
||||
- [Col Crunch](https://gitlab.com/colcrunch/)
|
||||
|
||||
Beta Testers / Bug Fixers:
|
||||
|
||||
- [ghoti](https://github.com/ghoti)
|
||||
- [ghoti](https://gitlab.com/ChainsawMcGinny/)
|
||||
- [mmolitor87](https://gitlab.com/mmolitor87/)
|
||||
- [TargetZ3R0](https://github.com/TargetZ3R0)
|
||||
- [kaezon](https://github.com/kaezon/)
|
||||
- [orbitroom](https://github.com/orbitroom/)
|
||||
- [tehfiend](https://github.com/tehfiend/)
|
||||
|
||||
Special thanks to [Nikdoof](https://github.com/nikdoof/), as his [auth](https://github.com/nikdoof/test-auth) was the foundation for the original work on this project.
|
||||
|
||||
Past Beta Testers / Bug Fixers:
|
||||
|
||||
- TrentBartlem (Testing and Bug Fixes)
|
||||
- IskFiend (Bug Fixes and Server Configuration)
|
||||
- Mr McClain (Bug Fixes and server configuration)
|
||||
|
||||
Special Thanks:
|
||||
|
||||
- Thanks to Nikdoof, without his old auth implementation this project wouldn't be as far as it is now.
|
||||
### Contributing
|
||||
Make sure you have signed the [License Agreement](https://developers.eveonline.com/resource/license-agreement) by logging in at [https://developers.eveonline.com](https://developers.eveonline.com) before submitting any pull requests.
|
||||
|
||||
@@ -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.0-dev'
|
||||
__version__ = '2.3.0'
|
||||
NAME = 'Alliance Auth v%s' % __version__
|
||||
default_app_config = 'allianceauth.apps.AllianceAuthConfig'
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
from django.contrib.auth.models import User, Permission
|
||||
from django.contrib.auth.models import User as BaseUser, Permission as BasePermission
|
||||
from django.utils.text import slugify
|
||||
from django.db.models import Q
|
||||
from allianceauth.services.hooks import ServicesHook
|
||||
|
||||
from allianceauth.authentication.models import State, get_guest_state, CharacterOwnership, UserProfile
|
||||
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, OwnershipRecord
|
||||
from allianceauth.hooks import get_hooks
|
||||
from allianceauth.eveonline.models import EveCharacter
|
||||
from django.forms import ModelForm
|
||||
|
||||
|
||||
def make_service_hooks_update_groups_action(service):
|
||||
@@ -38,6 +42,47 @@ def make_service_hooks_sync_nickname_action(service):
|
||||
return sync_nickname
|
||||
|
||||
|
||||
class QuerysetModelForm(ModelForm):
|
||||
# allows specifying FK querysets through kwarg
|
||||
def __init__(self, querysets=None, *args, **kwargs):
|
||||
querysets = querysets or {}
|
||||
super().__init__(*args, **kwargs)
|
||||
for field, qs in querysets.items():
|
||||
self.fields[field].queryset = qs
|
||||
|
||||
|
||||
class UserProfileInline(admin.StackedInline):
|
||||
model = UserProfile
|
||||
readonly_fields = ('state',)
|
||||
form = QuerysetModelForm
|
||||
verbose_name = ''
|
||||
verbose_name_plural = 'Profile'
|
||||
|
||||
def get_formset(self, request, obj=None, **kwargs):
|
||||
# main_character field can only show current value or unclaimed alts
|
||||
# if superuser, allow selecting from any unclaimed main
|
||||
query = Q()
|
||||
if obj and obj.profile.main_character:
|
||||
query |= Q(pk=obj.profile.main_character_id)
|
||||
if request.user.is_superuser:
|
||||
query |= Q(userprofile__isnull=True)
|
||||
else:
|
||||
query |= Q(character_ownership__user=obj)
|
||||
qs = EveCharacter.objects.filter(query)
|
||||
formset = super().get_formset(request, obj=obj, **kwargs)
|
||||
|
||||
def get_kwargs(self, index):
|
||||
return {'querysets': {'main_character': EveCharacter.objects.filter(query)}}
|
||||
formset.get_form_kwargs = get_kwargs
|
||||
return formset
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
|
||||
class UserAdmin(BaseUserAdmin):
|
||||
"""
|
||||
Extending Django's UserAdmin model
|
||||
@@ -62,6 +107,25 @@ class UserAdmin(BaseUserAdmin):
|
||||
|
||||
return actions
|
||||
list_filter = BaseUserAdmin.list_filter + ('profile__state',)
|
||||
inlines = BaseUserAdmin.inlines + [UserProfileInline]
|
||||
list_display = ('username', 'email', 'get_main_character', 'get_state', 'is_active')
|
||||
|
||||
def get_main_character(self, obj):
|
||||
return obj.profile.main_character
|
||||
get_main_character.short_description = "Main Character"
|
||||
|
||||
def get_state(self, obj):
|
||||
return obj.profile.state
|
||||
get_state.short_description = "State"
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return request.user.has_perm('auth.change_user')
|
||||
|
||||
def has_add_permission(self, request, obj=None):
|
||||
return request.user.has_perm('auth.add_user')
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return request.user.has_perm('auth.delete_user')
|
||||
|
||||
|
||||
@admin.register(State)
|
||||
@@ -96,31 +160,30 @@ class StateAdmin(admin.ModelAdmin):
|
||||
return obj.userprofile_set.all().count()
|
||||
|
||||
|
||||
@admin.register(UserProfile)
|
||||
class UserProfileAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ('user', 'state')
|
||||
search_fields = ('user__username', 'main_character__character_name')
|
||||
list_filter = ('state',)
|
||||
list_display = ('user', 'main_character')
|
||||
actions = None
|
||||
class BaseOwnershipAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'character')
|
||||
search_fields = ('user__username', 'character__character_name', 'character__corporation_name', 'character__alliance_name')
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
if obj and obj.pk:
|
||||
return 'owner_hash', 'character'
|
||||
return tuple()
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
@admin.register(OwnershipRecord)
|
||||
class OwnershipRecordAdmin(BaseOwnershipAdmin):
|
||||
list_display = BaseOwnershipAdmin.list_display + ('created',)
|
||||
|
||||
|
||||
@admin.register(CharacterOwnership)
|
||||
class CharacterOwnershipAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'character')
|
||||
search_fields = ('user__username', 'character__character_name', 'character__corporation_name', 'character__alliance_name')
|
||||
readonly_fields = ('owner_hash', 'character')
|
||||
class CharacterOwnershipAdmin(BaseOwnershipAdmin):
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
|
||||
class PermissionAdmin(admin.ModelAdmin):
|
||||
actions = None
|
||||
readonly_fields = [field.name for field in Permission._meta.fields]
|
||||
readonly_fields = [field.name for field in BasePermission._meta.fields]
|
||||
list_display = ('admin_name', 'name', 'codename', 'content_type')
|
||||
list_filter = ('content_type__app_label',)
|
||||
|
||||
@@ -134,23 +197,61 @@ class PermissionAdmin(admin.ModelAdmin):
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def has_module_permission(self, request):
|
||||
return True
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
# can see list but not edit it
|
||||
return not obj
|
||||
|
||||
|
||||
# Hack to allow registration of django.contrib.auth models in our authentication app
|
||||
class ProxyUser(User):
|
||||
class User(BaseUser):
|
||||
class Meta:
|
||||
proxy = True
|
||||
verbose_name = User._meta.verbose_name
|
||||
verbose_name_plural = User._meta.verbose_name_plural
|
||||
verbose_name = BaseUser._meta.verbose_name
|
||||
verbose_name_plural = BaseUser._meta.verbose_name_plural
|
||||
|
||||
|
||||
class ProxyPermission(Permission):
|
||||
class Permission(BasePermission):
|
||||
class Meta:
|
||||
proxy = True
|
||||
verbose_name = Permission._meta.verbose_name
|
||||
verbose_name_plural = Permission._meta.verbose_name_plural
|
||||
verbose_name = BasePermission._meta.verbose_name
|
||||
verbose_name_plural = BasePermission._meta.verbose_name_plural
|
||||
|
||||
|
||||
try:
|
||||
admin.site.unregister(User)
|
||||
admin.site.unregister(BaseUser)
|
||||
finally:
|
||||
admin.site.register(ProxyUser, UserAdmin)
|
||||
admin.site.register(ProxyPermission, PermissionAdmin)
|
||||
admin.site.register(User, UserAdmin)
|
||||
admin.site.register(Permission, PermissionAdmin)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=User)
|
||||
def redirect_pre_save(sender, signal=None, *args, **kwargs):
|
||||
pre_save.send(BaseUser, *args, **kwargs)
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def redirect_post_save(sender, signal=None, *args, **kwargs):
|
||||
post_save.send(BaseUser, *args, **kwargs)
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=User)
|
||||
def redirect_pre_delete(sender, signal=None, *args, **kwargs):
|
||||
pre_delete.send(BaseUser, *args, **kwargs)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=User)
|
||||
def redirect_post_delete(sender, signal=None, *args, **kwargs):
|
||||
post_delete.send(BaseUser, *args, **kwargs)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=User.groups.through)
|
||||
def redirect_m2m_changed_groups(sender, signal=None, *args, **kwargs):
|
||||
m2m_changed.send(BaseUser, *args, **kwargs)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=User.user_permissions.through)
|
||||
def redirect_m2m_changed_permissions(sender, signal=None, *args, **kwargs):
|
||||
m2m_changed.send(BaseUser, *args, **kwargs)
|
||||
|
||||
@@ -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):
|
||||
@@ -24,37 +27,54 @@ class StateBackend(ModelBackend):
|
||||
user_obj._perm_cache.update(self.get_state_permissions(user_obj))
|
||||
return user_obj._perm_cache
|
||||
|
||||
def authenticate(self, token=None):
|
||||
def authenticate(self, request=None, token=None, **credentials):
|
||||
if not token:
|
||||
return None
|
||||
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
|
||||
|
||||
68
allianceauth/authentication/decorators.py
Normal file
68
allianceauth/authentication/decorators.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from django.conf.urls import include
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from functools import wraps
|
||||
from django.shortcuts import redirect
|
||||
from django.contrib import messages
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
|
||||
def user_has_main_character(user):
|
||||
return bool(user.profile.main_character)
|
||||
|
||||
|
||||
def decorate_url_patterns(urls, decorator):
|
||||
url_list, app_name, namespace = include(urls)
|
||||
|
||||
def process_patterns(url_patterns):
|
||||
for pattern in url_patterns:
|
||||
if hasattr(pattern, 'url_patterns'):
|
||||
# this is an include - apply to all nested patterns
|
||||
process_patterns(pattern.url_patterns)
|
||||
else:
|
||||
# this is a pattern
|
||||
pattern.callback = decorator(pattern.callback)
|
||||
|
||||
process_patterns(url_list)
|
||||
return url_list, app_name, namespace
|
||||
|
||||
|
||||
def main_character_required(view_func):
|
||||
@wraps(view_func)
|
||||
def _wrapped_view(request, *args, **kwargs):
|
||||
if user_has_main_character(request.user):
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
messages.error(request, _('A main character is required to perform that action. Add one below.'))
|
||||
return redirect('authentication:dashboard')
|
||||
return login_required(_wrapped_view)
|
||||
|
||||
|
||||
def permissions_required(perm, login_url=None, raise_exception=False):
|
||||
"""
|
||||
Decorator for views that checks whether a user has a particular permission
|
||||
enabled, redirecting to the log-in page if necessary.
|
||||
If the raise_exception parameter is given the PermissionDenied exception
|
||||
is raised.
|
||||
|
||||
This decorator is identical to the django permission_required except it
|
||||
allows for passing a tuple/list of perms that will return true if any one
|
||||
of them is present.
|
||||
"""
|
||||
def check_perms(user):
|
||||
if isinstance(perm, str):
|
||||
perms = (perm,)
|
||||
else:
|
||||
perms = perm
|
||||
# First check if the user has the permission (even anon users)
|
||||
for perm_ in perms:
|
||||
perm_ = (perm_,)
|
||||
if user.has_perms(perm_):
|
||||
return True
|
||||
# In case the 403 handler should be called raise the exception
|
||||
if raise_exception:
|
||||
raise PermissionDenied
|
||||
# As the last resort, show the login form
|
||||
return False
|
||||
return user_passes_test(check_perms, login_url=login_url)
|
||||
@@ -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.'))
|
||||
@@ -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')
|
||||
@@ -103,16 +108,16 @@ def populate_ownerships(apps, schema_editor):
|
||||
|
||||
unique_character_owners = [t['character_id'] for t in
|
||||
Token.objects.all().values('character_id').annotate(n=models.Count('user')) if
|
||||
t['n'] == 1 and EveCharacter.objects.filter(character_id=t['character_id'].exists())]
|
||||
t['n'] == 1 and EveCharacter.objects.filter(character_id=t['character_id']).exists()]
|
||||
|
||||
tokens = Token.objects.filter(character_id__in=unique_character_owners)
|
||||
for c_id in unique_character_owners:
|
||||
ts = tokens.filter(character_id=c_id).order_by('created')
|
||||
for t in ts:
|
||||
if t.can_refresh:
|
||||
# find newest refreshable token and use it as basis for CharacterOwnership
|
||||
CharacterOwnership.objecs.create_by_token(t)
|
||||
break
|
||||
# find newest refreshable token and use it as basis for CharacterOwnership
|
||||
ts = tokens.filter(character_id=c_id).exclude(refresh_token__isnull=True).order_by('created')
|
||||
if ts.exists():
|
||||
token = ts[0]
|
||||
char = EveCharacter.objects.get(character_id=token.character_id)
|
||||
CharacterOwnership.objects.create(user_id=token.user_id, character_id=char.id, owner_hash=token.character_owner_hash)
|
||||
|
||||
|
||||
def create_profiles(apps, schema_editor):
|
||||
@@ -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,8 +158,16 @@ 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=u.pk) for u in User.objects.all()])
|
||||
AuthServicesInfo.objects.bulk_create([AuthServicesInfo(user_id=u.pk) for u in User.objects.all()])
|
||||
|
||||
# repopulate main characters
|
||||
for profile in UserProfile.objects.exclude(main_character__isnull=True).select_related('user', 'main_character'):
|
||||
@@ -154,8 +176,8 @@ def recreate_authservicesinfo(apps, schema_editor):
|
||||
|
||||
# repopulate states we understand
|
||||
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):
|
||||
@@ -203,7 +225,6 @@ class Migration(migrations.Migration):
|
||||
('permissions', models.ManyToManyField(blank=True, to='auth.Permission')),
|
||||
],
|
||||
options={
|
||||
'default_permissions': ('change',),
|
||||
'ordering': ['-priority'],
|
||||
},
|
||||
),
|
||||
@@ -222,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(
|
||||
@@ -233,7 +255,7 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.RunPython(disable_passwords, migrations.RunPython.noop),
|
||||
migrations.CreateModel(
|
||||
name='ProxyPermission',
|
||||
name='Permission',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
@@ -247,7 +269,7 @@ class Migration(migrations.Migration):
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProxyUser',
|
||||
name='User',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def remove_permission(apps, schema_editor):
|
||||
User = apps.get_model('auth', 'User')
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
Permission = apps.get_model('auth', 'Permission')
|
||||
ct = ContentType.objects.get_for_model(User)
|
||||
Permission.objects.filter(codename="view_fleetup", content_type=ct, name="view_fleetup").delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('authentication', '0016_ownershiprecord'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(remove_permission, migrations.RunPython.noop)
|
||||
]
|
||||
@@ -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)
|
||||
@@ -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 post_save, pre_delete, m2m_changed, pre_save
|
||||
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed
|
||||
from django.dispatch import receiver, Signal
|
||||
from esi.models import Token
|
||||
|
||||
@@ -11,7 +11,6 @@ from allianceauth.eveonline.models import EveCharacter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
state_changed = Signal(providing_args=['user', 'state'])
|
||||
|
||||
|
||||
@@ -32,23 +31,27 @@ def trigger_state_check(state):
|
||||
@receiver(m2m_changed, sender=State.member_characters.through)
|
||||
def state_member_characters_changed(sender, instance, action, *args, **kwargs):
|
||||
if action.startswith('post_'):
|
||||
logger.debug('State {} member characters changed. Re-evaluating membership.'.format(instance))
|
||||
trigger_state_check(instance)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=State.member_corporations.through)
|
||||
def state_member_corporations_changed(sender, instance, action, *args, **kwargs):
|
||||
if action.startswith('post_'):
|
||||
logger.debug('State {} member corporations changed. Re-evaluating membership.'.format(instance))
|
||||
trigger_state_check(instance)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=State.member_alliances.through)
|
||||
def state_member_alliances_changed(sender, instance, action, *args, **kwargs):
|
||||
if action.startswith('post_'):
|
||||
logger.debug('State {} member alliances changed. Re-evaluating membership.'.format(instance))
|
||||
trigger_state_check(instance)
|
||||
|
||||
|
||||
@receiver(post_save, sender=State)
|
||||
def state_saved(sender, instance, *args, **kwargs):
|
||||
logger.debug('State {} saved. Re-evaluating membership.'.format(instance))
|
||||
trigger_state_check(instance)
|
||||
|
||||
|
||||
@@ -59,6 +62,7 @@ def reassess_on_profile_save(sender, instance, created, *args, **kwargs):
|
||||
if not created:
|
||||
update_fields = kwargs.pop('update_fields', []) or []
|
||||
if 'state' not in update_fields:
|
||||
logger.debug('Profile for {} saved without state change. Re-evaluating state.'.format(instance.user))
|
||||
instance.assign_state()
|
||||
|
||||
|
||||
@@ -66,12 +70,15 @@ def reassess_on_profile_save(sender, instance, created, *args, **kwargs):
|
||||
def create_required_models(sender, instance, created, *args, **kwargs):
|
||||
# ensure all users have a model
|
||||
if created:
|
||||
logger.debug('User {} created. Creating default UserProfile.'.format(instance))
|
||||
UserProfile.objects.get_or_create(user=instance)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Token)
|
||||
def record_character_ownership(sender, instance, created, *args, **kwargs):
|
||||
if created:
|
||||
logger.debug('New token for {0} character {1} saved. Evaluating ownership.'.format(instance.user,
|
||||
instance.character_name))
|
||||
if instance.user:
|
||||
query = Q(owner_hash=instance.character_owner_hash) & Q(user=instance.user)
|
||||
else:
|
||||
@@ -80,10 +87,15 @@ def record_character_ownership(sender, instance, created, *args, **kwargs):
|
||||
CharacterOwnership.objects.filter(character__character_id=instance.character_id).exclude(query).delete()
|
||||
# create character if needed
|
||||
if EveCharacter.objects.filter(character_id=instance.character_id).exists() is False:
|
||||
logger.debug('Token is for a new character. Creating model for {0} ({1})'.format(instance.character_name,
|
||||
instance.character_id))
|
||||
EveCharacter.objects.create_character(instance.character_id)
|
||||
char = EveCharacter.objects.get(character_id=instance.character_id)
|
||||
# check if we need to create ownership
|
||||
if instance.user and not CharacterOwnership.objects.filter(character__character_id=instance.character_id).exists():
|
||||
if instance.user and not CharacterOwnership.objects.filter(
|
||||
character__character_id=instance.character_id).exists():
|
||||
logger.debug("Character {0} is not yet owned. Assigning ownership to {1}".format(instance.character_name,
|
||||
instance.user))
|
||||
CharacterOwnership.objects.update_or_create(character=char,
|
||||
defaults={'owner_hash': instance.character_owner_hash,
|
||||
'user': instance.user})
|
||||
@@ -91,20 +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:
|
||||
# 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():
|
||||
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).exists():
|
||||
# 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)
|
||||
@@ -114,8 +129,11 @@ def assign_state_on_active_change(sender, instance, *args, **kwargs):
|
||||
old_instance = User.objects.get(pk=instance.pk)
|
||||
if old_instance.is_active != instance.is_active:
|
||||
if instance.is_active:
|
||||
logger.debug("User {0} has been activated. Assigning state.".format(instance))
|
||||
instance.profile.assign_state()
|
||||
else:
|
||||
logger.debug(
|
||||
"User {0} has been deactivated. Revoking state and assigning to guest state.".format(instance))
|
||||
instance.profile.state = get_guest_state()
|
||||
instance.profile.save(update_fields=['state'])
|
||||
|
||||
@@ -124,6 +142,20 @@ def assign_state_on_active_change(sender, instance, *args, **kwargs):
|
||||
def check_state_on_character_update(sender, instance, *args, **kwargs):
|
||||
# if this is a main character updating, check that user's state
|
||||
try:
|
||||
logger.debug("Character {0} has been saved. Assessing owner's state for changes.".format(instance))
|
||||
instance.userprofile.assign_state()
|
||||
except UserProfile.DoesNotExist:
|
||||
logger.debug("Character {0} is not a main character. No state assessment required.".format(instance))
|
||||
pass
|
||||
|
||||
|
||||
@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)
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
<div class="col-lg-4 col-sm-2">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<td class="text-center"><img class="ra-avatar"
|
||||
src="https://image.eveonline.com/Character/{{ main.character_id }}_128.jpg">
|
||||
<td class="text-center">
|
||||
<img class="ra-avatar"src="{{ main.portrait_url_128 }}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -32,8 +32,8 @@
|
||||
<div class="col-lg-4 col-sm-2">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<td class="text-center"><img class="ra-avatar"
|
||||
src="https://image.eveonline.com/Corporation/{{ main.corporation_id }}_128.png">
|
||||
<td class="text-center">
|
||||
<img class="ra-avatar"src="{{ main.corporation_logo_url_128 }}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -45,8 +45,8 @@
|
||||
{% if main.alliance_id %}
|
||||
<table class="table">
|
||||
<tr>
|
||||
<td class="text-center"><img class="ra-avatar"
|
||||
src="https://image.eveonline.com/Alliance/{{ main.alliance_id }}_128.png">
|
||||
<td class="text-center">
|
||||
<img class="ra-avatar"src="{{ main.alliance_logo_url_128 }}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -57,7 +57,7 @@
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<div class="alert alert-danger" role="alert">{% trans "Missing main character model." %}</div>
|
||||
<div class="alert alert-danger" role="alert">{% trans "No main character set." %}</div>
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
<div class="col-xs-6">
|
||||
@@ -102,8 +102,7 @@
|
||||
{% for ownership in request.user.character_ownerships.all %}
|
||||
{% with ownership.character as char %}
|
||||
<tr>
|
||||
<td class="text-center"><img class="ra-avatar img-circle"
|
||||
src="https://image.eveonline.com/Character/{{ char.character_id }}_32.jpg">
|
||||
<td class="text-center"><img class="ra-avatar img-circle" src="{{ char.portrait_url_32 }}">
|
||||
</td>
|
||||
<td class="text-center">{{ char.character_name }}</td>
|
||||
<td class="text-center">{{ char.corporation_name }}</td>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<style>
|
||||
body {
|
||||
background: url('{% static 'authentication/img/background.jpg' %}') no-repeat scroll;
|
||||
background: url('{% static 'authentication/img/background.jpg' %}') no-repeat center center fixed;
|
||||
-webkit-background-size: cover;
|
||||
-moz-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
@@ -48,4 +48,4 @@
|
||||
{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<div class="dropdown">
|
||||
<form action="{% url 'set_language' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<input name="next" type="hidden" value="{{ request.get_full_path|slice:'3:' }}" />
|
||||
<select onchange="this.form.submit()" class="form-control" id="lang-select" name="language">
|
||||
{% get_language_info_list for LANGUAGES as languages %}
|
||||
{% for language in languages %}
|
||||
@@ -12,4 +11,4 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ You're receiving this email because someone has entered this email address while
|
||||
|
||||
If this was you, please go to the following URL to confirm your email address:
|
||||
|
||||
{{ url }}
|
||||
{{ scheme }}://{{ url }}
|
||||
|
||||
This link will expire in {{ expiration_days }} day(s).
|
||||
|
||||
|
||||
@@ -1,13 +1,70 @@
|
||||
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'
|
||||
|
||||
|
||||
class DecoratorTestCase(TestCase):
|
||||
@staticmethod
|
||||
@main_character_required
|
||||
def dummy_view(*args, **kwargs):
|
||||
return HttpResponse(status=200)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.main_user = AuthUtils.create_user('main_user', disconnect_signals=True)
|
||||
cls.no_main_user = AuthUtils.create_user('no_main_user', disconnect_signals=True)
|
||||
main_character = EveCharacter.objects.create(
|
||||
character_id=1,
|
||||
character_name='Main Character',
|
||||
corporation_id=1,
|
||||
corporation_name='Corp',
|
||||
corporation_ticker='CORP',
|
||||
)
|
||||
CharacterOwnership.objects.create(user=cls.main_user, character=main_character, owner_hash='1')
|
||||
cls.main_user.profile.main_character = main_character
|
||||
|
||||
def setUp(self):
|
||||
self.request = RequestFactory().get('/test/')
|
||||
|
||||
@mock.patch(MODULE_PATH + '.decorators.messages')
|
||||
def test_login_redirect(self, m):
|
||||
setattr(self.request, 'user', AnonymousUser())
|
||||
response = self.dummy_view(self.request)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
url = getattr(response, 'url', None)
|
||||
self.assertEqual(parse.urlparse(url).path, reverse(settings.LOGIN_URL))
|
||||
|
||||
@mock.patch(MODULE_PATH + '.decorators.messages')
|
||||
def test_main_character_redirect(self, m):
|
||||
setattr(self.request, 'user', self.no_main_user)
|
||||
response = self.dummy_view(self.request)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
url = getattr(response, 'url', None)
|
||||
self.assertEqual(url, reverse('authentication:dashboard'))
|
||||
|
||||
@mock.patch(MODULE_PATH + '.decorators.messages')
|
||||
def test_successful_request(self, m):
|
||||
setattr(self.request, 'user', self.main_user)
|
||||
response = self.dummy_view(self.request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class BackendTestCase(TestCase):
|
||||
@@ -35,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')
|
||||
@@ -58,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(token=t)
|
||||
self.assertEqual(user, self.old_user)
|
||||
self.assertTrue(CharacterOwnership.objects.filter(owner_hash='4', user=self.old_user).exists())
|
||||
self.assertTrue(user.profile.main_character)
|
||||
|
||||
def test_iterate_username(self):
|
||||
t = Token(character_id=self.unclaimed_character.character_id,
|
||||
character_name=self.unclaimed_character.character_name, character_owner_hash='3')
|
||||
@@ -130,28 +196,6 @@ class CharacterOwnershipTestCase(TestCase):
|
||||
self.user = User.objects.get(pk=self.user.pk)
|
||||
self.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
|
||||
@@ -286,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())
|
||||
|
||||
@@ -10,6 +10,7 @@ from django.urls import reverse
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from esi.decorators import token_required
|
||||
from esi.models import Token
|
||||
from registration.backends.hmac.views import RegistrationView as BaseRegistrationView, \
|
||||
ActivationView as BaseActivationView, REGISTRATION_SALT
|
||||
from registration.signals import user_registered
|
||||
@@ -71,17 +72,22 @@ have the email address embedded much like the username. Key creation and decodin
|
||||
@token_required(new=True, scopes=settings.LOGIN_TOKEN_SCOPES)
|
||||
def sso_login(request, token):
|
||||
user = authenticate(token=token)
|
||||
if user and user.is_active:
|
||||
login(request, user)
|
||||
return redirect(request.POST.get('next', request.GET.get('next', 'authentication:dashboard')))
|
||||
elif user and not user.email:
|
||||
# Store the new user PK in the session to enable us to identify the registering user in Step 2
|
||||
request.session['registration_uid'] = user.pk
|
||||
# Go to Step 2
|
||||
return redirect('registration_register')
|
||||
else:
|
||||
messages.error(request, _('Unable to authenticate as the selected character.'))
|
||||
return redirect(settings.LOGIN_URL)
|
||||
if user:
|
||||
token.user = user
|
||||
if Token.objects.exclude(pk=token.pk).equivalent_to(token).require_valid().exists():
|
||||
token.delete()
|
||||
else:
|
||||
token.save()
|
||||
if user.is_active:
|
||||
login(request, user)
|
||||
return redirect(request.POST.get('next', request.GET.get('next', 'authentication:dashboard')))
|
||||
elif not user.email:
|
||||
# Store the new user PK in the session to enable us to identify the registering user in Step 2
|
||||
request.session['registration_uid'] = user.pk
|
||||
# Go to Step 2
|
||||
return redirect('registration_register')
|
||||
messages.error(request, _('Unable to authenticate as the selected character.'))
|
||||
return redirect(settings.LOGIN_URL)
|
||||
|
||||
|
||||
# Step 2
|
||||
@@ -89,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):
|
||||
|
||||
@@ -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:'])
|
||||
|
||||
@@ -16,10 +16,16 @@ class CorpStatsQuerySet(models.QuerySet):
|
||||
assert char
|
||||
# build all accepted queries
|
||||
queries = [models.Q(token__user=user)]
|
||||
if user.has_perm('corputils.view_corp_corpstats'):
|
||||
queries.append(models.Q(corp__corporation_id=char.corporation_id))
|
||||
if user.has_perm('corputils.view_alliance_corpstats'):
|
||||
queries.append(models.Q(corp__alliance__alliance_id=char.alliance_id))
|
||||
if char.alliance_id is not None:
|
||||
queries.append(models.Q(corp__alliance__alliance_id=char.alliance_id))
|
||||
else:
|
||||
queries.append(models.Q(corp__corporation_id=char.corporation_id))
|
||||
if user.has_perm('corputils.view_corp_corpstats'):
|
||||
if user.has_perm('corputils.view_alliance_corpstats'):
|
||||
pass
|
||||
else:
|
||||
queries.append(models.Q(corp__corporation_id=char.corporation_id))
|
||||
if user.has_perm('corputils.view_state_corpstats'):
|
||||
queries.append(models.Q(corp__in=user.profile.state.member_corporations.all()))
|
||||
queries.append(models.Q(corp__alliance__in=user.profile.state.member_alliances.all()))
|
||||
|
||||
@@ -6,12 +6,23 @@ from bravado.exception import HTTPForbidden
|
||||
from django.db import models
|
||||
from esi.errors import TokenError
|
||||
from esi.models import Token
|
||||
from allianceauth.eveonline.models import EveCorporationInfo, EveCharacter
|
||||
from allianceauth.eveonline.models import EveCorporationInfo, EveCharacter,\
|
||||
EveAllianceInfo
|
||||
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 +51,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
|
||||
@@ -121,17 +131,20 @@ class CorpStats(models.Model):
|
||||
m.main_character and int(m.main_character.character_id) == int(
|
||||
m.character_id)])
|
||||
|
||||
def visible_to(self, user):
|
||||
return CorpStats.objects.filter(pk=self.pk).visible_to(user).exists()
|
||||
|
||||
def can_update(self, user):
|
||||
return user.is_superuser or user == self.token.user
|
||||
return self.token.user == user or self.visible_to(user)
|
||||
|
||||
def corp_logo(self, size=128):
|
||||
return "https://image.eveonline.com/Corporation/%s_%s.png" % (self.corp.corporation_id, size)
|
||||
return self.corp.logo_url(size)
|
||||
|
||||
def alliance_logo(self, size=128):
|
||||
if self.corp.alliance:
|
||||
return "https://image.eveonline.com/Alliance/%s_%s.png" % (self.corp.alliance.alliance_id, size)
|
||||
return self.corp.alliance.logo_url(size)
|
||||
else:
|
||||
return "https://image.eveonline.com/Alliance/1_%s.png" % size
|
||||
return EveAllianceInfo.generic_logo_url(1, size)
|
||||
|
||||
|
||||
class CorpMember(models.Model):
|
||||
@@ -173,10 +186,16 @@ class CorpMember(models.Model):
|
||||
return CharacterOwnership.objects.filter(character__character_id=self.character_id).exists()
|
||||
|
||||
def portrait_url(self, size=32):
|
||||
return "https://image.eveonline.com/Character/%s_%s.jpg" % (self.character_id, size)
|
||||
return EveCharacter.generic_portrait_url(self.character_id, size)
|
||||
|
||||
def __getattr__(self, item):
|
||||
if item.startswith('portrait_url_'):
|
||||
size = item.strip('portrait_url_')
|
||||
return self.portrait_url(size)
|
||||
return super(CorpMember, self).__getattr__(item)
|
||||
@property
|
||||
def portrait_url_32(self):
|
||||
return self.portrait_url(32)
|
||||
|
||||
@property
|
||||
def portrait_url_64(self):
|
||||
return self.portrait_url(64)
|
||||
|
||||
@property
|
||||
def portrait_url_128(self):
|
||||
return self.portrait_url(128)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
from celery import shared_task
|
||||
from allianceauth.corputils import CorpStats
|
||||
from allianceauth.corputils.models import CorpStats
|
||||
|
||||
|
||||
@shared_task
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
<tr>
|
||||
<td class="text-center col-lg-6
|
||||
{% if corpstats.corp.alliance %}{% else %}col-lg-offset-3{% endif %}"><img
|
||||
class="ra-avatar" src="{{ corpstats.corp_logo }}"></td>
|
||||
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 }}">
|
||||
<td class="text-center col-lg-6"><img class="ra-avatar" src="{{ corpstats.corp.alliance.logo_url_128 }}">
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
@@ -70,6 +70,7 @@
|
||||
{% for alt in main.alts %}
|
||||
{% if forloop.first %}
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="text-center">{% trans "Character" %}</th>
|
||||
<th class="text-center">{% trans "Corporation" %}</th>
|
||||
<th class="text-center">{% trans "Alliance" %}</th>
|
||||
@@ -77,10 +78,15 @@
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td class="text-center">{{ alt.character_name }}</td>
|
||||
<td class="text-center">{{ alt.corporation_name }}</td>
|
||||
<td class="text-center">{{ alt.alliance_name }}</td>
|
||||
<td class="text-center">
|
||||
<td class="text-center" style="width:5%">
|
||||
<div class="thumbnail" style="border: 0 none; box-shadow: none; background: transparent;">
|
||||
<img src="{{ alt.portrait_url_32 }}" class="img-circle">
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center" style="width:30%">{{ alt.character_name }}</td>
|
||||
<td class="text-center" style="width:30%">{{ alt.corporation_name }}</td>
|
||||
<td class="text-center" style="width:30%">{{ alt.alliance_name }}</td>
|
||||
<td class="text-center" style="width:5%">
|
||||
<a href="https://zkillboard.com/character/{{ alt.character_id }}/"
|
||||
class="label label-danger" target="_blank">
|
||||
{% trans "Killboard" %}
|
||||
@@ -175,9 +181,25 @@
|
||||
{% endblock %}
|
||||
{% block extra_script %}
|
||||
$(document).ready(function(){
|
||||
$('#table-mains').DataTable();
|
||||
$('#table-members').DataTable();
|
||||
$('#table-unregistered').DataTable();
|
||||
$('#table-mains').DataTable({
|
||||
"columnDefs": [
|
||||
{ "sortable": false, "targets": [1] },
|
||||
],
|
||||
});
|
||||
$('#table-members').DataTable({
|
||||
"columnDefs": [
|
||||
{ "searchable": false, "targets": [0, 2] },
|
||||
{ "sortable": false, "targets": [0, 2] },
|
||||
],
|
||||
"order": [[ 1, "asc" ]],
|
||||
});
|
||||
$('#table-unregistered').DataTable({
|
||||
"columnDefs": [
|
||||
{ "searchable": false, "targets": [0, 2] },
|
||||
{ "sortable": false, "targets": [0, 2] },
|
||||
],
|
||||
"order": [[ 1, "asc" ]],
|
||||
});
|
||||
|
||||
});
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -205,13 +205,13 @@ class CorpStatsPropertiesTestCase(TestCase):
|
||||
AuthUtils.connect_signals()
|
||||
|
||||
def test_logos(self):
|
||||
self.assertEqual(self.corpstats.corp_logo(size=128), 'https://image.eveonline.com/Corporation/2_128.png')
|
||||
self.assertEqual(self.corpstats.alliance_logo(size=128), 'https://image.eveonline.com/Alliance/1_128.png')
|
||||
self.assertEqual(self.corpstats.corp_logo(size=128), 'https://images.evetech.net/corporations/2/logo?size=128')
|
||||
self.assertEqual(self.corpstats.alliance_logo(size=128), 'https://images.evetech.net/alliances/1/logo?size=128')
|
||||
|
||||
alliance = EveAllianceInfo.objects.create(alliance_name='test alliance', alliance_id='3', alliance_ticker='TEST', executor_corp_id='2')
|
||||
self.corp.alliance = alliance
|
||||
self.corp.save()
|
||||
self.assertEqual(self.corpstats.alliance_logo(size=128), 'https://image.eveonline.com/Alliance/3_128.png')
|
||||
self.assertEqual(self.corpstats.alliance_logo(size=128), 'https://images.evetech.net/alliances/3/logo?size=128')
|
||||
alliance.delete()
|
||||
|
||||
|
||||
@@ -273,5 +273,7 @@ class CorpMemberTestCase(TestCase):
|
||||
AuthUtils.connect_signals()
|
||||
|
||||
def test_portrait_url(self):
|
||||
self.assertEquals(self.member.portrait_url(size=32), 'https://image.eveonline.com/Character/2_32.jpg')
|
||||
self.assertEquals(self.member.portrait_url(size=32), 'https://images.evetech.net/characters/2/portrait?size=32')
|
||||
self.assertEquals(self.member.portrait_url(size=32), self.member.portrait_url_32)
|
||||
self.assertEquals(self.member.portrait_url(size=64), self.member.portrait_url_64)
|
||||
self.assertEquals(self.member.portrait_url(size=128), self.member.portrait_url_128)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.contrib import admin
|
||||
from django.db import models
|
||||
from .models import AutogroupsConfig
|
||||
from .models import AutogroupsConfig, ManagedCorpGroup, ManagedAllianceGroup
|
||||
|
||||
import logging
|
||||
|
||||
@@ -37,3 +37,6 @@ class AutogroupsConfigAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
admin.site.register(AutogroupsConfig, AutogroupsConfigAdmin)
|
||||
admin.site.register(ManagedCorpGroup)
|
||||
admin.site.register(ManagedAllianceGroup)
|
||||
|
||||
|
||||
@@ -4,3 +4,6 @@ from django.apps import AppConfig
|
||||
class EveAutogroupsConfig(AppConfig):
|
||||
name = 'allianceauth.eveonline.autogroups'
|
||||
label = 'eve_autogroups'
|
||||
|
||||
def ready(self):
|
||||
import allianceauth.eveonline.autogroups.signals
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.5 on 2017-09-29 14:44
|
||||
# Generated by Django 1.11.6 on 2017-12-23 04:30
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
@@ -29,40 +29,31 @@ class Migration(migrations.Migration):
|
||||
('alliance_name_source', models.CharField(choices=[('ticker', 'Ticker'), ('name', 'Full name')], default='name', max_length=20)),
|
||||
('replace_spaces', models.BooleanField(default=False)),
|
||||
('replace_spaces_with', models.CharField(blank=True, default='', help_text='Any spaces in the group name will be replaced with this.', max_length=10)),
|
||||
('states', models.ManyToManyField(related_name='autogroups', to='authentication.State')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ManagedGroup',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ManagedAllianceGroup',
|
||||
fields=[
|
||||
('managedgroup_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='eve_autogroups.ManagedGroup')),
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('alliance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eveonline.EveAllianceInfo')),
|
||||
('config', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eve_autogroups.AutogroupsConfig')),
|
||||
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.Group')),
|
||||
],
|
||||
bases=('eve_autogroups.managedgroup',),
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ManagedCorpGroup',
|
||||
fields=[
|
||||
('managedgroup_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='eve_autogroups.ManagedGroup')),
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('config', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eve_autogroups.AutogroupsConfig')),
|
||||
('corp', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eveonline.EveCorporationInfo')),
|
||||
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.Group')),
|
||||
],
|
||||
bases=('eve_autogroups.managedgroup',),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='managedgroup',
|
||||
name='config',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eve_autogroups.AutogroupsConfig'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='managedgroup',
|
||||
name='group',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.Group'),
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='autogroupsconfig',
|
||||
@@ -74,4 +65,9 @@ class Migration(migrations.Migration):
|
||||
name='corp_managed_groups',
|
||||
field=models.ManyToManyField(help_text="A list of corporation groups created and maintained by this AutogroupConfig. You should not edit this list unless you know what you're doing.", related_name='corp_managed_config', through='eve_autogroups.ManagedCorpGroup', to='auth.Group'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='autogroupsconfig',
|
||||
name='states',
|
||||
field=models.ManyToManyField(related_name='autogroups', to='authentication.State'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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):
|
||||
@@ -110,21 +115,24 @@ class AutogroupsConfig(models.Model):
|
||||
group = None
|
||||
try:
|
||||
if not self.alliance_groups or not self.user_entitled_to_groups(user):
|
||||
logger.debug('User {} does not have required state'.format(user))
|
||||
logger.debug('User {} does not have required state for alliance group membership'.format(user))
|
||||
return
|
||||
else:
|
||||
alliance = user.profile.main_character.alliance
|
||||
if alliance is None:
|
||||
logger.debug('User {} alliance is None, cannot update group membership'.format(user))
|
||||
return
|
||||
group = self.get_alliance_group(alliance)
|
||||
except EveAllianceInfo.DoesNotExist:
|
||||
logger.warning('User {} main characters alliance does not exist in the database.'
|
||||
' Group membership not updated'.format(user))
|
||||
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:
|
||||
self.remove_user_from_corp_groups(user, except_group=group)
|
||||
self.remove_user_from_alliance_groups(user, except_group=group)
|
||||
if group is not None:
|
||||
logger.debug('Adding user {} to alliance group {}'.format(user, group))
|
||||
user.groups.add(group)
|
||||
|
||||
@transaction.atomic
|
||||
@@ -132,18 +140,20 @@ class AutogroupsConfig(models.Model):
|
||||
group = None
|
||||
try:
|
||||
if not self.corp_groups or not self.user_entitled_to_groups(user):
|
||||
logger.debug('User {} does not have required state'.format(user))
|
||||
logger.debug('User {} does not have required state for corp group membership'.format(user))
|
||||
else:
|
||||
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:
|
||||
self.remove_user_from_corp_groups(user, except_group=group)
|
||||
if group is not None:
|
||||
logger.debug('Adding user {} to corp group {}'.format(user, group))
|
||||
user.groups.add(group)
|
||||
|
||||
@transaction.atomic
|
||||
@@ -169,28 +179,28 @@ class AutogroupsConfig(models.Model):
|
||||
@transaction.atomic
|
||||
def create_alliance_group(self, alliance: EveAllianceInfo) -> Group:
|
||||
group, created = Group.objects.get_or_create(name=self.get_alliance_group_name(alliance))
|
||||
if created:
|
||||
ManagedAllianceGroup.objects.create(group=group, config=self, alliance=alliance)
|
||||
ManagedAllianceGroup.objects.get_or_create(group=group, config=self, alliance=alliance)
|
||||
return group
|
||||
|
||||
@transaction.atomic
|
||||
def create_corp_group(self, corp: EveCorporationInfo) -> Group:
|
||||
group, created = Group.objects.get_or_create(name=self.get_corp_group_name(corp))
|
||||
if created:
|
||||
ManagedCorpGroup.objects.create(group=group, config=self, corp=corp)
|
||||
ManagedCorpGroup.objects.get_or_create(group=group, config=self, corp=corp)
|
||||
return group
|
||||
|
||||
def delete_alliance_managed_groups(self):
|
||||
"""
|
||||
Deletes ALL managed alliance groups
|
||||
"""
|
||||
self.alliance_managed_groups.all().delete()
|
||||
for g in self.alliance_managed_groups.all():
|
||||
g.delete()
|
||||
|
||||
def delete_corp_managed_groups(self):
|
||||
"""
|
||||
Deletes ALL managed corp groups
|
||||
"""
|
||||
self.corp_managed_groups.all().delete()
|
||||
for g in self.corp_managed_groups.all():
|
||||
g.delete()
|
||||
|
||||
def get_alliance_group_name(self, alliance: EveAllianceInfo) -> str:
|
||||
if self.alliance_name_source == self.OPT_TICKER:
|
||||
@@ -225,6 +235,11 @@ class ManagedGroup(models.Model):
|
||||
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
||||
config = models.ForeignKey(AutogroupsConfig, on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def __str__(self):
|
||||
return "Managed Group: %s" % self.group.name
|
||||
|
||||
class ManagedCorpGroup(ManagedGroup):
|
||||
corp = models.ForeignKey(EveCorporationInfo, on_delete=models.CASCADE)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -82,6 +82,7 @@ class AutogroupsConfigTestCase(TestCase):
|
||||
|
||||
# Act
|
||||
obj.update_alliance_group_membership(self.member)
|
||||
obj.update_corp_group_membership(self.member) # check for no side effects
|
||||
|
||||
group = obj.create_alliance_group(self.alliance)
|
||||
group_qs = Group.objects.filter(pk=group.pk)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
23
allianceauth/eveonline/migrations/0010_alliance_ticker.py
Normal file
23
allianceauth/eveonline/migrations/0010_alliance_ticker.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -7,6 +7,90 @@ from .managers import EveAllianceManager, EveAllianceProviderManager
|
||||
from . import providers
|
||||
|
||||
|
||||
EVE_IMAGE_SERVER_URL = 'https://images.evetech.net'
|
||||
|
||||
|
||||
def _eve_entity_image_url(
|
||||
category: str,
|
||||
id: int,
|
||||
size: int = 32,
|
||||
variant: str = None,
|
||||
tenant: str = None,
|
||||
) -> str:
|
||||
"""returns image URL for an Eve Online ID.
|
||||
Supported categories: `alliance`, `corporation`, `character`
|
||||
|
||||
Arguments:
|
||||
- category: category of the ID
|
||||
- id: Eve ID of the entity
|
||||
- size: (optional) render size of the image.must be between 32 (default) and 1024
|
||||
- variant: (optional) image variant for category. currently not relevant.
|
||||
- tentant: (optional) Eve Server, either `tranquility`(default) or `singularity`
|
||||
|
||||
Returns:
|
||||
- URL string for the requested image on the Eve image server
|
||||
|
||||
Exceptions:
|
||||
- Throws ValueError on invalid input
|
||||
"""
|
||||
|
||||
# input validations
|
||||
categories = {
|
||||
'alliance': {
|
||||
'endpoint': 'alliances',
|
||||
'variants': [
|
||||
'logo'
|
||||
]
|
||||
},
|
||||
'corporation': {
|
||||
'endpoint': 'corporations',
|
||||
'variants': [
|
||||
'logo'
|
||||
]
|
||||
},
|
||||
'character': {
|
||||
'endpoint': 'characters',
|
||||
'variants': [
|
||||
'portrait'
|
||||
]
|
||||
}
|
||||
}
|
||||
tenants = ['tranquility', 'singularity']
|
||||
|
||||
if size < 32 or size > 1024 or (size & (size - 1) != 0):
|
||||
raise ValueError('Invalid size: {}'.format(size))
|
||||
|
||||
if category not in categories:
|
||||
raise ValueError('Invalid category {}'.format(category))
|
||||
else:
|
||||
endpoint = categories[category]['endpoint']
|
||||
|
||||
if variant:
|
||||
if variant not in categories[category]['variants']:
|
||||
raise ValueError('Invalid variant {} for category {}'.format(
|
||||
variant,
|
||||
category
|
||||
))
|
||||
else:
|
||||
variant = categories[category]['variants'][0]
|
||||
|
||||
if tenant and tenant not in tenants:
|
||||
raise ValueError('Invalid tentant {}'.format(tenant))
|
||||
|
||||
# compose result URL
|
||||
result = '{}/{}/{}/{}?size={}'.format(
|
||||
EVE_IMAGE_SERVER_URL,
|
||||
endpoint,
|
||||
id,
|
||||
variant,
|
||||
size
|
||||
)
|
||||
if tenant:
|
||||
result += '&tenant={}'.format(tenant)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class EveAllianceInfo(models.Model):
|
||||
alliance_id = models.CharField(max_length=254, unique=True)
|
||||
alliance_name = models.CharField(max_length=254, unique=True)
|
||||
@@ -35,6 +119,35 @@ class EveAllianceInfo(models.Model):
|
||||
def __str__(self):
|
||||
return self.alliance_name
|
||||
|
||||
@staticmethod
|
||||
def generic_logo_url(alliance_id: int, size: int = 32) -> str:
|
||||
"""image URL for the given alliance ID"""
|
||||
return _eve_entity_image_url('alliance', alliance_id, size)
|
||||
|
||||
def logo_url(self, size:int = 32) -> str:
|
||||
"""image URL of this alliance"""
|
||||
return self.generic_logo_url(self.alliance_id, size)
|
||||
|
||||
@property
|
||||
def logo_url_32(self) -> str:
|
||||
"""image URL for this alliance"""
|
||||
return self.logo_url(32)
|
||||
|
||||
@property
|
||||
def logo_url_64(self) -> str:
|
||||
"""image URL for this alliance"""
|
||||
return self.logo_url(64)
|
||||
|
||||
@property
|
||||
def logo_url_128(self) -> str:
|
||||
"""image URL for this alliance"""
|
||||
return self.logo_url(128)
|
||||
|
||||
@property
|
||||
def logo_url_256(self) -> str:
|
||||
"""image URL for this alliance"""
|
||||
return self.logo_url(256)
|
||||
|
||||
|
||||
class EveCorporationInfo(models.Model):
|
||||
corporation_id = models.CharField(max_length=254, unique=True)
|
||||
@@ -60,15 +173,45 @@ class EveCorporationInfo(models.Model):
|
||||
def __str__(self):
|
||||
return self.corporation_name
|
||||
|
||||
@staticmethod
|
||||
def generic_logo_url(corporation_id: int, size: int = 32) -> str:
|
||||
"""image URL for the given corporation ID"""
|
||||
return _eve_entity_image_url('corporation', corporation_id, size)
|
||||
|
||||
def logo_url(self, size:int = 32) -> str:
|
||||
"""image URL for this corporation"""
|
||||
return self.generic_logo_url(self.corporation_id, size)
|
||||
|
||||
@property
|
||||
def logo_url_32(self) -> str:
|
||||
"""image URL for this corporation"""
|
||||
return self.logo_url(32)
|
||||
|
||||
@property
|
||||
def logo_url_64(self) -> str:
|
||||
"""image URL for this corporation"""
|
||||
return self.logo_url(64)
|
||||
|
||||
@property
|
||||
def logo_url_128(self) -> str:
|
||||
"""image URL for this corporation"""
|
||||
return self.logo_url(128)
|
||||
|
||||
@property
|
||||
def logo_url_256(self) -> str:
|
||||
"""image URL for this corporation"""
|
||||
return self.logo_url(256)
|
||||
|
||||
|
||||
class EveCharacter(models.Model):
|
||||
character_id = models.CharField(max_length=254, unique=True)
|
||||
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()
|
||||
@@ -102,8 +245,89 @@ 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
|
||||
|
||||
def __str__(self):
|
||||
return self.character_name
|
||||
|
||||
@staticmethod
|
||||
def generic_portrait_url(character_id: int, size: int = 32) -> str:
|
||||
"""image URL for the given character ID"""
|
||||
return _eve_entity_image_url('character', character_id, size)
|
||||
|
||||
def portrait_url(self, size = 32) -> str:
|
||||
"""image URL for this character"""
|
||||
return self.generic_portrait_url(self.character_id, size)
|
||||
|
||||
@property
|
||||
def portrait_url_32(self) -> str:
|
||||
"""image URL for this character"""
|
||||
return self.portrait_url(32)
|
||||
|
||||
@property
|
||||
def portrait_url_64(self) -> str:
|
||||
"""image URL for this character"""
|
||||
return self.portrait_url(64)
|
||||
|
||||
@property
|
||||
def portrait_url_128(self) -> str:
|
||||
"""image URL for this character"""
|
||||
return self.portrait_url(128)
|
||||
|
||||
@property
|
||||
def portrait_url_256(self) -> str:
|
||||
"""image URL for this character"""
|
||||
return self.portrait_url(256)
|
||||
|
||||
def corporation_logo_url(self, size = 32) -> str:
|
||||
"""image URL for corporation of this character"""
|
||||
return EveCorporationInfo.generic_logo_url(self.corporation_id, size)
|
||||
|
||||
@property
|
||||
def corporation_logo_url_32(self) -> str:
|
||||
"""image URL for corporation of this character"""
|
||||
return self.corporation_logo_url(32)
|
||||
|
||||
@property
|
||||
def corporation_logo_url_64(self) -> str:
|
||||
"""image URL for corporation of this character"""
|
||||
return self.corporation_logo_url(64)
|
||||
|
||||
@property
|
||||
def corporation_logo_url_128(self) -> str:
|
||||
"""image URL for corporation of this character"""
|
||||
return self.corporation_logo_url(128)
|
||||
|
||||
@property
|
||||
def corporation_logo_url_256(self) -> str:
|
||||
"""image URL for corporation of this character"""
|
||||
return self.corporation_logo_url(256)
|
||||
|
||||
def alliance_logo_url(self, size = 32) -> str:
|
||||
"""image URL for alliance of this character or empty string"""
|
||||
if self.alliance_id:
|
||||
return EveAllianceInfo.generic_logo_url(self.alliance_id, size)
|
||||
else:
|
||||
return ''
|
||||
|
||||
@property
|
||||
def alliance_logo_url_32(self) -> str:
|
||||
"""image URL for alliance of this character or empty string"""
|
||||
return self.alliance_logo_url(32)
|
||||
|
||||
@property
|
||||
def alliance_logo_url_64(self) -> str:
|
||||
"""image URL for alliance of this character or empty string"""
|
||||
return self.alliance_logo_url(64)
|
||||
|
||||
@property
|
||||
def alliance_logo_url_128(self) -> str:
|
||||
"""image URL for alliance of this character or empty string"""
|
||||
return self.alliance_logo_url(128)
|
||||
|
||||
@property
|
||||
def alliance_logo_url_256(self) -> str:
|
||||
"""image URL for alliance of this character or empty string"""
|
||||
return self.alliance_logo_url(256)
|
||||
|
||||
@@ -4,6 +4,17 @@ 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
|
||||
post_character_affiliation
|
||||
"""
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -81,7 +92,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 +163,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 +177,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 +190,13 @@ 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
|
||||
affiliation = self.client.Character.post_characters_affiliation(characters=[character_id]).result()[0]
|
||||
|
||||
model = Character(
|
||||
id=character_id,
|
||||
name=data['name'],
|
||||
corp_id=data['corporation_id'],
|
||||
alliance_id=alliance_id,
|
||||
corp_id=affiliation['corporation_id'],
|
||||
alliance_id=affiliation['alliance_id'] if 'alliance_id' in affiliation else None,
|
||||
)
|
||||
return model
|
||||
except (HTTPNotFound, HTTPUnprocessableEntity):
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,8 +1,104 @@
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from ..models import EveCharacter, EveCorporationInfo, EveAllianceInfo
|
||||
from ..models import EveCharacter, EveCorporationInfo, \
|
||||
EveAllianceInfo, _eve_entity_image_url
|
||||
from ..providers import Alliance, Corporation, Character
|
||||
|
||||
|
||||
class EveUniverseImageUrlTestCase(TestCase):
|
||||
"""unit test for _eve_entity_image_url()"""
|
||||
|
||||
def test_sizes(self):
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('character', 42),
|
||||
'https://images.evetech.net/characters/42/portrait?size=32'
|
||||
)
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('character', 42, size=32),
|
||||
'https://images.evetech.net/characters/42/portrait?size=32'
|
||||
)
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('character', 42, size=64),
|
||||
'https://images.evetech.net/characters/42/portrait?size=64'
|
||||
)
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('character', 42, size=128),
|
||||
'https://images.evetech.net/characters/42/portrait?size=128'
|
||||
)
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('character', 42, size=256),
|
||||
'https://images.evetech.net/characters/42/portrait?size=256'
|
||||
)
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('character', 42, size=512),
|
||||
'https://images.evetech.net/characters/42/portrait?size=512'
|
||||
)
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('character', 42, size=1024),
|
||||
'https://images.evetech.net/characters/42/portrait?size=1024'
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
_eve_entity_image_url('corporation', 42, size=-5)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
_eve_entity_image_url('corporation', 42, size=0)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
_eve_entity_image_url('corporation', 42, size=31)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
_eve_entity_image_url('corporation', 42, size=1025)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
_eve_entity_image_url('corporation', 42, size=2048)
|
||||
|
||||
|
||||
def test_variant(self):
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('character', 42, variant='portrait'),
|
||||
'https://images.evetech.net/characters/42/portrait?size=32'
|
||||
)
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('alliance', 42, variant='logo'),
|
||||
'https://images.evetech.net/alliances/42/logo?size=32'
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
_eve_entity_image_url('character', 42, variant='logo')
|
||||
|
||||
|
||||
def test_alliance(self):
|
||||
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('alliance', 42),
|
||||
'https://images.evetech.net/alliances/42/logo?size=32'
|
||||
)
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('corporation', 42),
|
||||
'https://images.evetech.net/corporations/42/logo?size=32'
|
||||
)
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('character', 42),
|
||||
'https://images.evetech.net/characters/42/portrait?size=32'
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
_eve_entity_image_url('station', 42)
|
||||
|
||||
|
||||
def test_tenants(self):
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('character', 42, tenant='tranquility'),
|
||||
'https://images.evetech.net/characters/42/portrait?size=32&tenant=tranquility'
|
||||
)
|
||||
self.assertEqual(
|
||||
_eve_entity_image_url('character', 42, tenant='singularity'),
|
||||
'https://images.evetech.net/characters/42/portrait?size=32&tenant=singularity'
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
_eve_entity_image_url('character', 42, tenant='xxx')
|
||||
|
||||
|
||||
class EveCharacterTestCase(TestCase):
|
||||
def test_corporation_prop(self):
|
||||
"""
|
||||
@@ -119,3 +215,410 @@ class EveCharacterTestCase(TestCase):
|
||||
)
|
||||
|
||||
self.assertIsNone(character.alliance)
|
||||
|
||||
@patch('allianceauth.eveonline.providers.provider')
|
||||
def test_update_character(self, mock_provider):
|
||||
mock_provider.get_corp.return_value = Corporation(
|
||||
id=2002,
|
||||
name='Dummy Corp 2',
|
||||
ticker='DC2',
|
||||
ceo_id=1001,
|
||||
members=34,
|
||||
)
|
||||
|
||||
my_character = EveCharacter.objects.create(
|
||||
character_id='1001',
|
||||
character_name='Bruce Wayne',
|
||||
corporation_id='2001',
|
||||
corporation_name='Dummy Corp 1',
|
||||
corporation_ticker='DC1',
|
||||
alliance_id='3001',
|
||||
alliance_name='Dummy Alliance 1',
|
||||
)
|
||||
my_updated_character = Character(
|
||||
name='Bruce X. Wayne',
|
||||
corp_id=2002
|
||||
)
|
||||
my_character.update_character(my_updated_character)
|
||||
self.assertEqual(my_character.character_name, 'Bruce X. Wayne')
|
||||
|
||||
# todo: add test cases not yet covered, e.g. with alliance
|
||||
|
||||
|
||||
def test_image_url(self):
|
||||
self.assertEqual(
|
||||
EveCharacter.generic_portrait_url(42),
|
||||
_eve_entity_image_url('character', 42)
|
||||
)
|
||||
self.assertEqual(
|
||||
EveCharacter.generic_portrait_url(42, 256),
|
||||
_eve_entity_image_url('character', 42, 256)
|
||||
)
|
||||
|
||||
def test_portrait_urls(self):
|
||||
x = EveCharacter(
|
||||
character_id='42',
|
||||
character_name='character.name',
|
||||
corporation_id='123',
|
||||
corporation_name='corporation.name',
|
||||
corporation_ticker='ABC',
|
||||
)
|
||||
self.assertEqual(
|
||||
x.portrait_url(),
|
||||
_eve_entity_image_url('character', 42)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.portrait_url(64),
|
||||
_eve_entity_image_url('character', 42, size=64)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.portrait_url_32,
|
||||
_eve_entity_image_url('character', 42, size=32)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.portrait_url_64,
|
||||
_eve_entity_image_url('character', 42, size=64)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.portrait_url_128,
|
||||
_eve_entity_image_url('character', 42, size=128)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.portrait_url_256,
|
||||
_eve_entity_image_url('character', 42, size=256)
|
||||
)
|
||||
|
||||
|
||||
def test_corporation_logo_urls(self):
|
||||
x = EveCharacter(
|
||||
character_id='42',
|
||||
character_name='character.name',
|
||||
corporation_id='123',
|
||||
corporation_name='corporation.name',
|
||||
corporation_ticker='ABC',
|
||||
)
|
||||
self.assertEqual(
|
||||
x.corporation_logo_url(),
|
||||
_eve_entity_image_url('corporation', 123)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.corporation_logo_url(256),
|
||||
_eve_entity_image_url('corporation', 123, size=256)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.corporation_logo_url_32,
|
||||
_eve_entity_image_url('corporation', 123, size=32)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.corporation_logo_url_64,
|
||||
_eve_entity_image_url('corporation', 123, size=64)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.corporation_logo_url_128,
|
||||
_eve_entity_image_url('corporation', 123, size=128)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.corporation_logo_url_256,
|
||||
_eve_entity_image_url('corporation', 123, size=256)
|
||||
)
|
||||
|
||||
|
||||
def test_alliance_logo_urls(self):
|
||||
x = EveCharacter(
|
||||
character_id='42',
|
||||
character_name='character.name',
|
||||
corporation_id='123',
|
||||
corporation_name='corporation.name',
|
||||
corporation_ticker='ABC',
|
||||
)
|
||||
self.assertEqual(
|
||||
x.alliance_logo_url(),
|
||||
''
|
||||
)
|
||||
self.assertEqual(
|
||||
x.alliance_logo_url_32,
|
||||
''
|
||||
)
|
||||
self.assertEqual(
|
||||
x.alliance_logo_url_64,
|
||||
''
|
||||
)
|
||||
self.assertEqual(
|
||||
x.alliance_logo_url_128,
|
||||
''
|
||||
)
|
||||
self.assertEqual(
|
||||
x.alliance_logo_url_256,
|
||||
''
|
||||
)
|
||||
x.alliance_id = 987
|
||||
self.assertEqual(
|
||||
x.alliance_logo_url(),
|
||||
_eve_entity_image_url('alliance', 987)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.alliance_logo_url(128),
|
||||
_eve_entity_image_url('alliance', 987, size=128)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.alliance_logo_url_32,
|
||||
_eve_entity_image_url('alliance', 987, size=32)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.alliance_logo_url_64,
|
||||
_eve_entity_image_url('alliance', 987, size=64)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.alliance_logo_url_128,
|
||||
_eve_entity_image_url('alliance', 987, size=128)
|
||||
)
|
||||
self.assertEqual(
|
||||
x.alliance_logo_url_256,
|
||||
_eve_entity_image_url('alliance', 987, size=256)
|
||||
)
|
||||
|
||||
|
||||
class EveAllianceTestCase(TestCase):
|
||||
|
||||
def test_str(self):
|
||||
my_alliance = EveAllianceInfo(
|
||||
alliance_id=3001,
|
||||
alliance_name='Dummy Alliance 1',
|
||||
alliance_ticker='DA1',
|
||||
executor_corp_id=2001
|
||||
)
|
||||
self.assertEqual(str(my_alliance), 'Dummy Alliance 1')
|
||||
|
||||
@patch(
|
||||
'allianceauth.eveonline.models.EveCorporationInfo.objects.create_corporation'
|
||||
)
|
||||
def test_populate_alliance(self, mock_create_corporation):
|
||||
|
||||
def create_corp(corp_id):
|
||||
if corp_id == 2002:
|
||||
EveCorporationInfo.objects.create(
|
||||
corporation_id=2002,
|
||||
corporation_name='Dummy Corporation 2',
|
||||
corporation_ticker='DC2',
|
||||
member_count=87,
|
||||
)
|
||||
else:
|
||||
raise ValueError()
|
||||
|
||||
mock_EveAllianceProviderManager = Mock()
|
||||
mock_EveAllianceProviderManager.get_alliance.return_value = \
|
||||
Alliance(
|
||||
id=3001,
|
||||
name='Dummy Alliance 1',
|
||||
corp_ids=[2001, 2002]
|
||||
)
|
||||
mock_create_corporation.side_effect = create_corp
|
||||
|
||||
EveCorporationInfo.objects.create(
|
||||
corporation_id=2001,
|
||||
corporation_name='Dummy Corporation 1',
|
||||
corporation_ticker='DC1',
|
||||
member_count=42,
|
||||
)
|
||||
|
||||
my_alliance = EveAllianceInfo(
|
||||
alliance_id=3001,
|
||||
alliance_name='Dummy Alliance 1',
|
||||
alliance_ticker='DA1',
|
||||
executor_corp_id=2001
|
||||
)
|
||||
my_alliance.provider = mock_EveAllianceProviderManager
|
||||
my_alliance.save()
|
||||
my_alliance.populate_alliance()
|
||||
|
||||
for corporation in EveCorporationInfo.objects\
|
||||
.filter(corporation_id__in=[2001, 2002]
|
||||
):
|
||||
self.assertEqual(corporation.alliance, my_alliance)
|
||||
|
||||
def test_update_alliance_with_object(self):
|
||||
my_alliance = EveAllianceInfo.objects.create(
|
||||
alliance_id=3001,
|
||||
alliance_name='Dummy Alliance 1',
|
||||
alliance_ticker='DA1',
|
||||
executor_corp_id=2001
|
||||
)
|
||||
updated_alliance = Alliance(
|
||||
id=3002,
|
||||
name='Dummy Alliance 2',
|
||||
corp_ids=[2004],
|
||||
executor_corp_id=2004
|
||||
)
|
||||
my_alliance.update_alliance(updated_alliance)
|
||||
my_alliance.refresh_from_db()
|
||||
self.assertEqual(int(my_alliance.executor_corp_id), 2004)
|
||||
|
||||
# potential bug
|
||||
# update_alliance() is only updateting executor_corp_id when object is given
|
||||
|
||||
|
||||
def test_update_alliance_wo_object(self):
|
||||
mock_EveAllianceProviderManager = Mock()
|
||||
mock_EveAllianceProviderManager.get_alliance.return_value = \
|
||||
Alliance(
|
||||
id=3002,
|
||||
name='Dummy Alliance 2',
|
||||
corp_ids=[2004],
|
||||
executor_corp_id=2004
|
||||
)
|
||||
|
||||
my_alliance = EveAllianceInfo.objects.create(
|
||||
alliance_id=3001,
|
||||
alliance_name='Dummy Alliance 1',
|
||||
alliance_ticker='DA1',
|
||||
executor_corp_id=2001
|
||||
)
|
||||
my_alliance.provider = mock_EveAllianceProviderManager
|
||||
my_alliance.save()
|
||||
updated_alliance = Alliance(
|
||||
name='Dummy Alliance 2',
|
||||
corp_ids=[2004],
|
||||
executor_corp_id=2004
|
||||
)
|
||||
my_alliance.update_alliance()
|
||||
my_alliance.refresh_from_db()
|
||||
self.assertEqual(int(my_alliance.executor_corp_id), 2004)
|
||||
|
||||
# potential bug
|
||||
# update_alliance() is only updateting executor_corp_id nothing else ???
|
||||
|
||||
|
||||
def test_image_url(self):
|
||||
self.assertEqual(
|
||||
EveAllianceInfo.generic_logo_url(42),
|
||||
_eve_entity_image_url('alliance', 42)
|
||||
)
|
||||
self.assertEqual(
|
||||
EveAllianceInfo.generic_logo_url(42, 256),
|
||||
_eve_entity_image_url('alliance', 42, 256)
|
||||
)
|
||||
|
||||
def test_logo_url(self):
|
||||
x = EveAllianceInfo(
|
||||
alliance_id='42',
|
||||
alliance_name='alliance.name',
|
||||
alliance_ticker='ABC',
|
||||
executor_corp_id='123'
|
||||
)
|
||||
self.assertEqual(
|
||||
x.logo_url(),
|
||||
'https://images.evetech.net/alliances/42/logo?size=32'
|
||||
)
|
||||
self.assertEqual(
|
||||
x.logo_url(64),
|
||||
'https://images.evetech.net/alliances/42/logo?size=64'
|
||||
)
|
||||
self.assertEqual(
|
||||
x.logo_url_32,
|
||||
'https://images.evetech.net/alliances/42/logo?size=32'
|
||||
)
|
||||
self.assertEqual(
|
||||
x.logo_url_64,
|
||||
'https://images.evetech.net/alliances/42/logo?size=64'
|
||||
)
|
||||
self.assertEqual(
|
||||
x.logo_url_128,
|
||||
'https://images.evetech.net/alliances/42/logo?size=128'
|
||||
)
|
||||
self.assertEqual(
|
||||
x.logo_url_256,
|
||||
'https://images.evetech.net/alliances/42/logo?size=256'
|
||||
)
|
||||
|
||||
|
||||
class EveCorporationTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
my_alliance = EveAllianceInfo.objects.create(
|
||||
alliance_id=3001,
|
||||
alliance_name='Dummy Alliance 1',
|
||||
alliance_ticker='DA1',
|
||||
executor_corp_id=2001
|
||||
)
|
||||
self.my_corp = EveCorporationInfo(
|
||||
corporation_id=2001,
|
||||
corporation_name='Dummy Corporation 1',
|
||||
corporation_ticker='DC1',
|
||||
member_count=42,
|
||||
alliance=my_alliance
|
||||
)
|
||||
|
||||
def test_str(self):
|
||||
self.assertEqual(str(self.my_corp), 'Dummy Corporation 1')
|
||||
|
||||
def test_update_corporation_from_object_w_alliance(self):
|
||||
updated_corp = Corporation(
|
||||
members=87
|
||||
)
|
||||
self.my_corp.update_corporation(updated_corp)
|
||||
self.assertEqual(self.my_corp.member_count, 87)
|
||||
|
||||
# potential bug
|
||||
# update_corporation updates member_count only
|
||||
|
||||
def test_update_corporation_no_object_w_alliance(self):
|
||||
mock_provider = Mock()
|
||||
mock_provider.get_corporation.return_value = Corporation(
|
||||
members=87
|
||||
)
|
||||
self.my_corp.provider = mock_provider
|
||||
|
||||
self.my_corp.update_corporation()
|
||||
self.assertEqual(self.my_corp.member_count, 87)
|
||||
|
||||
def test_update_corporation_from_object_wo_alliance(self):
|
||||
my_corp2 = EveCorporationInfo(
|
||||
corporation_id=2011,
|
||||
corporation_name='Dummy Corporation 11',
|
||||
corporation_ticker='DC11',
|
||||
member_count=6
|
||||
)
|
||||
updated_corp = Corporation(
|
||||
members=8
|
||||
)
|
||||
my_corp2.update_corporation(updated_corp)
|
||||
self.assertEqual(my_corp2.member_count, 8)
|
||||
self.assertIsNone(my_corp2.alliance)
|
||||
|
||||
|
||||
def test_image_url(self):
|
||||
self.assertEqual(
|
||||
EveCorporationInfo.generic_logo_url(42),
|
||||
_eve_entity_image_url('corporation', 42)
|
||||
)
|
||||
self.assertEqual(
|
||||
EveCorporationInfo.generic_logo_url(42, 256),
|
||||
_eve_entity_image_url('corporation', 42, 256)
|
||||
)
|
||||
|
||||
def test_logo_url(self):
|
||||
self.assertEqual(
|
||||
self.my_corp.logo_url(),
|
||||
'https://images.evetech.net/corporations/2001/logo?size=32'
|
||||
)
|
||||
self.assertEqual(
|
||||
self.my_corp.logo_url(64),
|
||||
'https://images.evetech.net/corporations/2001/logo?size=64'
|
||||
)
|
||||
self.assertEqual(
|
||||
self.my_corp.logo_url_32,
|
||||
'https://images.evetech.net/corporations/2001/logo?size=32'
|
||||
)
|
||||
self.assertEqual(
|
||||
self.my_corp.logo_url_64,
|
||||
'https://images.evetech.net/corporations/2001/logo?size=64'
|
||||
)
|
||||
self.assertEqual(
|
||||
self.my_corp.logo_url_128,
|
||||
'https://images.evetech.net/corporations/2001/logo?size=128'
|
||||
)
|
||||
self.assertEqual(
|
||||
self.my_corp.logo_url_256,
|
||||
'https://images.evetech.net/corporations/2001/logo?size=256'
|
||||
)
|
||||
|
||||
|
||||
545
allianceauth/eveonline/tests/test_providers.py
Normal file
545
allianceauth/eveonline/tests/test_providers.py
Normal file
@@ -0,0 +1,545 @@
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from bravado.exception import HTTPNotFound, HTTPUnprocessableEntity
|
||||
from django.test import TestCase
|
||||
|
||||
from ..models import EveCharacter, EveCorporationInfo, EveAllianceInfo
|
||||
from ..providers import ObjectNotFound, Entity, Character, Corporation, \
|
||||
Alliance, ItemType, EveProvider, EveSwaggerProvider
|
||||
|
||||
|
||||
class TestObjectNotFound(TestCase):
|
||||
|
||||
def test_str(self):
|
||||
x = ObjectNotFound(1001, 'Character')
|
||||
self.assertEqual(str(x), 'Character with ID 1001 not found.')
|
||||
|
||||
|
||||
class TestEntity(TestCase):
|
||||
|
||||
def test_str(self):
|
||||
x = Entity(1001, 'Bruce Wayne')
|
||||
self.assertEqual(str(x), 'Bruce Wayne')
|
||||
|
||||
# bug - does not return a string
|
||||
"""
|
||||
x = Entity(1001)
|
||||
self.assertEqual(str(x), '')
|
||||
|
||||
x = Entity()
|
||||
self.assertEqual(str(x), '')
|
||||
"""
|
||||
|
||||
def test_repr(self):
|
||||
x = Entity(1001, 'Bruce Wayne')
|
||||
self.assertEqual(repr(x), '<Entity (1001): Bruce Wayne>')
|
||||
|
||||
x = Entity(1001)
|
||||
self.assertEqual(repr(x), '<Entity (1001): None>')
|
||||
|
||||
x = Entity()
|
||||
self.assertEqual(repr(x), '<Entity (None): None>')
|
||||
|
||||
|
||||
def test_bool(self):
|
||||
x = Entity(1001)
|
||||
self.assertTrue(bool(x))
|
||||
|
||||
x = Entity()
|
||||
self.assertFalse(bool(x))
|
||||
|
||||
def test_eq(self):
|
||||
x1 = Entity(1001)
|
||||
x2 = Entity(1001)
|
||||
y = Entity(1002)
|
||||
z1 = Entity()
|
||||
z2 = Entity()
|
||||
|
||||
self.assertEqual(x1, x2)
|
||||
self.assertNotEqual(x1, y)
|
||||
self.assertNotEqual(x1, z1)
|
||||
self.assertEqual(z1, z2)
|
||||
|
||||
# bug: missing _neq_ in Equity to compliment _eq_
|
||||
|
||||
|
||||
class TestCorporation(TestCase):
|
||||
|
||||
@patch('allianceauth.eveonline.providers.EveSwaggerProvider.get_alliance')
|
||||
def test_alliance_defined(self, mock_provider_get_alliance):
|
||||
my_alliance = Alliance(
|
||||
id=3001,
|
||||
name='Dummy Alliance',
|
||||
ticker='Dummy',
|
||||
corp_ids=[2001, 2002, 2003],
|
||||
executor_corp_id=2001
|
||||
)
|
||||
mock_provider_get_alliance.return_value = my_alliance
|
||||
|
||||
x = Corporation(alliance_id=3001)
|
||||
self.assertEqual(
|
||||
x.alliance,
|
||||
my_alliance
|
||||
)
|
||||
self.assertEqual(
|
||||
x.alliance,
|
||||
my_alliance
|
||||
)
|
||||
# should fetch alliance once only
|
||||
self.assertEqual(mock_provider_get_alliance.call_count, 1)
|
||||
|
||||
|
||||
@patch('allianceauth.eveonline.providers.EveSwaggerProvider.get_alliance')
|
||||
def test_alliance_not_defined(self, mock_provider_get_alliance):
|
||||
mock_provider_get_alliance.return_value = None
|
||||
|
||||
x = Corporation()
|
||||
self.assertEqual(
|
||||
x.alliance,
|
||||
Entity(None, None)
|
||||
)
|
||||
|
||||
|
||||
@patch('allianceauth.eveonline.providers.EveSwaggerProvider.get_character')
|
||||
def test_ceo(self, mock_provider_get_character):
|
||||
my_ceo = Character(
|
||||
id=1001,
|
||||
name='Bruce Wayne',
|
||||
corp_id=2001,
|
||||
alliance_id=3001
|
||||
)
|
||||
mock_provider_get_character.return_value = my_ceo
|
||||
|
||||
# fetch from provider if not defined
|
||||
x = Corporation()
|
||||
self.assertEqual(
|
||||
x.ceo,
|
||||
my_ceo
|
||||
)
|
||||
|
||||
# return existing if defined
|
||||
mock_provider_get_character.return_value = None
|
||||
self.assertEqual(
|
||||
x.ceo,
|
||||
my_ceo
|
||||
)
|
||||
self.assertEqual(mock_provider_get_character.call_count, 1)
|
||||
|
||||
# bug in ceo(): will try to fetch character even if ceo_id is None
|
||||
|
||||
|
||||
class TestAlliance(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.my_alliance = Alliance(
|
||||
id=3001,
|
||||
name='Dummy Alliance',
|
||||
ticker='Dummy',
|
||||
corp_ids=[2001, 2002, 2003],
|
||||
executor_corp_id=2001
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_corp(corp_id):
|
||||
corps = {
|
||||
2001: Corporation(
|
||||
id=2001,
|
||||
name='Dummy Corp 1',
|
||||
alliance_id=3001
|
||||
),
|
||||
2002: Corporation(
|
||||
id=2002,
|
||||
name='Dummy Corp 2',
|
||||
alliance_id=3001
|
||||
),
|
||||
2003: Corporation(
|
||||
id=2003,
|
||||
name='Dummy Corp 3',
|
||||
alliance_id=3001
|
||||
),
|
||||
}
|
||||
|
||||
if corp_id:
|
||||
return corps[int(corp_id)]
|
||||
|
||||
@patch('allianceauth.eveonline.providers.EveSwaggerProvider.get_corp')
|
||||
def test_corp(self, mock_provider_get_corp):
|
||||
mock_provider_get_corp.side_effect = TestAlliance._get_corp
|
||||
|
||||
# should fetch corp if not in the object
|
||||
self.assertEqual(
|
||||
self.my_alliance.corp(2001),
|
||||
TestAlliance._get_corp(2001)
|
||||
)
|
||||
# should fetch corp if not in the object
|
||||
self.assertEqual(
|
||||
self.my_alliance.corp(2002),
|
||||
TestAlliance._get_corp(2002)
|
||||
)
|
||||
# should return from the object if its there
|
||||
self.assertEqual(
|
||||
self.my_alliance.corp(2001),
|
||||
TestAlliance._get_corp(2001)
|
||||
)
|
||||
# should return from the object if its there
|
||||
self.assertEqual(
|
||||
self.my_alliance.corp(2002),
|
||||
TestAlliance._get_corp(2002)
|
||||
)
|
||||
# should be called once by used corp only
|
||||
self.assertEqual(mock_provider_get_corp.call_count, 2)
|
||||
|
||||
|
||||
@patch('allianceauth.eveonline.providers.EveSwaggerProvider.get_corp')
|
||||
def test_corps(self, mock_provider_get_corp):
|
||||
mock_provider_get_corp.side_effect = TestAlliance._get_corp
|
||||
|
||||
self.assertEqual(
|
||||
self.my_alliance.corps,
|
||||
[
|
||||
TestAlliance._get_corp(2001),
|
||||
TestAlliance._get_corp(2002),
|
||||
TestAlliance._get_corp(2003),
|
||||
]
|
||||
)
|
||||
|
||||
@patch('allianceauth.eveonline.providers.EveSwaggerProvider.get_corp')
|
||||
def test_executor_corp(self, mock_provider_get_corp):
|
||||
mock_provider_get_corp.side_effect = TestAlliance._get_corp
|
||||
|
||||
self.assertEqual(
|
||||
self.my_alliance.executor_corp,
|
||||
TestAlliance._get_corp(2001),
|
||||
)
|
||||
|
||||
x = Alliance()
|
||||
self.assertEqual(
|
||||
x.executor_corp,
|
||||
Entity(None, None),
|
||||
)
|
||||
|
||||
|
||||
class TestCharacter(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.my_character = Character(
|
||||
id=1001,
|
||||
name='Bruce Wayne',
|
||||
corp_id=2001,
|
||||
alliance_id=3001
|
||||
)
|
||||
|
||||
@patch('allianceauth.eveonline.providers.EveSwaggerProvider.get_corp')
|
||||
def test_corp(self, mock_provider_get_corp):
|
||||
my_corp = Corporation(
|
||||
id=2001,
|
||||
name='Dummy Corp 1'
|
||||
)
|
||||
mock_provider_get_corp.return_value = my_corp
|
||||
|
||||
self.assertEqual(self.my_character.corp, my_corp)
|
||||
self.assertEqual(self.my_character.corp, my_corp)
|
||||
|
||||
# should call the provider one time only
|
||||
self.assertEqual(mock_provider_get_corp.call_count, 1)
|
||||
|
||||
|
||||
@patch('allianceauth.eveonline.providers.EveSwaggerProvider.get_alliance')
|
||||
@patch('allianceauth.eveonline.providers.EveSwaggerProvider.get_corp')
|
||||
def test_alliance_has_one(
|
||||
self,
|
||||
mock_provider_get_corp,
|
||||
mock_provider_get_alliance,
|
||||
):
|
||||
my_corp = Corporation(
|
||||
id=2001,
|
||||
name='Dummy Corp 1',
|
||||
alliance_id=3001
|
||||
)
|
||||
mock_provider_get_corp.return_value = my_corp
|
||||
my_alliance = Alliance(
|
||||
id=3001,
|
||||
name='Dummy Alliance 1',
|
||||
executor_corp_id=2001,
|
||||
corp_ids=[2001, 2002]
|
||||
)
|
||||
mock_provider_get_alliance.return_value = my_alliance
|
||||
|
||||
self.assertEqual(self.my_character.alliance, my_alliance)
|
||||
self.assertEqual(self.my_character.alliance, my_alliance)
|
||||
|
||||
# should call the provider one time only
|
||||
self.assertEqual(mock_provider_get_corp.call_count, 1)
|
||||
self.assertEqual(mock_provider_get_alliance.call_count, 1)
|
||||
|
||||
|
||||
def test_alliance_has_none(self):
|
||||
self.my_character.alliance_id = None
|
||||
self.assertEqual(self.my_character.alliance, Entity(None, None))
|
||||
|
||||
|
||||
class TestItemType(TestCase):
|
||||
|
||||
def test_init(self):
|
||||
x = ItemType(id=99, name='Dummy Item')
|
||||
self.assertIsInstance(x, ItemType)
|
||||
|
||||
|
||||
class TestEveProvider(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.my_provider = EveProvider()
|
||||
|
||||
def test_get_alliance(self):
|
||||
with self.assertRaises(NotImplementedError):
|
||||
self.my_provider.get_alliance(3001)
|
||||
|
||||
def test_get_corp(self):
|
||||
with self.assertRaises(NotImplementedError):
|
||||
self.my_provider.get_corp(2001)
|
||||
|
||||
def test_get_character(self):
|
||||
with self.assertRaises(NotImplementedError):
|
||||
self.my_provider.get_character(1001)
|
||||
|
||||
# bug: should be calling NotImplementedError() not NotImplemented
|
||||
"""
|
||||
def test_get_itemtype(self):
|
||||
with self.assertRaises(NotImplementedError):
|
||||
self.my_provider.get_itemtype(4001)
|
||||
"""
|
||||
|
||||
|
||||
class TestEveSwaggerProvider(TestCase):
|
||||
|
||||
|
||||
@staticmethod
|
||||
def esi_get_alliances_alliance_id(alliance_id):
|
||||
alliances = {
|
||||
3001: {
|
||||
'name': 'Dummy Alliance 1',
|
||||
'ticker': 'DA1',
|
||||
'executor_corporation_id': 2001
|
||||
},
|
||||
3002: {
|
||||
'name': 'Dummy Alliance 2',
|
||||
'ticker': 'DA2'
|
||||
}
|
||||
}
|
||||
mock_result = Mock()
|
||||
if alliance_id in alliances:
|
||||
mock_result.result.return_value = alliances[alliance_id]
|
||||
return mock_result
|
||||
else:
|
||||
raise HTTPNotFound(Mock())
|
||||
|
||||
|
||||
@staticmethod
|
||||
def esi_get_alliances_alliance_id_corporations(alliance_id):
|
||||
alliances = {
|
||||
3001: [2001, 2002, 2003],
|
||||
3002: [2004, 2005]
|
||||
}
|
||||
mock_result = Mock()
|
||||
if alliance_id in alliances:
|
||||
mock_result.result.return_value = alliances[alliance_id]
|
||||
return mock_result
|
||||
else:
|
||||
raise HTTPNotFound(Mock())
|
||||
|
||||
|
||||
@staticmethod
|
||||
def esi_get_corporations_corporation_id(corporation_id):
|
||||
corporations = {
|
||||
2001: {
|
||||
'name': 'Dummy Corp 1',
|
||||
'ticker': 'DC1',
|
||||
'ceo_id': 1001,
|
||||
'member_count': 42,
|
||||
'alliance_id': 3001
|
||||
},
|
||||
2002: {
|
||||
'name': 'Dummy Corp 2',
|
||||
'ticker': 'DC2',
|
||||
'ceo_id': 1011,
|
||||
'member_count': 5
|
||||
}
|
||||
}
|
||||
mock_result = Mock()
|
||||
if corporation_id in corporations:
|
||||
mock_result.result.return_value = corporations[corporation_id]
|
||||
return mock_result
|
||||
else:
|
||||
raise HTTPNotFound(Mock())
|
||||
|
||||
|
||||
@staticmethod
|
||||
def esi_get_characters_character_id(character_id):
|
||||
characters = {
|
||||
1001: {
|
||||
'name': 'Bruce Wayne',
|
||||
'corporation_id': 2001,
|
||||
'alliance_id': 3001
|
||||
},
|
||||
1002: {
|
||||
'name': 'Peter Parker',
|
||||
'corporation_id': 2101
|
||||
}
|
||||
}
|
||||
mock_result = Mock()
|
||||
if character_id in characters:
|
||||
mock_result.result.return_value = characters[character_id]
|
||||
return mock_result
|
||||
else:
|
||||
raise HTTPNotFound(Mock())
|
||||
|
||||
|
||||
@staticmethod
|
||||
def esi_post_characters_affiliation(characters):
|
||||
character_data = {
|
||||
1001: {
|
||||
'corporation_id': 2001,
|
||||
'alliance_id': 3001
|
||||
},
|
||||
1002: {
|
||||
'corporation_id': 2101
|
||||
}
|
||||
}
|
||||
mock_result = Mock()
|
||||
if isinstance(characters, list):
|
||||
characters_result = list()
|
||||
for character_id in characters:
|
||||
if character_id in character_data:
|
||||
characters_result.append(character_data[character_id])
|
||||
else:
|
||||
raise HTTPNotFound(Mock())
|
||||
mock_result.result.return_value = characters_result
|
||||
return mock_result
|
||||
else:
|
||||
raise TypeError()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def esi_get_universe_types_type_id(type_id):
|
||||
types = {
|
||||
4001: {
|
||||
'name': 'Dummy Type 1'
|
||||
},
|
||||
4002: {
|
||||
'name': 'Dummy Type 2'
|
||||
}
|
||||
}
|
||||
mock_result = Mock()
|
||||
if type_id in types:
|
||||
mock_result.result.return_value = types[type_id]
|
||||
return mock_result
|
||||
else:
|
||||
raise HTTPNotFound(Mock())
|
||||
|
||||
|
||||
@patch('allianceauth.eveonline.providers.esi_client_factory')
|
||||
def test_str(self, mock_esi_client_factory):
|
||||
my_provider = EveSwaggerProvider()
|
||||
self.assertEqual(str(my_provider), 'esi')
|
||||
|
||||
|
||||
@patch('allianceauth.eveonline.providers.esi_client_factory')
|
||||
def test_get_alliance(self, mock_esi_client_factory):
|
||||
mock_esi_client_factory.return_value\
|
||||
.Alliance.get_alliances_alliance_id \
|
||||
= TestEveSwaggerProvider.esi_get_alliances_alliance_id
|
||||
mock_esi_client_factory.return_value\
|
||||
.Alliance.get_alliances_alliance_id_corporations \
|
||||
= TestEveSwaggerProvider.esi_get_alliances_alliance_id_corporations
|
||||
|
||||
my_provider = EveSwaggerProvider()
|
||||
|
||||
# fully defined alliance
|
||||
my_alliance = my_provider.get_alliance(3001)
|
||||
self.assertEqual(my_alliance.id, 3001)
|
||||
self.assertEqual(my_alliance.name, 'Dummy Alliance 1')
|
||||
self.assertEqual(my_alliance.ticker, 'DA1')
|
||||
self.assertListEqual(my_alliance.corp_ids, [2001, 2002, 2003])
|
||||
self.assertEqual(my_alliance.executor_corp_id, 2001)
|
||||
|
||||
# alliance missing executor_corporation_id
|
||||
my_alliance = my_provider.get_alliance(3002)
|
||||
self.assertEqual(my_alliance.id, 3002)
|
||||
self.assertEqual(my_alliance.executor_corp_id, None)
|
||||
|
||||
# alliance not found
|
||||
with self.assertRaises(ObjectNotFound):
|
||||
my_provider.get_alliance(3999)
|
||||
|
||||
|
||||
@patch('allianceauth.eveonline.providers.esi_client_factory')
|
||||
def test_get_corp(self, mock_esi_client_factory):
|
||||
mock_esi_client_factory.return_value\
|
||||
.Corporation.get_corporations_corporation_id \
|
||||
= TestEveSwaggerProvider.esi_get_corporations_corporation_id
|
||||
|
||||
my_provider = EveSwaggerProvider()
|
||||
|
||||
# corporation with alliance
|
||||
my_corp = my_provider.get_corp(2001)
|
||||
self.assertEqual(my_corp.id, 2001)
|
||||
self.assertEqual(my_corp.name, 'Dummy Corp 1')
|
||||
self.assertEqual(my_corp.ticker, 'DC1')
|
||||
self.assertEqual(my_corp.ceo_id, 1001)
|
||||
self.assertEqual(my_corp.members, 42)
|
||||
self.assertEqual(my_corp.alliance_id, 3001)
|
||||
|
||||
# corporation wo/ alliance
|
||||
my_corp = my_provider.get_corp(2002)
|
||||
self.assertEqual(my_corp.id, 2002)
|
||||
self.assertEqual(my_corp.alliance_id, None)
|
||||
|
||||
# corporation not found
|
||||
with self.assertRaises(ObjectNotFound):
|
||||
my_provider.get_corp(2999)
|
||||
|
||||
|
||||
@patch('allianceauth.eveonline.providers.esi_client_factory')
|
||||
def test_get_character(self, mock_esi_client_factory):
|
||||
mock_esi_client_factory.return_value\
|
||||
.Character.get_characters_character_id \
|
||||
= TestEveSwaggerProvider.esi_get_characters_character_id
|
||||
mock_esi_client_factory.return_value\
|
||||
.Character.post_characters_affiliation \
|
||||
= TestEveSwaggerProvider.esi_post_characters_affiliation
|
||||
|
||||
my_provider = EveSwaggerProvider()
|
||||
|
||||
# character with alliance
|
||||
my_character = my_provider.get_character(1001)
|
||||
self.assertEqual(my_character.id, 1001)
|
||||
self.assertEqual(my_character.name, 'Bruce Wayne')
|
||||
self.assertEqual(my_character.corp_id, 2001)
|
||||
self.assertEqual(my_character.alliance_id, 3001)
|
||||
|
||||
# character wo/ alliance
|
||||
my_character = my_provider.get_character(1002)
|
||||
self.assertEqual(my_character.id, 1002)
|
||||
self.assertEqual(my_character.alliance_id, None)
|
||||
|
||||
# character not found
|
||||
with self.assertRaises(ObjectNotFound):
|
||||
my_provider.get_character(1999)
|
||||
|
||||
|
||||
@patch('allianceauth.eveonline.providers.esi_client_factory')
|
||||
def test_get_itemtype(self, mock_esi_client_factory):
|
||||
mock_esi_client_factory.return_value\
|
||||
.Universe.get_universe_types_type_id \
|
||||
= TestEveSwaggerProvider.esi_get_universe_types_type_id
|
||||
|
||||
my_provider = EveSwaggerProvider()
|
||||
|
||||
# type exists
|
||||
my_type = my_provider.get_itemtype(4001)
|
||||
self.assertEqual(my_type.id, 4001)
|
||||
self.assertEqual(my_type.name, 'Dummy Type 1')
|
||||
|
||||
# type not found
|
||||
with self.assertRaises(ObjectNotFound):
|
||||
my_provider.get_itemtype(4999)
|
||||
@@ -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:'])
|
||||
|
||||
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from allianceauth.optimer.models import OpTimer
|
||||
|
||||
|
||||
class FatlinkForm(forms.Form):
|
||||
fatname = forms.CharField(label=_('Name of fat-link'), required=True)
|
||||
fleet = forms.CharField(label=_("Fleet Name"), max_length=50)
|
||||
duration = forms.IntegerField(label=_("Duration of fat-link"), required=True, initial=30, min_value=1,
|
||||
max_value=2147483647)
|
||||
fleet = forms.ModelChoiceField(label=_("Fleet"), queryset=OpTimer.objects.all().order_by('operation_name'))
|
||||
max_value=2147483647, help_text=_('minutes'))
|
||||
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
# Generated by Django 1.10.1 on 2016-09-05 21:39
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
from django.utils.timezone import utc
|
||||
from django.utils import timezone
|
||||
|
||||
import allianceauth.fleetactivitytracking.models
|
||||
|
||||
@@ -36,9 +34,9 @@ class Migration(migrations.Migration):
|
||||
name='Fatlink',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('fatdatetime', models.DateTimeField(default=datetime.datetime(2016, 9, 5, 21, 39, 17, 307954, tzinfo=utc))),
|
||||
('fatdatetime', models.DateTimeField(default=timezone.now)),
|
||||
('duration', models.PositiveIntegerField()),
|
||||
('fleet', models.CharField(default=b'', max_length=254)),
|
||||
('fleet', models.CharField(default='', max_length=254)),
|
||||
('name', models.CharField(max_length=254)),
|
||||
('hash', models.CharField(max_length=254, unique=True)),
|
||||
('creator', models.ForeignKey(on_delete=models.SET(
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 2.0.2 on 2018-02-28 18:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fleetactivitytracking', '0004_make_strings_more_stringy'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='fatlink',
|
||||
name='name',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='fatlink',
|
||||
name='fleet',
|
||||
field=models.CharField(max_length=254),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.0.6 on 2018-08-03 04:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fleetactivitytracking', '0005_remove_fat_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='fat',
|
||||
name='shiptype',
|
||||
field=models.CharField(max_length=100),
|
||||
),
|
||||
]
|
||||
@@ -12,20 +12,19 @@ def get_sentinel_user():
|
||||
class Fatlink(models.Model):
|
||||
fatdatetime = models.DateTimeField(default=timezone.now)
|
||||
duration = models.PositiveIntegerField()
|
||||
fleet = models.CharField(max_length=254, default="")
|
||||
name = models.CharField(max_length=254)
|
||||
fleet = models.CharField(max_length=254)
|
||||
hash = models.CharField(max_length=254, unique=True)
|
||||
creator = models.ForeignKey(User, on_delete=models.SET(get_sentinel_user))
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
return self.fleet
|
||||
|
||||
|
||||
class Fat(models.Model):
|
||||
character = models.ForeignKey(EveCharacter, on_delete=models.CASCADE)
|
||||
fatlink = models.ForeignKey(Fatlink, on_delete=models.CASCADE)
|
||||
system = models.CharField(max_length=30)
|
||||
shiptype = models.CharField(max_length=30)
|
||||
shiptype = models.CharField(max_length=100)
|
||||
station = models.CharField(max_length=125)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -12,11 +12,11 @@
|
||||
<div class="panel-heading">{{ character_name }}</div>
|
||||
<div class="panel-body">
|
||||
<div class="col-lg-2 col-sm-2">
|
||||
<img class="ra-avatar img-responsive" src="https://image.eveonline.com/Character/{{ character_id }}_128.jpg">
|
||||
<img class="ra-avatar img-responsive" src="{{ character_portrait_url }}">
|
||||
</div>
|
||||
<div class="col-lg-10 col-sm-2">
|
||||
<div class="alert alert-danger" role="alert">{% trans "Character not registered!" %}</div>
|
||||
{% trans "This character is not part of any registered API-key. You must go to" %} <a href=" {% url 'auth_api_key_management' %}">{% trans "API key management</a> and add an API with the character on before being able to click fleet attendance links." %}
|
||||
{% trans "This character is not associated with an auth account." %} <a href=" {% url 'authentication:add_character' %}">{% trans "Add it here" %}</a> {% trans "before attempting to click fleet attendance links." %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
<h1 class="page-header text-center">{% blocktrans %}Participation data statistics for {{ month }}, {{ year }}{% endblocktrans %}
|
||||
{% if char_id %}
|
||||
<div class="text-right">
|
||||
<a href="{% url 'fatlink:user_statistics_month' char_id previous_month|date:"Y" previous_month|date:"m" %}" class="btn btn-info">{% trans "Previous month" %}</a>
|
||||
<a href="{% url 'fatlink:user_statistics_month' char_id next_month|date:"Y" next_month|date:"m" %}" class="btn btn-info">{% trans "Next month" %}</a>
|
||||
<a href="{% url 'fatlink:user_statistics_month' char_id previous_month|date:'Y' previous_month|date:'m' %}" class="btn btn-info">{% trans "Previous month" %}</a>
|
||||
<a href="{% url 'fatlink:user_statistics_month' char_id next_month|date:'Y' next_month|date:'m' %}" class="btn btn-info">{% trans "Next month" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h1>
|
||||
<h2>{% blocktrans %}{{ user }} has collected {{ n_fats }} links this month.{% endblocktrans %}</h2>
|
||||
<h2>{% blocktrans %}{{ user }} has collected {{ n_fats }} link{{ n_fats|pluralize }} this month.{% endblocktrans %}</h2>
|
||||
<table class="table table-responsive">
|
||||
<tr>
|
||||
<th class="col-md-2 text-center">{% trans "Ship" %}</th>
|
||||
@@ -29,26 +29,24 @@
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% if created_fats %}
|
||||
<h2>{% blocktrans %}{{ user }} has created {{ n_created_fats }} links this month.{% endblocktrans %}</h2>
|
||||
<h2>{% blocktrans %}{{ user }} has created {{ n_created_fats }} link{{ n_created_fats|pluralize }} this month.{% endblocktrans %}</h2>
|
||||
{% if created_fats %}
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th class="text-center">{% trans "Name" %}</th>
|
||||
<th class="text-center">{% trans "Creator" %}</th>
|
||||
<th class="text-center">{% trans "Fleet" %}</th>
|
||||
<th class="text-center">{% trans "Creator" %}</th>
|
||||
<th class="text-center">{% trans "Eve Time" %}</th>
|
||||
<th class="text-center">{% trans "Duration" %}</th>
|
||||
<th class="text-center">{% trans "Edit" %}</th>
|
||||
</tr>
|
||||
{% for link in created_fats %}
|
||||
<tr>
|
||||
<td class="text-center"><a href="{% url 'auth_click_fatlink_view' %}{{ link.hash }}/{{ link.name }}">{{ link.name }}</a></td>
|
||||
<td class="text-center"><a href="{% url 'fatlink:click' link.hash %}" class="label label-primary">{{ link.fleet }}</a></td>
|
||||
<td class="text-center">{{ link.creator.username }}</td>
|
||||
<td class="text-center">{{ link.fleet }}</td>
|
||||
<td class="text-center">{{ link.fatdatetime }}</td>
|
||||
<td class="text-center">{{ link.duration }}</td>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'auth_modify_fatlink_view' %}{{ link.hash }}/{{ link.name }}">
|
||||
<a href="{% url 'fatlink:modify' link.hash %}">
|
||||
<button type="button" class="btn btn-info"><span
|
||||
class="glyphicon glyphicon-edit"></span></button>
|
||||
</a>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
{% for memberStat in fatStats %}
|
||||
<tr>
|
||||
<td>
|
||||
<img src="https://image.eveonline.com/Character/{{ memberStat.mainchid }}_32.jpg" class="ra-avatar img-responsive">
|
||||
<img src="{{ memberStat.mainchar.portrait_url_32 }}" class="ra-avatar img-responsive">
|
||||
</td>
|
||||
<td class="text-center">{{ memberStat.mainchar.character_name }}</td>
|
||||
<td class="text-center">{{ memberStat.n_chars }}</td>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
{% for corpStat in fatStats %}
|
||||
<tr>
|
||||
<td>
|
||||
<img src="https://image.eveonline.com/Corporation/{{ corpStat.corp.corporation_id }}_32.png" class="ra-avatar img-responsive">
|
||||
<img src="{{ corpStat.corp.logo_url_32 }}" class="ra-avatar img-responsive">
|
||||
</td>
|
||||
<td class="text-center"><a href="{% url 'fatlink:statistics_corp' corpStat.corp.corporation_id %}">[{{ corpStat.corp.corporation_ticker }}]</td>
|
||||
<td class="text-center">{{ corpStat.corp.corporation_name }}</td>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
{% if fats %}
|
||||
<table class="table table-responsive">
|
||||
<tr>
|
||||
<th class="text-center">{% trans "fatname" %}</th>
|
||||
<th class="text-center">{% trans "Fleet" %}</th>
|
||||
<th class="text-center">{% trans "Character" %}</th>
|
||||
<th class="text-center">{% trans "System" %}</th>
|
||||
<th class="text-center">{% trans "Ship" %}</th>
|
||||
@@ -32,7 +32,7 @@
|
||||
</tr>
|
||||
{% for fat in fats %}
|
||||
<tr>
|
||||
<td class="text-center">{{ fat.fatlink.name }}</td>
|
||||
<td class="text-center">{{ fat.fatlink.fleet }}</td>
|
||||
<td class="text-center">{{ fat.character.character_name }}</td>
|
||||
{% if fat.station != "No Station" %}
|
||||
<td class="text-center">{% blocktrans %}Docked in {% endblocktrans %}{{ fat.system }}</td>
|
||||
@@ -79,13 +79,13 @@
|
||||
</tr>
|
||||
{% for link in fatlinks %}
|
||||
<tr>
|
||||
<td class="text-center"><a href="{% url 'fatlink:click_fatlink' %}{{ link.hash }}/{{ link.name }}">{{ link.name }}</a></td>
|
||||
<td class="text-center"><a href="{% url 'fatlink:click' link.hash %}" class="label label-primary">{{ link.fleet }}</a></td>
|
||||
<td class="text-center">{{ link.creator.username }}</td>
|
||||
<td class="text-center">{{ link.fleet }}</td>
|
||||
<td class="text-center">{{ link.fatdatetime }}</td>
|
||||
<td class="text-center">{{ link.duration }}</td>
|
||||
<td class="text-center">
|
||||
<a href="{% url 'fatlink:modify' %}{{ link.hash }}/{{ link.name }}" class="btn btn-info">
|
||||
<a href="{% url 'fatlink:modify' link.hash %}" class="btn btn-info">
|
||||
<span class="glyphicon glyphicon-edit"></span>
|
||||
</a>
|
||||
</td>
|
||||
|
||||
@@ -25,10 +25,6 @@ urlpatterns = [
|
||||
views.fatlink_monthly_personal_statistics_view,
|
||||
name='user_statistics_month'),
|
||||
url(r'^create/$', views.create_fatlink_view, name='create'),
|
||||
url(r'^modify/$', views.modify_fatlink_view, name='modify'),
|
||||
url(r'^modify/(?P<hash>[a-zA-Z0-9_-]+)/([a-z0-9_-]+)$',
|
||||
views.modify_fatlink_view),
|
||||
url(r'^link/$', views.fatlink_view, name='click_fatlink'),
|
||||
url(r'^link/(?P<hash>[a-zA-Z0-9]+)/(?P<fatname>[a-z0-9_-]+)/$',
|
||||
views.click_fatlink_view),
|
||||
url(r'^modify/(?P<fat_hash>[a-zA-Z0-9_-]+)/$', views.modify_fatlink_view, name='modify'),
|
||||
url(r'^link/(?P<fat_hash>[a-zA-Z0-9]+)/$', views.click_fatlink_view, name='click'),
|
||||
]
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
|
||||
from allianceauth.authentication.models import CharacterOwnership
|
||||
from django.contrib import messages
|
||||
@@ -17,13 +15,23 @@ from esi.decorators import token_required
|
||||
from allianceauth.eveonline.providers import provider
|
||||
from .forms import FatlinkForm
|
||||
from .models import Fatlink, Fat
|
||||
from slugify import slugify
|
||||
from django.utils.crypto import get_random_string
|
||||
|
||||
from allianceauth.eveonline.models import EveAllianceInfo
|
||||
from allianceauth.eveonline.models import 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__)
|
||||
|
||||
@@ -114,7 +122,7 @@ def fatlink_statistics_corp_view(request, corpid, year=None, month=None):
|
||||
start_of_next_month = first_day_of_next_month(year, month)
|
||||
start_of_previous_month = first_day_of_previous_month(year, month)
|
||||
fat_stats = {}
|
||||
corp_members = CharacterOwnership.objects.filter(character__corporation_id=corpid).values('user_id').distinct()
|
||||
corp_members = CharacterOwnership.objects.filter(character__corporation_id=corpid).order_by('user_id').values('user_id').distinct()
|
||||
|
||||
for member in corp_members:
|
||||
try:
|
||||
@@ -181,7 +189,7 @@ def fatlink_personal_statistics_view(request, year=datetime.date.today().year):
|
||||
|
||||
personal_fats = Fat.objects.select_related('fatlink').filter(user=user).order_by('id')
|
||||
|
||||
monthlystats = [0 for month in range(1, 13)]
|
||||
monthlystats = [0 for i in range(1, 13)]
|
||||
|
||||
for fat in personal_fats:
|
||||
fatdate = fat.fatlink.fatdatetime
|
||||
@@ -236,8 +244,8 @@ def fatlink_monthly_personal_statistics_view(request, year, month, char_id=None)
|
||||
@login_required
|
||||
@token_required(
|
||||
scopes=['esi-location.read_location.v1', 'esi-location.read_ship_type.v1', 'esi-universe.read_structures.v1'])
|
||||
def click_fatlink_view(request, token, hash, fatname):
|
||||
fatlink = get_object_or_404(Fatlink, hash=hash, name=fatname)
|
||||
def click_fatlink_view(request, token, fat_hash=None):
|
||||
fatlink = get_object_or_404(Fatlink, hash=fat_hash)
|
||||
|
||||
if (timezone.now() - fatlink.fatdatetime) < datetime.timedelta(seconds=(fatlink.duration * 60)):
|
||||
|
||||
@@ -279,8 +287,13 @@ def click_fatlink_view(request, token, hash, fatname):
|
||||
err_messages.append(message[0])
|
||||
messages.error(request, ' '.join(err_messages))
|
||||
else:
|
||||
context = {'character_id': token.character_id,
|
||||
'character_name': token.character_name}
|
||||
context = {
|
||||
'character_id': token.character_id,
|
||||
'character_name': token.character_name,
|
||||
'character_portrait_url': EveCharacter.generic_portrait_url(
|
||||
token.character_id, 128
|
||||
),
|
||||
}
|
||||
return render(request, 'fleetactivitytracking/characternotexisting.html', context=context)
|
||||
else:
|
||||
messages.error(request, _('FAT link has expired.'))
|
||||
@@ -298,12 +311,11 @@ def create_fatlink_view(request):
|
||||
logger.debug("Submitting fleetactivitytracking by user %s" % request.user)
|
||||
if form.is_valid():
|
||||
fatlink = Fatlink()
|
||||
fatlink.name = slugify(form.cleaned_data["fatname"])
|
||||
fatlink.fleet = form.cleaned_data["fleet"]
|
||||
fatlink.duration = form.cleaned_data["duration"]
|
||||
fatlink.fatdatetime = timezone.now()
|
||||
fatlink.creator = request.user
|
||||
fatlink.hash = ''.join(random.choice(string.ascii_letters + string.digits) for i in range(10))
|
||||
fatlink.hash = get_random_string(length=15)
|
||||
try:
|
||||
fatlink.full_clean()
|
||||
fatlink.save()
|
||||
@@ -331,25 +343,19 @@ def create_fatlink_view(request):
|
||||
|
||||
@login_required
|
||||
@permission_required('auth.fleetactivitytracking')
|
||||
def modify_fatlink_view(request, hash=""):
|
||||
def modify_fatlink_view(request, fat_hash=None):
|
||||
logger.debug("modify_fatlink_view called by user %s" % request.user)
|
||||
if not hash:
|
||||
return redirect('fatlink:view')
|
||||
|
||||
try:
|
||||
fatlink = Fatlink.objects.get(hash=hash)
|
||||
except Fatlink.DoesNotExist:
|
||||
raise Http404
|
||||
fatlink = get_object_or_404(Fatlink, hash=fat_hash)
|
||||
|
||||
if request.GET.get('removechar', None):
|
||||
character_id = request.GET.get('removechar')
|
||||
character = EveCharacter.objects.get(character_id=character_id)
|
||||
logger.debug("Removing character %s from fleetactivitytracking %s" % (character.character_name, fatlink.name))
|
||||
logger.debug("Removing character %s from fleetactivitytracking %s" % (character.character_name, fatlink))
|
||||
|
||||
Fat.objects.filter(fatlink=fatlink).filter(character=character).delete()
|
||||
|
||||
if request.GET.get('deletefat', None):
|
||||
logger.debug("Removing fleetactivitytracking %s" % fatlink.name)
|
||||
logger.debug("Removing fleetactivitytracking %s" % fatlink)
|
||||
fatlink.delete()
|
||||
return redirect('fatlink:view')
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
default_app_config = 'allianceauth.fleetup.apps.FleetupConfig'
|
||||
@@ -1,6 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class FleetupConfig(AppConfig):
|
||||
name = 'allianceauth.fleetup'
|
||||
label = 'fleetup'
|
||||
@@ -1,27 +0,0 @@
|
||||
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
||||
|
||||
from allianceauth import hooks
|
||||
from . import urls
|
||||
|
||||
|
||||
class FleetUpMenu(MenuItemHook):
|
||||
def __init__(self):
|
||||
MenuItemHook.__init__(self, 'Fleet-Up',
|
||||
'fa fa-arrow-up fa-fw',
|
||||
'fleetup:view',
|
||||
navactive=['fleetup:'])
|
||||
|
||||
def render(self, request):
|
||||
if request.user.has_perm('auth.view_fleetup'):
|
||||
return MenuItemHook.render(self, request)
|
||||
return ''
|
||||
|
||||
|
||||
@hooks.register('menu_item_hook')
|
||||
def register_menu():
|
||||
return FleetUpMenu()
|
||||
|
||||
|
||||
@hooks.register('url_hook')
|
||||
def register_url():
|
||||
return UrlHook(urls, 'fleetup', r'^fleetup/')
|
||||
@@ -1,189 +0,0 @@
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.utils import timezone
|
||||
from datetime import datetime
|
||||
|
||||
import logging
|
||||
import requests
|
||||
import hashlib
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FleetUpManager:
|
||||
APP_KEY = settings.FLEETUP_APP_KEY
|
||||
USER_ID = settings.FLEETUP_USER_ID
|
||||
API_ID = settings.FLEETUP_API_ID
|
||||
GROUP_ID = settings.FLEETUP_GROUP_ID
|
||||
BASE_URL = "http://api.fleet-up.com/Api.svc/{}/{}/{}".format(APP_KEY, USER_ID, API_ID)
|
||||
|
||||
TZ = timezone.utc
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def _request_cache_key(cls, url):
|
||||
h = hashlib.sha1()
|
||||
h.update(url.encode('utf-8'))
|
||||
return 'FLEETUP_ENDPOINT_' + h.hexdigest()
|
||||
|
||||
@classmethod
|
||||
def _cache_until_seconds(cls, cache_until_json):
|
||||
# Format comes in like "/Date(1493896236163)/"
|
||||
try:
|
||||
epoch_ms = int(cache_until_json[6:-2])
|
||||
cache_delta = datetime.fromtimestamp(epoch_ms/1000) - datetime.now()
|
||||
cache_delta_seconds = cache_delta.total_seconds()
|
||||
if cache_delta_seconds < 0:
|
||||
return 0
|
||||
elif cache_delta_seconds > 3600:
|
||||
return 3600
|
||||
else:
|
||||
return cache_delta_seconds
|
||||
except TypeError:
|
||||
logger.debug("Couldn't convert CachedUntil time, defaulting to 600 seconds")
|
||||
return 600
|
||||
|
||||
@classmethod
|
||||
def get_endpoint(cls, url):
|
||||
try:
|
||||
cache_key = cls._request_cache_key(url)
|
||||
cached = cache.get(cache_key)
|
||||
if cached:
|
||||
return cached
|
||||
|
||||
r = requests.get(url)
|
||||
r.raise_for_status()
|
||||
|
||||
json = r.json()
|
||||
|
||||
if json['Success']:
|
||||
cache.set(cache_key, json, cls._cache_until_seconds(json['CachedUntilUTC']))
|
||||
return json
|
||||
except requests.exceptions.ConnectionError:
|
||||
logger.warning("Can't connect to Fleet-Up API, is it offline?!")
|
||||
except requests.HTTPError:
|
||||
logger.exception("Error accessing Fleetup API")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_fleetup_members(cls):
|
||||
url = "{}/GroupCharacters/{}".format(cls.BASE_URL, cls.GROUP_ID)
|
||||
try:
|
||||
fmembers = cls.get_endpoint(url)
|
||||
if not fmembers:
|
||||
return None
|
||||
return {row["UserId"]: {"user_id": row["UserId"],
|
||||
"char_name": row["EveCharName"],
|
||||
"char_id": row["EveCharId"],
|
||||
"corporation": row["Corporation"]} for row in fmembers["Data"]}
|
||||
except (ValueError, UnicodeDecodeError, TypeError):
|
||||
logger.debug("No fleetup members retrieved.")
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def get_fleetup_operations(cls):
|
||||
url = "{}/Operations/{}".format(cls.BASE_URL, cls.GROUP_ID)
|
||||
foperations = cls.get_endpoint(url)
|
||||
if foperations is None:
|
||||
return None
|
||||
return {row["StartString"]: {"subject": row["Subject"],
|
||||
"start": timezone.make_aware(
|
||||
datetime.strptime(row["StartString"], "%Y-%m-%d %H:%M:%S"), cls.TZ),
|
||||
"end": timezone.make_aware(
|
||||
datetime.strptime(row["EndString"], "%Y-%m-%d %H:%M:%S"), cls.TZ),
|
||||
"operation_id": row["OperationId"],
|
||||
"location": row["Location"],
|
||||
"location_info": row["LocationInfo"],
|
||||
"details": row["Details"],
|
||||
"url": row["Url"],
|
||||
"doctrine": row["Doctrines"],
|
||||
"organizer": row["Organizer"]} for row in foperations["Data"]}
|
||||
|
||||
@classmethod
|
||||
def get_fleetup_timers(cls):
|
||||
url = "{}/Timers/{}".format(cls.BASE_URL, cls.GROUP_ID)
|
||||
ftimers = cls.get_endpoint(url)
|
||||
if not ftimers:
|
||||
return None
|
||||
return {row["ExpiresString"]: {"solarsystem": row["SolarSystem"],
|
||||
"planet": row["Planet"],
|
||||
"moon": row["Moon"],
|
||||
"owner": row["Owner"],
|
||||
"type": row["Type"],
|
||||
"timer_type": row["TimerType"],
|
||||
"expires": timezone.make_aware(
|
||||
datetime.strptime(row["ExpiresString"], "%Y-%m-%d %H:%M:%S"), cls.TZ),
|
||||
"notes": row["Notes"]} for row in ftimers["Data"]}
|
||||
|
||||
@classmethod
|
||||
def get_fleetup_doctrines(cls):
|
||||
url = "{}/Doctrines/{}".format(cls.BASE_URL, cls.GROUP_ID)
|
||||
fdoctrines = cls.get_endpoint(url)
|
||||
if not fdoctrines:
|
||||
return None
|
||||
return {"fleetup_doctrines": fdoctrines["Data"]}
|
||||
|
||||
@classmethod
|
||||
def get_fleetup_doctrine(cls, doctrinenumber):
|
||||
url = "{}/DoctrineFittings/{}".format(cls.BASE_URL, doctrinenumber)
|
||||
fdoctrine = cls.get_endpoint(url)
|
||||
if not fdoctrine:
|
||||
return None
|
||||
return {"fitting_doctrine": fdoctrine}
|
||||
|
||||
@classmethod
|
||||
def get_fleetup_fittings(cls):
|
||||
url = "{}/Fittings/{}".format(cls.BASE_URL, cls.GROUP_ID)
|
||||
ffittings = cls.get_endpoint(url)
|
||||
if not ffittings:
|
||||
return None
|
||||
return {row["FittingId"]: {"fitting_id": row["FittingId"],
|
||||
"name": row["Name"],
|
||||
"icon_id": row["EveTypeId"],
|
||||
"hull": row["HullType"],
|
||||
"shiptype": row["ShipType"],
|
||||
"estimated": row["EstPrice"],
|
||||
"faction": row["Faction"],
|
||||
"categories": row["Categories"],
|
||||
"last_update":
|
||||
timezone.make_aware(
|
||||
datetime.strptime(row["LastUpdatedString"], "%Y-%m-%d %H:%M:%S"), cls.TZ)}
|
||||
for row in ffittings["Data"]}
|
||||
|
||||
@classmethod
|
||||
def get_fleetup_fitting(cls, fittingnumber):
|
||||
url = "{}/Fitting/{}".format(cls.BASE_URL, fittingnumber)
|
||||
try:
|
||||
ffitting = cls.get_endpoint(url)
|
||||
if not ffitting:
|
||||
return None
|
||||
return {"fitting_data": ffitting["Data"]}
|
||||
except KeyError:
|
||||
logger.warning("Failed to retrieve fleetup fitting number %s" % fittingnumber)
|
||||
return {"fitting_data": {}}
|
||||
|
||||
@classmethod
|
||||
def get_fleetup_doctrineid(cls, fittingnumber):
|
||||
url = "{}/Fitting/{}".format(cls.BASE_URL, fittingnumber)
|
||||
try:
|
||||
fdoctrineid = cls.get_endpoint(url)
|
||||
if not fdoctrineid:
|
||||
return None
|
||||
return fdoctrineid['Data']['Doctrines'][0]['DoctrineId']
|
||||
except (KeyError, IndexError):
|
||||
logger.debug("Fleetup fitting number %s not in a doctrine." % fittingnumber)
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def get_fleetup_fitting_eft(cls, fittingnumber):
|
||||
url = "{}/Fitting/{}/eft".format(cls.BASE_URL, fittingnumber)
|
||||
try:
|
||||
ffittingeft = cls.get_endpoint(url)
|
||||
if not ffittingeft:
|
||||
return None
|
||||
return {"fitting_eft": ffittingeft["Data"]["FittingData"]}
|
||||
except KeyError:
|
||||
logger.warning("Fleetup fitting eft not found for fitting number %s" % fittingnumber)
|
||||
return {"fitting_eft": {}}
|
||||
@@ -1,48 +0,0 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% load bootstrap %}
|
||||
{% load staticfiles %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}Characters - FleetUp{% endblock page_title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
{% if perms.auth.corp_stats %}
|
||||
{% include "fleetup/menu.html" %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Characters registered on Fleet-Up.com" %}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="col-lg-6">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-hover table-striped">
|
||||
<tr>
|
||||
<th class="col-md-1"></th>
|
||||
<th class="col-md-1">{% trans "Character" %}</th>
|
||||
<th class="col-md-1">{% trans "Corporation" %}</th>
|
||||
<th class="col-md-1">Fleet-Up(id)</th>
|
||||
</tr>
|
||||
{% for char_name, user_id in member_list %}
|
||||
<tr>
|
||||
<td>
|
||||
<img src="http://image.eveonline.com/Character/{{ user_id.char_id }}_32.jpg" class="img-circle">
|
||||
</td>
|
||||
<td>
|
||||
<p>{{ user_id.char_name }}</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>{{ user_id.corporation }}</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>{{ user_id.user_id }}</p>
|
||||
</td>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -1,67 +0,0 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% load bootstrap %}
|
||||
{% load staticfiles %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}Doctrine - FleetUp{% endblock page_title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
{% include "fleetup/menu.html" %}
|
||||
<div class="panel">
|
||||
{% for a, j in doctrine.items %}
|
||||
{% regroup j.Data|dictsort:"Role" by Role as role_list %}
|
||||
|
||||
{% for Role in role_list %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><b>{{ Role.grouper }}</b></h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table table-condensed table-hover table-striped">
|
||||
<tr>
|
||||
<th class="col-md-1"></th>
|
||||
<th class="col-md-1">{% trans "Name" %}</th>
|
||||
<th class="col-md-1">{% trans "Role" %}</th>
|
||||
<th class="col-md-1">{% trans "Hull type" %}</th>
|
||||
<th class="col-md-1">{% trans "Ship type" %}</th>
|
||||
<th class="col-md-1">{% trans "Estimated ISK" %}</th>
|
||||
<th class="col-md-2">{% trans "Categories" %}</th>
|
||||
</tr>
|
||||
{% for item in Role.list %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'fleetup:fitting' item.FittingId %}"><img src="https://image.eveonline.com/InventoryType/{{ item.EveTypeId }}_32.png"></a>
|
||||
</td>
|
||||
<td>
|
||||
{{ item.Name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ item.Role }}
|
||||
</td>
|
||||
<td>
|
||||
{{ item.HullType }}
|
||||
</td>
|
||||
<td>
|
||||
{{ item.ShipType }}
|
||||
</td>
|
||||
<td>
|
||||
{% load humanize %}{{ item.EstPrice|intword }}
|
||||
</td>
|
||||
<td>
|
||||
{% for categories in item.Categories %}
|
||||
{{ categories }},
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
@@ -1,62 +0,0 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% load bootstrap %}
|
||||
{% load staticfiles %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}Doctrines - FleetUp{% endblock page_title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
{% include "fleetup/menu.html" %}
|
||||
<div class="panel">
|
||||
{% if doctrines_list %}
|
||||
{% for a, j in doctrines_list.items %}
|
||||
{% regroup j|dictsort:"FolderName" by FolderName as folder_list %}
|
||||
{% for FolderName in folder_list %}
|
||||
<div class="col-lg-8">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><b>{{ FolderName.grouper }}</b></h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table table-condensed table-hover table-striped">
|
||||
<tr>
|
||||
<th class="col-lg-1"></th>
|
||||
<th class="col-lg-4">{% trans "Name" %}</th>
|
||||
<th class="col-lg-3">{% trans "Doctrine" %}</th>
|
||||
<th class="col-lg-4">{% trans "Last updated" %}</th>
|
||||
<!--<th class="col-lg-1">Owner</th>
|
||||
<th class="col-lg-2">Note</th>-->
|
||||
</tr>
|
||||
{% for item in FolderName.list %}
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'fleetup:doctrine' item.DoctrineId %}"><img src="https://image.eveonline.com/InventoryType/{{ item.IconId }}_32.png"></a>
|
||||
</td>
|
||||
<td>
|
||||
{{ item.Name }}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'fleetup:doctrine' item.DoctrineId %}" class="btn btn-info btn-sm">{{ item.FolderName }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ item.LastUpdatedString }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% else %}
|
||||
<h3>{% trans "There seems to be no Doctrines in here at the moment!" %}</h3>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
@@ -1,129 +0,0 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% load bootstrap %}
|
||||
{% load staticfiles %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}{% trans "Doctrine - FleetUp" %}{% endblock page_title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
{% include "fleetup/menu.html" %}
|
||||
<div class="tab-content">
|
||||
<div id="fit" class="tab-pane fade in active">
|
||||
<div class="col-lg-3">
|
||||
{% for x, y in fitting_data.items %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "This fit is part of a doctrine" %}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% for doctrin in y.Doctrines %}
|
||||
<h4>{{ doctrin.Name }}</h4>
|
||||
<div class="col-lg-12">
|
||||
<p>{% trans "Role in doctrine:" %} {{ doctrin.Role }}</p>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<p>{% trans "Priority:" %}</p>
|
||||
</div>
|
||||
<div class="col-lg-8">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-striped" role="progressbar" aria-valuenow="{{ doctrin.Priority }}" aria-valuemin="0" aria-valuemax="5" style="width: {% widthratio doctrin.Priority 5 100 %}%;">
|
||||
{{ doctrin.Priority }}/5
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<a class="btn btn-primary" href="{% url 'fleetup:doctrine' doctrin.DoctrineId %}">{% trans "See doctrine" %}</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Fit categories" %}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% for category in y.Categories %}
|
||||
<span class="label label-success">{{ category }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "All fits in this Doctrine" %}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="list-group">
|
||||
{% for arbit, orbit in doctrines_list.items %}
|
||||
|
||||
{% for fitting in orbit.Data %}
|
||||
<a href="{% url 'fleetup:fitting' fitting.FittingId %}" class="list-group-item">
|
||||
|
||||
<h4 class="list-group-item-heading">{{ fitting.Name }}<span class="pull-right"><img src="https://image.eveonline.com/InventoryType/{{ fitting.EveTypeId }}_32.png" class="img-circle"></span></h4>
|
||||
<p class="list-group-item-heading">{{ fitting.Role }} - {{ fitting.ShipType }}</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{% for a, j in fitting_data.items %}
|
||||
<h3 class="panel-title">{{ j.Name }}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="col-lg-3">
|
||||
<img src="https://image.eveonline.com/InventoryType/{{ j.EveTypeId }}_64.png" class="img-responsive">
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<p>{% trans "Hull:" %} <b>{{ j.HullType }}</b></p>
|
||||
<p>{% trans "Ship:" %} <b>{{ j.ShipType }}</b></p>
|
||||
{% load humanize %}
|
||||
<p>{% trans "Estimated price:" %} <b>{{ j.EstPrice|intword }} ISK</b></p>
|
||||
</div>
|
||||
{% regroup j.FittingData by Slot as fitting_list %}
|
||||
<table class="table table-condensed table-hover">
|
||||
<tr>
|
||||
<th class="col-lg-1"></th>
|
||||
<th class="col-lg-11"></th>
|
||||
</tr>
|
||||
{% for Slot in fitting_list %}
|
||||
<tr class="info">
|
||||
<td></td><td><b>{{ Slot.grouper }}</b></td>
|
||||
</tr>
|
||||
{% for item in Slot.list %}
|
||||
<tr>
|
||||
<td><img src="https://image.eveonline.com/InventoryType/{{ item.TypeId }}_32.png" class="img-responsive"></td>
|
||||
<td> {{ item.Quantity }}x {{ item.TypeName }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "EFT/Export" %}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% for data in fitting_eft.items %}
|
||||
{% autoescape off %}
|
||||
<textarea class="form-control" rows="25" spellcheck="false" onclick="this.focus();this.select()" readonly>{{ fitting_eft.fitting_eft }}</textarea>
|
||||
{% endautoescape %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
@@ -1,55 +0,0 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% load bootstrap %}
|
||||
{% load staticfiles %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}Fittings - FleetUp{% endblock page_title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
{% include "fleetup/menu.html" %}
|
||||
<div class="panel">
|
||||
{% if fitting_list %}
|
||||
<table class="table table-condensed table-hover table-striped">
|
||||
<tr>
|
||||
<th class="col-md-1"></th>
|
||||
<th class="col-md-1">{% trans "Name" %}</th>
|
||||
<th class="col-md-1">{% trans "Hull" %}</th>
|
||||
<th class="col-md-1">{% trans "Ship type" %}</th>
|
||||
<th class="col-md-1">{% trans "Estimated ISK" %}</th>
|
||||
<th class="col-md-2">{% trans "Categories" %}</th>
|
||||
</tr>
|
||||
{% for id, fittings in fitting_list %}
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'fleetup:fitting' fittings.fitting_id %}"><img src="https://image.eveonline.com/InventoryType/{{ fittings.icon_id }}_32.png"></a>
|
||||
</td>
|
||||
<td>
|
||||
{{ fittings.name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ fittings.hull }}
|
||||
</td>
|
||||
<td>
|
||||
{{ fittings.shiptype }}
|
||||
</td>
|
||||
<td>
|
||||
{% load humanize %}{{ fittings.estimated|intword }}
|
||||
</td>
|
||||
<td>
|
||||
{% for categories in fittings.categories %}
|
||||
{{ categories }},
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<h3>{% trans "There seems to be no Fittings in here at the moment!" %}</h3>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
@@ -1,256 +0,0 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% load bootstrap %}
|
||||
{% load staticfiles %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}FleetUp{% endblock page_title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
{% include "fleetup/menu.html" %}
|
||||
<div class="panel">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active"><a data-toggle="tab" href="#operations">{% trans "Operations" %}</a></li>
|
||||
<li><a data-toggle="tab" href="#timers">{% trans "Timers" %}</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div id="operations" class="tab-pane fade in active">
|
||||
<div class="col-lg-7">
|
||||
{% if operations_list %}
|
||||
{% for subject, start in operations_list %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><b>{{ start.subject }}</b></h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table table-condensed">
|
||||
<tr>
|
||||
<th class="col-md-6">{% trans "Start" %}</th>
|
||||
<th class="col-md-6">{% trans "End" %}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="col-md-6">{{ start.start|date:"l d M H:i" }} <span class="label label-success">{% trans "Eve Time" %}</span></td>
|
||||
|
||||
<td class="col-md-6">{{ start.end|date:"l d M H:i" }} <span class="label label-success">{% trans "Eve Time" %}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="col-md-6">
|
||||
<span id="localtime{{ start.operation_id }}"></span> <span class='label label-success'>Local time</span><br>
|
||||
<div id="countdown{{ start.operation_id }}"></div>
|
||||
</td>
|
||||
|
||||
<td class="col-md-6"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<p>{{ start.details }}</p>
|
||||
|
||||
<div class="col-lg-12">
|
||||
<table class="table table-condensed table-striped">
|
||||
<tr>
|
||||
<th class="col-md-4">{% trans "Location" %}</th>
|
||||
<th class="col-md-4">{% trans "Doctrine" %}</th>
|
||||
<th class="col-md-2">{% trans "Organizer" %}</th>
|
||||
<th class="col-md-2">{% trans "URL" %}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{{ start.location }} - {{ start.location_info }} <a href="http://evemaps.dotlan.net/system/{{ start.location }}" target="_blank" class="label label-success">Dotlan</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if start.doctrine %}
|
||||
{% for doctrine in start.doctrine %}
|
||||
|
||||
<a href="{% url 'fleetup:doctrine' doctrine.Id %}" class="label label-success">{{ doctrine.Name }}</a>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
{% else %}
|
||||
<span class="label label-danger">{% trans "TBA" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ start.organizer }}
|
||||
</td>
|
||||
<td>
|
||||
{% ifequal start.url "" %}
|
||||
<div class="label label-danger">{% trans "No link" %}</div>
|
||||
{% else %}
|
||||
<a href="{{ start.url }}" target="_blank" class="label label-success">{% trans "External link" %}</a>
|
||||
{% endifequal %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<h3>{% trans "There seems to be no Operations in the near future." %}</h3>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h2 class="panel-title">{% trans "Current Eve Time:" %}</h2>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div id="current-time"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% if timers_list %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h2 class="panel-title">{% trans "Timers" %}</h2>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table table-condensed table-hover table-striped">
|
||||
{% for notes, type in timers_list %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ type.solarsystem }}
|
||||
</td>
|
||||
<td>
|
||||
{{ type.expires|date:"l d M H:i" }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div id="timers" class="tab-pane fade in">
|
||||
<div class="col-lg-12">
|
||||
{% if timers_list %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h2 class="panel-title">{% trans "Timers" %}</h2>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="col-lg-12">
|
||||
<table class="table table-condensed table-hover table-striped">
|
||||
<tr>
|
||||
<th class="col-lg-1">{% trans "Type" %}</th>
|
||||
<th class="col-lg-1">{% trans "Structure" %}</th>
|
||||
<th class="col-lg-2">{% trans "Location" %}</th>
|
||||
<th class="col-lg-2">{% trans "Expires(EVE-time)" %}</th>
|
||||
<th class="col-lg-1">{% trans "Owner" %}</th>
|
||||
<th class="col-lg-2">{% trans "Note" %}</th>
|
||||
</tr>
|
||||
{% for notes, type in timers_list %}
|
||||
<tr>
|
||||
<td>
|
||||
{% ifequal type.type "Final" %}
|
||||
<span class="label label-danger">
|
||||
{{ type.type }}</span>{% else %}{{ type.type }}{% endifequal %}
|
||||
</td>
|
||||
<td>
|
||||
{{ type.timer_type }}
|
||||
</td>
|
||||
<td>
|
||||
{{ type.solarsystem }} - Planet:{{ type.planet }} Moon:{{ type.moon }}
|
||||
</td>
|
||||
<td>
|
||||
{{ type.expires|date:"l d M H:i" }}
|
||||
</td>
|
||||
<td>
|
||||
{{ type.owner }}
|
||||
</td>
|
||||
<td>
|
||||
{{ type.notes }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<h3>{% trans "There seems to be no Timers in the near future." %}</h3>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% include 'bundles/moment-js.html' with locale=True %}
|
||||
<script src="{% static 'js/timers.js' %}"></script>
|
||||
<script type="text/javascript">
|
||||
// Data
|
||||
var timers = [
|
||||
{% for start, op in operations_list %}
|
||||
{
|
||||
'id': {{ op.operation_id }},
|
||||
'start': moment("{{ op.start | date:"c" }}"),
|
||||
'end': moment("{{ op.end | date:"c" }}"),
|
||||
'expired': false
|
||||
},
|
||||
{% endfor %}
|
||||
]
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
|
||||
timedUpdate();
|
||||
setAllLocalTimes();
|
||||
|
||||
// Start timed updates
|
||||
setInterval(timedUpdate, 1000);
|
||||
|
||||
function timedUpdate() {
|
||||
updateClock();
|
||||
updateAllTimers();
|
||||
}
|
||||
|
||||
function updateAllTimers () {
|
||||
var l = timers.length;
|
||||
for (var i=0; i < l; ++i) {
|
||||
if (timers[i].expired) continue;
|
||||
updateTimer(timers[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a timer
|
||||
* @param timer Timer information
|
||||
* @param timer.start Date of the timer
|
||||
* @param timer.id Id number of the timer
|
||||
* @param timer.expired
|
||||
*/
|
||||
function updateTimer(timer) {
|
||||
if (timer.start.isAfter(Date.now())) {
|
||||
var duration = moment.duration(timer.start - moment(), 'milliseconds');
|
||||
document.getElementById("countdown" + timer.id).innerHTML = getDurationString(duration);
|
||||
} else {
|
||||
timer.expired = true;
|
||||
document.getElementById("countdown" + timer.id).innerHTML = "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set all local time fields
|
||||
*/
|
||||
function setAllLocalTimes() {
|
||||
var l = timers.length;
|
||||
for (var i=0; i < l; ++i) {
|
||||
setLocalTime(timers[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the local time info for the timer
|
||||
* @param timer Timer information
|
||||
* @param timer.start Date of the timer
|
||||
* @param timer.id Id number of the timer
|
||||
*/
|
||||
function setLocalTime(timer) {
|
||||
document.getElementById("localtime" + timer.id).innerHTML = timer.start.format("ddd @ LT");
|
||||
}
|
||||
|
||||
function updateClock() {
|
||||
document.getElementById("current-time").innerHTML = "<b>" + moment.utc().format('ddd, ll HH:mm:ss z') + "</b>";
|
||||
}
|
||||
</script>
|
||||
{% endblock content %}
|
||||
@@ -1,26 +0,0 @@
|
||||
{% load i18n %}
|
||||
{% load navactive %}
|
||||
|
||||
<nav class="navbar navbar-default">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
|
||||
<span class="sr-only">{% trans "Toggle navigation" %}</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="#">Fleet-Up</a>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="{% navactive request 'fleetup:view' %}"><a href="{% url 'fleetup:view' %}">{% trans "Ops and Timers" %}</a></li>
|
||||
<li class="{% navactive request 'fleetup:doctrines fleetup:doctrine' %}"><a href="{% url 'fleetup:doctrines' %}">{% trans "Doctrines" %}</a></li>
|
||||
<li class="{% navactive request 'fleetup:fittings fleetup:fitting' %}"><a href="{% url 'fleetup:fittings' %}">{% trans "Fittings" %}</a></li>
|
||||
{% if perms.auth.corp_stats %}
|
||||
<li class="{% navactive request 'fleetup:characters' %}"><a href="{% url 'fleetup:characters' %}">{% trans "Characters" %}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -1,503 +0,0 @@
|
||||
from unittest import mock
|
||||
|
||||
import requests_mock
|
||||
import json
|
||||
import datetime
|
||||
|
||||
from django.test import TestCase
|
||||
from django.utils.timezone import make_aware, utc
|
||||
|
||||
from allianceauth.fleetup.managers import FleetUpManager
|
||||
|
||||
|
||||
class FleetupManagerTestCase(TestCase):
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def test__request_cache_key(self):
|
||||
|
||||
cache_key = FleetUpManager._request_cache_key('testurl')
|
||||
|
||||
self.assertEqual('FLEETUP_ENDPOINT_a39562b6ef5b858220be13d2adb61d3f10cf8d61',
|
||||
cache_key)
|
||||
|
||||
@mock.patch('allianceauth.fleetup.managers.cache')
|
||||
@requests_mock.Mocker()
|
||||
def test_get_endpoint(self, cache, m):
|
||||
url = "http://example.com/test/endpoint/"
|
||||
json_data = {'data': "123456", 'CachedUntilUTC': '/Date(1493896236163)/', 'Success': True}
|
||||
m.register_uri('GET', url,
|
||||
text=json.dumps(json_data))
|
||||
|
||||
cache.get.return_value = None # No cached value
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_endpoint(url)
|
||||
|
||||
# Assert
|
||||
self.assertTrue(cache.get.called)
|
||||
self.assertTrue(cache.set.called)
|
||||
args, kwargs = cache.set.call_args
|
||||
self.assertDictEqual(json_data, args[1])
|
||||
|
||||
self.assertDictEqual(json_data, result)
|
||||
|
||||
@mock.patch('allianceauth.fleetup.managers.cache')
|
||||
@requests_mock.Mocker()
|
||||
def test_get_endpoint_error(self, cache, m):
|
||||
url = "http://example.com/test/endpoint/"
|
||||
json_data = {'data': [], 'Success': False}
|
||||
m.register_uri('GET', url,
|
||||
text=json.dumps(json_data),
|
||||
status_code=400)
|
||||
|
||||
cache.get.return_value = None # No cached value
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_endpoint(url)
|
||||
|
||||
# Assert
|
||||
self.assertTrue(cache.get.called)
|
||||
self.assertFalse(cache.set.called)
|
||||
self.assertIsNone(result)
|
||||
|
||||
@mock.patch('allianceauth.fleetup.managers.FleetUpManager.get_endpoint')
|
||||
def test_get_fleetup_members(self, get_endpoint):
|
||||
|
||||
get_endpoint.return_value = {"Data": [
|
||||
{
|
||||
'UserId': 1234,
|
||||
'EveCharName': 'test_name',
|
||||
'EveCharId': 5678,
|
||||
'Corporation': 'test_corporation',
|
||||
}
|
||||
]}
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_members()
|
||||
|
||||
# Asset
|
||||
self.assertTrue(get_endpoint.called)
|
||||
args, kwargs = get_endpoint.call_args
|
||||
self.assertEqual(args[0],
|
||||
FleetUpManager.BASE_URL + '/GroupCharacters/' +
|
||||
FleetUpManager.GROUP_ID)
|
||||
expected_result = {
|
||||
1234: {
|
||||
'user_id': 1234,
|
||||
'char_name': 'test_name',
|
||||
'char_id': 5678,
|
||||
'corporation': 'test_corporation',
|
||||
}
|
||||
}
|
||||
self.assertDictEqual(expected_result, result)
|
||||
|
||||
# Test None response
|
||||
# Arrange
|
||||
get_endpoint.return_value = None
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_members()
|
||||
|
||||
# Assert
|
||||
self.assertIsNone(result)
|
||||
|
||||
# Test Empty response
|
||||
# Arrange
|
||||
get_endpoint.return_value = {'Data': []}
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_members()
|
||||
|
||||
# Assert
|
||||
self.assertDictEqual({}, result)
|
||||
|
||||
@mock.patch('allianceauth.fleetup.managers.FleetUpManager.get_endpoint')
|
||||
def test_get_fleetup_operations(self, get_endpoint):
|
||||
|
||||
get_endpoint.return_value = {"Data": [
|
||||
{
|
||||
'Subject': 'test_operation',
|
||||
'StartString': '2017-05-06 11:11:11',
|
||||
'EndString': '2017-05-06 12:12:12',
|
||||
'OperationId': 1234,
|
||||
'Location': 'Jita',
|
||||
'LocationInfo': '4-4',
|
||||
'Details': 'This is a test operation',
|
||||
'Url': 'http://example.com/1234',
|
||||
'Doctrines': 'Foxcats',
|
||||
'Organizer': 'Example FC'
|
||||
}
|
||||
]}
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_operations()
|
||||
self.maxDiff = None
|
||||
# Asset
|
||||
self.assertTrue(get_endpoint.called)
|
||||
args, kwargs = get_endpoint.call_args
|
||||
self.assertEqual(args[0],
|
||||
FleetUpManager.BASE_URL + '/Operations/' +
|
||||
FleetUpManager.GROUP_ID)
|
||||
expected_result = {
|
||||
'2017-05-06 11:11:11': {
|
||||
'subject': 'test_operation',
|
||||
'start': make_aware(datetime.datetime(2017, 5, 6, 11, 11, 11), utc),
|
||||
'end': make_aware(datetime.datetime(2017, 5, 6, 12, 12, 12), utc),
|
||||
'operation_id': 1234,
|
||||
'location': 'Jita',
|
||||
'location_info': '4-4',
|
||||
'details': 'This is a test operation',
|
||||
'url': 'http://example.com/1234',
|
||||
'doctrine': 'Foxcats',
|
||||
'organizer': 'Example FC'
|
||||
}
|
||||
}
|
||||
self.assertDictEqual(expected_result, result)
|
||||
|
||||
# Test None response
|
||||
# Arrange
|
||||
get_endpoint.return_value = None
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_operations()
|
||||
|
||||
# Assert
|
||||
self.assertIsNone(result)
|
||||
|
||||
# Test Empty response
|
||||
# Arrange
|
||||
get_endpoint.return_value = {'Data': []}
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_operations()
|
||||
|
||||
# Assert
|
||||
self.assertDictEqual({}, result)
|
||||
|
||||
@mock.patch('allianceauth.fleetup.managers.FleetUpManager.get_endpoint')
|
||||
def test_get_fleetup_timers(self, get_endpoint):
|
||||
|
||||
get_endpoint.return_value = {"Data": [
|
||||
{
|
||||
'ExpiresString': '2017-05-06 11:11:11',
|
||||
'SolarSystem': 'Jita',
|
||||
'Planet': '4',
|
||||
'Moon': '4',
|
||||
'Owner': 'Caldari Navy',
|
||||
'Type': 'Caldari Station',
|
||||
'TimerType': 'Armor',
|
||||
'Notes': 'Burn Jita?'
|
||||
}
|
||||
]}
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_timers()
|
||||
|
||||
# Asset
|
||||
self.assertTrue(get_endpoint.called)
|
||||
args, kwargs = get_endpoint.call_args
|
||||
self.assertEqual(args[0],
|
||||
FleetUpManager.BASE_URL + '/Timers/' +
|
||||
FleetUpManager.GROUP_ID)
|
||||
expected_result = {
|
||||
'2017-05-06 11:11:11': {
|
||||
'expires': make_aware(datetime.datetime(2017, 5, 6, 11, 11, 11), utc),
|
||||
'solarsystem': 'Jita',
|
||||
'planet': '4',
|
||||
'moon': '4',
|
||||
'owner': 'Caldari Navy',
|
||||
'type': 'Caldari Station',
|
||||
'timer_type': 'Armor',
|
||||
'notes': 'Burn Jita?'
|
||||
}
|
||||
}
|
||||
self.assertDictEqual(expected_result, result)
|
||||
|
||||
# Test None response
|
||||
# Arrange
|
||||
get_endpoint.return_value = None
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_timers()
|
||||
|
||||
# Assert
|
||||
self.assertIsNone(result)
|
||||
|
||||
# Test Empty response
|
||||
# Arrange
|
||||
get_endpoint.return_value = {'Data': []}
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_timers()
|
||||
|
||||
# Assert
|
||||
self.assertDictEqual({}, result)
|
||||
|
||||
@mock.patch('allianceauth.fleetup.managers.FleetUpManager.get_endpoint')
|
||||
def test_get_fleetup_doctrines(self, get_endpoint):
|
||||
|
||||
get_endpoint.return_value = {"Data": [
|
||||
{
|
||||
'TestData': True
|
||||
}
|
||||
]}
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_doctrines()
|
||||
|
||||
# Asset
|
||||
self.assertTrue(get_endpoint.called)
|
||||
args, kwargs = get_endpoint.call_args
|
||||
self.assertEqual(args[0],
|
||||
FleetUpManager.BASE_URL + '/Doctrines/' +
|
||||
FleetUpManager.GROUP_ID)
|
||||
expected_result = {
|
||||
'fleetup_doctrines': [{
|
||||
'TestData': True
|
||||
}]
|
||||
}
|
||||
self.assertDictEqual(expected_result, result)
|
||||
|
||||
# Test None response
|
||||
# Arrange
|
||||
get_endpoint.return_value = None
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_doctrines()
|
||||
|
||||
# Assert
|
||||
self.assertIsNone(result)
|
||||
|
||||
# Test Empty response
|
||||
# Arrange
|
||||
get_endpoint.return_value = {'Data': []}
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_doctrines()
|
||||
|
||||
# Assert
|
||||
self.assertDictEqual({"fleetup_doctrines": []}, result)
|
||||
|
||||
@mock.patch('allianceauth.fleetup.managers.FleetUpManager.get_endpoint')
|
||||
def test_get_fleetup_doctrine(self, get_endpoint):
|
||||
|
||||
get_endpoint.return_value = {"Data": [
|
||||
{
|
||||
'TestData': True
|
||||
}
|
||||
]}
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_doctrine(1234)
|
||||
|
||||
# Asset
|
||||
self.assertTrue(get_endpoint.called)
|
||||
args, kwargs = get_endpoint.call_args
|
||||
self.assertEqual(args[0],
|
||||
FleetUpManager.BASE_URL + '/DoctrineFittings/1234')
|
||||
expected_result = {
|
||||
'fitting_doctrine': {'Data': [{
|
||||
'TestData': True
|
||||
}]}
|
||||
}
|
||||
self.assertDictEqual(expected_result, result)
|
||||
|
||||
# Test None response
|
||||
# Arrange
|
||||
get_endpoint.return_value = None
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_doctrine(1234)
|
||||
|
||||
# Assert
|
||||
self.assertIsNone(result)
|
||||
|
||||
# Test Empty response
|
||||
# Arrange
|
||||
get_endpoint.return_value = {'Data': []}
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_doctrine(1234)
|
||||
|
||||
# Assert
|
||||
self.assertDictEqual({"fitting_doctrine": {'Data': []}}, result)
|
||||
|
||||
@mock.patch('allianceauth.fleetup.managers.FleetUpManager.get_endpoint')
|
||||
def test_get_fleetup_fittings(self, get_endpoint):
|
||||
|
||||
get_endpoint.return_value = {"Data": [
|
||||
{
|
||||
'FittingId': 1234,
|
||||
'Name': 'Foxcat',
|
||||
'EveTypeId': 17726,
|
||||
'HullType': 'Battleship',
|
||||
'ShipType': 'Apocalypse Navy Issue',
|
||||
'EstPrice': 500000000,
|
||||
'Faction': 'Amarr',
|
||||
'Categories': ["Armor", "Laser"],
|
||||
'LastUpdatedString': '2017-05-06 11:11:11',
|
||||
}
|
||||
]}
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_fittings()
|
||||
|
||||
# Asset
|
||||
self.assertTrue(get_endpoint.called)
|
||||
expected_result = {
|
||||
1234: {
|
||||
'fitting_id': 1234,
|
||||
'name': 'Foxcat',
|
||||
'icon_id': 17726,
|
||||
'hull': 'Battleship',
|
||||
'shiptype': 'Apocalypse Navy Issue',
|
||||
'estimated': 500000000,
|
||||
'faction': 'Amarr',
|
||||
'categories': ["Armor", "Laser"],
|
||||
'last_update': make_aware(datetime.datetime(2017, 5, 6, 11, 11, 11), utc)
|
||||
}
|
||||
}
|
||||
self.assertDictEqual(expected_result, result)
|
||||
|
||||
# Test None response
|
||||
# Arrange
|
||||
get_endpoint.return_value = None
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_fittings()
|
||||
|
||||
# Assert
|
||||
self.assertIsNone(result)
|
||||
|
||||
# Test Empty response
|
||||
# Arrange
|
||||
get_endpoint.return_value = {'Data': []}
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_fittings()
|
||||
|
||||
# Assert
|
||||
self.assertDictEqual({}, result)
|
||||
|
||||
@mock.patch('allianceauth.fleetup.managers.FleetUpManager.get_endpoint')
|
||||
def test_get_fleetup_fitting(self, get_endpoint):
|
||||
|
||||
get_endpoint.return_value = {"Data":
|
||||
{
|
||||
'FittingData': [{}]
|
||||
}
|
||||
}
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_fitting(1234)
|
||||
|
||||
# Asset
|
||||
self.assertTrue(get_endpoint.called)
|
||||
args, kwargs = get_endpoint.call_args
|
||||
self.assertEqual(args[0], FleetUpManager.BASE_URL + '/Fitting/1234')
|
||||
expected_result = {
|
||||
'fitting_data': {
|
||||
'FittingData': [{}]
|
||||
}
|
||||
}
|
||||
self.assertDictEqual(expected_result, result)
|
||||
|
||||
# Test None response
|
||||
# Arrange
|
||||
get_endpoint.return_value = None
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_fitting(1234)
|
||||
|
||||
# Assert
|
||||
self.assertIsNone(result)
|
||||
|
||||
# Test Empty response
|
||||
# Arrange
|
||||
get_endpoint.return_value = {'Data': {}}
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_fitting(1234)
|
||||
|
||||
# Assert
|
||||
self.assertDictEqual({"fitting_data": {}}, result)
|
||||
|
||||
@mock.patch('allianceauth.fleetup.managers.FleetUpManager.get_endpoint')
|
||||
def test_get_fleetup_doctrineid(self, get_endpoint):
|
||||
|
||||
get_endpoint.return_value = {
|
||||
"Data": {
|
||||
'Doctrines': [{'DoctrineId': 4567}]
|
||||
}
|
||||
}
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_doctrineid(1234)
|
||||
|
||||
# Asset
|
||||
self.assertTrue(get_endpoint.called)
|
||||
args, kwargs = get_endpoint.call_args
|
||||
self.assertEqual(args[0], FleetUpManager.BASE_URL + '/Fitting/1234')
|
||||
|
||||
self.assertEqual(4567, result)
|
||||
|
||||
# Test None response
|
||||
# Arrange
|
||||
get_endpoint.return_value = None
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_doctrineid(1234)
|
||||
|
||||
# Assert
|
||||
self.assertIsNone(result)
|
||||
|
||||
# Test Empty response
|
||||
# Arrange
|
||||
get_endpoint.return_value = {'Data': {}}
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_doctrineid(1234)
|
||||
|
||||
# Assert
|
||||
self.assertDictEqual({}, result)
|
||||
|
||||
@mock.patch('allianceauth.fleetup.managers.FleetUpManager.get_endpoint')
|
||||
def test_get_fleetup_fitting_eft(self, get_endpoint):
|
||||
|
||||
get_endpoint.return_value = {
|
||||
"Data": {
|
||||
'FittingData': '[Apocalypse Navy Issue, Foxcat]'
|
||||
}
|
||||
}
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_fitting_eft(1234)
|
||||
|
||||
# Asset
|
||||
self.assertTrue(get_endpoint.called)
|
||||
args, kwargs = get_endpoint.call_args
|
||||
self.assertEqual(args[0], FleetUpManager.BASE_URL + '/Fitting/1234/eft')
|
||||
|
||||
self.assertDictEqual({"fitting_eft": '[Apocalypse Navy Issue, Foxcat]'},
|
||||
result)
|
||||
|
||||
# Test None response
|
||||
# Arrange
|
||||
get_endpoint.return_value = None
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_fitting_eft(1234)
|
||||
|
||||
# Assert
|
||||
self.assertIsNone(result)
|
||||
|
||||
# Test Empty response
|
||||
# Arrange
|
||||
get_endpoint.return_value = {'Data': {}}
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_fitting_eft(1234)
|
||||
|
||||
# Assert
|
||||
self.assertDictEqual({"fitting_eft": {}}, result)
|
||||
@@ -1,14 +0,0 @@
|
||||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = 'fleetup'
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', views.fleetup_view, name='view'),
|
||||
url(r'^fittings/$', views.fleetup_fittings, name='fittings'),
|
||||
url(r'^fittings/(?P<fittingnumber>[0-9]+)/$', views.fleetup_fitting, name='fitting'),
|
||||
url(r'^doctrines/$', views.fleetup_doctrines, name='doctrines'),
|
||||
url(r'^characters/$', views.fleetup_characters, name='characters'),
|
||||
url(r'^doctrines/(?P<doctrinenumber>[0-9]+)/$', views.fleetup_doctrine, name='doctrine'),
|
||||
]
|
||||
@@ -1,112 +0,0 @@
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.shortcuts import render
|
||||
from django.template.defaulttags import register
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .managers import FleetUpManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@register.filter
|
||||
def get_item(dictionary, key):
|
||||
return dictionary.get(key)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('auth.view_fleetup')
|
||||
def fleetup_view(request):
|
||||
logger.debug("fleetup_view called by user %s" % request.user)
|
||||
|
||||
operations_list = FleetUpManager.get_fleetup_operations()
|
||||
if operations_list is None:
|
||||
messages.add_message(request, messages.ERROR, _("Failed to get operations list, contact your administrator"))
|
||||
operations_list = {}
|
||||
timers_list = FleetUpManager.get_fleetup_timers()
|
||||
if timers_list is None:
|
||||
messages.add_message(request, messages.ERROR, _("Failed to get timers list, contact your administrator"))
|
||||
timers_list = {}
|
||||
now = datetime.datetime.now().strftime('%H:%M:%S')
|
||||
|
||||
context = {"timers_list": sorted(timers_list.items()),
|
||||
"operations_list": sorted(operations_list.items()),
|
||||
"now": now}
|
||||
|
||||
return render(request, 'fleetup/index.html', context=context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('auth.human_resources')
|
||||
@permission_required('auth.view_fleetup')
|
||||
def fleetup_characters(request):
|
||||
logger.debug("fleetup_characters called by user %s" % request.user)
|
||||
|
||||
member_list = FleetUpManager.get_fleetup_members()
|
||||
if member_list is None:
|
||||
messages.add_message(request, messages.ERROR, _("Failed to get member list, contact your administrator"))
|
||||
member_list = {}
|
||||
|
||||
context = {"member_list": sorted(member_list.items())}
|
||||
|
||||
return render(request, 'fleetup/characters.html', context=context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('auth.view_fleetup')
|
||||
def fleetup_fittings(request):
|
||||
logger.debug("fleetup_fittings called by user %s" % request.user)
|
||||
fitting_list = FleetUpManager.get_fleetup_fittings()
|
||||
|
||||
if fitting_list is None:
|
||||
messages.add_message(request, messages.ERROR, _("Failed to get fitting list, contact your administrator"))
|
||||
fitting_list = {}
|
||||
|
||||
context = {"fitting_list": sorted(fitting_list.items())}
|
||||
return render(request, 'fleetup/fittingsview.html', context=context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('auth.view_fleetup')
|
||||
def fleetup_fitting(request, fittingnumber):
|
||||
logger.debug("fleetup_fitting called by user %s" % request.user)
|
||||
fitting_eft = FleetUpManager.get_fleetup_fitting_eft(fittingnumber)
|
||||
fitting_data = FleetUpManager.get_fleetup_fitting(fittingnumber)
|
||||
doctrinenumber = FleetUpManager.get_fleetup_doctrineid(fittingnumber)
|
||||
doctrines_list = FleetUpManager.get_fleetup_doctrine(doctrinenumber)
|
||||
|
||||
if fitting_eft is None or fitting_data is None or doctrinenumber is None:
|
||||
messages.add_message(request, messages.ERROR, _("There was an error getting some of the data for this fitting. "
|
||||
"Contact your administrator"))
|
||||
|
||||
context = {"fitting_eft": fitting_eft,
|
||||
"fitting_data": fitting_data,
|
||||
"doctrines_list": doctrines_list}
|
||||
return render(request, 'fleetup/fitting.html', context=context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('auth.view_fleetup')
|
||||
def fleetup_doctrines(request):
|
||||
logger.debug("fleetup_doctrines called by user %s" % request.user)
|
||||
doctrines_list = FleetUpManager.get_fleetup_doctrines()
|
||||
if doctrines_list is None:
|
||||
messages.add_message(request, messages.ERROR, _("Failed to get doctrines list, contact your administrator"))
|
||||
|
||||
context = {"doctrines_list": doctrines_list}
|
||||
return render(request, 'fleetup/doctrinesview.html', context=context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('auth.view_fleetup')
|
||||
def fleetup_doctrine(request, doctrinenumber):
|
||||
logger.debug("fleetup_doctrine called by user %s" % request.user)
|
||||
doctrine = FleetUpManager.get_fleetup_doctrine(doctrinenumber)
|
||||
if doctrine is None:
|
||||
messages.add_message(request, messages.ERROR, _("Failed to get doctine, contact your administrator"))
|
||||
context = {"doctrine": doctrine}
|
||||
return render(request, 'fleetup/doctrine.html', context=context)
|
||||
@@ -1,28 +1,68 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
from django.contrib.auth.models import Group as BaseGroup
|
||||
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed
|
||||
from django.dispatch import receiver
|
||||
from .models import AuthGroup
|
||||
from .models import GroupRequest
|
||||
|
||||
|
||||
class AuthGroupAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin model for AuthGroup
|
||||
"""
|
||||
filter_horizontal = ('group_leaders',)
|
||||
class AuthGroupInlineAdmin(admin.StackedInline):
|
||||
model = AuthGroup
|
||||
filter_horizontal = ('group_leaders', 'states',)
|
||||
fields = ('description', 'group_leaders', 'states', 'internal', 'hidden', 'open', 'public')
|
||||
verbose_name_plural = 'Auth Settings'
|
||||
verbose_name = ''
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return request.user.has_perm('auth.change_group')
|
||||
|
||||
|
||||
class ProxyGroup(Group):
|
||||
class GroupAdmin(admin.ModelAdmin):
|
||||
filter_horizontal = ('permissions',)
|
||||
inlines = (AuthGroupInlineAdmin,)
|
||||
|
||||
|
||||
class Group(BaseGroup):
|
||||
class Meta:
|
||||
proxy = True
|
||||
verbose_name = Group._meta.verbose_name
|
||||
verbose_name_plural = Group._meta.verbose_name_plural
|
||||
verbose_name = BaseGroup._meta.verbose_name
|
||||
verbose_name_plural = BaseGroup._meta.verbose_name_plural
|
||||
|
||||
try:
|
||||
admin.site.unregister(Group)
|
||||
admin.site.unregister(BaseGroup)
|
||||
finally:
|
||||
admin.site.register(ProxyGroup)
|
||||
admin.site.register(Group, GroupAdmin)
|
||||
|
||||
|
||||
admin.site.register(GroupRequest)
|
||||
admin.site.register(AuthGroup, AuthGroupAdmin)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Group)
|
||||
def redirect_pre_save(sender, signal=None, *args, **kwargs):
|
||||
pre_save.send(BaseGroup, *args, **kwargs)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Group)
|
||||
def redirect_post_save(sender, signal=None, *args, **kwargs):
|
||||
post_save.send(BaseGroup, *args, **kwargs)
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=Group)
|
||||
def redirect_pre_delete(sender, signal=None, *args, **kwargs):
|
||||
pre_delete.send(BaseGroup, *args, **kwargs)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=Group)
|
||||
def redirect_post_delete(sender, signal=None, *args, **kwargs):
|
||||
post_delete.send(BaseGroup, *args, **kwargs)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Group.permissions.through)
|
||||
def redirect_m2m_changed_permissions(sender, signal=None, *args, **kwargs):
|
||||
m2m_changed.send(BaseGroup, *args, **kwargs)
|
||||
@@ -4,3 +4,4 @@ from django.apps import AppConfig
|
||||
class GroupManagementConfig(AppConfig):
|
||||
name = 'allianceauth.groupmanagement'
|
||||
label = 'groupmanagement'
|
||||
verbose_name = 'Group Management'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.contrib.auth.models import Group
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
class GroupManager:
|
||||
@@ -6,7 +7,12 @@ class GroupManager:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_joinable_groups():
|
||||
def get_joinable_groups(state):
|
||||
return Group.objects.select_related('authgroup').exclude(authgroup__internal=True)\
|
||||
.filter(Q(authgroup__states=state) | Q(authgroup__states=None))
|
||||
|
||||
@staticmethod
|
||||
def get_all_non_internal_groups():
|
||||
return Group.objects.select_related('authgroup').exclude(authgroup__internal=True)
|
||||
|
||||
@staticmethod
|
||||
@@ -14,13 +20,35 @@ class GroupManager:
|
||||
return Group.objects.select_related('authgroup').filter(authgroup__group_leaders__in=[user])
|
||||
|
||||
@staticmethod
|
||||
def joinable_group(group):
|
||||
def joinable_group(group, state):
|
||||
"""
|
||||
Check if a group is a user joinable group, i.e.
|
||||
not an internal group for Corp, Alliance, Members etc
|
||||
Check if a group is a user/state joinable group, i.e.
|
||||
not an internal group for Corp, Alliance, Members etc,
|
||||
or restricted from the user's current state.
|
||||
:param group: django.contrib.auth.models.Group object
|
||||
:param state: allianceauth.authentication.State object
|
||||
:return: bool True if its joinable, False otherwise
|
||||
"""
|
||||
if len(group.authgroup.states.all()) != 0 and state not in group.authgroup.states.all():
|
||||
return False
|
||||
return not group.authgroup.internal
|
||||
|
||||
@staticmethod
|
||||
def check_internal_group(group):
|
||||
"""
|
||||
Check if a group is auditable, i.e not an internal group
|
||||
:param group: django.contrib.auth.models.Group object
|
||||
:return: bool True if it is auditable, false otherwise
|
||||
"""
|
||||
return not group.authgroup.internal
|
||||
|
||||
@staticmethod
|
||||
def check_internal_group(group):
|
||||
"""
|
||||
Check if a group is auditable, i.e not an internal group
|
||||
:param group: django.contrib.auth.models.Group object
|
||||
:return: bool True if it is auditable, false otherwise
|
||||
"""
|
||||
return not group.authgroup.internal
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -15,7 +15,7 @@ class Migration(migrations.Migration):
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ProxyGroup',
|
||||
name='Group',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.10 on 2018-02-23 23:09
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def delete_permissions(apps, schema_editor):
|
||||
AuthGroup = apps.get_model('groupmanagement', 'AuthGroup')
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
Permission = apps.get_model('auth', 'Permission')
|
||||
ct = ContentType.objects.get_for_model(AuthGroup)
|
||||
Permission.objects.filter(content_type=ct).delete()
|
||||
|
||||
|
||||
def recreate_permissions(apps, schema_editor):
|
||||
AuthGroup = apps.get_model('groupmanagement', 'AuthGroup')
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
Permission = apps.get_model('auth', 'Permission')
|
||||
ct = ContentType.objects.get_for_model(AuthGroup)
|
||||
Permission.objects.create(content_type=ct, name='Can add auth group', codename='add_authgroup')
|
||||
Permission.objects.create(content_type=ct, name='Can delete auth group', codename='delete_authgroup')
|
||||
Permission.objects.create(content_type=ct, name='Can change auth group', codename='change_authgroup')
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('groupmanagement', '0007_on_delete'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='authgroup',
|
||||
options={'default_permissions': (), 'permissions': (('request_groups', 'Can request non-public groups'),)},
|
||||
),
|
||||
migrations.RunPython(delete_permissions, recreate_permissions)
|
||||
]
|
||||
28
allianceauth/groupmanagement/migrations/0009_requestlog.py
Normal file
28
allianceauth/groupmanagement/migrations/0009_requestlog.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 2.0.6 on 2018-06-04 02:45
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('auth', '0008_alter_user_username_max_length'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('groupmanagement', '0008_remove_authgroup_permissions'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='RequestLog',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('request_type', models.NullBooleanField(default=0)),
|
||||
('request_info', models.CharField(max_length=254)),
|
||||
('action', models.BooleanField(default=0)),
|
||||
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.Group')),
|
||||
('request_actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 2.0.6 on 2018-07-11 00:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('authentication', '0016_ownershiprecord'),
|
||||
('groupmanagement', '0009_requestlog'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='authgroup',
|
||||
name='states',
|
||||
field=models.ManyToManyField(blank=True, help_text='States listed here will have the ability to join this group provided they have the proper permissions.', related_name='valid_states', to='authentication.State'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 2.0.8 on 2018-12-07 08:56
|
||||
|
||||
import datetime
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('groupmanagement', '0010_authgroup_states'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='requestlog',
|
||||
name='date',
|
||||
field=models.DateTimeField(default=datetime.datetime(2018, 12, 7, 8, 56, 33, 846342)),
|
||||
),
|
||||
]
|
||||
@@ -3,8 +3,8 @@ from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from allianceauth.eveonline.models import EveCharacter
|
||||
from allianceauth.authentication.models import State
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class GroupRequest(models.Model):
|
||||
@@ -25,6 +25,38 @@ class GroupRequest(models.Model):
|
||||
return self.user.username + ":" + self.group.name
|
||||
|
||||
|
||||
class RequestLog(models.Model):
|
||||
request_type = models.NullBooleanField(default=0)
|
||||
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
||||
request_info = models.CharField(max_length=254)
|
||||
action = models.BooleanField(default=0)
|
||||
request_actor = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
date = models.DateTimeField(auto_now_add=datetime.utcnow())
|
||||
|
||||
def requestor(self):
|
||||
return self.request_info.split(":")[0]
|
||||
|
||||
def type_to_str(self):
|
||||
if self.request_type is None:
|
||||
return "Removed"
|
||||
elif self.request_type is True:
|
||||
return "Leave"
|
||||
elif self.request_type is False:
|
||||
return "Join"
|
||||
|
||||
def action_to_str(self):
|
||||
if self.action is True:
|
||||
return "Accept"
|
||||
elif self.action is False:
|
||||
return "Reject"
|
||||
|
||||
def req_char(self):
|
||||
usr = self.requestor()
|
||||
user = User.objects.get(username=usr)
|
||||
return user.profile.main_character
|
||||
|
||||
|
||||
|
||||
class AuthGroup(models.Model):
|
||||
"""
|
||||
Extends Django Group model with a one-to-one field
|
||||
@@ -67,6 +99,10 @@ class AuthGroup(models.Model):
|
||||
"specifically. Use the auth.group_management permission to allow "
|
||||
"a user to manage all groups.")
|
||||
|
||||
states = models.ManyToManyField(State, related_name='valid_states', blank=True,
|
||||
help_text="States listed here will have the ability to join this group provided "
|
||||
"they have the proper permissions.")
|
||||
|
||||
description = models.CharField(max_length=512, blank=True, help_text="Description of the group shown to users.")
|
||||
|
||||
def __str__(self):
|
||||
@@ -76,6 +112,7 @@ class AuthGroup(models.Model):
|
||||
permissions = (
|
||||
("request_groups", u"Can request non-public groups"),
|
||||
)
|
||||
default_permissions = tuple()
|
||||
|
||||
|
||||
@receiver(post_save, sender=Group)
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
{% extends "allianceauth/base.html" %}
|
||||
{% load staticfiles %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}{{ group }} {% trans "Audit Log" %}{% endblock page_title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<br>
|
||||
{% include 'groupmanagement/menu.html' %}
|
||||
<div>
|
||||
<h3>{{ group }} Audit Log</h3>
|
||||
<p> All times displayed are EVE/UTC.</p>
|
||||
{% if entries %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped" id="log-entries">
|
||||
<thead>
|
||||
<th class="text-center" scope="col">{% trans "Date/Time" %}</th>
|
||||
<th class="text-center" scope="col">{% trans "Requestor" %}</th>
|
||||
<th class="text-center" scope="col">{% trans "Main Character" %}</th>
|
||||
<th class="text-center" scope="col">{% trans "Group" %}</th>
|
||||
<th class="text-center" scope="col">{% trans "Type" %}</th>
|
||||
<th class="text-center" scope="col">{% trans "Action" %}</th>
|
||||
<th class="text-center" scope="col">{% trans "Actor" %}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in entries %}
|
||||
<tr>
|
||||
<td class="text-center">{{ entry.date }}</td>
|
||||
<td class="text-center">{{ entry.requestor }}</td>
|
||||
<td class="text-center">{{ entry.req_char }}</td>
|
||||
<td class="text-center">{{ entry.group }}</td>
|
||||
<td class="text-center">{{ entry.type_to_str }}</td>
|
||||
{% if entry.request_type is None %}
|
||||
<td class="text-center"> Removed</td>
|
||||
{% else %}
|
||||
<td class="text-center">{{ entry.action_to_str }}</td>
|
||||
{% endif %}
|
||||
<td class="text-center">{{ entry.request_actor }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning text-center">{% trans "No entries found for this group." %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block extra_javascript %}
|
||||
{% include 'bundles/datatables-js.html' %}
|
||||
{% endblock %}
|
||||
{% block extra_css %}
|
||||
{% include 'bundles/datatables-css.html' %}
|
||||
{% endblock %}
|
||||
{% block extra_script %}
|
||||
$(document).ready(function(){
|
||||
$('#log-entries').DataTable();
|
||||
});
|
||||
{% endblock %}
|
||||
|
||||
@@ -41,6 +41,9 @@
|
||||
title="{% trans "View Members" %}">
|
||||
<i class="glyphicon glyphicon-eye-open"></i>
|
||||
</a>
|
||||
<a href="{% url "groupmanagement:audit_log" group.id %}" class="btn btn-info" title="{% trans "Audit Members" %}">
|
||||
<i class="glyphicon glyphicon-list-alt"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -19,7 +19,7 @@ url
|
||||
{% for g in groups %}
|
||||
<tr>
|
||||
<td class="text-center">{{ g.group.name }}</td>
|
||||
<td class="text-center">{{ g.group.authgroup.description }}</td>
|
||||
<td class="text-center">{{ g.group.authgroup.description|urlize }}</td>
|
||||
<td class="text-center">
|
||||
{% if g.group in user.groups.all %}
|
||||
{% if not g.request %}
|
||||
@@ -32,9 +32,15 @@ url
|
||||
</button>
|
||||
{% endif %}
|
||||
{% elif not g.request %}
|
||||
<a href="{% url 'groupmanagement:request_add' g.group.id %}" class="btn btn-success">
|
||||
{% trans "Request" %}
|
||||
</a>
|
||||
{% if g.group.authgroup.open %}
|
||||
<a href="{% url 'groupmanagement:request_add' g.group.id %}" class="btn btn-success">
|
||||
{% trans "Join" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'groupmanagement:request_add' g.group.id %}" class="btn btn-primary">
|
||||
{% trans "Request" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-primary" disabled>
|
||||
{{ g.request.status }}
|
||||
|
||||
@@ -12,6 +12,7 @@ urlpatterns = [
|
||||
name='membership'),
|
||||
url(r'^membership/(\w+)/$', views.group_membership_list,
|
||||
name='membership_list'),
|
||||
url(r'^membership/(\w+)/audit/$', views.group_membership_audit, name="audit_log"),
|
||||
url(r'^membership/(\w+)/remove/(\w+)/$', views.group_membership_remove,
|
||||
name='membership_remove'),
|
||||
url(r'^request_add/(\w+)', views.group_request_add,
|
||||
|
||||
@@ -5,15 +5,18 @@ from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
from django.contrib.auth.models import Group
|
||||
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||
from django.core.paginator import Paginator, EmptyPage
|
||||
from django.db.models import Count
|
||||
from django.http import Http404
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from .managers import GroupManager
|
||||
from .models import GroupRequest
|
||||
from .models import GroupRequest, RequestLog
|
||||
|
||||
from allianceauth.notifications import notify
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -53,7 +56,7 @@ def group_membership(request):
|
||||
# Get all open and closed groups
|
||||
if GroupManager.has_management_permission(request.user):
|
||||
# Full access
|
||||
groups = GroupManager.get_joinable_groups()
|
||||
groups = GroupManager.get_all_non_internal_groups()
|
||||
else:
|
||||
# Group leader specific
|
||||
groups = GroupManager.get_group_leaders_groups(request.user)
|
||||
@@ -65,6 +68,31 @@ def group_membership(request):
|
||||
return render(request, 'groupmanagement/groupmembership.html', context=render_items)
|
||||
|
||||
|
||||
@login_required
|
||||
@user_passes_test(GroupManager.can_manage_groups)
|
||||
def group_membership_audit(request, group_id):
|
||||
logger.debug("group_management_audit called by user %s" % request.user)
|
||||
group = get_object_or_404(Group, id=group_id)
|
||||
try:
|
||||
|
||||
# Check its a joinable group i.e. not corp or internal
|
||||
# And the user has permission to manage it
|
||||
if not GroupManager.check_internal_group(group) or not GroupManager.can_manage_group(request.user, group):
|
||||
logger.warning("User %s attempted to view the membership of group %s but permission was denied" %
|
||||
(request.user, group_id))
|
||||
raise PermissionDenied
|
||||
|
||||
except ObjectDoesNotExist:
|
||||
raise Http404("Group does not exist")
|
||||
render_items = {'group': group.name}
|
||||
entries = RequestLog.objects.filter(group=group).order_by('-date')
|
||||
render_items['entries'] = entries
|
||||
|
||||
return render(request, 'groupmanagement/audit.html', context=render_items)
|
||||
|
||||
|
||||
|
||||
|
||||
@login_required
|
||||
@user_passes_test(GroupManager.can_manage_groups)
|
||||
def group_membership_list(request, group_id):
|
||||
@@ -74,7 +102,7 @@ def group_membership_list(request, group_id):
|
||||
|
||||
# Check its a joinable group i.e. not corp or internal
|
||||
# And the user has permission to manage it
|
||||
if not GroupManager.joinable_group(group) or not GroupManager.can_manage_group(request.user, group):
|
||||
if not GroupManager.check_internal_group(group) or not GroupManager.can_manage_group(request.user, group):
|
||||
logger.warning("User %s attempted to view the membership of group %s but permission was denied" %
|
||||
(request.user, group_id))
|
||||
raise PermissionDenied
|
||||
@@ -105,13 +133,16 @@ def group_membership_remove(request, group_id, user_id):
|
||||
try:
|
||||
# Check its a joinable group i.e. not corp or internal
|
||||
# And the user has permission to manage it
|
||||
if not GroupManager.joinable_group(group) or not GroupManager.can_manage_group(request.user, group):
|
||||
if not GroupManager.check_internal_group(group) or not GroupManager.can_manage_group(request.user, group):
|
||||
logger.warning("User %s attempted to remove a user from group %s but permission was denied" % (request.user,
|
||||
group_id))
|
||||
raise PermissionDenied
|
||||
|
||||
try:
|
||||
user = group.user_set.get(id=user_id)
|
||||
request_info = user.username + ":" + group.name
|
||||
log = RequestLog(request_type=None,group=group,request_info=request_info,action=1,request_actor=request.user)
|
||||
log.save()
|
||||
# Remove group from user
|
||||
user.groups.remove(group)
|
||||
logger.info("User %s removed user %s from group %s" % (request.user, user, group))
|
||||
@@ -133,12 +164,14 @@ def group_accept_request(request, group_request_id):
|
||||
try:
|
||||
group, created = Group.objects.get_or_create(name=group_request.group.name)
|
||||
|
||||
if not GroupManager.joinable_group(group_request.group) or \
|
||||
if not GroupManager.joinable_group(group_request.group, group_request.user.profile.state) or \
|
||||
not GroupManager.can_manage_group(request.user, group_request.group):
|
||||
raise PermissionDenied
|
||||
|
||||
group_request.user.groups.add(group)
|
||||
group_request.user.save()
|
||||
log = RequestLog(request_type=group_request.leave_request,group=group,request_info=group_request.__str__(),action=1,request_actor=request.user)
|
||||
log.save()
|
||||
group_request.delete()
|
||||
logger.info("User %s accepted group request from user %s to group %s" % (
|
||||
request.user, group_request.user, group_request.group.name))
|
||||
@@ -172,6 +205,8 @@ def group_reject_request(request, group_request_id):
|
||||
if group_request:
|
||||
logger.info("User %s rejected group request from user %s to group %s" % (
|
||||
request.user, group_request.user, group_request.group.name))
|
||||
log = RequestLog(request_type=group_request.leave_request,group=group_request.group,request_info=group_request.__str__(),action=0,request_actor=request.user)
|
||||
log.save()
|
||||
group_request.delete()
|
||||
notify(group_request.user, "Group Application Rejected", level="danger",
|
||||
message="Your application to %s has been rejected." % group_request.group)
|
||||
@@ -204,6 +239,8 @@ def group_leave_accept_request(request, group_request_id):
|
||||
group, created = Group.objects.get_or_create(name=group_request.group.name)
|
||||
group_request.user.groups.remove(group)
|
||||
group_request.user.save()
|
||||
log = RequestLog(request_type=group_request.leave_request,group=group_request.group,request_info=group_request.__str__(),action=1,request_actor=request.user)
|
||||
log.save()
|
||||
group_request.delete()
|
||||
logger.info("User %s accepted group leave request from user %s to group %s" % (
|
||||
request.user, group_request.user, group_request.group.name))
|
||||
@@ -236,6 +273,8 @@ def group_leave_reject_request(request, group_request_id):
|
||||
raise PermissionDenied
|
||||
|
||||
if group_request:
|
||||
log = RequestLog(request_type=group_request.leave_request,group=group_request.group,request_info=group_request.__str__(),action=0,request_actor=request.user)
|
||||
log.save()
|
||||
group_request.delete()
|
||||
logger.info("User %s rejected group leave request from user %s for group %s" % (
|
||||
request.user, group_request.user, group_request.group.name))
|
||||
@@ -262,7 +301,7 @@ def groups_view(request):
|
||||
logger.debug("groups_view called by user %s" % request.user)
|
||||
groups = []
|
||||
|
||||
group_query = GroupManager.get_joinable_groups()
|
||||
group_query = GroupManager.get_joinable_groups(request.user.profile.state)
|
||||
|
||||
if not request.user.has_perm('groupmanagement.request_groups'):
|
||||
# Filter down to public groups only for non-members
|
||||
@@ -284,11 +323,18 @@ def groups_view(request):
|
||||
def group_request_add(request, group_id):
|
||||
logger.debug("group_request_add called by user %s for group id %s" % (request.user, group_id))
|
||||
group = Group.objects.get(id=group_id)
|
||||
if not GroupManager.joinable_group(group):
|
||||
state = request.user.profile.state
|
||||
if not GroupManager.joinable_group(group, state):
|
||||
logger.warning("User %s attempted to join group id %s but it is not a joinable group" %
|
||||
(request.user, group_id))
|
||||
messages.warning(request, _("You cannot join that group"))
|
||||
return redirect('groupmanagement:groups')
|
||||
if group in request.user.groups.all():
|
||||
# User is already a member of this group.
|
||||
logger.warning("User %s attempted to join group id %s but they are already a member." %
|
||||
(request.user, group_id))
|
||||
messages.warning(request, "You are already a member of that group.")
|
||||
return redirect('groupmanagement:groups')
|
||||
if not request.user.has_perm('groupmanagement.request_groups') and not group.authgroup.public:
|
||||
# Does not have the required permission, trying to join a non-public group
|
||||
logger.warning("User %s attempted to join group id %s but it is not a public group" %
|
||||
@@ -299,11 +345,15 @@ def group_request_add(request, group_id):
|
||||
logger.info("%s joining %s as is an open group" % (request.user, group))
|
||||
request.user.groups.add(group)
|
||||
return redirect("groupmanagement:groups")
|
||||
req = GroupRequest.objects.filter(user=request.user, group=group)
|
||||
if len(req) > 0:
|
||||
logger.info("%s attempted to join %s but already has an open application" % (request.user, group))
|
||||
messages.warning(request, "You already have a pending application for that group.")
|
||||
return redirect("groupmanagement:groups")
|
||||
grouprequest = GroupRequest()
|
||||
grouprequest.status = _('Pending')
|
||||
grouprequest.group = group
|
||||
grouprequest.user = request.user
|
||||
grouprequest.main_char = request.user.profile.main_character
|
||||
grouprequest.leave_request = False
|
||||
grouprequest.save()
|
||||
logger.info("Created group request for user %s to group %s" % (request.user, Group.objects.get(id=group_id)))
|
||||
@@ -315,7 +365,7 @@ def group_request_add(request, group_id):
|
||||
def group_request_leave(request, group_id):
|
||||
logger.debug("group_request_leave called by user %s for group id %s" % (request.user, group_id))
|
||||
group = Group.objects.get(id=group_id)
|
||||
if not GroupManager.joinable_group(group):
|
||||
if not GroupManager.check_internal_group(group):
|
||||
logger.warning("User %s attempted to leave group id %s but it is not a joinable group" %
|
||||
(request.user, group_id))
|
||||
messages.warning(request, _("You cannot leave that group"))
|
||||
@@ -329,11 +379,19 @@ def group_request_leave(request, group_id):
|
||||
logger.info("%s leaving %s as is an open group" % (request.user, group))
|
||||
request.user.groups.remove(group)
|
||||
return redirect("groupmanagement:groups")
|
||||
req = GroupRequest.objects.filter(user=request.user, group=group)
|
||||
if len(req) > 0:
|
||||
logger.info("%s attempted to leave %s but already has an pending leave request." % (request.user, group))
|
||||
messages.warning(request, "You already have a pending leave request for that group.")
|
||||
return redirect("groupmanagement:groups")
|
||||
if getattr(settings, 'AUTO_LEAVE', False):
|
||||
logger.info("%s leaving joinable group %s due to auto_leave" % (request.user, group))
|
||||
request.user.groups.remove(group)
|
||||
return redirect('groupmanagement:groups')
|
||||
grouprequest = GroupRequest()
|
||||
grouprequest.status = _('Pending')
|
||||
grouprequest.group = group
|
||||
grouprequest.user = request.user
|
||||
grouprequest.main_char = request.user.profile.main_character
|
||||
grouprequest.leave_request = True
|
||||
grouprequest.save()
|
||||
logger.info("Created group leave request for user %s to group %s" % (request.user, Group.objects.get(id=group_id)))
|
||||
|
||||
@@ -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:'])
|
||||
|
||||
@@ -154,6 +154,12 @@
|
||||
class="btn btn-primary">
|
||||
<span class="glyphicon glyphicon-eye-open"></span>
|
||||
</a>
|
||||
{% if perms.hrapplications.delete_application %}
|
||||
<a href="{% url 'hrapplications:remove' app.id %}"
|
||||
class="btn btn-danger">
|
||||
<span class="glyphicon glyphicon-remove"></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<img class="ra-avatar img-responsive img-circle"
|
||||
src="https://image.eveonline.com/Character/{{ char.character_id }}_32.jpg">
|
||||
src="{{ char.portrait_url_32 }}">
|
||||
</td>
|
||||
<td class="text-center">{{ char.character_name }}</td>
|
||||
<td class="text-center">{{ char.corporation_name }}</td>
|
||||
|
||||
@@ -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"),
|
||||
]
|
||||
|
||||
@@ -67,7 +67,7 @@ def hr_application_create_view(request, form_id=None):
|
||||
""))
|
||||
response.save()
|
||||
logger.info("%s created %s" % (request.user, application))
|
||||
return redirect('hrapplications:view')
|
||||
return redirect('hrapplications:personal_view', application.id)
|
||||
else:
|
||||
questions = app_form.questions.all()
|
||||
return render(request, 'hrapplications/create.html',
|
||||
@@ -95,7 +95,7 @@ def hr_application_personal_view(request, app_id):
|
||||
return render(request, 'hrapplications/view.html', context=context)
|
||||
else:
|
||||
logger.warn("User %s not authorized to view %s" % (request.user, app))
|
||||
return redirect('hrapplications:view')
|
||||
return redirect('hrapplications:personal_view')
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -110,7 +110,7 @@ def hr_application_personal_removal(request, app_id):
|
||||
logger.warn("User %s attempting to delete reviewed app %s" % (request.user, app))
|
||||
else:
|
||||
logger.warn("User %s not authorized to delete %s" % (request.user, app))
|
||||
return redirect('hrapplications:view')
|
||||
return redirect('hrapplications:index')
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -158,7 +158,7 @@ def hr_application_remove(request, app_id):
|
||||
logger.info("User %s deleting %s" % (request.user, app))
|
||||
app.delete()
|
||||
notify(app.user, "Application Deleted", message="Your application to %s was deleted." % app.form.corp)
|
||||
return redirect('hrapplications:view')
|
||||
return redirect('hrapplications:index')
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -175,7 +175,7 @@ def hr_application_approve(request, app_id):
|
||||
level="success")
|
||||
else:
|
||||
logger.warn("User %s not authorized to approve %s" % (request.user, app))
|
||||
return redirect('hrapplications:view')
|
||||
return redirect('hrapplications:index')
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -192,7 +192,7 @@ def hr_application_reject(request, app_id):
|
||||
level="danger")
|
||||
else:
|
||||
logger.warn("User %s not authorized to reject %s" % (request.user, app))
|
||||
return redirect('hrapplications:view')
|
||||
return redirect('hrapplications:index')
|
||||
|
||||
|
||||
@login_required
|
||||
|
||||
BIN
allianceauth/locale/de/LC_MESSAGES/django.mo
Normal file
BIN
allianceauth/locale/de/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
allianceauth/locale/es/LC_MESSAGES/django.mo
Normal file
BIN
allianceauth/locale/es/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2043
allianceauth/locale/es/LC_MESSAGES/django.po
Normal file
2043
allianceauth/locale/es/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user