Restructure Alliance Auth package (#867)

* Refactor allianceauth into its own package

* Add setup

* Add missing default_app_config declarations

* Fix timerboard namespacing

* Remove obsolete future imports

* Remove py2 mock support

* Remove six

* Add experimental 3.7 support and multiple Dj versions

* Remove python_2_unicode_compatible

* Add navhelper as local package

* Update requirements
This commit is contained in:
Basraah
2017-09-19 09:46:40 +10:00
committed by GitHub
parent d10580b56b
commit 786859294d
538 changed files with 1197 additions and 1523 deletions

View File

@@ -0,0 +1 @@
default_app_config = 'allianceauth.corputils.apps.CorpUtilsConfig'

View File

@@ -0,0 +1,6 @@
from django.contrib import admin
from .models import CorpStats, CorpMember
admin.site.register(CorpStats)
admin.site.register(CorpMember)

View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class CorpUtilsConfig(AppConfig):
name = 'allianceauth.corputils'
label = 'corputils'

View File

@@ -0,0 +1,30 @@
from allianceauth.services.hooks import MenuItemHook, UrlHook
from allianceauth import hooks
from allianceauth.corputils import urls
class CorpStats(MenuItemHook):
def __init__(self):
MenuItemHook.__init__(self,
'Corporation Stats',
'fa fa-share-alt fa-fw grayiconecolor',
'corputils:view',
navactive=['corputils:'])
def render(self, request):
if request.user.has_perm('corputils.view_corp_corpstats') or request.user.has_perm(
'corputils.view_alliance_corpstats') or request.user.has_perm(
'corputils.add_corpstats') or request.user.has_perm('corputils.view_state_corpstats'):
return MenuItemHook.render(self, request)
return ''
@hooks.register('menu_item_hook')
def register_menu():
return CorpStats()
@hooks.register('url_hook')
def register_url():
return UrlHook(urls, 'corputils', r'^corpstats/')

View File

@@ -0,0 +1,42 @@
from django.db import models
import logging
logger = logging.getLogger(__name__)
class CorpStatsQuerySet(models.QuerySet):
def visible_to(self, user):
# superusers get all visible
if user.is_superuser:
logger.debug('Returning all corpstats for superuser %s.' % user)
return self
try:
char = user.profile.main_character
assert char
# build all accepted queries
queries = [models.Q(token__user=user)]
if user.has_perm('corputils.view_corp_corpstats'):
queries.append(models.Q(corp__corporation_id=char.corporation_id))
if user.has_perm('corputils.view_alliance_corpstats'):
queries.append(models.Q(corp__alliance__alliance_id=char.alliance_id))
if user.has_perm('corputils.view_state_corpstats'):
queries.append(models.Q(corp__in=user.profile.state.member_corporations.all()))
queries.append(models.Q(corp__alliance__in=user.profile.state.member_alliances.all()))
logger.debug('%s queries for user %s visible corpstats.' % (len(queries), user))
# filter based on queries
query = queries.pop()
for q in queries:
query |= q
return self.filter(query)
except AssertionError:
logger.debug('User %s has no main character. No corpstats visible.' % user)
return self.none()
class CorpStatsManager(models.Manager):
def get_queryset(self):
return CorpStatsQuerySet(self.model, using=self._db)
def visible_to(self, user):
return self.get_queryset().visible_to(user)

View File

@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-12-14 21:36
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('esi', '0002_scopes_20161208'),
('eveonline', '0004_eveapikeypair_sso_verified'),
]
operations = [
migrations.CreateModel(
name='CorpStats',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('last_update', models.DateTimeField(auto_now=True)),
('_members', models.TextField(default='{}')),
('corp', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='eveonline.EveCorporationInfo')),
('token', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='esi.Token')),
],
options={
'default_permissions': ('add', 'change', 'remove', 'view_corp', 'view_alliance', 'view_blue'),
'verbose_name': 'corp stats',
'verbose_name_plural': 'corp stats',
'permissions': (('corp_apis', 'Can view API keys of members of their corporation.'), ('alliance_apis', 'Can view API keys of members of their alliance.'), ('blue_apis', 'Can view API keys of members of blue corporations.')),
},
),
]

View File

