From ee22f2c4e07e711d7c4bb22025e86d8e7bdd9ea4 Mon Sep 17 00:00:00 2001
From: Arnaud Fabre <arnaud.fabre@camobscura.fr>
Date: Wed, 1 Jul 2015 12:23:37 +0200
Subject: [PATCH] votes / legislature models

---
 .gitignore                                    |   6 -
 legislature/migrations/0001_initial.py        |  36 ------
 legislature/migrations/__init__.py            |   0
 legislature/models.py                         |  86 ++++++++-----
 .../legislature/representative_view.haml      |   3 +
 legislature/views.py                          |   4 +-
 representatives                               |   1 +
 votes/admin.py                                |  35 +++++-
 .../{admin_import_vote.py => admin_views.py}  |   5 +-
 votes/admin_views_flymake.py                  | 117 ++++++++++++++++++
 votes/migrations/0002_auto_20150616_1516.py   |  26 ++++
 votes/migrations/0003_auto_20150616_1523.py   |  26 ++++
 votes/migrations/0004_auto_20150616_1527.py   |  21 ++++
 votes/migrations/0005_auto_20150617_1243.py   |  26 ++++
 votes/models.py                               |  44 ++++++-
 votes/tasks.py                                |  44 +++++++
 votes/templates/votes/admin/import.haml       |   2 +-
 17 files changed, 395 insertions(+), 87 deletions(-)
 delete mode 100644 legislature/migrations/0001_initial.py
 delete mode 100644 legislature/migrations/__init__.py
 create mode 120000 representatives
 rename votes/{admin_import_vote.py => admin_views.py} (96%)
 create mode 100644 votes/admin_views_flymake.py
 create mode 100644 votes/migrations/0002_auto_20150616_1516.py
 create mode 100644 votes/migrations/0003_auto_20150616_1523.py
 create mode 100644 votes/migrations/0004_auto_20150616_1527.py
 create mode 100644 votes/migrations/0005_auto_20150617_1243.py
 create mode 100644 votes/tasks.py

diff --git a/.gitignore b/.gitignore
index 651fa196..91a88847 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,3 @@
-# Django apps
-
-compotista_django-representatives
-representatives
-chronograph
-
 *.sqlite3
 
 # SASS Cache
diff --git a/legislature/migrations/0001_initial.py b/legislature/migrations/0001_initial.py
deleted file mode 100644
index c7559608..00000000
--- a/legislature/migrations/0001_initial.py
+++ /dev/null
@@ -1,36 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
-from django.db import models, migrations
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('representatives', '0001_initial'),
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='MemopolGroup',
-            fields=[
-                ('group', models.OneToOneField(parent_link=True, primary_key=True, serialize=False, to='representatives.Group')),
-                ('active', models.BooleanField(default=False)),
-            ],
-            options={
-            },
-            bases=('representatives.group',),
-        ),
-        migrations.CreateModel(
-            name='MemopolRepresentative',
-            fields=[
-                ('representative_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='representatives.Representative')),
-                ('representative_remote_id', models.CharField(unique=True, max_length=255)),
-                ('score', models.IntegerField(default=0)),
-                ('country', models.ForeignKey(to='representatives.Country', null=True)),
-            ],
-            options={
-            },
-            bases=('representatives.representative',),
-        ),
-    ]
diff --git a/legislature/migrations/__init__.py b/legislature/migrations/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/legislature/models.py b/legislature/models.py
index ef1faec5..834d4fdc 100644
--- a/legislature/models.py
+++ b/legislature/models.py
@@ -22,11 +22,29 @@ from datetime import datetime
 
 from django.db import models
 from django.core.exceptions import ObjectDoesNotExist
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+from django.utils.functional import cached_property
 
-from representatives.models import Representative, Group, Country
+from representatives.models import Representative, Mandate, Country
 from representatives_votes.models import Vote
+from core.utils import create_child_instance_from_parent
 
-class MemopolRepresentative(Representative):
+
+class MemopolRepresentative(models.Model):
+
+    # We should link a memopol representative to a representative based
+    # on the remote_id attribute
+    parent_identifier = 'remote_id'
+    child_parent_identifier = 'representative_remote_id'
+    
+    representative = models.OneToOneField(
+        Representative,
+        parent_link=True,
+        related_name='extra',
+        null=True,
+        on_delete=models.SET_NULL
+    )
     
     representative_remote_id = models.CharField(max_length=255, unique=True)
     country = models.ForeignKey(Country, null=True)
