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 ' <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