Compare commits

..

55 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 bdb3ab366f)
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 77c93ed96b)
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 70c2a4a6e4)
2018-02-23 13:36:12 -05:00
Adarnof
5962f0f29f Do not sanitize Discord names
(cherry-picked from commit 8ce8789631)
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 99b136b824)

Create new roles with desired attributes in one call.

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

(cherry picked from commit 3080d7d868)
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 a64dda2a2e)
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
Adarnof
eee6a9132d Use local swagger spec files (#866)
Allows auth to keep working if CCP changes "latest" definition.
Requires adarnauth-esi>=1.4
2017-09-17 01:08:02 -04:00
Basraah
9d90af4a3d Fixes #865 & adds unit tests 2017-09-13 20:16:07 +10:00
Adarnof
72305de2d8 Correct username hashing on py3 2017-09-13 00:29:59 -04:00
Adarnof
8f58f76001 Stop using v3(dev) Alliance resource
It keeps changing.
2017-09-12 20:16:15 -04:00
Adarnof
a969b6117b Fix missing operation in v3 alliance resource 2017-09-12 12:34:43 -04:00
Basraah
97762119b3 Remove .idea folder 2017-09-12 11:33:42 +10:00
Adarnof
4bdead5ef2 Version bump to v1.15.4 2017-09-11 20:52:07 -04:00
Adarnof
8987cf2199 Use Django's cache framework for service group names (#857)
Use django-redis-cache backend for locking get_or_set
No longer require group-related tasks to be locked to one simultaneous execution.
Remove legacy service group cache models.

Truncate Discord nicknames to 32 characters
Correct Discourse group name extension using only valid leading characters.
Prevent name slicing from ending with illegal character

Closes #801
Closes #847
Closes #835
Closes #852
2017-09-11 20:42:13 -04:00
Basraah
27c9b09116 Add encoding declaration to prevent #819 2017-09-11 12:59:13 +10:00
Adarnof
0ac0f71fef Correct SeAT API logic (#860)
* Do not attempt to change user email on SeAT if unchanged.
This prevents HTTP422 from being raised on password resets.

*Delete users on deactivation.
The existing disable user logic does nothing and results in a HTTP500.
It's safe to delete users entirely - the API keys are retained.

Fixes #844
2017-09-08 13:42:35 -04:00
Basraah
3f454743a9 Openfire group names fix (#859)
* Force lowercase group names

* Fix comparison of group names

* Sanitise group name for broadcast message
2017-09-05 13:12:27 -04:00
Basraah
c2f12eed26 Fixes #753 2017-09-05 14:46:38 +10:00
Basraah
1b1b692ac0 Add missing block tag 2017-09-03 09:09:48 +10:00
mmolitor87
dc8ed2d510 Supervisor celery simplification (#849)
Combines celery confs under one process group for easy restarting. Updates docs to reflect new commands.
2017-09-01 15:43:54 -04:00
Basraah
049c1c66aa Make Fleet-up datetimes timezone aware (#856)
Additionally fix logger depreciation warnings
2017-09-01 15:41:45 -04:00
Adarnof
8028660a8f Return tuple on SeAT activation failure
Addresses #844
2017-09-01 00:40:22 -04:00
ghoti
e6532025f8 fixes #848 - could be bug, could be permanent unclear change from CCP (#854) 2017-09-01 00:29:31 -04:00
cameronurnes
3361d36bbf Allow preset choices for questions in HR (#836) 2017-09-01 00:25:15 -04:00
Basraah
2ab45b1019 Fix cache type error with retry time 2017-07-26 08:59:22 +10:00
Adarnof
c5b55283d1 Version bump to v1.15.3 2017-07-19 22:07:36 -04:00
ghoti
d937d5b5d4 Use primary key instead of user object for api_refresh 2017-07-11 21:39:29 -04:00
65 changed files with 893 additions and 558 deletions

22
.idea/allianceauth.iml generated
View File

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="FacetManager">
<facet type="django" name="Django">
<configuration>
<option name="rootFolder" value="$MODULE_DIR$" />
<option name="settingsModule" value="alliance_auth/settings.py.example" />
<option name="manageScript" value="manage.py" />
<option name="environment" value="&lt;map/&gt;" />
<option name="commandsToSkip" value="" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 2.7.11 virtualenv at ~/1.6" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Django" />
</component>
</module>

12
.idea/dataSources.ids generated
View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<component name="dataSourceStorage">
<data-source source="LOCAL" name="Django default" uuid="3eb61453-647a-4832-8320-f3561f039abc">
<database-info product="" version="" jdbc-version="" driver-name="" driver-version=""/>
</data-source>
<data-source source="LOCAL" name="Django phpbb3" uuid="2de247c2-1951-4e74-8276-6a1c89c396fa">
<database-info product="" version="" jdbc-version="" driver-name="" driver-version=""/>
</data-source>
<data-source source="LOCAL" name="Django mumble" uuid="9963e5ca-7f2f-4dd3-9175-bc7102dfd48c">
<database-info product="" version="" jdbc-version="" driver-name="" driver-version=""/>
</data-source>
</component>

20
.idea/dataSources.xml generated
View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="Django default" uuid="3eb61453-647a-4832-8320-f3561f039abc">
<driver-ref>mysql</driver-ref>
<jdbc-driver>com.mysql.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://127.0.0.1:3306/alliance_auth</jdbc-url>
</data-source>
<data-source source="LOCAL" name="Django phpbb3" uuid="2de247c2-1951-4e74-8276-6a1c89c396fa">
<driver-ref>mysql</driver-ref>
<jdbc-driver>com.mysql.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://127.0.0.1:3306/alliance_forum</jdbc-url>
</data-source>
<data-source source="LOCAL" name="Django mumble" uuid="9963e5ca-7f2f-4dd3-9175-bc7102dfd48c">
<driver-ref>mysql</driver-ref>
<jdbc-driver>com.mysql.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://127.0.0.1:3306/alliance_mumble</jdbc-url>
</data-source>
</component>
</project>

5
.idea/encodings.xml generated
View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false" />
</project>

View File

@@ -1,11 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0" is_locked="false">
<option name="myName" value="Project Default" />
<option name="myLocal" value="false" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
</profile>
</component>

View File

@@ -1,7 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="PROJECT_PROFILE" />
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated
View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 2.7.11 virtualenv at ~/1.6" project-jdk-type="Python SDK" />
<component name="PythonCompatibilityInspectionAdvertiser">
<option name="version" value="1" />
</component>
</project>

9
.idea/modules.xml generated
View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/allianceauth.iml" filepath="$PROJECT_DIR$/.idea/allianceauth.iml" />
</modules>
</component>
</project>

View File

@@ -1,5 +0,0 @@
<component name="DependencyValidationManager">
<state>
<option name="SKIP_IMPORT_STATEMENTS" value="false" />
</state>
</component>

7
.idea/vcs.xml generated
View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

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

View File

@@ -1,3 +1,4 @@
# -*- coding: UTF-8 -*-
""" """
Django settings for alliance_auth project. Django settings for alliance_auth project.
@@ -210,10 +211,10 @@ MESSAGE_TAGS = {
CACHES = { CACHES = {
"default": { "default": {
"BACKEND": "django_redis.cache.RedisCache", "BACKEND": "redis_cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1", "LOCATION": "localhost:6379",
"OPTIONS": { "OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient", "DB": 1,
} }
} }
} }
@@ -481,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
@@ -489,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')

View File

@@ -11,6 +11,10 @@ from corputils.managers import CorpStatsManager
from operator import attrgetter from operator import attrgetter
import json import json
import logging import logging
import os
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -41,7 +45,7 @@ class CorpStats(models.Model):
def update(self): def update(self):
try: try:
c = self.token.get_esi_client(Character='v4', Corporation='v2') c = self.token.get_esi_client(spec_file=SWAGGER_SPEC_PATH)
assert c.Character.get_characters_character_id(character_id=self.token.character_id).result()[ assert c.Character.get_characters_character_id(character_id=self.token.character_id).result()[
'corporation_id'] == int(self.corp.corporation_id) 'corporation_id'] == int(self.corp.corporation_id)
members = c.Corporation.get_corporations_corporation_id_members( members = c.Corporation.get_corporations_corporation_id_members(
@@ -52,7 +56,6 @@ class CorpStats(models.Model):
# the swagger spec doesn't have a maxItems count # the swagger spec doesn't have a maxItems count
# manual testing says we can do over 350, but let's not risk it # manual testing says we can do over 350, but let's not risk it
member_id_chunks = [member_ids[i:i + 255] for i in range(0, len(member_ids), 255)] member_id_chunks = [member_ids[i:i + 255] for i in range(0, len(member_ids), 255)]
c = self.token.get_esi_client(Character='v1') # ccplease bump versions of whole resources
member_name_chunks = [c.Character.get_characters_names(character_ids=id_chunk).result() for id_chunk in member_name_chunks = [c.Character.get_characters_names(character_ids=id_chunk).result() for id_chunk in
member_id_chunks] member_id_chunks]
member_list = {} member_list = {}

1
corputils/swagger.json Normal file

File diff suppressed because one or more lines are too long

View File

@@ -12,6 +12,9 @@ from eveonline.models import EveCharacter, EveCorporationInfo
from corputils.models import CorpStats from corputils.models import CorpStats
from esi.decorators import token_required from esi.decorators import token_required
from bravado.exception import HTTPError from bravado.exception import HTTPError
import os
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json')
MEMBERS_PER_PAGE = int(getattr(settings, 'CORPSTATS_MEMBERS_PER_PAGE', 20)) MEMBERS_PER_PAGE = int(getattr(settings, 'CORPSTATS_MEMBERS_PER_PAGE', 20))
@@ -41,9 +44,8 @@ def corpstats_add(request, token):
if EveCharacter.objects.filter(character_id=token.character_id).exists(): if EveCharacter.objects.filter(character_id=token.character_id).exists():
corp_id = EveCharacter.objects.get(character_id=token.character_id).corporation_id corp_id = EveCharacter.objects.get(character_id=token.character_id).corporation_id
else: else:
corp_id = \ corp_id = token.get_esi_client(spec_file=SWAGGER_SPEC_PATH).Character.get_characters_character_id(
token.get_esi_client(Character='v4').Character.get_characters_character_id(character_id=token.character_id).result()[ character_id=token.character_id).result()['corporation_id']
'corporation_id']
corp = EveCorporationInfo.objects.get(corporation_id=corp_id) corp = EveCorporationInfo.objects.get(corporation_id=corp_id)
cs = CorpStats.objects.create(token=token, corp=corp) cs = CorpStats.objects.create(token=token, corp=corp)
try: try:

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ CentOS:
Auth provides example config files for the celery workers, the periodic task scheduler (celery beat), and the mumble authenticator. All of these are available in `thirdparty/Supervisor`. Auth provides example config files for the celery workers, the periodic task scheduler (celery beat), and the mumble authenticator. All of these are available in `thirdparty/Supervisor`.
For most users, all you have to do is copy the config files to `/etc/supervisor/conf.d` then restart the service. Copy `auth-celerybeat.conf` and `auth-celeryd.conf` for the celery workers, and `auth-mumble.conf` for the mumble authenticator. For all three just use a wildcard: For most users, all you have to do is copy the config files to `/etc/supervisor/conf.d` then restart the service. Copy `auth.conf` for the celery workers, and `auth-mumble.conf` for the mumble authenticator. For all three just use a wildcard:
sudo cp thirdparty/Supervisor/* /etc/supervisor/conf.d sudo cp thirdparty/Supervisor/* /etc/supervisor/conf.d
@@ -44,6 +44,24 @@ Processes will be `STARTING`, `RUNNING`, or `ERROR`. If an error has occurred, c
- celery workers: `log/worker.log` - celery workers: `log/worker.log`
- celery beat: `log/beat.log` - celery beat: `log/beat.log`
- authenticator: `log/authenticator.log` - authenticator: `log/authenticator.log`
## Restarting Processes
To restart the celery group:
sudo supervisorctl restart auth:*
To restart just celerybeat:
sudo supervisorctl restart auth:celerybeat
To restart just celeryd:
sudo supervisorctl restart auth:celeryd
To restart just mumble authenticator:
sudo supervisorctl restart auth-mumble
## Customizing Config Files ## Customizing Config Files

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,9 @@ import json
from bravado.exception import HTTPNotFound, HTTPUnprocessableEntity from bravado.exception import HTTPNotFound, HTTPUnprocessableEntity
import evelink import evelink
import logging import logging
import os
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -228,7 +231,7 @@ class EveProvider(object):
@python_2_unicode_compatible @python_2_unicode_compatible
class EveSwaggerProvider(EveProvider): class EveSwaggerProvider(EveProvider):
def __init__(self, token=None, adapter=None): def __init__(self, token=None, adapter=None):
self.client = esi_client_factory(token=token, Alliance='v1', Character='v4', Corporation='v2', Universe='v2') self.client = esi_client_factory(token=token, spec_file=SWAGGER_SPEC_PATH)
self.adapter = adapter or self self.adapter = adapter or self
def __str__(self): def __str__(self):
@@ -244,7 +247,7 @@ class EveSwaggerProvider(EveProvider):
data['alliance_name'], data['alliance_name'],
data['ticker'], data['ticker'],
corps, corps,
data['executor_corporation_id'], data['executor_corp'],
) )
return model return model
except HTTPNotFound: except HTTPNotFound:

1
eveonline/swagger.json Normal file

File diff suppressed because one or more lines are too long

View File

@@ -36,7 +36,7 @@ def refresh_api(api):
EveManager.create_character_obj(c, api.user, api.api_id) EveManager.create_character_obj(c, api.user, api.api_id)
current_chars = EveCharacter.objects.filter(api_id=api.api_id) current_chars = EveCharacter.objects.filter(api_id=api.api_id)
for c in current_chars: for c in current_chars:
if not int(c.character_id) in [c.id for c in characters]: if not int(c.character_id) in [d.id for d in characters]:
logger.info("Character %s no longer found on API ID %s" % (c, api.api_id)) logger.info("Character %s no longer found on API ID %s" % (c, api.api_id))
c.delete() c.delete()
except evelink.api.APIError as e: except evelink.api.APIError as e:
@@ -60,7 +60,7 @@ def refresh_api(api):
api.api_id, e.required_mask, e.api_mask), level="danger") api.api_id, e.required_mask, e.api_mask), level="danger")
still_valid = False still_valid = False
except EveApiManager.ApiServerUnreachableError as e: except EveApiManager.ApiServerUnreachableError as e:
logger.warn("Error updating API %s\n%s" % (api.api_id, str(e))) logger.warning("Error updating API %s\n%s" % (api.api_id, str(e)))
finally: finally:
if not still_valid: if not still_valid:
EveManager.delete_characters_by_api_id(api.api_id, api.user.id) EveManager.delete_characters_by_api_id(api.api_id, api.user.id)
@@ -71,7 +71,8 @@ def refresh_api(api):
@app.task @app.task
def refresh_user_apis(user): def refresh_user_apis(pk):
user = User.objects.get(pk=pk)
logger.debug('Refreshing all APIs belonging to user %s' % user) logger.debug('Refreshing all APIs belonging to user %s' % user)
apis = EveApiKeyPair.objects.filter(user=user) apis = EveApiKeyPair.objects.filter(user=user)
for x in apis: for x in apis:
@@ -98,7 +99,7 @@ def run_api_refresh():
return return
for u in User.objects.all(): for u in User.objects.all():
refresh_user_apis.delay(u) refresh_user_apis.delay(u.pk)
@app.task @app.task

View File

@@ -1 +0,0 @@
# Create your tests here.

View File

View File

@@ -0,0 +1,125 @@
from __future__ import unicode_literals
try:
# Py3
from unittest import mock
except ImportError:
# Py2
import mock
from django.test import TestCase
from alliance_auth.tests.auth_utils import AuthUtils
from eveonline.providers import Character, Alliance, Corporation
from eveonline.managers import EveManager
from eveonline import tasks
from eveonline.models import EveApiKeyPair, EveCharacter
from services.managers.eve_api_manager import EveApiManager
MODULE_PATH = 'eveonline.tasks'
class EveOnlineTasksTestCase(TestCase):
def setUp(self):
self.user = AuthUtils.create_member('joebloggs')
self.api_key = EveApiKeyPair.objects.create(api_id='0118999',
api_key='hunter2',
user=self.user,
sso_verified=True)
@mock.patch(MODULE_PATH + '.EveApiManager.validate_api')
@mock.patch(MODULE_PATH + '.EveManager.get_characters_from_api')
def test_refresh_api_characters(self, get_characters_from_api, validate_api):
# Arrange
provider = mock.MagicMock()
provider.get_alliance.return_value = Alliance(provider, 22222, 'Test Alliance', 'TEST', [11111], 11111)
provider.get_corp.return_value = Corporation(provider, 11111, 'Test Corp', 'HERP', 12345, [12345, 23456], 22222)
mock_api_data = [
Character(provider, 12345, 'testchar1', 11111, 22222),
Character(provider, 23456, 'Will beAdded', 11111, 22222)
]
get_characters_from_api.return_value = mock_api_data
validate_api.return_value = True
EveManager.create_character_obj(mock_api_data[0], self.user, '0118999')
EveManager.create_character_obj(Character(provider, 34567, 'deletedcharacter', 11111, 22222),
self.user, '0118999')
# Act
tasks.refresh_api(self.api_key)
# Assert
self.assertTrue(EveCharacter.objects.filter(character_id='12345').exists())
self.assertTrue(EveCharacter.objects.filter(character_id='23456').exists())
self.assertFalse(EveCharacter.objects.filter(character_id='34567').exists())
args, kwargs = validate_api.call_args
self.assertEqual(args[0], self.api_key.api_id)
self.assertEqual(args[1], self.api_key.api_key)
self.assertEqual(args[2], self.api_key.user)
@mock.patch(MODULE_PATH + '.EveApiManager.validate_api')
@mock.patch(MODULE_PATH + '.EveManager')
def test_refresh_api_evelink_exception(self, evemanager, validate_api):
import evelink
validate_api.side_effect = evelink.api.APIError()
tasks.refresh_api(self.api_key)
self.assertTrue(validate_api.called)
self.assertFalse(evemanager.get_characters_from_api.called)
self.assertFalse(evemanager.delete_characters_by_api_id.called)
self.assertFalse(evemanager.delete_api_key_pair.called)
@mock.patch(MODULE_PATH + '.EveApiManager.validate_api')
@mock.patch(MODULE_PATH + '.EveManager')
def test_refresh_api_invalid(self, evemanager, validate_api):
validate_api.side_effect = EveApiManager.ApiInvalidError(self.api_key.api_id)
tasks.refresh_api(self.api_key)
self.assertTrue(validate_api.called)
self.assertFalse(evemanager.get_characters_from_api.called)
self.assertTrue(evemanager.delete_characters_by_api_id.called)
self.assertTrue(evemanager.delete_api_key_pair.called)
@mock.patch(MODULE_PATH + '.EveApiManager.validate_api')
@mock.patch(MODULE_PATH + '.EveManager')
def test_refresh_api_accountvalidationerror(self, evemanager, validate_api):
validate_api.side_effect = EveApiManager.ApiAccountValidationError(self.api_key.api_id)
tasks.refresh_api(self.api_key)
self.assertTrue(validate_api.called)
self.assertFalse(evemanager.get_characters_from_api.called)
self.assertTrue(evemanager.delete_characters_by_api_id.called)
self.assertTrue(evemanager.delete_api_key_pair.called)
@mock.patch(MODULE_PATH + '.EveApiManager.validate_api')
@mock.patch(MODULE_PATH + '.EveManager')
def test_refresh_api_maskvalidationerror(self, evemanager, validate_api):
validate_api.side_effect = EveApiManager.ApiMaskValidationError('12345', '1111', self.api_key.api_id)
tasks.refresh_api(self.api_key)
self.assertTrue(validate_api.called)
self.assertFalse(evemanager.get_characters_from_api.called)
self.assertTrue(evemanager.delete_characters_by_api_id.called)
self.assertTrue(evemanager.delete_api_key_pair.called)
@mock.patch(MODULE_PATH + '.EveApiManager.validate_api')
@mock.patch(MODULE_PATH + '.EveManager')
def test_refresh_api_invalid(self, evemanager, validate_api):
validate_api.side_effect = EveApiManager.ApiServerUnreachableError(self.api_key.api_id)
tasks.refresh_api(self.api_key)
self.assertTrue(validate_api.called)
self.assertFalse(evemanager.get_characters_from_api.called)
# Lets hope we never see that again
self.assertFalse(evemanager.delete_characters_by_api_id.called)
self.assertFalse(evemanager.delete_api_key_pair.called)

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -2,6 +2,8 @@
{% load i18n %} {% load i18n %}
{% block title %}Fleet Participation{% endblock %} {% block title %}Fleet Participation{% endblock %}
{% block page_title %}{% trans "Fleet Participation" %}{% endblock %} {% block page_title %}{% trans "Fleet Participation" %}{% endblock %}
{% block content %}
<div class="col-lg-12"> <div class="col-lg-12">
<h1 class="page-header text-center">{% trans "Character not found!" %}</h1> <h1 class="page-header text-center">{% trans "Character not found!" %}</h1>
<div class="col-lg-12 container" id="example"> <div class="col-lg-12 container" id="example">

View File

@@ -37,7 +37,7 @@
<td class="text-center">{{ fat.user }}</td> <td class="text-center">{{ fat.user }}</td>
<td class="text-center">{{ fat.character.character_name }}</td> <td class="text-center">{{ fat.character.character_name }}</td>
{% if fat.station != "No Station" %} {% if fat.station != "No Station" %}
<td class="text-center">{% blocktrans %}Docked in {{ fat.system }}{% endblocktrans %}</td> <td class="text-center">{% blocktrans %}Docked in {% endblocktrans %}{{ fat.system }}</td>
{% else %} {% else %}
<td class="text-center">{{ fat.system }}</td> <td class="text-center">{{ fat.system }}</td>
{% endif %} {% endif %}

View File

@@ -36,7 +36,7 @@
<td class="text-center">{{ fat.fatlink.name }}</td> <td class="text-center">{{ fat.fatlink.name }}</td>
<td class="text-center">{{ fat.character.character_name }}</td> <td class="text-center">{{ fat.character.character_name }}</td>
{% if fat.station != "No Station" %} {% if fat.station != "No Station" %}
<td class="text-center">{% blocktrans %}Docked in {{ fat.system }}{% endblocktrans %}</td> <td class="text-center">{% blocktrans %}Docked in {% endblocktrans %}{{ fat.system }}</td>
{% else %} {% else %}
<td class="text-center">{{ fat.system }}</td> <td class="text-center">{{ fat.system }}</td>
{% endif %} {% endif %}

View File

@@ -16,16 +16,15 @@ from eveonline.managers import EveManager
from authentication.models import AuthServicesInfo from authentication.models import AuthServicesInfo
from fleetactivitytracking.forms import FatlinkForm from fleetactivitytracking.forms import FatlinkForm
from fleetactivitytracking.models import Fatlink, Fat from fleetactivitytracking.models import Fatlink, Fat
from esi.decorators import token_required from esi.decorators import token_required
from slugify import slugify from slugify import slugify
import string import string
import random import random
import datetime import datetime
import logging import logging
import os
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -53,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):
@@ -70,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):
@@ -133,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}
@@ -171,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}
@@ -256,7 +263,7 @@ def click_fatlink_view(request, token, hash, fatname):
if character: if character:
# get data # get data
c = token.get_esi_client(Location='v1', Universe='v2') c = token.get_esi_client(spec_file=SWAGGER_SPEC_PATH)
location = c.Location.get_characters_character_id_location(character_id=token.character_id).result() location = c.Location.get_characters_character_id_location(character_id=token.character_id).result()
ship = c.Location.get_characters_character_id_ship(character_id=token.character_id).result() ship = c.Location.get_characters_character_id_ship(character_id=token.character_id).result()
location['solar_system_name'] = \ location['solar_system_name'] = \
@@ -266,7 +273,6 @@ def click_fatlink_view(request, token, hash, fatname):
location['station_name'] = \ location['station_name'] = \
c.Universe.get_universe_stations_station_id(station_id=location['station_id']).result()['name'] c.Universe.get_universe_stations_station_id(station_id=location['station_id']).result()['name']
elif location['structure_id']: elif location['structure_id']:
c = token.get_esi_client(Universe='v1')
location['station_name'] = \ location['station_name'] = \
c.Universe.get_universe_structures_structure_id(structure_id=location['structure_id']).result()[ c.Universe.get_universe_structures_structure_id(structure_id=location['structure_id']).result()[
'name'] 'name']

View File

@@ -1,6 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.utils import timezone
from datetime import datetime from datetime import datetime
import logging import logging
@@ -17,6 +18,8 @@ class FleetUpManager:
GROUP_ID = settings.FLEETUP_GROUP_ID GROUP_ID = settings.FLEETUP_GROUP_ID
BASE_URL = "http://api.fleet-up.com/Api.svc/{}/{}/{}".format(APP_KEY, USER_ID, API_ID) BASE_URL = "http://api.fleet-up.com/Api.svc/{}/{}/{}".format(APP_KEY, USER_ID, API_ID)
TZ = timezone.utc
def __init__(self): def __init__(self):
pass pass
@@ -60,7 +63,7 @@ class FleetUpManager:
cache.set(cache_key, json, cls._cache_until_seconds(json['CachedUntilUTC'])) cache.set(cache_key, json, cls._cache_until_seconds(json['CachedUntilUTC']))
return json return json
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
logger.warn("Can't connect to Fleet-Up API, is it offline?!") logger.warning("Can't connect to Fleet-Up API, is it offline?!")
except requests.HTTPError: except requests.HTTPError:
logger.exception("Error accessing Fleetup API") logger.exception("Error accessing Fleetup API")
return None return None
@@ -87,8 +90,10 @@ class FleetUpManager:
if foperations is None: if foperations is None:
return None return None
return {row["StartString"]: {"subject": row["Subject"], return {row["StartString"]: {"subject": row["Subject"],
"start": datetime.strptime(row["StartString"], "%Y-%m-%d %H:%M:%S"), "start": timezone.make_aware(
"end": datetime.strptime(row["EndString"], "%Y-%m-%d %H:%M:%S"), datetime.strptime(row["StartString"], "%Y-%m-%d %H:%M:%S"), cls.TZ),
"end": timezone.make_aware(
datetime.strptime(row["EndString"], "%Y-%m-%d %H:%M:%S"), cls.TZ),
"operation_id": row["OperationId"], "operation_id": row["OperationId"],
"location": row["Location"], "location": row["Location"],
"location_info": row["LocationInfo"], "location_info": row["LocationInfo"],
@@ -109,9 +114,9 @@ class FleetUpManager:
"owner": row["Owner"], "owner": row["Owner"],
"type": row["Type"], "type": row["Type"],
"timer_type": row["TimerType"], "timer_type": row["TimerType"],
"expires": (datetime.strptime(row["ExpiresString"], "%Y-%m-%d %H:%M:%S")), "expires": timezone.make_aware(
datetime.strptime(row["ExpiresString"], "%Y-%m-%d %H:%M:%S"), cls.TZ),
"notes": row["Notes"]} for row in ftimers["Data"]} "notes": row["Notes"]} for row in ftimers["Data"]}
return {}
@classmethod @classmethod
def get_fleetup_doctrines(cls): def get_fleetup_doctrines(cls):
@@ -143,9 +148,10 @@ class FleetUpManager:
"estimated": row["EstPrice"], "estimated": row["EstPrice"],
"faction": row["Faction"], "faction": row["Faction"],
"categories": row["Categories"], "categories": row["Categories"],
"last_update": ( "last_update":
datetime.strptime(row["LastUpdatedString"], "%Y-%m-%d %H:%M:%S"))} for row in timezone.make_aware(
ffittings["Data"]} datetime.strptime(row["LastUpdatedString"], "%Y-%m-%d %H:%M:%S"), cls.TZ)}
for row in ffittings["Data"]}
@classmethod @classmethod
def get_fleetup_fitting(cls, fittingnumber): def get_fleetup_fitting(cls, fittingnumber):
@@ -156,7 +162,7 @@ class FleetUpManager:
return None return None
return {"fitting_data": ffitting["Data"]} return {"fitting_data": ffitting["Data"]}
except KeyError: except KeyError:
logger.warn("Failed to retrieve fleetup fitting number %s" % fittingnumber) logger.warning("Failed to retrieve fleetup fitting number %s" % fittingnumber)
return {"fitting_data": {}} return {"fitting_data": {}}
@classmethod @classmethod
@@ -180,5 +186,5 @@ class FleetUpManager:
return None return None
return {"fitting_eft": ffittingeft["Data"]["FittingData"]} return {"fitting_eft": ffittingeft["Data"]["FittingData"]}
except KeyError: except KeyError:
logger.warn("Fleetup fitting eft not found for fitting number %s" % fittingnumber) logger.warning("Fleetup fitting eft not found for fitting number %s" % fittingnumber)
return {"fitting_eft": {}} return {"fitting_eft": {}}

View File

@@ -12,6 +12,7 @@ import json
import datetime import datetime
from django.test import TestCase from django.test import TestCase
from django.utils.timezone import make_aware, utc
from fleetup.managers import FleetUpManager from fleetup.managers import FleetUpManager
@@ -148,8 +149,8 @@ class FleetupManagerTestCase(TestCase):
expected_result = { expected_result = {
'2017-05-06 11:11:11': { '2017-05-06 11:11:11': {
'subject': 'test_operation', 'subject': 'test_operation',
'start': datetime.datetime(2017, 5, 6, 11, 11, 11), 'start': make_aware(datetime.datetime(2017, 5, 6, 11, 11, 11), utc),
'end': datetime.datetime(2017, 5, 6, 12, 12, 12), 'end': make_aware(datetime.datetime(2017, 5, 6, 12, 12, 12), utc),
'operation_id': 1234, 'operation_id': 1234,
'location': 'Jita', 'location': 'Jita',
'location_info': '4-4', 'location_info': '4-4',
@@ -208,7 +209,7 @@ class FleetupManagerTestCase(TestCase):
FleetUpManager.GROUP_ID) FleetUpManager.GROUP_ID)
expected_result = { expected_result = {
'2017-05-06 11:11:11': { '2017-05-06 11:11:11': {
'expires': datetime.datetime(2017, 5, 6, 11, 11, 11), 'expires': make_aware(datetime.datetime(2017, 5, 6, 11, 11, 11), utc),
'solarsystem': 'Jita', 'solarsystem': 'Jita',
'planet': '4', 'planet': '4',
'moon': '4', 'moon': '4',
@@ -361,7 +362,7 @@ class FleetupManagerTestCase(TestCase):
'estimated': 500000000, 'estimated': 500000000,
'faction': 'Amarr', 'faction': 'Amarr',
'categories': ["Armor", "Laser"], 'categories': ["Armor", "Laser"],
'last_update': datetime.datetime(2017, 5, 6, 11, 11, 11) 'last_update': make_aware(datetime.datetime(2017, 5, 6, 11, 11, 11), utc)
} }
} }
self.assertDictEqual(expected_result, result) self.assertDictEqual(expected_result, result)

View File

@@ -6,9 +6,22 @@ from hrapplications.models import ApplicationQuestion
from hrapplications.models import ApplicationForm from hrapplications.models import ApplicationForm
from hrapplications.models import ApplicationResponse from hrapplications.models import ApplicationResponse
from hrapplications.models import ApplicationComment from hrapplications.models import ApplicationComment
from hrapplications.models import ApplicationChoice
class ChoiceInline(admin.TabularInline):
model = ApplicationChoice
extra = 0
verbose_name_plural = 'Choices (optional)'
verbose_name= 'Choice'
class QuestionAdmin(admin.ModelAdmin):
fieldsets = [
(None, {'fields': ['title', 'help_text', 'multi_select']}),
]
inlines = [ChoiceInline]
admin.site.register(Application) admin.site.register(Application)
admin.site.register(ApplicationComment) admin.site.register(ApplicationComment)
admin.site.register(ApplicationQuestion) admin.site.register(ApplicationQuestion, QuestionAdmin)
admin.site.register(ApplicationForm) admin.site.register(ApplicationForm)
admin.site.register(ApplicationResponse) admin.site.register(ApplicationResponse)

View File

@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-23 19:46
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('hrapplications', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='ApplicationChoice',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('choice_text', models.CharField(max_length=200, verbose_name='Choice')),
],
),
migrations.AlterField(
model_name='applicationquestion',
name='title',
field=models.CharField(max_length=254, verbose_name='Question'),
),
migrations.AddField(
model_name='applicationchoice',
name='question',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='hrapplications.ApplicationQuestion'),
),
]

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

@@ -11,13 +11,22 @@ from authentication.models import AuthServicesInfo
@python_2_unicode_compatible @python_2_unicode_compatible
class ApplicationQuestion(models.Model): class ApplicationQuestion(models.Model):
title = models.CharField(max_length=254) 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
@python_2_unicode_compatible
class ApplicationChoice(models.Model):
question = models.ForeignKey(ApplicationQuestion,on_delete=models.CASCADE,related_name="choices")
choice_text = models.CharField(max_length=200, verbose_name='Choice')
def __str__(self):
return self.choice_text
@python_2_unicode_compatible @python_2_unicode_compatible
class ApplicationForm(models.Model): class ApplicationForm(models.Model):
questions = models.ManyToManyField(ApplicationQuestion) questions = models.ManyToManyField(ApplicationQuestion)

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ dnspython
passlib passlib
requests>=2.9.1 requests>=2.9.1
bcrypt bcrypt
slugify python-slugify>=1.2
requests-oauthlib requests-oauthlib
sleekxmpp sleekxmpp
redis redis
@@ -16,11 +16,11 @@ django>=1.10,<2.0
django-bootstrap-form django-bootstrap-form
django-navhelper django-navhelper
django-bootstrap-pagination django-bootstrap-pagination
django-redis>=4.4 django-redis-cache>=1.7.1
django-recaptcha django-recaptcha
django-celery-beat django-celery-beat
# awating pyghassen/openfire-restapi #1 to fix installation issues # awating pyghassen/openfire-restapi #1 to fix installation issues
git+https://github.com/adarnof/openfire-restapi git+https://github.com/adarnof/openfire-restapi
git+https://github.com/adarnof/adarnauth-esi adarnauth-esi>=1.4.1,<2.0

View File

@@ -1,5 +0,0 @@
from __future__ import unicode_literals
from django.contrib import admin
from services.models import GroupCache
admin.site.register(GroupCache)

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-09-02 06:07
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('services', '0002_auto_20161016_0135'),
]
operations = [
migrations.DeleteModel(
name='GroupCache',
),
]

View File

@@ -1,18 +0,0 @@
from __future__ import unicode_literals
from django.utils.encoding import python_2_unicode_compatible
from django.db import models
@python_2_unicode_compatible
class GroupCache(models.Model):
SERVICE_CHOICES = (
("discourse", "discourse"),
("discord", "discord"),
)
created = models.DateTimeField(auto_now_add=True)
groups = models.TextField(default={})
service = models.CharField(max_length=254, choices=SERVICE_CHOICES, unique=True)
def __str__(self):
return self.service

View File

@@ -1,16 +1,15 @@
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 services.models import GroupCache
from requests_oauthlib import OAuth2Session from requests_oauthlib import OAuth2Session
from functools import wraps from functools import wraps
import logging import logging
import datetime import datetime
import time import time
from django.utils import timezone
from django.core.cache import cache from django.core.cache import cache
from hashlib import md5
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -20,9 +19,13 @@ EVE_IMAGE_SERVER = "https://image.eveonline.com"
AUTH_URL = "https://discordapp.com/api/oauth2/authorize" AUTH_URL = "https://discordapp.com/api/oauth2/authorize"
TOKEN_URL = "https://discordapp.com/api/oauth2/token" TOKEN_URL = "https://discordapp.com/api/oauth2/token"
# needs administrator, since Discord can't get their permissions system to work """
# was kick members, manage roles, manage nicknames Previously all we asked for was permission to kick members, manage roles, and manage nicknames.
#BOT_PERMISSIONS = 0x00000002 + 0x10000000 + 0x08000000 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, create instant invite
# BOT_PERMISSIONS = 0x00000002 + 0x10000000 + 0x08000000 + 0x00000001
BOT_PERMISSIONS = 0x00000008 BOT_PERMISSIONS = 0x00000008
# get user ID, accept invite # get user ID, accept invite
@@ -31,7 +34,7 @@ SCOPES = [
'guilds.join', 'guilds.join',
] ]
GROUP_CACHE_MAX_AGE = datetime.timedelta(minutes=30) GROUP_CACHE_MAX_AGE = getattr(settings, 'DISCORD_GROUP_CACHE_MAX_AGE', 2 * 60 * 60) # 2 hours default
class DiscordApiException(Exception): class DiscordApiException(Exception):
@@ -47,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):
@@ -106,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:
if 'Retry-After' in e.response.headers: try:
retry_after = e.response.headers['Retry-After'] retry_after = int(e.response.headers['Retry-After'])
else: 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=int(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!!")
@@ -135,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)
@@ -151,9 +161,12 @@ class DiscordOAuthManager:
pass pass
@staticmethod @staticmethod
def _sanitize_groupname(name): def _sanitize_name(name):
name = name.strip(' _') return name[:32]
return re.sub('[^\w.-]', '', name)
@staticmethod
def _sanitize_group_name(name):
return name[:100]
@staticmethod @staticmethod
def generate_bot_add_url(): def generate_bot_add_url():
@@ -172,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:
@@ -196,10 +219,12 @@ 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)
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 = {'nick': nickname, } data = {'nick': nickname}
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id) path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id)
r = requests.patch(path, headers=custom_headers, json=data) r = requests.patch(path, headers=custom_headers, json=data)
logger.debug("Got status code %s after setting nickname for Discord user ID %s (%s)" % ( logger.debug("Got status code %s after setting nickname for Discord user ID %s (%s)" % (
@@ -230,7 +255,7 @@ class DiscordOAuthManager:
return False return False
@staticmethod @staticmethod
def __get_groups(): def _get_groups():
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.get(path, headers=custom_headers) r = requests.get(path, headers=custom_headers)
@@ -239,80 +264,77 @@ class DiscordOAuthManager:
return r.json() return r.json()
@staticmethod @staticmethod
def __update_group_cache(): def _generate_cache_role_key(name):
GroupCache.objects.filter(service="discord").delete() return 'DISCORD_ROLE_NAME__%s' % md5(str(name).encode('utf-8')).hexdigest()
cache = GroupCache.objects.create(service="discord")
cache.groups = json.dumps(DiscordOAuthManager.__get_groups())
cache.save()
return cache
@staticmethod @staticmethod
def __get_group_cache(): def _group_name_to_id(name):
if not GroupCache.objects.filter(service="discord").exists(): name = DiscordOAuthManager._sanitize_group_name(name)
DiscordOAuthManager.__update_group_cache()
cache = GroupCache.objects.get(service="discord") def get_or_make_role():
age = timezone.now() - cache.created groups = DiscordOAuthManager._get_groups()
if age > GROUP_CACHE_MAX_AGE: for g in groups:
logger.debug("Group cache has expired. Triggering update.") if g['name'] == name:
cache = DiscordOAuthManager.__update_group_cache() return g['id']
return json.loads(cache.groups) return DiscordOAuthManager._create_group(name)['id']
return cache.get_or_set(DiscordOAuthManager._generate_cache_role_key(name), get_or_make_role, GROUP_CACHE_MAX_AGE)
@staticmethod @staticmethod
def __group_name_to_id(name): def __generate_role(name, **kwargs):
cache = DiscordOAuthManager.__get_group_cache()
for g in cache:
if g['name'] == name:
return g['id']
logger.debug("Group %s not found on Discord. Creating" % name)
DiscordOAuthManager.__create_group(name)
return DiscordOAuthManager.__group_name_to_id(name)
@staticmethod
def __group_id_to_name(id):
cache = DiscordOAuthManager.__get_group_cache()
for g in cache:
if g['id'] == id:
return g['name']
raise KeyError("Group ID %s not found on Discord" % id)
@staticmethod
def __generate_role():
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)
DiscordOAuthManager.__edit_role(role['id'], name)
DiscordOAuthManager.__update_group_cache() @staticmethod
def _get_user(user_id):
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id)
r = requests.get(path, headers=custom_headers)
r.raise_for_status()
return r.json()
@staticmethod
def _get_user_roles(user_id):
user = DiscordOAuthManager._get_user(user_id)
return user['roles']
@staticmethod
def _modify_user_role(user_id, role_id, method):
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id) + "/roles/" + str(
role_id)
r = getattr(requests, method)(path, headers=custom_headers)
r.raise_for_status()
logger.debug("%s role %s for user %s" % (method, role_id, user_id))
@staticmethod @staticmethod
@api_backoff @api_backoff
def update_groups(user_id, groups): def update_groups(user_id, groups):
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN} group_ids = [DiscordOAuthManager._group_name_to_id(DiscordOAuthManager._sanitize_group_name(g)) for g in groups]
group_ids = [DiscordOAuthManager.__group_name_to_id(DiscordOAuthManager._sanitize_groupname(g)) for g in groups] user_group_ids = DiscordOAuthManager._get_user_roles(user_id)
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id) for g in group_ids:
data = {'roles': group_ids} if g not in user_group_ids:
r = requests.patch(path, headers=custom_headers, json=data) DiscordOAuthManager._modify_user_role(user_id, g, 'put')
logger.debug("Received status code %s after setting user roles" % r.status_code) time.sleep(1) # we're gonna be hammering the API here
r.raise_for_status() for g in user_group_ids:
if g not in group_ids:
DiscordOAuthManager._modify_user_role(user_id, g, 'delete')
time.sleep(1)

View File

@@ -6,11 +6,10 @@ 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
from services.tasks import only_one
from .models import DiscordUser from .models import DiscordUser
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -22,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
@@ -65,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)
@@ -98,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
@@ -127,3 +135,11 @@ class DiscordTasks:
@classmethod @classmethod
def disable(cls): def disable(cls):
DiscordUser.objects.all().delete() DiscordUser.objects.all().delete()
@staticmethod
def get_nickname(user):
return EveManager.get_main_character(user).character_name
@staticmethod
def get_groups(user):
return [g.name for g in user.groups.all()]

View File

@@ -208,11 +208,11 @@ class DiscordManagerTestCase(TestCase):
def setUp(self): def setUp(self):
pass pass
def test__sanitize_groupname(self): def test__sanitize_group_name(self):
test_group_name = ' Group Name_Test_' test_group_name = str(10**103)
group_name = DiscordOAuthManager._sanitize_groupname(test_group_name) group_name = DiscordOAuthManager._sanitize_group_name(test_group_name)
self.assertEqual(group_name, 'GroupName_Test') self.assertEqual(group_name, test_group_name[:100])
def test_generate_Bot_add_url(self): def test_generate_Bot_add_url(self):
from . import manager from . import manager
@@ -267,18 +267,20 @@ class DiscordManagerTestCase(TestCase):
headers = {'accept': 'application/json', 'authorization': 'Bearer accesstoken'} headers = {'accept': 'application/json', 'authorization': 'Bearer accesstoken'}
m.register_uri('POST',
manager.DISCORD_URL + '/invites/'+str(settings.DISCORD_INVITE_CODE),
request_headers=headers,
text='{}')
m.register_uri('GET', m.register_uri('GET',
manager.DISCORD_URL + "/users/@me", manager.DISCORD_URL + "/users/@me",
request_headers=headers, request_headers=headers,
text=json.dumps({'id': "123456"})) text=json.dumps({'id': "123456"}))
headers = {'accept': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
m.register_uri('PUT',
manager.DISCORD_URL + '/guilds/' + str(settings.DISCORD_GUILD_ID) + '/members/123456',
request_headers=headers,
text='{}')
# Act # Act
return_value = DiscordOAuthManager.add_user('abcdef') return_value = DiscordOAuthManager.add_user('abcdef', [])
# Assert # Assert
self.assertEqual(return_value, '123456') self.assertEqual(return_value, '123456')
@@ -351,66 +353,66 @@ class DiscordManagerTestCase(TestCase):
# Assert # Assert
self.assertTrue(result) self.assertTrue(result)
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._DiscordOAuthManager__get_group_cache') @mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._get_user_roles')
@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._DiscordOAuthManager__get_group_cache') @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._DiscordOAuthManager__get_group_cache') @mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._get_user_roles')
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._group_name_to_id')
@requests_mock.Mocker() @requests_mock.Mocker()
def test_update_groups_global_backoff(self, group_cache, djcache, m): def test_update_groups_global_backoff(self, name_to_id, user_groups, djcache, m):
from . import manager from . import manager
# Arrange # Arrange
groups = ['Member'] groups = ['Member']
group_cache.return_value = [{'id': 111, 'name': 'Member'}] user_groups.return_value = []
name_to_id.return_value = '111'
headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN} headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
user_id = 12345 user_id = 12345
request_url = '{}/guilds/{}/members/{}'.format(manager.DISCORD_URL, settings.DISCORD_GUILD_ID, user_id) request_url = '{}/guilds/{}/members/{}/roles/{}'.format(manager.DISCORD_URL, settings.DISCORD_GUILD_ID, user_id, name_to_id.return_value)
djcache.get.return_value = None # No existing backoffs in cache djcache.get.return_value = None # No existing backoffs in cache
m.patch(request_url, m.put(request_url,
request_headers=headers, request_headers=headers,
headers={'Retry-After': '200', 'X-RateLimit-Global': 'true'}, headers={'Retry-After': '200000', 'X-RateLimit-Global': 'true'},
status_code=429) status_code=429)
# Act & Assert # Act & Assert
with self.assertRaises(manager.DiscordApiBackoff) as bo: with self.assertRaises(manager.DiscordApiBackoff) as bo:
try: try:
DiscordOAuthManager.update_groups(user_id, groups, blocking=False) DiscordOAuthManager.update_groups(user_id, groups, blocking=False)
except manager.DiscordApiBackoff as bo: except manager.DiscordApiBackoff as bo:
self.assertEqual(bo.retry_after, 200, 'Retry-After time must be equal to Retry-After set in header') self.assertEqual(bo.retry_after, 200000, 'Retry-After time must be equal to Retry-After set in header')
self.assertTrue(bo.global_ratelimit, 'global_ratelimit must be True') self.assertTrue(bo.global_ratelimit, 'global_ratelimit must be True')
raise bo raise bo

View File

@@ -1,17 +1,15 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import logging import logging
import requests import requests
import random
import string
import datetime
import json
import re import re
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.core.cache import cache
from services.models import GroupCache from hashlib import md5
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
GROUP_CACHE_MAX_AGE = getattr(settings, 'DISCOURSE_GROUP_CACHE_MAX_AGE', 2 * 60 * 60) # default 2 hours
class DiscourseError(Exception): class DiscourseError(Exception):
def __init__(self, endpoint, errors): def __init__(self, endpoint, errors):
@@ -21,12 +19,13 @@ class DiscourseError(Exception):
def __str__(self): def __str__(self):
return "API execution failed.\nErrors: %s\nEndpoint: %s" % (self.errors, self.endpoint) return "API execution failed.\nErrors: %s\nEndpoint: %s" % (self.errors, self.endpoint)
# not exhaustive, only the ones we need # not exhaustive, only the ones we need
ENDPOINTS = { ENDPOINTS = {
'groups': { 'groups': {
'list': { 'list': {
'path': "/admin/groups.json", 'path': "/groups/search.json",
'method': requests.get, 'method': 'get',
'args': { 'args': {
'required': [], 'required': [],
'optional': [], 'optional': [],
@@ -34,7 +33,7 @@ ENDPOINTS = {
}, },
'create': { 'create': {
'path': "/admin/groups", 'path': "/admin/groups",
'method': requests.post, 'method': 'post',
'args': { 'args': {
'required': ['name'], 'required': ['name'],
'optional': ['visible'], 'optional': ['visible'],
@@ -42,7 +41,7 @@ ENDPOINTS = {
}, },
'add_user': { 'add_user': {
'path': "/admin/groups/%s/members.json", 'path': "/admin/groups/%s/members.json",
'method': requests.put, 'method': 'put',
'args': { 'args': {
'required': ['usernames'], 'required': ['usernames'],
'optional': [], 'optional': [],
@@ -50,7 +49,7 @@ ENDPOINTS = {
}, },
'remove_user': { 'remove_user': {
'path': "/admin/groups/%s/members.json", 'path': "/admin/groups/%s/members.json",
'method': requests.delete, 'method': 'delete',
'args': { 'args': {
'required': ['username'], 'required': ['username'],
'optional': [], 'optional': [],
@@ -58,7 +57,7 @@ ENDPOINTS = {
}, },
'delete': { 'delete': {
'path': "/admin/groups/%s.json", 'path': "/admin/groups/%s.json",
'method': requests.delete, 'method': 'delete',
'args': { 'args': {
'required': [], 'required': [],
'optional': [], 'optional': [],
@@ -68,7 +67,7 @@ ENDPOINTS = {
'users': { 'users': {
'create': { 'create': {
'path': "/users", 'path': "/users",
'method': requests.post, 'method': 'post',
'args': { 'args': {
'required': ['name', 'email', 'password', 'username'], 'required': ['name', 'email', 'password', 'username'],
'optional': ['active'], 'optional': ['active'],
@@ -76,7 +75,7 @@ ENDPOINTS = {
}, },
'update': { 'update': {
'path': "/users/%s.json", 'path': "/users/%s.json",
'method': requests.put, 'method': 'put',
'args': { 'args': {
'required': ['params'], 'required': ['params'],
'optional': [], 'optional': [],
@@ -84,7 +83,7 @@ ENDPOINTS = {
}, },
'get': { 'get': {
'path': "/users/%s.json", 'path': "/users/%s.json",
'method': requests.get, 'method': 'get',
'args': { 'args': {
'required': [], 'required': [],
'optional': [], 'optional': [],
@@ -92,7 +91,7 @@ ENDPOINTS = {
}, },
'activate': { 'activate': {
'path': "/admin/users/%s/activate", 'path': "/admin/users/%s/activate",
'method': requests.put, 'method': 'put',
'args': { 'args': {
'required': [], 'required': [],
'optional': [], 'optional': [],
@@ -100,7 +99,7 @@ ENDPOINTS = {
}, },
'set_email': { 'set_email': {
'path': "/users/%s/preferences/email", 'path': "/users/%s/preferences/email",
'method': requests.put, 'method': 'put',
'args': { 'args': {
'required': ['email'], 'required': ['email'],
'optional': [], 'optional': [],
@@ -108,7 +107,7 @@ ENDPOINTS = {
}, },
'suspend': { 'suspend': {
'path': "/admin/users/%s/suspend", 'path': "/admin/users/%s/suspend",
'method': requests.put, 'method': 'put',
'args': { 'args': {
'required': ['duration', 'reason'], 'required': ['duration', 'reason'],
'optional': [], 'optional': [],
@@ -116,7 +115,7 @@ ENDPOINTS = {
}, },
'unsuspend': { 'unsuspend': {
'path': "/admin/users/%s/unsuspend", 'path': "/admin/users/%s/unsuspend",
'method': requests.put, 'method': 'put',
'args': { 'args': {
'required': [], 'required': [],
'optional': [], 'optional': [],
@@ -124,7 +123,7 @@ ENDPOINTS = {
}, },
'logout': { 'logout': {
'path': "/admin/users/%s/log_out", 'path': "/admin/users/%s/log_out",
'method': requests.post, 'method': 'post',
'args': { 'args': {
'required': [], 'required': [],
'optional': [], 'optional': [],
@@ -132,7 +131,7 @@ ENDPOINTS = {
}, },
'external': { 'external': {
'path': "/users/by-external/%s.json", 'path': "/users/by-external/%s.json",
'method': requests.get, 'method': 'get',
'args': { 'args': {
'required': [], 'required': [],
'optional': [], 'optional': [],
@@ -146,8 +145,7 @@ class DiscourseManager:
def __init__(self): def __init__(self):
pass pass
GROUP_CACHE_MAX_AGE = datetime.timedelta(minutes=30) REVOKED_EMAIL = 'revoked@localhost'
REVOKED_EMAIL = 'revoked@' + settings.DOMAIN
SUSPEND_DAYS = 99999 SUSPEND_DAYS = 99999
SUSPEND_REASON = "Disabled by auth." SUSPEND_REASON = "Disabled by auth."
@@ -171,7 +169,8 @@ class DiscourseManager:
for arg in kwargs: for arg in kwargs:
if arg not in endpoint['args']['required'] and arg not in endpoint['args']['optional'] and not silent: if arg not in endpoint['args']['required'] and arg not in endpoint['args']['optional'] and not silent:
logger.warn("Received unrecognized kwarg %s for endpoint %s" % (arg, endpoint)) logger.warn("Received unrecognized kwarg %s for endpoint %s" % (arg, endpoint))
r = endpoint['method'](settings.DISCOURSE_URL + endpoint['parsed_url'], params=params, json=data) r = getattr(requests, endpoint['method'])(settings.DISCOURSE_URL + endpoint['parsed_url'], params=params,
json=data)
try: try:
if 'errors' in r.json() and not silent: if 'errors' in r.json() and not silent:
logger.error("Discourse execution failed.\nEndpoint: %s\nErrors: %s" % (endpoint, r.json()['errors'])) logger.error("Discourse execution failed.\nEndpoint: %s\nErrors: %s" % (endpoint, r.json()['errors']))
@@ -190,67 +189,59 @@ class DiscourseManager:
return out return out
@staticmethod @staticmethod
def __generate_random_pass(): def _get_groups():
return ''.join([random.choice(string.ascii_letters + string.digits) for n in range(16)])
@staticmethod
def __get_groups():
endpoint = ENDPOINTS['groups']['list'] endpoint = ENDPOINTS['groups']['list']
data = DiscourseManager.__exc(endpoint) data = DiscourseManager.__exc(endpoint)
return [g for g in data if not g['automatic']] return [g for g in data if not g['automatic']]
@staticmethod @staticmethod
def __update_group_cache(): def _create_group(name):
GroupCache.objects.filter(service="discourse").delete()
cache = GroupCache.objects.create(service="discourse")
cache.groups = json.dumps(DiscourseManager.__get_groups())
cache.save()
return cache
@staticmethod
def __get_group_cache():
if not GroupCache.objects.filter(service="discourse").exists():
DiscourseManager.__update_group_cache()
cache = GroupCache.objects.get(service="discourse")
age = timezone.now() - cache.created
if age > DiscourseManager.GROUP_CACHE_MAX_AGE:
logger.debug("Group cache has expired. Triggering update.")
cache = DiscourseManager.__update_group_cache()
return json.loads(cache.groups)
@staticmethod
def __create_group(name):
endpoint = ENDPOINTS['groups']['create'] endpoint = ENDPOINTS['groups']['create']
DiscourseManager.__exc(endpoint, name=name[:20], visible=True) return DiscourseManager.__exc(endpoint, name=name[:20], visible=True)['basic_group']
DiscourseManager.__update_group_cache()
@staticmethod
def _generate_cache_group_name_key(name):
return 'DISCOURSE_GROUP_NAME__%s' % md5(name.encode('utf-8')).hexdigest()
@staticmethod
def _generate_cache_group_id_key(g_id):
return 'DISCOURSE_GROUP_ID__%s' % g_id
@staticmethod @staticmethod
def __group_name_to_id(name): def __group_name_to_id(name):
cache = DiscourseManager.__get_group_cache() name = DiscourseManager._sanitize_groupname(name)
for g in cache:
if g['name'] == name[0:20]: def get_or_create_group():
return g['id'] groups = DiscourseManager._get_groups()
logger.debug("Group %s not found on Discourse. Creating" % name) for g in groups:
DiscourseManager.__create_group(name) if g['name'].lower() == name.lower():
return DiscourseManager.__group_name_to_id(name) return g['id']
return DiscourseManager._create_group(name)['id']
return cache.get_or_set(DiscourseManager._generate_cache_group_name_key(name), get_or_create_group,
GROUP_CACHE_MAX_AGE)
@staticmethod @staticmethod
def __group_id_to_name(id): def __group_id_to_name(g_id):
cache = DiscourseManager.__get_group_cache() def get_group_name():
for g in cache: groups = DiscourseManager._get_groups()
if g['id'] == id: for g in groups:
return g['name'] if g['id'] == g_id:
raise KeyError("Group ID %s not found on Discourse" % id) return g['name']
raise KeyError("Group ID %s not found on Discourse" % g_id)
return cache.get_or_set(DiscourseManager._generate_cache_group_id_key(g_id), get_group_name,
GROUP_CACHE_MAX_AGE)
@staticmethod @staticmethod
def __add_user_to_group(id, username): def __add_user_to_group(g_id, username):
endpoint = ENDPOINTS['groups']['add_user'] endpoint = ENDPOINTS['groups']['add_user']
DiscourseManager.__exc(endpoint, id, usernames=[username]) DiscourseManager.__exc(endpoint, g_id, usernames=[username])
@staticmethod @staticmethod
def __remove_user_from_group(id, username): def __remove_user_from_group(g_id, username):
endpoint = ENDPOINTS['groups']['remove_user'] endpoint = ENDPOINTS['groups']['remove_user']
DiscourseManager.__exc(endpoint, id, username=username) DiscourseManager.__exc(endpoint, g_id, username=username)
@staticmethod @staticmethod
def __generate_group_dict(names): def __generate_group_dict(names):
@@ -269,10 +260,6 @@ class DiscourseManager:
data = DiscourseManager.__get_user(name, silent=silent) data = DiscourseManager.__get_user(name, silent=silent)
return data['user']['id'] return data['user']['id']
@staticmethod
def __user_id_to_name(id):
raise NotImplementedError
@staticmethod @staticmethod
def __get_user(username, silent=False): def __get_user(username, silent=False):
endpoint = ENDPOINTS['users']['get'] endpoint = ENDPOINTS['users']['get']
@@ -281,14 +268,14 @@ class DiscourseManager:
@staticmethod @staticmethod
def __activate_user(username): def __activate_user(username):
endpoint = ENDPOINTS['users']['activate'] endpoint = ENDPOINTS['users']['activate']
id = DiscourseManager.__user_name_to_id(username) u_id = DiscourseManager.__user_name_to_id(username)
DiscourseManager.__exc(endpoint, id) DiscourseManager.__exc(endpoint, u_id)
@staticmethod @staticmethod
def __update_user(username, **kwargs): def __update_user(username, **kwargs):
endpoint = ENDPOINTS['users']['update'] endpoint = ENDPOINTS['users']['update']
id = DiscourseManager.__user_name_to_id(username) u_id = DiscourseManager.__user_name_to_id(username)
DiscourseManager.__exc(endpoint, id, params=kwargs) DiscourseManager.__exc(endpoint, u_id, params=kwargs)
@staticmethod @staticmethod
def __create_user(username, email, password): def __create_user(username, email, password):
@@ -300,21 +287,21 @@ class DiscourseManager:
try: try:
DiscourseManager.__user_name_to_id(username, silent=True) DiscourseManager.__user_name_to_id(username, silent=True)
return True return True
except: except DiscourseError:
return False return False
@staticmethod @staticmethod
def __suspend_user(username): def __suspend_user(username):
id = DiscourseManager.__user_name_to_id(username) u_id = DiscourseManager.__user_name_to_id(username)
endpoint = ENDPOINTS['users']['suspend'] endpoint = ENDPOINTS['users']['suspend']
return DiscourseManager.__exc(endpoint, id, duration=DiscourseManager.SUSPEND_DAYS, return DiscourseManager.__exc(endpoint, u_id, duration=DiscourseManager.SUSPEND_DAYS,
reason=DiscourseManager.SUSPEND_REASON) reason=DiscourseManager.SUSPEND_REASON)
@staticmethod @staticmethod
def __unsuspend(username): def __unsuspend(username):
id = DiscourseManager.__user_name_to_id(username) u_id = DiscourseManager.__user_name_to_id(username)
endpoint = ENDPOINTS['users']['unsuspend'] endpoint = ENDPOINTS['users']['unsuspend']
return DiscourseManager.__exc(endpoint, id) return DiscourseManager.__exc(endpoint, u_id)
@staticmethod @staticmethod
def __set_email(username, email): def __set_email(username, email):
@@ -322,47 +309,53 @@ class DiscourseManager:
return DiscourseManager.__exc(endpoint, username, email=email) return DiscourseManager.__exc(endpoint, username, email=email)
@staticmethod @staticmethod
def __logout(id): def __logout(u_id):
endpoint = ENDPOINTS['users']['logout'] endpoint = ENDPOINTS['users']['logout']
return DiscourseManager.__exc(endpoint, id) return DiscourseManager.__exc(endpoint, u_id)
@staticmethod @staticmethod
def __get_user_by_external(id): def __get_user_by_external(u_id):
endpoint = ENDPOINTS['users']['external'] endpoint = ENDPOINTS['users']['external']
return DiscourseManager.__exc(endpoint, id) return DiscourseManager.__exc(endpoint, u_id)
@staticmethod @staticmethod
def __user_id_by_external_id(id): def __user_id_by_external_id(u_id):
data = DiscourseManager.__get_user_by_external(id) data = DiscourseManager.__get_user_by_external(u_id)
return data['user']['id'] return data['user']['id']
@staticmethod
def _sanitize_name(name):
name = name.replace(' ', '_')
name = name.replace("'", '')
name = name.lstrip(' _')
name = name[:20]
name = name.rstrip(' _')
return name
@staticmethod @staticmethod
def _sanitize_username(username): def _sanitize_username(username):
sanitized = username.replace(" ", "_") return DiscourseManager._sanitize_name(username)
sanitized = sanitized.strip(' _')
sanitized = sanitized.replace("'", "")
return sanitized
@staticmethod @staticmethod
def _sanitize_groupname(name): def _sanitize_groupname(name):
name = name.strip(' _')
name = re.sub('[^\w]', '', name) name = re.sub('[^\w]', '', name)
name = DiscourseManager._sanitize_name(name)
if len(name) < 3: if len(name) < 3:
name = name + "".join('_' for i in range(3-len(name))) name = "Group " + name
return name[:20] return name
@staticmethod @staticmethod
def update_groups(user): def update_groups(user):
groups = [] groups = []
for g in user.groups.all(): for g in user.groups.all():
groups.append(DiscourseManager._sanitize_groupname(str(g)[:20])) groups.append(DiscourseManager._sanitize_groupname(str(g)))
logger.debug("Updating discourse user %s groups to %s" % (user, groups)) logger.debug("Updating discourse user %s groups to %s" % (user, groups))
group_dict = DiscourseManager.__generate_group_dict(groups) group_dict = DiscourseManager.__generate_group_dict(groups)
inv_group_dict = {v: k for k, v in group_dict.items()} inv_group_dict = {v: k for k, v in group_dict.items()}
username = DiscourseManager.__get_user_by_external(user.pk)['user']['username'] username = DiscourseManager.__get_user_by_external(user.pk)['user']['username']
user_groups = DiscourseManager.__get_user_groups(username) user_groups = DiscourseManager.__get_user_groups(username)
add_groups = [group_dict[x] for x in group_dict if not group_dict[x] in user_groups] add_groups = [group_dict[x] for x in group_dict if not group_dict[x] in user_groups]
rem_groups = [x for x in user_groups if not x in inv_group_dict] rem_groups = [x for x in user_groups if x not in inv_group_dict]
if add_groups or rem_groups: if add_groups or rem_groups:
logger.info( logger.info(
"Updating discourse user %s groups: adding %s, removing %s" % (username, add_groups, rem_groups)) "Updating discourse user %s groups: adding %s, removing %s" % (username, add_groups, rem_groups))

View File

@@ -5,8 +5,6 @@ from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from notifications import notify from notifications import notify
from services.tasks import only_one
from .manager import DiscourseManager from .manager import DiscourseManager
from .models import DiscourseUser from .models import DiscourseUser

View File

@@ -61,7 +61,7 @@ class OpenfireManager:
@staticmethod @staticmethod
def _sanitize_groupname(name): def _sanitize_groupname(name):
name = name.strip(' _') name = name.strip(' _').lower()
return re.sub('[^\w.-]', '', name) return re.sub('[^\w.-]', '', name)
@staticmethod @staticmethod
@@ -120,9 +120,10 @@ class OpenfireManager:
logger.error("Unable to update openfire user %s password - user not found on server." % username) logger.error("Unable to update openfire user %s password - user not found on server." % username)
return "" return ""
@staticmethod @classmethod
def update_user_groups(username, groups): def update_user_groups(cls, username, groups):
logger.debug("Updating openfire user %s groups %s" % (username, groups)) logger.debug("Updating openfire user %s groups %s" % (username, groups))
s_groups = list(map(cls._sanitize_groupname, groups)) # Sanitized group names
api = ofUsers(settings.OPENFIRE_ADDRESS, settings.OPENFIRE_SECRET_KEY) api = ofUsers(settings.OPENFIRE_ADDRESS, settings.OPENFIRE_SECRET_KEY)
response = api.get_user_groups(username) response = api.get_user_groups(username)
remote_groups = [] remote_groups = []
@@ -130,16 +131,15 @@ class OpenfireManager:
remote_groups = response['groupname'] remote_groups = response['groupname']
if isinstance(remote_groups, six.string_types): if isinstance(remote_groups, six.string_types):
remote_groups = [remote_groups] remote_groups = [remote_groups]
remote_groups = list(map(cls._sanitize_groupname, remote_groups))
logger.debug("Openfire user %s has groups %s" % (username, remote_groups)) logger.debug("Openfire user %s has groups %s" % (username, remote_groups))
add_groups = [] add_groups = []
del_groups = [] del_groups = []
for g in groups: for g in s_groups:
g = OpenfireManager._sanitize_groupname(g)
if g not in remote_groups: if g not in remote_groups:
add_groups.append(g) add_groups.append(g)
for g in remote_groups: for g in remote_groups:
g = OpenfireManager._sanitize_groupname(g) if g not in s_groups:
if g not in groups:
del_groups.append(g) del_groups.append(g)
logger.info( logger.info(
"Updating openfire groups for user %s - adding %s, removing %s" % (username, add_groups, del_groups)) "Updating openfire groups for user %s - adding %s, removing %s" % (username, add_groups, del_groups))
@@ -155,10 +155,11 @@ class OpenfireManager:
api.delete_user_groups(username, groups) api.delete_user_groups(username, groups)
logger.info("Deleted groups %s from openfire user %s" % (groups, username)) logger.info("Deleted groups %s from openfire user %s" % (groups, username))
@staticmethod @classmethod
def send_broadcast_message(group_name, broadcast_message): def send_broadcast_message(cls, group_name, broadcast_message):
logger.debug("Sending jabber ping to group %s with message %s" % (group_name, broadcast_message)) s_group_name = cls._sanitize_groupname(group_name)
to_address = group_name + '@' + settings.BROADCAST_SERVICE_NAME + '.' + settings.JABBER_URL logger.debug("Sending jabber ping to group %s with message %s" % (s_group_name, broadcast_message))
to_address = s_group_name + '@' + settings.BROADCAST_SERVICE_NAME + '.' + settings.JABBER_URL
xmpp = PingBot(settings.BROADCAST_USER, settings.BROADCAST_USER_PASSWORD, to_address, broadcast_message) xmpp = PingBot(settings.BROADCAST_USER, settings.BROADCAST_USER_PASSWORD, to_address, broadcast_message)
xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0030') # Service Discovery
xmpp.register_plugin('xep_0199') # XMPP Ping xmpp.register_plugin('xep_0199') # XMPP Ping

View File

@@ -219,3 +219,28 @@ class OpenfireManagerTestCase(TestCase):
result_username = self.manager._OpenfireManager__sanitize_username(test_username) result_username = self.manager._OpenfireManager__sanitize_username(test_username)
self.assertEqual(result_username, 'My_Test\\20User\\22\\27\\26\\2f\\3a\\3c\\3e\\40name\\5c20name') self.assertEqual(result_username, 'My_Test\\20User\\22\\27\\26\\2f\\3a\\3c\\3e\\40name\\5c20name')
def test__sanitize_groupname(self):
test_groupname = " My_Test Groupname"
result_groupname = self.manager._sanitize_groupname(test_groupname)
self.assertEqual(result_groupname, "my_testgroupname")
@mock.patch(MODULE_PATH + '.manager.ofUsers')
def test_update_user_groups(self, api):
groups = ["AddGroup", "othergroup", "Guest Group"]
server_groups = ["othergroup", "Guest Group", "REMOVE group"]
username = "testuser"
api_instance = api.return_value
api_instance.get_user_groups.return_value = {'groupname': server_groups}
self.manager.update_user_groups(username, groups)
self.assertTrue(api_instance.add_user_groups.called)
args, kwargs = api_instance.add_user_groups.call_args
self.assertEqual(args[1], ["addgroup"])
self.assertTrue(api_instance.delete_user_groups.called)
args, kwargs = api_instance.delete_user_groups.call_args
self.assertEqual(args[1], ["removegroup"])

View File

@@ -63,7 +63,7 @@ class SeatManager:
logger.info("Added SeAT user with username %s" % sanitized) logger.info("Added SeAT user with username %s" % sanitized)
return sanitized, password return sanitized, password
logger.info("Failed to add SeAT user with username %s" % sanitized) logger.info("Failed to add SeAT user with username %s" % sanitized)
return None return None, None
@classmethod @classmethod
def delete_user(cls, username): def delete_user(cls, username):
@@ -75,29 +75,10 @@ class SeatManager:
return username return username
return None return None
@classmethod
def disable_user(cls, username):
""" Disable user """
ret = cls.exec_request('user/{}'.format(username), 'put', active=0)
logger.debug(ret)
ret = cls.exec_request('user/{}'.format(username), 'put', email="")
logger.debug(ret)
if cls._response_ok(ret):
try:
cls.update_roles(username, [])
logger.info("Disabled SeAT user with username %s" % username)
return username
except KeyError:
# if something goes wrong, delete user from seat instead of disabling
if cls.delete_user(username):
return username
logger.info("Failed to disabled SeAT user with username %s" % username)
return None
@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)
@@ -105,14 +86,34 @@ 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
def _check_email_changed(cls, username, email):
"""Compares email to one set on SeAT"""
ret = cls.exec_request('user/{}'.format(username), 'get', raise_for_status=True)
return ret['email'] != email
@classmethod @classmethod
def update_user(cls, username, email, password): def update_user(cls, username, email, password):
""" Edit user info """ """ Edit user info """
logger.debug("Updating SeAT username %s with email %s and password" % (username, email)) if cls._check_email_changed(username, email):
ret = cls.exec_request('user/{}'.format(username), 'put', email=email) # if we try to set the email to whatever it is already on SeAT, we get a HTTP422 error
logger.debug(ret) logger.debug("Updating SeAT username %s with email %s and password" % (username, email))
if not cls._response_ok(ret): ret = cls.exec_request('user/{}'.format(username), 'put', email=email)
logger.warn("Failed to update email for username {}".format(username)) logger.debug(ret)
if not cls._response_ok(ret):
logger.warn("Failed to update email for username {}".format(username))
ret = cls.exec_request('user/{}'.format(username), 'put', password=password) ret = cls.exec_request('user/{}'.format(username), 'put', password=password)
logger.debug(ret) logger.debug(ret)
if not cls._response_ok(ret): if not cls._response_ok(ret):
@@ -275,5 +276,5 @@ class SeatManager:
@staticmethod @staticmethod
def username_hash(username): def username_hash(username):
m = hashlib.sha1() m = hashlib.sha1()
m.update(username) m.update(username.encode('utf-8'))
return m.hexdigest() return m.hexdigest()

View File

@@ -246,6 +246,10 @@ class Teamspeak3Manager:
logger.debug("Deleting user %s with id %s from TS3 server." % (user, uid)) logger.debug("Deleting user %s with id %s from TS3 server." % (user, uid))
if user: if user:
clients = self.server.send_command('clientlist') clients = self.server.send_command('clientlist')
if isinstance(clients, dict):
# Rewrap list
clients = [clients]
for client in clients: for client in clients:
try: try:
if client['keys']['client_database_id'] == user: if client['keys']['client_database_id'] == user:

View File

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

View File

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

View File

@@ -5,29 +5,33 @@
{% block title %}Apply To {{ corp.corporation_name }}{% endblock title %} {% block title %}Apply To {{ corp.corporation_name }}{% endblock title %}
{% block page_title %}{% trans "Apply To" %} {{ corp.corporation_name }}{% endblock page_title %} {% block page_title %}{% trans "Apply To" %} {{ corp.corporation_name }}{% endblock page_title %}
{% block content %} {% block content %}
<div class="col-lg-12"> <div class="col-lg-12">
<h1 class="page-header text-center">{% trans "Apply To" %} {{ corp.corporation_name }}</h1> <h1 class="page-header text-center">{% trans "Apply To" %} {{ corp.corporation_name }}</h1>
<div class="container-fluid"> <div class="container-fluid">
<div class="col-md-4 col-md-offset-4"> <div class="col-md-4 col-md-offset-4">
<div class="row"> <div class="row">
<form class="form-signin"> <form class="form-signin">
{% csrf_token %} {% csrf_token %}
{% for question in questions %} {% for question in questions %}
<div class="form-group"> <div class="form-group">
<label class="control-label" for="id_{{ question.pk }}">{{ question.title }}</label> <label class="control-label" for="id_{{ question.pk }}">{{ question.title }}</label>
<div class=" "> <div class=" ">
{% if question.help_text %} {% if question.help_text %}
<div cass="text-center">{{ question.help_text }}</div> <div cass="text-center">{{ question.help_text }}</div>
{% endif %} {% endif %}
<textarea class="form-control" cols="40" id="id_{{ question.pk }}" name="{{ question.pk }}" rows="10"></textarea> {% for choice in question.choices.all %}
</div> <input type={% if question.multi_select == False %}"radio"{% else %}"checkbox"{% endif %} name="{{ question.pk }}" id="id_{{ question.pk }}" value="{{ choice.choice_text }}" />
</div> <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br />
{% endfor %} {% empty %}
<button class="btn btn-lg btn-primary btn-block" type="submit" formmethod="post">{% trans "Submit" %}</button> <textarea class="form-control" cols="30" id="id_{{ question.pk }}" name="{{ question.pk }}" rows="4"></textarea>
</form> {% endfor %}
</div> </div>
</div>
{% endfor %}
<button class="btn btn-lg btn-primary btn-block" type="submit" formmethod="post">{% trans "Submit" %}</button>
</form>
</div> </div>
</div> </div>
</div> </div>
</div>
{% endblock %} {% endblock %}

View File

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

View File

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

View File

@@ -1,10 +0,0 @@
[program:auth-celerybeat]
command=celery -A alliance_auth beat
directory=/home/allianceserver/allianceauth
user=allianceserver
stdout_logfile=/home/allianceserver/allianceauth/log/beat.log
stderr_logfile=/home/allianceserver/allianceauth/log/beat.log
autostart=true
autorestart=true
startsecs=10
priority=999

View File

@@ -1,13 +0,0 @@
[program:auth-celeryd]
command=celery -A alliance_auth worker
directory=/home/allianceserver/allianceauth
user=allianceserver
numprocs=1
stdout_logfile=/home/allianceserver/allianceauth/log/worker.log
stderr_logfile=/home/allianceserver/allianceauth/log/worker.log
autostart=true
autorestart=true
startsecs=10
stopwaitsecs = 600
killasgroup=true
priority=1000

28
thirdparty/Supervisor/auth.conf vendored Normal file
View File

@@ -0,0 +1,28 @@
[program:celerybeat]
command=celery -A alliance_auth beat
directory=/home/allianceserver/allianceauth
user=allianceserver
stdout_logfile=/home/allianceserver/allianceauth/log/beat.log
stderr_logfile=/home/allianceserver/allianceauth/log/beat.log
autostart=true
autorestart=true
startsecs=10
priority=998
[program:celeryd]
command=celery -A alliance_auth worker
directory=/home/allianceserver/allianceauth
user=allianceserver
numprocs=1
stdout_logfile=/home/allianceserver/allianceauth/log/worker.log
stderr_logfile=/home/allianceserver/allianceauth/log/worker.log
autostart=true
autorestart=true
startsecs=10
stopwaitsecs = 600
killasgroup=true
priority=998
[group:auth]
programs=celerybeat,celeryd
priority=999

View File

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