@@ -34,14 +52,23 @@ class MemopolRepresentative(Representative):
     
     def update_score(self):
         score = 0
-        for vote in self.representative.votes.all():
-            proposal = vote.m_proposal
-            if proposal.recommendation:
-                recommendation = proposal.recommendation
-                if vote.position != recommendation.recommendation:
-                    score -= recommendation.weight
-                else:
-                    score += recommendation.weight
+        for vote in self.votes.all():
+            proposal = vote.proposal
+            try:
+                if proposal.recommendation:
+                    recommendation = proposal.recommendation
+                    if ( vote.position != recommendation.recommendation
+                         and (
+                             vote.position == 'abstain' or
+                             recommendation.recommendation == 'abstain' )):
+                        score -= (recommendation.weight / 2)
+                    elif vote.position != recommendation.recommendation:
+                        score -= recommendation.weight
+                    else:
+                        score += recommendation.weight
+            except Exception:
+                pass
+
         self.score = score
         self.save()
 
@@ -66,11 +93,11 @@ class MemopolRepresentative(Representative):
             self.save()
 
 
-    # @property
-    # def votes(self):
-        # return Vote.objects.filter(
-            # representative_remote_id = self.remote_id
-        # )
+    @cached_property
+    def votes(self):
+        return Vote.objects.filter(
+            representative_remote_id = self.remote_id
+        )
 
     def active_mandates(self):
         return self.mandates.filter(
@@ -88,20 +115,19 @@ class MemopolRepresentative(Representative):
             group__kind='group'
         )
 
-class MemopolGroup(Group):
-    group = models.OneToOneField(
-        Group,
-        parent_link = True
-    )
-    
-    active = models.BooleanField(default=False)
-
-    def update_active(self):
-        self.active = False
-        for mandate in self.mandates.all():
-            if mandate.end_date > datetime.date(datetime.now()):
-                self.active = True
-                break
-        self.save()
+@receiver(post_save, sender=Representative)
+def create_memopolrepresentative_from_representative(instance, **kwargs):
+    # create_child_instance_from_parent(MemopolRepresentative, instance)
+    pass
+
+
+@receiver(post_save, sender=Mandate)
+def update_memopolrepresentative_country(instance, created, **kwargs):
+    return
+    if not created:
+        return
 
+    # Update representative country
+    if instance.group.kind == 'country' and instance.representative.extra.country == None:
+        instance.representative.extra.update_country()
 
diff --git a/legislature/templates/legislature/representative_view.haml b/legislature/templates/legislature/representative_view.haml
index 2765b1e4..2ccd1347 100644
--- a/legislature/templates/legislature/representative_view.haml
+++ b/legislature/templates/legislature/representative_view.haml
@@ -9,6 +9,9 @@
     
   %h1= representative.full_name
 
+  %h2
+    SCORE : {{ representative.extra.score }}
+
   %p
     %strong
       %a{:href => "{{ representative.current_group_mandate|by_group_url }}"}
