Compare commits

...

15 Commits

Author SHA1 Message Date
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
26 changed files with 189 additions and 58 deletions

View File

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

File diff suppressed because one or more lines are too long

View File

@ -25,6 +25,17 @@ Required for displaying web content
apache2 libapache2-mod-php5 libapache2-mod-wsgi
### 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
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

@ -65,6 +65,11 @@ Now build:
sudo ./launcher bootstrap 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
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:
sudo a2ensite discourse
sudo a2enmod proxy_http
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
### 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_KEY`: the key you just generated
***
### 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:
@ -120,6 +176,7 @@ Save, now change settings.py and add the following:
### 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

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'),)
def __str__(self):
output = "Fat-link for %s" % self.character.character_name
return output.encode('utf-8')
return "Fat-link for %s" % self.character.character_name

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()
self.blue = self.corp.is_blue
@property
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):
@ -70,8 +74,12 @@ class MemberStat(object):
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()
@property
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):
@ -132,7 +140,7 @@ def fatlink_statistics_corp_view(request, corpid, year=None, month=None):
# 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)
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,
'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
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.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,
'previous_month': start_of_previous_month}

View File

@ -16,7 +16,7 @@ class ChoiceInline(admin.TabularInline):
class QuestionAdmin(admin.ModelAdmin):
fieldsets = [
(None, {'fields': ['title', 'help_text']}),
(None, {'fields': ['title', 'help_text', 'multi_select']}),
]
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):
title = models.CharField(max_length=254, verbose_name='Question')
help_text = models.CharField(max_length=254, blank=True, null=True)
multi_select = models.BooleanField(default=False)
def __str__(self):
return "Question: " + self.title

View File

@ -71,8 +71,8 @@ def hr_application_create_view(request, form_id=None):
application.save()
for question in app_form.questions.all():
response = ApplicationResponse(question=question, application=application)
response.answer = request.POST.get(str(question.pk),
"Failed to retrieve answer provided by applicant.")
response.answer = "\n".join(request.POST.getlist(str(question.pk),
""))
response.save()
logger.info("%s created %s" % (request.user, application))
return redirect('auth_hrapplications_view')

View File

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

View File

@ -5,7 +5,7 @@ dnspython
passlib
requests>=2.9.1
bcrypt
slugify
python-slugify>=1.2
requests-oauthlib
sleekxmpp
redis
@ -23,4 +23,4 @@ django-celery-beat
# awating pyghassen/openfire-restapi #1 to fix installation issues
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,
'SMF_URL': settings.SMF_URL,
'MARKET_URL': settings.MARKET_URL,
'SEAT_URL': settings.SEAT_URL,
'EXTERNAL_MEDIA_URL': settings.EXTERNAL_MEDIA_URL,
'CURRENT_UTC_TIME': timezone.now(),
'BLUE_API_MASK': settings.BLUE_API_MASK,

View File

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

View File

@ -2,6 +2,7 @@ from __future__ import unicode_literals
import requests
import json
import re
import math
from django.conf import settings
from requests_oauthlib import OAuth2Session
from functools import wraps
@ -50,12 +51,20 @@ class DiscordApiTooBusy(DiscordApiException):
class DiscordApiBackoff(DiscordApiException):
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__()
self.retry_after = retry_after
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):
@ -117,12 +126,12 @@ def api_backoff(func):
retry_after = int(e.response.headers['Retry-After'])
except (TypeError, KeyError):
# Pick some random time
retry_after = 5
retry_after = 5000
logger.info("Received backoff from API of %s seconds, handling" % retry_after)
# Store value in redis
backoff_until = (datetime.datetime.utcnow() +
datetime.timedelta(seconds=retry_after))
datetime.timedelta(milliseconds=retry_after))
global_backoff = bool(e.response.headers.get('X-RateLimit-Global', False))
if global_backoff:
logger.info("Global backoff!!")
@ -138,7 +147,7 @@ def api_backoff(func):
# Sleep if we're blocking
if blocking:
logger.info("Blocking Back off from API calls for %s seconds" % bo.retry_after)
time.sleep(10 if bo.retry_after > 10 else bo.retry_after)
time.sleep((10 if bo.retry_after > 10 else bo.retry_after) / 1000)
else:
# Otherwise raise exception and let caller handle the backoff
raise DiscordApiBackoff(retry_after=bo.retry_after, global_ratelimit=bo.global_ratelimit)