@@ -0,0 +1,128 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-12-14 21:48
from __future__ import unicode_literals
from django.db import migrations
PERMISSIONS = {
'user': [
'corp_apis',
'alliance_apis',
],
'corpstats': {
'corp_apis': 'Can view API keys of members of their corporation.',
'alliance_apis': 'Can view API keys of members of their alliance.',
'blue_apis': 'Can view API keys of members of blue corporations.',
'view_corp_corpstats': 'Can view corp stats of their corporation.',
'view_alliance_corpstats': 'Can view corp stats of members of their alliance.',
'view_blue_corpstats': 'Can view corp stats of blue corporations.',
}
}
def user_permissions_dict(apps):
Permission = apps.get_model('auth', 'Permission')
ContentType = apps.get_model('contenttypes', 'ContentType')
User = apps.get_model('auth', 'User')
CorpStats = apps.get_model('corputils', 'CorpStats')
user_ct = ContentType.objects.get_for_model(User)
corpstats_ct = ContentType.objects.get_for_model(CorpStats)
return {
'user': {x: Permission.objects.get_or_create(name=x, codename=x, content_type=user_ct)[0] for x in PERMISSIONS['user']},
'corpstats': {x: Permission.objects.get_or_create(codename=x, name=y, content_type=corpstats_ct)[0] for x, y in PERMISSIONS['corpstats'].items()},
}
def users_with_permission(apps, perm):
User = apps.get_model('auth', 'User')
return User.objects.filter(user_permissions=perm.pk)
def groups_with_permission(apps, perm):
Group = apps.get_model('auth', 'Group')
return Group.objects.filter(permissions=perm.pk)
def forward(apps, schema_editor):
perm_dict = user_permissions_dict(apps)
corp_users = users_with_permission(apps, perm_dict['user']['corp_apis'])
for u in corp_users:
u.user_permissions.add(perm_dict['corpstats']['corp_apis'].pk)
u.user_permissions.add(perm_dict['corpstats']['view_corp_corpstats'].pk)
alliance_users = users_with_permission(apps, perm_dict['user']['alliance_apis'])
for u in alliance_users:
u.user_permissions.add(perm_dict['corpstats']['alliance_apis'].pk)
u.user_permissions.add(perm_dict['corpstats']['view_alliance_corpstats'].pk)
corp_groups = groups_with_permission(apps, perm_dict['user']['corp_apis'])
for g in corp_groups:
g.permissions.add(perm_dict['corpstats']['corp_apis'].pk)
g.permissions.add(perm_dict['corpstats']['view_corp_corpstats'].pk)
alliance_groups = groups_with_permission(apps, perm_dict['user']['alliance_apis'])
for g in alliance_groups:
g.permissions.add(perm_dict['corpstats']['alliance_apis'].pk)
g.permissions.add(perm_dict['corpstats']['view_alliance_corpstats'].pk)
for name, perm in perm_dict['user'].items():
perm.delete()
def reverse(apps, schema_editor):
perm_dict = user_permissions_dict(apps)
corp_users = users_with_permission(apps, perm_dict['corpstats']['view_corp_corpstats'])
corp_api_users = users_with_permission(apps, perm_dict['corpstats']['corp_apis'])
corp_us = corp_users | corp_api_users
for u in corp_us.distinct():
u.user_permissions.add(perm_dict['user']['corp_apis'].pk)
for u in corp_users:
u.user_permissions.remove(perm_dict['corpstats']['view_corp_corpstats'].pk)
for u in corp_api_users:
u.user_permissions.remove(perm_dict['corpstats']['corp_apis'].pk)
alliance_users = users_with_permission(apps, perm_dict['corpstats']['view_alliance_corpstats'])
alliance_api_users = users_with_permission(apps, perm_dict['corpstats']['alliance_apis'])
alliance_us = alliance_users | alliance_api_users
for u in alliance_us.distinct():
u.user_permissions.add(perm_dict['user']['alliance_apis'].pk)
for u in alliance_users:
u.user_permissions.remove(perm_dict['corpstats']['view_alliance_corpstats'].pk)
for u in alliance_api_users:
u.user_permissions.remove(perm_dict['corpstats']['alliance_apis'].pk)
corp_groups = groups_with_permission(apps, perm_dict['corpstats']['view_corp_corpstats'])
corp_api_groups = groups_with_permission(apps, perm_dict['corpstats']['corp_apis'])
for g in corp_groups.distinct():
g.permissions.add(perm_dict['user']['corp_apis'].pk)
for g in corp_groups:
g.permissions.remove(perm_dict['corpstats']['view_corp_corpstats'].pk)
for g in corp_api_groups:
g.permissions.remove(perm_dict['corpstats']['corp_apis'].pk)
alliance_groups = groups_with_permission(apps, perm_dict['corpstats']['view_alliance_corpstats'])
alliance_api_groups = groups_with_permission(apps, perm_dict['corpstats']['alliance_apis'])
alliance_gs = alliance_groups | alliance_api_groups
for g in alliance_gs.distinct():
g.permissions.add(perm_dict['user']['alliance_apis'].pk)
for g in alliance_groups:
g.permissions.remove(perm_dict['corpstats']['view_alliance_corpstats'].pk)
for g in alliance_api_groups:
g.permissions.remove(perm_dict['corpstats']['alliance_apis'].pk)
class Migration(migrations.Migration):
dependencies = [
('corputils', '0001_initial'),
('authentication', '0005_delete_perms'),
('auth', '0008_alter_user_username_max_length'),
]
operations = [
migrations.RunPython(forward, reverse),
]

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-03-22 23:35
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('corputils', '0002_migrate_permissions'),
]
operations = [
migrations.AlterModelOptions(
name='corpstats',
options={'permissions': (('corp_apis', 'Can view API keys of members of their corporation.'), ('alliance_apis', 'Can view API keys of members of their alliance.'), ('blue_apis', 'Can view API keys of members of blue corporations.'), ('view_corp_corpstats', 'Can view corp stats of their corporation.'), ('view_alliance_corpstats', 'Can view corp stats of members of their alliance.'), ('view_blue_corpstats', 'Can view corp stats of blue corporations.')), 'verbose_name': 'corp stats', 'verbose_name_plural': 'corp stats'},
),
]

View File

