diff --git a/corputils/admin.py b/corputils/admin.py
index f3bb4062..934d066b 100644
--- a/corputils/admin.py
+++ b/corputils/admin.py
@@ -1,5 +1,6 @@
from __future__ import unicode_literals
-from corputils.models import CorpStats
+from corputils.models import CorpStats, CorpMember
from django.contrib import admin
admin.site.register(CorpStats)
+admin.site.register(CorpMember)
\ No newline at end of file
diff --git a/corputils/migrations/0004_member_models.py b/corputils/migrations/0004_member_models.py
new file mode 100644
index 00000000..6a3516cb
--- /dev/null
+++ b/corputils/migrations/0004_member_models.py
@@ -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_make_strings_more_stringy'),
+ ]
+
+ 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',
+ ),
+ ]
diff --git a/corputils/models.py b/corputils/models.py
index 39b7b905..c084f841 100644
--- a/corputils/models.py
+++ b/corputils/models.py
@@ -8,8 +8,6 @@ from notifications import notify
from authentication.models import CharacterOwnership, UserProfile
from bravado.exception import HTTPForbidden
from corputils.managers import CorpStatsManager
-from operator import attrgetter
-import json
import logging
logger = logging.getLogger(__name__)
@@ -20,7 +18,6 @@ class CorpStats(models.Model):
token = models.ForeignKey(Token, on_delete=models.CASCADE)
corp = models.OneToOneField(EveCorporationInfo)
last_update = models.DateTimeField(auto_now=True)
- _members = models.TextField(default='{}')
class Meta:
permissions = (
@@ -52,15 +49,23 @@ class CorpStats(models.Model):
# 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)]
- c = self.token.get_esi_client(Character='v1') # ccplease bump versions of whole resources
+ 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_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})
- self.members = member_list
- self.save()
+ # 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()
+
except TokenError as e:
logger.warning("%s failed to update: %s" % (self, e))
if self.token.user:
@@ -82,87 +87,95 @@ class CorpStats(models.Model):
self.delete()
@property
- def members(self):
- return json.loads(self._members)
-
- @members.setter
- def members(self, dict):
- self._members = json.dumps(dict)
-
- @property
- def member_ids(self):
- return [id for id, name in self.members.items()]
-
- @property
- def member_names(self):
- return [name for id, name in self.members.items()]
-
def member_count(self):
- return len(self.members)
+ return self.members.count()
- @staticmethod
- def user_count(members):
- mainchars = []
- for member in members:
- if hasattr(member.main, 'character_name'):
- mainchars.append(member.main.character_name)
- return len(set(mainchars))
+ @property
+ def user_count(self):
+ return len(set([m.main_character for m in self.members.all() if m.main_character]))
- def registered_characters(self):
- return len(CharacterOwnership.objects.filter(character__character_id__in=self.member_ids))
+ @property
+ def registered_member_count(self):
+ return len(self.registered_members)
- @python_2_unicode_compatible
- class MemberObject(object):
- def __init__(self, character_id, character_name):
- self.character_id = character_id
- self.character_name = character_name
- try:
- char = EveCharacter.objects.get(character_id=character_id)
- self.main_user = char.character_ownership.user
- self.main = self.main_user.profile.main_character
- self.registered = True
- except (EveCharacter.DoesNotExist, CharacterOwnership.DoesNotExist, UserProfile.DoesNotExist, AttributeError):
- self.main = None
- self.registered = False
- self.main_user = ''
+ @property
+ def registered_members(self):
+ return self.members.filter(pk__in=[m.pk for m in self.members.all() if m.registered])
- def __str__(self):
- return self.character_name
+ @property
+ def unregistered_member_count(self):
+ return self.member_count - self.registered_member_count
- def portrait_url(self, size=32):
- return "https://image.eveonline.com/Character/%s_%s.jpg" % (self.character_id, size)
+ @property
+ def unregistered_members(self):
+ return self.members.filter(pk__in=[m.pk for m in self.members.all() if not m.registered])
- def get_member_objects(self):
- member_list = [CorpStats.MemberObject(id, name) for id, name in self.members.items()]
- outlist = sorted([m for m in member_list if m.main_user], key=attrgetter('main_user', 'character_name'))
- outlist = outlist + sorted([m for m in member_list if not m.main_user], key=attrgetter('character_name'))
- return outlist
+ @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
- @python_2_unicode_compatible
- class ViewModel(object):
- def __init__(self, corpstats, user):
- self.corp = corpstats.corp
- self.members = corpstats.get_member_objects()
- self.can_update = corpstats.can_update(user)
- self.total_members = len(self.members)
- self.total_users = corpstats.user_count(self.members)
- self.registered_members = corpstats.registered_characters()
- self.last_updated = corpstats.last_update
+ def corp_logo(self, size=128):
+ return "https://image.eveonline.com/Corporation/%s_%s.png" % (self.corp.corporation_id, size)
- def __str__(self):
- return str(self.corp)
+ 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
- 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')
- def get_view_model(self, user):
- return CorpStats.ViewModel(self, user)
+ 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)
\ No newline at end of file
diff --git a/corputils/templates/corputils/corpstats.html b/corputils/templates/corputils/corpstats.html
index 2ba17dc2..ceebadea 100644
--- a/corputils/templates/corputils/corpstats.html
+++ b/corputils/templates/corputils/corpstats.html
@@ -4,55 +4,108 @@
{% load bootstrap_pagination %}
{% load eveonline_extras %}
{% block member_data %}
- {% if corpstats %}
-
-
-
-
-  |
- {% if corpstats.corp.alliance %}
-  |
- {% endif %}
-
-
- {{ corpstats.corp.corporation_name }} |
- {% if corpstats.corp.alliance %}
- {{ corpstats.corp.alliance.alliance_name }} |
- {% endif %}
-
-
+ {% if corpstats %}
+
+
+
+
+  |
+ {% if corpstats.corp.alliance %}
+
+ |
+ {% endif %}
+
+
+ {{ corpstats.corp.corporation_name }} |
+ {% if corpstats.corp.alliance %}
+ {{ corpstats.corp.alliance.alliance_name }} |
+ {% endif %}
+
+
+
+
+
+
+
{% trans "Registration Index" %}
+
+
+ {{ corpstats.registered_member_count }}/{{ corpstats.member_count }}
-
-
-
{% trans "Registration Index: " %} {{ corpstats.total_users }} Main Character{{ corpstats.total_users|pluralize }}
-
-
- {{ corpstats.registered_members }}/{{ corpstats.total_members }}
-
+
+
+
+
+
+
+
+
+ {% trans "Last update:" %} {{ corpstats.last_update|naturaltime }}
+
+
+
+
-
-
-
-
-
-
-
{% trans "Members" %}
-
-
- {% trans "Last update:" %} {{ corpstats.last_updated|naturaltime }}
- {% if corpstats.can_update %}
-
-
-
- {% endif %}
+
+
+
+
{% bootstrap_paginate mains range=10 %}
+
+
+
+ {% trans "Main" %} |
+ {% trans "Characters" %} |
+
+ {% for main in mains %}
+
+
+
+ 
+
+ {{ main }}
+
+
+ |
+
+
+ {% for alt in main.alts %}
+ {% if forloop.first %}
+
+ {% trans "Character" %} |
+ {% trans "zKillboard" %} |
+ {% trans "Corporation" %} |
+ {% trans "Alliance" %} |
+
+ {% endif %}
+
+ {{ alt.character_name }} |
+ {% trans "Killboard" %} |
+ {{ alt.corporation_name }} |
+ {{ alt.alliance_name }} |
+
+ {% endfor %}
+
+ |
+
+ {% endfor %}
+
-
-
- {% bootstrap_paginate members range=10 %}
-
+
+
{% bootstrap_paginate members range=10 %}
@@ -67,10 +120,38 @@
 |
{{ member.character_name }} |
- {% trans "Killboard" %} |
- {{ member.main.character_name }} |
- {{ member.main.corporation_name }} |
- {{ member.main.alliance_name }} |
+ {% trans "Killboard" %} |
+ {{ member.main_character.character_name }} |
+ {{ member.main_character.corporation_name }} |
+ {{ member.main_character.alliance_name }} |
+
+ {% endfor %}
+
+
+
+
+
{% bootstrap_paginate unregistered range=10 %}
+
+
+
+ |
+ {% trans "Character" %} |
+ {% trans "zKillboard" %} |
+
+ {% for member in unregistered %}
+
+  |
+ {{ member.character_name }} |
+
+
+ {% trans "Killboard" %}
+
+ |
{% endfor %}
@@ -79,5 +160,7 @@
- {% endif %}
+
+
+ {% endif %}
{% endblock %}
diff --git a/corputils/templates/corputils/search.html b/corputils/templates/corputils/search.html
index c80d36ec..dfe65418 100644
--- a/corputils/templates/corputils/search.html
+++ b/corputils/templates/corputils/search.html
@@ -27,9 +27,9 @@
{{ result.1.character_name }} |
{{ result.0.corp.corporation_name }} |
{% trans "Killboard" %} |
-
{{ result.1.main.character_name }} |
-
{{ result.1.main.corporation_name }} |
-
{{ result.1.main.alliance_name }} |
+
{{ result.1.main_character.character_name }} |
+
{{ result.1.main_character.corporation_name }} |
+
{{ result.1.main_character.alliance_name }} |
{% endfor %}
diff --git a/corputils/views.py b/corputils/views.py
index aa6053ea..07dd478f 100644
--- a/corputils/views.py
+++ b/corputils/views.py
@@ -94,14 +94,20 @@ def corpstats_view(request, corp_id=None):
# paginate
members = []
+ mains = []
+ unregistered = []
if corpstats:
page = request.GET.get('page', 1)
- members = get_page(corpstats.get_member_objects(), page)
+ members = get_page(corpstats.members.all(), page)
+ mains = get_page(corpstats.mains.all(), page)
+ unregistered = get_page(corpstats.unregistered_members.all(), page)
if corpstats:
context.update({
- 'corpstats': corpstats.get_view_model(request.user),
+ 'corpstats': corpstats,
'members': members,
+ 'mains': mains,
+ 'unregistered': unregistered,
})
return render(request, 'corputils/corpstats.html', context=context)
@@ -112,14 +118,10 @@ def corpstats_view(request, corp_id=None):
def corpstats_update(request, corp_id):
corp = get_object_or_404(EveCorporationInfo, corporation_id=corp_id)
corpstats = get_object_or_404(CorpStats, corp=corp)
- if corpstats.can_update(request.user):
- try:
- corpstats.update()
- except HTTPError as e:
- messages.error(request, str(e))
- else:
- raise PermissionDenied(
- 'You do not have permission to update member data for the selected corporation statistics module.')
+ 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:
@@ -132,13 +134,11 @@ def corpstats_search(request):
results = []
search_string = request.GET.get('search_string', None)
if search_string:
- has_similar = CorpStats.objects.filter(_members__icontains=search_string).visible_to(request.user)
+ has_similar = CorpStats.objects.filter(members__character_name__icontains=search_string).visible_to(request.user).distinct()
for corpstats in has_similar:
- similar = [(member_id, corpstats.members[member_id]) for member_id in corpstats.members if
- search_string.lower() in corpstats.members[member_id].lower()]
+ similar = corpstats.members.filter(character_name__icontains=search_string)
for s in similar:
- results.append(
- (corpstats, CorpStats.MemberObject(s[0], s[1])))
+ results.append((corpstats, s))
page = request.GET.get('page', 1)
results = sorted(results, key=lambda x: x[1].character_name)
results_page = get_page(results, page)