diff --git a/legislature/views.py b/legislature/views.py
index 334892a4..56299c43 100644
--- a/legislature/views.py
+++ b/legislature/views.py
@@ -132,8 +132,8 @@ def _render_list(request, representative_list, num_by_page=30):
 def groups_by_kind(request, kind):
     groups = Group.objects.filter(
         kind=kind,
-        memopolgroup__active=True
-    )
+        mandates__end_date__gte=datetime.now()
+    ).distinct().order_by('name')
 
     return render(
         request,
diff --git a/representatives b/representatives
new file mode 120000
index 00000000..f6fb692f
--- /dev/null
+++ b/representatives
@@ -0,0 +1 @@
+../django-representatives/representatives
\ No newline at end of file
diff --git a/votes/admin.py b/votes/admin.py
index ef533b9c..3542eee6 100644
--- a/votes/admin.py
+++ b/votes/admin.py
@@ -20,16 +20,45 @@
 from __future__ import absolute_import
 
 from django.contrib import admin
+from django.core.urlresolvers import reverse
+
+from .admin_views import import_vote_with_recommendation, import_vote
+from .models import Recommendation, MemopolDossier
 
-from .admin_import_vote import import_vote_with_recommendation, import_vote
-from .models import Recommendation
 
 admin.site.register_view('import_vote', view=import_vote)
 admin.site.register_view('import_vote_with_recommendation', view=import_vote_with_recommendation)
 
+def link_to_edit(obj, field):
+    try:
+        related_obj = getattr(obj, field)
+        url = reverse(
+            'admin:{}_{}_change'.format(
+                related_obj._meta.app_label,
+            related_obj._meta.object_name.lower()
+            ),
+            args=(related_obj.pk,)
+
+        )
+        return '&nbsp;<strong><a href="{url}">{obj}</a></strong>'.format(url=url,obj=related_obj)
+        
+    except:
+        return '???'
+    
+class MemopolDossierAdmin(admin.ModelAdmin):
+    
+    list_display = ('name', 'dossier')
+    search_fields = ('name',)
+
 
 class RecommendationsAdmin(admin.ModelAdmin):
-    list_display = ('title', 'recommendation', 'proposal', 'weight')
+
+    def link_to_proposal(self):
+        return link_to_edit(self, 'proposal')
+    link_to_proposal.allow_tags = True
+
+    list_display = ('id', 'title', link_to_proposal, 'recommendation','weight')
     search_fields = ('title', 'recommendation', 'proposal')
 
+admin.site.register(MemopolDossier, MemopolDossierAdmin)
 admin.site.register(Recommendation, RecommendationsAdmin)
diff --git a/votes/admin_import_vote.py b/votes/admin_views.py
similarity index 96%
rename from votes/admin_import_vote.py
rename to votes/admin_views.py
index f192d0ab..b2ea1798 100644
--- a/votes/admin_import_vote.py
+++ b/votes/admin_views.py
@@ -106,10 +106,9 @@ def import_vote(request):
     else:
         proposal_id = request.GET.get('import', None)
         if proposal_id:
-            api_url = '{}/api/proposals/{}'.format(toutatis_server, proposal_id)
-            proposal = requests.get(api_url).json()
+            # api_url = '{}/api/proposals/{}'.format(toutatis_server, proposal_id)
+            # proposal = requests.get(api_url).json()
 
-            
             call_command('import_proposal_from_toutatis', proposal_id, interactive=False)
             # call_command('update_memopol_votes', proposal['dossier_reference'], interactive=False)
             return redirect('/admin/')
diff --git a/votes/admin_views_flymake.py b/votes/admin_views_flymake.py
new file mode 100644
index 00000000..b2ea1798
--- /dev/null
+++ b/votes/admin_views_flymake.py
@@ -0,0 +1,117 @@
+# coding: utf-8
+
+# This file is part of memopol.
+#
+# memopol is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of
+# the License, or any later version.
+#
+# memopol is distributed in the hope that it will
+# be useful, but WITHOUT ANY WARRANTY; without even the implied
+# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+# See the GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU General Affero Public
+# License along with django-representatives.
+# If not, see <http://www.gnu.org/licenses/>.
+#
+# Copyright (C) 2015 Arnaud Fabre <af@laquadrature.net>
+from __future__ import absolute_import
+
+from django.conf import settings
+from django.shortcuts import render, redirect
+from django import forms
+from django.core.management import call_command
+
+import requests
+
+from representatives_votes.models import Proposal
+from .forms import RecommendationForm
+
+    
+class SearchForm(forms.Form):
+    query = forms.CharField(label='Search', max_length=100)
+
+
+def import_vote_with_recommendation(request):
+    context = {}
+    toutatis_server = getattr(settings,
+                              'TOUTATIS_SERVER',
+                              'http://toutatis.mm.staz.be')
+    
+    if request.method == 'POST' and 'search' in request.POST:
+        form = SearchForm(request.POST) 
+        if form.is_valid():
+            query = form.cleaned_data['query']
+            context['api_url'] = '{}/api/proposals/?search={}&limit=1000'.format(
+                toutatis_server,
+                query
+            )
+            r = requests.get(context['api_url'])
+            context['results'] = r.json()
+    elif request.method == 'POST' and 'create_recommendation' in request.POST:
+        form = RecommendationForm(data=request.POST)
+        if form.is_valid():
+            # First import proposal
+            proposal_id = int(request.POST['proposal_id'])
+            api_url = '{}/api/proposals/{}'.format(toutatis_server, proposal_id)
+            proposal = requests.get(api_url).json()
+            
+            call_command('import_proposal_from_toutatis', proposal_id, interactive=False)
+            # call_command('update_memopol_votes', proposal['dossier_reference'], interactive=False)
+
+            memopol_proposal = Proposal.objects.get(
+                title = proposal['title'],
+                datetime = proposal['datetime'],
+                kind = proposal['kind'],
+            )
+            recommendation = form.save(commit=False)
+            recommendation.proposal = memopol_proposal
+            recommendation.save()
+
+            return redirect('/admin/votes/recommendation/')
+    else:
+        proposal_id = request.GET.get('import', None)
+        if proposal_id:
+            api_url = '{}/api/proposals/{}'.format(toutatis_server, proposal_id)
+            proposal = requests.get(api_url).json()
+
+            context['recommendation_proposal_title'] = proposal['title']
+            context['recommendation_proposal_dossier_title'] = proposal['dossier_title']
+            context['recommendation_proposal_id'] = proposal_id
+            context['recommendation_form'] = RecommendationForm()
+        form = SearchForm()
+        
+    context['form'] = form
+    return render(request, 'votes/admin/import.html', context)
+
+def import_vote(request):
+    context = {}
+    toutatis_server = getattr(settings,
+                              'TOUTATIS_SERVER',
+                              'http://toutatis.mm.staz.be')
+    
+    if request.method == 'POST' and 'search' in request.POST:
+        print(request.POST)
+        form = SearchForm(request.POST) 
+        if form.is_valid():
+            query = form.cleaned_data['query']
+            context['api_url'] = '{}/api/proposals/?search={}&limit=1000'.format(
+                toutatis_server,
+                query
+            )
+            r = requests.get(context['api_url'])
+            context['results'] = r.json()
+    else:
+        proposal_id = request.GET.get('import', None)
+        if proposal_id:
+            # api_url = '{}/api/proposals/{}'.format(toutatis_server, proposal_id)
+            # proposal = requests.get(api_url).json()
+
+            call_command('import_proposal_from_toutatis', proposal_id, interactive=False)
+            # call_command('update_memopol_votes', proposal['dossier_reference'], interactive=False)
+            return redirect('/admin/')
+        form = SearchForm()
+    context['form'] = form
+    return render(request, 'votes/admin/import.html', context)
diff --git a/votes/migrations/0002_auto_20150616_1516.py b/votes/migrations/0002_auto_20150616_1516.py
new file mode 100644
index 00000000..a618c655
--- /dev/null
+++ b/votes/migrations/0002_auto_20150616_1516.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import core.fields
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('representatives_votes', '0002_auto_20150616_1249'),
+        ('votes', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='memopoldossier',
+            name='dossier_ptr',
+        ),
+        migrations.AddField(
+            model_name='memopoldossier',
+            name='dossier',
+            field=core.fields.AutoOneToOneField(primary_key=True, default=0, serialize=False, to='representatives_votes.Dossier'),
+            preserve_default=False,
+        ),
+    ]
diff --git a/votes/migrations/0003_auto_20150616_1523.py b/votes/migrations/0003_auto_20150616_1523.py
new file mode 100644
index 00000000..87d21ba3
--- /dev/null
+++ b/votes/migrations/0003_auto_20150616_1523.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('votes', '0002_auto_20150616_1516'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='memopoldossier',
+            name='description',
+            field=models.TextField(default=b'', blank=True),
+            preserve_default=True,
+        ),
+        migrations.AlterField(
+            model_name='memopoldossier',
+            name='name',
+            field=models.CharField(default=b'', max_length=1000, blank=True),
+            preserve_default=True,
+        ),
+    ]
diff --git a/votes/migrations/0004_auto_20150616_1527.py b/votes/migrations/0004_auto_20150616_1527.py
new file mode 100644
index 00000000..74cd4b02
--- /dev/null
+++ b/votes/migrations/0004_auto_20150616_1527.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import core.fields
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('votes', '0003_auto_20150616_1523'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='memopoldossier',
+            name='dossier',
+            field=core.fields.AutoOneToOneField(parent_link=True, related_name='extra', primary_key=True, serialize=False, to='representatives_votes.Dossier'),
+            preserve_default=True,
+        ),
+    ]
diff --git a/votes/migrations/0005_auto_20150617_1243.py b/votes/migrations/0005_auto_20150617_1243.py
new file mode 100644
index 00000000..0f0f2dc3
--- /dev/null
+++ b/votes/migrations/0005_auto_20150617_1243.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('votes', '0004_auto_20150616_1527'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='memopoldossier',
+            name='dossier_reference',
+            field=models.CharField(default='', max_length=200),
+            preserve_default=False,
+        ),
+        migrations.AlterField(
+            model_name='memopoldossier',
+            name='dossier',
+            field=models.OneToOneField(parent_link=True, related_name='extra', primary_key=True, serialize=False, to='representatives_votes.Dossier'),
+            preserve_default=True,
+        ),
+    ]
diff --git a/votes/models.py b/votes/models.py
index 8e1b1dda..26ade88a 100644
--- a/votes/models.py
+++ b/votes/models.py
@@ -19,16 +19,21 @@
 # Copyright (C) 2015 Arnaud Fabre <af@laquadrature.net>
 
 from django.db import models
