From bb3e7a04498b7582df94fc588df925a253fae2fc Mon Sep 17 00:00:00 2001 From: Adarnof Date: Mon, 10 Apr 2017 14:27:17 -0400 Subject: [PATCH 01/14] Tolerate validating submitted email if 2+ users Addresses #783 --- authentication/forms.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/authentication/forms.py b/authentication/forms.py index 7374ac03..5603dc4d 100644 --- a/authentication/forms.py +++ b/authentication/forms.py @@ -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') From d8043ff73532194e536af92f273e2ebd85ae3781 Mon Sep 17 00:00:00 2001 From: Basraah Date: Tue, 11 Apr 2017 11:53:12 +1000 Subject: [PATCH 02/14] Add Gunicorn docs (#777) * Added gunicorn docs * Changes suggested by @Betriebsrat --- docs/installation/auth/gunicorn.md | 121 +++++++++++++++++++++++++++++ docs/installation/auth/index.md | 1 + 2 files changed, 122 insertions(+) create mode 100644 docs/installation/auth/gunicorn.md diff --git a/docs/installation/auth/gunicorn.md b/docs/installation/auth/gunicorn.md new file mode 100644 index 00000000..60a6cdae --- /dev/null +++ b/docs/installation/auth/gunicorn.md @@ -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 + + 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 + +``` + +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. diff --git a/docs/installation/auth/index.md b/docs/installation/auth/index.md index 7657fd2f..f0a51456 100644 --- a/docs/installation/auth/index.md +++ b/docs/installation/auth/index.md @@ -8,6 +8,7 @@ centos settings apache + gunicorn cloudflare supervisor quickstart From 901dd5033adb6d3051b5a89a17ca95c758ed9cb8 Mon Sep 17 00:00:00 2001 From: Basraah Date: Thu, 27 Apr 2017 10:28:06 +1000 Subject: [PATCH 03/14] Added a cut down apache config --- docs/installation/auth/apache.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/installation/auth/apache.md b/docs/installation/auth/apache.md index da87f6aa..56aa5a55 100644 --- a/docs/installation/auth/apache.md +++ b/docs/installation/auth/apache.md @@ -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 + +``` + + 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/ + + + Require all granted + + + + Require all granted + + +``` + ### Own SSL Cert - Apache 2.4 or newer: - [000-default.conf](http://pastebin.com/3LLzyNmV) From 5a93128f4f0a60c043e0824feb8c0dbf198a26a7 Mon Sep 17 00:00:00 2001 From: Adarnof Date: Thu, 27 Apr 2017 22:29:13 -0400 Subject: [PATCH 04/14] Prevent FAT CorpStats creation for missing corp models. --- fleetactivitytracking/views.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/fleetactivitytracking/views.py b/fleetactivitytracking/views.py index f31f7cf7..4d53ab0e 100644 --- a/fleetactivitytracking/views.py +++ b/fleetactivitytracking/views.py @@ -9,6 +9,7 @@ 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 @@ -100,19 +101,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) From 372e582c6e66b5c14c05be356d2f52c953d3f722 Mon Sep 17 00:00:00 2001 From: Basraah Date: Thu, 4 May 2017 06:50:27 +1000 Subject: [PATCH 05/14] Nginx docs (#794) --- docs/installation/auth/index.md | 1 + docs/installation/auth/nginx.md | 93 +++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 docs/installation/auth/nginx.md diff --git a/docs/installation/auth/index.md b/docs/installation/auth/index.md index f0a51456..0e314277 100644 --- a/docs/installation/auth/index.md +++ b/docs/installation/auth/index.md @@ -7,6 +7,7 @@ ubuntu centos settings + nginx apache gunicorn cloudflare diff --git a/docs/installation/auth/nginx.md b/docs/installation/auth/nginx.md new file mode 100644 index 00000000..3dfb646c --- /dev/null +++ b/docs/installation/auth/nginx.md @@ -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/). From 17dd7c04c7516e25260c19b20df13869675a6e12 Mon Sep 17 00:00:00 2001 From: Basraah Date: Thu, 4 May 2017 06:53:16 +1000 Subject: [PATCH 06/14] Replace django-celery with base Celery (#791) Update celery tasks to new style & remove djcelery Vanilla celery + django-celery-beat take over the role of djcelery. Task schedules are consolidated into settings instead of residing in code. Update docs and example supervisor configs. --- alliance_auth/settings.py.example | 28 ++++++++++++++++------ alliance_auth/tests/test_settings.py | 6 +---- corputils/tasks.py | 7 +++--- docs/installation/auth/quickstart.md | 4 ++-- docs/installation/auth/supervisor.md | 8 +++---- docs/installation/services/teamspeak3.md | 2 +- docs/maintenance/troubleshooting.md | 10 +++----- eveonline/tasks.py | 20 ++++++++-------- eveonline/views.py | 2 +- requirements.txt | 6 ++--- run_alliance_corp_update.py | 2 +- thirdparty/Supervisor/auth-celerybeat.conf | 2 +- thirdparty/Supervisor/auth-celeryd.conf | 2 +- 13 files changed, 51 insertions(+), 48 deletions(-) diff --git a/alliance_auth/settings.py.example b/alliance_auth/settings.py.example index 3454c46b..60eff347 100644 --- a/alliance_auth/settings.py.example +++ b/alliance_auth/settings.py.example @@ -12,17 +12,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 +45,7 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.humanize', - 'djcelery', + 'django_celery_beat', 'bootstrapform', 'authentication', 'services', @@ -237,6 +232,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 ################# diff --git a/alliance_auth/tests/test_settings.py b/alliance_auth/tests/test_settings.py index d4720472..5c3c6b3a 100644 --- a/alliance_auth/tests/test_settings.py +++ b/alliance_auth/tests/test_settings.py @@ -4,14 +4,10 @@ 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' @@ -40,7 +36,7 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.humanize', - 'djcelery', + 'django_celery_beat', 'bootstrapform', 'authentication', 'services', diff --git a/corputils/tasks.py b/corputils/tasks.py index 2dbb7197..8aa63ef1 100644 --- a/corputils/tasks.py +++ b/corputils/tasks.py @@ -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) diff --git a/docs/installation/auth/quickstart.md b/docs/installation/auth/quickstart.md index 2d20cce6..ca409485 100644 --- a/docs/installation/auth/quickstart.md +++ b/docs/installation/auth/quickstart.md @@ -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' diff --git a/docs/installation/auth/supervisor.md b/docs/installation/auth/supervisor.md index 8d89d14e..6e71ec8a 100644 --- a/docs/installation/auth/supervisor.md +++ b/docs/installation/auth/supervisor.md @@ -20,7 +20,7 @@ CentOS: ## 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: @@ -41,15 +41,15 @@ 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` ## 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. diff --git a/docs/installation/services/teamspeak3.md b/docs/installation/services/teamspeak3.md index 8ff38838..b98928fa 100644 --- a/docs/installation/services/teamspeak3.md +++ b/docs/installation/services/teamspeak3.md @@ -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: diff --git a/docs/maintenance/troubleshooting.md b/docs/maintenance/troubleshooting.md index 6cd94466..1330932a 100644 --- a/docs/maintenance/troubleshooting.md +++ b/docs/maintenance/troubleshooting.md @@ -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) diff --git a/eveonline/tasks.py b/eveonline/tasks.py index d15ab692..bce37a33 100644 --- a/eveonline/tasks.py +++ b/eveonline/tasks.py @@ -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,12 @@ def refresh_api(api): level="danger") -@task +@app.task def refresh_user_apis(user): 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,7 +91,7 @@ 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") @@ -101,18 +101,18 @@ def run_api_refresh(): refresh_user_apis.delay(u) -@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 +123,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: diff --git a/eveonline/views.py b/eveonline/views.py index 6a0610a9..b4f46e4d 100755 --- a/eveonline/views.py +++ b/eveonline/views.py @@ -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: diff --git a/requirements.txt b/requirements.txt index 14fbe899..a5028544 100755 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,7 @@ slugify requests-oauthlib sleekxmpp redis +celery>=4.0.2 # Django Stuff # django>=1.10,<2.0 @@ -17,10 +18,7 @@ django-navhelper django-bootstrap-pagination django-redis>=4.4 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 diff --git a/run_alliance_corp_update.py b/run_alliance_corp_update.py index 938961d9..db3d5967 100644 --- a/run_alliance_corp_update.py +++ b/run_alliance_corp_update.py @@ -1,5 +1,5 @@ from __future__ import unicode_literals from eveonline.tasks import run_corp_update -run_corp_update() +run_corp_update.apply() quit() diff --git a/thirdparty/Supervisor/auth-celerybeat.conf b/thirdparty/Supervisor/auth-celerybeat.conf index 7832f22b..527fd7e2 100644 --- a/thirdparty/Supervisor/auth-celerybeat.conf +++ b/thirdparty/Supervisor/auth-celerybeat.conf @@ -1,5 +1,5 @@ [program:auth-celerybeat] -command=python manage.py celerybeat +command=celery -A alliance_auth beat directory=/home/allianceserver/allianceauth user=allianceserver stdout_logfile=/home/allianceserver/allianceauth/log/beat.log diff --git a/thirdparty/Supervisor/auth-celeryd.conf b/thirdparty/Supervisor/auth-celeryd.conf index e00f37ec..033cdc88 100644 --- a/thirdparty/Supervisor/auth-celeryd.conf +++ b/thirdparty/Supervisor/auth-celeryd.conf @@ -1,5 +1,5 @@ [program:auth-celeryd] -command=python manage.py celeryd +command=celery -A alliance_auth worker directory=/home/allianceserver/allianceauth user=allianceserver numprocs=1 From aad3bd6f573f4ec016032a43d6930fba71e74b6b Mon Sep 17 00:00:00 2001 From: mmolitor87 Date: Wed, 3 May 2017 15:53:44 -0500 Subject: [PATCH 07/14] sets language value to default for phpbb (#771) Without this being set users get "The language you specified is not valid." when trying to edit global settings such as timezone or style. --- services/modules/phpbb3/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/modules/phpbb3/manager.py b/services/modules/phpbb3/manager.py index f3d60caf..396b27e1 100755 --- a/services/modules/phpbb3/manager.py +++ b/services/modules/phpbb3/manager.py @@ -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" From 4556a0e740cc3b01afee52fbd67062c1c8b5f761 Mon Sep 17 00:00:00 2001 From: iAddz Date: Wed, 3 May 2017 21:55:10 +0100 Subject: [PATCH 08/14] SRP QOL + Validation (#786) - new UI for srp management (mass performs, inline editing) - unique validation for srp killboard links - character auth ownership verification for killboard links - removed remnants of old error messaging system & replaced with current standard - added a confirmation popup when deleting fleet SRP's --- alliance_auth/urls.py | 11 +- services/managers/srp_manager.py | 3 +- srp/form.py | 4 - srp/views.py | 190 +++++++------ stock/static/css/checkbox.css | 58 ++++ stock/templates/bundles/x-editable-js.html | 4 + stock/templates/bundles/x-editable.css.html | 4 + stock/templates/registered/srpfleetdata.html | 250 ++++++++++++------ .../templates/registered/srpfleetrequest.html | 25 +- .../registered/srpfleetrequestamount.html | 31 --- stock/templates/registered/srpmanagement.html | 4 +- 11 files changed, 355 insertions(+), 229 deletions(-) create mode 100644 stock/static/css/checkbox.css create mode 100644 stock/templates/bundles/x-editable-js.html create mode 100644 stock/templates/bundles/x-editable.css.html delete mode 100755 stock/templates/registered/srpfleetrequestamount.html diff --git a/alliance_auth/urls.py b/alliance_auth/urls.py index 5ccbc7dc..ec817f45 100755 --- a/alliance_auth/urls.py +++ b/alliance_auth/urls.py @@ -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'), @@ -179,8 +182,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, diff --git a/services/managers/srp_manager.py b/services/managers/srp_manager.py index a71c3ca2..94bca0db 100644 --- a/services/managers/srp_manager.py +++ b/services/managers/srp_manager.py @@ -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") diff --git a/srp/form.py b/srp/form.py index 2d0d6504..036d6438 100755 --- a/srp/form.py +++ b/srp/form.py @@ -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")) diff --git a/srp/views.py b/srp/views.py index 775a7f96..1c8cb3b5 100755 --- a/srp/views.py +++ b/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 diff --git a/stock/static/css/checkbox.css b/stock/static/css/checkbox.css new file mode 100644 index 00000000..6c2eb2f1 --- /dev/null +++ b/stock/static/css/checkbox.css @@ -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; +} \ No newline at end of file diff --git a/stock/templates/bundles/x-editable-js.html b/stock/templates/bundles/x-editable-js.html new file mode 100644 index 00000000..ab132384 --- /dev/null +++ b/stock/templates/bundles/x-editable-js.html @@ -0,0 +1,4 @@ +{% load static %} + + + diff --git a/stock/templates/bundles/x-editable.css.html b/stock/templates/bundles/x-editable.css.html new file mode 100644 index 00000000..535d4b40 --- /dev/null +++ b/stock/templates/bundles/x-editable.css.html @@ -0,0 +1,4 @@ +{% load staticfiles %} + + + diff --git a/stock/templates/registered/srpfleetdata.html b/stock/templates/registered/srpfleetdata.html index ce526b04..1bbbe862 100755 --- a/stock/templates/registered/srpfleetdata.html +++ b/stock/templates/registered/srpfleetdata.html @@ -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' %} + + +{% endblock extra_css %} {% block content %}
@@ -27,92 +62,147 @@ {% endif %}
- + {% if srpfleetrequests %} - - - - - - - - - - - {% if perms.auth.srp_management %} - - {% endif %} - - {% for srpfleetrequest in srpfleetrequests %} - - - - - - - - - + + {% csrf_token %} + + + + + {% endif %} + + + +
{% trans "Pilot Name" %}{% trans "Killboard Link" %}{% trans "Additional Info" %}{% trans "Ship Type" %}{% trans "Killboard Loss Amt" %}{% trans "SRP ISK Cost" %}{% trans "Post Time" %}{% trans "Status" %}{% trans "Actions" %}
{{ srpfleetrequest.character.character_name }} - Link - {{ srpfleetrequest.additional_info }}{{ srpfleetrequest.srp_ship_name }}ISK: {{ srpfleetrequest.kb_total_loss | intcomma }}ISK: {{ srpfleetrequest.srp_total_amount | intcomma }}{{ srpfleetrequest.post_time | date:"Y-m-d H:i" }} - {% if srpfleetrequest.srp_status == "Approved" %} -
- {% trans "Approved" %} -
- {% elif srpfleetrequest.srp_status == "Rejected" %} -
- {% trans "Rejected" %} -
- {% else %} -
- {% trans "Pending" %} -
- {% endif %} -
- - - - {% if srpfleetrequest.srp_status in "RejectedPending" %} - - - - {% elif srpfleetrequest.srp_status == "" %} - - - - {% endif %} - {% if srpfleetrequest.srp_status in "ApprovedPending" %} - - - - {% elif srpfleetrequest.srp_status == "" %} - - - - {% endif %} - - - - -
+ + + + + + + {% endblocktrans %} + + + {% if perms.auth.srp_management %} + {% endif %} - {% endfor %} -
{% trans "Pilot Name" %}{% trans "Killboard Link" %}{% trans "Additional Info" %}{% trans "Ship Type" %}{% trans "Killboard Loss Amt" %}{% trans "SRP ISK Cost" %} + {% blocktrans %}{% trans "Post Time" %}{% trans "Status" %}{% trans "Actions" %}
+ {% for srpfleetrequest in srpfleetrequests %} + + {{ srpfleetrequest.character.character_name }} + + Link + + {{ srpfleetrequest.additional_info }} + {{ srpfleetrequest.srp_ship_name }} + {{ srpfleetrequest.kb_total_loss | intcomma }} ISK + {{ srpfleetrequest.srp_total_amount | intcomma }} ISK + {{ srpfleetrequest.post_time | date:"Y-m-d H:i" }} + + {% if srpfleetrequest.srp_status == "Approved" %} +
+ {% trans "Approved" %} +
+ {% elif srpfleetrequest.srp_status == "Rejected" %} +
+ {% trans "Rejected" %} +
+ {% else %} +
+ {% trans "Pending" %} +
+ {% endif %} + + {% if perms.auth.srp_management %} + +
+ +
+ + {% endif %} + + {% endfor %} + + + {% else %}
{% trans "No SRP requests for this fleet." %}
{% endif %} - {% 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 %} diff --git a/stock/templates/registered/srpfleetrequest.html b/stock/templates/registered/srpfleetrequest.html index c2beff83..48a15ede 100755 --- a/stock/templates/registered/srpfleetrequest.html +++ b/stock/templates/registered/srpfleetrequest.html @@ -15,24 +15,13 @@
- {% if no_srp_code %} - - {% else %} - {% if completed == False %} - - {% else %} - - - {% endif %} - {% endif %} +
diff --git a/stock/templates/registered/srpfleetrequestamount.html b/stock/templates/registered/srpfleetrequestamount.html deleted file mode 100755 index cc1f4fe6..00000000 --- a/stock/templates/registered/srpfleetrequestamount.html +++ /dev/null @@ -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 %} - -
-

