Compare commits

...

34 Commits

Author SHA1 Message Date
Adarnof
26eebec918
Version bump to v1.15.8 2018-04-15 19:15:11 -04:00
Adarnof
072d1b9db6 Sanitize username on Discord user join.
Thanks @iakopo
2018-04-02 20:41:34 -04:00
Adarnof
8c957e9cb7 Correct queryset ordering.
Closes #1001
2018-03-27 15:52:32 -04:00
Adarnof
69a686a98a Group list API endpoint has moved.
Allow infinite group cache age.

Thanks @TargetZ3R0

(cherry-picked from bdb3ab366fba2cd7e1ae799b5b75e46c63d95bf3)
2018-03-22 18:05:45 -04:00
Adarnof
c69b41738b Stop using the patch method for setting roles.
Switch to dedicated add/remove endpoints.
Allow setting max cache age to None for infinite.

Apparently patch has issues.

Thanks @TargetZ3R0 and Discord devs <3
2018-03-19 18:17:29 -04:00
Adarnof
a096023553 Prevent checking out v2 when trying to install v1 2018-02-24 01:30:07 -05:00
Adarnof
5eecee49f5 Correct broken template tags.
(cherry picked from commit 77c93ed96ba8f12e403674c89616edd51f34d7b9)
2018-02-23 22:28:19 -05:00
Ariel Rin
d8f4d56dd8 Add Timerboard Structures, step 2 (#976)
Added additional labels for added structure types
2018-02-23 21:35:49 -05:00
Adarnof
d58ac8a718 Remove references to removed setting.
Version bump.
2018-02-23 13:57:39 -05:00
Adarnof
d503243e12 Use new endpoint for adding Discord users.
Closes #974
(cherry picked from commit 70c2a4a6e46ce34be1b981a60fd7efd065c235fb)
2018-02-23 13:36:12 -05:00
Adarnof
5962f0f29f Do not sanitize Discord names
(cherry-picked from commit 8ce87896313d675ec942d1c774e3b93afbc3dc1f)
2018-02-23 13:36:12 -05:00
Adarnof
a2f4226381 Delete Discord users if they've left the server.
Closes #968

(cherry picked from commit 99b136b824d831f57a4000bb6813608083f1e4b5)

Create new roles with desired attributes in one call.

(cherry picked from commit ae4116c0f6a31997966505114f61b87745575dc1)
2018-02-22 15:50:35 -05:00
Adarnof
1ce041b90a Prevent new roles from being sorted separately.
Addresses #969

(cherry picked from commit 3080d7d868cda97b6aa265b23d05c0301d15115c)
2018-02-22 14:44:48 -05:00
Adarnof
91ec924acc Ensure api backoff returns result of decorated function 2018-02-22 02:08:32 -05:00
Adarnof
0f1535161c Handle HTTP429 on nickname API endpoint
Closes #971

(cherry picked from commit a64dda2a2e64d1629f389adcf7ce67d15c3085bd)
2018-02-21 17:52:32 -05:00
Adarnof
1caa4b6baa
Merge pull request #973 from soratidus999/timerupdates
Updated Structure Choices
2018-02-21 17:20:22 -05:00
Ariel Rin
0474fa6d17
Updated Strucure Choices
Added Refineries, and a Moon Mining Option
Also changed spacing to be consistent and be easier to read
2018-02-21 23:01:08 +10:00
Adarnof
e1907d9d17
Do not localize comment count
Closes #910
2018-01-07 21:07:53 -05:00
ghoti
2e214e442c Sort Completed HR apps by create date (most recent first) (#931) 2017-12-20 18:03:51 -05:00
Adarnof
0d64441538
Version bump to v1.15.6 2017-11-17 16:22:03 -05:00
Adarnof
58a333c67a
Case-insensitive group name to ID translation
Seems Discourse won't let you create `Group` if `group` already exists (`422 Name has already been taken`).

Thanks @huberfe
2017-11-17 13:07:41 -05:00
Adarnof
6837f94e59
Disable SeAT accounts instead of deleting. (#915)
See eveseat/web@1abb402
2017-11-03 19:20:31 -04:00
phaynu
16987fcaf0 Extending Choices for Questions in hrapplications to Allow Multiselect (#911)
An additional field at the question level defines whether the choices for the question are multi-select or not. The template will render the choices with radio buttons or checkboxes depending on multi-select. Multiple selected choices are saved with a line break between them.
2017-10-25 00:35:19 -04:00
Derptron
ebd3be3f46 Documentation update (#850)
* Update to the Dependency in regard to using SeAT
* Update to the installation of SSL-Certificates with Discourse
* CleanUp of some missing information in the discourse section
2017-10-05 13:15:34 +10:00
Adarnof
a02e5f400a Version bump to v1.15.5 2017-10-03 22:37:56 -04:00
Adarnof
65c168939d Handle FAT ZeroDivisionErrors
Closes #881
2017-10-03 21:50:32 -04:00
Adarnof
313cac6ac7 Handle new zKillboard API format
Closes #872
2017-10-01 12:53:03 -04:00
Adarnof
0145ea82c8 Correct py3 __str__ support.
Change slugify package for py3
2017-09-30 18:38:05 -04:00
Adarnof
0cdc5ffbd5 Use pypi versioned adarnauth-esi 2017-09-27 18:52:48 -04:00
Basraah
0bdd044378 Improve support for milliseconds backoff 2017-09-26 09:02:37 +10:00
Adarnof
ad266ea2ee Increase tested retry after
Apparently tests take longer than 200ms to evaluate here.
2017-09-25 18:36:53 -04:00
Adarnof
7ea8c9e50d Retry after in milliseconds
Closes #874
2017-09-25 18:21:23 -04:00
mmolitor87
9a015fd582 Change index images to font (#841)
* Change index images to font

* Added SEAT_URL reference and added it to the index template
2017-09-23 08:29:08 +10:00
Adarnof
7ca1c87c87 Minimize swagger spec files. 2017-09-20 01:31:04 -04:00
33 changed files with 404 additions and 179 deletions

View File

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

View File

@ -482,7 +482,6 @@ TEAMSPEAK3_PUBLIC_URL = os.environ.get('AA_TEAMSPEAK3_PUBLIC_URL', 'example.com'
###################################### ######################################
# DISCORD_GUILD_ID - ID of the guild to manage # DISCORD_GUILD_ID - ID of the guild to manage
# DISCORD_BOT_TOKEN - oauth token of the app bot user # DISCORD_BOT_TOKEN - oauth token of the app bot user
# DISCORD_INVITE_CODE - invite code to the server
# DISCORD_APP_ID - oauth app client ID # DISCORD_APP_ID - oauth app client ID
# DISCORD_APP_SECRET - oauth app secret # DISCORD_APP_SECRET - oauth app secret
# DISCORD_CALLBACK_URL - oauth callback url # DISCORD_CALLBACK_URL - oauth callback url
@ -490,7 +489,6 @@ TEAMSPEAK3_PUBLIC_URL = os.environ.get('AA_TEAMSPEAK3_PUBLIC_URL', 'example.com'
###################################### ######################################
DISCORD_GUILD_ID = os.environ.get('AA_DISCORD_GUILD_ID', '') DISCORD_GUILD_ID = os.environ.get('AA_DISCORD_GUILD_ID', '')
DISCORD_BOT_TOKEN = os.environ.get('AA_DISCORD_BOT_TOKEN', '') DISCORD_BOT_TOKEN = os.environ.get('AA_DISCORD_BOT_TOKEN', '')
DISCORD_INVITE_CODE = os.environ.get('AA_DISCORD_INVITE_CODE', '')
DISCORD_APP_ID = os.environ.get('AA_DISCORD_APP_ID', '') DISCORD_APP_ID = os.environ.get('AA_DISCORD_APP_ID', '')
DISCORD_APP_SECRET = os.environ.get('AA_DISCORD_APP_SECRET', '') DISCORD_APP_SECRET = os.environ.get('AA_DISCORD_APP_SECRET', '')
DISCORD_CALLBACK_URL = os.environ.get('AA_DISCORD_CALLBACK_URL', 'http://example.com/discord/callback') DISCORD_CALLBACK_URL = os.environ.get('AA_DISCORD_CALLBACK_URL', 'http://example.com/discord/callback')

File diff suppressed because one or more lines are too long

View File

@ -57,7 +57,7 @@ Enter the folder by issuing `cd allianceauth`
Ensure you're on the latest version with the following: Ensure you're on the latest version with the following:
git tag | sort -n | tail -1 | xargs git checkout git checkout v1.15.7
Python package dependencies can be installed from the requirements file: Python package dependencies can be installed from the requirements file:

View File

@ -25,6 +25,17 @@ Required for displaying web content
apache2 libapache2-mod-php5 libapache2-mod-wsgi apache2 libapache2-mod-php5 libapache2-mod-wsgi
### PHP ### PHP
```eval_rst
.. note::
If you are not planing to install either phpBB, smf, evernus alliance market, etc do not install these modules.
```
```eval_rst
.. important::
If you are not planing to use SeAT; php7.0 is the minimum required.
```
Required for phpBB, smf, evernus alliance market, etc Required for phpBB, smf, evernus alliance market, etc
php5 php5-gd php5-mysqlnd php5-curl php5-gd php5-intl php-pear php5-imagick php5-imap php5-mcrypt php5-memcache php5-ming php5-ps php5-pspell php5-recode php5-snmp php5-sqlite php5-tidy php5-xmlrpc php5-xsl php5 php5-gd php5-mysqlnd php5-curl php5-gd php5-intl php-pear php5-imagick php5-imap php5-mcrypt php5-memcache php5-ming php5-ps php5-pspell php5-recode php5-snmp php5-sqlite php5-tidy php5-xmlrpc php5-xsl

View File

@ -46,7 +46,7 @@ Enter the folder by issuing `cd allianceauth`
Ensure you're on the latest version with the following: Ensure you're on the latest version with the following:
git tag | sort -n | tail -1 | xargs git checkout git checkout v1.15.7
Python package dependencies can be installed from the requirements file: Python package dependencies can be installed from the requirements file:

View File

@ -21,13 +21,6 @@ with a server ID of `120631096835571712`
Update settings.py, inputting the server ID as `DISCORD_GUILD_ID` Update settings.py, inputting the server ID as `DISCORD_GUILD_ID`
### Generating an Invite
Still on the Discord site, in your new server, an invite needs to be generated for users to join. If you with for users to initially join a different channel than `#general`, create it and follow the steps below, substituting this channel for `#general`.
On the left bar under the Text Channels heading, hover over `#general` on the right site. There are two icons, a box with an arrow and a gear. Press the box, then on the bottom left select Advanced Settings. Set the expiration to never, and no limit on uses. Press generate.
This returns a code that looks like `https://discord.gg/0fmA8MyXV6qt7XAZ`. The part after the last slash, `0fmA8MyXV6qt7XAZ`, is the invite code. Update settings.py, inputting this invite code as `DISCORD_INVITE_CODE`
### Registering an Application ### Registering an Application
Navigate to the [Discord Developers site.](https://discordapp.com/developers/applications/me) Press the plus sign to create a new application. Navigate to the [Discord Developers site.](https://discordapp.com/developers/applications/me) Press the plus sign to create a new application.

View File

@ -65,6 +65,11 @@ Now build:
sudo ./launcher bootstrap app sudo ./launcher bootstrap app
sudo ./launcher start app sudo ./launcher start app
#### Errors:
in case you run into not enough RAM for the docker bootstraping you might want to consider using `./discourse-setup` command. It will start bootstraping and is going to create the `/containers/app.yml` which you can edit.
Note: every time you change something in the `app.yml` you must bootstrap again which will take between *2-8 minutes* and is accomplished by `./launcher rebuild app`.
***
## Apache config ## Apache config
Discourse must run on its own subdomain - it can't handle routing behind an alias like '/forums'. To do so, make a new apache config: Discourse must run on its own subdomain - it can't handle routing behind an alias like '/forums'. To do so, make a new apache config:
@ -81,9 +86,59 @@ And enter the following, changing the port if you used a different number:
Now enable proxies and restart apache: Now enable proxies and restart apache:
sudo a2ensite discourse
sudo a2enmod proxy_http sudo a2enmod proxy_http
sudo service apache2 reload sudo service apache2 reload
### Setting up SSL
It is 2017 and there is no reason why you should not setup a SSL certificate and enforce https. You may want to consider certbot with Let's encrypt: https://www.digitalocean.com/community/tutorials/how-to-secure-apache-with-let-s-encrypt-on-ubuntu-16-04
sudo certbot --apache -d example.com
now adapt the apache configuration:
sudo nano /etc/apache2/sites-enabled/discourse.conf
and adapt it followlingly:
<VirtualHost *:80>
ServerName discourse.example.com
RewriteEngine on
RewriteCond %{SERVER_NAME} =discourse.example.com
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>
Then adapt change the ssl-config file:
sudo nano /etc/apache2/sites-enabled/discourse-le-ssl.conf
and adapt it followlingly:
<IfModule mod_ssl.c>
<VirtualHost *:443>
ServerName discourse.example.com
ProxyPass / http://127.0.0.1:7890/
ProxyPassReverse / http://127.0.0.1:7890/
ProxyPreserveHost On
RequestHeader set X-FORWARDED-PROTOCOL https
RequestHeader set X-FORWARDED-SSL on
SSLCertificateFile /etc/letsencrypt/live/discourse.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/discourse.example.com/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>
</IfModule>
make sure that `a2enmod headers` is enabled and run:
sudo service apache2 restart
Now you are all set-up and can even enforce https in discourse settings.
## Configure API ## Configure API
### Generate admin account ### Generate admin account
@ -108,6 +163,7 @@ Scroll down to the Discourse section and set the following:
- `DISCOURSE_API_USERNAME`: the username of the admin account you generated the API key with - `DISCOURSE_API_USERNAME`: the username of the admin account you generated the API key with
- `DISCOURSE_API_KEY`: the key you just generated - `DISCOURSE_API_KEY`: the key you just generated
***
### Configure SSO ### Configure SSO
Navigate to `discourse.example.com` and log in. Back to the admin site, scroll down to find SSO settings and set the following: Navigate to `discourse.example.com` and log in. Back to the admin site, scroll down to find SSO settings and set the following:
@ -120,6 +176,7 @@ Save, now change settings.py and add the following:
### Enable for your members ### Enable for your members
Set either or both of `ENABLE_AUTH_DISCOURSE` and `ENABLE_BLUE_DISCOURSE` in settings.py for your members to gain access. Save and exit with control+o, enter, control+x. Assign discourse permissions for each auth-group that should have access to discourse.
You might want to setup Read/Write/Delete rights per Auth group in discourse as you can limit which categories shall be accessablie per auth-group.
## Done ## Done

File diff suppressed because one or more lines are too long

View File

@ -36,5 +36,5 @@ class Fat(models.Model):
unique_together = (('character', 'fatlink'),) unique_together = (('character', 'fatlink'),)
def __str__(self): def __str__(self):
output = "Fat-link for %s" % self.character.character_name return "Fat-link for %s" % self.character.character_name
return output.encode('utf-8')

File diff suppressed because one or more lines are too long

View File

@ -52,8 +52,12 @@ class CorpStat(object):
fatlink__fatdatetime__gte=start_of_month).filter(fatlink__fatdatetime__lte=start_of_next_month).count() fatlink__fatdatetime__gte=start_of_month).filter(fatlink__fatdatetime__lte=start_of_next_month).count()
self.blue = self.corp.is_blue self.blue = self.corp.is_blue
@property
def avg_fat(self): def avg_fat(self):
return "%.2f" % (float(self.n_fats) / float(self.corp.member_count)) try:
return "%.2f" % (float(self.n_fats) / float(self.corp.member_count))
except ZeroDivisionError:
return "%.2f" % 0
class MemberStat(object): class MemberStat(object):
@ -69,9 +73,13 @@ class MemberStat(object):
self.n_chars = nchars self.n_chars = nchars
self.n_fats = Fat.objects.filter(user_id=member['user_id']).filter( 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() fatlink__fatdatetime__gte=start_of_month).filter(fatlink__fatdatetime__lte=start_of_next_month).count()
@property
def avg_fat(self): def avg_fat(self):
return "%.2f" % (float(self.n_fats) / float(self.n_chars)) try:
return "%.2f" % (float(self.n_fats) / float(self.n_chars))
except ZeroDivisionError:
return "%.2f" % 0
def first_day_of_next_month(year, month): def first_day_of_next_month(year, month):
@ -132,7 +140,7 @@ def fatlink_statistics_corp_view(request, corpid, year=None, month=None):
# collect and sort stats # collect and sort stats
stat_list = [fat_stats[x] for x in fat_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.mainchar.character_name)
stat_list.sort(key=lambda stat: (stat.n_fats, stat.n_fats / stat.n_chars), reverse=True) stat_list.sort(key=lambda stat: (stat.n_fats, stat.avg_fat), reverse=True)
context = {'fatStats': stat_list, 'month': start_of_month.strftime("%B"), 'year': year, context = {'fatStats': stat_list, 'month': start_of_month.strftime("%B"), 'year': year,
'previous_month': start_of_previous_month, 'corpid': corpid} 'previous_month': start_of_previous_month, 'corpid': corpid}
@ -170,7 +178,7 @@ def fatlink_statistics_view(request, year=datetime.date.today().year, month=date
# collect and sort stats # collect and sort stats
stat_list = [fat_stats[x] for x in fat_stats] stat_list = [fat_stats[x] for x in fat_stats]
stat_list.sort(key=lambda stat: stat.corp.corporation_name) stat_list.sort(key=lambda stat: stat.corp.corporation_name)
stat_list.sort(key=lambda stat: (stat.n_fats, stat.n_fats / stat.corp.member_count), reverse=True) stat_list.sort(key=lambda stat: (stat.n_fats, stat.avg_fat), reverse=True)
context = {'fatStats': stat_list, 'month': start_of_month.strftime("%B"), 'year': year, context = {'fatStats': stat_list, 'month': start_of_month.strftime("%B"), 'year': year,
'previous_month': start_of_previous_month} 'previous_month': start_of_previous_month}

View File

@ -16,7 +16,7 @@ class ChoiceInline(admin.TabularInline):
class QuestionAdmin(admin.ModelAdmin): class QuestionAdmin(admin.ModelAdmin):
fieldsets = [ fieldsets = [
(None, {'fields': ['title', 'help_text']}), (None, {'fields': ['title', 'help_text', 'multi_select']}),
] ]
inlines = [ChoiceInline] inlines = [ChoiceInline]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2017-10-20 13:51
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hrapplications', '0002_choices_for_questions'),
]
operations = [
migrations.AddField(
model_name='applicationquestion',
name='multi_select',
field=models.BooleanField(default=False),
),
]

View File

@ -13,6 +13,7 @@ from authentication.models import AuthServicesInfo
class ApplicationQuestion(models.Model): class ApplicationQuestion(models.Model):
title = models.CharField(max_length=254, verbose_name='Question') title = models.CharField(max_length=254, verbose_name='Question')
help_text = models.CharField(max_length=254, blank=True, null=True) help_text = models.CharField(max_length=254, blank=True, null=True)
multi_select = models.BooleanField(default=False)
def __str__(self): def __str__(self):
return "Question: " + self.title return "Question: " + self.title

View File

@ -39,13 +39,14 @@ def hr_application_management_view(request):
except EveCharacter.DoesNotExist: except EveCharacter.DoesNotExist:
pass pass
if request.user.is_superuser: if request.user.is_superuser:
corp_applications = Application.objects.filter(approved=None) corp_applications = Application.objects.filter(approved=None).order_by('-created')
finished_corp_applications = Application.objects.exclude(approved=None) finished_corp_applications = Application.objects.exclude(approved=None).order_by('-created')
elif request.user.has_perm('auth.human_resources') and main_char: elif request.user.has_perm('auth.human_resources') and main_char:
if ApplicationForm.objects.filter(corp__corporation_id=main_char.corporation_id).exists(): if ApplicationForm.objects.filter(corp__corporation_id=main_char.corporation_id).exists():
app_form = ApplicationForm.objects.get(corp__corporation_id=main_char.corporation_id) app_form = ApplicationForm.objects.get(corp__corporation_id=main_char.corporation_id)
corp_applications = Application.objects.filter(form=app_form).filter(approved=None) corp_applications = Application.objects.filter(form=app_form).filter(approved=None).order_by('-created')
finished_corp_applications = Application.objects.filter(form=app_form).filter(approved__in=[True, False]) finished_corp_applications = Application.objects.filter(form=app_form).filter(
approved__in=[True, False]).order_by('-created')
logger.debug("Retrieved %s personal, %s corp applications for %s" % ( logger.debug("Retrieved %s personal, %s corp applications for %s" % (
len(request.user.applications.all()), len(corp_applications), request.user)) len(request.user.applications.all()), len(corp_applications), request.user))
context = { context = {
@ -71,8 +72,8 @@ def hr_application_create_view(request, form_id=None):
application.save() application.save()
for question in app_form.questions.all(): for question in app_form.questions.all():
response = ApplicationResponse(question=question, application=application) response = ApplicationResponse(question=question, application=application)
response.answer = request.POST.get(str(question.pk), response.answer = "\n".join(request.POST.getlist(str(question.pk),
"Failed to retrieve answer provided by applicant.") ""))
response.save() response.save()
logger.info("%s created %s" % (request.user, application)) logger.info("%s created %s" % (request.user, application))
return redirect('auth_hrapplications_view') return redirect('auth_hrapplications_view')

View File

@ -21,5 +21,4 @@ class optimer(models.Model):
eve_character = models.ForeignKey(EveCharacter) eve_character = models.ForeignKey(EveCharacter)
def __str__(self): def __str__(self):
output = self.operation_name return self.operation_name
return output.encode('utf-8')

View File

@ -5,7 +5,7 @@ dnspython
passlib passlib
requests>=2.9.1 requests>=2.9.1
bcrypt bcrypt
slugify python-slugify>=1.2
requests-oauthlib requests-oauthlib
sleekxmpp sleekxmpp
redis redis
@ -23,4 +23,4 @@ django-celery-beat
# awating pyghassen/openfire-restapi #1 to fix installation issues # awating pyghassen/openfire-restapi #1 to fix installation issues
git+https://github.com/adarnof/openfire-restapi git+https://github.com/adarnof/openfire-restapi
git+https://github.com/adarnof/adarnauth-esi adarnauth-esi>=1.4.1,<2.0

View File

@ -15,6 +15,7 @@ def auth_settings(request):
'IPS4_URL': settings.IPS4_URL, 'IPS4_URL': settings.IPS4_URL,
'SMF_URL': settings.SMF_URL, 'SMF_URL': settings.SMF_URL,
'MARKET_URL': settings.MARKET_URL, 'MARKET_URL': settings.MARKET_URL,
'SEAT_URL': settings.SEAT_URL,
'EXTERNAL_MEDIA_URL': settings.EXTERNAL_MEDIA_URL, 'EXTERNAL_MEDIA_URL': settings.EXTERNAL_MEDIA_URL,
'CURRENT_UTC_TIME': timezone.now(), 'CURRENT_UTC_TIME': timezone.now(),
'BLUE_API_MASK': settings.BLUE_API_MASK, 'BLUE_API_MASK': settings.BLUE_API_MASK,

View File

@ -27,11 +27,11 @@ class srpManager:
r = requests.get(url, headers=headers) r = requests.get(url, headers=headers)
result = r.json()[0] result = r.json()[0]
if result: if result:
ship_type = result['victim']['shipTypeID'] ship_type = result['victim']['ship_type_id']
logger.debug("Ship type for kill ID %s is determined to be %s" % (kill_id, ship_type)) logger.debug("Ship type for kill ID %s is %s" % (kill_id, ship_type))
ship_value = result['zkb']['totalValue'] ship_value = result['zkb']['totalValue']
logger.debug("total loss value for kill id %s is %s" % (kill_id, ship_value)) logger.debug("Total loss value for kill id %s is %s" % (kill_id, ship_value))
victim_name = result['victim']['characterName'] victim_id = result['victim']['character_id']
return ship_type, ship_value, victim_name return ship_type, ship_value, victim_id
else: else:
raise ValueError("Invalid Kill ID") raise ValueError("Invalid Kill ID")

View File

@ -1,7 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import requests
import json
import re import re
import requests
import math
from django.conf import settings from django.conf import settings
from requests_oauthlib import OAuth2Session from requests_oauthlib import OAuth2Session
from functools import wraps from functools import wraps
@ -24,8 +24,8 @@ Previously all we asked for was permission to kick members, manage roles, and ma
Users have reported weird unauthorized errors we don't understand. So now we ask for full server admin. Users have reported weird unauthorized errors we don't understand. So now we ask for full server admin.
It's almost fixed the problem. It's almost fixed the problem.
""" """
# kick members, manage roles, manage nicknames # kick members, manage roles, manage nicknames, create instant invite
# BOT_PERMISSIONS = 0x00000002 + 0x10000000 + 0x08000000 # BOT_PERMISSIONS = 0x00000002 + 0x10000000 + 0x08000000 + 0x00000001
BOT_PERMISSIONS = 0x00000008 BOT_PERMISSIONS = 0x00000008
# get user ID, accept invite # get user ID, accept invite
@ -34,7 +34,7 @@ SCOPES = [
'guilds.join', 'guilds.join',
] ]
GROUP_CACHE_MAX_AGE = int(getattr(settings, 'DISCORD_GROUP_CACHE_MAX_AGE', 2 * 60 * 60)) # 2 hours default GROUP_CACHE_MAX_AGE = getattr(settings, 'DISCORD_GROUP_CACHE_MAX_AGE', 2 * 60 * 60) # 2 hours default
class DiscordApiException(Exception): class DiscordApiException(Exception):
@ -50,12 +50,20 @@ class DiscordApiTooBusy(DiscordApiException):
class DiscordApiBackoff(DiscordApiException): class DiscordApiBackoff(DiscordApiException):
def __init__(self, retry_after, global_ratelimit): def __init__(self, retry_after, global_ratelimit):
"""
:param retry_after: int time to retry after in milliseconds
:param global_ratelimit: bool Is the API under a global backoff
"""
super(DiscordApiException, self).__init__() super(DiscordApiException, self).__init__()
self.retry_after = retry_after self.retry_after = retry_after
self.global_ratelimit = global_ratelimit self.global_ratelimit = global_ratelimit
@property
def retry_after_seconds(self):
return math.ceil(self.retry_after / 1000)
cache_time_format = '%Y-%m-%d %H:%M:%S'
cache_time_format = '%Y-%m-%d %H:%M:%S.%f'
def api_backoff(func): def api_backoff(func):
@ -109,20 +117,19 @@ def api_backoff(func):
global_ratelimit=bool(existing_global_backoff) global_ratelimit=bool(existing_global_backoff)
) )
logger.debug("Calling API calling function") logger.debug("Calling API calling function")
func(*args, **kwargs) return func(*args, **kwargs)
break
except requests.HTTPError as e: except requests.HTTPError as e:
if e.response.status_code == 429: if e.response.status_code == 429:
try: try:
retry_after = int(e.response.headers['Retry-After']) retry_after = int(e.response.headers['Retry-After'])
except (TypeError, KeyError): except (TypeError, KeyError):
# Pick some random time # Pick some random time
retry_after = 5 retry_after = 5000
logger.info("Received backoff from API of %s seconds, handling" % retry_after) logger.info("Received backoff from API of %s seconds, handling" % retry_after)
# Store value in redis # Store value in redis
backoff_until = (datetime.datetime.utcnow() + backoff_until = (datetime.datetime.utcnow() +
datetime.timedelta(seconds=retry_after)) datetime.timedelta(milliseconds=retry_after))
global_backoff = bool(e.response.headers.get('X-RateLimit-Global', False)) global_backoff = bool(e.response.headers.get('X-RateLimit-Global', False))
if global_backoff: if global_backoff:
logger.info("Global backoff!!") logger.info("Global backoff!!")
@ -138,7 +145,7 @@ def api_backoff(func):
# Sleep if we're blocking # Sleep if we're blocking
if blocking: if blocking:
logger.info("Blocking Back off from API calls for %s seconds" % bo.retry_after) logger.info("Blocking Back off from API calls for %s seconds" % bo.retry_after)
time.sleep(10 if bo.retry_after > 10 else bo.retry_after) time.sleep((10 if bo.retry_after > 10 else bo.retry_after) / 1000)
else: else:
# Otherwise raise exception and let caller handle the backoff # Otherwise raise exception and let caller handle the backoff
raise DiscordApiBackoff(retry_after=bo.retry_after, global_ratelimit=bo.global_ratelimit) raise DiscordApiBackoff(retry_after=bo.retry_after, global_ratelimit=bo.global_ratelimit)
@ -155,12 +162,11 @@ class DiscordOAuthManager:
@staticmethod @staticmethod
def _sanitize_name(name): def _sanitize_name(name):
return re.sub('[^\w.-]', '', name)[:32] return name[:32]
@staticmethod @staticmethod
def _sanitize_groupname(name): def _sanitize_group_name(name):
name = name.strip(' _') return name[:100]
return DiscordOAuthManager._sanitize_name(name)
@staticmethod @staticmethod
def generate_bot_add_url(): def generate_bot_add_url():
@ -179,23 +185,33 @@ class DiscordOAuthManager:
return token return token
@staticmethod @staticmethod
def add_user(code): def add_user(code, groups, nickname=None):
try: try:
token = DiscordOAuthManager._process_callback_code(code)['access_token'] token = DiscordOAuthManager._process_callback_code(code)['access_token']
logger.debug("Received token from OAuth") logger.debug("Received token from OAuth")
custom_headers = {'accept': 'application/json', 'authorization': 'Bearer ' + token} custom_headers = {'accept': 'application/json', 'authorization': 'Bearer ' + token}
path = DISCORD_URL + "/invites/" + str(settings.DISCORD_INVITE_CODE)
r = requests.post(path, headers=custom_headers)
logger.debug("Got status code %s after accepting Discord invite" % r.status_code)
r.raise_for_status()
path = DISCORD_URL + "/users/@me" path = DISCORD_URL + "/users/@me"
r = requests.get(path, headers=custom_headers) r = requests.get(path, headers=custom_headers)
logger.debug("Got status code %s after retrieving Discord profile" % r.status_code) logger.debug("Got status code %s after retrieving Discord profile" % r.status_code)
r.raise_for_status() r.raise_for_status()
user_id = r.json()['id'] user_id = r.json()['id']
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id)
group_ids = [DiscordOAuthManager._group_name_to_id(DiscordOAuthManager._sanitize_group_name(g)) for g in
groups]
data = {
'roles': group_ids,
'access_token': token,
}
if nickname:
data['nick'] = DiscordOAuthManager._sanitize_name(nickname)
custom_headers['authorization'] = 'Bot ' + settings.DISCORD_BOT_TOKEN
r = requests.put(path, headers=custom_headers, json=data)
logger.debug("Got status code %s after joining Discord server" % r.status_code)
r.raise_for_status()
logger.info("Added Discord user ID %s to server." % user_id) logger.info("Added Discord user ID %s to server." % user_id)
return user_id return user_id
except: except:
@ -203,6 +219,7 @@ class DiscordOAuthManager:
return None return None
@staticmethod @staticmethod
@api_backoff
def update_nickname(user_id, nickname): def update_nickname(user_id, nickname):
try: try:
nickname = DiscordOAuthManager._sanitize_name(nickname) nickname = DiscordOAuthManager._sanitize_name(nickname)
@ -252,7 +269,7 @@ class DiscordOAuthManager:
@staticmethod @staticmethod
def _group_name_to_id(name): def _group_name_to_id(name):
name = DiscordOAuthManager._sanitize_groupname(name) name = DiscordOAuthManager._sanitize_group_name(name)
def get_or_make_role(): def get_or_make_role():
groups = DiscordOAuthManager._get_groups() groups = DiscordOAuthManager._get_groups()
@ -263,42 +280,61 @@ class DiscordOAuthManager:
return cache.get_or_set(DiscordOAuthManager._generate_cache_role_key(name), get_or_make_role, GROUP_CACHE_MAX_AGE) return cache.get_or_set(DiscordOAuthManager._generate_cache_role_key(name), get_or_make_role, GROUP_CACHE_MAX_AGE)
@staticmethod @staticmethod
def __generate_role(): def __generate_role(name, **kwargs):
custom_headers = {'accept': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN} custom_headers = {'accept': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/roles" path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/roles"
r = requests.post(path, headers=custom_headers) data = {'name': name}
data.update(kwargs)
r = requests.post(path, headers=custom_headers, json=data)
logger.debug("Received status code %s after generating new role." % r.status_code) logger.debug("Received status code %s after generating new role." % r.status_code)
r.raise_for_status() r.raise_for_status()
return r.json() return r.json()
@staticmethod @staticmethod
def __edit_role(role_id, name, color=0, hoist=True, permissions=36785152): def __edit_role(role_id, **kwargs):
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN} custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
data = {
'color': color,
'hoist': hoist,
'name': name,
'permissions': permissions,
}
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/roles/" + str(role_id) path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/roles/" + str(role_id)
r = requests.patch(path, headers=custom_headers, data=json.dumps(data)) r = requests.patch(path, headers=custom_headers, json=kwargs)
logger.debug("Received status code %s after editing role id %s" % (r.status_code, role_id)) logger.debug("Received status code %s after editing role id %s" % (r.status_code, role_id))
r.raise_for_status() r.raise_for_status()
return r.json() return r.json()
@staticmethod @staticmethod
def _create_group(name): def _create_group(name):
role = DiscordOAuthManager.__generate_role() return DiscordOAuthManager.__generate_role(name)
return DiscordOAuthManager.__edit_role(role['id'], name)
@staticmethod
def _get_user(user_id):
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id)
r = requests.get(path, headers=custom_headers)
r.raise_for_status()
return r.json()
@staticmethod
def _get_user_roles(user_id):
user = DiscordOAuthManager._get_user(user_id)
return user['roles']
@staticmethod
def _modify_user_role(user_id, role_id, method):
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id) + "/roles/" + str(
role_id)
r = getattr(requests, method)(path, headers=custom_headers)
r.raise_for_status()
logger.debug("%s role %s for user %s" % (method, role_id, user_id))
@staticmethod @staticmethod
@api_backoff @api_backoff
def update_groups(user_id, groups): def update_groups(user_id, groups):
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN} group_ids = [DiscordOAuthManager._group_name_to_id(DiscordOAuthManager._sanitize_group_name(g)) for g in groups]
group_ids = [DiscordOAuthManager._group_name_to_id(DiscordOAuthManager._sanitize_groupname(g)) for g in groups] user_group_ids = DiscordOAuthManager._get_user_roles(user_id)
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id) for g in group_ids:
data = {'roles': group_ids} if g not in user_group_ids:
r = requests.patch(path, headers=custom_headers, json=data) DiscordOAuthManager._modify_user_role(user_id, g, 'put')
logger.debug("Received status code %s after setting user roles" % r.status_code) time.sleep(1) # we're gonna be hammering the API here
r.raise_for_status() for g in user_group_ids:
if g not in group_ids:
DiscordOAuthManager._modify_user_role(user_id, g, 'delete')
time.sleep(1)

View File

@ -6,7 +6,7 @@ from alliance_auth.celeryapp import app
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from requests.exceptions import HTTPError
from eveonline.managers import EveManager from eveonline.managers import EveManager
from notifications import notify from notifications import notify
from services.modules.discord.manager import DiscordOAuthManager, DiscordApiBackoff from services.modules.discord.manager import DiscordOAuthManager, DiscordApiBackoff
@ -21,15 +21,16 @@ class DiscordTasks:
@classmethod @classmethod
def add_user(cls, user, code): def add_user(cls, user, code):
user_id = DiscordOAuthManager.add_user(code) groups = DiscordTasks.get_groups(user)
nickname = None
if settings.DISCORD_SYNC_NAMES:
nickname = DiscordTasks.get_nickname(user)
user_id = DiscordOAuthManager.add_user(code, groups, nickname=nickname)
if user_id: if user_id:
discord_user = DiscordUser() discord_user = DiscordUser()
discord_user.user = user discord_user.user = user
discord_user.uid = user_id discord_user.uid = user_id
discord_user.save() discord_user.save()
if settings.DISCORD_SYNC_NAMES:
cls.update_nickname.delay(user.pk)
cls.update_groups.delay(user.pk)
return True return True
return False return False
@ -64,19 +65,23 @@ class DiscordTasks:
user = User.objects.get(pk=pk) user = User.objects.get(pk=pk)
logger.debug("Updating discord groups for user %s" % user) logger.debug("Updating discord groups for user %s" % user)
if DiscordTasks.has_account(user): if DiscordTasks.has_account(user):
groups = [] groups = DiscordTasks.get_groups(user)
for group in user.groups.all():
groups.append(str(group.name))
if len(groups) == 0:
logger.debug("No syncgroups found for user. Adding empty group.")
groups.append('empty')
logger.debug("Updating user %s discord groups to %s" % (user, groups)) logger.debug("Updating user %s discord groups to %s" % (user, groups))
try: try:
DiscordOAuthManager.update_groups(user.discord.uid, groups) DiscordOAuthManager.update_groups(user.discord.uid, groups)
except DiscordApiBackoff as bo: except DiscordApiBackoff as bo:
logger.info("Discord group sync API back off for %s, " logger.info("Discord group sync API back off for %s, "
"retrying in %s seconds" % (user, bo.retry_after)) "retrying in %s seconds" % (user, bo.retry_after_seconds))
raise task_self.retry(countdown=bo.retry_after) raise task_self.retry(countdown=bo.retry_after_seconds)
except HTTPError as e:
if e.response.status_code == 404:
try:
if e.response.json()['code'] == 10007:
# user has left the server
DiscordTasks.delete_user(user)
return
finally:
raise e
except Exception as e: except Exception as e:
if task_self: if task_self:
logger.exception("Discord group sync failed for %s, retrying in 10 mins" % user) logger.exception("Discord group sync failed for %s, retrying in 10 mins" % user)
@ -97,18 +102,22 @@ class DiscordTasks:
@staticmethod @staticmethod
@app.task(bind=True, name='discord.update_nickname') @app.task(bind=True, name='discord.update_nickname')
def update_nickname(self, pk): def update_nickname(task_self, pk):
user = User.objects.get(pk=pk) user = User.objects.get(pk=pk)
logger.debug("Updating discord nickname for user %s" % user) logger.debug("Updating discord nickname for user %s" % user)
if DiscordTasks.has_account(user): if DiscordTasks.has_account(user):
character = EveManager.get_main_character(user) name = DiscordTasks.get_nickname(user)
logger.debug("Updating user %s discord nickname to %s" % (user, character.character_name)) logger.debug("Updating user %s discord nickname to %s" % (user, name))
try: try:
DiscordOAuthManager.update_nickname(user.discord.uid, character.character_name) DiscordOAuthManager.update_nickname(user.discord.uid, name)
except DiscordApiBackoff as bo:
logger.info("Discord nickname update API back off for %s, "
"retrying in %s seconds" % (user, bo.retry_after_seconds))
raise task_self.retry(countdown=bo.retry_after_seconds)
except Exception as e: except Exception as e:
if self: if task_self:
logger.exception("Discord nickname sync failed for %s, retrying in 10 mins" % user) logger.exception("Discord nickname sync failed for %s, retrying in 10 mins" % user)
raise self.retry(countdown=60 * 10) raise task_self.retry(countdown=60 * 10)
else: else:
# Rethrow # Rethrow
raise e raise e
@ -126,3 +135,11 @@ class DiscordTasks:
@classmethod @classmethod
def disable(cls): def disable(cls):
DiscordUser.objects.all().delete() DiscordUser.objects.all().delete()
@staticmethod
def get_nickname(user):
return EveManager.get_main_character(user).character_name
@staticmethod
def get_groups(user):
return [g.name for g in user.groups.all()]

View File

@ -208,11 +208,11 @@ class DiscordManagerTestCase(TestCase):
def setUp(self): def setUp(self):
pass pass
def test__sanitize_groupname(self): def test__sanitize_group_name(self):
test_group_name = ' Group Name_Test_' test_group_name = str(10**103)
group_name = DiscordOAuthManager._sanitize_groupname(test_group_name) group_name = DiscordOAuthManager._sanitize_group_name(test_group_name)
self.assertEqual(group_name, 'GroupName_Test') self.assertEqual(group_name, test_group_name[:100])
def test_generate_Bot_add_url(self): def test_generate_Bot_add_url(self):
from . import manager from . import manager
@ -267,18 +267,20 @@ class DiscordManagerTestCase(TestCase):
headers = {'accept': 'application/json', 'authorization': 'Bearer accesstoken'} headers = {'accept': 'application/json', 'authorization': 'Bearer accesstoken'}
m.register_uri('POST',
manager.DISCORD_URL + '/invites/'+str(settings.DISCORD_INVITE_CODE),
request_headers=headers,
text='{}')
m.register_uri('GET', m.register_uri('GET',
manager.DISCORD_URL + "/users/@me", manager.DISCORD_URL + "/users/@me",
request_headers=headers, request_headers=headers,
text=json.dumps({'id': "123456"})) text=json.dumps({'id': "123456"}))
headers = {'accept': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
m.register_uri('PUT',
manager.DISCORD_URL + '/guilds/' + str(settings.DISCORD_GUILD_ID) + '/members/123456',
request_headers=headers,
text='{}')
# Act # Act
return_value = DiscordOAuthManager.add_user('abcdef') return_value = DiscordOAuthManager.add_user('abcdef', [])
# Assert # Assert
self.assertEqual(return_value, '123456') self.assertEqual(return_value, '123456')
@ -351,66 +353,66 @@ class DiscordManagerTestCase(TestCase):
# Assert # Assert
self.assertTrue(result) self.assertTrue(result)
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._get_user_roles')
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._get_groups') @mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._get_groups')
@requests_mock.Mocker() @requests_mock.Mocker()
def test_update_groups(self, group_cache, m): def test_update_groups(self, group_cache, user_roles, m):
from . import manager from . import manager
import json import json
# Arrange # Arrange
groups = ['Member', 'Blue', 'Special Group'] groups = ['Member', 'Blue', 'SpecialGroup']
group_cache.return_value = [{'id': 111, 'name': 'Member'}, group_cache.return_value = [{'id': '111', 'name': 'Member'},
{'id': 222, 'name': 'Blue'}, {'id': '222', 'name': 'Blue'},
{'id': 333, 'name': 'SpecialGroup'}, {'id': '333', 'name': 'SpecialGroup'},
{'id': 444, 'name': 'NotYourGroup'}] {'id': '444', 'name': 'NotYourGroup'}]
user_roles.return_value = ['444']
headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN} headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
user_id = 12345 user_id = 12345
request_url = '{}/guilds/{}/members/{}'.format(manager.DISCORD_URL, settings.DISCORD_GUILD_ID, user_id) user_request_url = '{}/guilds/{}/members/{}'.format(manager.DISCORD_URL, settings.DISCORD_GUILD_ID, user_id)
group_request_urls = ['{}/guilds/{}/members/{}/roles/{}'.format(manager.DISCORD_URL, settings.DISCORD_GUILD_ID, user_id, g['id']) for g in group_cache.return_value]
m.patch(request_url, m.patch(user_request_url, request_headers=headers)
request_headers=headers) [m.put(url, request_headers=headers) for url in group_request_urls[:-1]]
m.delete(group_request_urls[-1], request_headers=headers)
# Act # Act
DiscordOAuthManager.update_groups(user_id, groups) DiscordOAuthManager.update_groups(user_id, groups)
# Assert # Assert
self.assertEqual(len(m.request_history), 1, 'Must be one HTTP call made') self.assertEqual(len(m.request_history), 4, 'Must be 4 HTTP calls made')
history = json.loads(m.request_history[0].text)
self.assertIn('roles', history, "'The request must send JSON object with the 'roles' key")
self.assertIn(111, history['roles'], 'The group id 111 must be added to the request')
self.assertIn(222, history['roles'], 'The group id 222 must be added to the request')
self.assertIn(333, history['roles'], 'The group id 333 must be added to the request')
self.assertNotIn(444, history['roles'], 'The group id 444 must NOT be added to the request')
@mock.patch(MODULE_PATH + '.manager.cache') @mock.patch(MODULE_PATH + '.manager.cache')
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._get_groups') @mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._get_user_roles')
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._group_name_to_id')
@requests_mock.Mocker() @requests_mock.Mocker()
def test_update_groups_backoff(self, group_cache, djcache, m): def test_update_groups_backoff(self, name_to_id, user_groups, djcache, m):
from . import manager from . import manager
# Arrange # Arrange
groups = ['Member'] groups = ['Member']
group_cache.return_value = [{'id': 111, 'name': 'Member'}] user_groups.return_value = []
name_to_id.return_value = '111'
headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN} headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
user_id = 12345 user_id = 12345
request_url = '{}/guilds/{}/members/{}'.format(manager.DISCORD_URL, settings.DISCORD_GUILD_ID, user_id) request_url = '{}/guilds/{}/members/{}/roles/{}'.format(manager.DISCORD_URL, settings.DISCORD_GUILD_ID, user_id, name_to_id.return_value)
djcache.get.return_value = None # No existing backoffs in cache djcache.get.return_value = None # No existing backoffs in cache
m.patch(request_url, m.put(request_url,
request_headers=headers, request_headers=headers,
headers={'Retry-After': '200'}, headers={'Retry-After': '200000'},
status_code=429) status_code=429)
# Act & Assert # Act & Assert
with self.assertRaises(manager.DiscordApiBackoff) as bo: with self.assertRaises(manager.DiscordApiBackoff) as bo:
try: try:
DiscordOAuthManager.update_groups(user_id, groups, blocking=False) DiscordOAuthManager.update_groups(user_id, groups, blocking=False)
except manager.DiscordApiBackoff as bo: except manager.DiscordApiBackoff as bo:
self.assertEqual(bo.retry_after, 200, 'Retry-After time must be equal to Retry-After set in header') self.assertEqual(bo.retry_after, 200000, 'Retry-After time must be equal to Retry-After set in header')
self.assertFalse(bo.global_ratelimit, 'global_ratelimit must be False') self.assertFalse(bo.global_ratelimit, 'global_ratelimit must be False')
raise bo raise bo
@ -420,32 +422,34 @@ class DiscordManagerTestCase(TestCase):
self.assertTrue(datetime.datetime.strptime(args[1], manager.cache_time_format) > datetime.datetime.now()) self.assertTrue(datetime.datetime.strptime(args[1], manager.cache_time_format) > datetime.datetime.now())
@mock.patch(MODULE_PATH + '.manager.cache') @mock.patch(MODULE_PATH + '.manager.cache')
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._get_groups') @mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._get_user_roles')
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._group_name_to_id')
@requests_mock.Mocker() @requests_mock.Mocker()
def test_update_groups_global_backoff(self, group_cache, djcache, m): def test_update_groups_global_backoff(self, name_to_id, user_groups, djcache, m):
from . import manager from . import manager
# Arrange # Arrange
groups = ['Member'] groups = ['Member']
group_cache.return_value = [{'id': 111, 'name': 'Member'}] user_groups.return_value = []
name_to_id.return_value = '111'
headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN} headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
user_id = 12345 user_id = 12345
request_url = '{}/guilds/{}/members/{}'.format(manager.DISCORD_URL, settings.DISCORD_GUILD_ID, user_id) request_url = '{}/guilds/{}/members/{}/roles/{}'.format(manager.DISCORD_URL, settings.DISCORD_GUILD_ID, user_id, name_to_id.return_value)
djcache.get.return_value = None # No existing backoffs in cache djcache.get.return_value = None # No existing backoffs in cache
m.patch(request_url, m.put(request_url,
request_headers=headers, request_headers=headers,
headers={'Retry-After': '200', 'X-RateLimit-Global': 'true'}, headers={'Retry-After': '200000', 'X-RateLimit-Global': 'true'},
status_code=429) status_code=429)
# Act & Assert # Act & Assert
with self.assertRaises(manager.DiscordApiBackoff) as bo: with self.assertRaises(manager.DiscordApiBackoff) as bo:
try: try:
DiscordOAuthManager.update_groups(user_id, groups, blocking=False) DiscordOAuthManager.update_groups(user_id, groups, blocking=False)
except manager.DiscordApiBackoff as bo: except manager.DiscordApiBackoff as bo:
self.assertEqual(bo.retry_after, 200, 'Retry-After time must be equal to Retry-After set in header') self.assertEqual(bo.retry_after, 200000, 'Retry-After time must be equal to Retry-After set in header')
self.assertTrue(bo.global_ratelimit, 'global_ratelimit must be True') self.assertTrue(bo.global_ratelimit, 'global_ratelimit must be True')
raise bo raise bo