+from django.utils.functional import cached_property
+from django.db.models.signals import post_save
+from django.dispatch import receiver
 
 from representatives_votes.models import Vote, Proposal, Dossier
 from legislature.models import MemopolRepresentative
+from core.utils import create_child_instance_from_parent
+from .tasks import update_representatives_score_for_proposal
+
 
 class Recommendation(models.Model):
     SCORE_TABLE = {
         ('abstain', 'abstain'): 1,
         ('abstain', 'for'): -0.5,
         ('abstain', 'against'): -0.5,
-        
     }
 
     VOTECHOICES = (
@@ -36,27 +41,54 @@ class Recommendation(models.Model):
         ('for', 'for'),
         ('against', 'against')
     )
-    
+
     proposal = models.OneToOneField(
         Proposal,
         related_name='recommendation'
     )
-    
+
     recommendation = models.CharField(max_length=10, choices=VOTECHOICES)
     title = models.CharField(max_length=1000, blank=True)
     description = models.TextField(blank=True)
     weight = models.IntegerField(default=0)
 
 
+@receiver(post_save, sender=Recommendation)
+def update_score(instance, **kwargs):
+    update_representatives_score_for_proposal(instance.proposal)
+
+
 class MemopolDossier(Dossier):
-    name = models.CharField(max_length=1000)
-    description = models.TextField(blank=True)
+    parent_identifier = 'reference'
+    child_parent_identifier = 'dossier_reference'
+
+    dossier = models.OneToOneField(
+        Dossier,
+        primary_key=True,
+        parent_link=True,
+        related_name='extra'
+    )
+
+    dossier_reference = models.CharField(max_length=200)
+    name = models.CharField(max_length=1000, blank=True, default='')
+    description = models.TextField(blank=True, default='')
+
+    def save(self, *args, **kwargs):
+        if not self.name:
+            self.name = self.dossier.title
+        return super(MemopolDossier, self).save(*args, **kwargs)
+
+
+@receiver(post_save, sender=Dossier)
+def create_memopolrepresentative_from_representative(instance, **kwargs):
+    create_child_instance_from_parent(MemopolDossier, instance)
+
 
 class MemopolVote(Vote):
     class Meta:
         proxy = True
 
