mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2026-02-04 06:06:19 +01:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bdead5ef2 | ||
|
|
8987cf2199 | ||
|
|
27c9b09116 | ||
|
|
0ac0f71fef | ||
|
|
3f454743a9 | ||
|
|
c2f12eed26 | ||
|
|
1b1b692ac0 | ||
|
|
dc8ed2d510 | ||
|
|
049c1c66aa | ||
|
|
8028660a8f | ||
|
|
e6532025f8 | ||
|
|
3361d36bbf | ||
|
|
2ab45b1019 | ||
|
|
c5b55283d1 | ||
|
|
d937d5b5d4 | ||
|
|
b41521bcb5 | ||
|
|
882cafb4ba | ||
|
|
5db340c64a | ||
|
|
e76b0789f3 | ||
|
|
d2e32d3da3 | ||
|
|
12cfc552da | ||
|
|
dc10245158 | ||
|
|
1b0c3c3bfc | ||
|
|
aec013b93c | ||
|
|
4556a0e740 | ||
|
|
aad3bd6f57 | ||
|
|
17dd7c04c7 | ||
|
|
372e582c6e | ||
|
|
5a93128f4f | ||
|
|
901dd5033a | ||
|
|
d8043ff735 | ||
|
|
bb3e7a0449 | ||
|
|
806962cda5 | ||
|
|
2cd43280e2 | ||
|
|
6c94640552 | ||
|
|
250c376abb | ||
|
|
fb22aaf731 | ||
|
|
9897c0bbba | ||
|
|
7d0aa2b5ec | ||
|
|
27628dc70b |
@@ -4,5 +4,5 @@ from __future__ import absolute_import, unicode_literals
|
||||
# Django starts so that shared_task will use this app.
|
||||
from .celeryapp import app as celery_app # noqa
|
||||
|
||||
__version__ = '1.15.1'
|
||||
__version__ = '1.15.4'
|
||||
NAME = 'Alliance Auth v%s' % __version__
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
"""
|
||||
Django settings for alliance_auth project.
|
||||
|
||||
@@ -12,17 +13,12 @@ https://docs.djangoproject.com/en/1.10/ref/settings/
|
||||
|
||||
import os
|
||||
|
||||
import djcelery
|
||||
|
||||
from django.contrib import messages
|
||||
from celery.schedules import crontab
|
||||
|
||||
djcelery.setup_loader()
|
||||
|
||||
# Celery configuration
|
||||
BROKER_URL = 'redis://localhost:6379/0'
|
||||
CELERYBEAT_SCHEDULER = "djcelery.schedulers.DatabaseScheduler"
|
||||
CELERYBEAT_SCHEDULE = dict()
|
||||
CELERYBEAT_SCHEDULER = "django_celery_beat.schedulers.DatabaseScheduler"
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
@@ -50,7 +46,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.humanize',
|
||||
'djcelery',
|
||||
'django_celery_beat',
|
||||
'bootstrapform',
|
||||
'authentication',
|
||||
'services',
|
||||
@@ -62,6 +58,7 @@ INSTALLED_APPS = [
|
||||
'optimer',
|
||||
'corputils',
|
||||
'fleetactivitytracking',
|
||||
'fleetup',
|
||||
'notifications',
|
||||
'esi',
|
||||
'permissions_tool',
|
||||
@@ -214,10 +211,10 @@ MESSAGE_TAGS = {
|
||||
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": "redis://127.0.0.1:6379/1",
|
||||
"BACKEND": "redis_cache.RedisCache",
|
||||
"LOCATION": "localhost:6379",
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
"DB": 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -237,6 +234,25 @@ NOCAPTCHA = True
|
||||
##
|
||||
#####################################################
|
||||
|
||||
|
||||
#########################
|
||||
# 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
|
||||
#################
|
||||
@@ -426,11 +442,10 @@ BROADCAST_SERVICE_NAME = os.environ.get('AA_BROADCAST_SERVICE_NAME', "broadcast"
|
||||
######################################
|
||||
# Mumble Configuration
|
||||
######################################
|
||||
# MUMBLE_URL - Mumble server url
|
||||
# MUMBLE_SERVER_ID - Mumble server id
|
||||
# MUMBLE_URL - Mumble server host
|
||||
# Do not include leading http:// or mumble://
|
||||
######################################
|
||||
MUMBLE_URL = os.environ.get('AA_MUMBLE_URL', "https://example.com")
|
||||
MUMBLE_SERVER_ID = int(os.environ.get('AA_MUMBLE_SERVER_ID', '1'))
|
||||
MUMBLE_URL = os.environ.get('AA_MUMBLE_URL', "example.com")
|
||||
|
||||
######################################
|
||||
# PHPBB3 Configuration
|
||||
@@ -648,6 +663,10 @@ LOGGING = {
|
||||
'handlers': ['log_file', 'console', 'notifications'],
|
||||
'level': 'ERROR',
|
||||
},
|
||||
'fleetup': {
|
||||
'handlers': ['log_file', 'console', 'notifications'],
|
||||
'level': 'DEBUG',
|
||||
},
|
||||
'util': {
|
||||
'handlers': ['log_file', 'console', 'notifications'],
|
||||
'level': 'DEBUG',
|
||||
|
||||
@@ -4,20 +4,17 @@ Alliance Auth Test Suite Django settings.
|
||||
|
||||
import os
|
||||
|
||||
import djcelery
|
||||
|
||||
from django.contrib import messages
|
||||
|
||||
import alliance_auth
|
||||
|
||||
djcelery.setup_loader()
|
||||
|
||||
# Use nose to run all tests
|
||||
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
|
||||
|
||||
NOSE_ARGS = [
|
||||
#'--with-coverage',
|
||||
#'--cover-package=',
|
||||
#'--exe', # If your tests need this to be found/run, check they py files are not chmodded +x
|
||||
]
|
||||
|
||||
# Celery configuration
|
||||
@@ -40,7 +37,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.humanize',
|
||||
'djcelery',
|
||||
'django_celery_beat',
|
||||
'bootstrapform',
|
||||
'authentication',
|
||||
'services',
|
||||
@@ -52,6 +49,7 @@ INSTALLED_APPS = [
|
||||
'optimer',
|
||||
'corputils',
|
||||
'fleetactivitytracking',
|
||||
'fleetup',
|
||||
'notifications',
|
||||
'esi',
|
||||
'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_SYNC_NAMES - enable to force discord nicknames to be set to eve char name (bot needs Manage Nicknames permission)
|
||||
######################################
|
||||
DISCORD_GUILD_ID = os.environ.get('AA_DISCORD_GUILD_ID', '')
|
||||
DISCORD_BOT_TOKEN = os.environ.get('AA_DISCORD_BOT_TOKEN', '')
|
||||
DISCORD_INVITE_CODE = os.environ.get('AA_DISCORD_INVITE_CODE', '')
|
||||
DISCORD_APP_ID = os.environ.get('AA_DISCORD_APP_ID', '')
|
||||
DISCORD_APP_SECRET = os.environ.get('AA_DISCORD_APP_SECRET', '')
|
||||
DISCORD_CALLBACK_URL = os.environ.get('AA_DISCORD_CALLBACK_URL', 'http://example.com/discord_callback')
|
||||
DISCORD_GUILD_ID = os.environ.get('AA_DISCORD_GUILD_ID', '0118999')
|
||||
DISCORD_BOT_TOKEN = os.environ.get('AA_DISCORD_BOT_TOKEN', 'bottoken')
|
||||
DISCORD_INVITE_CODE = os.environ.get('AA_DISCORD_INVITE_CODE', 'invitecode')
|
||||
DISCORD_APP_ID = os.environ.get('AA_DISCORD_APP_ID', 'appid')
|
||||
DISCORD_APP_SECRET = os.environ.get('AA_DISCORD_APP_SECRET', 'secret')
|
||||
DISCORD_CALLBACK_URL = os.environ.get('AA_DISCORD_CALLBACK_URL', 'http://example.com/discord/callback')
|
||||
DISCORD_SYNC_NAMES = 'True' == os.environ.get('AA_DISCORD_SYNC_NAMES', 'False')
|
||||
|
||||
######################################
|
||||
|
||||
@@ -11,7 +11,7 @@ import groupmanagement.views
|
||||
import optimer.views
|
||||
import timerboard.views
|
||||
import fleetactivitytracking.views
|
||||
import fleetup.views
|
||||
import fleetup.urls
|
||||
import srp.views
|
||||
import notifications.views
|
||||
import hrapplications.views
|
||||
@@ -55,11 +55,14 @@ urlpatterns = [
|
||||
name='auth_srp_fleet_mark_completed'),
|
||||
url(r'^srp_fleet_mark_uncompleted/(\w+)', srp.views.srp_fleet_mark_uncompleted,
|
||||
name='auth_srp_fleet_mark_uncompleted'),
|
||||
url(r'^srp_request_remove/(\w+)', srp.views.srp_request_remove,
|
||||
url(r'^srp_request_remove/', srp.views.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'),
|
||||
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
|
||||
url(r'^remove_notifications/(\w+)/$', notifications.views.remove_notification, name='auth_remove_notification'),
|
||||
@@ -72,12 +75,7 @@ urlpatterns = [
|
||||
urlpatterns += i18n_patterns(
|
||||
|
||||
# Fleetup
|
||||
url(r'^fleetup/$', fleetup.views.fleetup_view, name='auth_fleetup_view'),
|
||||
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'),
|
||||
url(r'^fleetup/', include(fleetup.urls.urlpatterns)),
|
||||
|
||||
# Authentication
|
||||
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_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_amount_update/(\w+)'), srp.views.srp_request_update_amount_view,
|
||||
name="auth_srp_request_update_amount_view"),
|
||||
|
||||
# Tools
|
||||
url(_(r'^tool/fleet_formatter_tool/$'), services.views.fleet_formatter_view,
|
||||
@@ -193,6 +189,9 @@ urlpatterns += i18n_patterns(
|
||||
# FleetActivityTracking (FAT)
|
||||
url(r'^fat/$', fleetactivitytracking.views.fatlink_view, name='auth_fatlink_view'),
|
||||
url(r'^fat/statistics/$', fleetactivitytracking.views.fatlink_statistics_view, name='auth_fatlink_view_statistics'),
|
||||
url(r'^fat/statistics/corp/(\w+)$', fleetactivitytracking.views.fatlink_statistics_corp_view, name='auth_fatlink_view_statistics_corp'),
|
||||
url(r'^fat/statistics/corp/(?P<corpid>\w+)/(?P<year>[0-9]+)/(?P<month>[0-9]+)/', fleetactivitytracking.views.fatlink_statistics_corp_view,
|
||||
name='auth_fatlink_view_statistics_corp_month'),
|
||||
url(r'^fat/statistics/(?P<year>[0-9]+)/(?P<month>[0-9]+)/$', fleetactivitytracking.views.fatlink_statistics_view,
|
||||
name='auth_fatlink_view_statistics_month'),
|
||||
url(r'^fat/user/statistics/$', fleetactivitytracking.views.fatlink_personal_statistics_view,
|
||||
|
||||
@@ -30,12 +30,8 @@ class RegistrationForm(forms.Form):
|
||||
if ' ' in self.cleaned_data['username']:
|
||||
raise forms.ValidationError('Username cannot contain a space')
|
||||
|
||||
# We attempt to get the user object if we succeed we know email as been used
|
||||
try:
|
||||
User.objects.get(email=self.cleaned_data['email'])
|
||||
if User.objects.filter(email=self.cleaned_data['email']).count() >= 1:
|
||||
raise forms.ValidationError('Email as already been used')
|
||||
except User.DoesNotExist:
|
||||
pass
|
||||
|
||||
if not re.match("^\w+$", self.cleaned_data['username']):
|
||||
raise forms.ValidationError('Username contains illegal characters')
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
from corputils.models import CorpStats
|
||||
from celery.task import task, periodic_task
|
||||
from celery.task.schedules import crontab
|
||||
from alliance_auth.celeryapp import app
|
||||
|
||||
|
||||
@task
|
||||
@app.task
|
||||
def update_corpstats(pk):
|
||||
cs = CorpStats.objects.get(pk=pk)
|
||||
cs.update()
|
||||
|
||||
|
||||
@periodic_task(run_every=crontab(minute=0, hour="*/6"))
|
||||
@app.task
|
||||
def update_all_corpstats():
|
||||
for cs in CorpStats.objects.all():
|
||||
update_corpstats.delay(cs.pk)
|
||||
|
||||
@@ -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.
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
### 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
|
||||
- Apache 2.4 or newer:
|
||||
- [000-default.conf](http://pastebin.com/3LLzyNmV)
|
||||
|
||||
121
docs/installation/auth/gunicorn.md
Normal file
121
docs/installation/auth/gunicorn.md
Normal 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.
|
||||
@@ -7,7 +7,9 @@
|
||||
ubuntu
|
||||
centos
|
||||
settings
|
||||
nginx
|
||||
apache
|
||||
gunicorn
|
||||
cloudflare
|
||||
supervisor
|
||||
quickstart
|
||||
|
||||
93
docs/installation/auth/nginx.md
Normal file
93
docs/installation/auth/nginx.md
Normal 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/).
|
||||
@@ -10,5 +10,5 @@ The big goal of AllianceAuth is the automation of group membership, so we’ll n
|
||||
|
||||
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 'python manage.py celerybeat'
|
||||
screen -dm bash -c 'celery -A alliance_auth worker'
|
||||
screen -dm bash -c 'celery -A alliance_auth beat'
|
||||
|
||||
@@ -215,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_`
|
||||
### BLUE_ALLIANCE_GROUPS
|
||||
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
|
||||
Characters of any corp or alliance with their ID here will be treated as a member.
|
||||
### CORP_IDS
|
||||
|
||||
@@ -15,14 +15,14 @@ Ubuntu:
|
||||
CentOS:
|
||||
|
||||
sudo yum install supervisor
|
||||
sudo systemctl enable supervisor.service
|
||||
sudo systemctl start supervisor.service
|
||||
sudo systemctl enable supervisord.service
|
||||
sudo systemctl start supervisord.service
|
||||
|
||||
## 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.conf` for the celery workers, and `auth-mumble.conf` for the mumble authenticator. For all three just use a wildcard:
|
||||
|
||||
sudo cp thirdparty/Supervisor/* /etc/supervisor/conf.d
|
||||
|
||||
@@ -41,15 +41,33 @@ To ensure the processes are working, check their status:
|
||||
sudo supervisorctl status
|
||||
|
||||
Processes will be `STARTING`, `RUNNING`, or `ERROR`. If an error has occurred, check their log files:
|
||||
- celeryd: `log/worker.log`
|
||||
- celerybeat: `log/beat.log`
|
||||
- celery workers: `log/worker.log`
|
||||
- celery beat: `log/beat.log`
|
||||
- authenticator: `log/authenticator.log`
|
||||
|
||||
## Restarting Processes
|
||||
|
||||
To restart the celery group:
|
||||
|
||||
sudo supervisorctl restart auth:*
|
||||
|
||||
To restart just celerybeat:
|
||||
|
||||
sudo supervisorctl restart auth:celerybeat
|
||||
|
||||
To restart just celeryd:
|
||||
|
||||
sudo supervisorctl restart auth:celeryd
|
||||
|
||||
To restart just mumble authenticator:
|
||||
|
||||
sudo supervisorctl restart auth-mumble
|
||||
|
||||
## 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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -60,4 +78,4 @@ Most often this is caused by a permissions issue on the allianceauth directory (
|
||||
|
||||
### Workers are using old settings
|
||||
|
||||
Every time the codebase is updated or settings file changed, workers will have to be restarted. Easiest way is to restart the supervisor service (see configuration above for commands)
|
||||
Every time the codebase is updated or settings file changed, workers will have to be restarted. Easiest way is to restart the supervisor service (see configuration above for commands)
|
||||
|
||||
@@ -68,4 +68,4 @@ Enter your database password and press Check. If all the boxes come back green p
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -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
|
||||
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:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
redis-cli FLUSHALL
|
||||
python manage.py celeryd --purge
|
||||
celery -A alliance_auth worker --purge
|
||||
|
||||
Press control+C once.
|
||||
|
||||
python manage.py celeryd --discard
|
||||
|
||||
Press control+C once.
|
||||
Press Control+C once.
|
||||
|
||||
Now start celery again with [these background process commands.](../installation/auth/quickstart.md)
|
||||
|
||||
|
||||
@@ -228,7 +228,7 @@ class EveProvider(object):
|
||||
@python_2_unicode_compatible
|
||||
class EveSwaggerProvider(EveProvider):
|
||||
def __init__(self, token=None, adapter=None):
|
||||
self.client = esi_client_factory(token=token, Alliance='v1', Character='v4', Corporation='v2', Universe='v2')
|
||||
self.client = esi_client_factory(token=token, Alliance='v3', Character='v4', Corporation='v2', Universe='v2')
|
||||
self.adapter = adapter or self
|
||||
|
||||
def __str__(self):
|
||||
@@ -330,9 +330,9 @@ class EveXmlProvider(EveProvider):
|
||||
self.adapter,
|
||||
id,
|
||||
corpinfo['name'],
|
||||
corpinfo['ticker'],
|
||||
corpinfo['ceo']['id'],
|
||||
corpinfo['members']['current'],
|
||||
corpinfo['ticker'],
|
||||
corpinfo['alliance']['id'] if corpinfo['alliance'] else None,
|
||||
)
|
||||
return model
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
from __future__ import unicode_literals
|
||||
from django.conf import settings
|
||||
from celery.task import periodic_task
|
||||
from django.contrib.auth.models import User
|
||||
from notifications import notify
|
||||
from celery import task
|
||||
from celery.task.schedules import crontab
|
||||
from authentication.models import AuthServicesInfo
|
||||
from eveonline.managers import EveManager
|
||||
from eveonline.models import EveApiKeyPair
|
||||
@@ -17,10 +15,12 @@ from authentication.tasks import set_state
|
||||
import logging
|
||||
import evelink
|
||||
|
||||
from alliance_auth.celeryapp import app
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@task
|
||||
@app.task
|
||||
def refresh_api(api):
|
||||
logger.debug('Running update on api key %s' % api.api_id)
|
||||
still_valid = True
|
||||
@@ -70,12 +70,13 @@ def refresh_api(api):
|
||||
level="danger")
|
||||
|
||||
|
||||
@task
|
||||
def refresh_user_apis(user):
|
||||
@app.task
|
||||
def refresh_user_apis(pk):
|
||||
user = User.objects.get(pk=pk)
|
||||
logger.debug('Refreshing all APIs belonging to user %s' % user)
|
||||
apis = EveApiKeyPair.objects.filter(user=user)
|
||||
for x in apis:
|
||||
refresh_api(x)
|
||||
refresh_api.apply(args=(x,))
|
||||
# Check our main character
|
||||
auth = AuthServicesInfo.objects.get(user=user)
|
||||
if auth.main_char_id:
|
||||
@@ -91,28 +92,28 @@ def refresh_user_apis(user):
|
||||
set_state(user)
|
||||
|
||||
|
||||
@periodic_task(run_every=crontab(minute=0, hour="*/3"))
|
||||
@app.task
|
||||
def run_api_refresh():
|
||||
if not EveApiManager.check_if_api_server_online():
|
||||
logger.warn("Aborted scheduled API key refresh: API server unreachable")
|
||||
return
|
||||
|
||||
for u in User.objects.all():
|
||||
refresh_user_apis.delay(u)
|
||||
refresh_user_apis.delay(u.pk)
|
||||
|
||||
|
||||
@task
|
||||
@app.task
|
||||
def update_corp(id, is_blue=None):
|
||||
EveManager.update_corporation(id, is_blue=is_blue)
|
||||
|
||||
|
||||
@task
|
||||
@app.task
|
||||
def update_alliance(id, is_blue=None):
|
||||
EveManager.update_alliance(id, is_blue=is_blue)
|
||||
EveManager.populate_alliance(id)
|
||||
|
||||
|
||||
@periodic_task(run_every=crontab(minute=0, hour="*/2"))
|
||||
@app.task
|
||||
def run_corp_update():
|
||||
if not EveApiManager.check_if_api_server_online():
|
||||
logger.warn("Aborted updating corp and alliance models: API server unreachable")
|
||||
@@ -123,7 +124,7 @@ def run_corp_update():
|
||||
is_blue = True if corp_id in settings.STR_BLUE_CORP_IDS else False
|
||||
try:
|
||||
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:
|
||||
EveManager.create_corporation(corp_id, is_blue=is_blue)
|
||||
except ObjectNotFound:
|
||||
|
||||
@@ -131,7 +131,7 @@ def api_key_removal(request, 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))
|
||||
if not EveCharacter.objects.filter(character_id=authinfo.main_char_id).exists():
|
||||
authinfo.main_char_id = None
|
||||
authinfo.main_char_id = ''
|
||||
authinfo.save()
|
||||
set_state(request.user)
|
||||
return redirect("auth_dashboard")
|
||||
@@ -164,7 +164,7 @@ def user_refresh_api(request, api_id):
|
||||
if EveApiKeyPair.objects.filter(api_id=api_id).exists():
|
||||
api_key_pair = EveApiKeyPair.objects.get(api_id=api_id)
|
||||
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})
|
||||
set_state(request.user)
|
||||
else:
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
{% load i18n %}
|
||||
{% block title %}Fleet Participation{% endblock %}
|
||||
{% block page_title %}{% trans "Fleet Participation" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header text-center">{% trans "Character not found!" %}</h1>
|
||||
<div class="col-lg-12 container" id="example">
|
||||
|
||||
@@ -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 %}
|
||||
@@ -24,14 +24,16 @@
|
||||
<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 "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>
|
||||
{% for corpStat in fatStats %}
|
||||
<tr>
|
||||
<td>
|
||||
<img src="https://image.eveonline.com/Corporation/{{ corpStat.corp.corporation_id }}_32.png" class="ra-avatar img-responsive">
|
||||
</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.member_count }}</td>
|
||||
<td class="text-center">{{ corpStat.n_fats }}</td>
|
||||
@@ -42,3 +44,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
{% block extra_script %}
|
||||
$(document).ready(function(){
|
||||
$("[rel=tooltip]").tooltip();
|
||||
{% endblock extra_script %}
|
||||
|
||||
@@ -9,9 +9,11 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils import timezone
|
||||
from django.contrib import messages
|
||||
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||
from django.db.models import Q
|
||||
from eveonline.models import EveCharacter
|
||||
from eveonline.models import EveCorporationInfo
|
||||
from eveonline.managers import EveManager
|
||||
from authentication.models import AuthServicesInfo
|
||||
from fleetactivitytracking.forms import FatlinkForm
|
||||
from fleetactivitytracking.models import Fatlink, Fat
|
||||
|
||||
@@ -55,6 +57,24 @@ class CorpStat(object):
|
||||
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):
|
||||
if month == 12:
|
||||
return datetime.datetime(year + 1, 1, 1)
|
||||
@@ -88,6 +108,41 @@ def fatlink_view(request):
|
||||
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
|
||||
@permission_required('auth.fleetactivitytracking_statistics')
|
||||
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 = {}
|
||||
|
||||
# get FAT stats for member corps
|
||||
for corp_id in settings.STR_CORP_IDS:
|
||||
fat_stats[corp_id] = CorpStat(corp_id, start_of_month, start_of_next_month)
|
||||
for alliance_id in settings.STR_ALLIANCE_IDS:
|
||||
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)
|
||||
query = Q(corporation_id__in=settings.STR_CORP_IDS) | Q(alliance__alliance_id__in=settings.STR_ALLIANCE_IDS)
|
||||
for corp in EveCorporationInfo.objects.filter(query).distinct():
|
||||
fat_stats[corp.corporation_id] = CorpStat(corp.corporation_id, start_of_month, start_of_next_month)
|
||||
|
||||
# get FAT stats for corps not in alliance
|
||||
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)
|
||||
|
||||
for fat in fats_in_span:
|
||||
if fat.character.corporation_id not in fat_stats:
|
||||
for fat in fats_in_span.exclude(character__corporation_id__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,
|
||||
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.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:
|
||||
context = {'fatStats': stat_list, 'month': start_of_month.strftime("%B"), 'year': year,
|
||||
'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}
|
||||
context.update({'next_month': start_of_next_month})
|
||||
|
||||
return render(request, 'fleetactivitytracking/fatlinkstatisticsview.html', context=context)
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
from __future__ import unicode_literals
|
||||
default_app_config = 'fleetup.apps.FleetupConfig'
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
@@ -1 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
@@ -1,178 +1,190 @@
|
||||
from __future__ import unicode_literals
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.utils import timezone
|
||||
from datetime import datetime
|
||||
|
||||
import logging
|
||||
import requests
|
||||
import json
|
||||
import hashlib
|
||||
|
||||
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:
|
||||
APP_KEY = settings.FLEETUP_APP_KEY
|
||||
USER_ID = settings.FLEETUP_USER_ID
|
||||
API_ID = settings.FLEETUP_API_ID
|
||||
GROUP_ID = settings.FLEETUP_GROUP_ID
|
||||
BASE_URL = "http://api.fleet-up.com/Api.svc/{}/{}/{}".format(APP_KEY, USER_ID, API_ID)
|
||||
|
||||
TZ = timezone.utc
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_fleetup_members():
|
||||
url = "http://api.fleet-up.com/Api.svc/" + str(appkey) + "/" + str(userid) + "/" + str(
|
||||
apiid) + "/GroupCharacters/" + str(groupid) + ""
|
||||
@classmethod
|
||||
def _request_cache_key(cls, url):
|
||||
h = hashlib.sha1()
|
||||
h.update(url.encode('utf-8'))
|
||||
return 'FLEETUP_ENDPOINT_' + h.hexdigest()
|
||||
|
||||
@classmethod
|
||||
def _cache_until_seconds(cls, cache_until_json):
|
||||
# Format comes in like "/Date(1493896236163)/"
|
||||
try:
|
||||
jsondata = requests.get(url).content
|
||||
fmembers = json.loads(jsondata.decode())
|
||||
epoch_ms = int(cache_until_json[6:-2])
|
||||
cache_delta = datetime.fromtimestamp(epoch_ms/1000) - datetime.now()
|
||||
cache_delta_seconds = cache_delta.total_seconds()
|
||||
if cache_delta_seconds < 0:
|
||||
return 0
|
||||
elif cache_delta_seconds > 3600:
|
||||
return 3600
|
||||
else:
|
||||
return cache_delta_seconds
|
||||
except TypeError:
|
||||
logger.debug("Couldn't convert CachedUntil time, defaulting to 600 seconds")
|
||||
return 600
|
||||
|
||||
@classmethod
|
||||
def get_endpoint(cls, url):
|
||||
try:
|
||||
cache_key = cls._request_cache_key(url)
|
||||
cached = cache.get(cache_key)
|
||||
if cached:
|
||||
return cached
|
||||
|
||||
r = requests.get(url)
|
||||
r.raise_for_status()
|
||||
|
||||
json = r.json()
|
||||
|
||||
if json['Success']:
|
||||
cache.set(cache_key, json, cls._cache_until_seconds(json['CachedUntilUTC']))
|
||||
return json
|
||||
except requests.exceptions.ConnectionError:
|
||||
logger.warning("Can't connect to Fleet-Up API, is it offline?!")
|
||||
except requests.HTTPError:
|
||||
logger.exception("Error accessing Fleetup API")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_fleetup_members(cls):
|
||||
url = "{}/GroupCharacters/{}".format(cls.BASE_URL, cls.GROUP_ID)
|
||||
try:
|
||||
fmembers = cls.get_endpoint(url)
|
||||
if not fmembers:
|
||||
return None
|
||||
return {row["UserId"]: {"user_id": row["UserId"],
|
||||
"char_name": row["EveCharName"],
|
||||
"char_id": row["EveCharId"],
|
||||
"corporation": row["Corporation"]} for row in fmembers["Data"]}
|
||||
except requests.exceptions.ConnectionError:
|
||||
logger.warn("Can't connect to Fleet-Up API, is it offline?!")
|
||||
except (ValueError, UnicodeDecodeError, TypeError):
|
||||
logger.debug("No fleetup members retrieved.")
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def get_fleetup_operations():
|
||||
url = "http://api.fleet-up.com/Api.svc/" + str(appkey) + "/" + str(userid) + "/" + str(
|
||||
apiid) + "/Operations/" + str(groupid) + ""
|
||||
try:
|
||||
jsondata = requests.get(url).content
|
||||
foperations = json.loads(jsondata.decode())
|
||||
return {row["StartString"]: {"subject": row["Subject"],
|
||||
"start": (datetime.strptime(row["StartString"], "%Y-%m-%d %H:%M:%S")),
|
||||
"end": (datetime.strptime(row["EndString"], "%Y-%m-%d %H:%M:%S")),
|
||||
"operation_id": row["OperationId"],
|
||||
"location": row["Location"],
|
||||
"location_info": row["LocationInfo"],
|
||||
"details": row["Details"],
|
||||
"url": row["Url"],
|
||||
"doctrine": row["Doctrines"],
|
||||
"organizer": row["Organizer"]} for row in foperations["Data"]}
|
||||
except requests.exceptions.ConnectionError:
|
||||
logger.warn("Can't connect to Fleet-Up API, is it offline?!")
|
||||
except (ValueError, UnicodeDecodeError):
|
||||
logger.debug("No fleetup operations retrieved.")
|
||||
return {}
|
||||
@classmethod
|
||||
def get_fleetup_operations(cls):
|
||||
url = "{}/Operations/{}".format(cls.BASE_URL, cls.GROUP_ID)
|
||||
foperations = cls.get_endpoint(url)
|
||||
if foperations is None:
|
||||
return None
|
||||
return {row["StartString"]: {"subject": row["Subject"],
|
||||
"start": timezone.make_aware(
|
||||
datetime.strptime(row["StartString"], "%Y-%m-%d %H:%M:%S"), cls.TZ),
|
||||
"end": timezone.make_aware(
|
||||
datetime.strptime(row["EndString"], "%Y-%m-%d %H:%M:%S"), cls.TZ),
|
||||
"operation_id": row["OperationId"],
|
||||
"location": row["Location"],
|
||||
"location_info": row["LocationInfo"],
|
||||
"details": row["Details"],
|
||||
"url": row["Url"],
|
||||
"doctrine": row["Doctrines"],
|
||||
"organizer": row["Organizer"]} for row in foperations["Data"]}
|
||||
|
||||
@staticmethod
|
||||
def get_fleetup_timers():
|
||||
url = "http://api.fleet-up.com/Api.svc/" + str(appkey) + "/" + str(userid) + "/" + str(
|
||||
apiid) + "/Timers/" + str(groupid) + ""
|
||||
try:
|
||||
jsondata = requests.get(url).content
|
||||
ftimers = json.loads(jsondata.decode())
|
||||
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 {}
|
||||
@classmethod
|
||||
def get_fleetup_timers(cls):
|
||||
url = "{}/Timers/{}".format(cls.BASE_URL, cls.GROUP_ID)
|
||||
ftimers = cls.get_endpoint(url)
|
||||
if not ftimers:
|
||||
return None
|
||||
return {row["ExpiresString"]: {"solarsystem": row["SolarSystem"],
|
||||
"planet": row["Planet"],
|
||||
"moon": row["Moon"],
|
||||
"owner": row["Owner"],
|
||||
"type": row["Type"],
|
||||
"timer_type": row["TimerType"],
|
||||
"expires": timezone.make_aware(
|
||||
datetime.strptime(row["ExpiresString"], "%Y-%m-%d %H:%M:%S"), cls.TZ),
|
||||
"notes": row["Notes"]} for row in ftimers["Data"]}
|
||||
|
||||
@staticmethod
|
||||
def get_fleetup_doctrines():
|
||||
url = "http://api.fleet-up.com/Api.svc/" + str(appkey) + "/" + str(userid) + "/" + str(
|
||||
apiid) + "/Doctrines/" + str(groupid) + ""
|
||||
try:
|
||||
jsondata = requests.get(url).content
|
||||
fdoctrines = json.loads(jsondata.decode())
|
||||
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": []}
|
||||
@classmethod
|
||||
def get_fleetup_doctrines(cls):
|
||||
url = "{}/Doctrines/{}".format(cls.BASE_URL, cls.GROUP_ID)
|
||||
fdoctrines = cls.get_endpoint(url)
|
||||
if not fdoctrines:
|
||||
return None
|
||||
return {"fleetup_doctrines": fdoctrines["Data"]}
|
||||
|
||||
@staticmethod
|
||||
def get_fleetup_doctrine(doctrinenumber):
|
||||
url = "http://api.fleet-up.com/Api.svc/" + str(appkey) + "/" + str(userid) + "/" + str(
|
||||
apiid) + "/DoctrineFittings/%s" % doctrinenumber
|
||||
try:
|
||||
jsondata = requests.get(url).content
|
||||
fdoctrine = json.loads(jsondata.decode())
|
||||
return {"fitting_doctrine": fdoctrine}
|
||||
except requests.exceptions.ConnectionError:
|
||||
logger.warn("Can't connect to Fleet-Up API, is it offline?!")
|
||||
except (ValueError, UnicodeDecodeError):
|
||||
logger.warn("Fleetup doctrine number %s not found" % doctrinenumber)
|
||||
return {"fitting_doctrine": {}}
|
||||
@classmethod
|
||||
def get_fleetup_doctrine(cls, doctrinenumber):
|
||||
url = "{}/DoctrineFittings/{}".format(cls.BASE_URL, doctrinenumber)
|
||||
fdoctrine = cls.get_endpoint(url)
|
||||
if not fdoctrine:
|
||||
return None
|
||||
return {"fitting_doctrine": fdoctrine}
|
||||
|
||||
@staticmethod
|
||||
def get_fleetup_fittings():
|
||||
url = "http://api.fleet-up.com/Api.svc/" + str(appkey) + "/" + str(userid) + "/" + str(
|
||||
apiid) + "/Fittings/" + str(groupid) + ""
|
||||
try:
|
||||
jsondata = requests.get(url).content
|
||||
ffittings = json.loads(jsondata.decode())
|
||||
return {row["FittingId"]: {"fitting_id": row["FittingId"],
|
||||
"name": row["Name"],
|
||||
"icon_id": row["EveTypeId"],
|
||||
"hull": row["HullType"],
|
||||
"shiptype": row["ShipType"],
|
||||
"estimated": row["EstPrice"],
|
||||
"faction": row["Faction"],
|
||||
"categories": row["Categories"],
|
||||
"last_update": (
|
||||
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 {}
|
||||
@classmethod
|
||||
def get_fleetup_fittings(cls):
|
||||
url = "{}/Fittings/{}".format(cls.BASE_URL, cls.GROUP_ID)
|
||||
ffittings = cls.get_endpoint(url)
|
||||
if not ffittings:
|
||||
return None
|
||||
return {row["FittingId"]: {"fitting_id": row["FittingId"],
|
||||
"name": row["Name"],
|
||||
"icon_id": row["EveTypeId"],
|
||||
"hull": row["HullType"],
|
||||
"shiptype": row["ShipType"],
|
||||
"estimated": row["EstPrice"],
|
||||
"faction": row["Faction"],
|
||||
"categories": row["Categories"],
|
||||
"last_update":
|
||||
timezone.make_aware(
|
||||
datetime.strptime(row["LastUpdatedString"], "%Y-%m-%d %H:%M:%S"), cls.TZ)}
|
||||
for row in ffittings["Data"]}
|
||||
|
||||
@staticmethod
|
||||
def get_fleetup_fitting(fittingnumber):
|
||||
url = "http://api.fleet-up.com/Api.svc/" + str(appkey) + "/" + str(userid) + "/" + str(
|
||||
apiid) + "/Fitting/%s" % fittingnumber
|
||||
@classmethod
|
||||
def get_fleetup_fitting(cls, fittingnumber):
|
||||
url = "{}/Fitting/{}".format(cls.BASE_URL, fittingnumber)
|
||||
try:
|
||||
jsondata = requests.get(url).content
|
||||
ffitting = json.loads(jsondata.decode())
|
||||
ffitting = cls.get_endpoint(url)
|
||||
if not ffitting:
|
||||
return None
|
||||
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:
|
||||
logger.warn("Failed to retrieve fleetup fitting number %s" % fittingnumber)
|
||||
logger.warning("Failed to retrieve fleetup fitting number %s" % fittingnumber)
|
||||
return {"fitting_data": {}}
|
||||
|
||||
@staticmethod
|
||||
def get_fleetup_doctrineid(fittingnumber):
|
||||
url = "http://api.fleet-up.com/Api.svc/" + str(appkey) + "/" + str(userid) + "/" + str(
|
||||
apiid) + "/Fitting/%s" % fittingnumber
|
||||
@classmethod
|
||||
def get_fleetup_doctrineid(cls, fittingnumber):
|
||||
url = "{}/Fitting/{}".format(cls.BASE_URL, fittingnumber)
|
||||
try:
|
||||
jsondata = requests.get(url).content
|
||||
fdoctrineid = json.loads(jsondata.decode())
|
||||
fdoctrineid = cls.get_endpoint(url)
|
||||
if not fdoctrineid:
|
||||
return None
|
||||
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):
|
||||
logger.debug("Fleetup fitting number %s not in a doctrine." % fittingnumber)
|
||||
return None
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def get_fleetup_fitting_eft(fittingnumber):
|
||||
url = "http://api.fleet-up.com/Api.svc/" + str(appkey) + "/" + str(userid) + "/" + str(
|
||||
apiid) + "/Fitting/%s/eft" % fittingnumber
|
||||
@classmethod
|
||||
def get_fleetup_fitting_eft(cls, fittingnumber):
|
||||
url = "{}/Fitting/{}/eft".format(cls.BASE_URL, fittingnumber)
|
||||
try:
|
||||
jsondata = requests.get(url).content
|
||||
ffittingeft = json.loads(jsondata.decode())
|
||||
ffittingeft = cls.get_endpoint(url)
|
||||
if not ffittingeft:
|
||||
return None
|
||||
return {"fitting_eft": ffittingeft["Data"]["FittingData"]}
|
||||
except requests.exceptions.ConnectionError:
|
||||
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)
|
||||
except KeyError:
|
||||
logger.warning("Fleetup fitting eft not found for fitting number %s" % fittingnumber)
|
||||
return {"fitting_eft": {}}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
@@ -9,30 +9,7 @@
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
{% if perms.auth.corp_stats %}
|
||||
<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><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>
|
||||
{% include "fleetup/menu.html" %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Characters registered on Fleet-Up.com" %}</h3>
|
||||
@@ -8,30 +8,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<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><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>
|
||||
{% include "fleetup/menu.html" %}
|
||||
<div class="panel">
|
||||
{% for a, j in doctrine.items %}
|
||||
{% regroup j.Data|dictsort:"Role" by Role as role_list %}
|
||||
@@ -8,30 +8,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<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><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>
|
||||
{% include "fleetup/menu.html" %}
|
||||
<div class="panel">
|
||||
{% if doctrines_list %}
|
||||
{% for a, j in doctrines_list.items %}
|
||||
@@ -8,30 +8,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<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="#">{% 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>
|
||||
{% include "fleetup/menu.html" %}
|
||||
<div class="tab-content">
|
||||
<div id="fit" class="tab-pane fade in active">
|
||||
<div class="col-lg-3">
|
||||
@@ -56,8 +33,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-8 col-lg-offset-7">
|
||||
<a class="btn btn-primary" href="/fleetup/doctrines/{{ doctrin.DoctrineId }}/">{% trans "See doctrine" %}</a>
|
||||
<div class="pull-right">
|
||||
<a class="btn btn-primary" href="{% url 'auth_fleetup_doctrine' doctrin.DoctrineId %}">{% trans "See doctrine" %}</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -140,7 +117,7 @@
|
||||
<div class="panel-body">
|
||||
{% for data in fitting_eft.items %}
|
||||
{% 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 %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -8,30 +8,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<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="#">{% 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>
|
||||
{% include "fleetup/menu.html" %}
|
||||
<div class="panel">
|
||||
{% if fitting_list %}
|
||||
<table class="table table-condensed table-hover table-striped">
|
||||
@@ -8,30 +8,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<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="#">{% 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>
|
||||
{% include "fleetup/menu.html" %}
|
||||
<div class="panel">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active"><a data-toggle="tab" href="#operations">{% trans "Operations" %}</a></li>
|
||||
26
fleetup/templates/fleetup/menu.html
Normal file
26
fleetup/templates/fleetup/menu.html
Normal 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
0
fleetup/tests.py → fleetup/tests/__init__.py
Executable file → Normal file
510
fleetup/tests/test_manager.py
Normal file
510
fleetup/tests/test_manager.py
Normal file
@@ -0,0 +1,510 @@
|
||||
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 django.utils.timezone import make_aware, utc
|
||||
|
||||
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': make_aware(datetime.datetime(2017, 5, 6, 11, 11, 11), utc),
|
||||
'end': make_aware(datetime.datetime(2017, 5, 6, 12, 12, 12), utc),
|
||||
'operation_id': 1234,
|
||||
'location': 'Jita',
|
||||
'location_info': '4-4',
|
||||
'details': 'This is a test operation',
|
||||
'url': 'http://example.com/1234',
|
||||
'doctrine': 'Foxcats',
|
||||
'organizer': 'Example FC'
|
||||
}
|
||||
}
|
||||
self.assertDictEqual(expected_result, result)
|
||||
|
||||
# Test None response
|
||||
# Arrange
|
||||
get_endpoint.return_value = None
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_operations()
|
||||
|
||||
# Assert
|
||||
self.assertIsNone(result)
|
||||
|
||||
# Test Empty response
|
||||
# Arrange
|
||||
get_endpoint.return_value = {'Data': []}
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_operations()
|
||||
|
||||
# Assert
|
||||
self.assertDictEqual({}, result)
|
||||
|
||||
@mock.patch('fleetup.managers.FleetUpManager.get_endpoint')
|
||||
def test_get_fleetup_timers(self, get_endpoint):
|
||||
|
||||
get_endpoint.return_value = {"Data": [
|
||||
{
|
||||
'ExpiresString': '2017-05-06 11:11:11',
|
||||
'SolarSystem': 'Jita',
|
||||
'Planet': '4',
|
||||
'Moon': '4',
|
||||
'Owner': 'Caldari Navy',
|
||||
'Type': 'Caldari Station',
|
||||
'TimerType': 'Armor',
|
||||
'Notes': 'Burn Jita?'
|
||||
}
|
||||
]}
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_timers()
|
||||
|
||||
# Asset
|
||||
self.assertTrue(get_endpoint.called)
|
||||
args, kwargs = get_endpoint.call_args
|
||||
self.assertEqual(args[0],
|
||||
FleetUpManager.BASE_URL + '/Timers/' +
|
||||
FleetUpManager.GROUP_ID)
|
||||
expected_result = {
|
||||
'2017-05-06 11:11:11': {
|
||||
'expires': make_aware(datetime.datetime(2017, 5, 6, 11, 11, 11), utc),
|
||||
'solarsystem': 'Jita',
|
||||
'planet': '4',
|
||||
'moon': '4',
|
||||
'owner': 'Caldari Navy',
|
||||
'type': 'Caldari Station',
|
||||
'timer_type': 'Armor',
|
||||
'notes': 'Burn Jita?'
|
||||
}
|
||||
}
|
||||
self.assertDictEqual(expected_result, result)
|
||||
|
||||
# Test None response
|
||||
# Arrange
|
||||
get_endpoint.return_value = None
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_timers()
|
||||
|
||||
# Assert
|
||||
self.assertIsNone(result)
|
||||
|
||||
# Test Empty response
|
||||
# Arrange
|
||||
get_endpoint.return_value = {'Data': []}
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_timers()
|
||||
|
||||
# Assert
|
||||
self.assertDictEqual({}, result)
|
||||
|
||||
@mock.patch('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': make_aware(datetime.datetime(2017, 5, 6, 11, 11, 11), utc)
|
||||
}
|
||||
}
|
||||
self.assertDictEqual(expected_result, result)
|
||||
|
||||
# Test None response
|
||||
# Arrange
|
||||
get_endpoint.return_value = None
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_fittings()
|
||||
|
||||
# Assert
|
||||
self.assertIsNone(result)
|
||||
|
||||
# Test Empty response
|
||||
# Arrange
|
||||
get_endpoint.return_value = {'Data': []}
|
||||
|
||||
# Act
|
||||
result = FleetUpManager.get_fleetup_fittings()
|
||||
|
||||
# Assert
|
||||
self.assertDictEqual({}, result)
|
||||
|
||||
@mock.patch('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
13
fleetup/urls.py
Normal 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
38
fleetup/views.py
Executable file → Normal file
@@ -4,6 +4,8 @@ from django.shortcuts import render
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
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 authentication.decorators import members_and_blues
|
||||
|
||||
@@ -23,14 +25,20 @@ def fleetup_view(request):
|
||||
logger.debug("fleetup_view called by user %s" % request.user)
|
||||
|
||||
operations_list = FleetUpManager.get_fleetup_operations()
|
||||
if operations_list is None:
|
||||
messages.add_message(request, messages.ERROR, _("Failed to get operations list, contact your administrator"))
|
||||
operations_list = {}
|
||||
timers_list = FleetUpManager.get_fleetup_timers()
|
||||
if timers_list is None:
|
||||
messages.add_message(request, messages.ERROR, _("Failed to get timers list, contact your administrator"))
|
||||
timers_list = {}
|
||||
now = datetime.datetime.now().strftime('%H:%M:%S')
|
||||
|
||||
context = {"timers_list": sorted(timers_list.items()),
|
||||
"operations_list": sorted(operations_list.items()),
|
||||
"now": now}
|
||||
|
||||
return render(request, 'registered/fleetup.html', context=context)
|
||||
return render(request, 'fleetup/index.html', context=context)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -39,10 +47,13 @@ def fleetup_characters(request):
|
||||
logger.debug("fleetup_characters called by user %s" % request.user)
|
||||
|
||||
member_list = FleetUpManager.get_fleetup_members()
|
||||
if member_list is None:
|
||||
messages.add_message(request, messages.ERROR, _("Failed to get member list, contact your administrator"))
|
||||
member_list = {}
|
||||
|
||||
context = {"member_list": sorted(member_list.items())}
|
||||
|
||||
return render(request, 'registered/fleetupcharacters.html', context=context)
|
||||
return render(request, 'fleetup/characters.html', context=context)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -50,8 +61,13 @@ def fleetup_characters(request):
|
||||
def fleetup_fittings(request):
|
||||
logger.debug("fleetup_fittings called by user %s" % request.user)
|
||||
fitting_list = FleetUpManager.get_fleetup_fittings()
|
||||
|
||||
if fitting_list is None:
|
||||
messages.add_message(request, messages.ERROR, _("Failed to get fitting list, contact your administrator"))
|
||||
fitting_list = {}
|
||||
|
||||
context = {"fitting_list": sorted(fitting_list.items())}
|
||||
return render(request, 'registered/fleetupfittingsview.html', context=context)
|
||||
return render(request, 'fleetup/fittingsview.html', context=context)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -62,10 +78,15 @@ def fleetup_fitting(request, fittingnumber):
|
||||
fitting_data = FleetUpManager.get_fleetup_fitting(fittingnumber)
|
||||
doctrinenumber = FleetUpManager.get_fleetup_doctrineid(fittingnumber)
|
||||
doctrines_list = FleetUpManager.get_fleetup_doctrine(doctrinenumber)
|
||||
|
||||
if fitting_eft is None or fitting_data is None or doctrinenumber is None:
|
||||
messages.add_message(request, messages.ERROR, _("There was an error getting some of the data for this fitting. "
|
||||
"Contact your administrator"))
|
||||
|
||||
context = {"fitting_eft": fitting_eft,
|
||||
"fitting_data": fitting_data,
|
||||
"doctrines_list": doctrines_list}
|
||||
return render(request, 'registered/fleetupfitting.html', context=context)
|
||||
return render(request, 'fleetup/fitting.html', context=context)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -73,8 +94,11 @@ def fleetup_fitting(request, fittingnumber):
|
||||
def fleetup_doctrines(request):
|
||||
logger.debug("fleetup_doctrines called by user %s" % request.user)
|
||||
doctrines_list = FleetUpManager.get_fleetup_doctrines()
|
||||
if doctrines_list is None:
|
||||
messages.add_message(request, messages.ERROR, _("Failed to get doctrines list, contact your administrator"))
|
||||
|
||||
context = {"doctrines_list": doctrines_list}
|
||||
return render(request, 'registered/fleetupdoctrinesview.html', context=context)
|
||||
return render(request, 'fleetup/doctrinesview.html', context=context)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -82,5 +106,7 @@ def fleetup_doctrines(request):
|
||||
def fleetup_doctrine(request, doctrinenumber):
|
||||
logger.debug("fleetup_doctrine called by user %s" % request.user)
|
||||
doctrine = FleetUpManager.get_fleetup_doctrine(doctrinenumber)
|
||||
if doctrine is None:
|
||||
messages.add_message(request, messages.ERROR, _("Failed to get doctine, contact your administrator"))
|
||||
context = {"doctrine": doctrine}
|
||||
return render(request, 'registered/fleetupdoctrine.html', context=context)
|
||||
return render(request, 'fleetup/doctrine.html', context=context)
|
||||
|
||||
@@ -6,9 +6,22 @@ from hrapplications.models import ApplicationQuestion
|
||||
from hrapplications.models import ApplicationForm
|
||||
from hrapplications.models import ApplicationResponse
|
||||
from hrapplications.models import ApplicationComment
|
||||
from hrapplications.models import ApplicationChoice
|
||||
|
||||
class ChoiceInline(admin.TabularInline):
|
||||
model = ApplicationChoice
|
||||
extra = 0
|
||||
verbose_name_plural = 'Choices (optional)'
|
||||
verbose_name= 'Choice'
|
||||
|
||||
class QuestionAdmin(admin.ModelAdmin):
|
||||
fieldsets = [
|
||||
(None, {'fields': ['title', 'help_text']}),
|
||||
]
|
||||
inlines = [ChoiceInline]
|
||||
|
||||
admin.site.register(Application)
|
||||
admin.site.register(ApplicationComment)
|
||||
admin.site.register(ApplicationQuestion)
|
||||
admin.site.register(ApplicationQuestion, QuestionAdmin)
|
||||
admin.site.register(ApplicationForm)
|
||||
admin.site.register(ApplicationResponse)
|
||||
|
||||
33
hrapplications/migrations/0002_choices_for_questions.py
Normal file
33
hrapplications/migrations/0002_choices_for_questions.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-08-23 19:46
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hrapplications', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ApplicationChoice',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('choice_text', models.CharField(max_length=200, verbose_name='Choice')),
|
||||
],
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='applicationquestion',
|
||||
name='title',
|
||||
field=models.CharField(max_length=254, verbose_name='Question'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='applicationchoice',
|
||||
name='question',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='hrapplications.ApplicationQuestion'),
|
||||
),
|
||||
]
|
||||
@@ -11,13 +11,21 @@ from authentication.models import AuthServicesInfo
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ApplicationQuestion(models.Model):
|
||||
title = models.CharField(max_length=254)
|
||||
title = models.CharField(max_length=254, verbose_name='Question')
|
||||
help_text = models.CharField(max_length=254, blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return "Question: " + self.title
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ApplicationChoice(models.Model):
|
||||
question = models.ForeignKey(ApplicationQuestion,on_delete=models.CASCADE,related_name="choices")
|
||||
choice_text = models.CharField(max_length=200, verbose_name='Choice')
|
||||
|
||||
def __str__(self):
|
||||
return self.choice_text
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ApplicationForm(models.Model):
|
||||
questions = models.ManyToManyField(ApplicationQuestion)
|
||||
|
||||
@@ -6,9 +6,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
class opForm(forms.Form):
|
||||
doctrine = forms.CharField(max_length=254, required=True, label=_('Doctrine'))
|
||||
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"))
|
||||
duration = forms.CharField(max_length=254, required=True, label=_("Duration"))
|
||||
operation_name = forms.CharField(max_length=254, required=True, label=_("Operation Name"))
|
||||
fc = forms.CharField(max_length=254, required=True, label=_("Fleet Commander"))
|
||||
details = forms.CharField(max_length=254, required=False, label=_("Extra Details"))
|
||||
|
||||
23
optimer/migrations/0002_auto_20170413_0442.py
Normal file
23
optimer/migrations/0002_auto_20170413_0442.py
Normal 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',
|
||||
),
|
||||
]
|
||||
@@ -13,12 +13,10 @@ class optimer(models.Model):
|
||||
|
||||
doctrine = 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)
|
||||
duration = models.CharField(max_length=25, default="")
|
||||
operation_name = 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)
|
||||
eve_character = models.ForeignKey(EveCharacter)
|
||||
|
||||
|
||||
@@ -22,7 +22,11 @@ logger = logging.getLogger(__name__)
|
||||
@permission_required('auth.optimer_view')
|
||||
def optimer_view(request):
|
||||
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)
|
||||
|
||||
@@ -44,12 +48,10 @@ def add_optimer_view(request):
|
||||
op = optimer()
|
||||
op.doctrine = form.cleaned_data['doctrine']
|
||||
op.system = form.cleaned_data['system']
|
||||
op.location = form.cleaned_data['location']
|
||||
op.start = form.cleaned_data['start']
|
||||
op.duration = form.cleaned_data['duration']
|
||||
op.operation_name = form.cleaned_data['operation_name']
|
||||
op.fc = form.cleaned_data['fc']
|
||||
op.details = form.cleaned_data['details']
|
||||
op.create_time = post_time
|
||||
op.eve_character = character
|
||||
op.save()
|
||||
@@ -93,12 +95,10 @@ def edit_optimer(request, optimer_id):
|
||||
character = EveManager.get_character_by_id(auth_info.main_char_id)
|
||||
op.doctrine = form.cleaned_data['doctrine']
|
||||
op.system = form.cleaned_data['system']
|
||||
op.location = form.cleaned_data['location']
|
||||
op.start = form.cleaned_data['start']
|
||||
op.duration = form.cleaned_data['duration']
|
||||
op.operation_name = form.cleaned_data['operation_name']
|
||||
op.fc = form.cleaned_data['fc']
|
||||
op.details = form.cleaned_data['details']
|
||||
op.eve_character = character
|
||||
logger.info("User %s updating optimer id %s " % (request.user, optimer_id))
|
||||
op.save()
|
||||
@@ -108,12 +108,10 @@ def edit_optimer(request, optimer_id):
|
||||
data = {
|
||||
'doctrine': op.doctrine,
|
||||
'system': op.system,
|
||||
'location': op.location,
|
||||
'start': op.start,
|
||||
'duration': op.duration,
|
||||
'operation_name': op.operation_name,
|
||||
'fc': op.fc,
|
||||
'details': op.details,
|
||||
}
|
||||
form = opForm(initial=data)
|
||||
return render(request, 'registered/optimerupdate.html', context={'form': form})
|
||||
|
||||
@@ -9,18 +9,16 @@ slugify
|
||||
requests-oauthlib
|
||||
sleekxmpp
|
||||
redis
|
||||
celery>=4.0.2
|
||||
|
||||
# Django Stuff #
|
||||
django>=1.10,<2.0
|
||||
django-bootstrap-form
|
||||
django-navhelper
|
||||
django-bootstrap-pagination
|
||||
django-redis>=4.4
|
||||
django-redis-cache>=1.7.1
|
||||
django-recaptcha
|
||||
|
||||
# awating release for fix to celery/django-celery#447
|
||||
# django-celery
|
||||
git+https://github.com/celery/django-celery
|
||||
django-celery-beat
|
||||
|
||||
# awating pyghassen/openfire-restapi #1 to fix installation issues
|
||||
git+https://github.com/adarnof/openfire-restapi
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
from eveonline.tasks import run_corp_update
|
||||
|
||||
run_corp_update()
|
||||
run_corp_update.apply()
|
||||
quit()
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
from django.contrib import admin
|
||||
from services.models import GroupCache
|
||||
|
||||
admin.site.register(GroupCache)
|
||||
@@ -31,6 +31,7 @@ class srpManager:
|
||||
logger.debug("Ship type for kill ID %s is determined to be %s" % (kill_id, ship_type))
|
||||
ship_value = result['zkb']['totalValue']
|
||||
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:
|
||||
raise ValueError("Invalid Kill ID")
|
||||
|
||||
18
services/migrations/0003_delete_groupcache.py
Normal file
18
services/migrations/0003_delete_groupcache.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.5 on 2017-09-02 06:07
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('services', '0002_auto_20161016_0135'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='GroupCache',
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.db import models
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class GroupCache(models.Model):
|
||||
SERVICE_CHOICES = (
|
||||
("discourse", "discourse"),
|
||||
("discord", "discord"),
|
||||
)
|
||||
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
groups = models.TextField(default={})
|
||||
service = models.CharField(max_length=254, choices=SERVICE_CHOICES, unique=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.service
|
||||
|
||||
@@ -3,11 +3,13 @@ import requests
|
||||
import json
|
||||
import re
|
||||
from django.conf import settings
|
||||
from services.models import GroupCache
|
||||
from requests_oauthlib import OAuth2Session
|
||||
from functools import wraps
|
||||
import logging
|
||||
import datetime
|
||||
from django.utils import timezone
|
||||
import time
|
||||
from django.core.cache import cache
|
||||
from hashlib import md5
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -17,9 +19,13 @@ EVE_IMAGE_SERVER = "https://image.eveonline.com"
|
||||
AUTH_URL = "https://discordapp.com/api/oauth2/authorize"
|
||||
TOKEN_URL = "https://discordapp.com/api/oauth2/token"
|
||||
|
||||
# needs administrator, since Discord can't get their permissions system to work
|
||||
# was kick members, manage roles, manage nicknames
|
||||
#BOT_PERMISSIONS = 0x00000002 + 0x10000000 + 0x08000000
|
||||
"""
|
||||
Previously all we asked for was permission to kick members, manage roles, and manage nicknames.
|
||||
Users have reported weird unauthorized errors we don't understand. So now we ask for full server admin.
|
||||
It's almost fixed the problem.
|
||||
"""
|
||||
# kick members, manage roles, manage nicknames
|
||||
# BOT_PERMISSIONS = 0x00000002 + 0x10000000 + 0x08000000
|
||||
BOT_PERMISSIONS = 0x00000008
|
||||
|
||||
# get user ID, accept invite
|
||||
@@ -28,17 +34,133 @@ SCOPES = [
|
||||
'guilds.join',
|
||||
]
|
||||
|
||||
GROUP_CACHE_MAX_AGE = datetime.timedelta(minutes=30)
|
||||
GROUP_CACHE_MAX_AGE = int(getattr(settings, 'DISCORD_GROUP_CACHE_MAX_AGE', 2 * 60 * 60)) # 2 hours default
|
||||
|
||||
|
||||
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:
|
||||
try:
|
||||
retry_after = int(e.response.headers['Retry-After'])
|
||||
except (TypeError, KeyError):
|
||||
# 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=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:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _sanitize_name(name):
|
||||
return re.sub('[^\w.-]', '', name)[:32]
|
||||
|
||||
@staticmethod
|
||||
def _sanitize_groupname(name):
|
||||
name = name.strip(' _')
|
||||
return re.sub('[^\w.-]', '', name)
|
||||
return DiscordOAuthManager._sanitize_name(name)
|
||||
|
||||
@staticmethod
|
||||
def generate_bot_add_url():
|
||||
@@ -83,8 +205,9 @@ class DiscordOAuthManager:
|
||||
@staticmethod
|
||||
def update_nickname(user_id, nickname):
|
||||
try:
|
||||
nickname = DiscordOAuthManager._sanitize_name(nickname)
|
||||
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
|
||||
data = {'nick': nickname, }
|
||||
data = {'nick': nickname}
|
||||
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id)
|
||||
r = requests.patch(path, headers=custom_headers, json=data)
|
||||
logger.debug("Got status code %s after setting nickname for Discord user ID %s (%s)" % (
|
||||
@@ -115,7 +238,7 @@ class DiscordOAuthManager:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def __get_groups():
|
||||
def _get_groups():
|
||||
custom_headers = {'accept': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
|
||||
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/roles"
|
||||
r = requests.get(path, headers=custom_headers)
|
||||
@@ -124,41 +247,20 @@ class DiscordOAuthManager:
|
||||
return r.json()
|
||||
|
||||
@staticmethod
|
||||
def __update_group_cache():
|
||||
GroupCache.objects.filter(service="discord").delete()
|
||||
cache = GroupCache.objects.create(service="discord")
|
||||
cache.groups = json.dumps(DiscordOAuthManager.__get_groups())
|
||||
cache.save()
|
||||
return cache
|
||||
def _generate_cache_role_key(name):
|
||||
return 'DISCORD_ROLE_NAME__%s' % md5(str(name).encode('utf-8')).hexdigest()
|
||||
|
||||
@staticmethod
|
||||
def __get_group_cache():
|
||||
if not GroupCache.objects.filter(service="discord").exists():
|
||||
DiscordOAuthManager.__update_group_cache()
|
||||
cache = GroupCache.objects.get(service="discord")
|
||||
age = timezone.now() - cache.created
|
||||
if age > GROUP_CACHE_MAX_AGE:
|
||||
logger.debug("Group cache has expired. Triggering update.")
|
||||
cache = DiscordOAuthManager.__update_group_cache()
|
||||
return json.loads(cache.groups)
|
||||
def _group_name_to_id(name):
|
||||
name = DiscordOAuthManager._sanitize_groupname(name)
|
||||
|
||||
@staticmethod
|
||||
def __group_name_to_id(name):
|
||||
cache = DiscordOAuthManager.__get_group_cache()
|
||||
for g in cache:
|
||||
if g['name'] == name:
|
||||
return g['id']
|
||||
logger.debug("Group %s not found on Discord. Creating" % name)
|
||||
DiscordOAuthManager.__create_group(name)
|
||||
return DiscordOAuthManager.__group_name_to_id(name)
|
||||
|
||||
@staticmethod
|
||||
def __group_id_to_name(id):
|
||||
cache = DiscordOAuthManager.__get_group_cache()
|
||||
for g in cache:
|
||||
if g['id'] == id:
|
||||
return g['name']
|
||||
raise KeyError("Group ID %s not found on Discord" % id)
|
||||
def get_or_make_role():
|
||||
groups = DiscordOAuthManager._get_groups()
|
||||
for g in groups:
|
||||
if g['name'] == name:
|
||||
return g['id']
|
||||
return DiscordOAuthManager._create_group(name)['id']
|
||||
return cache.get_or_set(DiscordOAuthManager._generate_cache_role_key(name), get_or_make_role, GROUP_CACHE_MAX_AGE)
|
||||
|
||||
@staticmethod
|
||||
def __generate_role():
|
||||
@@ -185,17 +287,18 @@ class DiscordOAuthManager:
|
||||
return r.json()
|
||||
|
||||
@staticmethod
|
||||
def __create_group(name):
|
||||
def _create_group(name):
|
||||
role = DiscordOAuthManager.__generate_role()
|
||||
DiscordOAuthManager.__edit_role(role['id'], name)
|
||||
DiscordOAuthManager.__update_group_cache()
|
||||
return DiscordOAuthManager.__edit_role(role['id'], name)
|
||||
|
||||
@staticmethod
|
||||
@api_backoff
|
||||
def update_groups(user_id, groups):
|
||||
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
|
||||
group_ids = [DiscordOAuthManager.__group_name_to_id(DiscordOAuthManager._sanitize_groupname(g)) for g in groups]
|
||||
group_ids = [DiscordOAuthManager._group_name_to_id(DiscordOAuthManager._sanitize_groupname(g)) for g in groups]
|
||||
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id)
|
||||
data = {'roles': group_ids}
|
||||
r = requests.patch(path, headers=custom_headers, json=data)
|
||||
logger.debug("Received status code %s after setting user roles" % r.status_code)
|
||||
r.raise_for_status()
|
||||
|
||||
|
||||
@@ -9,8 +9,7 @@ from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
from eveonline.managers import EveManager
|
||||
from notifications import notify
|
||||
from services.modules.discord.manager import DiscordOAuthManager
|
||||
from services.tasks import only_one
|
||||
from services.modules.discord.manager import DiscordOAuthManager, DiscordApiBackoff
|
||||
from .models import DiscordUser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -74,6 +73,10 @@ class DiscordTasks:
|
||||
logger.debug("Updating user %s discord groups to %s" % (user, groups))
|
||||
try:
|
||||
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:
|
||||
if task_self:
|
||||
logger.exception("Discord group sync failed for %s, retrying in 10 mins" % user)
|
||||
@@ -118,7 +121,7 @@ class DiscordTasks:
|
||||
def update_all_nicknames():
|
||||
logger.debug("Updating ALL discord nicknames")
|
||||
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
|
||||
def disable(cls):
|
||||
|
||||
@@ -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">
|
||||
{% 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>
|
||||
</a>
|
||||
{% 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>
|
||||
</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>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -17,6 +17,10 @@ from alliance_auth.tests.auth_utils import AuthUtils
|
||||
from .auth_hooks import DiscordService
|
||||
from .models import DiscordUser
|
||||
from .tasks import DiscordTasks
|
||||
from .manager import DiscordOAuthManager
|
||||
|
||||
import requests_mock
|
||||
import datetime
|
||||
|
||||
MODULE_PATH = 'services.modules.discord'
|
||||
|
||||
@@ -198,3 +202,254 @@ class DiscordViewsTestCase(TestCase):
|
||||
self.assertRedirects(response, expected_url='/en/services/', target_status_code=200)
|
||||
with self.assertRaises(ObjectDoesNotExist):
|
||||
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._get_groups')
|
||||
@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._get_groups')
|
||||
@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._get_groups')
|
||||
@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())
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
from __future__ import unicode_literals
|
||||
import logging
|
||||
import requests
|
||||
import random
|
||||
import string
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from services.models import GroupCache
|
||||
from django.core.cache import cache
|
||||
from hashlib import md5
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
GROUP_CACHE_MAX_AGE = int(getattr(settings, 'DISCOURSE_GROUP_CACHE_MAX_AGE', 2 * 60 * 60)) # default 2 hours
|
||||
|
||||
|
||||
class DiscourseError(Exception):
|
||||
def __init__(self, endpoint, errors):
|
||||
@@ -21,12 +19,13 @@ class DiscourseError(Exception):
|
||||
def __str__(self):
|
||||
return "API execution failed.\nErrors: %s\nEndpoint: %s" % (self.errors, self.endpoint)
|
||||
|
||||
|
||||
# not exhaustive, only the ones we need
|
||||
ENDPOINTS = {
|
||||
'groups': {
|
||||
'list': {
|
||||
'path': "/admin/groups.json",
|
||||
'method': requests.get,
|
||||
'method': 'get',
|
||||
'args': {
|
||||
'required': [],
|
||||
'optional': [],
|
||||
@@ -34,7 +33,7 @@ ENDPOINTS = {
|
||||
},
|
||||
'create': {
|
||||
'path': "/admin/groups",
|
||||
'method': requests.post,
|
||||
'method': 'post',
|
||||
'args': {
|
||||
'required': ['name'],
|
||||
'optional': ['visible'],
|
||||
@@ -42,7 +41,7 @@ ENDPOINTS = {
|
||||
},
|
||||
'add_user': {
|
||||
'path': "/admin/groups/%s/members.json",
|
||||
'method': requests.put,
|
||||
'method': 'put',
|
||||
'args': {
|
||||
'required': ['usernames'],
|
||||
'optional': [],
|
||||
@@ -50,7 +49,7 @@ ENDPOINTS = {
|
||||
},
|
||||
'remove_user': {
|
||||
'path': "/admin/groups/%s/members.json",
|
||||
'method': requests.delete,
|
||||
'method': 'delete',
|
||||
'args': {
|
||||
'required': ['username'],
|
||||
'optional': [],
|
||||
@@ -58,7 +57,7 @@ ENDPOINTS = {
|
||||
},
|
||||
'delete': {
|
||||
'path': "/admin/groups/%s.json",
|
||||
'method': requests.delete,
|
||||
'method': 'delete',
|
||||
'args': {
|
||||
'required': [],
|
||||
'optional': [],
|
||||
@@ -68,7 +67,7 @@ ENDPOINTS = {
|
||||
'users': {
|
||||
'create': {
|
||||
'path': "/users",
|
||||
'method': requests.post,
|
||||
'method': 'post',
|
||||
'args': {
|
||||
'required': ['name', 'email', 'password', 'username'],
|
||||
'optional': ['active'],
|
||||
@@ -76,7 +75,7 @@ ENDPOINTS = {
|
||||
},
|
||||
'update': {
|
||||
'path': "/users/%s.json",
|
||||
'method': requests.put,
|
||||
'method': 'put',
|
||||
'args': {
|
||||
'required': ['params'],
|
||||
'optional': [],
|
||||
@@ -84,7 +83,7 @@ ENDPOINTS = {
|
||||
},
|
||||
'get': {
|
||||
'path': "/users/%s.json",
|
||||
'method': requests.get,
|
||||
'method': 'get',
|
||||
'args': {
|
||||
'required': [],
|
||||
'optional': [],
|
||||
@@ -92,7 +91,7 @@ ENDPOINTS = {
|
||||
},
|
||||
'activate': {
|
||||
'path': "/admin/users/%s/activate",
|
||||
'method': requests.put,
|
||||
'method': 'put',
|
||||
'args': {
|
||||
'required': [],
|
||||
'optional': [],
|
||||
@@ -100,7 +99,7 @@ ENDPOINTS = {
|
||||
},
|
||||
'set_email': {
|
||||
'path': "/users/%s/preferences/email",
|
||||
'method': requests.put,
|
||||
'method': 'put',
|
||||
'args': {
|
||||
'required': ['email'],
|
||||
'optional': [],
|
||||
@@ -108,7 +107,7 @@ ENDPOINTS = {
|
||||
},
|
||||
'suspend': {
|
||||
'path': "/admin/users/%s/suspend",
|
||||
'method': requests.put,
|
||||
'method': 'put',
|
||||
'args': {
|
||||
'required': ['duration', 'reason'],
|
||||
'optional': [],
|
||||
@@ -116,7 +115,7 @@ ENDPOINTS = {
|
||||
},
|
||||
'unsuspend': {
|
||||
'path': "/admin/users/%s/unsuspend",
|
||||
'method': requests.put,
|
||||
'method': 'put',
|
||||
'args': {
|
||||
'required': [],
|
||||
'optional': [],
|
||||
@@ -124,7 +123,7 @@ ENDPOINTS = {
|
||||
},
|
||||
'logout': {
|
||||
'path': "/admin/users/%s/log_out",
|
||||
'method': requests.post,
|
||||
'method': 'post',
|
||||
'args': {
|
||||
'required': [],
|
||||
'optional': [],
|
||||
@@ -132,7 +131,7 @@ ENDPOINTS = {
|
||||
},
|
||||
'external': {
|
||||
'path': "/users/by-external/%s.json",
|
||||
'method': requests.get,
|
||||
'method': 'get',
|
||||
'args': {
|
||||
'required': [],
|
||||
'optional': [],
|
||||
@@ -146,8 +145,7 @@ class DiscourseManager:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
GROUP_CACHE_MAX_AGE = datetime.timedelta(minutes=30)
|
||||
REVOKED_EMAIL = 'revoked@' + settings.DOMAIN
|
||||
REVOKED_EMAIL = 'revoked@localhost'
|
||||
SUSPEND_DAYS = 99999
|
||||
SUSPEND_REASON = "Disabled by auth."
|
||||
|
||||
@@ -171,7 +169,8 @@ class DiscourseManager:
|
||||
for arg in kwargs:
|
||||
if arg not in endpoint['args']['required'] and arg not in endpoint['args']['optional'] and not silent:
|
||||
logger.warn("Received unrecognized kwarg %s for endpoint %s" % (arg, endpoint))
|
||||
r = endpoint['method'](settings.DISCOURSE_URL + endpoint['parsed_url'], params=params, json=data)
|
||||
r = getattr(requests, endpoint['method'])(settings.DISCOURSE_URL + endpoint['parsed_url'], params=params,
|
||||
json=data)
|
||||
try:
|
||||
if 'errors' in r.json() and not silent:
|
||||
logger.error("Discourse execution failed.\nEndpoint: %s\nErrors: %s" % (endpoint, r.json()['errors']))
|
||||
@@ -190,67 +189,59 @@ class DiscourseManager:
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def __generate_random_pass():
|
||||
return ''.join([random.choice(string.ascii_letters + string.digits) for n in range(16)])
|
||||
|
||||
@staticmethod
|
||||
def __get_groups():
|
||||
def _get_groups():
|
||||
endpoint = ENDPOINTS['groups']['list']
|
||||
data = DiscourseManager.__exc(endpoint)
|
||||
return [g for g in data if not g['automatic']]
|
||||
|
||||
@staticmethod
|
||||
def __update_group_cache():
|
||||
GroupCache.objects.filter(service="discourse").delete()
|
||||
cache = GroupCache.objects.create(service="discourse")
|
||||
cache.groups = json.dumps(DiscourseManager.__get_groups())
|
||||
cache.save()
|
||||
return cache
|
||||
|
||||
@staticmethod
|
||||
def __get_group_cache():
|
||||
if not GroupCache.objects.filter(service="discourse").exists():
|
||||
DiscourseManager.__update_group_cache()
|
||||
cache = GroupCache.objects.get(service="discourse")
|
||||
age = timezone.now() - cache.created
|
||||
if age > DiscourseManager.GROUP_CACHE_MAX_AGE:
|
||||
logger.debug("Group cache has expired. Triggering update.")
|
||||
cache = DiscourseManager.__update_group_cache()
|
||||
return json.loads(cache.groups)
|
||||
|
||||
@staticmethod
|
||||
def __create_group(name):
|
||||
def _create_group(name):
|
||||
endpoint = ENDPOINTS['groups']['create']
|
||||
DiscourseManager.__exc(endpoint, name=name[:20], visible=True)
|
||||
DiscourseManager.__update_group_cache()
|
||||
return DiscourseManager.__exc(endpoint, name=name[:20], visible=True)['basic_group']
|
||||
|
||||
@staticmethod
|
||||
def _generate_cache_group_name_key(name):
|
||||
return 'DISCOURSE_GROUP_NAME__%s' % md5(name.encode('utf-8')).hexdigest()
|
||||
|
||||
@staticmethod
|
||||
def _generate_cache_group_id_key(g_id):
|
||||
return 'DISCOURSE_GROUP_ID__%s' % g_id
|
||||
|
||||
@staticmethod
|
||||
def __group_name_to_id(name):
|
||||
cache = DiscourseManager.__get_group_cache()
|
||||
for g in cache:
|
||||
if g['name'] == name[0:20]:
|
||||
return g['id']
|
||||
logger.debug("Group %s not found on Discourse. Creating" % name)
|
||||
DiscourseManager.__create_group(name)
|
||||
return DiscourseManager.__group_name_to_id(name)
|
||||
name = DiscourseManager._sanitize_groupname(name)
|
||||
|
||||
def get_or_create_group():
|
||||
groups = DiscourseManager._get_groups()
|
||||
for g in groups:
|
||||
if g['name'] == name:
|
||||
return g['id']
|
||||
return DiscourseManager._create_group(name)['id']
|
||||
|
||||
return cache.get_or_set(DiscourseManager._generate_cache_group_name_key(name), get_or_create_group,
|
||||
GROUP_CACHE_MAX_AGE)
|
||||
|
||||
@staticmethod
|
||||
def __group_id_to_name(id):
|
||||
cache = DiscourseManager.__get_group_cache()
|
||||
for g in cache:
|
||||
if g['id'] == id:
|
||||
return g['name']
|
||||
raise KeyError("Group ID %s not found on Discourse" % id)
|
||||
def __group_id_to_name(g_id):
|
||||
def get_group_name():
|
||||
groups = DiscourseManager._get_groups()
|
||||
for g in groups:
|
||||
if g['id'] == g_id:
|
||||
return g['name']
|
||||
raise KeyError("Group ID %s not found on Discourse" % g_id)
|
||||
|
||||
return cache.get_or_set(DiscourseManager._generate_cache_group_id_key(g_id), get_group_name,
|
||||
GROUP_CACHE_MAX_AGE)
|
||||
|
||||
@staticmethod
|
||||
def __add_user_to_group(id, username):
|
||||
def __add_user_to_group(g_id, username):
|
||||
endpoint = ENDPOINTS['groups']['add_user']
|
||||
DiscourseManager.__exc(endpoint, id, usernames=[username])
|
||||
DiscourseManager.__exc(endpoint, g_id, usernames=[username])
|
||||
|
||||
@staticmethod
|
||||
def __remove_user_from_group(id, username):
|
||||
def __remove_user_from_group(g_id, username):
|
||||
endpoint = ENDPOINTS['groups']['remove_user']
|
||||
DiscourseManager.__exc(endpoint, id, username=username)
|
||||
DiscourseManager.__exc(endpoint, g_id, username=username)
|
||||
|
||||
@staticmethod
|
||||
def __generate_group_dict(names):
|
||||
@@ -269,10 +260,6 @@ class DiscourseManager:
|
||||
data = DiscourseManager.__get_user(name, silent=silent)
|
||||
return data['user']['id']
|
||||
|
||||
@staticmethod
|
||||
def __user_id_to_name(id):
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def __get_user(username, silent=False):
|
||||
endpoint = ENDPOINTS['users']['get']
|
||||
@@ -281,14 +268,14 @@ class DiscourseManager:
|
||||
@staticmethod
|
||||
def __activate_user(username):
|
||||
endpoint = ENDPOINTS['users']['activate']
|
||||
id = DiscourseManager.__user_name_to_id(username)
|
||||
DiscourseManager.__exc(endpoint, id)
|
||||
u_id = DiscourseManager.__user_name_to_id(username)
|
||||
DiscourseManager.__exc(endpoint, u_id)
|
||||
|
||||
@staticmethod
|
||||
def __update_user(username, **kwargs):
|
||||
endpoint = ENDPOINTS['users']['update']
|
||||
id = DiscourseManager.__user_name_to_id(username)
|
||||
DiscourseManager.__exc(endpoint, id, params=kwargs)
|
||||
u_id = DiscourseManager.__user_name_to_id(username)
|
||||
DiscourseManager.__exc(endpoint, u_id, params=kwargs)
|
||||
|
||||
@staticmethod
|
||||
def __create_user(username, email, password):
|
||||
@@ -300,21 +287,21 @@ class DiscourseManager:
|
||||
try:
|
||||
DiscourseManager.__user_name_to_id(username, silent=True)
|
||||
return True
|
||||
except:
|
||||
except DiscourseError:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def __suspend_user(username):
|
||||
id = DiscourseManager.__user_name_to_id(username)
|
||||
u_id = DiscourseManager.__user_name_to_id(username)
|
||||
endpoint = ENDPOINTS['users']['suspend']
|
||||
return DiscourseManager.__exc(endpoint, id, duration=DiscourseManager.SUSPEND_DAYS,
|
||||
return DiscourseManager.__exc(endpoint, u_id, duration=DiscourseManager.SUSPEND_DAYS,
|
||||
reason=DiscourseManager.SUSPEND_REASON)
|
||||
|
||||
@staticmethod
|
||||
def __unsuspend(username):
|
||||
id = DiscourseManager.__user_name_to_id(username)
|
||||
u_id = DiscourseManager.__user_name_to_id(username)
|
||||
endpoint = ENDPOINTS['users']['unsuspend']
|
||||
return DiscourseManager.__exc(endpoint, id)
|
||||
return DiscourseManager.__exc(endpoint, u_id)
|
||||
|
||||
@staticmethod
|
||||
def __set_email(username, email):
|
||||
@@ -322,47 +309,53 @@ class DiscourseManager:
|
||||
return DiscourseManager.__exc(endpoint, username, email=email)
|
||||
|
||||
@staticmethod
|
||||
def __logout(id):
|
||||
def __logout(u_id):
|
||||
endpoint = ENDPOINTS['users']['logout']
|
||||
return DiscourseManager.__exc(endpoint, id)
|
||||
return DiscourseManager.__exc(endpoint, u_id)
|
||||
|
||||
@staticmethod
|
||||
def __get_user_by_external(id):
|
||||
def __get_user_by_external(u_id):
|
||||
endpoint = ENDPOINTS['users']['external']
|
||||
return DiscourseManager.__exc(endpoint, id)
|
||||
return DiscourseManager.__exc(endpoint, u_id)
|
||||
|
||||
@staticmethod
|
||||
def __user_id_by_external_id(id):
|
||||
data = DiscourseManager.__get_user_by_external(id)
|
||||
def __user_id_by_external_id(u_id):
|
||||
data = DiscourseManager.__get_user_by_external(u_id)
|
||||
return data['user']['id']
|
||||
|
||||
@staticmethod
|
||||
def _sanitize_name(name):
|
||||
name = name.replace(' ', '_')
|
||||
name = name.replace("'", '')
|
||||
name = name.lstrip(' _')
|
||||
name = name[:20]
|
||||
name = name.rstrip(' _')
|
||||
return name
|
||||
|
||||
@staticmethod
|
||||
def _sanitize_username(username):
|
||||
sanitized = username.replace(" ", "_")
|
||||
sanitized = sanitized.strip(' _')
|
||||
sanitized = sanitized.replace("'", "")
|
||||
return sanitized
|
||||
return DiscourseManager._sanitize_name(username)
|
||||
|
||||
@staticmethod
|
||||
def _sanitize_groupname(name):
|
||||
name = name.strip(' _')
|
||||
name = re.sub('[^\w]', '', name)
|
||||
name = DiscourseManager._sanitize_name(name)
|
||||
if len(name) < 3:
|
||||
name = name + "".join('_' for i in range(3-len(name)))
|
||||
return name[:20]
|
||||
name = "Group " + name
|
||||
return name
|
||||
|
||||
@staticmethod
|
||||
def update_groups(user):
|
||||
groups = []
|
||||
for g in user.groups.all():
|
||||
groups.append(DiscourseManager._sanitize_groupname(str(g)[:20]))
|
||||
groups.append(DiscourseManager._sanitize_groupname(str(g)))
|
||||
logger.debug("Updating discourse user %s groups to %s" % (user, groups))
|
||||
group_dict = DiscourseManager.__generate_group_dict(groups)
|
||||
inv_group_dict = {v: k for k, v in group_dict.items()}
|
||||
username = DiscourseManager.__get_user_by_external(user.pk)['user']['username']
|
||||
user_groups = DiscourseManager.__get_user_groups(username)
|
||||
add_groups = [group_dict[x] for x in group_dict if not group_dict[x] in user_groups]
|
||||
rem_groups = [x for x in user_groups if not x in inv_group_dict]
|
||||
rem_groups = [x for x in user_groups if x not in inv_group_dict]
|
||||
if add_groups or rem_groups:
|
||||
logger.info(
|
||||
"Updating discourse user %s groups: adding %s, removing %s" % (username, add_groups, rem_groups))
|
||||
@@ -376,6 +369,5 @@ class DiscourseManager:
|
||||
logger.debug("Disabling user %s Discourse access." % user)
|
||||
d_user = DiscourseManager.__get_user_by_external(user.pk)
|
||||
DiscourseManager.__logout(d_user['user']['id'])
|
||||
DiscourseManager.__suspend_user(d_user['user']['username'])
|
||||
logger.info("Disabled user %s Discourse access." % user)
|
||||
return True
|
||||
|
||||
@@ -5,8 +5,6 @@ from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from notifications import notify
|
||||
|
||||
from services.tasks import only_one
|
||||
|
||||
from .manager import DiscourseManager
|
||||
from .models import DiscourseUser
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
<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 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>
|
||||
|
||||
@@ -21,6 +21,7 @@ class MumbleService(ServicesHook):
|
||||
self.urlpatterns = urlpatterns
|
||||
self.service_url = settings.MUMBLE_URL
|
||||
self.access_perm = 'mumble.access_mumble'
|
||||
self.service_ctrl_template = 'registered/mumble_service_ctrl.html'
|
||||
|
||||
def delete_user(self, user, notify_user=False):
|
||||
logging.debug("Deleting user %s %s account" % (user, self.name))
|
||||
@@ -57,6 +58,7 @@ class MumbleService(ServicesHook):
|
||||
'service_name': self.title,
|
||||
'urls': urls,
|
||||
'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 '',
|
||||
}, request=request)
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -61,7 +61,7 @@ class OpenfireManager:
|
||||
|
||||
@staticmethod
|
||||
def _sanitize_groupname(name):
|
||||
name = name.strip(' _')
|
||||
name = name.strip(' _').lower()
|
||||
return re.sub('[^\w.-]', '', name)
|
||||
|
||||
@staticmethod
|
||||
@@ -120,9 +120,10 @@ class OpenfireManager:
|
||||
logger.error("Unable to update openfire user %s password - user not found on server." % username)
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def update_user_groups(username, groups):
|
||||
@classmethod
|
||||
def update_user_groups(cls, username, groups):
|
||||
logger.debug("Updating openfire user %s groups %s" % (username, groups))
|
||||
s_groups = list(map(cls._sanitize_groupname, groups)) # Sanitized group names
|
||||
api = ofUsers(settings.OPENFIRE_ADDRESS, settings.OPENFIRE_SECRET_KEY)
|
||||
response = api.get_user_groups(username)
|
||||
remote_groups = []
|
||||
@@ -130,16 +131,15 @@ class OpenfireManager:
|
||||
remote_groups = response['groupname']
|
||||
if isinstance(remote_groups, six.string_types):
|
||||
remote_groups = [remote_groups]
|
||||
remote_groups = list(map(cls._sanitize_groupname, remote_groups))
|
||||
logger.debug("Openfire user %s has groups %s" % (username, remote_groups))
|
||||
add_groups = []
|
||||
del_groups = []
|
||||
for g in groups:
|
||||
g = OpenfireManager._sanitize_groupname(g)
|
||||
for g in s_groups:
|
||||
if g not in remote_groups:
|
||||
add_groups.append(g)
|
||||
for g in remote_groups:
|
||||
g = OpenfireManager._sanitize_groupname(g)
|
||||
if g not in groups:
|
||||
if g not in s_groups:
|
||||
del_groups.append(g)
|
||||
logger.info(
|
||||
"Updating openfire groups for user %s - adding %s, removing %s" % (username, add_groups, del_groups))
|
||||
@@ -155,10 +155,11 @@ class OpenfireManager:
|
||||
api.delete_user_groups(username, groups)
|
||||
logger.info("Deleted groups %s from openfire user %s" % (groups, username))
|
||||
|
||||
@staticmethod
|
||||
def send_broadcast_message(group_name, broadcast_message):
|
||||
logger.debug("Sending jabber ping to group %s with message %s" % (group_name, broadcast_message))
|
||||
to_address = group_name + '@' + settings.BROADCAST_SERVICE_NAME + '.' + settings.JABBER_URL
|
||||
@classmethod
|
||||
def send_broadcast_message(cls, group_name, broadcast_message):
|
||||
s_group_name = cls._sanitize_groupname(group_name)
|
||||
logger.debug("Sending jabber ping to group %s with message %s" % (s_group_name, broadcast_message))
|
||||
to_address = s_group_name + '@' + settings.BROADCAST_SERVICE_NAME + '.' + settings.JABBER_URL
|
||||
xmpp = PingBot(settings.BROADCAST_USER, settings.BROADCAST_USER_PASSWORD, to_address, broadcast_message)
|
||||
xmpp.register_plugin('xep_0030') # Service Discovery
|
||||
xmpp.register_plugin('xep_0199') # XMPP Ping
|
||||
|
||||
@@ -219,3 +219,28 @@ class OpenfireManagerTestCase(TestCase):
|
||||
result_username = self.manager._OpenfireManager__sanitize_username(test_username)
|
||||
|
||||
self.assertEqual(result_username, 'My_Test\\20User\\22\\27\\26\\2f\\3a\\3c\\3e\\40name\\5c20name')
|
||||
|
||||
def test__sanitize_groupname(self):
|
||||
test_groupname = " My_Test Groupname"
|
||||
|
||||
result_groupname = self.manager._sanitize_groupname(test_groupname)
|
||||
|
||||
self.assertEqual(result_groupname, "my_testgroupname")
|
||||
|
||||
@mock.patch(MODULE_PATH + '.manager.ofUsers')
|
||||
def test_update_user_groups(self, api):
|
||||
groups = ["AddGroup", "othergroup", "Guest Group"]
|
||||
server_groups = ["othergroup", "Guest Group", "REMOVE group"]
|
||||
username = "testuser"
|
||||
api_instance = api.return_value
|
||||
api_instance.get_user_groups.return_value = {'groupname': server_groups}
|
||||
|
||||
self.manager.update_user_groups(username, groups)
|
||||
|
||||
self.assertTrue(api_instance.add_user_groups.called)
|
||||
args, kwargs = api_instance.add_user_groups.call_args
|
||||
self.assertEqual(args[1], ["addgroup"])
|
||||
|
||||
self.assertTrue(api_instance.delete_user_groups.called)
|
||||
args, kwargs = api_instance.delete_user_groups.call_args
|
||||
self.assertEqual(args[1], ["removegroup"])
|
||||
|
||||
@@ -18,7 +18,7 @@ logger = logging.getLogger(__name__)
|
||||
class Phpbb3Manager:
|
||||
SQL_ADD_USER = r"INSERT INTO phpbb_users (username, username_clean, " \
|
||||
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"
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@ from __future__ import unicode_literals
|
||||
import random
|
||||
import string
|
||||
import requests
|
||||
import hashlib
|
||||
from eveonline.managers import EveManager
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.cache import cache
|
||||
|
||||
from six import iteritems
|
||||
|
||||
@@ -20,7 +22,7 @@ class SeatManager:
|
||||
RESPONSE_OK = 'ok'
|
||||
|
||||
@staticmethod
|
||||
def __santatize_username(username):
|
||||
def __sanitize_username(username):
|
||||
sanatized = username.replace(" ", "_")
|
||||
return sanatized.lower()
|
||||
|
||||
@@ -33,7 +35,7 @@ class SeatManager:
|
||||
return cls.RESPONSE_OK in response
|
||||
|
||||
@staticmethod
|
||||
def exec_request(endpoint, func, **kwargs):
|
||||
def exec_request(endpoint, func, raise_for_status=False, **kwargs):
|
||||
""" Send an https api request """
|
||||
try:
|
||||
endpoint = '{0}/api/v1/{1}'.format(settings.SEAT_URL, endpoint)
|
||||
@@ -43,23 +45,25 @@ class SeatManager:
|
||||
ret = getattr(requests, func)(endpoint, headers=headers, data=kwargs)
|
||||
ret.raise_for_status()
|
||||
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))
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def add_user(cls, username, email):
|
||||
""" Add user to service """
|
||||
sanatized = str(SeatManager.__santatize_username(username))
|
||||
logger.debug("Adding user to SeAT with username %s" % sanatized)
|
||||
password = SeatManager.__generate_random_pass()
|
||||
ret = SeatManager.exec_request('user', 'post', username=sanatized, email=str(email), password=password)
|
||||
sanitized = str(cls.__sanitize_username(username))
|
||||
logger.debug("Adding user to SeAT with username %s" % sanitized)
|
||||
password = cls.__generate_random_pass()
|
||||
ret = cls.exec_request('user', 'post', username=sanitized, email=str(email), password=password)
|
||||
logger.debug(ret)
|
||||
if cls._response_ok(ret):
|
||||
logger.info("Added SeAT user with username %s" % sanatized)
|
||||
return sanatized, password
|
||||
logger.info("Failed to add SeAT user with username %s" % sanatized)
|
||||
return None
|
||||
logger.info("Added SeAT user with username %s" % sanitized)
|
||||
return sanitized, password
|
||||
logger.info("Failed to add SeAT user with username %s" % sanitized)
|
||||
return None, None
|
||||
|
||||
@classmethod
|
||||
def delete_user(cls, username):
|
||||
@@ -71,29 +75,10 @@ class SeatManager:
|
||||
return username
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def disable_user(cls, username):
|
||||
""" Disable user """
|
||||
ret = cls.exec_request('user/{}'.format(username), 'put', active=0)
|
||||
logger.debug(ret)
|
||||
ret = cls.exec_request('user/{}'.format(username), 'put', email="")
|
||||
logger.debug(ret)
|
||||
if cls._response_ok(ret):
|
||||
try:
|
||||
cls.update_roles(username, [])
|
||||
logger.info("Disabled SeAT user with username %s" % username)
|
||||
return username
|
||||
except KeyError:
|
||||
# if something goes wrong, delete user from seat instead of disabling
|
||||
if cls.delete_user(username):
|
||||
return username
|
||||
logger.info("Failed to disabled SeAT user with username %s" % username)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def enable_user(cls, username):
|
||||
""" 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)
|
||||
if cls._response_ok(ret):
|
||||
logger.info("Enabled SeAT user with username %s" % username)
|
||||
@@ -101,16 +86,23 @@ class SeatManager:
|
||||
logger.info("Failed to enabled SeAT user with username %s" % username)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _check_email_changed(cls, username, email):
|
||||
"""Compares email to one set on SeAT"""
|
||||
ret = cls.exec_request('user/{}'.format(username), 'get', raise_for_status=True)
|
||||
return ret['email'] != email
|
||||
|
||||
@classmethod
|
||||
def update_user(cls, username, email, password):
|
||||
""" Edit user info """
|
||||
logger.debug("Updating SeAT username %s with email %s and password hash starting with %s" % (username, email,
|
||||
password[0:5]))
|
||||
ret = SeatManager.exec_request('user/{}'.format(username), 'put', email=email)
|
||||
logger.debug(ret)
|
||||
if not cls._response_ok(ret):
|
||||
logger.warn("Failed to update email for username {}".format(username))
|
||||
ret = SeatManager.exec_request('user/{}'.format(username), 'put', password=password)
|
||||
if cls._check_email_changed(username, email):
|
||||
# if we try to set the email to whatever it is already on SeAT, we get a HTTP422 error
|
||||
logger.debug("Updating SeAT username %s with email %s and password" % (username, email))
|
||||
ret = cls.exec_request('user/{}'.format(username), 'put', email=email)
|
||||
logger.debug(ret)
|
||||
if not cls._response_ok(ret):
|
||||
logger.warn("Failed to update email for username {}".format(username))
|
||||
ret = cls.exec_request('user/{}'.format(username), 'put', password=password)
|
||||
logger.debug(ret)
|
||||
if not cls._response_ok(ret):
|
||||
logger.warn("Failed to update password for username {}".format(username))
|
||||
@@ -118,25 +110,25 @@ class SeatManager:
|
||||
logger.info("Updated SeAT user with username %s" % username)
|
||||
return username
|
||||
|
||||
@staticmethod
|
||||
def update_user_password(username, email, plain_password=None):
|
||||
@classmethod
|
||||
def update_user_password(cls, username, email, plain_password=None):
|
||||
logger.debug("Settings new SeAT password for user %s" % username)
|
||||
if not plain_password:
|
||||
plain_password = SeatManager.__generate_random_pass()
|
||||
if SeatManager.update_user(username, email, plain_password):
|
||||
plain_password = cls.__generate_random_pass()
|
||||
if cls.update_user(username, email, plain_password):
|
||||
return plain_password
|
||||
|
||||
@staticmethod
|
||||
def check_user_status(username):
|
||||
sanatized = str(SeatManager.__santatize_username(username))
|
||||
logger.debug("Checking SeAT status for user %s" % sanatized)
|
||||
ret = SeatManager.exec_request('user/{}'.format(sanatized), 'get')
|
||||
@classmethod
|
||||
def check_user_status(cls, username):
|
||||
sanitized = str(cls.__sanitize_username(username))
|
||||
logger.debug("Checking SeAT status for user %s" % sanitized)
|
||||
ret = cls.exec_request('user/{}'.format(sanitized), 'get')
|
||||
logger.debug(ret)
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def get_all_seat_eveapis():
|
||||
seat_all_keys = SeatManager.exec_request('key', 'get')
|
||||
@classmethod
|
||||
def get_all_seat_eveapis(cls):
|
||||
seat_all_keys = cls.exec_request('key', 'get')
|
||||
seat_keys = {}
|
||||
for key in seat_all_keys:
|
||||
try:
|
||||
@@ -145,117 +137,132 @@ class SeatManager:
|
||||
seat_keys[key["key_id"]] = None
|
||||
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
|
||||
if user:
|
||||
keypars = EveManager.get_api_key_pairs(user)
|
||||
try:
|
||||
userinfo = SeatManager.check_user_status(user.seat.username)
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
keypairs = EveManager.get_api_key_pairs(user)
|
||||
else:
|
||||
# retrieve all api keys instead
|
||||
keypars = 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")
|
||||
keypairs = EveManager.get_all_api_key_pairs()
|
||||
|
||||
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
|
||||
for key, key_user in iteritems(seat_all_keys):
|
||||
# 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)
|
||||
try:
|
||||
if (ret['info']['type'] == "Account") or (ret['info']['type'] == "Character"):
|
||||
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)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_all_roles():
|
||||
@classmethod
|
||||
def get_all_roles(cls):
|
||||
groups = {}
|
||||
ret = SeatManager.exec_request('role', 'get')
|
||||
ret = cls.exec_request('role', 'get')
|
||||
logger.debug(ret)
|
||||
for group in ret:
|
||||
groups[group["title"]] = group["id"]
|
||||
logger.debug("Retrieved role list from SeAT: %s" % str(groups))
|
||||
return groups
|
||||
|
||||
@staticmethod
|
||||
def add_role(role):
|
||||
ret = SeatManager.exec_request('role/new', 'post', name=role)
|
||||
@classmethod
|
||||
def add_role(cls, role):
|
||||
ret = cls.exec_request('role/new', 'post', name=role)
|
||||
logger.debug(ret)
|
||||
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)
|
||||
return role_info["id"]
|
||||
|
||||
@staticmethod
|
||||
def add_role_to_user(user_id, role_id):
|
||||
ret = SeatManager.exec_request('role/grant-user-role/{}/{}'.format(user_id, role_id), 'get')
|
||||
@classmethod
|
||||
def add_role_to_user(cls, user_id, role_id):
|
||||
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))
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def revoke_role_from_user(user_id, role_id):
|
||||
ret = SeatManager.exec_request('role/revoke-user-role/{}/{}'.format(user_id, role_id), 'get')
|
||||
@classmethod
|
||||
def revoke_role_from_user(cls, user_id, role_id):
|
||||
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))
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def update_roles(seat_user, roles):
|
||||
@classmethod
|
||||
def update_roles(cls, 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 = {}
|
||||
if type(user_info["roles"]) is list:
|
||||
for role in user_info["roles"]:
|
||||
user_roles[role["title"]] = role["id"]
|
||||
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())
|
||||
remroles = set(user_roles.keys()) - set(roles)
|
||||
|
||||
logger.info("Updating SeAT roles for user %s - adding %s, removing %s" % (seat_user, addroles, remroles))
|
||||
for r in addroles:
|
||||
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))
|
||||
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:
|
||||
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()
|
||||
|
||||
@@ -28,7 +28,7 @@ class SeatTasks:
|
||||
|
||||
@classmethod
|
||||
def delete_user(cls, user, notify_user=False):
|
||||
if cls.has_account(user) and SeatManager.disable_user(user.seat.username):
|
||||
if cls.has_account(user) and SeatManager.delete_user(user.seat.username):
|
||||
user.seat.delete()
|
||||
logger.info("Successfully deactivated SeAT for user %s" % user)
|
||||
if notify_user:
|
||||
|
||||
@@ -100,10 +100,10 @@ class SeatHooksTestCase(TestCase):
|
||||
|
||||
# Test none user is deleted
|
||||
none_user = User.objects.get(username=self.none_user)
|
||||
manager.disable_user.return_value = 'abc123'
|
||||
manager.delete_user.return_value = 'abc123'
|
||||
SeatUser.objects.create(user=none_user, username='abc123')
|
||||
service.validate_user(none_user)
|
||||
self.assertTrue(manager.disable_user.called)
|
||||
self.assertTrue(manager.delete_user.called)
|
||||
with self.assertRaises(ObjectDoesNotExist):
|
||||
none_seat = User.objects.get(username=self.none_user).seat
|
||||
|
||||
@@ -115,7 +115,7 @@ class SeatHooksTestCase(TestCase):
|
||||
result = service.delete_user(member)
|
||||
|
||||
self.assertTrue(result)
|
||||
self.assertTrue(manager.disable_user.called)
|
||||
self.assertTrue(manager.delete_user.called)
|
||||
with self.assertRaises(ObjectDoesNotExist):
|
||||
seat_user = User.objects.get(username=self.member).seat
|
||||
|
||||
@@ -177,7 +177,7 @@ class SeatViewsTestCase(TestCase):
|
||||
|
||||
response = self.client.get(urls.reverse('auth_deactivate_seat'))
|
||||
|
||||
self.assertTrue(manager.disable_user.called)
|
||||
self.assertTrue(manager.delete_user.called)
|
||||
self.assertRedirects(response, expected_url=urls.reverse('auth_services'), target_status_code=200)
|
||||
with self.assertRaises(ObjectDoesNotExist):
|
||||
seat_user = User.objects.get(pk=self.member.pk).seat
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from __future__ import unicode_literals
|
||||
from django.shortcuts import render, redirect
|
||||
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
|
||||
|
||||
@@ -15,6 +17,7 @@ import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ACCESS_PERM = 'seat.access_seat'
|
||||
SERVICE_NAME = {'service': 'SeAT'}
|
||||
|
||||
|
||||
@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)
|
||||
SeatTasks.update_roles.delay(request.user.pk)
|
||||
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)
|
||||
credentials = {
|
||||
'username': request.user.seat.username,
|
||||
@@ -46,6 +51,9 @@ def activate_seat(request):
|
||||
}
|
||||
return render(request, 'registered/service_credentials.html',
|
||||
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)
|
||||
return redirect("auth_services")
|
||||
|
||||
@@ -56,10 +64,15 @@ def deactivate_seat(request):
|
||||
logger.debug("deactivate_seat called by user %s" % request.user)
|
||||
# false we failed
|
||||
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)
|
||||
return redirect("auth_services")
|
||||
else:
|
||||
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)
|
||||
return redirect("auth_services")
|
||||
|
||||
@@ -76,10 +89,15 @@ def reset_seat_password(request):
|
||||
'username': request.user.seat.username,
|
||||
'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)
|
||||
return render(request, 'registered/service_credentials.html',
|
||||
context={'credentials': credentials, 'service': 'SeAT'})
|
||||
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")
|
||||
|
||||
|
||||
@@ -98,11 +116,17 @@ def set_seat_password(request):
|
||||
request.user.email,
|
||||
plain_password=password)
|
||||
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)
|
||||
return redirect("auth_services")
|
||||
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)
|
||||
else:
|
||||
messages.add_message(request, messages.ERROR, _('Invalid password.'))
|
||||
logger.error("Invalid SeAT password provided")
|
||||
else:
|
||||
logger.debug("Request is not type POST - providing empty form.")
|
||||
|
||||
@@ -246,6 +246,10 @@ class Teamspeak3Manager:
|
||||
logger.debug("Deleting user %s with id %s from TS3 server." % (user, uid))
|
||||
if user:
|
||||
clients = self.server.send_command('clientlist')
|
||||
if isinstance(clients, dict):
|
||||
# Rewrap list
|
||||
clients = [clients]
|
||||
|
||||
for client in clients:
|
||||
try:
|
||||
if client['keys']['client_database_id'] == user:
|
||||
|
||||
@@ -1,34 +1,27 @@
|
||||
{% 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>
|
||||
<td class="text-center">Teamspeak 3</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">
|
||||
{% 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>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="ts3server://{{ TEAMSPEAK3_PUBLIC_URL }}?token={{ authinfo.teamspeak3_perm_key }}&nickname={{ authinfo.teamspeak3_uid }}"
|
||||
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">
|
||||
<a href="{% url 'auth_verify_teamspeak3' %}" title="Verify Client ID" class="btn btn-success" title="Verify">
|
||||
<span class="glyphicon glyphicon-log-in"></span>
|
||||
</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>
|
||||
</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>
|
||||
</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 %}
|
||||
</td>
|
||||
</tr>
|
||||
</tr>
|
||||
|
||||
@@ -66,7 +66,7 @@ def verify_teamspeak3(request):
|
||||
logger.debug("Validated user %s joined TS server" % request.user)
|
||||
return redirect("auth_services")
|
||||
else:
|
||||
form = TeamspeakJoinForm({'username': request.user.teamspeak3.uid})
|
||||
form = TeamspeakJoinForm(initial={'username': request.user.teamspeak3.uid})
|
||||
context = {
|
||||
'form': form,
|
||||
'authinfo': {'teamspeak3_uid': request.user.teamspeak3.uid,
|
||||
|
||||
@@ -53,7 +53,7 @@ def m2m_changed_user_permissions(sender, instance, action, *args, **kwargs):
|
||||
svc.validate_user(instance)
|
||||
except:
|
||||
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())
|
||||
|
||||
|
||||
@@ -25,9 +25,5 @@ class SrpFleetUserRequestForm(forms.Form):
|
||||
return data
|
||||
|
||||
|
||||
class SrpFleetUpdateCostForm(forms.Form):
|
||||
srp_total_amount = forms.IntegerField(required=True, label=_("Total SRP Amount"))
|
||||
|
||||
|
||||
class SrpFleetMainUpdateForm(forms.Form):
|
||||
fleet_aar_link = forms.CharField(required=True, label=_("After Action Report Link"))
|
||||
|
||||
190
srp/views.py
190
srp/views.py
@@ -3,13 +3,13 @@ from django.shortcuts import render, redirect
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.contrib import messages
|
||||
from django.http import JsonResponse
|
||||
from eveonline.managers import EveManager
|
||||
from authentication.models import AuthServicesInfo
|
||||
from srp.models import SrpFleetMain
|
||||
from srp.models import SrpUserRequest
|
||||
from srp.form import SrpFleetMainForm
|
||||
from srp.form import SrpFleetUserRequestForm
|
||||
from srp.form import SrpFleetUpdateCostForm
|
||||
from srp.form import SrpFleetMainUpdateForm
|
||||
from services.managers.srp_manager import srpManager
|
||||
from notifications import notify
|
||||
@@ -58,7 +58,7 @@ def srp_fleet_view(request, fleet_id):
|
||||
if SrpFleetMain.objects.filter(id=fleet_id).exists():
|
||||
fleet_main = SrpFleetMain.objects.get(id=fleet_id)
|
||||
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}
|
||||
|
||||
return render(request, 'registered/srpfleetdata.html', context=context)
|
||||
@@ -194,18 +194,23 @@ def srp_fleet_mark_uncompleted(request, fleet_id):
|
||||
@members_and_blues()
|
||||
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))
|
||||
completed = False
|
||||
no_srp_code = 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))
|
||||
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':
|
||||
form = SrpFleetUserRequestForm(request.POST)
|
||||
logger.debug("Request type POST contains form valid: %s" % 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)
|
||||
character = EveManager.get_character_by_id(authinfo.main_char_id)
|
||||
srp_fleet_main = SrpFleetMain.objects.get(fleet_srp_code=fleet_srp)
|
||||
@@ -219,7 +224,7 @@ def srp_request_view(request, fleet_srp):
|
||||
|
||||
try:
|
||||
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:
|
||||
logger.debug("User %s Submitted Invalid Killmail Link %s or server could not be reached" % (
|
||||
request.user, srp_request.killboard_link))
|
||||
@@ -227,117 +232,138 @@ def srp_request_view(request, fleet_srp):
|
||||
messages.error(request,
|
||||
_("Your SRP request Killmail link is invalid. Please make sure you are using zKillboard."))
|
||||
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:
|
||||
logger.debug("Returning blank 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)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('auth.srp_management')
|
||||
def srp_request_remove(request, srp_request_id):
|
||||
logger.debug("srp_request_remove called by user %s for srp request id %s" % (request.user, srp_request_id))
|
||||
def srp_request_remove(request):
|
||||
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
|
||||
|
||||
if SrpUserRequest.objects.filter(id=srp_request_id).exists():
|
||||
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))
|
||||
messages.success(request, _('Deleted SRP request from %(character)s for their %(ship)s.') % {
|
||||
"character": srpuserrequest.character, "ship": srpuserrequest.srp_ship_name})
|
||||
for srp_request_id in request.POST:
|
||||
if numrequests == 0:
|
||||
messages.warning(request, _("No SRP requests selected"))
|
||||
return redirect("auth_srp_management_view")
|
||||
if srp_request_id == "csrfmiddlewaretoken":
|
||||
continue
|
||||
if SrpUserRequest.objects.filter(id=srp_request_id).exists():
|
||||
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:
|
||||
logger.error("Unable to delete srp request id %s for user %s - request matching id not found." % (
|
||||
srp_request_id, request.user))
|
||||
messages.error(request, _('Unable to locate SRP request with ID %(requestid)s') % {"requestid": srp_request_id})
|
||||
return redirect("auth_srp_management_view")
|
||||
else:
|
||||
messages.success(request, _('Deleted %(numrequests)s SRP requests') % {"numrequests": numrequests})
|
||||
return redirect("auth_srp_fleet_view", stored_fleet_view)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('auth.srp_management')
|
||||
def srp_request_approve(request, srp_request_id):
|
||||
logger.debug("srp_request_approve called by user %s for srp request id %s" % (request.user, srp_request_id))
|
||||
def srp_request_approve(request):
|
||||
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
|
||||
|
||||
if SrpUserRequest.objects.filter(id=srp_request_id).exists():
|
||||
srpuserrequest = SrpUserRequest.objects.get(id=srp_request_id)
|
||||
stored_fleet_view = srpuserrequest.srp_fleet_main.id
|
||||
srpuserrequest.srp_status = "Approved"
|
||||
if srpuserrequest.srp_total_amount == 0:
|
||||
srpuserrequest.srp_total_amount = srpuserrequest.kb_total_loss
|
||||
srpuserrequest.save()
|
||||
logger.info("Approved SRP request id %s for character %s by user %s" % (
|
||||
srp_request_id, srpuserrequest.character, request.user))
|
||||
messages.success(request, _('Approved SRP request from %(character)s for their %(ship)s.') % {
|
||||
"character": srpuserrequest.character, "ship": srpuserrequest.srp_ship_name})
|
||||
notify(
|
||||
srpuserrequest.character.user,
|
||||
'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)
|
||||
)
|
||||
for srp_request_id in request.POST:
|
||||
if numrequests == 0:
|
||||
messages.warning(request, _("No SRP requests selected"))
|
||||
return redirect("auth_srp_management_view")
|
||||
if srp_request_id == "csrfmiddlewaretoken":
|
||||
continue
|
||||
if SrpUserRequest.objects.filter(id=srp_request_id).exists():
|
||||
srpuserrequest = SrpUserRequest.objects.get(id=srp_request_id)
|
||||
stored_fleet_view = srpuserrequest.srp_fleet_main.id
|
||||
srpuserrequest.srp_status = "Approved"
|
||||
if srpuserrequest.srp_total_amount == 0:
|
||||
srpuserrequest.srp_total_amount = srpuserrequest.kb_total_loss
|
||||
srpuserrequest.save()
|
||||
logger.info("Approved SRP request id %s for character %s by user %s" % (
|
||||
srp_request_id, srpuserrequest.character, request.user))
|
||||
notify(
|
||||
srpuserrequest.character.user,
|
||||
'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:
|
||||
logger.error("Unable to approve srp request id %s on behalf of user %s - request matching id not found." % (
|
||||
srp_request_id, request.user))
|
||||
messages.error(request, _('Unable to locate SRP request with ID %(requestid)s') % {"requestid": srp_request_id})
|
||||
return redirect("auth_srp_management_view")
|
||||
else:
|
||||
messages.success(request, _('Approved %(numrequests)s SRP requests') % {"numrequests": numrequests})
|
||||
return redirect("auth_srp_fleet_view", stored_fleet_view)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('auth.srp_management')
|
||||
def srp_request_reject(request, srp_request_id):
|
||||
logger.debug("srp_request_reject called by user %s for srp request id %s" % (request.user, srp_request_id))
|
||||
def srp_request_reject(request):
|
||||
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
|
||||
|
||||
if SrpUserRequest.objects.filter(id=srp_request_id).exists():
|
||||
srpuserrequest = SrpUserRequest.objects.get(id=srp_request_id)
|
||||
stored_fleet_view = srpuserrequest.srp_fleet_main.id
|
||||
srpuserrequest.srp_status = "Rejected"
|
||||
srpuserrequest.save()
|
||||
logger.info("SRP request id %s for character %s rejected by %s" % (
|
||||
srp_request_id, srpuserrequest.character, request.user))
|
||||
messages.success(request, _('Rejected SRP request from %(character)s for their %(ship)s.') % {
|
||||
"character": srpuserrequest.character, "ship": srpuserrequest.srp_ship_name})
|
||||
notify(
|
||||
srpuserrequest.character.user,
|
||||
'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)
|
||||
)
|
||||
|
||||
for srp_request_id in request.POST:
|
||||
if numrequests == 0:
|
||||
messages.warning(request, _("No SRP requests selected"))
|
||||
return redirect("auth_srp_management_view")
|
||||
if srp_request_id == "csrfmiddlewaretoken":
|
||||
continue
|
||||
if SrpUserRequest.objects.filter(id=srp_request_id).exists():
|
||||
srpuserrequest = SrpUserRequest.objects.get(id=srp_request_id)
|
||||
stored_fleet_view = srpuserrequest.srp_fleet_main.id
|
||||
srpuserrequest.srp_status = "Rejected"
|
||||
srpuserrequest.save()
|
||||
logger.info("SRP request id %s for character %s rejected by %s" % (
|
||||
srp_request_id, srpuserrequest.character, request.user))
|
||||
notify(
|
||||
srpuserrequest.character.user,
|
||||
'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:
|
||||
logger.error("Unable to reject SRP request id %s on behalf of user %s - request matching id not found." % (
|
||||
srp_request_id, request.user))
|
||||
messages.error(request, _('Unable to locate SRP request with ID %(requestid)s') % {"requestid": srp_request_id})
|
||||
return redirect("auth_srp_management_view")
|
||||
else:
|
||||
messages.success(request, _('Rejected %(numrequests)s SRP requests.') % {"numrequests": numrequests})
|
||||
return redirect("auth_srp_fleet_view", stored_fleet_view)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('auth.srp_management')
|
||||
def srp_request_update_amount_view(request, fleet_srp_request_id):
|
||||
logger.debug("srp_request_update_amount_view called by user %s for fleet srp request id %s" % (
|
||||
def srp_request_update_amount(request, fleet_srp_request_id):
|
||||
logger.debug("srp_request_update_amount called by user %s for fleet srp request id %s" % (
|
||||
request.user, fleet_srp_request_id))
|
||||
|
||||
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})
|
||||
return redirect("auth_srp_management_view")
|
||||
|
||||
if request.method == 'POST':
|
||||
form = SrpFleetUpdateCostForm(request.POST)
|
||||
logger.debug("Request type POST contains form valid: %s" % form.is_valid())
|
||||
if form.is_valid():
|
||||
srp_request = SrpUserRequest.objects.get(id=fleet_srp_request_id)
|
||||
srp_request.srp_total_amount = form.cleaned_data['srp_total_amount']
|
||||
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)
|
||||
srp_request = SrpUserRequest.objects.get(id=fleet_srp_request_id)
|
||||
srp_request.srp_total_amount = request.POST['value']
|
||||
srp_request.save()
|
||||
logger.info("Updated srp request id %s total to %s by user %s" % (
|
||||
fleet_srp_request_id, request.POST['value'], request.user))
|
||||
return JsonResponse({"success":True,"pk":fleet_srp_request_id,"newValue":request.POST['value']})
|
||||
|
||||
|
||||
@login_required
|
||||
|
||||
58
stock/static/css/checkbox.css
Normal file
58
stock/static/css/checkbox.css
Normal 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;
|
||||
}
|
||||
4
stock/templates/bundles/x-editable-js.html
Normal file
4
stock/templates/bundles/x-editable-js.html
Normal 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 -->
|
||||
4
stock/templates/bundles/x-editable.css.html
Normal file
4
stock/templates/bundles/x-editable.css.html
Normal 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 -->
|
||||
44
stock/templates/registered/fleetoptable.html
Normal file
44
stock/templates/registered/fleetoptable.html
Normal 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 %}
|
||||
@@ -5,29 +5,33 @@
|
||||
{% block title %}Apply To {{ corp.corporation_name }}{% endblock title %}
|
||||
{% block page_title %}{% trans "Apply To" %} {{ corp.corporation_name }}{% endblock page_title %}
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header text-center">{% trans "Apply To" %} {{ corp.corporation_name }}</h1>
|
||||
<div class="container-fluid">
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
<div class="row">
|
||||
<form class="form-signin">
|
||||
{% csrf_token %}
|
||||
{% for question in questions %}
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="id_{{ question.pk }}">{{ question.title }}</label>
|
||||
<div class=" ">
|
||||
{% if question.help_text %}
|
||||
<div cass="text-center">{{ question.help_text }}</div>
|
||||
{% endif %}
|
||||
<textarea class="form-control" cols="40" id="id_{{ question.pk }}" name="{{ question.pk }}" rows="10"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit" formmethod="post">{% trans "Submit" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header text-center">{% trans "Apply To" %} {{ corp.corporation_name }}</h1>
|
||||
<div class="container-fluid">
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
<div class="row">
|
||||
<form class="form-signin">
|
||||
{% csrf_token %}
|
||||
{% for question in questions %}
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="id_{{ question.pk }}">{{ question.title }}</label>
|
||||
<div class=" ">
|
||||
{% if question.help_text %}
|
||||
<div cass="text-center">{{ question.help_text }}</div>
|
||||
{% endif %}
|
||||
{% for choice in question.choices.all %}
|
||||
<input type="radio" name="{{ question.pk }}" id="id_{{ question.pk }}" value="{{ choice.choice_text }}" />
|
||||
<label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br />
|
||||
{% empty %}
|
||||
<textarea class="form-control" cols="30" id="id_{{ question.pk }}" name="{{ question.pk }}" rows="4"></textarea>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit" formmethod="post">{% trans "Submit" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -17,62 +17,26 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</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 %}
|
||||
<tr>
|
||||
<td style="width:150px" class="text-center">{{ ops.operation_name }}</td>
|
||||
<td style="width:150px" class="text-center">{{ ops.doctrine }}</td>
|
||||
<td class="text-center">
|
||||
<a href="http://evemaps.dotlan.net/system/{{ ops.system }}">{{ ops.system }}</a>
|
||||
</td>
|
||||
<td style="width:150px" class="text-center">{{ ops.location }}</td>
|
||||
<td style="width:150px" 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 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>
|
||||
<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>
|
||||
|
||||
<h4><b>{% trans "Next Timers" %}</b></h4>
|
||||
{% if future_timers %}
|
||||
{% include "registered/fleetoptable.html" with timers=future_timers %}
|
||||
{% 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 %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
{% block content %}
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header text-center">{% trans "Available Services" %}</h1>
|
||||
<table class="table table-bordered">
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th class="text-center">{% trans "Service" %}</th>
|
||||
<th class="text-center">{% trans "Username" %}</th>
|
||||
|
||||
@@ -7,23 +7,23 @@
|
||||
<td class="text-center">
|
||||
{% ifequal username "" %}
|
||||
{% 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>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% 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>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% 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>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% 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>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -7,7 +7,42 @@
|
||||
{% block title %}Alliance Auth{% endblock %}
|
||||
|
||||
{% 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 %}
|
||||
<div class="col-lg-12">
|
||||
@@ -27,92 +62,147 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</h1>
|
||||
<div class="alert alert-info" role="alert">
|
||||
<div class="text-right">
|
||||
<b>{% trans "Total Losses:" %} {{ srpfleetrequests.count }}</b>
|
||||
|
||||
<b>{% trans "Total ISK Cost:" %} {{ totalcost | intcomma }}</b>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if srpfleetrequests %}
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th class="text-center">{% trans "Pilot Name" %}</th>
|
||||
<th class="text-center">{% trans "Killboard Link" %}</th>
|
||||
<th class="text-center">{% trans "Additional Info" %}</th>
|
||||
<th class="text-center">{% trans "Ship Type" %}</th>
|
||||
<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>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<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 %}
|
||||
|
||||
<td class="text-center">
|
||||
<a href="{% url 'auth_srp_request_update_amount_view' srpfleetrequest.id %}" class="btn btn-info" title="Update Value">
|
||||
<span class="glyphicon glyphicon-usd"></span>
|
||||
</a>
|
||||
{% if srpfleetrequest.srp_status in "RejectedPending" %}
|
||||
<a href="{% url 'auth_srp_request_approve' srpfleetrequest.id %}" class="btn btn-success" title="Approve">
|
||||
<span class="glyphicon glyphicon-ok"></span>
|
||||
</a>
|
||||
{% elif srpfleetrequest.srp_status == "" %}
|
||||
<a href="{% url 'auth_srp_request_approve' srpfleetrequest.id %}" class="btn btn-success" title="Approve">
|
||||
<span class="glyphicon glyphicon-ok"></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if srpfleetrequest.srp_status in "ApprovedPending" %}
|
||||
<a href="{% url 'auth_srp_request_reject' srpfleetrequest.id %}" class="btn btn-warning" title="Reject">
|
||||
<span class="glyphicon glyphicon-remove"></span>
|
||||
</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>
|
||||
<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>
|
||||
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th class="text-center">{% trans "Pilot Name" %}</th>
|
||||
<th class="text-center">{% trans "Killboard Link" %}</th>
|
||||
<th class="text-center">{% trans "Additional Info" %}</th>
|
||||
<th class="text-center">{% trans "Ship Type" %}</th>
|
||||
<th class="text-center">{% trans "Killboard Loss Amt" %}</th>
|
||||
<th class="text-center">{% trans "SRP ISK Cost" %}
|
||||
{% blocktrans %}<i class="glyphicon glyphicon-question-sign" rel="tooltip" title="Click value to edit
|
||||
Enter to save&next
|
||||
ESC to cancel"
|
||||
id="blah"></i></th>{% endblocktrans %}
|
||||
<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>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% 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">{{ 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 %}
|
||||
<div class="alert alert-warning text-center">{% trans "No SRP requests for this fleet." %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% 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 %}
|
||||
|
||||
@@ -15,24 +15,13 @@
|
||||
<div class="container-fluid">
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
<div class="row">
|
||||
{% if no_srp_code %}
|
||||
<div class="alert alert-danger" role="alert">{% trans "SRP Code Does Not Exist" %}</div>
|
||||
{% else %}
|
||||
{% if completed == False %}
|
||||
<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 "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 %}
|
||||
<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 "Create SRP Request" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,31 +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 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 %}
|
||||
@@ -41,7 +41,7 @@
|
||||
<th class="text-center">{% trans "Fleet ISK Cost" %}</th>
|
||||
<th class="text-center">{% trans "SRP Status" %}</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>
|
||||
{% for srpfleet in srpfleets %}
|
||||
<tr>
|
||||
@@ -98,7 +98,7 @@
|
||||
<span class="glyphicon glyphicon-pencil"></span>
|
||||
</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>
|
||||
</a>
|
||||
{% if srpfleet.fleet_srp_code %}
|
||||
|
||||
@@ -7,3 +7,4 @@ nose>=1.3.7
|
||||
django-nose>=1.4.4
|
||||
coverage>=4.3.1
|
||||
coveralls>=1.1
|
||||
requests-mock>=1.2.0
|
||||
|
||||
10
thirdparty/Supervisor/auth-celerybeat.conf
vendored
10
thirdparty/Supervisor/auth-celerybeat.conf
vendored
@@ -1,10 +0,0 @@
|
||||
[program:auth-celerybeat]
|
||||
command=python manage.py celerybeat
|
||||
directory=/home/allianceserver/allianceauth
|
||||
user=allianceserver
|
||||
stdout_logfile=/home/allianceserver/allianceauth/log/beat.log
|
||||
stderr_logfile=/home/allianceserver/allianceauth/log/beat.log
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startsecs=10
|
||||
priority=999
|
||||
13
thirdparty/Supervisor/auth-celeryd.conf
vendored
13
thirdparty/Supervisor/auth-celeryd.conf
vendored
@@ -1,13 +0,0 @@
|
||||
[program:auth-celeryd]
|
||||
command=python manage.py celeryd
|
||||
directory=/home/allianceserver/allianceauth
|
||||
user=allianceserver
|
||||
numprocs=1
|
||||
stdout_logfile=/home/allianceserver/allianceauth/log/worker.log
|
||||
stderr_logfile=/home/allianceserver/allianceauth/log/worker.log
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startsecs=10
|
||||
stopwaitsecs = 600
|
||||
killasgroup=true
|
||||
priority=1000
|
||||
28
thirdparty/Supervisor/auth.conf
vendored
Normal file
28
thirdparty/Supervisor/auth.conf
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
[program:celerybeat]
|
||||
command=celery -A alliance_auth beat
|
||||
directory=/home/allianceserver/allianceauth
|
||||
user=allianceserver
|
||||
stdout_logfile=/home/allianceserver/allianceauth/log/beat.log
|
||||
stderr_logfile=/home/allianceserver/allianceauth/log/beat.log
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startsecs=10
|
||||
priority=998
|
||||
|
||||
[program:celeryd]
|
||||
command=celery -A alliance_auth worker
|
||||
directory=/home/allianceserver/allianceauth
|
||||
user=allianceserver
|
||||
numprocs=1
|
||||
stdout_logfile=/home/allianceserver/allianceauth/log/worker.log
|
||||
stderr_logfile=/home/allianceserver/allianceauth/log/worker.log
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startsecs=10
|
||||
stopwaitsecs = 600
|
||||
killasgroup=true
|
||||
priority=998
|
||||
|
||||
[group:auth]
|
||||
programs=celerybeat,celeryd
|
||||
priority=999
|
||||
@@ -40,14 +40,8 @@ def timer_view(request):
|
||||
else:
|
||||
corp_timers = []
|
||||
timer_list = Timer.objects.filter(corp_timer=False)
|
||||
closest_timer = None
|
||||
if timer_list:
|
||||
closest_timer = \
|
||||
sorted(list(Timer.objects.all().filter(corp_timer=False)), key=lambda d: (timezone.now()))[0]
|
||||
logger.debug("Determined closest timer is %s" % closest_timer)
|
||||
render_items = {'timers': Timer.objects.all().filter(corp_timer=False),
|
||||
'corp_timers': corp_timers,
|
||||
'closest_timer': closest_timer,
|
||||
'future_timers': Timer.objects.all().filter(corp_timer=False).filter(
|
||||
eve_time__gte=datetime.datetime.now()),
|
||||
'past_timers': Timer.objects.all().filter(corp_timer=False).filter(
|
||||
|
||||
Reference in New Issue
Block a user