@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-03-26 20:13
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import json
def convert_json_to_members(apps, schema_editor):
CorpStats = apps.get_model('corputils', 'CorpStats')
CorpMember = apps.get_model('corputils', 'CorpMember')
for cs in CorpStats.objects.all():
members = json.loads(cs._members)
CorpMember.objects.bulk_create(
[CorpMember(corpstats=cs, character_id=member_id, character_name=member_name) for member_id, member_name in
members.items()]
)
def convert_members_to_json(apps, schema_editor):
CorpStats = apps.get_model('corputils', 'CorpStats')
for cs in CorpStats.objects.all():
cs._members = json.dumps({m.character_id: m.character_name for m in cs.members.all()})
cs.save()
class Migration(migrations.Migration):
dependencies = [
('corputils', '0003_granular_permissions'),
]
operations = [
migrations.CreateModel(
name='CorpMember',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('character_id', models.PositiveIntegerField()),
('character_name', models.CharField(max_length=37)),
],
options={
'ordering': ['character_name'],
},
),
migrations.AddField(
model_name='corpmember',
name='corpstats',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='members',
to='corputils.CorpStats'),
),
migrations.AlterUniqueTogether(
name='corpmember',
unique_together=set([('corpstats', 'character_id')]),
),
migrations.RunPython(convert_json_to_members, convert_members_to_json),
migrations.RemoveField(
model_name='corpstats',
name='_members',
),
]

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-06-10 15:34
from __future__ import unicode_literals
from django.db import migrations
def delete_permissions(apps, schema_editor):
CorpStats = apps.get_model('corputils', 'CorpStats')
ContentType = apps.get_model('contenttypes', 'ContentType')
Permission = apps.get_model('auth', 'Permission')
ct = ContentType.objects.get_for_model(CorpStats)
perms = Permission.objects.filter(content_type=ct)
perms.filter(codename__contains='api').delete()
perms.filter(codename='view_corpstats').delete()
perms.filter(codename__contains='blue').delete()
perms.filter(codename__contains='remove').delete()
g = perms.get(codename='view_corp_corpstats')
g.name = 'Can view corp stats of their corporation.'
g.save()
g = perms.get(codename='view_alliance_corpstats')
g.name = 'Can view corp stats of members of their alliance.'
g.save()
class Migration(migrations.Migration):
dependencies = [
('corputils', '0004_member_models'),
]
operations = [
migrations.AlterModelOptions(
name='corpstats',
options={'permissions': (('view_corp_corpstats', 'Can view corp stats of their corporation.'), ('view_alliance_corpstats', 'Can view corp stats of members of their alliance.'), ('view_state_corpstats', 'Can view corp stats of members of their auth state.')), 'verbose_name': 'corp stats', 'verbose_name_plural': 'corp stats'},
),
migrations.RunPython(delete_permissions, migrations.RunPython.noop),
]

View File

@@ -0,0 +1,182 @@
import logging
import os
from allianceauth.authentication.models import CharacterOwnership, UserProfile
from bravado.exception import HTTPForbidden
from django.db import models
from esi.errors import TokenError
from esi.models import Token
from allianceauth.eveonline.models import EveCorporationInfo, EveCharacter
from allianceauth.notifications import notify
from allianceauth.corputils.managers import CorpStatsManager
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json')
logger = logging.getLogger(__name__)
class CorpStats(models.Model):
token = models.ForeignKey(Token, on_delete=models.CASCADE)
corp = models.OneToOneField(EveCorporationInfo)
last_update = models.DateTimeField(auto_now=True)
class Meta:
permissions = (
('view_corp_corpstats', 'Can view corp stats of their corporation.'),
('view_alliance_corpstats', 'Can view corp stats of members of their alliance.'),
('view_state_corpstats', 'Can view corp stats of members of their auth state.'),
)
verbose_name = "corp stats"
verbose_name_plural = "corp stats"
objects = CorpStatsManager()
def __str__(self):
return "%s for %s" % (self.__class__.__name__, self.corp)
def update(self):
try:
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()[
'corporation_id'] == int(self.corp.corporation_id)
members = c.Corporation.get_corporations_corporation_id_members(
corporation_id=self.corp.corporation_id).result()
member_ids = [m['character_id'] for m in members]
# requesting too many ids per call results in a HTTP400
# the swagger spec doesn't have a maxItems count
# 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_name_chunks = [c.Character.get_characters_names(character_ids=id_chunk).result() for id_chunk in
member_id_chunks]
member_list = {}
for name_chunk in member_name_chunks:
member_list.update({m['character_id']: m['character_name'] for m in name_chunk})
# bulk create new member models
missing_members = [m_id for m_id in member_ids if
not CorpMember.objects.filter(corpstats=self, character_id=m_id).exists()]
CorpMember.objects.bulk_create(
[CorpMember(character_id=m_id, character_name=member_list[m_id], corpstats=self) for m_id in
missing_members])
# purge old members
self.members.exclude(character_id__in=member_ids).delete()
# update the timer
self.save()
except TokenError as e:
logger.warning("%s failed to update: %s" % (self, e))
if self.token.user:
notify(self.token.user, "%s failed to update with your ESI token." % self,
message="Your token has expired or is no longer valid. Please add a new one to create a new CorpStats.",
level="error")
self.delete()
except HTTPForbidden as e:
logger.warning("%s failed to update: %s" % (self, e))
if self.token.user:
notify(self.token.user, "%s failed to update with your ESI token." % self,
message="%s: %s" % (e.status_code, e.message), level="error")
self.delete()
except AssertionError:
logger.warning("%s token character no longer in corp." % self)
if self.token.user:
notify(self.token.user, "%s cannot update with your ESI token." % self,
message="%s cannot update with your ESI token as you have left corp." % self, level="error")
self.delete()
@property
def member_count(self):
return self.members.count()
@property
def user_count(self):
return len(set([m.main_character for m in self.members.all() if m.main_character]))
@property
def registered_member_count(self):
return len(self.registered_members)
@property
def registered_members(self):
return self.members.filter(pk__in=[m.pk for m in self.members.all() if m.registered])
@property
def unregistered_member_count(self):
return self.member_count - self.registered_member_count
@property
def unregistered_members(self):
return self.members.filter(pk__in=[m.pk for m in self.members.all() if not m.registered])
@property
def main_count(self):
return len(self.mains)
@property
def mains(self):
return self.members.filter(pk__in=[m.pk for m in self.members.all() if
m.main_character and int(m.main_character.character_id) == int(
m.character_id)])
def can_update(self, user):
return user.is_superuser or user == self.token.user
def corp_logo(self, size=128):
return "https://image.eveonline.com/Corporation/%s_%s.png" % (self.corp.corporation_id, size)
def alliance_logo(self, size=128):
if self.corp.alliance:
return "https://image.eveonline.com/Alliance/%s_%s.png" % (self.corp.alliance.alliance_id, size)
else:
return "https://image.eveonline.com/Alliance/1_%s.png" % size
class CorpMember(models.Model):
character_id = models.PositiveIntegerField()
character_name = models.CharField(max_length=37)
corpstats = models.ForeignKey(CorpStats, on_delete=models.CASCADE, related_name='members')
class Meta:
# not making character_id unique in case a character moves between two corps while only one updates
unique_together = ('corpstats', 'character_id')
ordering = ['character_name']
def __str__(self):
return self.character_name
@property
def character(self):
try:
return EveCharacter.objects.get(character_id=self.character_id)
except EveCharacter.DoesNotExist:
return None
@property
def main_character(self):
try:
return self.character.character_ownership.user.profile.main_character
except (CharacterOwnership.DoesNotExist, UserProfile.DoesNotExist, AttributeError):
return None
@property
def alts(self):
if self.main_character:
return [co.character for co in self.main_character.character_ownership.user.character_ownerships.all()]
else:
return []
@property
def registered(self):
return CharacterOwnership.objects.filter(character__character_id=self.character_id).exists()
def portrait_url(self, size=32):
return "https://image.eveonline.com/Character/%s_%s.jpg" % (self.character_id, size)
def __getattr__(self, item):
if item.startswith('portrait_url_'):
size = item.strip('portrait_url_')
return self.portrait_url(size)
return super(CorpMember, self).__getattr__(item)

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,14 @@
from allianceauth.celeryapp import app
from allianceauth.corputils import CorpStats
@app.task
def update_corpstats(pk):
cs = CorpStats.objects.get(pk=pk)
cs.update()
@app.task
def update_all_corpstats():
for cs in CorpStats.objects.all():
update_corpstats.delay(cs.pk)