{% trans "Update SRP Amount" %}

- -
-
-
- -
-
-
- -
- -{% endblock content %} diff --git a/stock/templates/registered/srpmanagement.html b/stock/templates/registered/srpmanagement.html index 5bd9b408..c56ca713 100755 --- a/stock/templates/registered/srpmanagement.html +++ b/stock/templates/registered/srpmanagement.html @@ -41,7 +41,7 @@ {% trans "Fleet ISK Cost" %} {% trans "SRP Status" %} {% trans "Pending Requests" %} - {% trans "Actions" %} + {% trans "Actions" %} {% for srpfleet in srpfleets %} @@ -98,7 +98,7 @@ - + {% if srpfleet.fleet_srp_code %} From aec013b93cc52b5d6b19779c85c8ffcb9fe7b9e8 Mon Sep 17 00:00:00 2001 From: Basraah Date: Fri, 5 May 2017 00:31:57 +1000 Subject: [PATCH 09/14] SeAT service cleanup (#796) Manager function tidyup Hopefully improved the key sync function, at least it should be easier to follow whats happening now. Remove partial logging of unhashed passwords Added user feedback --- services/modules/seat/manager.py | 200 +++++++++++++++++-------------- services/modules/seat/views.py | 24 ++++ 2 files changed, 133 insertions(+), 91 deletions(-) diff --git a/services/modules/seat/manager.py b/services/modules/seat/manager.py index 453ff546..f75acd65 100644 --- a/services/modules/seat/manager.py +++ b/services/modules/seat/manager.py @@ -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,22 +45,24 @@ 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) + 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 @classmethod @@ -93,7 +97,7 @@ class SeatManager: @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) @@ -104,13 +108,12 @@ class SeatManager: @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("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 = SeatManager.exec_request('user/{}'.format(username), 'put', password=password) + 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 +121,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 +148,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() diff --git a/services/modules/seat/views.py b/services/modules/seat/views.py index 6a1e6a51..d110d95b 100644 --- a/services/modules/seat/views.py +++ b/services/modules/seat/views.py @@ -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.") From 1b0c3c3bfca05adebfea6f63d83a13a08e11de9f Mon Sep 17 00:00:00 2001 From: iAddz Date: Thu, 4 May 2017 15:39:03 +0100 Subject: [PATCH 10/14] Corp member view for fat stats (#785) Additionally fixes 500's from showing when - a corporation object has been deleted - an account doesn't have a main char set Add Math tooltips --- alliance_auth/urls.py | 3 + .../fatlinkstatisticscorpview.html | 48 ++++++++++++++ .../fatlinkstatisticsview.html | 10 ++- fleetactivitytracking/views.py | 62 +++++++++++++++++-- 4 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html diff --git a/alliance_auth/urls.py b/alliance_auth/urls.py index ec817f45..0e4c792d 100755 --- a/alliance_auth/urls.py +++ b/alliance_auth/urls.py @@ -194,6 +194,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\w+)/(?P[0-9]+)/(?P[0-9]+)/', fleetactivitytracking.views.fatlink_statistics_corp_view, + name='auth_fatlink_view_statistics_corp_month'), url(r'^fat/statistics/(?P[0-9]+)/(?P[0-9]+)/$', fleetactivitytracking.views.fatlink_statistics_view, name='auth_fatlink_view_statistics_month'), url(r'^fat/user/statistics/$', fleetactivitytracking.views.fatlink_personal_statistics_view, diff --git a/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html b/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html new file mode 100644 index 00000000..2c086155 --- /dev/null +++ b/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html @@ -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 %} +
+

{% blocktrans %}Participation data statistics for {{ month }}, {{ year }}{% endblocktrans %} +
+ {% trans "Previous month" %} + {% if next_month %} + {% trans "Next month" %} + {% endif %} +
+

+ {% if fatStats %} + + + + + + + + + {% for memberStat in fatStats %} + + + + + + + + {% endfor %} +
{% trans "Main Character" %}{% trans "Characters" %}{% trans "Fats" %}{% trans "Average fats" %} + +
+ + {{ memberStat.mainchar.character_name }}{{ memberStat.n_chars }}{{ memberStat.n_fats }}{{ memberStat.avg_fat }}
+ {% endif %} +
+{% endblock content %} +{% block extra_script %} +$(document).ready(function(){ + $("[rel=tooltip]").tooltip(); +{% endblock extra_script %} diff --git a/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html b/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html index 436f1be0..6327bee8 100644 --- a/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html +++ b/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html @@ -24,14 +24,16 @@ {% trans "Corp" %} {% trans "Members" %} {% trans "Fats" %} - {% trans "Average fats" %} + {% trans "Average fats" %} + + {% for corpStat in fatStats %} - [{{ corpStat.corp.corporation_ticker }}] + [{{ corpStat.corp.corporation_ticker }}] {{ corpStat.corp.corporation_name }} {{ corpStat.corp.member_count }} {{ corpStat.n_fats }} @@ -42,3 +44,7 @@ {% endif %} {% endblock content %} +{% block extra_script %} +$(document).ready(function(){ + $("[rel=tooltip]").tooltip(); +{% endblock extra_script %} diff --git a/fleetactivitytracking/views.py b/fleetactivitytracking/views.py index 4d53ab0e..ed2ef119 100644 --- a/fleetactivitytracking/views.py +++ b/fleetactivitytracking/views.py @@ -13,6 +13,7 @@ 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 @@ -56,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) @@ -89,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): @@ -119,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) From dc10245158b9f740b56d4df12b8371a9e3a8d15d Mon Sep 17 00:00:00 2001 From: Adarnof Date: Thu, 4 May 2017 22:31:56 -0400 Subject: [PATCH 11/14] Do not suspend account on disabling user. --- services/modules/discourse/manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/services/modules/discourse/manager.py b/services/modules/discourse/manager.py index 9d1413b6..8ce5321e 100644 --- a/services/modules/discourse/manager.py +++ b/services/modules/discourse/manager.py @@ -376,6 +376,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 From 12cfc552da0eedde02e12fc600889f39c777d4e9 Mon Sep 17 00:00:00 2001 From: Adarnof Date: Thu, 4 May 2017 22:41:27 -0400 Subject: [PATCH 12/14] Correct Corporation creation using XML provider Closes #734 --- eveonline/providers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eveonline/providers.py b/eveonline/providers.py index 391bb41c..90cc491d 100644 --- a/eveonline/providers.py +++ b/eveonline/providers.py @@ -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 From d2e32d3da3849e431d291248a785f84e9bdcb64b Mon Sep 17 00:00:00 2001 From: iAddz Date: Thu, 11 May 2017 17:32:45 +0100 Subject: [PATCH 13/14] Fleet op timer ui update (#787) --- optimer/form.py | 2 - optimer/migrations/0002_auto_20170413_0442.py | 23 ++++++ optimer/models.py | 2 - optimer/views.py | 12 ++-- stock/templates/registered/fleetoptable.html | 44 ++++++++++++ .../registered/operationmanagement.html | 72 +++++-------------- timerboard/views.py | 6 -- 7 files changed, 90 insertions(+), 71 deletions(-) create mode 100644 optimer/migrations/0002_auto_20170413_0442.py create mode 100644 stock/templates/registered/fleetoptable.html diff --git a/optimer/form.py b/optimer/form.py index 6e476677..1edc995f 100644 --- a/optimer/form.py +++ b/optimer/form.py @@ -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")) diff --git a/optimer/migrations/0002_auto_20170413_0442.py b/optimer/migrations/0002_auto_20170413_0442.py new file mode 100644 index 00000000..1161bbe1 --- /dev/null +++ b/optimer/migrations/0002_auto_20170413_0442.py @@ -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', + ), + ] diff --git a/optimer/models.py b/optimer/models.py index 32670bf0..354d8dbe 100644 --- a/optimer/models.py +++ b/optimer/models.py @@ -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) diff --git a/optimer/views.py b/optimer/views.py index e10c35e7..6cf42652 100644 --- a/optimer/views.py +++ b/optimer/views.py @@ -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}) diff --git a/stock/templates/registered/fleetoptable.html b/stock/templates/registered/fleetoptable.html new file mode 100644 index 00000000..954d2574 --- /dev/null +++ b/stock/templates/registered/fleetoptable.html @@ -0,0 +1,44 @@ +{% load i18n %} + +{% block content %} + + + + + + + + + + + {% if perms.auth.optimer_management %} + + + {% endif %} + + + {% for ops in timers %} + + + + + + + + + + {% if perms.auth.optimer_management %} + + + {% endif %} + + + {% endfor %} +
{% trans "Operation Name" %}{% trans "Doctrine" %}{% trans "Form Up System" %}{% trans "Start Time" %}{% trans "Local Time" %}{% trans "Duration" %}{% trans "FC" %}{% trans "Creator" %}{% trans "Action" %}
{{ ops.operation_name }}{{ ops.doctrine }} + {{ ops.system }} + {{ ops.start | date:"Y-m-d H:i" }}
{{ ops.duration }}{{ ops.fc }}{{ ops.eve_character }} + + + +
+{% endblock content %} diff --git a/stock/templates/registered/operationmanagement.html b/stock/templates/registered/operationmanagement.html index db4fa937..ae34b321 100644 --- a/stock/templates/registered/operationmanagement.html +++ b/stock/templates/registered/operationmanagement.html @@ -17,62 +17,26 @@ {% endif %} -
-
- {% trans "Current Eve Time:" %} -
-
-
- {% if optimer %} - - - - - - - - - - - - - {% if perms.auth.optimer_management %} - - - {% endif %} - - {% for ops in optimer %} - - - - - - - - - - - - {% if perms.auth.optimer_management %} - - - {% endif %} - - {% endfor %} - -
{% trans "Operation Name" %}{% trans "Doctrine" %}{% trans "Form Up System" %}{% trans "Form Up Location" %}{% trans "Start Time" %}{% trans "Local Time" %}{% trans "Duration" %}{% trans "FC" %}{% trans "Details" %}{% trans "Post Time" %}{% trans "Creator" %}{% trans "Action" %}
{{ ops.operation_name }}{{ ops.doctrine }} - {{ ops.system }} - {{ ops.location }}{{ ops.start | date:"Y-m-d H:i" }}
{{ ops.duration }}{{ ops.fc }}{{ ops.details }}{{ ops.post_time}}{{ ops.eve_character }} - - - - - - -
+
+
+ {% trans "Current Eve Time:" %} +
+
+
+ +

{% trans "Next Timers" %}

+ {% if future_timers %} + {% include "registered/fleetoptable.html" with timers=future_timers %} {% else %} -
{% trans "No fleet operations found." %}
+
{% trans "No upcoming timers." %}
+ {% endif %} + +

{% trans "Past Timers" %}

+ {% if past_timers %} + {% include "registered/fleetoptable.html" with timers=past_timers %} + {% else %} +
{% trans "No past timers." %}
{% endif %} diff --git a/timerboard/views.py b/timerboard/views.py index fe63c3c3..a2f645e4 100755 --- a/timerboard/views.py +++ b/timerboard/views.py @@ -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( From e76b0789f3436f394b799b2df3c7dfed31709359 Mon Sep 17 00:00:00 2001 From: Adarnof Date: Tue, 23 May 2017 11:03:52 -0400 Subject: [PATCH 14/14] correct centos service name Thanks @iAddz --- docs/installation/auth/supervisor.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/installation/auth/supervisor.md b/docs/installation/auth/supervisor.md index 6e71ec8a..c616a539 100644 --- a/docs/installation/auth/supervisor.md +++ b/docs/installation/auth/supervisor.md @@ -15,8 +15,8 @@ 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 @@ -60,4 +60,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) \ No newline at end of file +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)