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.
from .celeryapp import app as celery_app # noqa
__version__ = '1.15.4'
__version__ = '1.15.8'
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_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_SECRET - oauth app secret
# 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_BOT_TOKEN = os.environ.get('AA_DISCORD_BOT_TOKEN', '')
DISCORD_INVITE_CODE = os.environ.get('AA_DISCORD_INVITE_CODE', '')
DISCORD_APP_ID = os.environ.get('AA_DISCORD_APP_ID', '')
DISCORD_APP_SECRET = os.environ.get('AA_DISCORD_APP_SECRET', '')
DISCORD_CALLBACK_URL = os.environ.get('AA_DISCORD_CALLBACK_URL', 'http://example.com/discord/callback')

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:
git tag | sort -n | tail -1 | xargs git checkout
git checkout v1.15.7
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
### 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

@ -46,7 +46,7 @@ Enter the folder by issuing `cd allianceauth`
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:

View File

@ -21,13 +21,6 @@ with a server ID of `120631096835571712`
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
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 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):
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):
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

@ -39,13 +39,14 @@ def hr_application_management_view(request):
except EveCharacter.DoesNotExist:
pass
if request.user.is_superuser:
corp_applications = Application.objects.filter(approved=None)
finished_corp_applications = Application.objects.exclude(approved=None)
corp_applications = Application.objects.filter(approved=None).order_by('-created')
finished_corp_applications = Application.objects.exclude(approved=None).order_by('-created')
elif request.user.has_perm('auth.human_resources') and main_char:
if ApplicationForm.objects.filter(corp__corporation_id=main_char.corporation_id).exists():
app_form = ApplicationForm.objects.get(corp__corporation_id=main_char.corporation_id)
corp_applications = Application.objects.filter(form=app_form).filter(approved=None)
finished_corp_applications = Application.objects.filter(form=app_form).filter(approved__in=[True, False])
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]).order_by('-created')
logger.debug("Retrieved %s personal, %s corp applications for %s" % (
len(request.user.applications.all()), len(corp_applications), request.user))
context = {
@ -71,8 +72,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

@ -1,7 +1,7 @@
from __future__ import unicode_literals
import requests
import json
import re
import requests
import math
from django.conf import settings
from requests_oauthlib import OAuth2Session
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.
It's almost fixed the problem.
"""
# kick members, manage roles, manage nicknames
# BOT_PERMISSIONS = 0x00000002 + 0x10000000 + 0x08000000
# kick members, manage roles, manage nicknames, create instant invite
# BOT_PERMISSIONS = 0x00000002 + 0x10000000 + 0x08000000 + 0x00000001
BOT_PERMISSIONS = 0x00000008
# get user ID, accept invite
@ -34,7 +34,7 @@ SCOPES = [
'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):
@ -50,12 +50,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):
@ -109,20 +117,19 @@ def api_backoff(func):
global_ratelimit=bool(existing_global_backoff)
)
logger.debug("Calling API calling function")
func(*args, **kwargs)
break
return func(*args, **kwargs)
except requests.HTTPError as e:
if e.response.status_code == 429:
try:
retry_after = int(e.response.headers['Retry-After'])
except (TypeError, KeyError):
# Pick some random time
retry_after = 5
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 +145,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)
@ -155,12 +162,11 @@ class DiscordOAuthManager:
@staticmethod
def _sanitize_name(name):
return re.sub('[^\w.-]', '', name)[:32]
return name[:32]
@staticmethod
def _sanitize_groupname(name):
name = name.strip(' _')
return DiscordOAuthManager._sanitize_name(name)
def _sanitize_group_name(name):
return name[:100]
@staticmethod
def generate_bot_add_url():
@ -179,23 +185,33 @@ class DiscordOAuthManager:
return token
@staticmethod
def add_user(code):
def add_user(code, groups, nickname=None):
try:
token = DiscordOAuthManager._process_callback_code(code)['access_token']
logger.debug("Received token from OAuth")
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"
r = requests.get(path, headers=custom_headers)
logger.debug("Got status code %s after retrieving Discord profile" % r.status_code)
r.raise_for_status()
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)
return user_id
except:
@ -203,6 +219,7 @@ class DiscordOAuthManager:
return None
@staticmethod
@api_backoff
def update_nickname(user_id, nickname):
try:
nickname = DiscordOAuthManager._sanitize_name(nickname)
@ -252,7 +269,7 @@ class DiscordOAuthManager:
@staticmethod
def _group_name_to_id(name):
name = DiscordOAuthManager._sanitize_groupname(name)
name = DiscordOAuthManager._sanitize_group_name(name)
def get_or_make_role():
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)
@staticmethod
def __generate_role():
def __generate_role(name, **kwargs):
custom_headers = {'accept': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
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)
r.raise_for_status()
return r.json()
@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}
data = {
'color': color,
'hoist': hoist,
'name': name,
'permissions': permissions,
}
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))
r.raise_for_status()
return r.json()
@staticmethod
def _create_group(name):
role = DiscordOAuthManager.__generate_role()
return DiscordOAuthManager.__edit_role(role['id'], name)
return DiscordOAuthManager.__generate_role(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
@api_backoff
def update_groups(user_id, groups):
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
group_ids = [DiscordOAuthManager._group_name_to_id(DiscordOAuthManager._sanitize_groupname(g)) for g in groups]
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id)
data = {'roles': group_ids}
r = requests.patch(path, headers=custom_headers, json=data)
logger.debug("Received status code %s after setting user roles" % r.status_code)
r.raise_for_status()
group_ids = [DiscordOAuthManager._group_name_to_id(DiscordOAuthManager._sanitize_group_name(g)) for g in groups]
user_group_ids = DiscordOAuthManager._get_user_roles(user_id)
for g in group_ids:
if g not in user_group_ids:
DiscordOAuthManager._modify_user_role(user_id, g, 'put')
time.sleep(1) # we're gonna be hammering the API here
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.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from requests.exceptions import HTTPError
from eveonline.managers import EveManager
from notifications import notify
from services.modules.discord.manager import DiscordOAuthManager, DiscordApiBackoff
@ -21,15 +21,16 @@ class DiscordTasks:
@classmethod
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:
discord_user = DiscordUser()
discord_user.user = user
discord_user.uid = user_id
discord_user.save()
if settings.DISCORD_SYNC_NAMES:
cls.update_nickname.delay(user.pk)
cls.update_groups.delay(user.pk)
return True
return False
@ -64,19 +65,23 @@ class DiscordTasks:
user = User.objects.get(pk=pk)
logger.debug("Updating discord groups for user %s" % user)
if DiscordTasks.has_account(user):
groups = []
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')
groups = DiscordTasks.get_groups(user)
logger.debug("Updating user %s discord groups to %s" % (user, groups))
try:
DiscordOAuthManager.update_groups(user.discord.uid, groups)
except DiscordApiBackoff as bo:
logger.info("Discord group sync API back off for %s, "
"retrying in %s seconds" % (user, bo.retry_after))
raise task_self.retry(countdown=bo.retry_after)
"retrying in %s seconds" % (user, bo.retry_after_seconds))
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:
if task_self:
logger.exception("Discord group sync failed for %s, retrying in 10 mins" % user)
@ -97,18 +102,22 @@ class DiscordTasks:
@staticmethod
@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)
logger.debug("Updating discord nickname for user %s" % user)
if DiscordTasks.has_account(user):
character = EveManager.get_main_character(user)
logger.debug("Updating user %s discord nickname to %s" % (user, character.character_name))
name = DiscordTasks.get_nickname(user)
logger.debug("Updating user %s discord nickname to %s" % (user, name))
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:
if self:
if task_self:
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:
# Rethrow
raise e
@ -126,3 +135,11 @@ class DiscordTasks:
@classmethod
def disable(cls):
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):
pass
def test__sanitize_groupname(self):
test_group_name = ' Group Name_Test_'
group_name = DiscordOAuthManager._sanitize_groupname(test_group_name)
def test__sanitize_group_name(self):
test_group_name = str(10**103)
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):
from . import manager
@ -267,18 +267,20 @@ class DiscordManagerTestCase(TestCase):
headers = {'accept': 'application/json', 'authorization': 'Bearer accesstoken'}
m.register_uri('POST',
manager.DISCORD_URL + '/invites/'+str(settings.DISCORD_INVITE_CODE),
request_headers=headers,
text='{}')
m.register_uri('GET',
manager.DISCORD_URL + "/users/@me",
request_headers=headers,
text=json.dumps({'id': "123456"}))
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
return_value = DiscordOAuthManager.add_user('abcdef')
return_value = DiscordOAuthManager.add_user('abcdef', [])
# Assert
self.assertEqual(return_value, '123456')
@ -351,58 +353,58 @@ class DiscordManagerTestCase(TestCase):
# Assert
self.assertTrue(result)
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._get_user_roles')
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._get_groups')
@requests_mock.Mocker()
def test_update_groups(self, group_cache, m):
def test_update_groups(self, group_cache, user_roles, m):
from . import manager
import json
# Arrange
groups = ['Member', 'Blue', 'SpecialGroup']
group_cache.return_value = [{'id': 111, 'name': 'Member'},
{'id': 222, 'name': 'Blue'},
{'id': 333, 'name': 'SpecialGroup'},
{'id': 444, 'name': 'NotYourGroup'}]
group_cache.return_value = [{'id': '111', 'name': 'Member'},
{'id': '222', 'name': 'Blue'},
{'id': '333', 'name': 'SpecialGroup'},
{'id': '444', 'name': 'NotYourGroup'}]
user_roles.return_value = ['444']
headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
user_id = 12345
request_url = '{}/guilds/{}/members/{}'.format(manager.DISCORD_URL, settings.DISCORD_GUILD_ID, user_id)
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,
request_headers=headers)
m.patch(user_request_url, 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
DiscordOAuthManager.update_groups(user_id, groups)
# Assert
self.assertEqual(len(m.request_history), 1, 'Must be one HTTP call made')
history = json.loads(m.request_history[0].text)
self.assertIn('roles', history, "'The request must send JSON object with the 'roles' key")
self.assertIn(111, history['roles'], 'The group id 111 must be added to the request')
self.assertIn(222, history['roles'], 'The group id 222 must be added to the request')
self.assertIn(333, history['roles'], 'The group id 333 must be added to the request')
self.assertNotIn(444, history['roles'], 'The group id 444 must NOT be added to the request')
self.assertEqual(len(m.request_history), 4, 'Must be 4 HTTP calls made')
@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()
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
# Arrange
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}
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
m.patch(request_url,
m.put(request_url,
request_headers=headers,
headers={'Retry-After': '200'},
headers={'Retry-After': '200000'},
status_code=429)
# Act & Assert
@ -410,7 +412,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
@ -420,24 +422,26 @@ class DiscordManagerTestCase(TestCase):
self.assertTrue(datetime.datetime.strptime(args[1], manager.cache_time_format) > datetime.datetime.now())
@mock.patch(MODULE_PATH + '.manager.cache')
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._get_groups')
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._get_user_roles')
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._group_name_to_id')
@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
# Arrange
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}
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
m.patch(request_url,
m.put(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 +449,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

@ -8,7 +8,7 @@ from hashlib import md5
logger = logging.getLogger(__name__)
GROUP_CACHE_MAX_AGE = int(getattr(settings, 'DISCOURSE_GROUP_CACHE_MAX_AGE', 2 * 60 * 60)) # default 2 hours
GROUP_CACHE_MAX_AGE = getattr(settings, 'DISCOURSE_GROUP_CACHE_MAX_AGE', 2 * 60 * 60) # default 2 hours
class DiscourseError(Exception):
@ -24,7 +24,7 @@ class DiscourseError(Exception):
ENDPOINTS = {
'groups': {
'list': {
'path': "/admin/groups.json",
'path': "/groups/search.json",
'method': 'get',
'args': {
'required': [],
@ -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>

View File

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

View File

@ -120,7 +120,6 @@
Engineering Complex [XL]
</div>
{% endifequal %}
{% ifequal timer.structure "Station" %}
<div class="label label-danger">
Station
@ -131,6 +130,21 @@
TCU
</div>
{% 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" %}
<div class="label label-default">
Other
@ -265,6 +279,21 @@
TCU
</div>
{% 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" %}
<div class="label label-default">
Other
@ -400,6 +429,21 @@
TCU
</div>
{% 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" %}
<div class="label label-default">
Other

View File

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