View File

@@ -0,0 +1,38 @@
{% extends 'registered/base.html' %}
{% load i18n %}
{% block title %}{% trans "Corporation Member Data" %}{% endblock %}
{% block page_title %}{% trans "Corporation Member Data" %}{% endblock %}
{% block content %}
<div class="col-lg-12">
<h1 class="page-header text-center">{% trans "Corporation Member Data" %}</h1>
<div class="col-lg-10 col-lg-offset-1 container">
<nav class="navbar navbar-default">
<div class="container-fluid">
<ul class="nav navbar-nav">
<li class="dropdown">
<a href="#" id="dLabel" class="dropdown-toggle" role="button" data-toggle="dropdown" aria-haspopup="false" aria-expanded="false">{% trans "Corporations" %}<span class="caret"></span></a>
<ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">
{% for corpstat in available %}
<li>
<a href="{% url 'corputils:view_corp' corpstat.corp.corporation_id %}">{{ corpstat.corp.corporation_name }}</a>
</li>
{% endfor %}
</ul>
</li>
{% if perms.corputils.add_corpstats %}
<li>
<a href="{% url 'corputils:add' %}">{% trans "Add" %}</a>
</li>
{% endif %}
</ul>
<form class="navbar-form navbar-right" role="search" action="{% url 'corputils:search' %}" method="GET">
<div class="form-group">
<input type="text" class="form-control" name="search_string" placeholder="{% if search_string %}{{ search_string }}{% else %}{% trans "Search all corporations..." %}{% endif %}">
</div>
</form>
</div>
</nav>
{% block member_data %}{% endblock %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,183 @@
{% extends 'corputils/base.html' %}
{% load i18n %}
{% load humanize %}
{% block member_data %}
{% if corpstats %}
<div class="row">
<div class="col-lg-12 text-center">
<table class="table">
<tr>
<td class="text-center col-lg-6
{% if corpstats.corp.alliance %}{% else %}col-lg-offset-3{% endif %}"><img
class="ra-avatar" src="{{ corpstats.corp_logo }}"></td>
{% if corpstats.corp.alliance %}
<td class="text-center col-lg-6"><img class="ra-avatar" src="{{ corpstats.alliance_logo }}">
</td>
{% endif %}
</tr>
<tr>
<td class="text-center"><h4>{{ corpstats.corp.corporation_name }}</h4></td>
{% if corpstats.corp.alliance %}
<td class="text-center"><h4>{{ corpstats.corp.alliance.alliance_name }}</h4></td>
{% endif %}
</tr>
</table>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<div class="panel panel-default">
<div class="panel-heading">
<ul class="nav nav-pills pull-left">
<li class="active"><a href="#mains" data-toggle="pill">{% trans 'Mains' %} ({{ corpstats.main_count }})</a></li>
<li><a href="#members" data-toggle="pill">{% trans 'Members' %} ({{ corpstats.member_count }})</a></li>
<li><a href="#unregistered" data-toggle="pill">{% trans 'Unregistered' %} ({{ corpstats.unregistered_member_count }})</a></li>
</ul>
<div class="pull-right">
{% trans "Last update:" %} {{ corpstats.last_update|naturaltime }}
<a class="btn btn-success" type="button" href="{% url 'corputils:update' corpstats.corp.corporation_id %}" title="Update Now">
<span class="glyphicon glyphicon-refresh"></span>
</a>
</div>
<div class="clearfix"></div>
</div>
<div class="panel-body">
<div class="tab-content">
<div class="tab-pane fade in active" id="mains">
{% if mains %}
<div class="table-responsive">
<table class="table table-hover" id="table-mains">
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{% for main in mains %}
<tr>
<td class="text-center" style="vertical-align:middle">
<div class="thumbnail"
style="border: 0 none; box-shadow: none; background: transparent;">
<img src="{{ main.portrait_url_64 }}" class="img-circle">
<div class="caption text-center">
{{ main }}
</div>
</div>
</td>
<td>
<table class="table table-hover">
{% for alt in main.alts %}
{% if forloop.first %}
<tr>
<th class="text-center">{% trans "Character" %}</th>
<th class="text-center">{% trans "Corporation" %}</th>
<th class="text-center">{% trans "Alliance" %}</th>
<th class="text-center"></th>
</tr>
{% endif %}
<tr>
<td class="text-center">{{ alt.character_name }}</td>
<td class="text-center">{{ alt.corporation_name }}</td>
<td class="text-center">{{ alt.alliance_name }}</td>
<td class="text-center">
<a href="https://zkillboard.com/character/{{ alt.character_id }}/"
class="label label-danger" target="_blank">
{% trans "Killboard" %}
</a>
</td>
</tr>
{% endfor %}
</table>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
<div class="tab-pane fade" id="members">
{% if members %}
<div class="table-responsive">
<table class="table table-hover" id="table-members">
<thead>
<tr>
<th></th>
<th class="text-center">{% trans "Character" %}</th>
<th class="text-center"></th>
<th class="text-center">{% trans "Main Character" %}</th>
<th class="text-center">{% trans "Main Corporation" %}</th>
<th class="text-center">{% trans "Main Alliance" %}</th>
</tr>
</thead>
<tbody>
{% for member in members %}
<tr {% if not member.registered %}class="danger"{% endif %}>
<td><img src="{{ member.portrait_url }}" class="img-circle"></td>
<td class="text-center">{{ member.character_name }}</td>
<td class="text-center"><a
href="https://zkillboard.com/character/{{ member.character_id }}/"
class="label label-danger"
target="_blank">{% trans "Killboard" %}</a></td>
<td class="text-center">{{ member.main_character.character_name }}</td>
<td class="text-center">{{ member.main_character.corporation_name }}</td>
<td class="text-center">{{ member.main_character.alliance_name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
<div class="tab-pane fade" id="unregistered">
{% if unregistered %}
<div class="table-responsive">
<table class="table table-hover" id="table-unregistered">
<thead>
<tr>
<th></th>
<th class="text-center">{% trans "Character" %}</th>
<th class="text-center"></th>
</tr>
</thead>
<tbody>
{% for member in unregistered %}
<tr class="danger">
<td><img src="{{ member.portrait_url }}" class="img-circle"></td>
<td class="text-center">{{ member.character_name }}</td>
<td class="text-center">
<a href="https://zkillboard.com/character/{{ member.character_id }}/"
class="label label-danger"
target="_blank">
{% trans "Killboard" %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block extra_javascript %}
{% include 'bundles/datatables-js.html' %}
{% endblock %}
{% block extra_css %}
{% include 'bundles/datatables-css.html' %}
{% endblock %}
{% block extra_script %}
$(document).ready(function(){
$('#table-mains').DataTable();
$('#table-members').DataTable();
$('#table-unregistered').DataTable();
});
{% endblock %}

View File

@@ -0,0 +1,48 @@
{% extends "corputils/base.html" %}
{% load i18n %}
{% block member_data %}
<div class="panel panel-default">
<div class="panel-heading clearfix">
<div class="panel-title pull-left">{% trans "Search Results" %}</div>
</div>
<div class="panel-body">
<table class="table table-hover" id="table-search">
<thead>
<tr>
<th class="text-center"></th>
<th class="text-center">{% trans "Character" %}</th>
<th class="text-center">{% trans "Corporation" %}</th>
<th class="text-center">{% trans "zKillboard" %}</th>
<th class="text-center">{% trans "Main Character" %}</th>
<th class="text-center">{% trans "Main Corporation" %}</th>
<th class="text-center">{% trans "Main Alliance" %}</th>
</tr>
</thead>
<tbody>
{% for result in results %}
<tr {% if not result.1.registered %}class="danger"{% endif %}>
<td class="text-center"><img src="{{ result.1.portrait_url }}" class="img-circle"></td>
<td class="text-center">{{ result.1.character_name }}</td>
<td class="text-center">{{ result.0.corp.corporation_name }}</td>
<td class="text-center"><a href="https://zkillboard.com/character/{{ result.1.character_id }}/" class="label label-danger" target="_blank">{% trans "Killboard" %}</a></td>
<td class="text-center">{{ result.1.main_character.character_name }}</td>
<td class="text-center">{{ result.1.main_character.corporation_name }}</td>
<td class="text-center">{{ result.1.main_character.alliance_name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
{% block extra_javascript %}
{% include 'bundles/datatables-js.html' %}
{% endblock %}
{% block extra_css %}
{% include 'bundles/datatables-css.html' %}
{% endblock %}
{% block extra_script %}
$(document).ready(function(){
$('#table-search').DataTable();
});
{% endblock %}

View File

@@ -0,0 +1,277 @@
from unittest import mock
from django.test import TestCase
from allianceauth.tests.auth_utils import AuthUtils
from .models import CorpStats, CorpMember
from allianceauth.eveonline.models import EveCorporationInfo, EveAllianceInfo, EveCharacter
from esi.models import Token
from esi.errors import TokenError
from bravado.exception import HTTPForbidden
from django.contrib.auth.models import Permission
from allianceauth.authentication.models import CharacterOwnership
class CorpStatsManagerTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = AuthUtils.create_user('test')
AuthUtils.add_main_character(cls.user, 'test character', '1', corp_id='2', corp_name='test_corp', corp_ticker='TEST', alliance_id='3', alliance_name='TEST')
cls.user.profile.refresh_from_db()
cls.alliance = EveAllianceInfo.objects.create(alliance_id='3', alliance_name='test alliance', alliance_ticker='TEST', executor_corp_id='2')
cls.corp = EveCorporationInfo.objects.create(corporation_id='2', corporation_name='test corp', corporation_ticker='TEST', alliance=cls.alliance, member_count=1)
cls.token = Token.objects.create(user=cls.user, access_token='a', character_id='1', character_name='test character', character_owner_hash='z')
cls.corpstats = CorpStats.objects.create(corp=cls.corp, token=cls.token)
cls.view_corp_permission = Permission.objects.get_by_natural_key('view_corp_corpstats', 'corputils', 'corpstats')
cls.view_alliance_permission = Permission.objects.get_by_natural_key('view_alliance_corpstats', 'corputils', 'corpstats')
cls.view_state_permission = Permission.objects.get_by_natural_key('view_state_corpstats', 'corputils', 'corpstats')
cls.state = AuthUtils.create_state('test state', 500)
AuthUtils.assign_state(cls.user, cls.state, disconnect_signals=True)
def setUp(self):
self.user.refresh_from_db()
self.user.user_permissions.clear()
self.state.refresh_from_db()
self.state.member_corporations.clear()
self.state.member_alliances.clear()
def test_visible_superuser(self):
self.user.is_superuser = True
cs = CorpStats.objects.visible_to(self.user)
self.assertIn(self.corpstats, cs)
def test_visible_corporation(self):
self.user.user_permissions.add(self.view_corp_permission)
cs = CorpStats.objects.visible_to(self.user)
self.assertIn(self.corpstats, cs)
def test_visible_alliance(self):
self.user.user_permissions.add(self.view_alliance_permission)
cs = CorpStats.objects.visible_to(self.user)
self.assertIn(self.corpstats, cs)
def test_visible_state_corp_member(self):
self.state.member_corporations.add(self.corp)
self.user.user_permissions.add(self.view_state_permission)
cs = CorpStats.objects.visible_to(self.user)
self.assertIn(self.corpstats, cs)
def test_visible_state_alliance_member(self):
self.state.member_alliances.add(self.alliance)
self.user.user_permissions.add(self.view_state_permission)
cs = CorpStats.objects.visible_to(self.user)
self.assertIn(self.corpstats, cs)
class CorpStatsUpdateTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = AuthUtils.create_user('test')
AuthUtils.add_main_character(cls.user, 'test character', '1', corp_id='2', corp_name='test_corp', corp_ticker='TEST', alliance_id='3', alliance_name='TEST')
cls.token = Token.objects.create(user=cls.user, access_token='a', character_id='1', character_name='test character', character_owner_hash='z')
cls.corp = EveCorporationInfo.objects.create(corporation_id='2', corporation_name='test corp', corporation_ticker='TEST', member_count=1)
def setUp(self):
self.corpstats = CorpStats.objects.get_or_create(token=self.token, corp=self.corp)[0]
def test_can_update(self):
self.assertTrue(self.corpstats.can_update(self.user))
self.corpstats.token.user = None
self.assertFalse(self.corpstats.can_update(self.user))
self.user.is_superuser = True
self.assertTrue(self.corpstats.can_update(self.user))
self.user.refresh_from_db()
self.corpstats.token.refresh_from_db()
@mock.patch('esi.clients.SwaggerClient')
def test_update_add_member(self, SwaggerClient):
SwaggerClient.from_spec.return_value.Character.get_characters_character_id.return_value.result.return_value = {'corporation_id': 2}
SwaggerClient.from_spec.return_value.Corporation.get_corporations_corporation_id_members.return_value.result.return_value = [{'character_id': 1}]
SwaggerClient.from_spec.return_value.Character.get_characters_names.return_value.result.return_value = [{'character_id': 1, 'character_name': 'test character'}]
self.corpstats.update()
self.assertTrue(CorpMember.objects.filter(character_id='1', character_name='test character', corpstats=self.corpstats).exists())
@mock.patch('esi.clients.SwaggerClient')
def test_update_remove_member(self, SwaggerClient):
CorpMember.objects.create(character_id='2', character_name='old test character', corpstats=self.corpstats)
SwaggerClient.from_spec.return_value.Character.get_characters_character_id.return_value.result.return_value = {'corporation_id': 2}
SwaggerClient.from_spec.return_value.Corporation.get_corporations_corporation_id_members.return_value.result.return_value = [{'character_id': 1}]
SwaggerClient.from_spec.return_value.Character.get_characters_names.return_value.result.return_value = [{'character_id': 1, 'character_name': 'test character'}]
self.corpstats.update()
self.assertFalse(CorpMember.objects.filter(character_id='2', corpstats=self.corpstats).exists())
@mock.patch('allianceauth.corputils.models.notify')
@mock.patch('esi.clients.SwaggerClient')
def test_update_deleted_token(self, SwaggerClient, notify):
SwaggerClient.from_spec.return_value.Character.get_characters_character_id.return_value.result.return_value = {'corporation_id': 2}
SwaggerClient.from_spec.return_value.Corporation.get_corporations_corporation_id_members.return_value.result.side_effect = TokenError()
self.corpstats.update()
self.assertFalse(CorpStats.objects.filter(corp=self.corp).exists())
self.assertTrue(notify.called)
@mock.patch('allianceauth.corputils.models.notify')
@mock.patch('esi.clients.SwaggerClient')
def test_update_http_forbidden(self, SwaggerClient, notify):
SwaggerClient.from_spec.return_value.Character.get_characters_character_id.return_value.result.return_value = {'corporation_id': 2}
SwaggerClient.from_spec.return_value.Corporation.get_corporations_corporation_id_members.return_value.result.side_effect = HTTPForbidden(mock.Mock())
self.corpstats.update()
self.assertFalse(CorpStats.objects.filter(corp=self.corp).exists())
self.assertTrue(notify.called)
@mock.patch('allianceauth.corputils.models.notify')
@mock.patch('esi.clients.SwaggerClient')
def test_update_token_character_corp_changed(self, SwaggerClient, notify):
SwaggerClient.from_spec.return_value.Character.get_characters_character_id.return_value.result.return_value = {'corporation_id': 3}
self.corpstats.update()
self.assertFalse(CorpStats.objects.filter(corp=self.corp).exists())
self.assertTrue(notify.called)
class CorpStatsPropertiesTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = AuthUtils.create_user('test')
AuthUtils.add_main_character(cls.user, 'test character', '1', corp_id='2', corp_name='test_corp', corp_ticker='TEST', alliance_id='3', alliance_name='TEST')
cls.user.profile.refresh_from_db()
cls.token = Token.objects.create(user=cls.user, access_token='a', character_id='1', character_name='test character', character_owner_hash='z')
cls.corp = EveCorporationInfo.objects.create(corporation_id='2', corporation_name='test corp', corporation_ticker='TEST', member_count=1)
cls.corpstats = CorpStats.objects.create(token=cls.token, corp=cls.corp)
cls.character = EveCharacter.objects.create(character_name='another test character', character_id='4', corporation_id='2', corporation_name='test corp', corporation_ticker='TEST')
def test_member_count(self):
member = CorpMember.objects.create(corpstats=self.corpstats, character_id='1', character_name='test character')
self.assertEqual(self.corpstats.member_count, 1)
member.delete()
self.assertEqual(self.corpstats.member_count, 0)
def test_user_count(self):
AuthUtils.disconnect_signals()
co = CharacterOwnership.objects.create(character=self.character, user=self.user, owner_hash='a')
AuthUtils.connect_signals()
CorpMember.objects.create(corpstats=self.corpstats, character_id='4', character_name='test character')
self.assertEqual(self.corpstats.user_count, 1)
co.delete()
self.assertEqual(self.corpstats.user_count, 0)
def test_registered_members(self):
AuthUtils.disconnect_signals()
co = CharacterOwnership.objects.create(character=self.character, user=self.user, owner_hash='a')
AuthUtils.connect_signals()
member = CorpMember.objects.create(corpstats=self.corpstats, character_id='4', character_name='test character')
self.assertIn(member, self.corpstats.registered_members)
self.assertEqual(self.corpstats.registered_member_count, 1)
co.delete()
self.assertNotIn(member, self.corpstats.registered_members)
self.assertEqual(self.corpstats.registered_member_count, 0)
def test_unregistered_members(self):
member = CorpMember.objects.create(corpstats=self.corpstats, character_id='4', character_name='test character')
self.corpstats.refresh_from_db()
self.assertIn(member, self.corpstats.unregistered_members)
self.assertEqual(self.corpstats.unregistered_member_count, 1)
AuthUtils.disconnect_signals()
CharacterOwnership.objects.create(character=self.character, user=self.user, owner_hash='a')
AuthUtils.connect_signals()
self.assertNotIn(member, self.corpstats.unregistered_members)
self.assertEqual(self.corpstats.unregistered_member_count, 0)
def test_mains(self):
# test when is a main
member = CorpMember.objects.create(corpstats=self.corpstats, character_id='1', character_name='test character')
self.assertIn(member, self.corpstats.mains)
self.assertEqual(self.corpstats.main_count, 1)
# test when is an alt
old_main = self.user.profile.main_character
character = EveCharacter.objects.create(character_name='other character', character_id=10, corporation_name='test corp', corporation_id='2', corporation_ticker='TEST')
AuthUtils.disconnect_signals()
co = CharacterOwnership.objects.create(character=character, user=self.user, owner_hash='b')
self.user.profile.main_character = character
self.user.profile.save()
AuthUtils.connect_signals()
self.assertNotIn(member, self.corpstats.mains)
self.assertEqual(self.corpstats.main_count, 0)
# test when no ownership
co.delete()
self.assertNotIn(member, self.corpstats.mains)
self.assertEqual(self.corpstats.main_count, 0)
# transaction won't roll this back
AuthUtils.disconnect_signals()
self.user.profile.main_character = old_main
self.user.profile.save()
AuthUtils.connect_signals()
def test_logos(self):
self.assertEqual(self.corpstats.corp_logo(size=128), 'https://image.eveonline.com/Corporation/2_128.png')
self.assertEqual(self.corpstats.alliance_logo(size=128), 'https://image.eveonline.com/Alliance/1_128.png')
alliance = EveAllianceInfo.objects.create(alliance_name='test alliance', alliance_id='3', alliance_ticker='TEST', executor_corp_id='2')
self.corp.alliance = alliance
self.corp.save()
self.assertEqual(self.corpstats.alliance_logo(size=128), 'https://image.eveonline.com/Alliance/3_128.png')
alliance.delete()
class CorpMemberTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = AuthUtils.create_user('test')
AuthUtils.add_main_character(cls.user, 'test character', '1', corp_id='2', corp_name='test_corp', corp_ticker='TEST', alliance_id='3', alliance_name='TEST')
cls.user.profile.refresh_from_db()
cls.token = Token.objects.create(user=cls.user, access_token='a', character_id='1', character_name='test character', character_owner_hash='a')
cls.corp = EveCorporationInfo.objects.create(corporation_id='2', corporation_name='test corp', corporation_ticker='TEST', member_count=1)
cls.corpstats = CorpStats.objects.create(token=cls.token, corp=cls.corp)
cls.member = CorpMember.objects.create(corpstats=cls.corpstats, character_id='2', character_name='other test character')
def test_character(self):
self.assertIsNone(self.member.character)
character = EveCharacter.objects.create(character_id='2', character_name='other test character', corporation_id='2', corporation_name='test corp', corporation_ticker='TEST')
self.assertEqual(self.member.character, character)
def test_main_character(self):
AuthUtils.disconnect_signals()
# test when member.character is None
self.assertIsNone(self.member.main_character)
# test when member.character is not None but also not a main
character = EveCharacter.objects.create(character_id='2', character_name='other test character', corporation_id='2', corporation_name='test corp', corporation_ticker='TEST')
CharacterOwnership.objects.create(character=character, user=self.user, owner_hash='b')
self.member.refresh_from_db()
self.assertNotEqual(self.member.main_character, self.member.character)
self.assertEquals(self.member.main_character, self.user.profile.main_character)
# test when is main
old_main = self.user.profile.main_character
self.user.profile.main_character = character
self.user.profile.save()
self.member.refresh_from_db()
self.assertEqual(self.member.main_character, self.member.character)
self.assertEqual(self.user.profile.main_character, self.member.main_character)
# transaction won't roll this back
self.user.profile.main_character = old_main
self.user.profile.save()
AuthUtils.connect_signals()
def test_alts(self):
self.assertListEqual(self.member.alts, [])
character = EveCharacter.objects.create(character_id='2', character_name='other test character', corporation_id='2', corporation_name='test corp', corporation_ticker='TEST')
CharacterOwnership.objects.create(character=character, user=self.user, owner_hash='b')
self.assertIn(character, self.member.alts)
def test_registered(self):
self.assertFalse(self.member.registered)
AuthUtils.disconnect_signals()
character = EveCharacter.objects.create(character_id='2', character_name='other test character', corporation_id='2', corporation_name='test corp', corporation_ticker='TEST')
CharacterOwnership.objects.create(character=character, user=self.user, owner_hash='b')
self.assertTrue(self.member.registered)
AuthUtils.connect_signals()
def test_portrait_url(self):
self.assertEquals(self.member.portrait_url(size=32), 'https://image.eveonline.com/Character/2_32.jpg')
self.assertEquals(self.member.portrait_url(size=32), self.member.portrait_url_32)

View File

@@ -0,0 +1,12 @@
from django.conf.urls import url
from . import views
app_name = 'corputils'
urlpatterns = [
url(r'^$', views.corpstats_view, name='view'),
url(r'^add/$', views.corpstats_add, name='add'),
url(r'^(?P<corp_id>(\d)*)/$', views.corpstats_view, name='view_corp'),
url(r'^(?P<corp_id>(\d)+)/update/$', views.corpstats_update, name='update'),
url(r'^search/$', views.corpstats_search, name='search'),
]

View File

@@ -0,0 +1,134 @@
import os
from bravado.exception import HTTPError
from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required, user_passes_test
from django.core.exceptions import PermissionDenied
from django.db import IntegrityError
from django.shortcuts import render, redirect, get_object_or_404
from django.utils.translation import ugettext_lazy as _
from esi.decorators import token_required
from allianceauth.eveonline.managers import EveManager
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo
from .models import CorpStats
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json')
def access_corpstats_test(user):
return user.has_perm('corputils.view_corp_corpstats') or user.has_perm(
'corputils.view_alliance_corpstats') or user.has_perm('corputils.view_state_corpstats')
@login_required
@user_passes_test(access_corpstats_test)
@permission_required('corputils.add_corpstats')
@token_required(scopes='esi-corporations.read_corporation_membership.v1')
def corpstats_add(request, token):
try:
if EveCharacter.objects.filter(character_id=token.character_id).exists():
corp_id = EveCharacter.objects.get(character_id=token.character_id).corporation_id
else:
corp_id = \
token.get_esi_client(spec_file=SWAGGER_SPEC_PATH).Character.get_characters_character_id(
character_id=token.character_id).result()['corporation_id']
try:
corp = EveCorporationInfo.objects.get(corporation_id=corp_id)
except EveCorporationInfo.DoesNotExist:
corp = EveManager.create_corporation(corp_id)
cs = CorpStats.objects.create(token=token, corp=corp)
try:
cs.update()
except HTTPError as e:
messages.error(request, str(e))
assert cs.pk # ensure update was successful
if CorpStats.objects.filter(pk=cs.pk).visible_to(request.user).exists():
return redirect('corputils:view_corp', corp_id=corp.corporation_id)
except IntegrityError:
messages.error(request, _('Selected corp already has a statistics module.'))
except AssertionError:
messages.error(request, _('Failed to gather corporation statistics with selected token.'))
return redirect('corputils:view')
@login_required
@user_passes_test(access_corpstats_test)
def corpstats_view(request, corp_id=None):
corpstats = None
# get requested model
if corp_id:
corp = get_object_or_404(EveCorporationInfo, corporation_id=corp_id)
corpstats = get_object_or_404(CorpStats, corp=corp)
# get available models
available = CorpStats.objects.visible_to(request.user)
# ensure we can see the requested model
if corpstats and corpstats not in available:
raise PermissionDenied('You do not have permission to view the selected corporation statistics module.')
# get default model if none requested
if not corp_id and available.count() == 1:
corpstats = available[0]
elif not corp_id and available.count() > 1 and request.user.profile.main_character:
# get their main corp if available
try:
corpstats = available.get(corp__corporation_id=request.user.profile.main_character.corporation_id)
except CorpStats.DoesNotExist:
pass
context = {
'available': available,
}
if corpstats:
members = corpstats.members.all()
mains = corpstats.mains.all()
unregistered = corpstats.unregistered_members.all()
context.update({
'corpstats': corpstats,
'members': members,
'mains': mains,
'unregistered': unregistered,
})
return render(request, 'corputils/corpstats.html', context=context)
@login_required
@user_passes_test(access_corpstats_test)
def corpstats_update(request, corp_id):
corp = get_object_or_404(EveCorporationInfo, corporation_id=corp_id)
corpstats = get_object_or_404(CorpStats, corp=corp)
try:
corpstats.update()
except HTTPError as e:
messages.error(request, str(e))
if corpstats.pk:
return redirect('corputils:view_corp', corp_id=corp.corporation_id)
else:
return redirect('corputils:view')
@login_required
@user_passes_test(access_corpstats_test)
def corpstats_search(request):
results = []
search_string = request.GET.get('search_string', None)
if search_string:
has_similar = CorpStats.objects.filter(members__character_name__icontains=search_string).visible_to(
request.user).distinct()
for corpstats in has_similar:
similar = corpstats.members.filter(character_name__icontains=search_string)
for s in similar:
results.append((corpstats, s))
results = sorted(results, key=lambda x: x[1].character_name)
context = {
'available': CorpStats.objects.visible_to(request.user),
'results': results,
'search_string': search_string,
}
return render(request, 'corputils/search.html', context=context)
return redirect('corputils:view')