View File

@ -8,7 +8,7 @@ from hashlib import md5
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
GROUP_CACHE_MAX_AGE = int(getattr(settings, 'DISCOURSE_GROUP_CACHE_MAX_AGE', 2 * 60 * 60)) # default 2 hours GROUP_CACHE_MAX_AGE = getattr(settings, 'DISCOURSE_GROUP_CACHE_MAX_AGE', 2 * 60 * 60) # default 2 hours
class DiscourseError(Exception): class DiscourseError(Exception):
@ -24,7 +24,7 @@ class DiscourseError(Exception):
ENDPOINTS = { ENDPOINTS = {
'groups': { 'groups': {
'list': { 'list': {
'path': "/admin/groups.json", 'path': "/groups/search.json",
'method': 'get', 'method': 'get',
'args': { 'args': {
'required': [], 'required': [],
@ -214,7 +214,7 @@ class DiscourseManager:
def get_or_create_group(): def get_or_create_group():
groups = DiscourseManager._get_groups() groups = DiscourseManager._get_groups()
for g in groups: for g in groups:
if g['name'] == name: if g['name'].lower() == name.lower():
return g['id'] return g['id']
return DiscourseManager._create_group(name)['id'] return DiscourseManager._create_group(name)['id']

View File

@ -78,7 +78,7 @@ class SeatManager:
@classmethod @classmethod
def enable_user(cls, username): def enable_user(cls, username):
""" Enable user """ """ Enable user """
ret = cls.exec_request('user/{}'.format(username), 'put', active=1) ret = cls.exec_request('user/{}'.format(username), 'put', account_status=1)
logger.debug(ret) logger.debug(ret)
if cls._response_ok(ret): if cls._response_ok(ret):
logger.info("Enabled SeAT user with username %s" % username) logger.info("Enabled SeAT user with username %s" % username)
@ -86,6 +86,18 @@ class SeatManager:
logger.info("Failed to enabled SeAT user with username %s" % username) logger.info("Failed to enabled SeAT user with username %s" % username)
return None return None
@classmethod
def disable_user(cls, username):
""" Disable user """
cls.update_roles(username, [])
ret = cls.exec_request('user/{}'.format(username), 'put', account_status=0)
logger.debug(ret)
if cls._response_ok(ret):
logger.info("Disabled SeAT user with username %s" % username)
return username
logger.info("Failed to disable SeAT user with username %s" % username)
return None
@classmethod @classmethod
def _check_email_changed(cls, username, email): def _check_email_changed(cls, username, email):
"""Compares email to one set on SeAT""" """Compares email to one set on SeAT"""

View File

@ -28,7 +28,7 @@ class SeatTasks:
@classmethod @classmethod
def delete_user(cls, user, notify_user=False): def delete_user(cls, user, notify_user=False):
if cls.has_account(user) and SeatManager.delete_user(user.seat.username): if cls.has_account(user) and SeatManager.disable_user(user.seat.username):
user.seat.delete() user.seat.delete()
logger.info("Successfully deactivated SeAT for user %s" % user) logger.info("Successfully deactivated SeAT for user %s" % user)
if notify_user: if notify_user:

View File

@ -100,10 +100,10 @@ class SeatHooksTestCase(TestCase):
# Test none user is deleted # Test none user is deleted
none_user = User.objects.get(username=self.none_user) none_user = User.objects.get(username=self.none_user)
manager.delete_user.return_value = 'abc123' manager.disable_user.return_value = 'abc123'
SeatUser.objects.create(user=none_user, username='abc123') SeatUser.objects.create(user=none_user, username='abc123')
service.validate_user(none_user) service.validate_user(none_user)
self.assertTrue(manager.delete_user.called) self.assertTrue(manager.disable_user.called)
with self.assertRaises(ObjectDoesNotExist): with self.assertRaises(ObjectDoesNotExist):
none_seat = User.objects.get(username=self.none_user).seat none_seat = User.objects.get(username=self.none_user).seat
@ -115,7 +115,7 @@ class SeatHooksTestCase(TestCase):
result = service.delete_user(member) result = service.delete_user(member)
self.assertTrue(result) self.assertTrue(result)
self.assertTrue(manager.delete_user.called) self.assertTrue(manager.disable_user.called)
with self.assertRaises(ObjectDoesNotExist): with self.assertRaises(ObjectDoesNotExist):
seat_user = User.objects.get(username=self.member).seat seat_user = User.objects.get(username=self.member).seat
@ -177,7 +177,7 @@ class SeatViewsTestCase(TestCase):
response = self.client.get(urls.reverse('auth_deactivate_seat')) response = self.client.get(urls.reverse('auth_deactivate_seat'))
self.assertTrue(manager.delete_user.called) self.assertTrue(manager.disable_user.called)
self.assertRedirects(response, expected_url=urls.reverse('auth_services'), target_status_code=200) self.assertRedirects(response, expected_url=urls.reverse('auth_services'), target_status_code=200)
with self.assertRaises(ObjectDoesNotExist): with self.assertRaises(ObjectDoesNotExist):
seat_user = User.objects.get(pk=self.member.pk).seat seat_user = User.objects.get(pk=self.member.pk).seat

View File

@ -224,7 +224,7 @@ def srp_request_view(request, fleet_srp):
try: try:
srp_kill_link = srpManager.get_kill_id(srp_request.killboard_link) srp_kill_link = srpManager.get_kill_id(srp_request.killboard_link)
(ship_type_id, ship_value, victim_name) = srpManager.get_kill_data(srp_kill_link) (ship_type_id, ship_value, victim_id) = srpManager.get_kill_data(srp_kill_link)
except ValueError: except ValueError:
logger.debug("User %s Submitted Invalid Killmail Link %s or server could not be reached" % ( logger.debug("User %s Submitted Invalid Killmail Link %s or server could not be reached" % (
request.user, srp_request.killboard_link)) request.user, srp_request.killboard_link))
@ -235,7 +235,7 @@ def srp_request_view(request, fleet_srp):
characters = EveManager.get_characters_by_owner_id(request.user.id) characters = EveManager.get_characters_by_owner_id(request.user.id)
for character in characters: for character in characters:
if character.character_name == victim_name: if character.character_id == str(victim_id):
srp_request.srp_ship_name = EveManager.get_itemtype(ship_type_id).name srp_request.srp_ship_name = EveManager.get_itemtype(ship_type_id).name
srp_request.kb_total_loss = ship_value srp_request.kb_total_loss = ship_value
srp_request.post_time = post_time srp_request.post_time = post_time
@ -247,8 +247,8 @@ def srp_request_view(request, fleet_srp):
else: else:
continue continue
messages.error(request, messages.error(request,
_("%(charname)s does not belong to your Auth account. Please add the API key for this character and try again") _("Character ID %(charid)s does not belong to your Auth account. Please add the API key for this character and try again")
% {"charname": victim_name}) % {"charid": victim_id})
return redirect("auth_srp_management_view") return redirect("auth_srp_management_view")
else: else:
logger.debug("Returning blank SrpFleetUserRequestForm") logger.debug("Returning blank SrpFleetUserRequestForm")

View File

@ -4,6 +4,7 @@
<head lang="en"> <head lang="en">
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{{ SITE_NAME }}</title> <title>{{ SITE_NAME }}</title>
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
<style> <style>
html { html {
background: url('{% static 'img/index_images/index_blank_bg.jpg' %}') no-repeat scroll; background: url('{% static 'img/index_images/index_blank_bg.jpg' %}') no-repeat scroll;
@ -22,6 +23,7 @@
margin-top: -100px; margin-top: -100px;
margin-left: -200px; margin-left: -200px;
} }
#logo { #logo {
height: 200px; height: 200px;
width: 900px; width: 900px;
@ -31,9 +33,18 @@
margin-top: -100px; margin-top: -100px;
margin-left: -450px; margin-left: -450px;
} }
img { img {
border: 0; border: 0;
} }
a:link, a:hover, a:visited, a:active {
font-family: 'Roboto', sans-serif;
color: #ffffff;
font-size: 36px;
padding: 10px 20px 10px 20px;
text-decoration: none;
}
</style> </style>
</head> </head>
<body> <body>
@ -44,29 +55,31 @@
</div> </div>
<div id="content"> <div id="content">
<p style="text-align:center"> <p style="text-align:center">
<a href="/dashboard/"> <a href="/dashboard/">auth</a>
<img src="{% static 'img/index_images/auth.png' %}" alt="Auth">
</a>
</p> </p>
{% if FORUM_URL %} {% if FORUM_URL %}
<p style="text-align:center"> <p style="text-align:center">
<a href="{{FORUM_URL}}"> <a href="{{FORUM_URL}}">forum</a>
<img src="{% static 'img/index_images/forums.png' %}" alt="Forums"> </p>
</a> {% endif %}
{% if MARKET_URL %}
<p style="text-align:center">
<a href="{{MARKET_URL}}">market</a>
</p>
{% endif %}
{% if SEAT_URL %}
<p style="text-align:center">
<a href="{{SEAT_URL}}">seat</a>
</p> </p>
{% endif %} {% endif %}
{% if KILLBOARD_URL %} {% if KILLBOARD_URL %}
<p style="text-align:center"> <p style="text-align:center">
<a href="{{KILLBOARD_URL}}"> <a href="{{KILLBOARD_URL}}">killboard</a>
<img src="{% static 'img/index_images/killboard.png' %}" alt="Killboard">
</a>
</p> </p>
{% endif %} {% endif %}
{% if EXTERNAL_MEDIA_URL %} {% if EXTERNAL_MEDIA_URL %}
<p style="text-align:center"> <p style="text-align:center">
<a href="{{EXTERNAL_MEDIA_URL}}"> <a href="{{EXTERNAL_MEDIA_URL}}">media</a>
<img src="{% static 'img/index_images/media.png' %}" alt="External Media">
</a>
</p> </p>
{% endif %} {% endif %}
</div> </div>

View File

@ -20,7 +20,7 @@
<div cass="text-center">{{ question.help_text }}</div> <div cass="text-center">{{ question.help_text }}</div>
{% endif %} {% endif %}
{% for choice in question.choices.all %} {% for choice in question.choices.all %}
<input type="radio" name="{{ question.pk }}" id="id_{{ question.pk }}" value="{{ choice.choice_text }}" /> <input type={% if question.multi_select == False %}"radio"{% else %}"checkbox"{% endif %} name="{{ question.pk }}" id="id_{{ question.pk }}" value="{{ choice.choice_text }}" />
<label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br /> <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br />
{% empty %} {% empty %}
<textarea class="form-control" cols="30" id="id_{{ question.pk }}" name="{{ question.pk }}" rows="4"></textarea> <textarea class="form-control" cols="30" id="id_{{ question.pk }}" name="{{ question.pk }}" rows="4"></textarea>

View File

@ -118,9 +118,7 @@
<h4 class="panel-title"> <h4 class="panel-title">
<a class="collapsed" data-toggle="collapse" data-parent="#accordion" <a class="collapsed" data-toggle="collapse" data-parent="#accordion"
href="#collapseThree" aria-expanded="false" href="#collapseThree" aria-expanded="false"
aria-controls="collapseThree"> aria-controls="collapseThree">{% trans "Comments" %} - {{ comments|length }}</a>
{% blocktrans %}Comments - {{ comments|length }}{% endblocktrans %}
</a>
</h4> </h4>
</div> </div>
<div id="collapseThree" class="panel-collapse collapse" role="tabpanel" <div id="collapseThree" class="panel-collapse collapse" role="tabpanel"

View File

@ -120,7 +120,6 @@
Engineering Complex [XL] Engineering Complex [XL]
</div> </div>
{% endifequal %} {% endifequal %}
{% ifequal timer.structure "Station" %} {% ifequal timer.structure "Station" %}
<div class="label label-danger"> <div class="label label-danger">
Station Station
@ -131,6 +130,21 @@
TCU TCU
</div> </div>
{% endifequal %} {% endifequal %}
{% ifequal timer.structure "Refinery[M]" %}
<div class="label label-warning">
Refinery [M]
</div>
{% endifequal %}
{% ifequal timer.structure "Refinery[L]" %}
<div class="label label-warning">
Refinery [L]
</div>
{% endifequal %}
{% ifequal timer.structure "Moon Mining Cycle" %}
<div class="label label-success">
Moon Mining Cycle
</div>
{% endifequal %}
{% ifequal timer.structure "Other" %} {% ifequal timer.structure "Other" %}
<div class="label label-default"> <div class="label label-default">
Other Other
@ -265,6 +279,21 @@
TCU TCU
</div> </div>
{% endifequal %} {% endifequal %}
{% ifequal timer.structure "Refinery[M]" %}
<div class="label label-warning">
Refinery [M]
</div>
{% endifequal %}
{% ifequal timer.structure "Refinery[L]" %}
<div class="label label-warning">
Refinery [L]
</div>
{% endifequal %}
{% ifequal timer.structure "Moon Mining Cycle" %}
<div class="label label-success">
Moon Mining Cycle
</div>
{% endifequal %}
{% ifequal timer.structure "Other" %} {% ifequal timer.structure "Other" %}
<div class="label label-default"> <div class="label label-default">
Other Other
@ -400,6 +429,21 @@
TCU TCU
</div> </div>
{% endifequal %} {% endifequal %}
{% ifequal timer.structure "Refinery[M]" %}
<div class="label label-warning">
Refinery [M]
</div>
{% endifequal %}
{% ifequal timer.structure "Refinery[L]" %}
<div class="label label-warning">
Refinery [L]
</div>
{% endifequal %}
{% ifequal timer.structure "Moon Mining Cycle" %}
<div class="label label-success">
Moon Mining Cycle
</div>
{% endifequal %}
{% ifequal timer.structure "Other" %} {% ifequal timer.structure "Other" %}
<div class="label label-default"> <div class="label label-default">
Other Other

View File

@ -5,14 +5,26 @@ from django.utils.translation import ugettext_lazy as _
class TimerForm(forms.Form): class TimerForm(forms.Form):
structure_choices = [('POCO', 'POCO'), ('I-HUB', 'I-HUB'), ('POS[S]', 'POS[S]'), structure_choices = [('POCO', 'POCO'),
('POS[M]', 'POS[M]'), ('POS[L]', 'POS[L]'), ('Citadel[M]', 'Citadel[M]'), ('I-HUB', 'I-HUB'),
('Citadel[L]', 'Citadel[L]'), ('Citadel[XL]', 'Citadel[XL]'), ('POS[S]', 'POS[S]'),
('POS[M]', 'POS[M]'),
('POS[L]', 'POS[L]'),
('Citadel[M]', 'Citadel[M]'),
('Citadel[L]', 'Citadel[L]'),
('Citadel[XL]', 'Citadel[XL]'),
('Engineering Complex[M]', 'Engineering Complex[M]'), ('Engineering Complex[M]', 'Engineering Complex[M]'),
('Engineering Complex[L]', 'Engineering Complex[L]'), ('Engineering Complex[L]', 'Engineering Complex[L]'),
('Engineering Complex[XL]', 'Engineering Complex[XL]'), ('Engineering Complex[XL]', 'Engineering Complex[XL]'),
('Station', 'Station'), ('TCU', 'TCU'), (_('Other'), _('Other'))] ('Refinery[M]', 'Refinery[M]'),
objective_choices = [('Friendly', _('Friendly')), ('Hostile', _('Hostile')), ('Neutral', _('Neutral'))] ('Refinery[L]', 'Refinery[L]'),
('Station', 'Station'),
('TCU', 'TCU'),
('Moon Mining Cycle', 'Moon Mining Cycle'),
(_('Other'), _('Other'))]
objective_choices = [('Friendly', _('Friendly')),
('Hostile', _('Hostile')),
('Neutral', _('Neutral'))]
details = forms.CharField(max_length=254, required=True, label=_('Details')) details = forms.CharField(max_length=254, required=True, label=_('Details'))
system = forms.CharField(max_length=254, required=True, label=_("System")) system = forms.CharField(max_length=254, required=True, label=_("System"))