-    @property
+    @cached_property
     def representative(self):
         return MemopolRepresentative.objects.get(
             remote_id = self.representative_remote_id
diff --git a/votes/tasks.py b/votes/tasks.py
new file mode 100644
index 00000000..e57cd7b5
--- /dev/null
+++ b/votes/tasks.py
@@ -0,0 +1,44 @@
+# coding: utf-8
+
+# This file is part of memopol.
+#
+# memopol is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of
+# the License, or any later version.
+#
+# memopol is distributed in the hope that it will
+# be useful, but WITHOUT ANY WARRANTY; without even the implied
+# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+# See the GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU General Affero Public
+# License along with django-representatives.
+# If not, see <http://www.gnu.org/licenses/>.
+#
+# Copyright (C) 2015 Arnaud Fabre <af@laquadrature.net>
+
+from __future__ import absolute_import
+
+from celery import shared_task
+
+from legislature.models import MemopolRepresentative
+from django.db.models import get_model
+
+@shared_task
+def update_representatives_score():
+    '''
+    Update score for all representatives
+    '''
+    for representative in MemopolRepresentative.objects.all():
+        representative.update_score()
+
+@shared_task
+def update_representatives_score_for_proposal(proposal):
+    '''
+    Update score for representatives that have votes for proposal
+    '''
+    MemopolVote = get_model('votes', 'MemopolVote')
+    for vote in MemopolVote.objects.filter(proposal_id = proposal.id):
+        # Extra is the MemopolRepresentative object
+        vote.representative.extra.update_score()
diff --git a/votes/templates/votes/admin/import.haml b/votes/templates/votes/admin/import.haml
index 833dfb26..844b1e7c 100644
--- a/votes/templates/votes/admin/import.haml
+++ b/votes/templates/votes/admin/import.haml
@@ -8,7 +8,7 @@
 
   %form{:action => '', :method => 'post'}
     - csrf_token
-    {{ form }}.
+    {{ form }}
     %input{:type => 'submit', :value => 'Search', :name => 'search'}
 
   - if results
-- 
GitLab