View File

@ -75,8 +75,8 @@ class DiscordTasks:
DiscordOAuthManager.update_groups(user.discord.uid, groups)
except DiscordApiBackoff as bo:
logger.info("Discord group sync API back off for %s, "
"retrying in %s seconds" % (user, bo.retry_after))
raise task_self.retry(countdown=bo.retry_after)
"retrying in %s seconds" % (user, bo.retry_after_seconds))
raise task_self.retry(countdown=bo.retry_after_seconds)
except Exception as e:
if task_self:
logger.exception("Discord group sync failed for %s, retrying in 10 mins" % user)

View File

@ -402,7 +402,7 @@ class DiscordManagerTestCase(TestCase):
m.patch(request_url,
request_headers=headers,
headers={'Retry-After': '200'},
headers={'Retry-After': '200000'},
status_code=429)
# Act & Assert
@ -410,7 +410,7 @@ class DiscordManagerTestCase(TestCase):
try:
DiscordOAuthManager.update_groups(user_id, groups, blocking=False)
except manager.DiscordApiBackoff as bo:
self.assertEqual(bo.retry_after, 200, 'Retry-After time must be equal to Retry-After set in header')
self.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')
raise bo
@ -437,7 +437,7 @@ class DiscordManagerTestCase(TestCase):
m.patch(request_url,
request_headers=headers,
headers={'Retry-After': '200', 'X-RateLimit-Global': 'true'},
headers={'Retry-After': '200000', 'X-RateLimit-Global': 'true'},
status_code=429)
# Act & Assert
@ -445,7 +445,7 @@ class DiscordManagerTestCase(TestCase):
try:
DiscordOAuthManager.update_groups(user_id, groups, blocking=False)
except manager.DiscordApiBackoff as bo:
self.assertEqual(bo.retry_after, 200, 'Retry-After time must be equal to Retry-After set in header')
self.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')
raise bo

View File

@ -214,7 +214,7 @@ class DiscourseManager:
def get_or_create_group():
groups = DiscourseManager._get_groups()
for g in groups:
if g['name'] == name:
if g['name'].lower() == name.lower():
return g['id']
return DiscourseManager._create_group(name)['id']

View File

@ -78,7 +78,7 @@ class SeatManager:
@classmethod
def enable_user(cls, username):
""" 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)
if cls._response_ok(ret):
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)
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
def _check_email_changed(cls, username, email):
"""Compares email to one set on SeAT"""

View File

@ -28,7 +28,7 @@ class SeatTasks:
@classmethod
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()
logger.info("Successfully deactivated SeAT for user %s" % user)
if notify_user:

View File

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

View File

@ -224,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, 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:
logger.debug("User %s Submitted Invalid Killmail Link %s or server could not be reached" % (
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)
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.kb_total_loss = ship_value
srp_request.post_time = post_time
@ -247,8 +247,8 @@ def srp_request_view(request, fleet_srp):
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})
_("Character ID %(charid)s does not belong to your Auth account. Please add the API key for this character and try again")
% {"charid": victim_id})
return redirect("auth_srp_management_view")
else:
logger.debug("Returning blank SrpFleetUserRequestForm")

View File

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

View File

@ -20,7 +20,7 @@
<div cass="text-center">{{ question.help_text }}</div>
{% endif %}
{% for choice in question.choices.all %}
<input type="radio" name="{{ question.pk }}" id="id_{{ question.pk }}" value="{{ choice.choice_text }}" />
<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 />
{% empty %}
<textarea class="form-control" cols="30" id="id_{{ question.pk }}" name="{{ question.pk }}" rows="4"></textarea>