mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2025-07-21 18:22:27 +02:00
Compare commits
34 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
26eebec918 | ||
|
072d1b9db6 | ||
|
8c957e9cb7 | ||
|
69a686a98a | ||
|
c69b41738b | ||
|
a096023553 | ||
|
5eecee49f5 | ||
|
d8f4d56dd8 | ||
|
d58ac8a718 | ||
|
d503243e12 | ||
|
5962f0f29f | ||
|
a2f4226381 | ||
|
1ce041b90a | ||
|
91ec924acc | ||
|
0f1535161c | ||
|
1caa4b6baa | ||
|
0474fa6d17 | ||
|
e1907d9d17 | ||
|
2e214e442c | ||
|
0d64441538 | ||
|
58a333c67a | ||
|
6837f94e59 | ||
|
16987fcaf0 | ||
|
ebd3be3f46 | ||
|
a02e5f400a | ||
|
65c168939d | ||
|
313cac6ac7 | ||
|
0145ea82c8 | ||
|
0cdc5ffbd5 | ||
|
0bdd044378 | ||
|
ad266ea2ee | ||
|
7ea8c9e50d | ||
|
9a015fd582 | ||
|
7ca1c87c87 |
@ -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__
|
||||||
|
@ -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
@ -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:
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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
@ -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
@ -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}
|
||||||
|
@ -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]
|
||||||
|
|
||||||
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
@ -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
|
||||||
|
@ -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')
|
||||||
|
@ -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')
|
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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")
|
||||||
|
@ -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)
|
||||||
|
@ -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()]
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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']
|
||||||
|
|
||||||
|
@ -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"""
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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")
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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"))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user