Compare commits

...

38 Commits

Author SHA1 Message Date
Adarnof
b41521bcb5 Version bump to v1.15.2 2017-06-27 00:21:00 -04:00
Basraah
882cafb4ba Discord API rate limiting (#799)
Added discord too many requests handling decorator
Added tests for core Discord manager functions
Added discord backoff retry for celery
Added tests for update groups backoff
Support per-route and global rate limiting
2017-06-04 18:36:25 -04:00
Basraah
5db340c64a Fleet-up cleanup (#798)
Add caching and better error handling
Move fleetup templates into fleetup app
Move fleetup urls into fleetup app
Fix button overflow and url
Add manager unit tests
Fix python3 compatibility
2017-06-04 18:35:23 -04:00
Adarnof
e76b0789f3 correct centos service name
Thanks @iAddz
2017-05-23 11:03:52 -04:00
iAddz
d2e32d3da3 Fleet op timer ui update (#787) 2017-05-11 12:32:45 -04:00
Adarnof
12cfc552da Correct Corporation creation using XML provider
Closes #734
2017-05-04 22:41:27 -04:00
Adarnof
dc10245158 Do not suspend account on disabling user. 2017-05-04 22:31:56 -04:00
iAddz
1b0c3c3bfc Corp member view for fat stats (#785)
Additionally fixes 500's from showing when
- a corporation object has been deleted
- an account doesn't have a main char set

Add Math tooltips
2017-05-04 10:39:03 -04:00
Basraah
aec013b93c SeAT service cleanup (#796)
Manager function tidyup
Hopefully improved the key sync function, at least it should be easier to follow whats happening now.
Remove partial logging of unhashed passwords
Added user feedback
2017-05-04 10:31:57 -04:00
iAddz
4556a0e740 SRP QOL + Validation (#786)
- new UI for srp management (mass performs, inline editing)
- unique validation for srp killboard links
- character auth ownership verification for killboard links
- removed remnants of old error messaging system & replaced with current
standard
 - added a confirmation popup when deleting fleet SRP's
2017-05-03 16:55:10 -04:00
mmolitor87
aad3bd6f57 sets language value to default for phpbb (#771)
Without this being set users get "The language you specified is not
valid." when trying to edit global settings such as timezone or style.
2017-05-03 16:53:44 -04:00
Basraah
17dd7c04c7 Replace django-celery with base Celery (#791)
Update celery tasks to new style & remove djcelery
Vanilla celery + django-celery-beat take over the role of djcelery. Task schedules are consolidated into settings instead of residing in code.
Update docs and example supervisor configs.
2017-05-03 16:53:16 -04:00
Basraah
372e582c6e Nginx docs (#794) 2017-05-03 16:50:27 -04:00
Adarnof
5a93128f4f Prevent FAT CorpStats creation for missing corp models. 2017-04-27 22:29:13 -04:00
Basraah
901dd5033a Added a cut down apache config 2017-04-27 10:28:06 +10:00
Basraah
d8043ff735 Add Gunicorn docs (#777)
* Added gunicorn docs

* Changes suggested by @Betriebsrat
2017-04-11 11:53:12 +10:00
Adarnof
bb3e7a0449 Tolerate validating submitted email if 2+ users
Addresses #783
2017-04-10 14:27:17 -04:00
Basraah
806962cda5 Fix incorrect variable name 2017-04-02 21:50:48 +10:00
Basraah
2cd43280e2 Remove obsolete services settings 2017-03-31 13:22:43 +10:00
Adarnof
6c94640552 Set main_char_id to emptystring when main deleted
Closes #769
2017-03-18 21:30:17 -04:00
Adarnof
250c376abb Correct queuing of name syncs with user pk 2017-03-12 16:06:14 -04:00
Adarnof
fb22aaf731 Consolidate TS3 into base services table
Beautify services table with hover and no borders
Unify formatting of mumble/jabber/ts3 service URLs in table
2017-03-08 17:50:43 -05:00
Adarnof
9897c0bbba Provide mumble quick join link
Add button titles to services
2017-03-08 17:31:41 -05:00
Adarnof
7d0aa2b5ec Wait to validate user is on TS after presenting form (#758) 2017-03-07 23:41:48 -05:00
Adarnof
27628dc70b include http in front of example settings
@porowns
2017-03-07 16:28:19 -05:00
Basraah
ecb74e67b0 Fix link format 2017-02-28 12:41:59 +10:00
Basraah
de47e94870 Version bump to 1.15.1 2017-02-28 11:39:25 +10:00
Basraah
9238ac97cf Remove unnecessary package 2017-02-28 11:38:47 +10:00
Basraah
2e274d3baf Update Openfire broadcast tool (#742)
Allow users to ignore invalid certificates.
Added some limited user feedback.
Removed threading.
Prevent infinite connection attempt loops.
2017-02-28 11:30:26 +10:00
Basraah
c6118beddf Teamspeak 3 Updates (#741)
* Correct duplicate error and success messages to user

* Read out all buffer bytes before sending command

* Convert ts3 manager to use a single connection

Each instance of the class will now use a single connection and should
be cleanly disconnected when finished.

Compatible with `with` clauses and will automatically disconnect from
the TS3 server when it exits the `with` block.

* Update TS3 manager consumers to use new style

* Update unit tests to use new style manager
2017-02-28 11:28:51 +10:00
Nathan Morgan
e6e1339d71 Added Google reCaptcha (#738) 2017-02-28 11:27:24 +10:00
Basraah
693016e171 CDN Javascript & CSS fix and cleanup (#743)
* Add missing javascript and css

* Remove unnecessary javascript and css
2017-02-27 19:29:05 +10:00
Basraah
3e09f2179f Fix translation
Closes #740
2017-02-26 19:37:26 +10:00
Adarnof
3a1d0d0335 use v1 Universe for structure names 2017-02-25 17:24:12 -05:00
Adarnof
7c14aede26 Correct ObjectNotFound message for ESI corp IDs
Addresses #732
2017-02-23 17:32:09 -05:00
Adarnof
308dc9191f Create v1 Character ESI client for names lookup
Closes #731
Alter member sorting to put registered characters ahead of unregistered
Conditionally pluralize Main Characters on template
2017-02-23 10:42:26 -05:00
Adarnof
1a958384c3 Include images instead of imgur links 2017-02-22 23:52:27 -05:00
Adarnof
078ec785e4 Alter docs link to default version 2017-02-22 23:35:53 -05:00
104 changed files with 2473 additions and 1126 deletions

View File

@@ -2,7 +2,7 @@ Alliance Auth
============ ============
[![Join the chat at https://gitter.im/R4stl1n/allianceauth](https://badges.gitter.im/R4stl1n/allianceauth.svg)](https://gitter.im/R4stl1n/allianceauth?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Join the chat at https://gitter.im/R4stl1n/allianceauth](https://badges.gitter.im/R4stl1n/allianceauth.svg)](https://gitter.im/R4stl1n/allianceauth?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Documentation Status](https://readthedocs.org/projects/allianceauth/badge/?version=latest)](http://allianceauth.readthedocs.io/en/latest/?badge=latest) [![Documentation Status](https://readthedocs.org/projects/allianceauth/badge/?version=latest)](http://allianceauth.readthedocs.io/?badge=latest)
[![Build Status](https://travis-ci.org/allianceauth/allianceauth.svg?branch=master)](https://travis-ci.org/allianceauth/allianceauth) [![Build Status](https://travis-ci.org/allianceauth/allianceauth.svg?branch=master)](https://travis-ci.org/allianceauth/allianceauth)
[![Coverage Status](https://coveralls.io/repos/github/allianceauth/allianceauth/badge.svg?branch=master)](https://coveralls.io/github/allianceauth/allianceauth?branch=master) [![Coverage Status](https://coveralls.io/repos/github/allianceauth/allianceauth/badge.svg?branch=master)](https://coveralls.io/github/allianceauth/allianceauth?branch=master)
@@ -10,6 +10,8 @@ Alliance Auth
EVE service auth to help corps, alliances, and coalitions manage services. EVE service auth to help corps, alliances, and coalitions manage services.
Built for "The 99 Percent" open for anyone to use. Built for "The 99 Percent" open for anyone to use.
[Read the docs here.](http://allianceauth.rtfd.io)
Special Permissions In Admin: Special Permissions In Admin:
auth | user | group_management ( Access to add members to groups within the alliance ) auth | user | group_management ( Access to add members to groups within the alliance )

View File

@@ -4,5 +4,5 @@ from __future__ import absolute_import, unicode_literals
# Django starts so that shared_task will use this app. # Django starts so that shared_task will use this app.
from .celeryapp import app as celery_app # noqa from .celeryapp import app as celery_app # noqa
__version__ = '1.15.0' __version__ = '1.15.2'
NAME = 'Alliance Auth v%s' % __version__ NAME = 'Alliance Auth v%s' % __version__

View File

@@ -12,17 +12,12 @@ https://docs.djangoproject.com/en/1.10/ref/settings/
import os import os
import djcelery
from django.contrib import messages from django.contrib import messages
from celery.schedules import crontab from celery.schedules import crontab
djcelery.setup_loader()
# Celery configuration # Celery configuration
BROKER_URL = 'redis://localhost:6379/0' BROKER_URL = 'redis://localhost:6379/0'
CELERYBEAT_SCHEDULER = "djcelery.schedulers.DatabaseScheduler" CELERYBEAT_SCHEDULER = "django_celery_beat.schedulers.DatabaseScheduler"
CELERYBEAT_SCHEDULE = dict()
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -50,7 +45,7 @@ INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django.contrib.humanize', 'django.contrib.humanize',
'djcelery', 'django_celery_beat',
'bootstrapform', 'bootstrapform',
'authentication', 'authentication',
'services', 'services',
@@ -62,11 +57,13 @@ INSTALLED_APPS = [
'optimer', 'optimer',
'corputils', 'corputils',
'fleetactivitytracking', 'fleetactivitytracking',
'fleetup',
'notifications', 'notifications',
'esi', 'esi',
'permissions_tool', 'permissions_tool',
'geelweb.django.navhelper', 'geelweb.django.navhelper',
'bootstrap_pagination', 'bootstrap_pagination',
'captcha',
# Services # Services
'services.modules.mumble', 'services.modules.mumble',
@@ -221,12 +218,40 @@ CACHES = {
} }
} }
# Google Recaptcha
CAPTCHA_ENABLED = False
RECAPTCHA_PUBLIC_KEY = 'MyRecaptchaKey'
RECAPTCHA_PRIVATE_KEY = 'MyRecaptchaPrivateKey'
NOCAPTCHA = True
##################################################### #####################################################
## ##
## Auth configuration starts here ## Auth configuration starts here
## ##
##################################################### #####################################################
#########################
# CELERY SCHEDULED TASKS
#########################
CELERYBEAT_SCHEDULE = {
'run_api_refresh': {
'task': 'eveonline.tasks.run_api_refresh',
'schedule': crontab(minute=0, hour="*/3"),
},
'run_corp_update': {
'task': 'eveonline.tasks.run_corp_update',
'schedule': crontab(minute=0, hour="*/2"),
},
'update_all_corpstats': {
'task': 'corputils.tasks.update_all_corpstats',
'schedule': crontab(minute=0, hour="*/6"),
},
}
################# #################
# EMAIL SETTINGS # EMAIL SETTINGS
################# #################
@@ -416,11 +441,10 @@ BROADCAST_SERVICE_NAME = os.environ.get('AA_BROADCAST_SERVICE_NAME', "broadcast"
###################################### ######################################
# Mumble Configuration # Mumble Configuration
###################################### ######################################
# MUMBLE_URL - Mumble server url # MUMBLE_URL - Mumble server host
# MUMBLE_SERVER_ID - Mumble server id # Do not include leading http:// or mumble://
###################################### ######################################
MUMBLE_URL = os.environ.get('AA_MUMBLE_URL', "https://example.com") MUMBLE_URL = os.environ.get('AA_MUMBLE_URL', "example.com")
MUMBLE_SERVER_ID = int(os.environ.get('AA_MUMBLE_SERVER_ID', '1'))
###################################### ######################################
# PHPBB3 Configuration # PHPBB3 Configuration
@@ -638,6 +662,10 @@ LOGGING = {
'handlers': ['log_file', 'console', 'notifications'], 'handlers': ['log_file', 'console', 'notifications'],
'level': 'ERROR', 'level': 'ERROR',
}, },
'fleetup': {
'handlers': ['log_file', 'console', 'notifications'],
'level': 'DEBUG',
},
'util': { 'util': {
'handlers': ['log_file', 'console', 'notifications'], 'handlers': ['log_file', 'console', 'notifications'],
'level': 'DEBUG', 'level': 'DEBUG',

View File

@@ -4,20 +4,17 @@ Alliance Auth Test Suite Django settings.
import os import os
import djcelery
from django.contrib import messages from django.contrib import messages
import alliance_auth import alliance_auth
djcelery.setup_loader()
# Use nose to run all tests # Use nose to run all tests
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
NOSE_ARGS = [ NOSE_ARGS = [
#'--with-coverage', #'--with-coverage',
#'--cover-package=', #'--cover-package=',
#'--exe', # If your tests need this to be found/run, check they py files are not chmodded +x
] ]
# Celery configuration # Celery configuration
@@ -40,7 +37,7 @@ INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django.contrib.humanize', 'django.contrib.humanize',
'djcelery', 'django_celery_beat',
'bootstrapform', 'bootstrapform',
'authentication', 'authentication',
'services', 'services',
@@ -52,6 +49,7 @@ INSTALLED_APPS = [
'optimer', 'optimer',
'corputils', 'corputils',
'fleetactivitytracking', 'fleetactivitytracking',
'fleetup',
'notifications', 'notifications',
'esi', 'esi',
'permissions_tool', 'permissions_tool',
@@ -400,12 +398,12 @@ TEAMSPEAK3_PUBLIC_URL = os.environ.get('AA_TEAMSPEAK3_PUBLIC_URL', 'example.com'
# DISCORD_CALLBACK_URL - oauth callback url # DISCORD_CALLBACK_URL - oauth callback url
# DISCORD_SYNC_NAMES - enable to force discord nicknames to be set to eve char name (bot needs Manage Nicknames permission) # DISCORD_SYNC_NAMES - enable to force discord nicknames to be set to eve char name (bot needs Manage Nicknames permission)
###################################### ######################################
DISCORD_GUILD_ID = os.environ.get('AA_DISCORD_GUILD_ID', '') DISCORD_GUILD_ID = os.environ.get('AA_DISCORD_GUILD_ID', '0118999')
DISCORD_BOT_TOKEN = os.environ.get('AA_DISCORD_BOT_TOKEN', '') DISCORD_BOT_TOKEN = os.environ.get('AA_DISCORD_BOT_TOKEN', 'bottoken')
DISCORD_INVITE_CODE = os.environ.get('AA_DISCORD_INVITE_CODE', '') DISCORD_INVITE_CODE = os.environ.get('AA_DISCORD_INVITE_CODE', 'invitecode')
DISCORD_APP_ID = os.environ.get('AA_DISCORD_APP_ID', '') DISCORD_APP_ID = os.environ.get('AA_DISCORD_APP_ID', 'appid')
DISCORD_APP_SECRET = os.environ.get('AA_DISCORD_APP_SECRET', '') DISCORD_APP_SECRET = os.environ.get('AA_DISCORD_APP_SECRET', 'secret')
DISCORD_CALLBACK_URL = os.environ.get('AA_DISCORD_CALLBACK_URL', 'http://example.com/discord_callback') DISCORD_CALLBACK_URL = os.environ.get('AA_DISCORD_CALLBACK_URL', 'http://example.com/discord/callback')
DISCORD_SYNC_NAMES = 'True' == os.environ.get('AA_DISCORD_SYNC_NAMES', 'False') DISCORD_SYNC_NAMES = 'True' == os.environ.get('AA_DISCORD_SYNC_NAMES', 'False')
###################################### ######################################

View File

@@ -11,7 +11,7 @@ import groupmanagement.views
import optimer.views import optimer.views
import timerboard.views import timerboard.views
import fleetactivitytracking.views import fleetactivitytracking.views
import fleetup.views import fleetup.urls
import srp.views import srp.views
import notifications.views import notifications.views
import hrapplications.views import hrapplications.views
@@ -55,11 +55,14 @@ urlpatterns = [
name='auth_srp_fleet_mark_completed'), name='auth_srp_fleet_mark_completed'),
url(r'^srp_fleet_mark_uncompleted/(\w+)', srp.views.srp_fleet_mark_uncompleted, url(r'^srp_fleet_mark_uncompleted/(\w+)', srp.views.srp_fleet_mark_uncompleted,
name='auth_srp_fleet_mark_uncompleted'), name='auth_srp_fleet_mark_uncompleted'),
url(r'^srp_request_remove/(\w+)', srp.views.srp_request_remove, url(r'^srp_request_remove/', srp.views.srp_request_remove,
name="auth_srp_request_remove"), name="auth_srp_request_remove"),
url(r'srp_request_approve/(\w+)', srp.views.srp_request_approve, url(r'srp_request_approve/', srp.views.srp_request_approve,
name='auth_srp_request_approve'), name='auth_srp_request_approve'),
url(r'srp_request_reject/(\w+)', srp.views.srp_request_reject, name='auth_srp_request_reject'), url(r'srp_request_reject/', srp.views.srp_request_reject,
name='auth_srp_request_reject'),
url(_(r'srp_request_amount_update/(\w+)'), srp.views.srp_request_update_amount,
name="auth_srp_request_update_amount"),
# Notifications # Notifications
url(r'^remove_notifications/(\w+)/$', notifications.views.remove_notification, name='auth_remove_notification'), url(r'^remove_notifications/(\w+)/$', notifications.views.remove_notification, name='auth_remove_notification'),
@@ -72,12 +75,7 @@ urlpatterns = [
urlpatterns += i18n_patterns( urlpatterns += i18n_patterns(
# Fleetup # Fleetup
url(r'^fleetup/$', fleetup.views.fleetup_view, name='auth_fleetup_view'), url(r'^fleetup/', include(fleetup.urls.urlpatterns)),
url(r'^fleetup/fittings/$', fleetup.views.fleetup_fittings, name='auth_fleetup_fittings'),
url(r'^fleetup/fittings/(?P<fittingnumber>[0-9]+)/$', fleetup.views.fleetup_fitting, name='auth_fleetup_fitting'),
url(r'^fleetup/doctrines/$', fleetup.views.fleetup_doctrines, name='auth_fleetup_doctrines'),
url(r'^fleetup/characters/$', fleetup.views.fleetup_characters, name='auth_fleetup_characters'),
url(r'^fleetup/doctrines/(?P<doctrinenumber>[0-9]+)/$', fleetup.views.fleetup_doctrine, name='auth_fleetup_doctrine'),
# Authentication # Authentication
url(_(r'^login_user/'), authentication.views.login_user, name='auth_login_user'), url(_(r'^login_user/'), authentication.views.login_user, name='auth_login_user'),
@@ -179,8 +177,6 @@ urlpatterns += i18n_patterns(
url(_(r'^srp_fleet_add_view/$'), srp.views.srp_fleet_add_view, name='auth_srp_fleet_add_view'), url(_(r'^srp_fleet_add_view/$'), srp.views.srp_fleet_add_view, name='auth_srp_fleet_add_view'),
url(_(r'^srp_fleet_edit/(\w+)$'), srp.views.srp_fleet_edit_view, name='auth_srp_fleet_edit_view'), url(_(r'^srp_fleet_edit/(\w+)$'), srp.views.srp_fleet_edit_view, name='auth_srp_fleet_edit_view'),
url(_(r'^srp_request/(\w+)'), srp.views.srp_request_view, name='auth_srp_request_view'), url(_(r'^srp_request/(\w+)'), srp.views.srp_request_view, name='auth_srp_request_view'),
url(_(r'srp_request_amount_update/(\w+)'), srp.views.srp_request_update_amount_view,
name="auth_srp_request_update_amount_view"),
# Tools # Tools
url(_(r'^tool/fleet_formatter_tool/$'), services.views.fleet_formatter_view, url(_(r'^tool/fleet_formatter_tool/$'), services.views.fleet_formatter_view,
@@ -193,6 +189,9 @@ urlpatterns += i18n_patterns(
# FleetActivityTracking (FAT) # FleetActivityTracking (FAT)
url(r'^fat/$', fleetactivitytracking.views.fatlink_view, name='auth_fatlink_view'), url(r'^fat/$', fleetactivitytracking.views.fatlink_view, name='auth_fatlink_view'),
url(r'^fat/statistics/$', fleetactivitytracking.views.fatlink_statistics_view, name='auth_fatlink_view_statistics'), url(r'^fat/statistics/$', fleetactivitytracking.views.fatlink_statistics_view, name='auth_fatlink_view_statistics'),
url(r'^fat/statistics/corp/(\w+)$', fleetactivitytracking.views.fatlink_statistics_corp_view, name='auth_fatlink_view_statistics_corp'),
url(r'^fat/statistics/corp/(?P<corpid>\w+)/(?P<year>[0-9]+)/(?P<month>[0-9]+)/', fleetactivitytracking.views.fatlink_statistics_corp_view,
name='auth_fatlink_view_statistics_corp_month'),
url(r'^fat/statistics/(?P<year>[0-9]+)/(?P<month>[0-9]+)/$', fleetactivitytracking.views.fatlink_statistics_view, url(r'^fat/statistics/(?P<year>[0-9]+)/(?P<month>[0-9]+)/$', fleetactivitytracking.views.fatlink_statistics_view,
name='auth_fatlink_view_statistics_month'), name='auth_fatlink_view_statistics_month'),
url(r'^fat/user/statistics/$', fleetactivitytracking.views.fatlink_personal_statistics_view, url(r'^fat/user/statistics/$', fleetactivitytracking.views.fatlink_personal_statistics_view,

View File

@@ -2,6 +2,7 @@ from __future__ import unicode_literals
from django import forms from django import forms
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.conf import settings
import re import re
@@ -9,6 +10,10 @@ class LoginForm(forms.Form):
username = forms.CharField(label=_('Username'), max_length=32, required=True) username = forms.CharField(label=_('Username'), max_length=32, required=True)
password = forms.CharField(label=_('Password'), widget=forms.PasswordInput()) password = forms.CharField(label=_('Password'), widget=forms.PasswordInput())
if getattr(settings, 'CAPTCHA_ENABLED', False):
from captcha.fields import ReCaptchaField
captcha = ReCaptchaField()
class RegistrationForm(forms.Form): class RegistrationForm(forms.Form):
username = forms.CharField(label=_('Username'), max_length=30, required=True) username = forms.CharField(label=_('Username'), max_length=30, required=True)
@@ -17,16 +22,16 @@ class RegistrationForm(forms.Form):
email = forms.CharField(label=_('Email'), max_length=254, required=True) email = forms.CharField(label=_('Email'), max_length=254, required=True)
email_again = forms.CharField(label=_('Email Again'), max_length=254, required=True) email_again = forms.CharField(label=_('Email Again'), max_length=254, required=True)
if getattr(settings, 'CAPTCHA_ENABLED', False):
from captcha.fields import ReCaptchaField
captcha = ReCaptchaField()
def clean(self): def clean(self):
if ' ' in self.cleaned_data['username']: if ' ' in self.cleaned_data['username']:
raise forms.ValidationError('Username cannot contain a space') raise forms.ValidationError('Username cannot contain a space')
# We attempt to get the user object if we succeed we know email as been used if User.objects.filter(email=self.cleaned_data['email']).count() >= 1:
try:
User.objects.get(email=self.cleaned_data['email'])
raise forms.ValidationError('Email as already been used') raise forms.ValidationError('Email as already been used')
except User.DoesNotExist:
pass
if not re.match("^\w+$", self.cleaned_data['username']): if not re.match("^\w+$", self.cleaned_data['username']):
raise forms.ValidationError('Username contains illegal characters') raise forms.ValidationError('Username contains illegal characters')

View File

@@ -52,6 +52,7 @@ class CorpStats(models.Model):
# the swagger spec doesn't have a maxItems count # the swagger spec doesn't have a maxItems count
# manual testing says we can do over 350, but let's not risk it # manual testing says we can do over 350, but let's not risk it
member_id_chunks = [member_ids[i:i + 255] for i in range(0, len(member_ids), 255)] member_id_chunks = [member_ids[i:i + 255] for i in range(0, len(member_ids), 255)]
c = self.token.get_esi_client(Character='v1') # ccplease bump versions of whole resources
member_name_chunks = [c.Character.get_characters_names(character_ids=id_chunk).result() for id_chunk in member_name_chunks = [c.Character.get_characters_names(character_ids=id_chunk).result() for id_chunk in
member_id_chunks] member_id_chunks]
member_list = {} member_list = {}
@@ -163,7 +164,10 @@ class CorpStats(models.Model):
def get_member_objects(self, user): def get_member_objects(self, user):
show_apis = self.show_apis(user) show_apis = self.show_apis(user)
return sorted([CorpStats.MemberObject(id, name, show_apis=show_apis) for id, name in self.members.items()], key=attrgetter('main_user', 'character_name')) member_list = [CorpStats.MemberObject(id, name, show_apis=show_apis) for id, name in self.members.items()]
outlist = sorted([m for m in member_list if m.main_user], key=attrgetter('main_user', 'character_name'))
outlist = outlist + sorted([m for m in member_list if not m.main_user], key=attrgetter('character_name'))
return outlist
def can_update(self, user): def can_update(self, user):
return user.is_superuser or user == self.token.user return user.is_superuser or user == self.token.user

View File

@@ -1,15 +1,14 @@
from corputils.models import CorpStats from corputils.models import CorpStats
from celery.task import task, periodic_task from alliance_auth.celeryapp import app
from celery.task.schedules import crontab
@task @app.task
def update_corpstats(pk): def update_corpstats(pk):
cs = CorpStats.objects.get(pk=pk) cs = CorpStats.objects.get(pk=pk)
cs.update() cs.update()
@periodic_task(run_every=crontab(minute=0, hour="*/6")) @app.task
def update_all_corpstats(): def update_all_corpstats():
for cs in CorpStats.objects.all(): for cs in CorpStats.objects.all():
update_corpstats.delay(cs.pk) update_corpstats.delay(cs.pk)

View File

@@ -25,7 +25,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-lg-12"> <div class="col-lg-12">
<b>{% trans "API Index: " %}</b> {{ corpstats.total_users }} Main Characters <b>{% trans "API Index: " %}</b> {{ corpstats.total_users }} Main Character{{ corpstats.total_users|pluralize }}
<div class="progress"> <div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="{{ corpstats.registered_members }}" aria-valuemin="0" aria-valuemax="{{ corpstats.total_members }}" style="width: {% widthratio corpstats.registered_members corpstats.total_members 100 %}%;"> <div class="progress-bar" role="progressbar" aria-valuenow="{{ corpstats.registered_members }}" aria-valuemin="0" aria-valuemax="{{ corpstats.total_members }}" style="width: {% widthratio corpstats.registered_members corpstats.total_members 100 %}%;">
{{ corpstats.registered_members }}/{{ corpstats.total_members }} {{ corpstats.registered_members }}/{{ corpstats.total_members }}

View File

@@ -46,7 +46,10 @@ def corpstats_add(request, token):
'corporation_id'] 'corporation_id']
corp = EveCorporationInfo.objects.get(corporation_id=corp_id) corp = EveCorporationInfo.objects.get(corporation_id=corp_id)
cs = CorpStats.objects.create(token=token, corp=corp) cs = CorpStats.objects.create(token=token, corp=corp)
cs.update() try:
cs.update()
except HTTPError as e:
messages.error(request, str(e))
assert cs.pk # ensure update was successful assert cs.pk # ensure update was successful
if CorpStats.objects.filter(pk=cs.pk).visible_to(request.user).exists(): if CorpStats.objects.filter(pk=cs.pk).visible_to(request.user).exists():
return redirect('corputils:view_corp', corp_id=corp.corporation_id) return redirect('corputils:view_corp', corp_id=corp.corporation_id)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -6,17 +6,17 @@ This module is used to check the registration status of corp members and to dete
Upon initial install, nothing will be visible. For every corp, a model will have to be created before data can be viewed. Upon initial install, nothing will be visible. For every corp, a model will have to be created before data can be viewed.
![nothing is visible](http://i.imgur.com/va3DyT6.png) ![nothing is visible](/_static/images/features/corpstats/blank_header.png)
If you are a superuser, the add button will be immediate visible to you. If not, your user account requires the `add_corpstats` permission. If you are a superuser, the add button will be immediate visible to you. If not, your user account requires the `add_corpstats` permission.
Corp Stats requires an EVE SSO token to access data from the EVE Swagger Interface. Upon pressing the Add button, you will be prompted to authenticated. Please select the character who is in the corp you want data for. Corp Stats requires an EVE SSO token to access data from the EVE Swagger Interface. Upon pressing the Add button, you will be prompted to authenticated. Please select the character who is in the corp you want data for.
![authorize from the EVE site](http://i.imgur.com/OnyoOAZ.png) ![authorize from the EVE site](/_static/images/features/corpstats/eve_sso_authorization.png)
You will return to auth where you are asked to select a token with the green arrow button. If you want to use a different character, press the `LOG IN with EVE Online` button. You will return to auth where you are asked to select a token with the green arrow button. If you want to use a different character, press the `LOG IN with EVE Online` button.
![select an SSO token to create with](http://i.imgur.com/KdA0XH0.png) ![select an SSO token to create with](/_static/images/features/corpstats/select_sso_token.png)
If this works (and you have permission to view the Corp Stats you just created) you'll be returned to a view of the Corp Stats. If this works (and you have permission to view the Corp Stats you just created) you'll be returned to a view of the Corp Stats.
If it fails an error message will be displayed. If it fails an error message will be displayed.
@@ -25,7 +25,7 @@ If it fails an error message will be displayed.
### Navigation Bar ### Navigation Bar
![navigation bar](http://i.imgur.com/2l9gbml.png) ![navigation bar](/_static/images/features/corpstats/navbar.png)
This bar contains a dropdown menu of all available corps. If the user has the `add_corpstats` permission, a button to add a Corp Stats will be shown. This bar contains a dropdown menu of all available corps. If the user has the `add_corpstats` permission, a button to add a Corp Stats will be shown.
@@ -33,13 +33,13 @@ On the right of this bar is a search field. Press enter to search. It checks all
### API Index ### API Index
![API Index](http://i.imgur.com/P1U2WJ2.png) ![API Index](/_static/images/features/corpstats/api_index.png)
This is a visual indication of the number of registered characters. This is a visual indication of the number of registered characters.
### Last Update ### Last Update
![last update and update button](http://i.imgur.com/yHbueGK.png) ![last update and update button](/_static/images/features/corpstats/last_update.png)
Corp Stats do not automatically update. They update once upon creation for initial data, and whenever someone presses the update button. Corp Stats do not automatically update. They update once upon creation for initial data, and whenever someone presses the update button.
@@ -47,7 +47,7 @@ Only superusers and the creator of the Corp Stat can update it.
### Member List ### Member List
![member list](http://i.imgur.com/udEVoSh.png) ![member list](/_static/images/features/corpstats/member_list.png)
The list contains all characters in the corp. Red backgrounds means they are not registered in auth. If registered, and the user has the required permission to view APIs, a link to JackKnife will be present. The list contains all characters in the corp. Red backgrounds means they are not registered in auth. If registered, and the user has the required permission to view APIs, a link to JackKnife will be present.
A link to zKillboard is present for all characters. A link to zKillboard is present for all characters.
@@ -55,11 +55,11 @@ If registered, the character will also have a main character, main corporation,
This view is paginated: use the navigation arrows to view more pages (sorted alphabetically by character name), or search for a specific character. This view is paginated: use the navigation arrows to view more pages (sorted alphabetically by character name), or search for a specific character.
![pagination buttons](http://i.imgur.com/otcPGsU.png) ![pagination buttons](/_static/images/features/corpstats/pagination.png)
## Search View ## Search View
![search results](http://i.imgur.com/7wf0Q2C.png) ![search results](/_static/images/features/corpstats/search_view.png)
This view is essentially the same as the Corp Stats page, but not specific to a single corp. This view is essentially the same as the Corp Stats page, but not specific to a single corp.
The search query is visible in the search box. The search query is visible in the search box.

View File

@@ -5,6 +5,8 @@ AllianceAuth gets served using a Web Server Gateway Interface (WSGI) script. Thi
In the interest of ~~laziness~~ time-efficiency, scroll down for example configs. Use these, changing the ServerName to your domain name. In the interest of ~~laziness~~ time-efficiency, scroll down for example configs. Use these, changing the ServerName to your domain name.
If you're using a small VPS to host services with very limited memory resources, consider using NGINX with [Gunicorn](gunicorn.md). Even if you would like to use Apache, Gunicorn may give you lower memory usage over mod_wsgi.
### Required Parameters for AllianceAuth Core ### Required Parameters for AllianceAuth Core
The AllianceAuth core requires the following parameters to be set: The AllianceAuth core requires the following parameters to be set:
@@ -52,6 +54,31 @@ You can supply your own SSL certificates if you so desire. The alternative is ru
## Sample Config Files ## Sample Config Files
### Minimally functional config
```
<VirtualHost *:80>
ServerName example.com
ServerAdmin webmaster@localhost
DocumentRoot /var/www
WSGIDaemonProcess allianceauth python-path=/home/allianceserver/allianceauth
WSGIProcessGroup allianceauth
WSGIScriptAlias / /home/allianceserver/allianceauth/alliance_auth/wsgi.py
Alias /static/ /home/allianceserver/allianceauth/static/
<Directory /home/allianceserver/allianceauth/>
Require all granted
</Directory>
<Directory /var/www/>
Require all granted
</Directory>
</VirtualHost>
```
### Own SSL Cert ### Own SSL Cert
- Apache 2.4 or newer: - Apache 2.4 or newer:
- [000-default.conf](http://pastebin.com/3LLzyNmV) - [000-default.conf](http://pastebin.com/3LLzyNmV)

View File

@@ -0,0 +1,121 @@
# Gunicorn
[Gunicorn](http://gunicorn.org) is a Python WSGI HTTP Server for UNIX. The Gunicorn server is light on server resources, and fairly speedy.
If you find Apache's `mod_wsgi` to be a headache or want to use NGINX (or some other webserver), then Gunicorn could be for you. There are a number of other WSGI server options out there and this documentation should be enough for you to piece together how to get them working with your environment.
Check out the full [Gunicorn docs](http://docs.gunicorn.org/en/latest/index.html).
## Setting up Gunicorn
```eval_rst
.. note::
If you're using a virtual environment (and I would encourage you to do so when hosting Alliance Auth), activate it now. `source /path/to/venv/bin/activate`.
```
Install Gunicorn using pip, `pip install gunicorn`.
In your `allianceauth` base directory, try running `gunicorn --bind 0.0.0.0:8000 alliance_auth.wsgi`. You should be able to browse to http://yourserver:8000 and see your Alliance Auth installation running. Images and styling will be missing, but dont worry, your web server will provide them.
Once you validate its running, you can kill the process with Ctrl+C and continue.
## Running Gunicorn with Supervisor
You should use [Supervisor](supervisor.md) to keep all of Alliance Auth components running (instead of using screen). You don't _have to_ but we will be using it to start and run Gunicorn so you might as well.
### Sample Supervisor config
You'll want to edit `/etc/supervisor/conf.d/aauth_gunicorn.conf` (or whatever you want to call the config file)
```
[program:aauth-gunicorn]
user = www-data
directory=/home/allianceserver/allianceauth/
command=gunicorn alliance_auth.wsgi --workers=3 --timeout 120
autostart=true
autorestart=true
stopsignal=INT
```
- `[program:aauth-gunicorn]` - Change aauth-gunicorn to whatever you wish to call your process in Supervisor.
- `user = www-data` - Change to whatever user you wish Gunicorn to run as. You could even set this as allianceserver if you wished. I'll leave the question security of that up to you.
- `directory=/home/allianceserver/allianceauth/` - Needs to be the path to your Alliance Auth install.
- `command=gunicorn alliance_auth.wsgi --workers=3 --timeout 120` - Running Gunicorn and the options to launch with. This is where you have some decisions to make, we'll continue below.
#### Gunicorn Arguments
See the [Commonly Used Arguments](http://docs.gunicorn.org/en/latest/run.html#commonly-used-arguments) or [Full list of settings](http://docs.gunicorn.org/en/stable/settings.html) for more information.
##### Where to bind Gunicorn to?
What address are you going to use to reference it? By default, without a bind parameter, Gunicorn will bind to `127.0.0.1:8000`. This might be fine for your application. If it clashes with another application running on that port you will need to change it. I would suggest using UNIX sockets too, if you can.
For UNIX sockets add `--bind=unix:/run/allianceauth.sock` (or to a path you wish to use). Remember that your web server will need to be able to access this socket file.
For a TCP address add `--bind=127.0.0.1:8001` (or to the address/port you wish to use, but I would strongly advise against binding it to an external address).
Whatever you decide to use, remember it because we'll need it when configuring your webserver.
##### Number of workers
By default Gunicorn will spawn only one worker. The number you set this to will depend on your own server environment, how many visitors you have etc. Gunicorn suggests between 2-4 workers per core. Really you could probably get away with 2-4 in total for most installs.
Change it by adding `--workers=2` to the command.
##### Running with a virtual environment
If you're running with a virtual environment, you'll need to add the path to the `command=` config line.
e.g. `command=/path/to/venv/bin/gunicorn alliance_auth.wsgi`
### Starting via Supervisor
Once you have your configuration all sorted, you will need to reload your supervisor config `sudo service supervisor reload` and then you can start the Gunicorn server via `sudo supervisorctl start aauth-gunicorn` (or whatever you renamed it to). You should see something like the following `aauth-gunicorn: started`. If you get some other message, you'll need to consult the Supervisor log files, usually found in `/var/log/supervisor/`.
## Configuring your webserver
### NGINX
To your server config add:
```
location / {
proxy_pass http://127.0.0.1:8000;
proxy_read_timeout 90;
proxy_redirect http://127.0.0.1:8000/ http://$host/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
```
Set `proxy_pass` and `proxy_redirect` to the address you set under `--bind=`. Set the second part of `proxy_redirect` to the URL you're hosting services on. Tell NGINX to reload your config, job done. Enjoy your lower memory usage and better performance!
If PHP is stopping you moving to NGINX, check out php-fpm as a way to run your PHP applications.
### Apache
If you were using mod_wsgi before, make a backup of your old config first and then strip out all of the mod_wsgi config from your Apache VirtualHost first config.
Your config will need something along these lines:
```
ProxyPreserveHost On
<Location />
SSLRequireSSL
ProxyPass http://127.0.0.1:8000/
ProxyPassReverse http://127.0.0.1:8000/
RequestHeader set X-FORWARDED-PROTOCOL ssl
RequestHeader set X-FORWARDED-SSL on
</Location>
```
Set `ProxyPass` and `ProxyPassReverse` addresses to your `--bind=` address set earlier.
You will need to enable some Apache mods. `sudo a2enmod http_proxy` should take care of the dependencies.
Restart Apache and you should be done.
### Other web servers
Any web server capable of proxy passing should be able to sit in front of Gunicorn. Consult their documentation armed with your `--bind=` address and you should be able to find how to do it relatively easy.
## Restarting Gunicorn
In the past when you made changes you restarted the entire Apache server. This is no longer required. When you update or make configuration changes that ask you to restart Apache, instead you can just restart Gunicorn:
`sudo supervisorctl restart aauth-gunicorn`, or the service name you chose for it.

View File

@@ -7,7 +7,9 @@
ubuntu ubuntu
centos centos
settings settings
nginx
apache apache
gunicorn
cloudflare cloudflare
supervisor supervisor
quickstart quickstart

View File

@@ -0,0 +1,93 @@
# NGINX
## Overivew
Nginx (engine x) is a HTTP server known for its high performance, stability, simple configuration, and low resource consumption. Unlike traditional servers (i.e. Apache), Nginx doesn't rely on threads to serve requests, rather using an asynchronous event driven approach which permits predictable resource usage and performance under load.
If you're trying to cram Alliance Auth into a very small VPS of say, 1-2GB or less, then Nginx will be considerably friendlier to your resources compared to Apache.
You can read more about NGINX on the [NGINX wiki](https://www.nginx.com/resources/wiki/).
## Coming from Apache
If you're converting from Apache, here are some things to consider.
Nginx is lightweight for a reason. It doesn't try to do everything internally and instead concentrates on just being a good HTTP server. This means that, unlike Apache, it wont automatically run PHP scripts via mod_php and doesn't have an internal WSGI server like mod_wsgi. That doesn't mean that it can't, just that it relies on external processes to run these instead. This might be good or bad depending on your outlook. It's good because it allows you to segment your applications, restarting Alliance Auth wont impact your PHP applications. On the other hand it means more config and more management of services. For some people it will be worth it, for others losing the centralised nature of Apache may not be worth it.
```eval_rst
+-----------+----------------------------------------+
| Apache | Nginx Replacement |
+===========+========================================+
| mod_php | php5-fpm or php7-fpm (PHP FastCGI) |
+-----------+----------------------------------------+
| mod_wsgi | Gunicorn or other external WSGI server |
+-----------+----------------------------------------+
```
Your .htaccess files wont work. Nginx has a separate way of managing access to folders via the server config. Everything you can do with htaccess files you can do with Nginx config. [Read more on the Nginx wiki](https://www.nginx.com/resources/wiki/start/topics/examples/likeapache-htaccess/)
## Setting up Nginx
Install Nginx via your preferred package manager or other method. If you need help just search, there are plenty of guides on installing Nginx out there.
You will need to have [Gunicorn](gunicorn.md) or some other WSGI server setup for hosting Alliance Auth.
Create a config file in `/etc/nginx/sites-available` call it `alliance-auth.conf` or whatever your preferred name is and copy the basic config in. Make whatever changes you feel are necessary.
Create a symbolic link to enable the site `sudo ln -s /etc/nginx/sites-available/alliance-auth.conf /etc/nginx/sites-enabled/` and then reload Nginx for the config to take effect, `sudo service nginx reload` for Ubuntu.
### Basic config
```
server {
listen 80;
server_name example.com;
location = /favicon.ico { access_log off; log_not_found off; }
location /static/ {
alias /home/allianceserver/allianceauth/static/;
autoindex off;
}
# Gunicorn config goes below
location / {
include proxy_params;
proxy_pass http://127.0.0.1:8000;
}
}
```
#### Adding TLS/SSL
With [Let's Encrypt](https://letsencrypt.org/) offering free SSL certificates, there's no good reason to not run HTTPS anymore.
Your config will need a few additions once you've got your certificate.
```
listen 443 ssl http2; # Replace listen 80; with this
ssl_certificate /path/to/your/cert.crt;
ssl_certificate_key /path/to/your/cert.key;
ssl on;
ssl_session_cache builtin:1000 shared:SSL:10m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:EECDH+ECDSA+SHA384:EECDH+ECDSA+SHA256:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH+aRSA+RC4:EECDH:EDH+aRSA:RC4:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS;
ssl_prefer_server_ciphers on;
```
If you want to redirect all your non-SSL visitors to your secure site, below your main configs `server` block, add the following:
```
server {
listen 80;
server_name example.com;
# Redirect all HTTP requests to HTTPS with a 301 Moved Permanently response.
return 301 https://$host$request_uri;
}
```
If you have trouble with the `ssl_ciphers` listed here or some other part of the SSL config, try getting the values from [Mozilla's SSL Config Generator](https://mozilla.github.io/server-side-tls/ssl-config-generator/).

View File

@@ -10,5 +10,5 @@ The big goal of AllianceAuth is the automation of group membership, so well n
To start the background processes to sync groups and check api keys, issue these commands: To start the background processes to sync groups and check api keys, issue these commands:
screen -dm bash -c 'python manage.py celeryd' screen -dm bash -c 'celery -A alliance_auth worker'
screen -dm bash -c 'python manage.py celerybeat' screen -dm bash -c 'celery -A alliance_auth beat'

View File

@@ -68,6 +68,7 @@ If using Openfire, the following need to be set in accordance with the [install
- [BROADCAST_USER](#broadcast-user) - [BROADCAST_USER](#broadcast-user)
- [BROADCAST_USER_PASSWORD](#broadcast-user-password) - [BROADCAST_USER_PASSWORD](#broadcast-user-password)
- [BROADCAST_SERVICE_NAME](#broadcast-service-name) - [BROADCAST_SERVICE_NAME](#broadcast-service-name)
- [BROADCAST_IGNORE_INVALID_CERT](#broadcast-ignore-invalid-cert)
### Mumble ### Mumble
If using Mumble, the following needs to be set to the address of the mumble server: If using Mumble, the following needs to be set to the address of the mumble server:
@@ -136,6 +137,13 @@ Fittings and operations can be imported from Fleet-Up. Define the following to d
- [FLEETUP_API_ID](#fleetup-api-id) - [FLEETUP_API_ID](#fleetup-api-id)
- [FLEETUP_GROUP_ID](#fleetup-group-id) - [FLEETUP_GROUP_ID](#fleetup-group-id)
### CAPTCHA
To help prevent bots from registering and brute forcing the login. Get the reCaptcha keys from [here](https://www.google.com/recaptcha/intro/index.html)
- [CAPTCHA_ENABLED](#captcha_enabled)
- [RECAPTCHA_PUBLIC_KEY](#recaptcha_public_key)
- [RECAPTCHA_PRIVATE_KEY](#recaptcha_private_key)
- [NOCAPTCHA](#nocaptcha)
# Description of Settings # Description of Settings
## Django ## Django
### SECRET_KEY ### SECRET_KEY
@@ -150,6 +158,14 @@ List of databases available. Contains the Django database, and may include servi
Friendly name of the local language. Friendly name of the local language.
### TIME_ZONE ### TIME_ZONE
Friendly name of the local timezone. Friendly name of the local timezone.
### CAPTCHA_ENABLED
Enable Google reCaptcha
### RECAPTCHA_PUBLIC_KEY
Google reCaptcha public key
### RECAPTCHA_PRIVATE_KEY
Google reCaptcha private key
### NOCAPTCHA
Enable New No Captcha reCaptcha
### STATIC_URL ### STATIC_URL
Absolute URL to serve static files from. Absolute URL to serve static files from.
### STATIC_ROOT ### STATIC_ROOT
@@ -199,52 +215,6 @@ If `True`, add members to groups with their alliance name, prefixed with `Allian
If `True`, add blues to groups with their corp name, prefixed with `Corp_` If `True`, add blues to groups with their corp name, prefixed with `Corp_`
### BLUE_ALLIANCE_GROUPS ### BLUE_ALLIANCE_GROUPS
If `True`, add blues to groups with their alliance name, prefixed with `Alliance_` If `True`, add blues to groups with their alliance name, prefixed with `Alliance_`
## Alliance Service Setup
### ENABLE_AUTH_FORUM
Allow members of the owning corp or alliance to generate accounts on a Phpbb3 install.
### ENABLE_AUTH_JABBER
Allow members of the owning corp or alliance to generate accounts on an Openfire install.
### ENABLE_AUTH_MUMBLE
Allow members of the owning corp or alliance to generate accounts on a Mumble install.
### ENABLE_AUTH_IPBOARD
Allow members of the owning corp or alliance to generate accounts on an IPBoard install.
### ENABLE_AUTH_TEAMSPEAK3
Allow members of the owning corp or alliance to generate accounts on a Teamspeak3 install.
### ENABLE_AUTH_DISCORD
Allow members of the owning corp or alliance to link accounts to a Discord server.
### ENABLE_AUTH_DISCOURSE
Allow members of the owning corp or alliance to generate accounts on a Discourse install
### ENABLE_AUTH_IPS4
Allow members of the owning corp or alliance to generate accounts on a IPSuite4 install.
### ENABLE_AUTH_SMF
Allow members of the owning corp or alliance to generate accounts on a SMF install.
### ENABLE_AUTH_MARKET
Allow members of the owning corp or alliance to generate accounts on an alliance market install.
### ENABLE_AUTH_XENFORO
Allow members of the owning corp or alliance to generate accounts on a XenForo install.
## Blue Service Setup
### ENABLE_BLUE_FORUM
Allow blues of the owning corp or alliance to generate accounts on a Phpbb3 install.
### ENABLE_BLUE_JABBER
Allow blues of the owning corp or alliance to generate accounts on an Openfire install.
### ENABLE_BLUE_MUMBLE
Allow blues of the owning corp or alliance to generate accounts on a Mumble install.
### ENABLE_BLUE_IPBOARD
Allow blues of the owning corp or alliance to generate accounts on an IPBoard install.
### ENABLE_BLUE_TEAMSPEAK3
Allow blues of the owning corp or alliance to generate accounts on a Teamspeak3 install.
### ENABLE_BLUE_DISCORD
Allow blues of the owning corp or alliance to link accounts to a Discord server.
### ENABLE_BLUE_DISCOURSE
Allow blues of the owning corp or alliance to generate accounts on a Discourse install.
### ENABLE_BLUE_IPS4
Allow blues of the owning corp or alliance to generate accounts on an IPSuite4 install.
### ENABLE_BLUE_SMF
Allow blues of the owning corp or alliance to generate accounts on a SMF install.
### ENABLE_BLUE_MARKET
Allow blues of the owning corp or alliance to generate accounts on an alliance market install.
### ENABLE_BLUE_XENFORO
Allow blues of the owning corp or alliance to generate accounts on a XenForo install.
## Tenant Configuration ## Tenant Configuration
Characters of any corp or alliance with their ID here will be treated as a member. Characters of any corp or alliance with their ID here will be treated as a member.
### CORP_IDS ### CORP_IDS

View File

@@ -15,12 +15,12 @@ Ubuntu:
CentOS: CentOS:
sudo yum install supervisor sudo yum install supervisor
sudo systemctl enable supervisor.service sudo systemctl enable supervisord.service
sudo systemctl start supervisor.service sudo systemctl start supervisord.service
## Configuration ## Configuration
Auth provides example config files for the celery workers (celeryd), the periodic task scheduler (celerybeat), and the mumble authenticator. All of these are available in `thirdparty/Supervisor`. Auth provides example config files for the celery workers, the periodic task scheduler (celery beat), and the mumble authenticator. All of these are available in `thirdparty/Supervisor`.
For most users, all you have to do is copy the config files to `/etc/supervisor/conf.d` then restart the service. Copy `auth-celerybeat.conf` and `auth-celeryd.conf` for the celery workers, and `auth-mumble.conf` for the mumble authenticator. For all three just use a wildcard: For most users, all you have to do is copy the config files to `/etc/supervisor/conf.d` then restart the service. Copy `auth-celerybeat.conf` and `auth-celeryd.conf` for the celery workers, and `auth-mumble.conf` for the mumble authenticator. For all three just use a wildcard:
@@ -41,15 +41,15 @@ To ensure the processes are working, check their status:
sudo supervisorctl status sudo supervisorctl status
Processes will be `STARTING`, `RUNNING`, or `ERROR`. If an error has occurred, check their log files: Processes will be `STARTING`, `RUNNING`, or `ERROR`. If an error has occurred, check their log files:
- celeryd: `log/worker.log` - celery workers: `log/worker.log`
- celerybeat: `log/beat.log` - celery beat: `log/beat.log`
- authenticator: `log/authenticator.log` - authenticator: `log/authenticator.log`
## Customizing Config Files ## Customizing Config Files
The only real customization needed is if running in a virtual environment. The python path will have to be changed in order to start in the venv. The only real customization needed is if running in a virtual environment. The python path will have to be changed in order to start in the venv.
Edit the config files and find the line saying `command`. Replace `python` with `/path/to/venv/python`. This can be relative to the `directory` specified in the config file. Edit the config files and find the line saying `command`. Replace `python` with `/path/to/venv/bin/python`. For Celery replace `celery` with `/path/to/venv/bin/celery`. This can be relative to the `directory` specified in the config file.
Note that for config changes to be loaded, the supervisor service must be restarted. Note that for config changes to be loaded, the supervisor service must be restarted.

View File

@@ -68,4 +68,4 @@ Enter your database password and press Check. If all the boxes come back green p
## Update Auth Settings ## Update Auth Settings
Edit your aut settings file (`nano ~/allianceauth/alliance_auth/settings.py`) and replace `API_KEY_AUDIT_URL` with either `jacknife.example.com/?usid={api_id}&apik={vcode}` or `example.com/jacknife/?usid={api_id}&apik={vcode}` depending on your apache choice. Edit your aut settings file (`nano ~/allianceauth/alliance_auth/settings.py`) and replace `API_KEY_AUDIT_URL` with either `http://jacknife.example.com/?usid={api_id}&apik={vcode}` or `http://example.com/jacknife/?usid={api_id}&apik={vcode}` depending on your apache choice.

View File

@@ -83,6 +83,8 @@ Navigate to the `Server` tab, `Server Manager` subtab, and select `System Proper
- Value: `broadcast@example.com`, replacing the domain name with yours - Value: `broadcast@example.com`, replacing the domain name with yours
- Do not encrypt this property value - Do not encrypt this property value
If you have troubles getting broadcasts to work, you can try setting the optional (you will need to add it) `BROADCAST_IGNORE_INVALID_CERT` setting to `True`. This will allow invalid certificates to be used when connecting to the Openfire server to send a broadcast.
### Group Chat ### Group Chat
Channels are available which function like a chat room. Access can be controlled either by password or ACL (not unlike mumble). Channels are available which function like a chat room. Access can be controlled either by password or ACL (not unlike mumble).

View File

@@ -84,7 +84,7 @@ To enable advanced permissions, on your client go to the `Tools` menu, `Applicat
### TS group models not populating on admin site ### TS group models not populating on admin site
The method which populates these runs every 30 minutes. To populate manually, start a celery shell: The method which populates these runs every 30 minutes. To populate manually, start a celery shell:
python manage.py celery shell celery -A alliance_auth shell
And execute the update: And execute the update:

View File

@@ -19,7 +19,7 @@ Either you need to `sudo` that command, or it's a missing dependency. Check [the
### I'm getting an error 500 trying to connect to the website on a new install ### I'm getting an error 500 trying to connect to the website on a new install
Read the apache error log: `sudo nano /var/log/apache2/error.log` Read the apache error log: `sudo less /var/log/apache2/error.log`. Press Shift+G to go to the end of the file.
If it talks about failing to import something, google its name and install it. If it talks about failing to import something, google its name and install it.
@@ -36,13 +36,9 @@ Make sure the background processes are running: `ps aux | grep celery` should re
If that doesn't do it, try clearing the worker queue. First kill all celery processes as described above, then do the following: If that doesn't do it, try clearing the worker queue. First kill all celery processes as described above, then do the following:
redis-cli FLUSHALL redis-cli FLUSHALL
python manage.py celeryd --purge celery -A alliance_auth worker --purge
Press control+C once. Press Control+C once.
python manage.py celeryd --discard
Press control+C once.
Now start celery again with [these background process commands.](../installation/auth/quickstart.md) Now start celery again with [these background process commands.](../installation/auth/quickstart.md)

View File

@@ -264,7 +264,7 @@ class EveSwaggerProvider(EveProvider):
) )
return model return model
except HTTPNotFound: except HTTPNotFound:
raise ObjectNotFound(id, 'corporation') raise ObjectNotFound(corp_id, 'corporation')
def get_character(self, character_id): def get_character(self, character_id):
try: try:
@@ -330,9 +330,9 @@ class EveXmlProvider(EveProvider):
self.adapter, self.adapter,
id, id,
corpinfo['name'], corpinfo['name'],
corpinfo['ticker'],
corpinfo['ceo']['id'], corpinfo['ceo']['id'],
corpinfo['members']['current'], corpinfo['members']['current'],
corpinfo['ticker'],
corpinfo['alliance']['id'] if corpinfo['alliance'] else None, corpinfo['alliance']['id'] if corpinfo['alliance'] else None,
) )
return model return model

View File

@@ -1,10 +1,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from celery.task import periodic_task
from django.contrib.auth.models import User from django.contrib.auth.models import User
from notifications import notify from notifications import notify
from celery import task from celery import task
from celery.task.schedules import crontab
from authentication.models import AuthServicesInfo from authentication.models import AuthServicesInfo
from eveonline.managers import EveManager from eveonline.managers import EveManager
from eveonline.models import EveApiKeyPair from eveonline.models import EveApiKeyPair
@@ -17,10 +15,12 @@ from authentication.tasks import set_state
import logging import logging
import evelink import evelink
from alliance_auth.celeryapp import app
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@task @app.task
def refresh_api(api): def refresh_api(api):
logger.debug('Running update on api key %s' % api.api_id) logger.debug('Running update on api key %s' % api.api_id)
still_valid = True still_valid = True
@@ -70,12 +70,12 @@ def refresh_api(api):
level="danger") level="danger")
@task @app.task
def refresh_user_apis(user): def refresh_user_apis(user):
logger.debug('Refreshing all APIs belonging to user %s' % user) logger.debug('Refreshing all APIs belonging to user %s' % user)
apis = EveApiKeyPair.objects.filter(user=user) apis = EveApiKeyPair.objects.filter(user=user)
for x in apis: for x in apis:
refresh_api(x) refresh_api.apply(args=(x,))
# Check our main character # Check our main character
auth = AuthServicesInfo.objects.get(user=user) auth = AuthServicesInfo.objects.get(user=user)
if auth.main_char_id: if auth.main_char_id:
@@ -91,7 +91,7 @@ def refresh_user_apis(user):
set_state(user) set_state(user)
@periodic_task(run_every=crontab(minute=0, hour="*/3")) @app.task
def run_api_refresh(): def run_api_refresh():
if not EveApiManager.check_if_api_server_online(): if not EveApiManager.check_if_api_server_online():
logger.warn("Aborted scheduled API key refresh: API server unreachable") logger.warn("Aborted scheduled API key refresh: API server unreachable")
@@ -101,18 +101,18 @@ def run_api_refresh():
refresh_user_apis.delay(u) refresh_user_apis.delay(u)
@task @app.task
def update_corp(id, is_blue=None): def update_corp(id, is_blue=None):
EveManager.update_corporation(id, is_blue=is_blue) EveManager.update_corporation(id, is_blue=is_blue)
@task @app.task
def update_alliance(id, is_blue=None): def update_alliance(id, is_blue=None):
EveManager.update_alliance(id, is_blue=is_blue) EveManager.update_alliance(id, is_blue=is_blue)
EveManager.populate_alliance(id) EveManager.populate_alliance(id)
@periodic_task(run_every=crontab(minute=0, hour="*/2")) @app.task
def run_corp_update(): def run_corp_update():
if not EveApiManager.check_if_api_server_online(): if not EveApiManager.check_if_api_server_online():
logger.warn("Aborted updating corp and alliance models: API server unreachable") logger.warn("Aborted updating corp and alliance models: API server unreachable")
@@ -123,7 +123,7 @@ def run_corp_update():
is_blue = True if corp_id in settings.STR_BLUE_CORP_IDS else False is_blue = True if corp_id in settings.STR_BLUE_CORP_IDS else False
try: try:
if EveCorporationInfo.objects.filter(corporation_id=corp_id).exists(): if EveCorporationInfo.objects.filter(corporation_id=corp_id).exists():
update_corp(corp_id, is_blue=is_blue) update_corp.apply(args=(corp_id,), kwargs={'is_blue': is_blue})
else: else:
EveManager.create_corporation(corp_id, is_blue=is_blue) EveManager.create_corporation(corp_id, is_blue=is_blue)
except ObjectNotFound: except ObjectNotFound:

View File

@@ -131,7 +131,7 @@ def api_key_removal(request, api_id):
messages.success(request, _('Deleted API key %(apiid)s') % {"apiid": api_id}) messages.success(request, _('Deleted API key %(apiid)s') % {"apiid": api_id})
logger.info("Succesfully processed api delete request by user %s for api %s" % (request.user, api_id)) logger.info("Succesfully processed api delete request by user %s for api %s" % (request.user, api_id))
if not EveCharacter.objects.filter(character_id=authinfo.main_char_id).exists(): if not EveCharacter.objects.filter(character_id=authinfo.main_char_id).exists():
authinfo.main_char_id = None authinfo.main_char_id = ''
authinfo.save() authinfo.save()
set_state(request.user) set_state(request.user)
return redirect("auth_dashboard") return redirect("auth_dashboard")
@@ -164,7 +164,7 @@ def user_refresh_api(request, api_id):
if EveApiKeyPair.objects.filter(api_id=api_id).exists(): if EveApiKeyPair.objects.filter(api_id=api_id).exists():
api_key_pair = EveApiKeyPair.objects.get(api_id=api_id) api_key_pair = EveApiKeyPair.objects.get(api_id=api_id)
if api_key_pair.user == request.user: if api_key_pair.user == request.user:
refresh_api(api_key_pair) refresh_api.apply(args=(api_key_pair,))
messages.success(request, _('Refreshed API key %(apiid)s') % {"apiid": api_id}) messages.success(request, _('Refreshed API key %(apiid)s') % {"apiid": api_id})
set_state(request.user) set_state(request.user)
else: else:

View File

@@ -6,8 +6,6 @@
{% block title %}Alliance Auth - Fatlink Create{% endblock %} {% block title %}Alliance Auth - Fatlink Create{% endblock %}
{% block page_title %}{% trans "Create Fatlink" %}{% endblock page_title %} {% block page_title %}{% trans "Create Fatlink" %}{% endblock page_title %}
{% block extra_css %}
<link href="{% static 'css/jquery.datetimepicker.css' %}" rel="stylesheet" type="text/css">{% endblock extra_css %}
{% block content %} {% block content %}
<div class="col-lg-12"> <div class="col-lg-12">

View File

@@ -0,0 +1,48 @@
{% extends "public/base.html" %}
{% load bootstrap %}
{% load staticfiles %}
{% load i18n %}
{% block title %}Alliance Auth{% endblock %}
{% block page_title %}{% trans "Fatlink Corp Statistics" %}{% endblock page_title %}
{% block content %}
<div class="col-lg-12">
<h1 class="page-header text-center">{% blocktrans %}Participation data statistics for {{ month }}, {{ year }}{% endblocktrans %}
<div class="text-right">
<a href="{% url 'auth_fatlink_view_statistics_corp_month' corpid previous_month|date:"Y" previous_month|date:"m" %}" class="btn btn-info">{% trans "Previous month" %}</a>
{% if next_month %}
<a href="{% url 'auth_fatlink_view_statistics_corp_month' corpid next_month|date:"Y" next_month|date:"m" %}" class="btn btn-info">{% trans "Next month" %}</a>
{% endif %}
</div>
</h1>
{% if fatStats %}
<table class="table table-responsive">
<tr>
<th class="col-md-1"></th>
<th class="col-md-2 text-center">{% trans "Main Character" %}</th>
<th class="col-md-2 text-center">{% trans "Characters" %}</th>
<th class="col-md-2 text-center">{% trans "Fats" %}</th>
<th class="col-md-2 text-center">{% trans "Average fats" %}
<i class="glyphicon glyphicon-question-sign" rel="tooltip" title="Fats ÷ Characters"></i>
</th>
</tr>
{% for memberStat in fatStats %}
<tr>
<td>
<img src="https://image.eveonline.com/Character/{{ memberStat.mainchid }}_32.jpg" class="ra-avatar img-responsive">
</td>
<td class="text-center">{{ memberStat.mainchar.character_name }}</td>
<td class="text-center">{{ memberStat.n_chars }}</td>
<td class="text-center">{{ memberStat.n_fats }}</td>
<td class="text-center">{{ memberStat.avg_fat }}</td>
</tr>
{% endfor %}
</table>
{% endif %}
</div>
{% endblock content %}
{% block extra_script %}
$(document).ready(function(){
$("[rel=tooltip]").tooltip();
{% endblock extra_script %}

View File

@@ -24,14 +24,16 @@
<th class="col-md-5 text-center">{% trans "Corp" %}</th> <th class="col-md-5 text-center">{% trans "Corp" %}</th>
<th class="col-md-2 text-center">{% trans "Members" %}</th> <th class="col-md-2 text-center">{% trans "Members" %}</th>
<th class="col-md-2 text-center">{% trans "Fats" %}</th> <th class="col-md-2 text-center">{% trans "Fats" %}</th>
<th class="col-md-2 text-center">{% trans "Average fats" %}</th> <th class="col-md-2 text-center">{% trans "Average fats" %}
<i class="glyphicon glyphicon-question-sign" rel="tooltip" title="Fats ÷ Characters"></i>
</th>
</tr> </tr>
{% for corpStat in fatStats %} {% for corpStat in fatStats %}
<tr> <tr>
<td> <td>
<img src="https://image.eveonline.com/Corporation/{{ corpStat.corp.corporation_id }}_32.png" class="ra-avatar img-responsive"> <img src="https://image.eveonline.com/Corporation/{{ corpStat.corp.corporation_id }}_32.png" class="ra-avatar img-responsive">
</td> </td>
<td class="text-center">[{{ corpStat.corp.corporation_ticker }}]</td> <td class="text-center"><a href="{% url 'auth_fatlink_view_statistics_corp' corpStat.corp.corporation_id %}">[{{ corpStat.corp.corporation_ticker }}]</td>
<td class="text-center">{{ corpStat.corp.corporation_name }}</td> <td class="text-center">{{ corpStat.corp.corporation_name }}</td>
<td class="text-center">{{ corpStat.corp.member_count }}</td> <td class="text-center">{{ corpStat.corp.member_count }}</td>
<td class="text-center">{{ corpStat.n_fats }}</td> <td class="text-center">{{ corpStat.n_fats }}</td>
@@ -42,3 +44,7 @@
{% endif %} {% endif %}
</div> </div>
{% endblock content %} {% endblock content %}
{% block extra_script %}
$(document).ready(function(){
$("[rel=tooltip]").tooltip();
{% endblock extra_script %}

View File

@@ -9,9 +9,11 @@ from django.utils.translation import ugettext_lazy as _
from django.utils import timezone from django.utils import timezone
from django.contrib import messages from django.contrib import messages
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.db.models import Q
from eveonline.models import EveCharacter from eveonline.models import EveCharacter
from eveonline.models import EveCorporationInfo from eveonline.models import EveCorporationInfo
from eveonline.managers import EveManager from eveonline.managers import EveManager
from authentication.models import AuthServicesInfo
from fleetactivitytracking.forms import FatlinkForm from fleetactivitytracking.forms import FatlinkForm
from fleetactivitytracking.models import Fatlink, Fat from fleetactivitytracking.models import Fatlink, Fat
@@ -55,6 +57,24 @@ class CorpStat(object):
return "%.2f" % (float(self.n_fats) / float(self.corp.member_count)) return "%.2f" % (float(self.n_fats) / float(self.corp.member_count))
class MemberStat(object):
def __init__(self, member, start_of_month, start_of_next_month, mainchid=None):
if mainchid:
self.mainchid = mainchid
else:
self.mainchid = AuthServicesInfo.objects.get(user_id=member['user_id']).main_char_id
self.mainchar = EveCharacter.objects.get(character_id=self.mainchid)
nchars = 0
for alliance_id in settings.STR_ALLIANCE_IDS:
nchars += EveCharacter.objects.filter(user_id=member['user_id']).filter(alliance_id=alliance_id).count()
self.n_chars = nchars
self.n_fats = Fat.objects.filter(user_id=member['user_id']).filter(
fatlink__fatdatetime__gte=start_of_month).filter(fatlink__fatdatetime__lte=start_of_next_month).count()
def avg_fat(self):
return "%.2f" % (float(self.n_fats) / float(self.n_chars))
def first_day_of_next_month(year, month): def first_day_of_next_month(year, month):
if month == 12: if month == 12:
return datetime.datetime(year + 1, 1, 1) return datetime.datetime(year + 1, 1, 1)
@@ -88,6 +108,41 @@ def fatlink_view(request):
return render(request, 'fleetactivitytracking/fatlinkview.html', context=context) return render(request, 'fleetactivitytracking/fatlinkview.html', context=context)
@login_required
@permission_required('auth.fleetactivitytracking_statistics')
def fatlink_statistics_corp_view(request, corpid, year=None, month=None):
if year is None:
year = datetime.date.today().year
if month is None:
month = datetime.date.today().month
year = int(year)
month = int(month)
start_of_month = datetime.datetime(year, month, 1)
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 = EveCharacter.objects.filter(corporation_id=corpid).values('user_id').distinct()
for member in corp_members:
try:
fat_stats[member['user_id']] = MemberStat(member, start_of_month, start_of_next_month)
except ObjectDoesNotExist:
continue
# collect and sort stats
stat_list = [fat_stats[x] for x in fat_stats]
stat_list.sort(key=lambda stat: stat.mainchar.character_name)
stat_list.sort(key=lambda stat: (stat.n_fats, stat.n_fats / stat.n_chars), reverse=True)
context = {'fatStats': stat_list, 'month': start_of_month.strftime("%B"), 'year': year,
'previous_month': start_of_previous_month, 'corpid': corpid}
if datetime.datetime.now() > start_of_next_month:
context.update({'next_month': start_of_next_month})
return render(request, 'fleetactivitytracking/fatlinkstatisticscorpview.html', context=context)
@login_required @login_required
@permission_required('auth.fleetactivitytracking_statistics') @permission_required('auth.fleetactivitytracking_statistics')
def fatlink_statistics_view(request, year=datetime.date.today().year, month=datetime.date.today().month): def fatlink_statistics_view(request, year=datetime.date.today().year, month=datetime.date.today().month):
@@ -100,19 +155,16 @@ def fatlink_statistics_view(request, year=datetime.date.today().year, month=date
fat_stats = {} fat_stats = {}
# get FAT stats for member corps # get FAT stats for member corps
for corp_id in settings.STR_CORP_IDS: query = Q(corporation_id__in=settings.STR_CORP_IDS) | Q(alliance__alliance_id__in=settings.STR_ALLIANCE_IDS)
fat_stats[corp_id] = CorpStat(corp_id, start_of_month, start_of_next_month) for corp in EveCorporationInfo.objects.filter(query).distinct():
for alliance_id in settings.STR_ALLIANCE_IDS: fat_stats[corp.corporation_id] = CorpStat(corp.corporation_id, start_of_month, start_of_next_month)
alliance_corps = EveCorporationInfo.objects.filter(alliance__alliance_id=alliance_id)
for corp in alliance_corps:
fat_stats[corp.corporation_id] = CorpStat(corp.corporation_id, start_of_month, start_of_next_month)
# get FAT stats for corps not in alliance # get FAT stats for corps not in alliance
fats_in_span = Fat.objects.filter(fatlink__fatdatetime__gte=start_of_month).filter( fats_in_span = Fat.objects.filter(fatlink__fatdatetime__gte=start_of_month).filter(
fatlink__fatdatetime__lt=start_of_next_month).exclude(character__corporation_id__in=fat_stats) fatlink__fatdatetime__lt=start_of_next_month).exclude(character__corporation_id__in=fat_stats)
for fat in fats_in_span: for fat in fats_in_span.exclude(character__corporation_id__in=fat_stats):
if fat.character.corporation_id not in fat_stats: if EveCorporationInfo.objects.filter(corporation_id=fat.character.corporation_id).exists():
fat_stats[fat.character.corporation_id] = CorpStat(fat.character.corporation_id, start_of_month, fat_stats[fat.character.corporation_id] = CorpStat(fat.character.corporation_id, start_of_month,
start_of_next_month) start_of_next_month)
@@ -121,12 +173,10 @@ def fatlink_statistics_view(request, year=datetime.date.today().year, month=date
stat_list.sort(key=lambda stat: stat.corp.corporation_name) stat_list.sort(key=lambda stat: stat.corp.corporation_name)
stat_list.sort(key=lambda stat: (stat.n_fats, stat.n_fats / stat.corp.member_count), reverse=True) stat_list.sort(key=lambda stat: (stat.n_fats, stat.n_fats / stat.corp.member_count), reverse=True)
context = {'fatStats': stat_list, 'month': start_of_month.strftime("%B"), 'year': year,
'previous_month': start_of_previous_month}
if datetime.datetime.now() > start_of_next_month: if datetime.datetime.now() > start_of_next_month:
context = {'fatStats': stat_list, 'month': start_of_month.strftime("%B"), 'year': year, context.update({'next_month': start_of_next_month})
'previous_month': start_of_previous_month, 'next_month': start_of_next_month}
else:
context = {'fatStats': stat_list, 'month': start_of_month.strftime("%B"), 'year': year,
'previous_month': start_of_previous_month}
return render(request, 'fleetactivitytracking/fatlinkstatisticsview.html', context=context) return render(request, 'fleetactivitytracking/fatlinkstatisticsview.html', context=context)
@@ -212,13 +262,14 @@ def click_fatlink_view(request, token, hash, fatname):
location['solar_system_name'] = \ location['solar_system_name'] = \
c.Universe.get_universe_systems_system_id(system_id=location['solar_system_id']).result()[ c.Universe.get_universe_systems_system_id(system_id=location['solar_system_id']).result()[
'name'] 'name']
if location['structure_id']: if location['station_id']:
location['station_name'] = \
c.Universe.get_universe_stations_station_id(station_id=location['station_id']).result()['name']
elif location['structure_id']:
c = token.get_esi_client(Universe='v1')
location['station_name'] = \ location['station_name'] = \
c.Universe.get_universe_structures_structure_id(structure_id=location['structure_id']).result()[ c.Universe.get_universe_structures_structure_id(structure_id=location['structure_id']).result()[
'name'] 'name']
elif location['station_id']:
location['station_name'] = \
c.Universe.get_universe_stations_station_id(station_id=location['station_id']).result()['name']
else: else:
location['station_name'] = "No Station" location['station_name'] = "No Station"
ship['ship_type_name'] = EveManager.get_itemtype(ship['ship_type_id']).name ship['ship_type_name'] = EveManager.get_itemtype(ship['ship_type_id']).name

View File

@@ -1 +1,2 @@
from __future__ import unicode_literals from __future__ import unicode_literals
default_app_config = 'fleetup.apps.FleetupConfig'

View File

@@ -1 +0,0 @@
from __future__ import unicode_literals

View File

@@ -1 +0,0 @@
from __future__ import unicode_literals

View File

@@ -1,178 +1,184 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from django.core.cache import cache
from datetime import datetime from datetime import datetime
import logging import logging
import requests import requests
import json import hashlib
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
appkey = settings.FLEETUP_APP_KEY
userid = settings.FLEETUP_USER_ID
apiid = settings.FLEETUP_API_ID
groupid = settings.FLEETUP_GROUP_ID
class FleetUpManager: 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)
def __init__(self): def __init__(self):
pass pass
@staticmethod @classmethod
def get_fleetup_members(): def _request_cache_key(cls, url):
url = "http://api.fleet-up.com/Api.svc/" + str(appkey) + "/" + str(userid) + "/" + str( h = hashlib.sha1()
apiid) + "/GroupCharacters/" + str(groupid) + "" 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: try:
jsondata = requests.get(url).content epoch_ms = int(cache_until_json[6:-2])
fmembers = json.loads(jsondata.decode()) 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.warn("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"], return {row["UserId"]: {"user_id": row["UserId"],
"char_name": row["EveCharName"], "char_name": row["EveCharName"],
"char_id": row["EveCharId"], "char_id": row["EveCharId"],
"corporation": row["Corporation"]} for row in fmembers["Data"]} "corporation": row["Corporation"]} for row in fmembers["Data"]}
except requests.exceptions.ConnectionError:
logger.warn("Can't connect to Fleet-Up API, is it offline?!")
except (ValueError, UnicodeDecodeError, TypeError): except (ValueError, UnicodeDecodeError, TypeError):
logger.debug("No fleetup members retrieved.") logger.debug("No fleetup members retrieved.")
return {} return {}
@staticmethod @classmethod
def get_fleetup_operations(): def get_fleetup_operations(cls):
url = "http://api.fleet-up.com/Api.svc/" + str(appkey) + "/" + str(userid) + "/" + str( url = "{}/Operations/{}".format(cls.BASE_URL, cls.GROUP_ID)
apiid) + "/Operations/" + str(groupid) + "" foperations = cls.get_endpoint(url)
try: if foperations is None:
jsondata = requests.get(url).content return None
foperations = json.loads(jsondata.decode()) return {row["StartString"]: {"subject": row["Subject"],
return {row["StartString"]: {"subject": row["Subject"], "start": datetime.strptime(row["StartString"], "%Y-%m-%d %H:%M:%S"),
"start": (datetime.strptime(row["StartString"], "%Y-%m-%d %H:%M:%S")), "end": datetime.strptime(row["EndString"], "%Y-%m-%d %H:%M:%S"),
"end": (datetime.strptime(row["EndString"], "%Y-%m-%d %H:%M:%S")), "operation_id": row["OperationId"],
"operation_id": row["OperationId"], "location": row["Location"],
"location": row["Location"], "location_info": row["LocationInfo"],
"location_info": row["LocationInfo"], "details": row["Details"],
"details": row["Details"], "url": row["Url"],
"url": row["Url"], "doctrine": row["Doctrines"],
"doctrine": row["Doctrines"], "organizer": row["Organizer"]} for row in foperations["Data"]}
"organizer": row["Organizer"]} for row in foperations["Data"]}
except requests.exceptions.ConnectionError: @classmethod
logger.warn("Can't connect to Fleet-Up API, is it offline?!") def get_fleetup_timers(cls):
except (ValueError, UnicodeDecodeError): url = "{}/Timers/{}".format(cls.BASE_URL, cls.GROUP_ID)
logger.debug("No fleetup operations retrieved.") 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": (datetime.strptime(row["ExpiresString"], "%Y-%m-%d %H:%M:%S")),
"notes": row["Notes"]} for row in ftimers["Data"]}
return {} return {}
@staticmethod @classmethod
def get_fleetup_timers(): def get_fleetup_doctrines(cls):
url = "http://api.fleet-up.com/Api.svc/" + str(appkey) + "/" + str(userid) + "/" + str( url = "{}/Doctrines/{}".format(cls.BASE_URL, cls.GROUP_ID)
apiid) + "/Timers/" + str(groupid) + "" fdoctrines = cls.get_endpoint(url)
try: if not fdoctrines:
jsondata = requests.get(url).content return None
ftimers = json.loads(jsondata.decode()) return {"fleetup_doctrines": fdoctrines["Data"]}
return {row["ExpiresString"]: {"solarsystem": row["SolarSystem"],
"planet": row["Planet"],
"moon": row["Moon"],
"owner": row["Owner"],
"type": row["Type"],
"timer_type": row["TimerType"],
"expires": (datetime.strptime(row["ExpiresString"], "%Y-%m-%d %H:%M:%S")),
"notes": row["Notes"]} for row in ftimers["Data"]}
except requests.exceptions.ConnectionError:
logger.warn("Can't connect to Fleet-Up API, is it offline?!")
except (ValueError, UnicodeDecodeError, TypeError):
logger.debug("No fleetup timers retrieved.")
return {}
@staticmethod @classmethod
def get_fleetup_doctrines(): def get_fleetup_doctrine(cls, doctrinenumber):
url = "http://api.fleet-up.com/Api.svc/" + str(appkey) + "/" + str(userid) + "/" + str( url = "{}/DoctrineFittings/{}".format(cls.BASE_URL, doctrinenumber)
apiid) + "/Doctrines/" + str(groupid) + "" fdoctrine = cls.get_endpoint(url)
try: if not fdoctrine:
jsondata = requests.get(url).content return None
fdoctrines = json.loads(jsondata.decode()) return {"fitting_doctrine": fdoctrine}
return {"fleetup_doctrines": fdoctrines["Data"]}
except requests.exceptions.ConnectionError:
logger.warn("Can't connect to Fleet-Up API, is it offline?!")
except (ValueError, UnicodeDecodeError):
logger.debug("No fleetup doctrines retrieved.")
return {"fleetup_doctrines": []}
@staticmethod @classmethod
def get_fleetup_doctrine(doctrinenumber): def get_fleetup_fittings(cls):
url = "http://api.fleet-up.com/Api.svc/" + str(appkey) + "/" + str(userid) + "/" + str( url = "{}/Fittings/{}".format(cls.BASE_URL, cls.GROUP_ID)
apiid) + "/DoctrineFittings/%s" % doctrinenumber ffittings = cls.get_endpoint(url)
try: if not ffittings:
jsondata = requests.get(url).content return None
fdoctrine = json.loads(jsondata.decode()) return {row["FittingId"]: {"fitting_id": row["FittingId"],
return {"fitting_doctrine": fdoctrine} "name": row["Name"],
except requests.exceptions.ConnectionError: "icon_id": row["EveTypeId"],
logger.warn("Can't connect to Fleet-Up API, is it offline?!") "hull": row["HullType"],
except (ValueError, UnicodeDecodeError): "shiptype": row["ShipType"],
logger.warn("Fleetup doctrine number %s not found" % doctrinenumber) "estimated": row["EstPrice"],
return {"fitting_doctrine": {}} "faction": row["Faction"],
"categories": row["Categories"],
"last_update": (
datetime.strptime(row["LastUpdatedString"], "%Y-%m-%d %H:%M:%S"))} for row in
ffittings["Data"]}
@staticmethod @classmethod
def get_fleetup_fittings(): def get_fleetup_fitting(cls, fittingnumber):
url = "http://api.fleet-up.com/Api.svc/" + str(appkey) + "/" + str(userid) + "/" + str( url = "{}/Fitting/{}".format(cls.BASE_URL, fittingnumber)
apiid) + "/Fittings/" + str(groupid) + ""
try: try:
jsondata = requests.get(url).content ffitting = cls.get_endpoint(url)
ffittings = json.loads(jsondata.decode()) if not ffitting:
return {row["FittingId"]: {"fitting_id": row["FittingId"], return None
"name": row["Name"],
"icon_id": row["EveTypeId"],
"hull": row["HullType"],
"shiptype": row["ShipType"],
"estimated": row["EstPrice"],
"faction": row["Faction"],
"categories": row["Categories"],
"last_update": (
datetime.strptime(row["LastUpdatedString"], "%Y-%m-%d %H:%M:%S"))} for row in
ffittings["Data"]}
except requests.exceptions.ConnectionError:
logger.warn("Can't connect to Fleet-Up API, is it offline?!")
except (ValueError, UnicodeDecodeError, TypeError):
logger.debug("No fleetup fittings retrieved.")
return {}
@staticmethod
def get_fleetup_fitting(fittingnumber):
url = "http://api.fleet-up.com/Api.svc/" + str(appkey) + "/" + str(userid) + "/" + str(
apiid) + "/Fitting/%s" % fittingnumber
try:
jsondata = requests.get(url).content
ffitting = json.loads(jsondata.decode())
return {"fitting_data": ffitting["Data"]} return {"fitting_data": ffitting["Data"]}
except requests.exceptions.ConnectionError:
logger.warn("Can't connect to Fleet-Up API, is it offline?!")
except (ValueError, UnicodeDecodeError):
logger.warn("Fleetup fitting number %s not found" % fittingnumber)
except KeyError: except KeyError:
logger.warn("Failed to retrieve fleetup fitting number %s" % fittingnumber) logger.warn("Failed to retrieve fleetup fitting number %s" % fittingnumber)
return {"fitting_data": {}} return {"fitting_data": {}}
@staticmethod @classmethod
def get_fleetup_doctrineid(fittingnumber): def get_fleetup_doctrineid(cls, fittingnumber):
url = "http://api.fleet-up.com/Api.svc/" + str(appkey) + "/" + str(userid) + "/" + str( url = "{}/Fitting/{}".format(cls.BASE_URL, fittingnumber)
apiid) + "/Fitting/%s" % fittingnumber
try: try:
jsondata = requests.get(url).content fdoctrineid = cls.get_endpoint(url)
fdoctrineid = json.loads(jsondata.decode()) if not fdoctrineid:
return None
return fdoctrineid['Data']['Doctrines'][0]['DoctrineId'] return fdoctrineid['Data']['Doctrines'][0]['DoctrineId']
except requests.exceptions.ConnectionError:
logger.warn("Can't connect to Fleet-Up API, is it offline?!")
except (ValueError, UnicodeDecodeError):
logger.warn("Fleetup doctrine number not found for fitting number %s" % fittingnumber)
except (KeyError, IndexError): except (KeyError, IndexError):
logger.debug("Fleetup fitting number %s not in a doctrine." % fittingnumber) logger.debug("Fleetup fitting number %s not in a doctrine." % fittingnumber)
return None return {}
@staticmethod @classmethod
def get_fleetup_fitting_eft(fittingnumber): def get_fleetup_fitting_eft(cls, fittingnumber):
url = "http://api.fleet-up.com/Api.svc/" + str(appkey) + "/" + str(userid) + "/" + str( url = "{}/Fitting/{}/eft".format(cls.BASE_URL, fittingnumber)
apiid) + "/Fitting/%s/eft" % fittingnumber
try: try:
jsondata = requests.get(url).content ffittingeft = cls.get_endpoint(url)
ffittingeft = json.loads(jsondata.decode()) if not ffittingeft:
return None
return {"fitting_eft": ffittingeft["Data"]["FittingData"]} return {"fitting_eft": ffittingeft["Data"]["FittingData"]}
except requests.exceptions.ConnectionError: except KeyError:
logger.warn("Can't connect to Fleet-Up API, is it offline?!")
except (ValueError, UnicodeDecodeError):
logger.warn("Fleetup fitting eft not found for fitting number %s" % fittingnumber) logger.warn("Fleetup fitting eft not found for fitting number %s" % fittingnumber)
return {"fitting_eft": {}} return {"fitting_eft": {}}

View File

@@ -1 +0,0 @@
from __future__ import unicode_literals

View File

@@ -9,30 +9,7 @@
{% block content %} {% block content %}
<div class="col-lg-12"> <div class="col-lg-12">
{% if perms.auth.corp_stats %} {% if perms.auth.corp_stats %}
<nav class="navbar navbar-default"> {% include "fleetup/menu.html" %}
<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><a href="{% url 'auth_fleetup_view' %}">{% trans "Ops and Timers" %}</a></li>
<li><a href="{% url 'auth_fleetup_doctrines' %}">{% trans "Doctrines" %}</a></li>
<li><a href="{% url 'auth_fleetup_fittings' %}">{% trans "Fittings" %}</a></li>
<li class="active"><a href="{% url 'auth_fleetup_characters' %}">{% trans "Characters" %} <span class="sr-only">(current)</span></a></li>
<li></li>
</ul>
</div>
</div>
</nav>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title">{% trans "Characters registered on Fleet-Up.com" %}</h3> <h3 class="panel-title">{% trans "Characters registered on Fleet-Up.com" %}</h3>

View File

@@ -8,30 +8,7 @@
{% block content %} {% block content %}
<div class="col-lg-12"> <div class="col-lg-12">
<nav class="navbar navbar-default"> {% include "fleetup/menu.html" %}
<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><a href="{% url 'auth_fleetup_view' %}">{% trans "Ops and Timers" %}</a></li>
<li class="active"><a href="{% url 'auth_fleetup_doctrines' %}">{% trans "Doctrines" %} <span class="sr-only">(current)</span></a></li>
<li><a href="{% url 'auth_fleetup_fittings' %}">{% trans "Fittings" %}</a></li>
{% if perms.auth.corp_stats %}
<li><a href="{% url 'auth_fleetup_characters' %}">{% trans "Characters" %}</a></li>
{% endif %}
<li></li>
</ul>
</div>
</div>
</nav>
<div class="panel"> <div class="panel">
{% for a, j in doctrine.items %} {% for a, j in doctrine.items %}
{% regroup j.Data|dictsort:"Role" by Role as role_list %} {% regroup j.Data|dictsort:"Role" by Role as role_list %}

View File

@@ -8,30 +8,7 @@
{% block content %} {% block content %}
<div class="col-lg-12"> <div class="col-lg-12">
<nav class="navbar navbar-default"> {% include "fleetup/menu.html" %}
<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><a href="{% url 'auth_fleetup_view' %}">{% trans "Ops and Timers" %}</a></li>
<li class="active"><a href="{% url 'auth_fleetup_doctrines' %}">{% trans "Doctrines" %} <span class="sr-only">(current)</span></a></li>
<li><a href="{% url 'auth_fleetup_fittings' %}">{% trans "Fittings" %}</a></li>
{% if perms.auth.corp_stats %}
<li><a href="{% url 'auth_fleetup_characters' %}">{% trans "Characters" %}</a></li>
{% endif %}
<li></li>
</ul>
</div>
</div>
</nav>
<div class="panel"> <div class="panel">
{% if doctrines_list %} {% if doctrines_list %}
{% for a, j in doctrines_list.items %} {% for a, j in doctrines_list.items %}

View File

@@ -8,30 +8,7 @@
{% block content %} {% block content %}
<div class="col-lg-12"> <div class="col-lg-12">
<nav class="navbar navbar-default"> {% include "fleetup/menu.html" %}
<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="#">{% trans "Fleet-Up" %}</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li><a href="{% url 'auth_fleetup_view' %}">{% trans "Ops and Timers" %}</a></li>
<li><a href="{% url 'auth_fleetup_doctrines' %}">{% trans "Doctrines" %}</a></li>
<li class="active"><a href="{% url 'auth_fleetup_fittings' %}">{% trans "Fittings" %} <span class="sr-only">(current)</span></a></li>
{% if perms.auth.corp_stats %}
<li><a href="{% url 'auth_fleetup_characters' %}">{% trans "Characters" %}</a></li>
{% endif %}
<li></li>
</ul>
</div>
</div>
</nav>
<div class="tab-content"> <div class="tab-content">
<div id="fit" class="tab-pane fade in active"> <div id="fit" class="tab-pane fade in active">
<div class="col-lg-3"> <div class="col-lg-3">
@@ -56,8 +33,8 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-lg-8 col-lg-offset-7"> <div class="pull-right">
<a class="btn btn-primary" href="/fleetup/doctrines/{{ doctrin.DoctrineId }}/">{% trans "See doctrine" %}</a> <a class="btn btn-primary" href="{% url 'auth_fleetup_doctrine' doctrin.DoctrineId %}">{% trans "See doctrine" %}</a>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
@@ -140,7 +117,7 @@
<div class="panel-body"> <div class="panel-body">
{% for data in fitting_eft.items %} {% for data in fitting_eft.items %}
{% autoescape off %} {% autoescape off %}
<pre>{{ fitting_eft.fitting_eft }}</pre> <textarea class="form-control" rows="25" spellcheck="false" onclick="this.focus();this.select()" readonly>{{ fitting_eft.fitting_eft }}</textarea>
{% endautoescape %} {% endautoescape %}
{% endfor %} {% endfor %}
</div> </div>

View File

@@ -8,30 +8,7 @@
{% block content %} {% block content %}
<div class="col-lg-12"> <div class="col-lg-12">
<nav class="navbar navbar-default"> {% include "fleetup/menu.html" %}
<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="#">{% trans "Fleet-Up" %}</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li><a href="{% url 'auth_fleetup_view' %}">{% trans "Ops and Timers" %}</a></li>
<li><a href="{% url 'auth_fleetup_doctrines' %}">{% trans "Doctrines" %}</a></li>
<li class="active"><a href="{% url 'auth_fleetup_fittings' %}">{% trans "Fittings" %} <span class="sr-only">(current)</span></a></li>
{% if perms.auth.corp_stats %}
<li><a href="{% url 'auth_fleetup_characters' %}">{% trans "Characters" %}</a></li>
{% endif %}
<li></li>
</ul>
</div>
</div>
</nav>
<div class="panel"> <div class="panel">
{% if fitting_list %} {% if fitting_list %}
<table class="table table-condensed table-hover table-striped"> <table class="table table-condensed table-hover table-striped">

View File

@@ -8,30 +8,7 @@
{% block content %} {% block content %}
<div class="col-lg-12"> <div class="col-lg-12">
<nav class="navbar navbar-default"> {% include "fleetup/menu.html" %}
<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="#">{% trans "Fleet-Up" %}</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class="active"><a href="#">{% trans "Ops and Timers" %} <span class="sr-only">(current)</span></a></li>
<li><a href="{% url 'auth_fleetup_doctrines' %}">{% trans "Doctrines" %}</a></li>
<li><a href="{% url 'auth_fleetup_fittings' %}">{% trans "Fittings" %}</a></li>
{% if perms.auth.human_resources %}
<li><a href="{% url 'auth_fleetup_characters' %}">{% trans "Characters" %}</a></li>
{% endif %}
<li></li>
</ul>
</div>
</div>
</nav>
<div class="panel"> <div class="panel">
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
<li class="active"><a data-toggle="tab" href="#operations">{% trans "Operations" %}</a></li> <li class="active"><a data-toggle="tab" href="#operations">{% trans "Operations" %}</a></li>

View File

@@ -0,0 +1,26 @@
{% 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 'auth_fleetup_view' %}"><a href="{% url 'auth_fleetup_view' %}">{% trans "Ops and Timers" %}</a></li>
<li class="{% navactive request 'auth_fleetup_doctrines auth_fleetup_doctrine' %}"><a href="{% url 'auth_fleetup_doctrines' %}">{% trans "Doctrines" %}</a></li>
<li class="{% navactive request 'auth_fleetup_fittings auth_fleetup_fitting' %}"><a href="{% url 'auth_fleetup_fittings' %}">{% trans "Fittings" %}</a></li>
{% if perms.auth.corp_stats %}
<li class="{% navactive request 'auth_fleetup_characters' %}"><a href="{% url 'auth_fleetup_characters' %}">{% trans "Characters" %}</a></li>
{% endif %}
</ul>
</div>
</div>
</nav>

0
fleetup/tests.py → fleetup/tests/__init__.py Executable file → Normal file
View File

View File

@@ -0,0 +1,509 @@
from __future__ import unicode_literals
try:
# Py3
from unittest import mock
except ImportError:
# Py2
import mock
import requests_mock
import json
import datetime
from django.test import TestCase
from 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('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('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('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('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': datetime.datetime(2017, 5, 6, 11, 11, 11),
'end': datetime.datetime(2017, 5, 6, 12, 12, 12),
'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('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': datetime.datetime(2017, 5, 6, 11, 11, 11),
'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('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('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('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': datetime.datetime(2017, 5, 6, 11, 11, 11)
}
}
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('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('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('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)

13
fleetup/urls.py Normal file
View File

@@ -0,0 +1,13 @@
from __future__ import unicode_literals
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.fleetup_view, name='auth_fleetup_view'),
url(r'^fittings/$', views.fleetup_fittings, name='auth_fleetup_fittings'),
url(r'^fittings/(?P<fittingnumber>[0-9]+)/$', views.fleetup_fitting, name='auth_fleetup_fitting'),
url(r'^doctrines/$', views.fleetup_doctrines, name='auth_fleetup_doctrines'),
url(r'^characters/$', views.fleetup_characters, name='auth_fleetup_characters'),
url(r'^doctrines/(?P<doctrinenumber>[0-9]+)/$', views.fleetup_doctrine, name='auth_fleetup_doctrine'),
]

38
fleetup/views.py Executable file → Normal file
View File

@@ -4,6 +4,8 @@ from django.shortcuts import render
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.decorators import permission_required from django.contrib.auth.decorators import permission_required
from django.template.defaulttags import register from django.template.defaulttags import register
from django.contrib import messages
from django.utils.translation import ugettext_lazy as _
from fleetup.managers import FleetUpManager from fleetup.managers import FleetUpManager
from authentication.decorators import members_and_blues from authentication.decorators import members_and_blues
@@ -23,14 +25,20 @@ def fleetup_view(request):
logger.debug("fleetup_view called by user %s" % request.user) logger.debug("fleetup_view called by user %s" % request.user)
operations_list = FleetUpManager.get_fleetup_operations() 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() 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') now = datetime.datetime.now().strftime('%H:%M:%S')
context = {"timers_list": sorted(timers_list.items()), context = {"timers_list": sorted(timers_list.items()),
"operations_list": sorted(operations_list.items()), "operations_list": sorted(operations_list.items()),
"now": now} "now": now}
return render(request, 'registered/fleetup.html', context=context) return render(request, 'fleetup/index.html', context=context)
@login_required @login_required
@@ -39,10 +47,13 @@ def fleetup_characters(request):
logger.debug("fleetup_characters called by user %s" % request.user) logger.debug("fleetup_characters called by user %s" % request.user)
member_list = FleetUpManager.get_fleetup_members() 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())} context = {"member_list": sorted(member_list.items())}
return render(request, 'registered/fleetupcharacters.html', context=context) return render(request, 'fleetup/characters.html', context=context)
@login_required @login_required
@@ -50,8 +61,13 @@ def fleetup_characters(request):
def fleetup_fittings(request): def fleetup_fittings(request):
logger.debug("fleetup_fittings called by user %s" % request.user) logger.debug("fleetup_fittings called by user %s" % request.user)
fitting_list = FleetUpManager.get_fleetup_fittings() 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())} context = {"fitting_list": sorted(fitting_list.items())}
return render(request, 'registered/fleetupfittingsview.html', context=context) return render(request, 'fleetup/fittingsview.html', context=context)
@login_required @login_required
@@ -62,10 +78,15 @@ def fleetup_fitting(request, fittingnumber):
fitting_data = FleetUpManager.get_fleetup_fitting(fittingnumber) fitting_data = FleetUpManager.get_fleetup_fitting(fittingnumber)
doctrinenumber = FleetUpManager.get_fleetup_doctrineid(fittingnumber) doctrinenumber = FleetUpManager.get_fleetup_doctrineid(fittingnumber)
doctrines_list = FleetUpManager.get_fleetup_doctrine(doctrinenumber) 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, context = {"fitting_eft": fitting_eft,
"fitting_data": fitting_data, "fitting_data": fitting_data,
"doctrines_list": doctrines_list} "doctrines_list": doctrines_list}
return render(request, 'registered/fleetupfitting.html', context=context) return render(request, 'fleetup/fitting.html', context=context)
@login_required @login_required
@@ -73,8 +94,11 @@ def fleetup_fitting(request, fittingnumber):
def fleetup_doctrines(request): def fleetup_doctrines(request):
logger.debug("fleetup_doctrines called by user %s" % request.user) logger.debug("fleetup_doctrines called by user %s" % request.user)
doctrines_list = FleetUpManager.get_fleetup_doctrines() 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} context = {"doctrines_list": doctrines_list}
return render(request, 'registered/fleetupdoctrinesview.html', context=context) return render(request, 'fleetup/doctrinesview.html', context=context)
@login_required @login_required
@@ -82,5 +106,7 @@ def fleetup_doctrines(request):
def fleetup_doctrine(request, doctrinenumber): def fleetup_doctrine(request, doctrinenumber):
logger.debug("fleetup_doctrine called by user %s" % request.user) logger.debug("fleetup_doctrine called by user %s" % request.user)
doctrine = FleetUpManager.get_fleetup_doctrine(doctrinenumber) 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} context = {"doctrine": doctrine}
return render(request, 'registered/fleetupdoctrine.html', context=context) return render(request, 'fleetup/doctrine.html', context=context)

View File

@@ -6,9 +6,7 @@ from django.utils.translation import ugettext_lazy as _
class opForm(forms.Form): class opForm(forms.Form):
doctrine = forms.CharField(max_length=254, required=True, label=_('Doctrine')) doctrine = forms.CharField(max_length=254, required=True, label=_('Doctrine'))
system = forms.CharField(max_length=254, required=True, label=_("System")) system = forms.CharField(max_length=254, required=True, label=_("System"))
location = forms.CharField(max_length=254, required=True, label=_("Location"))
start = forms.DateTimeField(required=True, label=_("Start Time")) start = forms.DateTimeField(required=True, label=_("Start Time"))
duration = forms.CharField(max_length=254, required=True, label=_("Duration")) duration = forms.CharField(max_length=254, required=True, label=_("Duration"))
operation_name = forms.CharField(max_length=254, required=True, label=_("Operation Name")) operation_name = forms.CharField(max_length=254, required=True, label=_("Operation Name"))
fc = forms.CharField(max_length=254, required=True, label=_("Fleet Commander")) fc = forms.CharField(max_length=254, required=True, label=_("Fleet Commander"))
details = forms.CharField(max_length=254, required=False, label=_("Extra Details"))

View File

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2017-04-13 04:42
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('optimer', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='optimer',
name='details',
),
migrations.RemoveField(
model_name='optimer',
name='location',
),
]

View File

@@ -13,12 +13,10 @@ class optimer(models.Model):
doctrine = models.CharField(max_length=254, default="") doctrine = models.CharField(max_length=254, default="")
system = models.CharField(max_length=254, default="") system = models.CharField(max_length=254, default="")
location = models.CharField(max_length=254, default="")
start = models.DateTimeField(default=datetime.now) start = models.DateTimeField(default=datetime.now)
duration = models.CharField(max_length=25, default="") duration = models.CharField(max_length=25, default="")
operation_name = models.CharField(max_length=254, default="") operation_name = models.CharField(max_length=254, default="")
fc = models.CharField(max_length=254, default="") fc = models.CharField(max_length=254, default="")
details = models.CharField(max_length=254, default="")
post_time = models.DateTimeField(default=timezone.now) post_time = models.DateTimeField(default=timezone.now)
eve_character = models.ForeignKey(EveCharacter) eve_character = models.ForeignKey(EveCharacter)

View File

@@ -22,7 +22,11 @@ logger = logging.getLogger(__name__)
@permission_required('auth.optimer_view') @permission_required('auth.optimer_view')
def optimer_view(request): def optimer_view(request):
logger.debug("optimer_view called by user %s" % request.user) logger.debug("optimer_view called by user %s" % request.user)
render_items = {'optimer': optimer.objects.all(), } render_items = {'optimer': optimer.objects.all(),
'future_timers': optimer.objects.all().filter(
start__gte=timezone.now()),
'past_timers': optimer.objects.all().filter(
start__lt=timezone.now()).order_by('-start')}
return render(request, 'registered/operationmanagement.html', context=render_items) return render(request, 'registered/operationmanagement.html', context=render_items)
@@ -44,12 +48,10 @@ def add_optimer_view(request):
op = optimer() op = optimer()
op.doctrine = form.cleaned_data['doctrine'] op.doctrine = form.cleaned_data['doctrine']
op.system = form.cleaned_data['system'] op.system = form.cleaned_data['system']
op.location = form.cleaned_data['location']
op.start = form.cleaned_data['start'] op.start = form.cleaned_data['start']
op.duration = form.cleaned_data['duration'] op.duration = form.cleaned_data['duration']
op.operation_name = form.cleaned_data['operation_name'] op.operation_name = form.cleaned_data['operation_name']
op.fc = form.cleaned_data['fc'] op.fc = form.cleaned_data['fc']
op.details = form.cleaned_data['details']
op.create_time = post_time op.create_time = post_time
op.eve_character = character op.eve_character = character
op.save() op.save()
@@ -93,12 +95,10 @@ def edit_optimer(request, optimer_id):
character = EveManager.get_character_by_id(auth_info.main_char_id) character = EveManager.get_character_by_id(auth_info.main_char_id)
op.doctrine = form.cleaned_data['doctrine'] op.doctrine = form.cleaned_data['doctrine']
op.system = form.cleaned_data['system'] op.system = form.cleaned_data['system']
op.location = form.cleaned_data['location']
op.start = form.cleaned_data['start'] op.start = form.cleaned_data['start']
op.duration = form.cleaned_data['duration'] op.duration = form.cleaned_data['duration']
op.operation_name = form.cleaned_data['operation_name'] op.operation_name = form.cleaned_data['operation_name']
op.fc = form.cleaned_data['fc'] op.fc = form.cleaned_data['fc']
op.details = form.cleaned_data['details']
op.eve_character = character op.eve_character = character
logger.info("User %s updating optimer id %s " % (request.user, optimer_id)) logger.info("User %s updating optimer id %s " % (request.user, optimer_id))
op.save() op.save()
@@ -108,12 +108,10 @@ def edit_optimer(request, optimer_id):
data = { data = {
'doctrine': op.doctrine, 'doctrine': op.doctrine,
'system': op.system, 'system': op.system,
'location': op.location,
'start': op.start, 'start': op.start,
'duration': op.duration, 'duration': op.duration,
'operation_name': op.operation_name, 'operation_name': op.operation_name,
'fc': op.fc, 'fc': op.fc,
'details': op.details,
} }
form = opForm(initial=data) form = opForm(initial=data)
return render(request, 'registered/optimerupdate.html', context={'form': form}) return render(request, 'registered/optimerupdate.html', context={'form': form})

View File

@@ -9,6 +9,7 @@ slugify
requests-oauthlib requests-oauthlib
sleekxmpp sleekxmpp
redis redis
celery>=4.0.2
# Django Stuff # # Django Stuff #
django>=1.10,<2.0 django>=1.10,<2.0
@@ -16,12 +17,8 @@ django-bootstrap-form
django-navhelper django-navhelper
django-bootstrap-pagination django-bootstrap-pagination
django-redis>=4.4 django-redis>=4.4
django-recaptcha
# awating release for fix to celery/django-celery#447 django-celery-beat
# django-celery
git+https://github.com/celery/django-celery
git+git://github.com/nikdoof/python-ts3.git
# awating pyghassen/openfire-restapi #1 to fix installation issues # awating pyghassen/openfire-restapi #1 to fix installation issues
git+https://github.com/adarnof/openfire-restapi git+https://github.com/adarnof/openfire-restapi

View File

@@ -1,5 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from eveonline.tasks import run_corp_update from eveonline.tasks import run_corp_update
run_corp_update() run_corp_update.apply()
quit() quit()

View File

@@ -31,6 +31,7 @@ class srpManager:
logger.debug("Ship type for kill ID %s is determined to be %s" % (kill_id, ship_type)) logger.debug("Ship type for kill ID %s is determined to be %s" % (kill_id, ship_type))
ship_value = result['zkb']['totalValue'] ship_value = result['zkb']['totalValue']
logger.debug("total loss value for kill id %s is %s" % (kill_id, ship_value)) logger.debug("total loss value for kill id %s is %s" % (kill_id, ship_value))
return ship_type, ship_value victim_name = result['victim']['characterName']
return ship_type, ship_value, victim_name
else: else:
raise ValueError("Invalid Kill ID") raise ValueError("Invalid Kill ID")

View File

@@ -5,9 +5,12 @@ import re
from django.conf import settings from django.conf import settings
from services.models import GroupCache from services.models import GroupCache
from requests_oauthlib import OAuth2Session from requests_oauthlib import OAuth2Session
from functools import wraps
import logging import logging
import datetime import datetime
import time
from django.utils import timezone from django.utils import timezone
from django.core.cache import cache
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -31,6 +34,118 @@ SCOPES = [
GROUP_CACHE_MAX_AGE = datetime.timedelta(minutes=30) GROUP_CACHE_MAX_AGE = datetime.timedelta(minutes=30)
class DiscordApiException(Exception):
def __init__(self):
super(Exception, self).__init__()
class DiscordApiTooBusy(DiscordApiException):
def __init__(self):
super(DiscordApiException, self).__init__()
self.message = "The Discord API is too busy to process this request now, please try again later."
class DiscordApiBackoff(DiscordApiException):
def __init__(self, retry_after, global_ratelimit):
super(DiscordApiException, self).__init__()
self.retry_after = retry_after
self.global_ratelimit = global_ratelimit
cache_time_format = '%Y-%m-%d %H:%M:%S'
def api_backoff(func):
"""
Decorator, Handles HTTP 429 "Too Many Requests" messages from the Discord API
If blocking=True is specified, this function will block and retry
the function up to max_retries=n times, or 3 if retries is not specified.
If the API call still recieves a backoff timer this function will raise
a <DiscordApiTooBusy> exception.
If the caller chooses blocking=False, the decorator will raise a DiscordApiBackoff
exception and the caller can choose to retry after the given timespan available in
the retry_after property in seconds.
"""
class PerformBackoff(Exception):
def __init__(self, retry_after, retry_datetime, global_ratelimit):
super(Exception, self).__init__()
self.retry_after = int(retry_after)
self.retry_datetime = retry_datetime
self.global_ratelimit = global_ratelimit
@wraps(func)
def decorated(*args, **kwargs):
blocking = kwargs.get('blocking', False)
retries = kwargs.get('max_retries', 3)
# Strip our parameters
if 'max_retries' in kwargs:
del kwargs['max_retries']
if 'blocking' in kwargs:
del kwargs['blocking']
cache_key = 'DISCORD_BACKOFF_' + func.__name__
cache_global_key = 'DISCORD_BACKOFF_GLOBAL'
while retries > 0:
try:
try:
# Check global backoff first, then route backoff
existing_global_backoff = cache.get(cache_global_key)
existing_backoff = existing_global_backoff or cache.get(cache_key)
if existing_backoff:
backoff_timer = datetime.datetime.strptime(existing_backoff, cache_time_format)
if backoff_timer > datetime.datetime.utcnow():
backoff_seconds = (backoff_timer - datetime.datetime.utcnow()).total_seconds()
logger.debug("Still under backoff for {} seconds, backing off" % backoff_seconds)
# Still under backoff
raise PerformBackoff(
retry_after=backoff_seconds,
retry_datetime=backoff_timer,
global_ratelimit=bool(existing_global_backoff)
)
logger.debug("Calling API calling function")
func(*args, **kwargs)
break
except requests.HTTPError as e:
if e.response.status_code == 429:
if 'Retry-After' in e.response.headers:
retry_after = e.response.headers['Retry-After']
else:
# Pick some random time
retry_after = 5
logger.info("Received backoff from API of %s seconds, handling" % retry_after)
# Store value in redis
backoff_until = (datetime.datetime.utcnow() +
datetime.timedelta(seconds=int(retry_after)))
global_backoff = bool(e.response.headers.get('X-RateLimit-Global', False))
if global_backoff:
logger.info("Global backoff!!")
cache.set(cache_global_key, backoff_until.strftime(cache_time_format), retry_after)
else:
cache.set(cache_key, backoff_until.strftime(cache_time_format), retry_after)
raise PerformBackoff(retry_after=retry_after, retry_datetime=backoff_until,
global_ratelimit=global_backoff)
else:
# Not 429, re-raise
raise e
except PerformBackoff as bo:
# Sleep if we're blocking
if blocking:
logger.info("Blocking Back off from API calls for %s seconds" % bo.retry_after)
time.sleep(10 if bo.retry_after > 10 else bo.retry_after)
else:
# Otherwise raise exception and let caller handle the backoff
raise DiscordApiBackoff(retry_after=bo.retry_after, global_ratelimit=bo.global_ratelimit)
finally:
retries -= 1
if retries == 0:
raise DiscordApiTooBusy()
return decorated
class DiscordOAuthManager: class DiscordOAuthManager:
def __init__(self): def __init__(self):
pass pass
@@ -191,6 +306,7 @@ class DiscordOAuthManager:
DiscordOAuthManager.__update_group_cache() DiscordOAuthManager.__update_group_cache()
@staticmethod @staticmethod
@api_backoff
def update_groups(user_id, groups): def update_groups(user_id, groups):
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN} custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
group_ids = [DiscordOAuthManager.__group_name_to_id(DiscordOAuthManager._sanitize_groupname(g)) for g in groups] group_ids = [DiscordOAuthManager.__group_name_to_id(DiscordOAuthManager._sanitize_groupname(g)) for g in groups]
@@ -199,3 +315,4 @@ class DiscordOAuthManager:
r = requests.patch(path, headers=custom_headers, json=data) r = requests.patch(path, headers=custom_headers, json=data)
logger.debug("Received status code %s after setting user roles" % r.status_code) logger.debug("Received status code %s after setting user roles" % r.status_code)
r.raise_for_status() r.raise_for_status()

View File

@@ -9,7 +9,7 @@ from django.core.exceptions import ObjectDoesNotExist
from eveonline.managers import EveManager from eveonline.managers import EveManager
from notifications import notify from notifications import notify
from services.modules.discord.manager import DiscordOAuthManager from services.modules.discord.manager import DiscordOAuthManager, DiscordApiBackoff
from services.tasks import only_one from services.tasks import only_one
from .models import DiscordUser from .models import DiscordUser
@@ -74,6 +74,10 @@ class DiscordTasks:
logger.debug("Updating user %s discord groups to %s" % (user, groups)) logger.debug("Updating user %s discord groups to %s" % (user, groups))
try: try:
DiscordOAuthManager.update_groups(user.discord.uid, groups) DiscordOAuthManager.update_groups(user.discord.uid, groups)
except DiscordApiBackoff as bo:
logger.info("Discord group sync API back off for %s, "
"retrying in %s seconds" % (user, bo.retry_after))
raise task_self.retry(countdown=bo.retry_after)
except Exception as e: except Exception as e:
if task_self: if task_self:
logger.exception("Discord group sync failed for %s, retrying in 10 mins" % user) logger.exception("Discord group sync failed for %s, retrying in 10 mins" % user)
@@ -118,7 +122,7 @@ class DiscordTasks:
def update_all_nicknames(): def update_all_nicknames():
logger.debug("Updating ALL discord nicknames") logger.debug("Updating ALL discord nicknames")
for discord_user in DiscordUser.objects.exclude(uid__exact=''): for discord_user in DiscordUser.objects.exclude(uid__exact=''):
DiscordTasks.update_nickname.delay(discord_user.user.user_id) DiscordTasks.update_nickname.delay(discord_user.user.pk)
@classmethod @classmethod
def disable(cls): def disable(cls):

View File

@@ -7,14 +7,14 @@
<td class="text-center"><a href="https://discordapp.com/channels/{{ DISCORD_SERVER_ID }}/{{ DISCORD_SERVER_ID}}">https://discordapp.com</a></td> <td class="text-center"><a href="https://discordapp.com/channels/{{ DISCORD_SERVER_ID }}/{{ DISCORD_SERVER_ID}}">https://discordapp.com</a></td>
<td class="text-center"> <td class="text-center">
{% if not discord_uid %} {% if not discord_uid %}
<a href="{% url 'auth_activate_discord' %}" class="btn btn-warning"> <a href="{% url 'auth_activate_discord' %}" title="Activate" class="btn btn-warning">
<span class="glyphicon glyphicon-ok"></span> <span class="glyphicon glyphicon-ok"></span>
</a> </a>
{% else %} {% else %}
<a href="{% url 'auth_reset_discord' %}" class="btn btn-primary"> <a href="{% url 'auth_reset_discord' %}" title="Reset" class="btn btn-primary">
<span class="glyphicon glyphicon-refresh"></span> <span class="glyphicon glyphicon-refresh"></span>
</a> </a>
<a href="{% url 'auth_deactivate_discord' %}" class="btn btn-danger"> <a href="{% url 'auth_deactivate_discord' %}" title="Deactivate" class="btn btn-danger">
<span class="glyphicon glyphicon-remove"></span> <span class="glyphicon glyphicon-remove"></span>
</a> </a>
{% endif %} {% endif %}

View File

@@ -17,6 +17,10 @@ from alliance_auth.tests.auth_utils import AuthUtils
from .auth_hooks import DiscordService from .auth_hooks import DiscordService
from .models import DiscordUser from .models import DiscordUser
from .tasks import DiscordTasks from .tasks import DiscordTasks
from .manager import DiscordOAuthManager
import requests_mock
import datetime
MODULE_PATH = 'services.modules.discord' MODULE_PATH = 'services.modules.discord'
@@ -198,3 +202,254 @@ class DiscordViewsTestCase(TestCase):
self.assertRedirects(response, expected_url='/en/services/', target_status_code=200) self.assertRedirects(response, expected_url='/en/services/', target_status_code=200)
with self.assertRaises(ObjectDoesNotExist): with self.assertRaises(ObjectDoesNotExist):
discord_user = User.objects.get(pk=self.member.pk).discord discord_user = User.objects.get(pk=self.member.pk).discord
class DiscordManagerTestCase(TestCase):
def setUp(self):
pass
def test__sanitize_groupname(self):
test_group_name = ' Group Name_Test_'
group_name = DiscordOAuthManager._sanitize_groupname(test_group_name)
self.assertEqual(group_name, 'GroupName_Test')
def test_generate_Bot_add_url(self):
from . import manager
bot_add_url = DiscordOAuthManager.generate_bot_add_url()
auth_url = manager.AUTH_URL
real_bot_add_url = '{}?client_id=appid&scope=bot&permissions={}'.format(auth_url, manager.BOT_PERMISSIONS)
self.assertEqual(bot_add_url, real_bot_add_url)
def test_generate_oauth_redirect_url(self):
from . import manager
import urllib
import sys
oauth_url = DiscordOAuthManager.generate_oauth_redirect_url()
self.assertIn(manager.AUTH_URL, oauth_url)
self.assertIn('+'.join(manager.SCOPES), oauth_url)
self.assertIn(settings.DISCORD_APP_ID, oauth_url)
if sys.version_info[0] < 3:
# Py2
self.assertIn(urllib.quote_plus(settings.DISCORD_CALLBACK_URL), oauth_url)
else: # Py3
self.assertIn(urllib.parse.quote_plus(settings.DISCORD_CALLBACK_URL), oauth_url)
@mock.patch(MODULE_PATH + '.manager.OAuth2Session')
def test__process_callback_code(self, oauth):
from . import manager
instance = oauth.return_value
instance.fetch_token.return_value = {'access_token': 'mywonderfultoken'}
token = DiscordOAuthManager._process_callback_code('12345')
self.assertTrue(oauth.called)
args, kwargs = oauth.call_args
self.assertEqual(args[0], settings.DISCORD_APP_ID)
self.assertEqual(kwargs['redirect_uri'], settings.DISCORD_CALLBACK_URL)
self.assertTrue(instance.fetch_token.called)
args, kwargs = instance.fetch_token.call_args
self.assertEqual(args[0], manager.TOKEN_URL)
self.assertEqual(kwargs['client_secret'], settings.DISCORD_APP_SECRET)
self.assertEqual(kwargs['code'], '12345')
self.assertEqual(token['access_token'], 'mywonderfultoken')
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._process_callback_code')
@requests_mock.Mocker()
def test_add_user(self, oauth_token, m):
from . import manager
import json
# Arrange
oauth_token.return_value = {'access_token': 'accesstoken'}
headers = {'accept': 'application/json', 'authorization': 'Bearer accesstoken'}
m.register_uri('POST',
manager.DISCORD_URL + '/invites/'+str(settings.DISCORD_INVITE_CODE),
request_headers=headers,
text='{}')
m.register_uri('GET',
manager.DISCORD_URL + "/users/@me",
request_headers=headers,
text=json.dumps({'id': "123456"}))
# Act
return_value = DiscordOAuthManager.add_user('abcdef')
# Assert
self.assertEqual(return_value, '123456')
self.assertEqual(m.call_count, 2)
@requests_mock.Mocker()
def test_delete_user(self, m):
from . import manager
import json
# Arrange
headers = {'accept': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
user_id = 12345
request_url = '{}/guilds/{}/members/{}'.format(manager.DISCORD_URL, settings.DISCORD_GUILD_ID, user_id)
m.register_uri('DELETE',
request_url,
request_headers=headers,
text=json.dumps({}))
# Act
result = DiscordOAuthManager.delete_user(user_id)
# Assert
self.assertTrue(result)
###
# Test 404 (already deleted)
# Arrange
m.register_uri('DELETE',
request_url,
request_headers=headers,
status_code=404)
# Act
result = DiscordOAuthManager.delete_user(user_id)
# Assert
self.assertTrue(result)
###
# Test 500 (some random API error)
# Arrange
m.register_uri('DELETE',
request_url,
request_headers=headers,
status_code=500)
# Act
result = DiscordOAuthManager.delete_user(user_id)
# Assert
self.assertFalse(result)
@requests_mock.Mocker()
def test_update_nickname(self, m):
from . import manager
import json
# Arrange
headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
user_id = 12345
request_url = '{}/guilds/{}/members/{}'.format(manager.DISCORD_URL, settings.DISCORD_GUILD_ID, user_id)
m.patch(request_url,
request_headers=headers)
# Act
result = DiscordOAuthManager.update_nickname(user_id, 'somenick')
# Assert
self.assertTrue(result)
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._DiscordOAuthManager__get_group_cache')
@requests_mock.Mocker()
def test_update_groups(self, group_cache, m):
from . import manager
import json
# Arrange
groups = ['Member', 'Blue', 'Special Group']
group_cache.return_value = [{'id': 111, 'name': 'Member'},
{'id': 222, 'name': 'Blue'},
{'id': 333, 'name': 'SpecialGroup'},
{'id': 444, 'name': 'NotYourGroup'}]
headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
user_id = 12345
request_url = '{}/guilds/{}/members/{}'.format(manager.DISCORD_URL, settings.DISCORD_GUILD_ID, user_id)
m.patch(request_url,
request_headers=headers)
# Act
DiscordOAuthManager.update_groups(user_id, groups)
# Assert
self.assertEqual(len(m.request_history), 1, 'Must be one HTTP call made')
history = json.loads(m.request_history[0].text)
self.assertIn('roles', history, "'The request must send JSON object with the 'roles' key")
self.assertIn(111, history['roles'], 'The group id 111 must be added to the request')
self.assertIn(222, history['roles'], 'The group id 222 must be added to the request')
self.assertIn(333, history['roles'], 'The group id 333 must be added to the request')
self.assertNotIn(444, history['roles'], 'The group id 444 must NOT be added to the request')
@mock.patch(MODULE_PATH + '.manager.cache')
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._DiscordOAuthManager__get_group_cache')
@requests_mock.Mocker()
def test_update_groups_backoff(self, group_cache, djcache, m):
from . import manager
# Arrange
groups = ['Member']
group_cache.return_value = [{'id': 111, 'name': 'Member'}]
headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
user_id = 12345
request_url = '{}/guilds/{}/members/{}'.format(manager.DISCORD_URL, settings.DISCORD_GUILD_ID, user_id)
djcache.get.return_value = None # No existing backoffs in cache
m.patch(request_url,
request_headers=headers,
headers={'Retry-After': '200'},
status_code=429)
# Act & Assert
with self.assertRaises(manager.DiscordApiBackoff) as bo:
try:
DiscordOAuthManager.update_groups(user_id, groups, blocking=False)
except manager.DiscordApiBackoff as bo:
self.assertEqual(bo.retry_after, 200, 'Retry-After time must be equal to Retry-After set in header')
self.assertFalse(bo.global_ratelimit, 'global_ratelimit must be False')
raise bo
self.assertTrue(djcache.set.called)
args, kwargs = djcache.set.call_args
self.assertEqual(args[0], 'DISCORD_BACKOFF_update_groups')
self.assertTrue(datetime.datetime.strptime(args[1], manager.cache_time_format) > datetime.datetime.now())
@mock.patch(MODULE_PATH + '.manager.cache')
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._DiscordOAuthManager__get_group_cache')
@requests_mock.Mocker()
def test_update_groups_global_backoff(self, group_cache, djcache, m):
from . import manager
# Arrange
groups = ['Member']
group_cache.return_value = [{'id': 111, 'name': 'Member'}]
headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
user_id = 12345
request_url = '{}/guilds/{}/members/{}'.format(manager.DISCORD_URL, settings.DISCORD_GUILD_ID, user_id)
djcache.get.return_value = None # No existing backoffs in cache
m.patch(request_url,
request_headers=headers,
headers={'Retry-After': '200', 'X-RateLimit-Global': 'true'},
status_code=429)
# Act & Assert
with self.assertRaises(manager.DiscordApiBackoff) as bo:
try:
DiscordOAuthManager.update_groups(user_id, groups, blocking=False)
except manager.DiscordApiBackoff as bo:
self.assertEqual(bo.retry_after, 200, 'Retry-After time must be equal to Retry-After set in header')
self.assertTrue(bo.global_ratelimit, 'global_ratelimit must be True')
raise bo
self.assertTrue(djcache.set.called)
args, kwargs = djcache.set.call_args
self.assertEqual(args[0], 'DISCORD_BACKOFF_GLOBAL')
self.assertTrue(datetime.datetime.strptime(args[1], manager.cache_time_format) > datetime.datetime.now())

View File

@@ -376,6 +376,5 @@ class DiscourseManager:
logger.debug("Disabling user %s Discourse access." % user) logger.debug("Disabling user %s Discourse access." % user)
d_user = DiscourseManager.__get_user_by_external(user.pk) d_user = DiscourseManager.__get_user_by_external(user.pk)
DiscourseManager.__logout(d_user['user']['id']) DiscourseManager.__logout(d_user['user']['id'])
DiscourseManager.__suspend_user(d_user['user']['username'])
logger.info("Disabled user %s Discourse access." % user) logger.info("Disabled user %s Discourse access." % user)
return True return True

View File

@@ -4,5 +4,5 @@
<td class="text-center">{{ char.character_name }}</td> <td class="text-center">{{ char.character_name }}</td>
<td class="text-center"><a href="{{ DISCOURSE_URL }}">{{ DISCOURSE_URL }}</a></td> <td class="text-center"><a href="{{ DISCOURSE_URL }}">{{ DISCOURSE_URL }}</a></td>
<td class="text-center"> <td class="text-center">
<a class="btn btn-success" href="{{ DISCOURSE_URL }}"><span class="glyphicon glyphicon-arrow-right"></span></a> <a title="Go To Forums" class="btn btn-success" href="{{ DISCOURSE_URL }}"><span class="glyphicon glyphicon-arrow-right"></span></a>
</td> </td>

View File

@@ -21,6 +21,7 @@ class MumbleService(ServicesHook):
self.urlpatterns = urlpatterns self.urlpatterns = urlpatterns
self.service_url = settings.MUMBLE_URL self.service_url = settings.MUMBLE_URL
self.access_perm = 'mumble.access_mumble' self.access_perm = 'mumble.access_mumble'
self.service_ctrl_template = 'registered/mumble_service_ctrl.html'
def delete_user(self, user, notify_user=False): def delete_user(self, user, notify_user=False):
logging.debug("Deleting user %s %s account" % (user, self.name)) logging.debug("Deleting user %s %s account" % (user, self.name))
@@ -57,6 +58,7 @@ class MumbleService(ServicesHook):
'service_name': self.title, 'service_name': self.title,
'urls': urls, 'urls': urls,
'service_url': self.service_url, 'service_url': self.service_url,
'connect_url': request.user.mumble.username + '@' + self.service_url if MumbleTasks.has_account(request.user) else self.service_url,
'username': request.user.mumble.username if MumbleTasks.has_account(request.user) else '', 'username': request.user.mumble.username if MumbleTasks.has_account(request.user) else '',
}, request=request) }, request=request)

View File

@@ -0,0 +1,25 @@
<tr>
<td class="text-center">{{ service_name }}</td>
<td class="text-center">{{ username }}</td>
<td class="text-center"><a href="mumble://{{ service_url }}">{{ service_url }}</a></td>
<td class="text-center">
{% ifequal username "" %}
<a href="{% url urls.auth_activate %}" title="Activate" class="btn btn-warning">
<span class="glyphicon glyphicon-ok"></span>
</a>
{% else %}
<a href="{% url urls.auth_set_password %}" title="Set Password" class="btn btn-warning">
<span class="glyphicon glyphicon-pencil"></span>
</a>
<a href="{% url urls.auth_reset_password %}" title="Reset Password" class="btn btn-primary">
<span class="glyphicon glyphicon-refresh"></span>
</a>
<a href="{% url urls.auth_deactivate %}" title="Deactivate" class="btn btn-danger">
<span class="glyphicon glyphicon-remove"></span>
</a>
<a href="mumble://{{ connect_url }}" class="btn btn-success" title="Connect">
<span class="glyphicon glyphicon-arrow-right"></span>
</a>
{% endifequal %}
</td>
</tr>

View File

@@ -11,7 +11,6 @@ except ImportError:
import sleekxmpp import sleekxmpp
from django.conf import settings from django.conf import settings
import threading
from ofrestapi.users import Users as ofUsers from ofrestapi.users import Users as ofUsers
from ofrestapi import exception from ofrestapi import exception
@@ -24,12 +23,6 @@ class OpenfireManager:
def __init__(self): def __init__(self):
pass pass
@staticmethod
def send_broadcast_threaded(group_name, broadcast_message):
logger.debug("Starting broadcast to %s with message %s" % (group_name, broadcast_message))
broadcast_thread = XmppThread(1, "XMPP Broadcast Thread", 1, group_name, broadcast_message)
broadcast_thread.start()
@staticmethod @staticmethod
def __add_address_to_username(username): def __add_address_to_username(username):
address = urlparse(settings.OPENFIRE_ADDRESS).netloc.split(":")[0] address = urlparse(settings.OPENFIRE_ADDRESS).netloc.split(":")[0]
@@ -169,11 +162,19 @@ class OpenfireManager:
xmpp = PingBot(settings.BROADCAST_USER, settings.BROADCAST_USER_PASSWORD, to_address, broadcast_message) xmpp = PingBot(settings.BROADCAST_USER, settings.BROADCAST_USER_PASSWORD, to_address, broadcast_message)
xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0030') # Service Discovery
xmpp.register_plugin('xep_0199') # XMPP Ping xmpp.register_plugin('xep_0199') # XMPP Ping
if xmpp.connect(): if xmpp.connect(reattempt=False):
xmpp.process(block=True) xmpp.process(block=True)
logger.info("Sent jabber ping to group %s" % group_name) message = None
if xmpp.message_sent:
logger.debug("Sent jabber ping to group %s" % group_name)
return
else:
message = "Failed to send Openfire broadcast message."
logger.error(message)
raise PingBotException(message)
else: else:
raise ValueError("Unable to connect to jabber server.") logger.error("Unable to connect to jabber server")
raise PingBotException("Unable to connect to jabber server.")
class PingBot(sleekxmpp.ClientXMPP): class PingBot(sleekxmpp.ClientXMPP):
@@ -184,17 +185,28 @@ class PingBot(sleekxmpp.ClientXMPP):
def __init__(self, jid, password, recipient, message): def __init__(self, jid, password, recipient, message):
sleekxmpp.ClientXMPP.__init__(self, jid, password) sleekxmpp.ClientXMPP.__init__(self, jid, password)
self.reconnect_max_attempts = 5
self.auto_reconnect = False
# The message we wish to send, and the JID that # The message we wish to send, and the JID that
# will receive it. # will receive it.
self.recipient = recipient self.recipient = recipient
self.msg = message self.msg = message
# Success checking
self.message_sent = False
# The session_start event will be triggered when # The session_start event will be triggered when
# the bot establishes its connection with the server # the bot establishes its connection with the server
# and the XML streams are ready for use. We want to # and the XML streams are ready for use. We want to
# listen for this event so that we we can initialize # listen for this event so that we we can initialize
# our roster. # our roster.
self.add_event_handler("session_start", self.start) self.add_event_handler("session_start", self.start)
if getattr(settings, 'BROADCAST_IGNORE_INVALID_CERT', False):
self.add_event_handler("ssl_invalid_cert", self.discard)
def discard(self, *args, **kwargs):
# Discard the event
return
def start(self, event): def start(self, event):
self.send_presence() self.send_presence()
@@ -203,20 +215,11 @@ class PingBot(sleekxmpp.ClientXMPP):
self.send_message(mto=self.recipient, self.send_message(mto=self.recipient,
mbody=self.msg, mbody=self.msg,
mtype='chat') mtype='chat')
self.message_sent = True
# Using wait=True ensures that the send queue will be # Using wait=True ensures that the send queue will be
# emptied before ending the session. # emptied before ending the session.
self.disconnect(wait=True) self.disconnect(wait=True)
class XmppThread(threading.Thread): class PingBotException(Exception):
def __init__(self, thread_id, name, counter, group, message, ): pass
threading.Thread.__init__(self)
self.threadID = thread_id
self.name = name
self.counter = counter
self.group = group
self.message = message
def run(self):
OpenfireManager.send_broadcast_message(self.group, self.message)

View File

@@ -10,7 +10,7 @@ from eveonline.managers import EveManager
from eveonline.models import EveCharacter from eveonline.models import EveCharacter
from services.forms import ServicePasswordForm from services.forms import ServicePasswordForm
from .manager import OpenfireManager from .manager import OpenfireManager, PingBotException
from .tasks import OpenfireTasks from .tasks import OpenfireTasks
from .forms import JabberBroadcastForm from .forms import JabberBroadcastForm
from .models import OpenfireUser from .models import OpenfireUser
@@ -103,27 +103,29 @@ def jabber_broadcast_view(request):
if form.is_valid(): if form.is_valid():
main_char = EveManager.get_main_character(request.user) main_char = EveManager.get_main_character(request.user)
logger.debug("Processing jabber broadcast for user %s with main character %s" % (request.user, main_char)) logger.debug("Processing jabber broadcast for user %s with main character %s" % (request.user, main_char))
if main_char is not None: try:
message_to_send = form.cleaned_data[ if main_char is not None:
'message'] + "\n##### SENT BY: " + "[" + main_char.corporation_ticker + "]" + \ message_to_send = form.cleaned_data[
main_char.character_name + " TO: " + \ 'message'] + "\n##### SENT BY: " + "[" + main_char.corporation_ticker + "]" + \
form.cleaned_data['group'] + " WHEN: " + datetime.datetime.utcnow().strftime( main_char.character_name + " TO: " + \
"%Y-%m-%d %H:%M:%S") + " #####\n##### Replies are NOT monitored #####\n" form.cleaned_data['group'] + " WHEN: " + datetime.datetime.utcnow().strftime(
group_to_send = form.cleaned_data['group'] "%Y-%m-%d %H:%M:%S") + " #####\n##### Replies are NOT monitored #####\n"
group_to_send = form.cleaned_data['group']
OpenfireManager.send_broadcast_threaded(group_to_send, message_to_send, ) else:
message_to_send = form.cleaned_data[
'message'] + "\n##### SENT BY: " + "No character but can send pings?" + " TO: " + \
form.cleaned_data['group'] + " WHEN: " + datetime.datetime.utcnow().strftime(
"%Y-%m-%d %H:%M:%S") + " #####\n##### Replies are NOT monitored #####\n"
group_to_send = form.cleaned_data['group']
else: OpenfireManager.send_broadcast_message(group_to_send, message_to_send)
message_to_send = form.cleaned_data[
'message'] + "\n##### SENT BY: " + "No character but can send pings?" + " TO: " + \
form.cleaned_data['group'] + " WHEN: " + datetime.datetime.utcnow().strftime(
"%Y-%m-%d %H:%M:%S") + " #####\n##### Replies are NOT monitored #####\n"
group_to_send = form.cleaned_data['group']
OpenfireManager.send_broadcast_threaded(group_to_send, message_to_send, ) messages.success(request, 'Sent jabber broadcast to %s' % group_to_send)
logger.info("Sent jabber broadcast on behalf of user %s" % request.user)
except PingBotException as e:
messages.error(request, e)
messages.success(request, 'Sent jabber broadcast to %s' % group_to_send)
logger.info("Sent jabber broadcast on behalf of user %s" % request.user)
else: else:
form = JabberBroadcastForm() form = JabberBroadcastForm()
form.fields['group'].choices = allchoices form.fields['group'].choices = allchoices

View File

@@ -18,7 +18,7 @@ logger = logging.getLogger(__name__)
class Phpbb3Manager: class Phpbb3Manager:
SQL_ADD_USER = r"INSERT INTO phpbb_users (username, username_clean, " \ SQL_ADD_USER = r"INSERT INTO phpbb_users (username, username_clean, " \
r"user_password, user_email, group_id, user_regdate, user_permissions, " \ r"user_password, user_email, group_id, user_regdate, user_permissions, " \
r"user_sig) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)" r"user_sig, user_lang) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, 'en')"
SQL_DEL_USER = r"DELETE FROM phpbb_users where username = %s" SQL_DEL_USER = r"DELETE FROM phpbb_users where username = %s"

View File

@@ -2,9 +2,11 @@ from __future__ import unicode_literals
import random import random
import string import string
import requests import requests
import hashlib
from eveonline.managers import EveManager from eveonline.managers import EveManager
from django.conf import settings from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.cache import cache
from six import iteritems from six import iteritems
@@ -20,7 +22,7 @@ class SeatManager:
RESPONSE_OK = 'ok' RESPONSE_OK = 'ok'
@staticmethod @staticmethod
def __santatize_username(username): def __sanitize_username(username):
sanatized = username.replace(" ", "_") sanatized = username.replace(" ", "_")
return sanatized.lower() return sanatized.lower()
@@ -33,7 +35,7 @@ class SeatManager:
return cls.RESPONSE_OK in response return cls.RESPONSE_OK in response
@staticmethod @staticmethod
def exec_request(endpoint, func, **kwargs): def exec_request(endpoint, func, raise_for_status=False, **kwargs):
""" Send an https api request """ """ Send an https api request """
try: try:
endpoint = '{0}/api/v1/{1}'.format(settings.SEAT_URL, endpoint) endpoint = '{0}/api/v1/{1}'.format(settings.SEAT_URL, endpoint)
@@ -43,22 +45,24 @@ class SeatManager:
ret = getattr(requests, func)(endpoint, headers=headers, data=kwargs) ret = getattr(requests, func)(endpoint, headers=headers, data=kwargs)
ret.raise_for_status() ret.raise_for_status()
return ret.json() return ret.json()
except: except requests.HTTPError as e:
if raise_for_status:
raise e
logger.exception("Error encountered while performing API request to SeAT with url {}".format(endpoint)) logger.exception("Error encountered while performing API request to SeAT with url {}".format(endpoint))
return {} return {}
@classmethod @classmethod
def add_user(cls, username, email): def add_user(cls, username, email):
""" Add user to service """ """ Add user to service """
sanatized = str(SeatManager.__santatize_username(username)) sanitized = str(cls.__sanitize_username(username))
logger.debug("Adding user to SeAT with username %s" % sanatized) logger.debug("Adding user to SeAT with username %s" % sanitized)
password = SeatManager.__generate_random_pass() password = cls.__generate_random_pass()
ret = SeatManager.exec_request('user', 'post', username=sanatized, email=str(email), password=password) ret = cls.exec_request('user', 'post', username=sanitized, email=str(email), password=password)
logger.debug(ret) logger.debug(ret)
if cls._response_ok(ret): if cls._response_ok(ret):
logger.info("Added SeAT user with username %s" % sanatized) logger.info("Added SeAT user with username %s" % sanitized)
return sanatized, password return sanitized, password
logger.info("Failed to add SeAT user with username %s" % sanatized) logger.info("Failed to add SeAT user with username %s" % sanitized)
return None return None
@classmethod @classmethod
@@ -93,7 +97,7 @@ class SeatManager:
@classmethod @classmethod
def enable_user(cls, username): def enable_user(cls, username):
""" Enable user """ """ Enable user """
ret = SeatManager.exec_request('user/{}'.format(username), 'put', active=1) ret = cls.exec_request('user/{}'.format(username), 'put', active=1)
logger.debug(ret) logger.debug(ret)
if cls._response_ok(ret): if cls._response_ok(ret):
logger.info("Enabled SeAT user with username %s" % username) logger.info("Enabled SeAT user with username %s" % username)
@@ -104,13 +108,12 @@ class SeatManager:
@classmethod @classmethod
def update_user(cls, username, email, password): def update_user(cls, username, email, password):
""" Edit user info """ """ Edit user info """
logger.debug("Updating SeAT username %s with email %s and password hash starting with %s" % (username, email, logger.debug("Updating SeAT username %s with email %s and password" % (username, email))
password[0:5])) ret = cls.exec_request('user/{}'.format(username), 'put', email=email)
ret = SeatManager.exec_request('user/{}'.format(username), 'put', email=email)
logger.debug(ret) logger.debug(ret)
if not cls._response_ok(ret): if not cls._response_ok(ret):
logger.warn("Failed to update email for username {}".format(username)) logger.warn("Failed to update email for username {}".format(username))
ret = SeatManager.exec_request('user/{}'.format(username), 'put', password=password) ret = cls.exec_request('user/{}'.format(username), 'put', password=password)
logger.debug(ret) logger.debug(ret)
if not cls._response_ok(ret): if not cls._response_ok(ret):
logger.warn("Failed to update password for username {}".format(username)) logger.warn("Failed to update password for username {}".format(username))
@@ -118,25 +121,25 @@ class SeatManager:
logger.info("Updated SeAT user with username %s" % username) logger.info("Updated SeAT user with username %s" % username)
return username return username
@staticmethod @classmethod
def update_user_password(username, email, plain_password=None): def update_user_password(cls, username, email, plain_password=None):
logger.debug("Settings new SeAT password for user %s" % username) logger.debug("Settings new SeAT password for user %s" % username)
if not plain_password: if not plain_password:
plain_password = SeatManager.__generate_random_pass() plain_password = cls.__generate_random_pass()
if SeatManager.update_user(username, email, plain_password): if cls.update_user(username, email, plain_password):
return plain_password return plain_password
@staticmethod @classmethod
def check_user_status(username): def check_user_status(cls, username):
sanatized = str(SeatManager.__santatize_username(username)) sanitized = str(cls.__sanitize_username(username))
logger.debug("Checking SeAT status for user %s" % sanatized) logger.debug("Checking SeAT status for user %s" % sanitized)
ret = SeatManager.exec_request('user/{}'.format(sanatized), 'get') ret = cls.exec_request('user/{}'.format(sanitized), 'get')
logger.debug(ret) logger.debug(ret)
return ret return ret
@staticmethod @classmethod
def get_all_seat_eveapis(): def get_all_seat_eveapis(cls):
seat_all_keys = SeatManager.exec_request('key', 'get') seat_all_keys = cls.exec_request('key', 'get')
seat_keys = {} seat_keys = {}
for key in seat_all_keys: for key in seat_all_keys:
try: try:
@@ -145,117 +148,132 @@ class SeatManager:
seat_keys[key["key_id"]] = None seat_keys[key["key_id"]] = None
return seat_keys return seat_keys
@classmethod
def synchronize_eveapis(cls, user=None):
# Fetch all of the API keys stored in SeAT already
seat_all_keys = cls.get_all_seat_eveapis()
@staticmethod
def synchronize_eveapis(user=None):
seat_all_keys = SeatManager.get_all_seat_eveapis()
userinfo = None
# retrieve only user-specific api keys if user is specified # retrieve only user-specific api keys if user is specified
if user: if user:
keypars = EveManager.get_api_key_pairs(user) keypairs = EveManager.get_api_key_pairs(user)
try:
userinfo = SeatManager.check_user_status(user.seat.username)
except ObjectDoesNotExist:
pass
else: else:
# retrieve all api keys instead # retrieve all api keys instead
keypars = EveManager.get_all_api_key_pairs() keypairs = EveManager.get_all_api_key_pairs()
if keypars:
for keypar in keypars:
if keypar.api_id not in seat_all_keys.keys():
#Add new keys
logger.debug("Adding Api Key with ID %s" % keypar.api_id)
ret = SeatManager.exec_request('key', 'post', key_id=keypar.api_id, v_code=keypar.api_key)
logger.debug(ret)
else:
# remove it from the list so it doesn't get deleted in the last step
seat_all_keys.pop(keypar.api_id)
if not userinfo: # TODO: should the following be done only for new keys?
# Check the key's user status
logger.debug("Retrieving user name from Auth's SeAT users database")
try:
if keypar.user.seat.username:
logger.debug("Retrieving user %s info from SeAT users database" % keypar.user.seat.username)
userinfo = SeatManager.check_user_status(keypar.user.seat.username)
except ObjectDoesNotExist:
pass
if userinfo:
try:
# If the user has activated seat, assign the key to him.
logger.debug("Transferring Api Key with ID %s to user %s with ID %s " % (
keypar.api_id,
keypar.user.seat.username,
userinfo['id']))
ret = SeatManager.exec_request('key/transfer/{}/{}'.format(keypar.api_id, userinfo['id']),
'get')
logger.debug(ret)
except ObjectDoesNotExist:
logger.debug("User does not have SeAT activated, could not assign key to user")
if bool(seat_all_keys) and not user and hasattr(settings, 'SEAT_PURGE_DELETED') and settings.SEAT_PURGE_DELETED: for keypair in keypairs:
# Transfer the key if it isn't already in SeAT
if keypair.api_id not in seat_all_keys.keys():
# Add new keys
logger.debug("Adding Api Key with ID %s" % keypair.api_id)
try:
ret = cls.exec_request('key', 'post',
key_id=keypair.api_id,
v_code=keypair.api_key,
raise_for_status=True)
logger.debug(ret)
except requests.HTTPError as e:
if e.response.status_code == 400:
logger.debug("API key already exists")
else:
logger.exception("API key sync failed")
continue # Skip the rest of the key processing
else:
# remove it from the list so it doesn't get deleted in the last step
seat_all_keys.pop(keypair.api_id)
# Attach API key to the users SeAT account, if possible
try:
userinfo = cache.get_or_set('seat_user_status_' + cls.username_hash(keypair.user.seat.username),
lambda: cls.check_user_status(keypair.user.seat.username),
300) # Cache for 5 minutes
if not bool(userinfo):
# No SeAT account, skip
logger.debug("Could not find users SeAT id, cannot assign key to them")
continue
# If the user has activated seat, assign the key to them
logger.debug("Transferring Api Key with ID %s to user %s with ID %s " % (
keypair.api_id,
keypair.user.seat.username,
userinfo['id']))
ret = cls.exec_request('key/transfer/{}/{}'.format(keypair.api_id, userinfo['id']),
'get')
logger.debug(ret)
except ObjectDoesNotExist:
logger.debug("User does not have SeAT activated, could not assign key to user")
if bool(seat_all_keys) and not user and getattr(settings, 'SEAT_PURGE_DELETED', False):
# remove from SeAT keys that were removed from Auth # remove from SeAT keys that were removed from Auth
for key, key_user in iteritems(seat_all_keys): for key, key_user in iteritems(seat_all_keys):
# Remove the key only if it is an account or character key # Remove the key only if it is an account or character key
ret = SeatManager.exec_request('key/{}'.format(key), 'get') ret = cls.exec_request('key/{}'.format(key), 'get')
logger.debug(ret) logger.debug(ret)
try: try:
if (ret['info']['type'] == "Account") or (ret['info']['type'] == "Character"): if (ret['info']['type'] == "Account") or (ret['info']['type'] == "Character"):
logger.debug("Removing api key %s from SeAT database" % key) logger.debug("Removing api key %s from SeAT database" % key)
ret = SeatManager.exec_request('key/{}'.format(key), 'delete') ret = cls.exec_request('key/{}'.format(key), 'delete')
logger.debug(ret) logger.debug(ret)
except KeyError: except KeyError:
pass pass
@staticmethod @classmethod
def get_all_roles(): def get_all_roles(cls):
groups = {} groups = {}
ret = SeatManager.exec_request('role', 'get') ret = cls.exec_request('role', 'get')
logger.debug(ret) logger.debug(ret)
for group in ret: for group in ret:
groups[group["title"]] = group["id"] groups[group["title"]] = group["id"]
logger.debug("Retrieved role list from SeAT: %s" % str(groups)) logger.debug("Retrieved role list from SeAT: %s" % str(groups))
return groups return groups
@staticmethod @classmethod
def add_role(role): def add_role(cls, role):
ret = SeatManager.exec_request('role/new', 'post', name=role) ret = cls.exec_request('role/new', 'post', name=role)
logger.debug(ret) logger.debug(ret)
logger.info("Added Seat group %s" % role) logger.info("Added Seat group %s" % role)
role_info = SeatManager.exec_request('role/detail/{}'.format(role), 'get') role_info = cls.exec_request('role/detail/{}'.format(role), 'get')
logger.debug(role_info) logger.debug(role_info)
return role_info["id"] return role_info["id"]
@staticmethod @classmethod
def add_role_to_user(user_id, role_id): def add_role_to_user(cls, user_id, role_id):
ret = SeatManager.exec_request('role/grant-user-role/{}/{}'.format(user_id, role_id), 'get') ret = cls.exec_request('role/grant-user-role/{}/{}'.format(user_id, role_id), 'get')
logger.info("Added role %s to user %s" % (role_id, user_id)) logger.info("Added role %s to user %s" % (role_id, user_id))
return ret return ret
@staticmethod @classmethod
def revoke_role_from_user(user_id, role_id): def revoke_role_from_user(cls, user_id, role_id):
ret = SeatManager.exec_request('role/revoke-user-role/{}/{}'.format(user_id, role_id), 'get') ret = cls.exec_request('role/revoke-user-role/{}/{}'.format(user_id, role_id), 'get')
logger.info("Revoked role %s from user %s" % (role_id, user_id)) logger.info("Revoked role %s from user %s" % (role_id, user_id))
return ret return ret
@staticmethod @classmethod
def update_roles(seat_user, roles): def update_roles(cls, seat_user, roles):
logger.debug("Updating SeAT user %s with roles %s" % (seat_user, roles)) logger.debug("Updating SeAT user %s with roles %s" % (seat_user, roles))
user_info = SeatManager.check_user_status(seat_user) user_info = cls.check_user_status(seat_user)
user_roles = {} user_roles = {}
if type(user_info["roles"]) is list: if type(user_info["roles"]) is list:
for role in user_info["roles"]: for role in user_info["roles"]:
user_roles[role["title"]] = role["id"] user_roles[role["title"]] = role["id"]
logger.debug("Got user %s SeAT roles %s" % (seat_user, user_roles)) logger.debug("Got user %s SeAT roles %s" % (seat_user, user_roles))
seat_roles = SeatManager.get_all_roles() seat_roles = cls.get_all_roles()
addroles = set(roles) - set(user_roles.keys()) addroles = set(roles) - set(user_roles.keys())
remroles = set(user_roles.keys()) - set(roles) remroles = set(user_roles.keys()) - set(roles)
logger.info("Updating SeAT roles for user %s - adding %s, removing %s" % (seat_user, addroles, remroles)) logger.info("Updating SeAT roles for user %s - adding %s, removing %s" % (seat_user, addroles, remroles))
for r in addroles: for r in addroles:
if r not in seat_roles: if r not in seat_roles:
seat_roles[r] = SeatManager.add_role(r) seat_roles[r] = cls.add_role(r)
logger.debug("Adding role %s to SeAT user %s" % (r, seat_user)) logger.debug("Adding role %s to SeAT user %s" % (r, seat_user))
SeatManager.add_role_to_user(user_info["id"], seat_roles[r]) cls.add_role_to_user(user_info["id"], seat_roles[r])
for r in remroles: for r in remroles:
logger.debug("Removing role %s from user %s" % (r, seat_user)) logger.debug("Removing role %s from user %s" % (r, seat_user))
SeatManager.revoke_role_from_user(user_info["id"], seat_roles[r]) cls.revoke_role_from_user(user_info["id"], seat_roles[r])
@staticmethod
def username_hash(username):
m = hashlib.sha1()
m.update(username)
return m.hexdigest()

View File

@@ -1,6 +1,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.contrib import messages
from django.utils.translation import ugettext_lazy as _
from .manager import SeatManager from .manager import SeatManager
@@ -15,6 +17,7 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
ACCESS_PERM = 'seat.access_seat' ACCESS_PERM = 'seat.access_seat'
SERVICE_NAME = {'service': 'SeAT'}
@login_required @login_required
@@ -39,6 +42,8 @@ def activate_seat(request):
logger.debug("Updated SeatUser for user %s with SeAT credentials. Adding eve-apis..." % request.user) logger.debug("Updated SeatUser for user %s with SeAT credentials. Adding eve-apis..." % request.user)
SeatTasks.update_roles.delay(request.user.pk) SeatTasks.update_roles.delay(request.user.pk)
logger.info("Successfully activated SeAT for user %s" % request.user) logger.info("Successfully activated SeAT for user %s" % request.user)
messages.add_message(request, messages.SUCCESS, _('Successfully activated your %(service)s account.') %
SERVICE_NAME)
SeatManager.synchronize_eveapis(request.user) SeatManager.synchronize_eveapis(request.user)
credentials = { credentials = {
'username': request.user.seat.username, 'username': request.user.seat.username,
@@ -46,6 +51,9 @@ def activate_seat(request):
} }
return render(request, 'registered/service_credentials.html', return render(request, 'registered/service_credentials.html',
context={'credentials': credentials, 'service': 'SeAT'}) context={'credentials': credentials, 'service': 'SeAT'})
messages.add_message(request, messages.ERROR,
_('Failed to activate your %(service)s account, please contact your administrator.') %
SERVICE_NAME)
logger.error("Unsuccessful attempt to activate seat for user %s" % request.user) logger.error("Unsuccessful attempt to activate seat for user %s" % request.user)
return redirect("auth_services") return redirect("auth_services")
@@ -56,10 +64,15 @@ def deactivate_seat(request):
logger.debug("deactivate_seat called by user %s" % request.user) logger.debug("deactivate_seat called by user %s" % request.user)
# false we failed # false we failed
if SeatTasks.delete_user(request.user): if SeatTasks.delete_user(request.user):
messages.add_message(request, messages.SUCCESS,
_('Successfully deactivated your %(service)s account.') % SERVICE_NAME)
logger.info("Successfully deactivated SeAT for user %s" % request.user) logger.info("Successfully deactivated SeAT for user %s" % request.user)
return redirect("auth_services") return redirect("auth_services")
else: else:
logging.error("User does not have a SeAT account") logging.error("User does not have a SeAT account")
messages.add_message(request, messages.ERROR,
_('Failed to deactivate your %(service)s account, please contact your administrator.') %
SERVICE_NAME)
logger.error("Unsuccessful attempt to activate SeAT for user %s" % request.user) logger.error("Unsuccessful attempt to activate SeAT for user %s" % request.user)
return redirect("auth_services") return redirect("auth_services")
@@ -76,10 +89,15 @@ def reset_seat_password(request):
'username': request.user.seat.username, 'username': request.user.seat.username,
'password': result, 'password': result,
} }
messages.add_message(request, messages.SUCCESS,
_('Successfully reset your %(service)s password.') % {'service': 'SeAT'})
logger.info("Succesfully reset SeAT password for user %s" % request.user) logger.info("Succesfully reset SeAT password for user %s" % request.user)
return render(request, 'registered/service_credentials.html', return render(request, 'registered/service_credentials.html',
context={'credentials': credentials, 'service': 'SeAT'}) context={'credentials': credentials, 'service': 'SeAT'})
logger.error("Unsuccessful attempt to reset SeAT password for user %s" % request.user) logger.error("Unsuccessful attempt to reset SeAT password for user %s" % request.user)
messages.add_message(request, messages.ERROR,
_('Failed to reset your %(service)s password, please contact your administrator.') %
{'service': 'SeAT'})
return redirect("auth_services") return redirect("auth_services")
@@ -98,11 +116,17 @@ def set_seat_password(request):
request.user.email, request.user.email,
plain_password=password) plain_password=password)
if result: if result:
messages.add_message(request, messages.SUCCESS,
_('Successfully set your %(service)s password.') % SERVICE_NAME)
logger.info("Succesfully reset SeAT password for user %s" % request.user) logger.info("Succesfully reset SeAT password for user %s" % request.user)
return redirect("auth_services") return redirect("auth_services")
else: else:
messages.add_message(request, messages.ERROR,
_('Failed to set your %(service)s password, please contact your administrator.') %
SERVICE_NAME)
logger.error("Failed to install custom SeAT password for user %s" % request.user) logger.error("Failed to install custom SeAT password for user %s" % request.user)
else: else:
messages.add_message(request, messages.ERROR, _('Invalid password.'))
logger.error("Invalid SeAT password provided") logger.error("Invalid SeAT password provided")
else: else:
logger.debug("Request is not type POST - providing empty form.") logger.debug("Request is not type POST - providing empty form.")

View File

@@ -10,6 +10,7 @@ class TeamspeakJoinForm(forms.Form):
username = forms.CharField(widget=forms.HiddenInput()) username = forms.CharField(widget=forms.HiddenInput())
def clean(self): def clean(self):
if Teamspeak3Manager._get_userid(self.cleaned_data['username']): with Teamspeak3Manager() as ts3man:
return self.cleaned_data if ts3man._get_userid(self.cleaned_data['username']):
return self.cleaned_data
raise forms.ValidationError(_("Unable to locate user %s on server") % self.cleaned_data['username']) raise forms.ValidationError(_("Unable to locate user %s on server") % self.cleaned_data['username'])

View File

@@ -12,7 +12,31 @@ logger = logging.getLogger(__name__)
class Teamspeak3Manager: class Teamspeak3Manager:
def __init__(self): def __init__(self):
pass self._server = None
@property
def server(self):
if self._server is not None and self._server._connected:
return self._server
else:
raise ValueError("Teamspeak not connected")
def connect(self):
self._server = self.__get_created_server()
return self
def disconnect(self):
self._server.disconnect()
self._server = None
def __enter__(self):
logger.debug("Entering with statement, connecting")
self.connect()
return self
def __exit__(self, _type, value, traceback):
logger.debug("Exiting with statement, cleaning up")
self.disconnect()
@staticmethod @staticmethod
def __get_created_server(): def __get_created_server():
@@ -38,12 +62,10 @@ class Teamspeak3Manager:
sanatized = "[BLUE][" + corp_ticker + "]" + username sanatized = "[BLUE][" + corp_ticker + "]" + username
return sanatized[:30] return sanatized[:30]
@staticmethod def _get_userid(self, uid):
def _get_userid(uid):
logger.debug("Looking for uid %s on TS3 server." % uid) logger.debug("Looking for uid %s on TS3 server." % uid)
server = Teamspeak3Manager.__get_created_server()
try: try:
ret = server.send_command('customsearch', {'ident': 'sso_uid', 'pattern': uid}) ret = self.server.send_command('customsearch', {'ident': 'sso_uid', 'pattern': uid})
if ret and 'keys' in ret and 'cldbid' in ret['keys']: if ret and 'keys' in ret and 'cldbid' in ret['keys']:
logger.debug("Got userid %s for uid %s" % (ret['keys']['cldbid'], uid)) logger.debug("Got userid %s for uid %s" % (ret['keys']['cldbid'], uid))
return ret['keys']['cldbid'] return ret['keys']['cldbid']
@@ -52,11 +74,9 @@ class Teamspeak3Manager:
raise e raise e
return None return None
@staticmethod def _group_id_by_name(self, groupname):
def _group_id_by_name(groupname):
server = Teamspeak3Manager.__get_created_server()
logger.debug("Looking for group %s on TS3 server." % groupname) logger.debug("Looking for group %s on TS3 server." % groupname)
group_cache = server.send_command('servergrouplist') group_cache = self.server.send_command('servergrouplist')
logger.debug("Received group cache from server: %s" % group_cache) logger.debug("Received group cache from server: %s" % group_cache)
for group in group_cache: for group in group_cache:
logger.debug("Checking group %s" % group) logger.debug("Checking group %s" % group)
@@ -66,34 +86,30 @@ class Teamspeak3Manager:
logger.debug("Group %s not found on server." % groupname) logger.debug("Group %s not found on server." % groupname)
return None return None
@staticmethod def _create_group(self, groupname):
def _create_group(groupname):
logger.debug("Creating group %s on TS3 server." % groupname) logger.debug("Creating group %s on TS3 server." % groupname)
server = Teamspeak3Manager.__get_created_server() sgid = self._group_id_by_name(groupname)
sgid = Teamspeak3Manager._group_id_by_name(groupname)
if not sgid: if not sgid:
logger.debug("Group does not yet exist. Proceeding with creation.") logger.debug("Group does not yet exist. Proceeding with creation.")
ret = server.send_command('servergroupadd', {'name': groupname}) ret = self.server.send_command('servergroupadd', {'name': groupname})
Teamspeak3Manager.__group_cache = None self.__group_cache = None
sgid = ret['keys']['sgid'] sgid = ret['keys']['sgid']
server.send_command('servergroupaddperm', self.server.send_command('servergroupaddperm',
{'sgid': sgid, 'permsid': 'i_group_needed_modify_power', 'permvalue': 75, {'sgid': sgid, 'permsid': 'i_group_needed_modify_power', 'permvalue': 75,
'permnegated': 0, 'permskip': 0}) 'permnegated': 0, 'permskip': 0})
server.send_command('servergroupaddperm', self.server.send_command('servergroupaddperm',
{'sgid': sgid, 'permsid': 'i_group_needed_member_add_power', 'permvalue': 100, {'sgid': sgid, 'permsid': 'i_group_needed_member_add_power', 'permvalue': 100,
'permnegated': 0, 'permskip': 0}) 'permnegated': 0, 'permskip': 0})
server.send_command('servergroupaddperm', self.server.send_command('servergroupaddperm',
{'sgid': sgid, 'permsid': 'i_group_needed_member_remove_power', 'permvalue': 100, {'sgid': sgid, 'permsid': 'i_group_needed_member_remove_power', 'permvalue': 100,
'permnegated': 0, 'permskip': 0}) 'permnegated': 0, 'permskip': 0})
logger.info("Created group on TS3 server with name %s and id %s" % (groupname, sgid)) logger.info("Created group on TS3 server with name %s and id %s" % (groupname, sgid))
return sgid return sgid
@staticmethod def _user_group_list(self, cldbid):
def _user_group_list(cldbid):
logger.debug("Retrieving group list for user with id %s" % cldbid) logger.debug("Retrieving group list for user with id %s" % cldbid)
server = Teamspeak3Manager.__get_created_server()
try: try:
groups = server.send_command('servergroupsbyclientid', {'cldbid': cldbid}) groups = self.server.send_command('servergroupsbyclientid', {'cldbid': cldbid})
except TeamspeakError as e: except TeamspeakError as e:
if e.code == '1281': # no groups if e.code == '1281': # no groups
groups = [] groups = []
@@ -112,11 +128,9 @@ class Teamspeak3Manager:
logger.debug("Returning name/id pairing: %s" % outlist) logger.debug("Returning name/id pairing: %s" % outlist)
return outlist return outlist
@staticmethod def _group_list(self):
def _group_list():
logger.debug("Retrieving group list on TS3 server.") logger.debug("Retrieving group list on TS3 server.")
server = Teamspeak3Manager.__get_created_server() group_cache = self.server.send_command('servergrouplist')
group_cache = server.send_command('servergrouplist')
logger.debug("Received group cache from server: %s" % group_cache) logger.debug("Received group cache from server: %s" % group_cache)
outlist = {} outlist = {}
if group_cache: if group_cache:
@@ -128,35 +142,30 @@ class Teamspeak3Manager:
logger.debug("Returning name/id pairing: %s" % outlist) logger.debug("Returning name/id pairing: %s" % outlist)
return outlist return outlist
@staticmethod def _add_user_to_group(self, uid, groupid):
def _add_user_to_group(uid, groupid):
logger.debug("Adding group id %s to TS3 user id %s" % (groupid, uid)) logger.debug("Adding group id %s to TS3 user id %s" % (groupid, uid))
server = Teamspeak3Manager.__get_created_server() user_groups = self._user_group_list(uid)
user_groups = Teamspeak3Manager._user_group_list(uid)
if groupid not in user_groups.values(): if groupid not in user_groups.values():
logger.debug("User does not have group already. Issuing command to add.") logger.debug("User does not have group already. Issuing command to add.")
server.send_command('servergroupaddclient', self.server.send_command('servergroupaddclient',
{'sgid': str(groupid), 'cldbid': uid}) {'sgid': str(groupid), 'cldbid': uid})
logger.info("Added user id %s to group id %s on TS3 server." % (uid, groupid)) logger.info("Added user id %s to group id %s on TS3 server." % (uid, groupid))
@staticmethod def _remove_user_from_group(self, uid, groupid):
def _remove_user_from_group(uid, groupid):
logger.debug("Removing group id %s from TS3 user id %s" % (groupid, uid)) logger.debug("Removing group id %s from TS3 user id %s" % (groupid, uid))
server = Teamspeak3Manager.__get_created_server() user_groups = self._user_group_list(uid)
user_groups = Teamspeak3Manager._user_group_list(uid)
if str(groupid) in user_groups.values(): if str(groupid) in user_groups.values():
logger.debug("User is in group. Issuing command to remove.") logger.debug("User is in group. Issuing command to remove.")
server.send_command('servergroupdelclient', self.server.send_command('servergroupdelclient',
{'sgid': str(groupid), 'cldbid': uid}) {'sgid': str(groupid), 'cldbid': uid})
logger.info("Removed user id %s from group id %s on TS3 server." % (uid, groupid)) logger.info("Removed user id %s from group id %s on TS3 server." % (uid, groupid))
@staticmethod def _sync_ts_group_db(self):
def _sync_ts_group_db():
logger.debug("_sync_ts_group_db function called.") logger.debug("_sync_ts_group_db function called.")
try: try:
remote_groups = Teamspeak3Manager._group_list() remote_groups = self._group_list()
local_groups = TSgroup.objects.all() local_groups = TSgroup.objects.all()
logger.debug("Comparing remote groups to TSgroup objects: %s" % local_groups) logger.debug("Comparing remote groups to TSgroup objects: %s" % local_groups)
for key in remote_groups: for key in remote_groups:
@@ -181,23 +190,20 @@ class Teamspeak3Manager:
except: except:
logger.exception("An unhandled exception has occured while syncing TS groups.") logger.exception("An unhandled exception has occured while syncing TS groups.")
@staticmethod def add_user(self, username, corp_ticker):
def add_user(username, corp_ticker): username_clean = self.__santatize_username(self.__generate_username(username, corp_ticker))
username_clean = Teamspeak3Manager.__santatize_username(Teamspeak3Manager.__generate_username(username,
corp_ticker))
server = Teamspeak3Manager.__get_created_server()
logger.debug("Adding user to TS3 server with cleaned username %s" % username_clean) logger.debug("Adding user to TS3 server with cleaned username %s" % username_clean)
server_groups = Teamspeak3Manager._group_list() server_groups = self._group_list()
if settings.DEFAULT_AUTH_GROUP not in server_groups: if settings.DEFAULT_AUTH_GROUP not in server_groups:
Teamspeak3Manager._create_group(settings.DEFAULT_AUTH_GROUP) self._create_group(settings.DEFAULT_AUTH_GROUP)
alliance_group_id = Teamspeak3Manager._group_id_by_name(settings.DEFAULT_AUTH_GROUP) alliance_group_id = self._group_id_by_name(settings.DEFAULT_AUTH_GROUP)
try: try:
ret = server.send_command('tokenadd', {'tokentype': 0, 'tokenid1': alliance_group_id, 'tokenid2': 0, ret = self.server.send_command('tokenadd', {'tokentype': 0, 'tokenid1': alliance_group_id, 'tokenid2': 0,
'tokendescription': username_clean, 'tokendescription': username_clean,
'tokencustomset': "ident=sso_uid value=%s" % username_clean}) 'tokencustomset': "ident=sso_uid value=%s" % username_clean})
except TeamspeakError as e: except TeamspeakError as e:
logger.error("Failed to add teamspeak user %s: %s" % (username, str(e))) logger.error("Failed to add teamspeak user %s: %s" % (username, str(e)))
return "","" return "",""
@@ -210,22 +216,19 @@ class Teamspeak3Manager:
logger.exception("Failed to add teamspeak user %s - received response: %s" % (username_clean, ret)) logger.exception("Failed to add teamspeak user %s - received response: %s" % (username_clean, ret))
return "", "" return "", ""
@staticmethod def add_blue_user(self, username, corp_ticker):
def add_blue_user(username, corp_ticker): username_clean = self.__santatize_username(self.__generate_username_blue(username, corp_ticker))
username_clean = Teamspeak3Manager.__santatize_username(Teamspeak3Manager.__generate_username_blue(username,
corp_ticker))
server = Teamspeak3Manager.__get_created_server()
logger.debug("Adding user to TS3 server with cleaned username %s" % username_clean) logger.debug("Adding user to TS3 server with cleaned username %s" % username_clean)
server_groups = Teamspeak3Manager._group_list() server_groups = self._group_list()
if settings.DEFAULT_BLUE_GROUP not in server_groups: if settings.DEFAULT_BLUE_GROUP not in server_groups:
Teamspeak3Manager._create_group(settings.DEFAULT_BLUE_GROUP) self._create_group(settings.DEFAULT_BLUE_GROUP)
blue_group_id = Teamspeak3Manager._group_id_by_name(settings.DEFAULT_BLUE_GROUP) blue_group_id = self._group_id_by_name(settings.DEFAULT_BLUE_GROUP)
try: try:
ret = server.send_command('tokenadd', {'tokentype': 0, 'tokenid1': blue_group_id, 'tokenid2': 0, ret = self.server.send_command('tokenadd', {'tokentype': 0, 'tokenid1': blue_group_id, 'tokenid2': 0,
'tokendescription': username_clean, 'tokendescription': username_clean,
'tokencustomset': "ident=sso_uid value=%s" % username_clean}) 'tokencustomset': "ident=sso_uid value=%s" % username_clean})
except TeamspeakError as e: except TeamspeakError as e:
logger.error("Failed to add blue teamspeak user %s: %s" % (username, str(e))) logger.error("Failed to add blue teamspeak user %s: %s" % (username, str(e)))
return "","" return "",""
@@ -238,24 +241,22 @@ class Teamspeak3Manager:
logger.exception("Failed to add blue teamspeak user %s - received response: %s" % (username_clean, ret)) logger.exception("Failed to add blue teamspeak user %s - received response: %s" % (username_clean, ret))
return "", "" return "", ""
@staticmethod def delete_user(self, uid):
def delete_user(uid): user = self._get_userid(uid)
server = Teamspeak3Manager.__get_created_server()
user = Teamspeak3Manager._get_userid(uid)
logger.debug("Deleting user %s with id %s from TS3 server." % (user, uid)) logger.debug("Deleting user %s with id %s from TS3 server." % (user, uid))
if user: if user:
clients = server.send_command('clientlist') clients = self.server.send_command('clientlist')
for client in clients: for client in clients:
try: try:
if client['keys']['client_database_id'] == user: if client['keys']['client_database_id'] == user:
logger.debug("Found user %s on TS3 server - issuing deletion command." % user) logger.debug("Found user %s on TS3 server - issuing deletion command." % user)
server.send_command('clientkick', {'clid': client['keys']['clid'], 'reasonid': 5, self.server.send_command('clientkick', {'clid': client['keys']['clid'], 'reasonid': 5,
'reasonmsg': 'Auth service deleted'}) 'reasonmsg': 'Auth service deleted'})
except: except:
logger.exception("Failed to delete user id %s from TS3 - received response %s" % (uid, client)) logger.exception("Failed to delete user id %s from TS3 - received response %s" % (uid, client))
return False return False
try: try:
ret = server.send_command('clientdbdelete', {'cldbid': user}) ret = self.server.send_command('clientdbdelete', {'cldbid': user})
except TeamspeakError as e: except TeamspeakError as e:
logger.error("Failed to delete teamspeak user %s: %s" % (uid, str(e))) logger.error("Failed to delete teamspeak user %s: %s" % (uid, str(e)))
return False return False
@@ -269,33 +270,29 @@ class Teamspeak3Manager:
logger.warn("User with id %s not found on TS3 server. Assuming succesful deletion." % uid) logger.warn("User with id %s not found on TS3 server. Assuming succesful deletion." % uid)
return True return True
@staticmethod def check_user_exists(self, uid):
def check_user_exists(uid): if self._get_userid(uid):
if Teamspeak3Manager._get_userid(uid):
return True return True
return False return False
@staticmethod def generate_new_permissionkey(self, uid, username, corpticker):
def generate_new_permissionkey(uid, username, corpticker):
logger.debug("Re-issuing permission key for user id %s" % uid) logger.debug("Re-issuing permission key for user id %s" % uid)
Teamspeak3Manager.delete_user(uid) self.delete_user(uid)
return Teamspeak3Manager.add_user(username, corpticker) return self.add_user(username, corpticker)
@staticmethod def generate_new_blue_permissionkey(self, uid, username, corpticker):
def generate_new_blue_permissionkey(uid, username, corpticker):
logger.debug("Re-issuing blue permission key for user id %s" % uid) logger.debug("Re-issuing blue permission key for user id %s" % uid)
Teamspeak3Manager.delete_user(uid) self.delete_user(uid)
return Teamspeak3Manager.add_blue_user(username, corpticker) return self.add_blue_user(username, corpticker)
@staticmethod def update_groups(self, uid, ts_groups):
def update_groups(uid, ts_groups):
logger.debug("Updating uid %s TS3 groups %s" % (uid, ts_groups)) logger.debug("Updating uid %s TS3 groups %s" % (uid, ts_groups))
userid = Teamspeak3Manager._get_userid(uid) userid = self._get_userid(uid)
addgroups = [] addgroups = []
remgroups = [] remgroups = []
if userid is not None: if userid is not None:
user_ts_groups = Teamspeak3Manager._user_group_list(userid) user_ts_groups = self._user_group_list(userid)
logger.debug("User has groups on TS3 server: %s" % user_ts_groups) logger.debug("User has groups on TS3 server: %s" % user_ts_groups)
for key in user_ts_groups: for key in user_ts_groups:
user_ts_groups[key] = int(user_ts_groups[key]) user_ts_groups[key] = int(user_ts_groups[key])
@@ -309,8 +306,8 @@ class Teamspeak3Manager:
for g in addgroups: for g in addgroups:
logger.info("Adding Teamspeak user %s into group %s" % (userid, g)) logger.info("Adding Teamspeak user %s into group %s" % (userid, g))
Teamspeak3Manager._add_user_to_group(userid, g) self._add_user_to_group(userid, g)
for g in remgroups: for g in remgroups:
logger.info("Removing Teamspeak user %s from group %s" % (userid, g)) logger.info("Removing Teamspeak user %s from group %s" % (userid, g))
Teamspeak3Manager._remove_user_from_group(userid, g) self._remove_user_from_group(userid, g)

View File

@@ -25,11 +25,12 @@ class Teamspeak3Tasks:
def delete_user(cls, user, notify_user=False): def delete_user(cls, user, notify_user=False):
if cls.has_account(user): if cls.has_account(user):
logger.debug("User %s has TS3 account %s. Deleting." % (user, user.teamspeak3.uid)) logger.debug("User %s has TS3 account %s. Deleting." % (user, user.teamspeak3.uid))
if Teamspeak3Manager.delete_user(user.teamspeak3.uid): with Teamspeak3Manager() as ts3man:
user.teamspeak3.delete() if ts3man.delete_user(user.teamspeak3.uid):
if notify_user: user.teamspeak3.delete()
notify(user, 'TeamSpeak3 Account Disabled', level='danger') if notify_user:
return True notify(user, 'TeamSpeak3 Account Disabled', level='danger')
return True
return False return False
@staticmethod @staticmethod
@@ -43,7 +44,8 @@ class Teamspeak3Tasks:
@app.task() @app.task()
def run_ts3_group_update(): def run_ts3_group_update():
logger.debug("TS3 installed. Syncing local group objects.") logger.debug("TS3 installed. Syncing local group objects.")
Teamspeak3Manager._sync_ts_group_db() with Teamspeak3Manager() as ts3man:
ts3man._sync_ts_group_db()
@staticmethod @staticmethod
def disable(): def disable():
@@ -73,7 +75,8 @@ class Teamspeak3Tasks:
groups[ts_group.ts_group_name] = ts_group.ts_group_id groups[ts_group.ts_group_name] = ts_group.ts_group_id
logger.debug("Updating user %s teamspeak3 groups to %s" % (user, groups)) logger.debug("Updating user %s teamspeak3 groups to %s" % (user, groups))
try: try:
Teamspeak3Manager.update_groups(user.teamspeak3.uid, groups) with Teamspeak3Manager() as ts3man:
ts3man.update_groups(user.teamspeak3.uid, groups)
logger.debug("Updated user %s teamspeak3 groups." % user) logger.debug("Updated user %s teamspeak3 groups." % user)
except TeamspeakError as e: except TeamspeakError as e:
logger.error("Error occured while syncing TS groups for %s: %s" % (user, str(e))) logger.error("Error occured while syncing TS groups for %s: %s" % (user, str(e)))

View File

@@ -1,34 +1,27 @@
{% load i18n %} {% load i18n %}
<tr>
<th class="text-center">{% trans "Service" %}</th>
<th class="text-center">{% trans "Unique ID" %}</th>
<th class="text-center">PermissionKey</th>
<th class="text-center">{% trans "Action" %}</th>
</tr>
<tr> <tr>
<td class="text-center">Teamspeak 3</td> <td class="text-center">Teamspeak 3</td>
<td class="text-center">{{ authinfo.teamspeak3_uid }}</td> <td class="text-center">{{ authinfo.teamspeak3_uid }}</td>
<td class="text-center">{{ authinfo.teamspeak3_perm_key }}</td> <td class="text-center"><a href="ts3server://{{ TEAMSPEAK3_PUBLIC_URL }}">{{ TEAMSPEAK3_PUBLIC_URL }}</a></td>
<td class="text-center"> <td class="text-center">
{% ifequal authinfo.teamspeak3_uid "" %} {% ifequal authinfo.teamspeak3_uid "" %}
<a href="{% url 'auth_activate_teamspeak3' %}" class="btn btn-warning"> <a href="{% url 'auth_activate_teamspeak3' %}" title="Activate" class="btn btn-warning">
<span class="glyphicon glyphicon-ok"></span> <span class="glyphicon glyphicon-ok"></span>
</a> </a>
{% else %} {% else %}
<a href="ts3server://{{ TEAMSPEAK3_PUBLIC_URL }}?token={{ authinfo.teamspeak3_perm_key }}&nickname={{ authinfo.teamspeak3_uid }}" <a href="{% url 'auth_verify_teamspeak3' %}" title="Verify Client ID" class="btn btn-success" title="Verify">
title="{% trans "Quick Link" %}" class="btn btn-info">
<span class="glyphicon glyphicon-link"></span>
</a>
<a href="{% url 'auth_verify_teamspeak3' %}" class="btn btn-success" title="Verify">
<span class="glyphicon glyphicon-log-in"></span> <span class="glyphicon glyphicon-log-in"></span>
</a> </a>
<a href="{% url 'auth_reset_teamspeak3_perm' %}" class="btn btn-primary"> <a href="{% url 'auth_reset_teamspeak3_perm' %}" title="Refresh Token" class="btn btn-primary">
<span class="glyphicon glyphicon-refresh"></span> <span class="glyphicon glyphicon-refresh"></span>
</a> </a>
<a href="{% url 'auth_deactivate_teamspeak3' %}" class="btn btn-danger"> <a href="{% url 'auth_deactivate_teamspeak3' %}" title="Deactivate" class="btn btn-danger">
<span class="glyphicon glyphicon-remove"></span> <span class="glyphicon glyphicon-remove"></span>
</a> </a>
<a href="ts3server://{{ TEAMSPEAK3_PUBLIC_URL }}?nickname={{ authinfo.teamspeak3_uid }}" title="Connect" class="btn btn-success">
<span class="glyphicon glyphicon-arrow-right"></span>
</a>
{% endifequal %} {% endifequal %}
</td> </td>
</tr> </tr>

View File

@@ -75,20 +75,22 @@ class Teamspeak3HooksTestCase(TestCase):
@mock.patch(MODULE_PATH + '.tasks.Teamspeak3Manager') @mock.patch(MODULE_PATH + '.tasks.Teamspeak3Manager')
def test_update_all_groups(self, manager): def test_update_all_groups(self, manager):
instance = manager.return_value.__enter__.return_value
service = self.service() service = self.service()
service.update_all_groups() service.update_all_groups()
# Check member and blue user have groups updated # Check member and blue user have groups updated
self.assertTrue(manager.update_groups.called) self.assertTrue(instance.update_groups.called)
self.assertEqual(manager.update_groups.call_count, 2) self.assertEqual(instance.update_groups.call_count, 2)
def test_update_groups(self): def test_update_groups(self):
# Check member has Member group updated # Check member has Member group updated
with mock.patch(MODULE_PATH + '.tasks.Teamspeak3Manager') as manager: with mock.patch(MODULE_PATH + '.tasks.Teamspeak3Manager') as manager:
instance = manager.return_value.__enter__.return_value
service = self.service() service = self.service()
member = User.objects.get(username=self.member) member = User.objects.get(username=self.member)
service.update_groups(member) service.update_groups(member)
self.assertTrue(manager.update_groups.called) self.assertTrue(instance.update_groups.called)
args, kwargs = manager.update_groups.call_args args, kwargs = instance.update_groups.call_args
# update_groups(user.teamspeak3.uid, groups) # update_groups(user.teamspeak3.uid, groups)
self.assertEqual({'Member': 1}, args[1]) # Check groups self.assertEqual({'Member': 1}, args[1]) # Check groups
self.assertEqual(self.member, args[0]) # Check uid self.assertEqual(self.member, args[0]) # Check uid
@@ -98,7 +100,7 @@ class Teamspeak3HooksTestCase(TestCase):
service = self.service() service = self.service()
none_user = User.objects.get(username=self.none_user) none_user = User.objects.get(username=self.none_user)
service.update_groups(none_user) service.update_groups(none_user)
self.assertFalse(manager.update_user_groups.called) self.assertFalse(manager.return_value.__enter__.return_value.update_user_groups.called)
@mock.patch(MODULE_PATH + '.tasks.Teamspeak3Manager') @mock.patch(MODULE_PATH + '.tasks.Teamspeak3Manager')
def test_validate_user(self, manager): def test_validate_user(self, manager):
@@ -112,7 +114,7 @@ class Teamspeak3HooksTestCase(TestCase):
none_user = User.objects.get(username=self.none_user) none_user = User.objects.get(username=self.none_user)
Teamspeak3User.objects.create(user=none_user, uid='abc123', perm_key='132ACB') Teamspeak3User.objects.create(user=none_user, uid='abc123', perm_key='132ACB')
service.validate_user(none_user) service.validate_user(none_user)
self.assertTrue(manager.delete_user.called) self.assertTrue(manager.return_value.__enter__.return_value.delete_user.called)
with self.assertRaises(ObjectDoesNotExist): with self.assertRaises(ObjectDoesNotExist):
none_teamspeak3 = User.objects.get(username=self.none_user).teamspeak3 none_teamspeak3 = User.objects.get(username=self.none_user).teamspeak3
@@ -124,7 +126,7 @@ class Teamspeak3HooksTestCase(TestCase):
result = service.delete_user(member) result = service.delete_user(member)
self.assertTrue(result) self.assertTrue(result)
self.assertTrue(manager.delete_user.called) self.assertTrue(manager.return_value.__enter__.return_value.delete_user.called)
with self.assertRaises(ObjectDoesNotExist): with self.assertRaises(ObjectDoesNotExist):
teamspeak3_user = User.objects.get(username=self.member).teamspeak3 teamspeak3_user = User.objects.get(username=self.member).teamspeak3
@@ -182,11 +184,12 @@ class Teamspeak3ViewsTestCase(TestCase):
def test_activate(self, manager, forms_manager): def test_activate(self, manager, forms_manager):
self.login() self.login()
expected_username = 'auth_member' expected_username = 'auth_member'
manager.add_user.return_value = (expected_username, 'abc123') instance = manager.return_value.__enter__.return_value
instance.add_user.return_value = (expected_username, 'abc123')
response = self.client.get(urls.reverse('auth_activate_teamspeak3')) response = self.client.get(urls.reverse('auth_activate_teamspeak3'))
self.assertTrue(manager.add_user.called) self.assertTrue(instance.add_user.called)
teamspeak3_user = Teamspeak3User.objects.get(user=self.member) teamspeak3_user = Teamspeak3User.objects.get(user=self.member)
self.assertTrue(teamspeak3_user.uid) self.assertTrue(teamspeak3_user.uid)
self.assertTrue(teamspeak3_user.perm_key) self.assertTrue(teamspeak3_user.perm_key)
@@ -197,11 +200,12 @@ class Teamspeak3ViewsTestCase(TestCase):
def test_activate_blue(self, manager, forms_manager): def test_activate_blue(self, manager, forms_manager):
self.login(self.blue_user) self.login(self.blue_user)
expected_username = 'auth_blue' expected_username = 'auth_blue'
manager.add_blue_user.return_value = (expected_username, 'abc123') instance = manager.return_value.__enter__.return_value
instance.add_blue_user.return_value = (expected_username, 'abc123')
response = self.client.get(urls.reverse('auth_activate_teamspeak3')) response = self.client.get(urls.reverse('auth_activate_teamspeak3'))
self.assertTrue(manager.add_blue_user.called) self.assertTrue(instance.add_blue_user.called)
teamspeak3_user = Teamspeak3User.objects.get(user=self.blue_user) teamspeak3_user = Teamspeak3User.objects.get(user=self.blue_user)
self.assertTrue(teamspeak3_user.uid) self.assertTrue(teamspeak3_user.uid)
self.assertTrue(teamspeak3_user.perm_key) self.assertTrue(teamspeak3_user.perm_key)
@@ -213,15 +217,16 @@ class Teamspeak3ViewsTestCase(TestCase):
self.login() self.login()
expected_username = 'auth_member' expected_username = 'auth_member'
forms_manager._get_userid.return_value = '1234' forms_instance = manager.return_value.__enter__.return_value
forms_instance._get_userid.return_value = '1234'
Teamspeak3User.objects.update_or_create(user=self.member, defaults={'uid': '1234', 'perm_key': '5678'}) Teamspeak3User.objects.update_or_create(user=self.member, defaults={'uid': '1234', 'perm_key': '5678'})
data = {'username': 'auth_member'} data = {'username': 'auth_member'}
response = self.client.post(urls.reverse('auth_verify_teamspeak3'), data) response = self.client.post(urls.reverse('auth_verify_teamspeak3'), data)
self.assertTrue(manager.update_groups.called)
self.assertRedirects(response, urls.reverse('auth_services'), target_status_code=200) self.assertRedirects(response, urls.reverse('auth_services'), target_status_code=200)
self.assertTrue(manager.return_value.__enter__.return_value.update_groups.called)
@mock.patch(MODULE_PATH + '.tasks.Teamspeak3Manager') @mock.patch(MODULE_PATH + '.tasks.Teamspeak3Manager')
def test_deactivate(self, manager): def test_deactivate(self, manager):
@@ -230,7 +235,7 @@ class Teamspeak3ViewsTestCase(TestCase):
response = self.client.get(urls.reverse('auth_deactivate_teamspeak3')) response = self.client.get(urls.reverse('auth_deactivate_teamspeak3'))
self.assertTrue(manager.delete_user.called) self.assertTrue(manager.return_value.__enter__.return_value.delete_user.called)
self.assertRedirects(response, expected_url=urls.reverse('auth_services'), target_status_code=200) self.assertRedirects(response, expected_url=urls.reverse('auth_services'), target_status_code=200)
with self.assertRaises(ObjectDoesNotExist): with self.assertRaises(ObjectDoesNotExist):
teamspeak3_user = User.objects.get(pk=self.member.pk).teamspeak3 teamspeak3_user = User.objects.get(pk=self.member.pk).teamspeak3
@@ -241,7 +246,7 @@ class Teamspeak3ViewsTestCase(TestCase):
self.login() self.login()
Teamspeak3User.objects.create(user=self.member, uid='some member') Teamspeak3User.objects.create(user=self.member, uid='some member')
manager.generate_new_permissionkey.return_value = "valid_member", "123abc" manager.return_value.__enter__.return_value.generate_new_permissionkey.return_value = "valid_member", "123abc"
response = self.client.get(urls.reverse('auth_reset_teamspeak3_perm')) response = self.client.get(urls.reverse('auth_reset_teamspeak3_perm'))
@@ -249,7 +254,7 @@ class Teamspeak3ViewsTestCase(TestCase):
ts3_user = Teamspeak3User.objects.get(uid='valid_member') ts3_user = Teamspeak3User.objects.get(uid='valid_member')
self.assertEqual(ts3_user.uid, 'valid_member') self.assertEqual(ts3_user.uid, 'valid_member')
self.assertEqual(ts3_user.perm_key, '123abc') self.assertEqual(ts3_user.perm_key, '123abc')
self.assertTrue(tasks_manager.update_groups.called) self.assertTrue(tasks_manager.return_value.__enter__.return_value.update_groups.called)
@mock.patch(MODULE_PATH + '.tasks.Teamspeak3Manager') @mock.patch(MODULE_PATH + '.tasks.Teamspeak3Manager')
@mock.patch(MODULE_PATH + '.views.Teamspeak3Manager') @mock.patch(MODULE_PATH + '.views.Teamspeak3Manager')
@@ -257,7 +262,8 @@ class Teamspeak3ViewsTestCase(TestCase):
self.login(self.blue_user) self.login(self.blue_user)
Teamspeak3User.objects.create(user=self.blue_user, uid='some member') Teamspeak3User.objects.create(user=self.blue_user, uid='some member')
manager.generate_new_blue_permissionkey.return_value = "valid_blue", "123abc" manager.return_value.__enter__.return_value.generate_new_blue_permissionkey.return_value = ("valid_blue",
"123abc")
response = self.client.get(urls.reverse('auth_reset_teamspeak3_perm')) response = self.client.get(urls.reverse('auth_reset_teamspeak3_perm'))
@@ -265,7 +271,7 @@ class Teamspeak3ViewsTestCase(TestCase):
ts3_user = Teamspeak3User.objects.get(uid='valid_blue') ts3_user = Teamspeak3User.objects.get(uid='valid_blue')
self.assertEqual(ts3_user.uid, 'valid_blue') self.assertEqual(ts3_user.uid, 'valid_blue')
self.assertEqual(ts3_user.perm_key, '123abc') self.assertEqual(ts3_user.perm_key, '123abc')
self.assertTrue(tasks_manager.update_groups.called) self.assertTrue(tasks_manager.return_value.__enter__.return_value.update_groups.called)
class Teamspeak3SignalsTestCase(TestCase): class Teamspeak3SignalsTestCase(TestCase):

View File

@@ -50,13 +50,24 @@ class TS3Proto:
return True return True
def disconnect(self): def disconnect(self):
self.send_command("quit") if self._connected:
self._conn.close() try:
self._connected = False self.send("quit")
self._log.info('Disconnected') self._conn.close()
except:
self._log.exception('Error while disconnecting')
self._connected = False
self._log.info('Disconnected')
else:
self._log.info("Not connected")
def send_command(self, command, keys=None, opts=None): def send_command(self, command, keys=None, opts=None):
cmd = self.construct_command(command, keys=keys, opts=opts) cmd = self.construct_command(command, keys=keys, opts=opts)
# Clear read buffer of any stray bytes
self._conn.read_very_eager()
# Send command
self.send('%s\n' % cmd) self.send('%s\n' % cmd)
data = [] data = []

View File

@@ -28,17 +28,17 @@ def activate_teamspeak3(request):
authinfo = AuthServicesInfo.objects.get(user=request.user) authinfo = AuthServicesInfo.objects.get(user=request.user)
character = EveManager.get_main_character(request.user) character = EveManager.get_main_character(request.user)
ticker = character.corporation_ticker ticker = character.corporation_ticker
with Teamspeak3Manager() as ts3man:
if authinfo.state == BLUE_STATE: if authinfo.state == BLUE_STATE:
logger.debug("Adding TS3 user for blue user %s with main character %s" % (request.user, character)) logger.debug("Adding TS3 user for blue user %s with main character %s" % (request.user, character))
# Blue members should have alliance ticker (if in alliance) # Blue members should have alliance ticker (if in alliance)
if EveAllianceInfo.objects.filter(alliance_id=character.alliance_id).exists(): if EveAllianceInfo.objects.filter(alliance_id=character.alliance_id).exists():
alliance = EveAllianceInfo.objects.filter(alliance_id=character.alliance_id)[0] alliance = EveAllianceInfo.objects.filter(alliance_id=character.alliance_id)[0]
ticker = alliance.alliance_ticker ticker = alliance.alliance_ticker
result = Teamspeak3Manager.add_blue_user(character.character_name, ticker) result = ts3man.add_blue_user(character.character_name, ticker)
else: else:
logger.debug("Adding TS3 user for user %s with main character %s" % (request.user, character)) logger.debug("Adding TS3 user for user %s with main character %s" % (request.user, character))
result = Teamspeak3Manager.add_user(character.character_name, ticker) result = ts3man.add_user(character.character_name, ticker)
# if its empty we failed # if its empty we failed
if result[0] is not "": if result[0] is not "":
@@ -66,7 +66,7 @@ def verify_teamspeak3(request):
logger.debug("Validated user %s joined TS server" % request.user) logger.debug("Validated user %s joined TS server" % request.user)
return redirect("auth_services") return redirect("auth_services")
else: else:
form = TeamspeakJoinForm({'username': request.user.teamspeak3.uid}) form = TeamspeakJoinForm(initial={'username': request.user.teamspeak3.uid})
context = { context = {
'form': form, 'form': form,
'authinfo': {'teamspeak3_uid': request.user.teamspeak3.uid, 'authinfo': {'teamspeak3_uid': request.user.teamspeak3.uid,
@@ -82,8 +82,9 @@ def deactivate_teamspeak3(request):
if Teamspeak3Tasks.has_account(request.user) and Teamspeak3Tasks.delete_user(request.user): if Teamspeak3Tasks.has_account(request.user) and Teamspeak3Tasks.delete_user(request.user):
logger.info("Successfully deactivated TS3 for user %s" % request.user) logger.info("Successfully deactivated TS3 for user %s" % request.user)
messages.success(request, 'Deactivated TeamSpeak3 account.') messages.success(request, 'Deactivated TeamSpeak3 account.')
logger.error("Unsuccessful attempt to deactivate TS3 for user %s" % request.user) else:
messages.error(request, 'An error occurred while processing your TeamSpeak3 account.') logger.error("Unsuccessful attempt to deactivate TS3 for user %s" % request.user)
messages.error(request, 'An error occurred while processing your TeamSpeak3 account.')
return redirect("auth_services") return redirect("auth_services")
@@ -96,18 +97,19 @@ def reset_teamspeak3_perm(request):
authinfo = AuthServicesInfo.objects.get(user=request.user) authinfo = AuthServicesInfo.objects.get(user=request.user)
character = EveManager.get_main_character(request.user) character = EveManager.get_main_character(request.user)
logger.debug("Deleting TS3 user for user %s" % request.user) logger.debug("Deleting TS3 user for user %s" % request.user)
Teamspeak3Manager.delete_user(request.user.teamspeak3.uid) with Teamspeak3Manager() as ts3man:
ts3man.delete_user(request.user.teamspeak3.uid)
if authinfo.state == BLUE_STATE: if authinfo.state == BLUE_STATE:
logger.debug( logger.debug(
"Generating new permission key for blue user %s with main character %s" % (request.user, character)) "Generating new permission key for blue user %s with main character %s" % (request.user, character))
result = Teamspeak3Manager.generate_new_blue_permissionkey(request.user.teamspeak3.uid, result = ts3man.generate_new_blue_permissionkey(request.user.teamspeak3.uid,
character.character_name, character.character_name,
character.corporation_ticker) character.corporation_ticker)
else: else:
logger.debug("Generating new permission key for user %s with main character %s" % (request.user, character)) logger.debug("Generating new permission key for user %s with main character %s" % (request.user, character))
result = Teamspeak3Manager.generate_new_permissionkey(request.user.teamspeak3.uid, character.character_name, result = ts3man.generate_new_permissionkey(request.user.teamspeak3.uid, character.character_name,
character.corporation_ticker) character.corporation_ticker)
# if blank we failed # if blank we failed
if result[0] != "": if result[0] != "":

View File

@@ -53,7 +53,7 @@ def m2m_changed_user_permissions(sender, instance, action, *args, **kwargs):
svc.validate_user(instance) svc.validate_user(instance)
except: except:
logger.exception( logger.exception(
'Exception running validate_user for services module {} on user {}'.format(svc, user)) 'Exception running validate_user for services module {} on user {}'.format(svc, instance))
transaction.on_commit(lambda: validate_all_services()) transaction.on_commit(lambda: validate_all_services())

View File

@@ -25,9 +25,5 @@ class SrpFleetUserRequestForm(forms.Form):
return data return data
class SrpFleetUpdateCostForm(forms.Form):
srp_total_amount = forms.IntegerField(required=True, label=_("Total SRP Amount"))
class SrpFleetMainUpdateForm(forms.Form): class SrpFleetMainUpdateForm(forms.Form):
fleet_aar_link = forms.CharField(required=True, label=_("After Action Report Link")) fleet_aar_link = forms.CharField(required=True, label=_("After Action Report Link"))

View File

@@ -3,13 +3,13 @@ from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.decorators import permission_required from django.contrib.auth.decorators import permission_required
from django.contrib import messages from django.contrib import messages
from django.http import JsonResponse
from eveonline.managers import EveManager from eveonline.managers import EveManager
from authentication.models import AuthServicesInfo from authentication.models import AuthServicesInfo
from srp.models import SrpFleetMain from srp.models import SrpFleetMain
from srp.models import SrpUserRequest from srp.models import SrpUserRequest
from srp.form import SrpFleetMainForm from srp.form import SrpFleetMainForm
from srp.form import SrpFleetUserRequestForm from srp.form import SrpFleetUserRequestForm
from srp.form import SrpFleetUpdateCostForm
from srp.form import SrpFleetMainUpdateForm from srp.form import SrpFleetMainUpdateForm
from services.managers.srp_manager import srpManager from services.managers.srp_manager import srpManager
from notifications import notify from notifications import notify
@@ -58,7 +58,7 @@ def srp_fleet_view(request, fleet_id):
if SrpFleetMain.objects.filter(id=fleet_id).exists(): if SrpFleetMain.objects.filter(id=fleet_id).exists():
fleet_main = SrpFleetMain.objects.get(id=fleet_id) fleet_main = SrpFleetMain.objects.get(id=fleet_id)
context = {"fleet_id": fleet_id, "fleet_status": fleet_main.fleet_srp_status, context = {"fleet_id": fleet_id, "fleet_status": fleet_main.fleet_srp_status,
"srpfleetrequests": fleet_main.srpuserrequest_set.all(), "srpfleetrequests": fleet_main.srpuserrequest_set.order_by('srp_ship_name'),
"totalcost": fleet_main.total_cost} "totalcost": fleet_main.total_cost}
return render(request, 'registered/srpfleetdata.html', context=context) return render(request, 'registered/srpfleetdata.html', context=context)
@@ -194,18 +194,23 @@ def srp_fleet_mark_uncompleted(request, fleet_id):
@members_and_blues() @members_and_blues()
def srp_request_view(request, fleet_srp): def srp_request_view(request, fleet_srp):
logger.debug("srp_request_view called by user %s for fleet srp code %s" % (request.user, fleet_srp)) logger.debug("srp_request_view called by user %s for fleet srp code %s" % (request.user, fleet_srp))
completed = False
no_srp_code = False
if SrpFleetMain.objects.filter(fleet_srp_code=fleet_srp).exists() is False: if SrpFleetMain.objects.filter(fleet_srp_code=fleet_srp).exists() is False:
no_srp_code = True
logger.error("Unable to locate SRP Fleet using code %s for user %s" % (fleet_srp, request.user)) logger.error("Unable to locate SRP Fleet using code %s for user %s" % (fleet_srp, request.user))
messages.error(request,
_('Unable to locate SRP code with ID %(srpfleetid)s') % {"srpfleetid": fleet_srp})
return redirect("auth_srp_management_view")
if request.method == 'POST': if request.method == 'POST':
form = SrpFleetUserRequestForm(request.POST) form = SrpFleetUserRequestForm(request.POST)
logger.debug("Request type POST contains form valid: %s" % form.is_valid()) logger.debug("Request type POST contains form valid: %s" % form.is_valid())
if form.is_valid(): if form.is_valid():
if SrpUserRequest.objects.filter(killboard_link=form.cleaned_data['killboard_link']).exists():
messages.error(request,
_("This Killboard link has already been posted."))
return redirect("auth_srp_management_view")
authinfo = AuthServicesInfo.objects.get(user=request.user) authinfo = AuthServicesInfo.objects.get(user=request.user)
character = EveManager.get_character_by_id(authinfo.main_char_id) character = EveManager.get_character_by_id(authinfo.main_char_id)
srp_fleet_main = SrpFleetMain.objects.get(fleet_srp_code=fleet_srp) srp_fleet_main = SrpFleetMain.objects.get(fleet_srp_code=fleet_srp)
@@ -219,7 +224,7 @@ def srp_request_view(request, fleet_srp):
try: try:
srp_kill_link = srpManager.get_kill_id(srp_request.killboard_link) srp_kill_link = srpManager.get_kill_id(srp_request.killboard_link)
(ship_type_id, ship_value) = srpManager.get_kill_data(srp_kill_link) (ship_type_id, ship_value, victim_name) = srpManager.get_kill_data(srp_kill_link)
except ValueError: except ValueError:
logger.debug("User %s Submitted Invalid Killmail Link %s or server could not be reached" % ( logger.debug("User %s Submitted Invalid Killmail Link %s or server could not be reached" % (
request.user, srp_request.killboard_link)) request.user, srp_request.killboard_link))
@@ -227,117 +232,138 @@ def srp_request_view(request, fleet_srp):
messages.error(request, messages.error(request,
_("Your SRP request Killmail link is invalid. Please make sure you are using zKillboard.")) _("Your SRP request Killmail link is invalid. Please make sure you are using zKillboard."))
return redirect("auth_srp_management_view") return redirect("auth_srp_management_view")
srp_ship_name = EveManager.get_itemtype(ship_type_id).name
srp_request.srp_ship_name = srp_ship_name
kb_total_loss = ship_value
srp_request.kb_total_loss = kb_total_loss
srp_request.post_time = post_time
srp_request.save()
completed = True
logger.info("Created SRP Request on behalf of user %s for fleet name %s" % (
request.user, srp_fleet_main.fleet_name))
messages.success(request, _('Submitted SRP request for your %(ship)s.') % {"ship": srp_ship_name})
characters = EveManager.get_characters_by_owner_id(request.user.id)
for character in characters:
if character.character_name == victim_name:
srp_request.srp_ship_name = EveManager.get_itemtype(ship_type_id).name
srp_request.kb_total_loss = ship_value
srp_request.post_time = post_time
srp_request.save()
logger.info("Created SRP Request on behalf of user %s for fleet name %s" % (
request.user, srp_fleet_main.fleet_name))
messages.success(request, _('Submitted SRP request for your %(ship)s.') % {"ship": srp_request.srp_ship_name})
return redirect("auth_srp_management_view")
else:
continue
messages.error(request,
_("%(charname)s does not belong to your Auth account. Please add the API key for this character and try again")
% {"charname": victim_name})
return redirect("auth_srp_management_view")
else: else:
logger.debug("Returning blank SrpFleetUserRequestForm") logger.debug("Returning blank SrpFleetUserRequestForm")
form = SrpFleetUserRequestForm() form = SrpFleetUserRequestForm()
render_items = {'form': form, "completed": completed, "no_srp_code": no_srp_code} render_items = {'form': form}
return render(request, 'registered/srpfleetrequest.html', context=render_items) return render(request, 'registered/srpfleetrequest.html', context=render_items)
@login_required @login_required
@permission_required('auth.srp_management') @permission_required('auth.srp_management')
def srp_request_remove(request, srp_request_id): def srp_request_remove(request):
logger.debug("srp_request_remove called by user %s for srp request id %s" % (request.user, srp_request_id)) numrequests = len(request.POST)-1
logger.debug("srp_request_remove called by user %s for %s srp request id's" % (request.user, numrequests))
stored_fleet_view = None stored_fleet_view = None
for srp_request_id in request.POST:
if SrpUserRequest.objects.filter(id=srp_request_id).exists(): if numrequests == 0:
srpuserrequest = SrpUserRequest.objects.get(id=srp_request_id) messages.warning(request, _("No SRP requests selected"))
stored_fleet_view = srpuserrequest.srp_fleet_main.id return redirect("auth_srp_management_view")
srpuserrequest.delete() if srp_request_id == "csrfmiddlewaretoken":
logger.info("Deleted SRP request id %s for user %s" % (srp_request_id, request.user)) continue
messages.success(request, _('Deleted SRP request from %(character)s for their %(ship)s.') % { if SrpUserRequest.objects.filter(id=srp_request_id).exists():
"character": srpuserrequest.character, "ship": srpuserrequest.srp_ship_name}) srpuserrequest = SrpUserRequest.objects.get(id=srp_request_id)
stored_fleet_view = srpuserrequest.srp_fleet_main.id
srpuserrequest.delete()
logger.info("Deleted SRP request id %s for user %s" % (srp_request_id, request.user))
if stored_fleet_view is None: if stored_fleet_view is None:
logger.error("Unable to delete srp request id %s for user %s - request matching id not found." % ( logger.error("Unable to delete srp request id %s for user %s - request matching id not found." % (
srp_request_id, request.user)) srp_request_id, request.user))
messages.error(request, _('Unable to locate SRP request with ID %(requestid)s') % {"requestid": srp_request_id}) messages.error(request, _('Unable to locate SRP request with ID %(requestid)s') % {"requestid": srp_request_id})
return redirect("auth_srp_management_view") return redirect("auth_srp_management_view")
else: else:
messages.success(request, _('Deleted %(numrequests)s SRP requests') % {"numrequests": numrequests})
return redirect("auth_srp_fleet_view", stored_fleet_view) return redirect("auth_srp_fleet_view", stored_fleet_view)
@login_required @login_required
@permission_required('auth.srp_management') @permission_required('auth.srp_management')
def srp_request_approve(request, srp_request_id): def srp_request_approve(request):
logger.debug("srp_request_approve called by user %s for srp request id %s" % (request.user, srp_request_id)) numrequests = len(request.POST)-1
logger.debug("srp_request_approve called by user %s for %s srp request id's" % (request.user, numrequests))
stored_fleet_view = None stored_fleet_view = None
for srp_request_id in request.POST:
if SrpUserRequest.objects.filter(id=srp_request_id).exists(): if numrequests == 0:
srpuserrequest = SrpUserRequest.objects.get(id=srp_request_id) messages.warning(request, _("No SRP requests selected"))
stored_fleet_view = srpuserrequest.srp_fleet_main.id return redirect("auth_srp_management_view")
srpuserrequest.srp_status = "Approved" if srp_request_id == "csrfmiddlewaretoken":
if srpuserrequest.srp_total_amount == 0: continue
srpuserrequest.srp_total_amount = srpuserrequest.kb_total_loss if SrpUserRequest.objects.filter(id=srp_request_id).exists():
srpuserrequest.save() srpuserrequest = SrpUserRequest.objects.get(id=srp_request_id)
logger.info("Approved SRP request id %s for character %s by user %s" % ( stored_fleet_view = srpuserrequest.srp_fleet_main.id
srp_request_id, srpuserrequest.character, request.user)) srpuserrequest.srp_status = "Approved"
messages.success(request, _('Approved SRP request from %(character)s for their %(ship)s.') % { if srpuserrequest.srp_total_amount == 0:
"character": srpuserrequest.character, "ship": srpuserrequest.srp_ship_name}) srpuserrequest.srp_total_amount = srpuserrequest.kb_total_loss
notify( srpuserrequest.save()
srpuserrequest.character.user, logger.info("Approved SRP request id %s for character %s by user %s" % (
'SRP Request Approved', srp_request_id, srpuserrequest.character, request.user))
level='success', notify(
message='Your SRP request for a %s lost during %s has been approved for %s ISK.' % ( srpuserrequest.character.user,
srpuserrequest.srp_ship_name, srpuserrequest.srp_fleet_main.fleet_name, srpuserrequest.srp_total_amount) 'SRP Request Approved',
) level='success',
message='Your SRP request for a %s lost during %s has been approved for %s ISK.' % (
srpuserrequest.srp_ship_name, srpuserrequest.srp_fleet_main.fleet_name, srpuserrequest.srp_total_amount)
)
if stored_fleet_view is None: if stored_fleet_view is None:
logger.error("Unable to approve srp request id %s on behalf of user %s - request matching id not found." % ( logger.error("Unable to approve srp request id %s on behalf of user %s - request matching id not found." % (
srp_request_id, request.user)) srp_request_id, request.user))
messages.error(request, _('Unable to locate SRP request with ID %(requestid)s') % {"requestid": srp_request_id}) messages.error(request, _('Unable to locate SRP request with ID %(requestid)s') % {"requestid": srp_request_id})
return redirect("auth_srp_management_view") return redirect("auth_srp_management_view")
else: else:
messages.success(request, _('Approved %(numrequests)s SRP requests') % {"numrequests": numrequests})
return redirect("auth_srp_fleet_view", stored_fleet_view) return redirect("auth_srp_fleet_view", stored_fleet_view)
@login_required @login_required
@permission_required('auth.srp_management') @permission_required('auth.srp_management')
def srp_request_reject(request, srp_request_id): def srp_request_reject(request):
logger.debug("srp_request_reject called by user %s for srp request id %s" % (request.user, srp_request_id)) numrequests = len(request.POST)-1
logger.debug("srp_request_reject called by user %s for %s srp request id's" % (request.user, numrequests))
stored_fleet_view = None stored_fleet_view = None
for srp_request_id in request.POST:
if SrpUserRequest.objects.filter(id=srp_request_id).exists(): if numrequests == 0:
srpuserrequest = SrpUserRequest.objects.get(id=srp_request_id) messages.warning(request, _("No SRP requests selected"))
stored_fleet_view = srpuserrequest.srp_fleet_main.id return redirect("auth_srp_management_view")
srpuserrequest.srp_status = "Rejected" if srp_request_id == "csrfmiddlewaretoken":
srpuserrequest.save() continue
logger.info("SRP request id %s for character %s rejected by %s" % ( if SrpUserRequest.objects.filter(id=srp_request_id).exists():
srp_request_id, srpuserrequest.character, request.user)) srpuserrequest = SrpUserRequest.objects.get(id=srp_request_id)
messages.success(request, _('Rejected SRP request from %(character)s for their %(ship)s.') % { stored_fleet_view = srpuserrequest.srp_fleet_main.id
"character": srpuserrequest.character, "ship": srpuserrequest.srp_ship_name}) srpuserrequest.srp_status = "Rejected"
notify( srpuserrequest.save()
srpuserrequest.character.user, logger.info("SRP request id %s for character %s rejected by %s" % (
'SRP Request Rejected', srp_request_id, srpuserrequest.character, request.user))
level='danger', notify(
message='Your SRP request for a %s lost during %s has been rejected.' % ( srpuserrequest.character.user,
srpuserrequest.srp_ship_name, srpuserrequest.srp_fleet_main.fleet_name) 'SRP Request Rejected',
) level='danger',
message='Your SRP request for a %s lost during %s has been rejected.' % (
srpuserrequest.srp_ship_name, srpuserrequest.srp_fleet_main.fleet_name)
)
if stored_fleet_view is None: if stored_fleet_view is None:
logger.error("Unable to reject SRP request id %s on behalf of user %s - request matching id not found." % ( logger.error("Unable to reject SRP request id %s on behalf of user %s - request matching id not found." % (
srp_request_id, request.user)) srp_request_id, request.user))
messages.error(request, _('Unable to locate SRP request with ID %(requestid)s') % {"requestid": srp_request_id}) messages.error(request, _('Unable to locate SRP request with ID %(requestid)s') % {"requestid": srp_request_id})
return redirect("auth_srp_management_view") return redirect("auth_srp_management_view")
else: else:
messages.success(request, _('Rejected %(numrequests)s SRP requests.') % {"numrequests": numrequests})
return redirect("auth_srp_fleet_view", stored_fleet_view) return redirect("auth_srp_fleet_view", stored_fleet_view)
@login_required @login_required
@permission_required('auth.srp_management') @permission_required('auth.srp_management')
def srp_request_update_amount_view(request, fleet_srp_request_id): def srp_request_update_amount(request, fleet_srp_request_id):
logger.debug("srp_request_update_amount_view called by user %s for fleet srp request id %s" % ( logger.debug("srp_request_update_amount called by user %s for fleet srp request id %s" % (
request.user, fleet_srp_request_id)) request.user, fleet_srp_request_id))
if SrpUserRequest.objects.filter(id=fleet_srp_request_id).exists() is False: if SrpUserRequest.objects.filter(id=fleet_srp_request_id).exists() is False:
@@ -345,24 +371,12 @@ def srp_request_update_amount_view(request, fleet_srp_request_id):
messages.error(request, _('Unable to locate SRP request with ID %(requestid)s') % {"requestid": fleet_srp_request_id}) messages.error(request, _('Unable to locate SRP request with ID %(requestid)s') % {"requestid": fleet_srp_request_id})
return redirect("auth_srp_management_view") return redirect("auth_srp_management_view")
if request.method == 'POST': srp_request = SrpUserRequest.objects.get(id=fleet_srp_request_id)
form = SrpFleetUpdateCostForm(request.POST) srp_request.srp_total_amount = request.POST['value']
logger.debug("Request type POST contains form valid: %s" % form.is_valid()) srp_request.save()
if form.is_valid(): logger.info("Updated srp request id %s total to %s by user %s" % (
srp_request = SrpUserRequest.objects.get(id=fleet_srp_request_id) fleet_srp_request_id, request.POST['value'], request.user))
srp_request.srp_total_amount = form.cleaned_data['srp_total_amount'] return JsonResponse({"success":True,"pk":fleet_srp_request_id,"newValue":request.POST['value']})
srp_request.save()
logger.info("Updated srp request id %s total to %s by user %s" % (
fleet_srp_request_id, form.cleaned_data['srp_total_amount'], request.user))
messages.success(request, _('Updated SRP amount.'))
return redirect("auth_srp_fleet_view", srp_request.srp_fleet_main.id)
else:
logger.debug("Returning blank SrpFleetUpdateCostForm")
form = SrpFleetUpdateCostForm()
render_items = {'form': form}
return render(request, 'registered/srpfleetrequestamount.html', context=render_items)
@login_required @login_required

View File

@@ -0,0 +1,58 @@
.checkbox label:after,
.radio label:after {
content: '';
display: table;
clear: both;
}
.checkbox .cr,
.radio .cr {
position: relative;
display: inline-block;
border: 1px solid #a9a9a9;
border-radius: .25em;
width: 1.3em;
height: 1.3em;
float: left;
margin-right: .5em;
}
.radio .cr {
border-radius: 50%;
}
.checkbox .cr .cr-icon,
.radio .cr .cr-icon {
position: absolute;
font-size: .8em;
line-height: 0;
top: 50%;
left: 20%;
}
.radio .cr .cr-icon {
margin-left: 0.04em;
}
.checkbox label input[type="checkbox"],
.radio label input[type="radio"] {
display: none;
}
.checkbox label input[type="checkbox"] + .cr > .cr-icon,
.radio label input[type="radio"] + .cr > .cr-icon {
transform: scale(3) rotateZ(-20deg);
opacity: 0;
transition: all .3s ease-in;
}
.checkbox label input[type="checkbox"]:checked + .cr > .cr-icon,
.radio label input[type="radio"]:checked + .cr > .cr-icon {
transform: scale(1) rotateZ(0deg);
opacity: 1;
}
.checkbox label input[type="checkbox"]:disabled + .cr,
.radio label input[type="radio"]:disabled + .cr {
opacity: .5;
}

View File

@@ -0,0 +1,4 @@
{% load static %}
<!-- Start X-Editablle js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/x-editable/1.5.1/bootstrap3-editable/js/bootstrap-editable.min.js"></script>
<!-- End X-Editable js -->

View File

@@ -0,0 +1,4 @@
{% load staticfiles %}
<!-- X-Editable Core CSS -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/x-editable/1.5.1/bootstrap3-editable/css/bootstrap-editable.css" rel="stylesheet">
<!-- End Bootstrap CSS -->

View File

@@ -7,9 +7,6 @@
{% block title %}{% trans "Alliance Auth - Structure Timer Create" %}{% endblock %} {% block title %}{% trans "Alliance Auth - Structure Timer Create" %}{% endblock %}
{% block page_title %}{% trans "Timer Create" %}{% endblock page_title %} {% block page_title %}{% trans "Timer Create" %}{% endblock page_title %}
{% block extra_css %}
{% include 'bundles/jquery-datetimepicker-css.html' %}
{% endblock extra_css %}
{% block content %} {% block content %}
<div class="col-lg-12"> <div class="col-lg-12">
@@ -30,17 +27,3 @@
</div> </div>
{% endblock content %} {% endblock content %}
{% block extra_javascript %}
{% include 'bundles/jquery-datetimepicker-js.html' %}
{% endblock %}
{% block extra_script %}
$('#id_eve_time').datetimepicker({
lang: '{{ LANGUAGE_CODE }}',
maskInput: true,
format: 'Y-m-d H:i',minDate:0
});
{% endblock extra_script %}

View File

@@ -0,0 +1,44 @@
{% load i18n %}
{% block content %}
<table class="table">
<thead>
<tr>
<th class="text-center col-lg-3">{% trans "Operation Name" %}</th>
<th class="text-center col lg-2">{% trans "Doctrine" %}</th>
<th class="text-center col-lg-1">{% trans "Form Up System" %}</th>
<th class="text-center col-lg-1">{% trans "Start Time" %}</th>
<th class="text-center col-lg-1">{% trans "Local Time" %}</th>
<th class="text-center col-lg-1">{% trans "Duration" %}</th>
<th class="text-center col-lg-1">{% trans "FC" %}</th>
{% if perms.auth.optimer_management %}
<th class="text-center col-lg-1">{% trans "Creator" %}</th>
<th class="text-center col-lg-2">{% trans "Action" %}</th>
{% endif %}
</tr>
</thead>
{% for ops in timers %}
<tbody>
<tr>
<td class="text-center">{{ ops.operation_name }}</td>
<td class="text-center">{{ ops.doctrine }}</td>
<td class="text-center">
<a href="http://evemaps.dotlan.net/system/{{ ops.system }}">{{ ops.system }}</a>
</td>
<td class="text-center" nowrap>{{ ops.start | date:"Y-m-d H:i" }}</td>
<td class="text-center" nowrap><div id="localtime{{ ops.id }}"></div><div id="countdown{{ ops.id }}"></div></td>
<td class="text-center">{{ ops.duration }}</td>
<td class="text-center">{{ ops.fc }}</td>
{% if perms.auth.optimer_management %}
<td class="text-center">{{ ops.eve_character }}</td>
<td class="text-center">
<a href="{% url 'auth_remove_optimer' ops.id %}" class="btn btn-danger">
<span class="glyphicon glyphicon-remove"></span>
</a><a href="{% url 'auth_edit_optimer' ops.id %}" class="btn btn-info"><span class="glyphicon glyphicon-pencil"></span></a>
</td>
{% endif %}
</tr>
</tbody>
{% endfor %}
</table>
{% endblock content %}

View File

@@ -17,62 +17,26 @@
{% endif %} {% endif %}
</div> </div>
</h1> </h1>
<div class="col-lg-12 text-center row">
<div class="label label-info text-left">
<b>{% trans "Current Eve Time:" %} </b>
</div><div class="label label-info text-left" id="current-time"></div>
<br />
</div>
{% if optimer %}
<table class="table table-responsive">
<tr>
<th class="text-center">{% trans "Operation Name" %}</th>
<th class="text-center">{% trans "Doctrine" %}</th>
<th class="text-center">{% trans "Form Up System" %}</th>
<th class="text-center">{% trans "Form Up Location" %}</th>
<th class="text-center">{% trans "Start Time" %}</th>
<th class="text-center">{% trans "Local Time" %}</th>
<th class="text-center">{% trans "Duration" %}</th>
<th class="text-center">{% trans "FC" %}</th>
<th class="text-center">{% trans "Details" %}</th>
<th class="text-center">{% trans "Post Time" %}</th>
{% if perms.auth.optimer_management %}
<th class="text-center">{% trans "Creator" %}</th>
<th class="text-center">{% trans "Action" %}</th>
{% endif %}
</tr>
{% for ops in optimer %} <div class="col-lg-12 text-center row">
<tr> <div class="label label-info text-left">
<td style="width:150px" class="text-center">{{ ops.operation_name }}</td> <b>{% trans "Current Eve Time:" %} </b>
<td style="width:150px" class="text-center">{{ ops.doctrine }}</td> </div><div class="label label-info text-left" id="current-time"></div>
<td class="text-center"> <br />
<a href="http://evemaps.dotlan.net/system/{{ ops.system }}">{{ ops.system }}</a> </div>
</td>
<td style="width:150px" class="text-center">{{ ops.location }}</td> <h4><b>{% trans "Next Timers" %}</b></h4>
<td style="width:150px" class="text-center" nowrap>{{ ops.start | date:"Y-m-d H:i" }}</td> {% if future_timers %}
<td class="text-center" nowrap><div id="localtime{{ ops.id }}"></div><div id="countdown{{ ops.id }}"></div></td> {% include "registered/fleetoptable.html" with timers=future_timers %}
<td style="width:150px" class="text-center">{{ ops.duration }}</td>
<td style="width:150px" class="text-center">{{ ops.fc }}</td>
<td style="width:150px" class="text-center">{{ ops.details }}</td>
<td style="width:150px" class="text-center">{{ ops.post_time}}</td>
{% if perms.auth.optimer_management %}
<td style="width:150px" class="text-center">{{ ops.eve_character }}</td>
<td class="text-center">
<a href="{% url 'auth_remove_optimer' ops.id %}" class="btn btn-danger">
<span class="glyphicon glyphicon-remove"></span>
</a>
<a href="{% url 'auth_edit_optimer' ops.id %}" class="btn btn-info">
<span class="glyphicon glyphicon-pencil"></span>
</a>
</td>
{% endif %}
</tr>
{% endfor %}
</tr>
</table>
{% else %} {% else %}
<br /><div class="alert alert-warning text-center">{% trans "No fleet operations found." %}</div> <div class="alert alert-warning text-center">{% trans "No upcoming timers." %}</div>
{% endif %}
<h4><b>{% trans "Past Timers" %}</b></h4>
{% if past_timers %}
{% include "registered/fleetoptable.html" with timers=past_timers %}
{% else %}
<div class="alert alert-warning text-center">{% trans "No past timers." %}</div>
{% endif %} {% endif %}
</div> </div>

View File

@@ -7,8 +7,10 @@
{% block title %}Alliance Auth - Update Fleet Operation {% endblock %} {% block title %}Alliance Auth - Update Fleet Operation {% endblock %}
{% block page_title %}{% trans "Update AAR Link" %}{% endblock page_title %} {% block page_title %}{% trans "Update AAR Link" %}{% endblock page_title %}
{% block extra_css %} {% block extra_css %}
<link href="{% static 'css/jquery.datetimepicker.css' %}" rel="stylesheet" type="text/css">{% endblock extra_css %} {% include 'bundles/jquery-datetimepicker-css.html' %}
{% endblock extra_css %}
{% block content %} {% block content %}
@@ -37,6 +39,10 @@
{% endblock content %} {% endblock content %}
{% block extra_javascript %}
{% include 'bundles/jquery-datetimepicker-js.html' %}
{% endblock %}
{% block extra_script %} {% block extra_script %}
$('#id_start').datetimepicker({ $('#id_start').datetimepicker({

View File

@@ -10,7 +10,7 @@
{% block content %} {% block content %}
<div class="col-lg-12"> <div class="col-lg-12">
<h1 class="page-header text-center">{% trans "Available Services" %}</h1> <h1 class="page-header text-center">{% trans "Available Services" %}</h1>
<table class="table table-bordered"> <table class="table table-hover">
<tr> <tr>
<th class="text-center">{% trans "Service" %}</th> <th class="text-center">{% trans "Service" %}</th>
<th class="text-center">{% trans "Username" %}</th> <th class="text-center">{% trans "Username" %}</th>

View File

@@ -7,23 +7,23 @@
<td class="text-center"> <td class="text-center">
{% ifequal username "" %} {% ifequal username "" %}
{% if urls.auth_activate %} {% if urls.auth_activate %}
<a href="{% url urls.auth_activate %}" class="btn btn-warning"> <a href="{% url urls.auth_activate %}" title="Activate" class="btn btn-warning">
<span class="glyphicon glyphicon-ok"></span> <span class="glyphicon glyphicon-ok"></span>
</a> </a>
{% endif %} {% endif %}
{% else %} {% else %}
{% if urls.auth_set_password %} {% if urls.auth_set_password %}
<a href="{% url urls.auth_set_password %}" class="btn btn-warning"> <a href="{% url urls.auth_set_password %}" title="Set Password" class="btn btn-warning">
<span class="glyphicon glyphicon-pencil"></span> <span class="glyphicon glyphicon-pencil"></span>
</a> </a>
{% endif %} {% endif %}
{% if urls.auth_reset_password %} {% if urls.auth_reset_password %}
<a href="{% url urls.auth_reset_password %}" class="btn btn-primary"> <a href="{% url urls.auth_reset_password %}" title="Reset Password" class="btn btn-primary">
<span class="glyphicon glyphicon-refresh"></span> <span class="glyphicon glyphicon-refresh"></span>
</a> </a>
{% endif %} {% endif %}
{% if urls.auth_deactivate %} {% if urls.auth_deactivate %}
<a href="{% url urls.auth_deactivate %}" class="btn btn-danger"> <a href="{% url urls.auth_deactivate %}" title="Deactivate" class="btn btn-danger">
<span class="glyphicon glyphicon-remove"></span> <span class="glyphicon glyphicon-remove"></span>
</a> </a>
{% endif %} {% endif %}

View File

@@ -7,7 +7,8 @@
{% block page_title %}{% trans "SRP Fleet Create" %}{% endblock page_title %} {% block page_title %}{% trans "SRP Fleet Create" %}{% endblock page_title %}
{% block extra_css %} {% block extra_css %}
<link href="{% static 'css/jquery.datetimepicker.css' %}" rel="stylesheet" type="text/css">{% endblock extra_css %} {% include 'bundles/jquery-datetimepicker-css.html' %}
{% endblock extra_css %}
{% block content %} {% block content %}
@@ -40,6 +41,10 @@
{% endblock content %} {% endblock content %}
{% block extra_javascript %}
{% include 'bundles/jquery-datetimepicker-js.html' %}
{% endblock %}
{% block extra_script %} {% block extra_script %}
$('#id_fleet_time').datetimepicker({ $('#id_fleet_time').datetimepicker({

View File

@@ -7,7 +7,42 @@
{% block title %}Alliance Auth{% endblock %} {% block title %}Alliance Auth{% endblock %}
{% block page_title %}Srp Fleet Data{% endblock page_title %} {% block page_title %}Srp Fleet Data{% endblock page_title %}
{% block extra_css %}{% endblock extra_css %} {% block extra_css %}
{% include 'bundles/x-editable.css.html' %}
<link href="{% static 'css/checkbox.css' %}" rel="stylesheet" type="text/css">
<style>
.radio label, .checkbox label {
padding-left: 10px;
}
.editable {
width:150px;
text-align: center;
}
.editableform .form-control {
width: 95%;
text-align: center;
margin-left: 10px;
}
.editable-input {
width: 95%;
}
.radio, .checkbox {
margin-top: 0px;
margin-bottom: 0px;
}
.editable-error-block {
white-space: nowrap;
}
.editable-click, a.editable-click, a.editable-click:hover {
border-bottom: none;
}
.tooltip-inner {
white-space:pre;
max-width: none;
}
</style>
{% endblock extra_css %}
{% block content %} {% block content %}
<div class="col-lg-12"> <div class="col-lg-12">
@@ -27,92 +62,147 @@
{% endif %} {% endif %}
</div> </div>
</h1> </h1>
<div class="alert alert-info" role="alert">
<div class="text-right">
<b>{% trans "Total Losses:" %} {{ srpfleetrequests.count }}</b>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<b>{% trans "Total ISK Cost:" %} {{ totalcost | intcomma }}</b>
</div>
</div>
{% if srpfleetrequests %} {% if srpfleetrequests %}
<table class="table"> <form method="POST">
<tr> {% csrf_token %}
<th class="text-center">{% trans "Pilot Name" %}</th> <div class="alert alert-info" role="alert">
<th class="text-center">{% trans "Killboard Link" %}</th> <div class="text-right">
<th class="text-center">{% trans "Additional Info" %}</th> <b><span style="padding-right:2.5em">{% trans "Total Losses:" %} {{ srpfleetrequests.count }}</span></b>
<th class="text-center">{% trans "Ship Type" %}</th> <b><span style="padding-right:2.5em">{% trans "Total ISK Cost:" %} {{ totalcost | intcomma }}</span></b>
<th class="text-center">{% trans "Killboard Loss Amt" %}</th>
<th class="text-center">{% trans "SRP ISK Cost" %}</th>
<th class="text-center">{% trans "Post Time" %}</th>
<th class="text-center">{% trans "Status" %}</th>
{% if perms.auth.srp_management %}
<th class="text-center">{% trans "Actions" %}</th>
{% endif %}
</tr>
{% for srpfleetrequest in srpfleetrequests %}
<tr>
<td class="text-center">{{ srpfleetrequest.character.character_name }}</td>
<td class="text-center">
<a href="{{ srpfleetrequest.killboard_link }}"
target="_blank" class="label label-warning">Link</a>
</td>
<td class="text-center">{{ srpfleetrequest.additional_info }}</td>
<td class="text-center">{{ srpfleetrequest.srp_ship_name }}</td>
<td class="text-center">ISK: {{ srpfleetrequest.kb_total_loss | intcomma }}</td>
<td class="text-center">ISK: {{ srpfleetrequest.srp_total_amount | intcomma }}</td>
<td class="text-center">{{ srpfleetrequest.post_time | date:"Y-m-d H:i" }}</td>
<td class="text-center">
{% if srpfleetrequest.srp_status == "Approved" %}
<div class="label label-success">
{% trans "Approved" %}
</div>
{% elif srpfleetrequest.srp_status == "Rejected" %}
<div class="label label-danger">
{% trans "Rejected" %}
</div>
{% else %}
<div class="label label-warning">
{% trans "Pending" %}
</div>
{% endif %}
</td>
{% if perms.auth.srp_management %} {% if perms.auth.srp_management %}
<button type="submit" title="Approve" class="btn btn-success" formaction="{% url 'auth_srp_request_approve' %}">
<span class="glyphicon glyphicon-ok"></span>
</button>
<button type="submit" title="Reject" class="btn btn-warning" formaction="{% url 'auth_srp_request_reject' %}">
<span class="glyphicon glyphicon-remove"></span>
</button>
<button type="submit" title="Remove" onclick="return confirm('{% trans "Are you sure you want to delete SRP requests?" %}')" class="btn btn-danger" formaction="{% url 'auth_srp_request_remove' %}">
<span class="glyphicon glyphicon-trash"></span>
</button>
{% endif %}
</div>
</div>
<td class="text-center"> <table class="table">
<a href="{% url 'auth_srp_request_update_amount_view' srpfleetrequest.id %}" class="btn btn-info" title="Update Value"> <tr>
<span class="glyphicon glyphicon-usd"></span> <th class="text-center">{% trans "Pilot Name" %}</th>
</a> <th class="text-center">{% trans "Killboard Link" %}</th>
{% if srpfleetrequest.srp_status in "RejectedPending" %} <th class="text-center">{% trans "Additional Info" %}</th>
<a href="{% url 'auth_srp_request_approve' srpfleetrequest.id %}" class="btn btn-success" title="Approve"> <th class="text-center">{% trans "Ship Type" %}</th>
<span class="glyphicon glyphicon-ok"></span> <th class="text-center">{% trans "Killboard Loss Amt" %}</th>
</a> <th class="text-center">{% trans "SRP ISK Cost" %}
{% elif srpfleetrequest.srp_status == "" %} {% blocktrans %}<i class="glyphicon glyphicon-question-sign" rel="tooltip" title="Click value to edit
<a href="{% url 'auth_srp_request_approve' srpfleetrequest.id %}" class="btn btn-success" title="Approve"> Enter to save&next
<span class="glyphicon glyphicon-ok"></span> ESC to cancel"
</a> id="blah"></i></th>{% endblocktrans %}
{% endif %} <th class="text-center">{% trans "Post Time" %}</th>
{% if srpfleetrequest.srp_status in "ApprovedPending" %} <th class="text-center">{% trans "Status" %}</th>
<a href="{% url 'auth_srp_request_reject' srpfleetrequest.id %}" class="btn btn-warning" title="Reject"> {% if perms.auth.srp_management %}
<span class="glyphicon glyphicon-remove"></span> <th class="text-center">{% trans "Actions" %}</th>
</a>
{% elif srpfleetrequest.srp_status == "" %}
<a href="{% url 'auth_srp_request_reject' srpfleetrequest.id %}" class="btn btn-warning" title="Reject">
<span class="glyphicon glyphicon-remove"></span>
</a>
{% endif %}
<a href="{% url 'auth_srp_request_remove' srpfleetrequest.id %}" class="btn btn-danger" title="Remove">
<span class="glyphicon glyphicon-trash"></span>
</a>
</td>
{% endif %} {% endif %}
</tr> </tr>
{% endfor %} {% for srpfleetrequest in srpfleetrequests %}
</table> <tr>
<td class="text-center">{{ srpfleetrequest.character.character_name }}</td>
<td class="text-center">
<a href="{{ srpfleetrequest.killboard_link }}"
target="_blank" class="label label-warning">Link</a>
</td>
<td class="text-center">{{ srpfleetrequest.additional_info }}</td>
<td class="text-center">{{ srpfleetrequest.srp_ship_name }}</td>
<td class="text-center">{{ srpfleetrequest.kb_total_loss | intcomma }} ISK</td>
<td class="srp" data-name="srp_total_amount" data-type="number" data-pk="{{srpfleetrequest.id}}" data-url="{% url 'auth_srp_request_update_amount' srpfleetrequest.id %}" data-params="{csrfmiddlewaretoken:'{{csrf_token}}'}" class="text-center">{{ srpfleetrequest.srp_total_amount | intcomma }} ISK</td>
<td class="text-center">{{ srpfleetrequest.post_time | date:"Y-m-d H:i" }}</td>
<td class="text-center">
{% if srpfleetrequest.srp_status == "Approved" %}
<div class="label label-success">
{% trans "Approved" %}
</div>
{% elif srpfleetrequest.srp_status == "Rejected" %}
<div class="label label-danger">
{% trans "Rejected" %}
</div>
{% else %}
<div class="label label-warning">
{% trans "Pending" %}
</div>
{% endif %}
</td>
{% if perms.auth.srp_management %}
<td class="text-center">
<div class="checkbox">
<label style="font-size: 1.5em">
<input type="checkbox" name="{{srpfleetrequest.id}}">
<span class="cr"><i class="cr-icon fa fa-check"></i></span>
</label>
</div>
</td>
{% endif %}
</tr>
{% endfor %}
</table>
<div class="alert alert-info" role="alert">
<div class="text-right">
<b><span style="padding-right:2.5em">{% trans "Total Losses:" %} {{ srpfleetrequests.count }}</span></b>
<b><span style="padding-right:2.5em">{% trans "Total ISK Cost:" %} {{ totalcost | intcomma }}</span></b>
{% if perms.auth.srp_management %}
<button type="submit" title="Approve" class="btn btn-success" formaction="{% url 'auth_srp_request_approve' %}">
<span class="glyphicon glyphicon-ok"></span>
</button>
<button type="submit" title="Reject" class="btn btn-warning" formaction="{% url 'auth_srp_request_reject' %}">
<span class="glyphicon glyphicon-remove"></span>
</button>
<button type="submit" title="Remove" onclick="return confirm('{% trans "Are you sure you want to delete SRP requests?" %}')" class="btn btn-danger" formaction="{% url 'auth_srp_request_remove' %}">
<span class="glyphicon glyphicon-trash"></span>
</button>
{% endif %}
</div>
</div>
</form>
{% else %} {% else %}
<div class="alert alert-warning text-center">{% trans "No SRP requests for this fleet." %}</div> <div class="alert alert-warning text-center">{% trans "No SRP requests for this fleet." %}</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% endblock content %} {% endblock content %}
{% block extra_javascript %}
{% include 'bundles/x-editable-js.html' %}
{% endblock %}
{% block extra_script %}
$(document).ready(function() {
$.fn.editable.defaults.mode = 'inline';
$.fn.editable.defaults.showbuttons = false;
$.fn.editable.defaults.highlight = "#AAFF80";
$('.srp').editable({
display: function(value, response) {
return false;
},
success: function(response, newValue) {
newValue = parseInt(newValue);
newvalue = newValue.toLocaleString() + " ISK";
$(this).html(newvalue.bold());
},
validate: function(value) {
if (value === null || value === '') {
return 'Empty values not allowed';
}
}
});
$('.srp').on('hidden', function(e, reason){
if(reason === 'save' || reason === 'nochange') {
var $next = $(this).closest('tr').next().find('.editable');
setTimeout(function() {
$next.editable('show');
}, 400);
}
});
});
$(document).ready(function(){
$("[rel=tooltip]").tooltip({ placement: 'top'});
});
{% endblock extra_script %}

View File

@@ -6,8 +6,6 @@
{% block title %}Alliance Auth - SRP Request{% endblock %} {% block title %}Alliance Auth - SRP Request{% endblock %}
{% block page_title %}{% trans "SRP Request" %}{% endblock page_title %} {% block page_title %}{% trans "SRP Request" %}{% endblock page_title %}
{% block extra_css %}
<link href="{% static 'css/jquery.datetimepicker.css' %}" rel="stylesheet" type="text/css">{% endblock extra_css %}
{% block content %} {% block content %}
@@ -17,24 +15,13 @@
<div class="container-fluid"> <div class="container-fluid">
<div class="col-md-4 col-md-offset-4"> <div class="col-md-4 col-md-offset-4">
<div class="row"> <div class="row">
{% if no_srp_code %} <form class="form-signin" role="form" action="" method="POST">
<div class="alert alert-danger" role="alert">{% trans "SRP Code Does Not Exist" %}</div> {% csrf_token %}
{% else %} {{ form|bootstrap }}
{% if completed == False %} <br/>
<form class="form-signin" role="form" action="" method="POST"> <button class="btn btn-lg btn-primary btn-block" type="submit">{% trans "Create SRP Request" %}
{% csrf_token %} </button>
{{ form|bootstrap }} </form>
<br/>
<button class="btn btn-lg btn-primary btn-block" type="submit">{% trans "Create SRP Request" %}
</button>
</form>
{% else %}
<div class="alert alert-success" role="alert">{% trans "SRP Request Successfully Submitted" %}</div>
<div class="text-center">
<a href="{% url 'auth_srp_management_view' %}" class="btn btn-primary btn-lg">{% trans 'Continue' %}</a>
</div>
{% endif %}
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@@ -42,12 +29,3 @@
</div> </div>
{% endblock content %} {% endblock content %}
{% block extra_script %}
$('#id_fleet_time').datetimepicker({
maskInput: true,
format: 'Y-m-d H:i',minDate:0
});
{% endblock extra_script %}

View File

@@ -1,42 +0,0 @@
{% extends "public/base.html" %}
{% load bootstrap %}
{% load staticfiles %}
{% load i18n %}
{% block title %}Alliance Auth - Update SRP Amount{% endblock %}
{% block page_title %}{% trans "Update SRP Amount" %}{% endblock page_title %}
{% block extra_css %}
<link href="{% static 'css/jquery.datetimepicker.css' %}" rel="stylesheet" type="text/css">{% endblock extra_css %}
{% block content %}
<div class="col-lg-12">
<h1 class="page-header text-center">{% trans "Update SRP Amount" %}</h1>
<div class="container-fluid">
<div class="col-md-4 col-md-offset-4">
<div class="row">
<form class="form-signin" role="form" action="" method="POST">
{% csrf_token %}
{{ form|bootstrap }}
<br/>
<button class="btn btn-lg btn-primary btn-block" type="submit">{% trans "Update SRP Request Amount" %}
</button>
</form>
</div>
</div>
</div>
</div>
{% endblock content %}
{% block extra_script %}
$('#id_fleet_time').datetimepicker({
maskInput: true,
format: 'Y-m-d H:i',minDate:0
});
{% endblock extra_script %}

View File

@@ -6,8 +6,6 @@
{% block title %}Alliance Auth - Update AAR Link{% endblock %} {% block title %}Alliance Auth - Update AAR Link{% endblock %}
{% block page_title %}{% trans "Update AAR Link" %}{% endblock page_title %} {% block page_title %}{% trans "Update AAR Link" %}{% endblock page_title %}
{% block extra_css %}
<link href="{% static 'css/jquery.datetimepicker.css' %}" rel="stylesheet" type="text/css">{% endblock extra_css %}
{% block content %} {% block content %}
@@ -35,12 +33,3 @@
</div> </div>
{% endblock content %} {% endblock content %}
{% block extra_script %}
$('#id_fleet_time').datetimepicker({
maskInput: true,
format: 'Y-m-d H:i',minDate:0
});
{% endblock extra_script %}

View File

@@ -41,7 +41,7 @@
<th class="text-center">{% trans "Fleet ISK Cost" %}</th> <th class="text-center">{% trans "Fleet ISK Cost" %}</th>
<th class="text-center">{% trans "SRP Status" %}</th> <th class="text-center">{% trans "SRP Status" %}</th>
<th class="text-center">{% trans "Pending Requests" %}</th> <th class="text-center">{% trans "Pending Requests" %}</th>
<th class="text-center">{% trans "Actions" %}</th> <th width="100px" class="text-center">{% trans "Actions" %}</th>
</tr> </tr>
{% for srpfleet in srpfleets %} {% for srpfleet in srpfleets %}
<tr> <tr>
@@ -98,7 +98,7 @@
<span class="glyphicon glyphicon-pencil"></span> <span class="glyphicon glyphicon-pencil"></span>
</a> </a>
<a href="{% url 'auth_srp_fleet_remove' srpfleet.id %}" class="btn btn-danger" title="Remove"> <a href="{% url 'auth_srp_fleet_remove' srpfleet.id %}" onclick="return confirm('{% trans "Are you sure you want to delete this SRP code and its contents?" %}')" class="btn btn-danger" title="Remove">
<span class="glyphicon glyphicon-trash"></span> <span class="glyphicon glyphicon-trash"></span>
</a> </a>
{% if srpfleet.fleet_srp_code %} {% if srpfleet.fleet_srp_code %}

View File

@@ -6,8 +6,6 @@
{% block title %}Alliance Auth - Update Structure Timer {% endblock %} {% block title %}Alliance Auth - Update Structure Timer {% endblock %}
{% block page_title %}{% trans "Update AAR Link" %}{% endblock page_title %} {% block page_title %}{% trans "Update AAR Link" %}{% endblock page_title %}
{% block extra_css %}
<link href="{% static 'css/jquery.datetimepicker.css' %}" rel="stylesheet" type="text/css">{% endblock extra_css %}
{% block content %} {% block content %}
@@ -35,12 +33,3 @@
</div> </div>
{% endblock content %} {% endblock content %}
{% block extra_script %}
$('#id_fleet_time').datetimepicker({
maskInput: true,
format: 'Y-m-d H:i',minDate:0
});
{% endblock extra_script %}

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