diff --git a/.travis.yml b/.travis.yml index 25e99cbb80e0a6f0c14b57c893064f94cd5e177c..cb3d3ad2cdc2b2892ef1bfd6dd11a6b6e63dc6b9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,6 @@ script: - py.test memopol representatives_positions representatives_recommendations - rm -rf db.sqlite - django-admin migrate -- django-admin update_score after_success: - codecov deploy: diff --git a/bin/update_all b/bin/update_all index 5899c9c1d257bf97c5dd9b233b09a3859535eadb..2447b218e987ffc3dda2a4fdc5d109c74c609b57 100755 --- a/bin/update_all +++ b/bin/update_all @@ -10,7 +10,3 @@ bin/update_dossiers sleep 120 bin/update_votes - -sleep 120 - -bin/update_scores diff --git a/bin/update_scores b/bin/update_scores deleted file mode 100755 index a3234bc6b047aacad51c2ac82df886f70a6b63ed..0000000000000000000000000000000000000000 --- a/bin/update_scores +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -set -ex - -source ${OPENSHIFT_REPO_DIR}bin/lib.sh - -[ -n "$OPENSHIFT_REPO_DIR" ] && cd $OPENSHIFT_REPO_DIR -./manage.py update_score diff --git a/docs/api.rst b/docs/api.rst index 5621a2d55a6182a559a4d46205d7d7814e130bec..73388fff32b88cab6e76b99a5fcade109afe6e99 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -146,12 +146,12 @@ The following fields are available for filtering: * ``weight`` (alt.: gte, lte) * ``search``: searches in the ``title`` and ``description`` fields -Scored Votes ------------- +Vote Scores +----------- -The ``/api/scored_votes/[<pk>/]`` endpoints give access to scored votes; that -is, representative votes with their contribution to the representative score. -Only votes that match a recommendation are visible using this endpoint. The +The ``/api/vote_scores/[<pk>/]`` endpoints give access to scored votes; that is, +representative votes with their contribution to the representative score. Only +votes that match a recommendation are visible using this endpoint. The following fields are available for filtering: * ``representative`` diff --git a/memopol/api.py b/memopol/api.py index 2986d2afba16ca9323dca8b6603f8117f3ea5ae4..40ae7fc6a33ed651a5ecea45fa0ed2817e7f585e 100644 --- a/memopol/api.py +++ b/memopol/api.py @@ -17,7 +17,7 @@ from representatives_recommendations.api import ( DossierScoreViewSet, RecommendationViewSet, RepresentativeScoreViewSet, - ScoredVoteViewSet + VoteScoreViewSet ) @@ -32,5 +32,5 @@ router.register(r'proposals', ProposalViewSet) router.register(r'recommendations', RecommendationViewSet) router.register(r'representatives', RepresentativeViewSet) router.register(r'scores', RepresentativeScoreViewSet) -router.register(r'scored_votes', ScoredVoteViewSet) +router.register(r'vote_scores', VoteScoreViewSet) router.register(r'votes', VoteViewSet) diff --git a/memopol/fixtures/one_representative.json b/memopol/fixtures/one_representative.json index 6997e4934d99295d781ff96857b6ed2550d064f3..c260ea3bc586f77f7080d471becb1e3817954c0c 100644 --- a/memopol/fixtures/one_representative.json +++ b/memopol/fixtures/one_representative.json @@ -1096,13 +1096,6 @@ "model": "representatives_votes.vote", "pk": 25535 }, -{ - "fields": { - "score": -7 - }, - "model": "representatives_recommendations.representativescore", - "pk": 160 -}, { "fields": { "proposal": 5744, diff --git a/memopol/fixtures/smaller_sample.json b/memopol/fixtures/smaller_sample.json index 5c00da36d59bc8d097b0c28470247e40ca7613e8..7b58065f1ff35ed3b5c676c0d188fdb01b5f3391 100644 --- a/memopol/fixtures/smaller_sample.json +++ b/memopol/fixtures/smaller_sample.json @@ -24036,258 +24036,6 @@ "model": "representatives_votes.vote", "pk": 25535 }, -{ - "fields": { - "score": 15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 717 -}, -{ - "fields": { - "score": 15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 718 -}, -{ - "fields": { - "score": -10 - }, - "model": "representatives_recommendations.representativescore", - "pk": 439 -}, -{ - "fields": { - "score": 15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 748 -}, -{ - "fields": { - "score": -15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 744 -}, -{ - "fields": { - "score": -15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 681 -}, -{ - "fields": { - "score": 15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 684 -}, -{ - "fields": { - "score": 15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 719 -}, -{ - "fields": { - "score": 15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 692 -}, -{ - "fields": { - "score": -15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 736 -}, -{ - "fields": { - "score": -15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 678 -}, -{ - "fields": { - "score": 15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 738 -}, -{ - "fields": { - "score": 10 - }, - "model": "representatives_recommendations.representativescore", - "pk": 708 -}, -{ - "fields": { - "score": -15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 699 -}, -{ - "fields": { - "score": 15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 743 -}, -{ - "fields": { - "score": -15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 646 -}, -{ - "fields": { - "score": -15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 679 -}, -{ - "fields": { - "score": -15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 705 -}, -{ - "fields": { - "score": 15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 655 -}, -{ - "fields": { - "score": 15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 746 -}, -{ - "fields": { - "score": -7 - }, - "model": "representatives_recommendations.representativescore", - "pk": 160 -}, -{ - "fields": { - "score": 15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 642 -}, -{ - "fields": { - "score": 15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 645 -}, -{ - "fields": { - "score": 15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 650 -}, -{ - "fields": { - "score": 15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 670 -}, -{ - "fields": { - "score": -15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 747 -}, -{ - "fields": { - "score": -15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 644 -}, -{ - "fields": { - "score": 15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 750 -}, -{ - "fields": { - "score": 5 - }, - "model": "representatives_recommendations.representativescore", - "pk": 282 -}, -{ - "fields": { - "score": 3 - }, - "model": "representatives_recommendations.representativescore", - "pk": 647 -}, -{ - "fields": { - "score": 15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 651 -}, -{ - "fields": { - "score": -15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 653 -}, -{ - "fields": { - "score": 15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 661 -}, -{ - "fields": { - "score": -15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 666 -}, -{ - "fields": { - "score": 15 - }, - "model": "representatives_recommendations.representativescore", - "pk": 697 -}, -{ - "fields": { - "score": -7 - }, - "model": "representatives_recommendations.representativescore", - "pk": 614 -}, { "fields": { "proposal": 5744, diff --git a/memopol/views.py b/memopol/views.py index e0f26602bcd345bc3ce32d00f2381e881bdfa6d7..9e0c6e3dba284fd2843b33eecdc50faf97a3bc46 100644 --- a/memopol/views.py +++ b/memopol/views.py @@ -7,7 +7,7 @@ from representatives.models import Representative from representatives_votes import views as representatives_votes_views from representatives_votes.models import Dossier, Proposal from representatives_positions.forms import PositionForm -from representatives_recommendations.models import ScoredVote +from representatives_recommendations.models import VoteScore class RepresentativeList( @@ -42,7 +42,7 @@ class RepresentativeDetail(representatives_views.RepresentativeDetail): def get_queryset(self): qs = super(RepresentativeDetail, self).get_queryset() - votes = ScoredVote.objects.filter( + votes = VoteScore.objects.filter( proposal__in=Proposal.objects.exclude(recommendation=None), ).select_related('proposal__recommendation') qs = qs.prefetch_related(models.Prefetch('votes', queryset=votes)) diff --git a/representatives_recommendations/api.py b/representatives_recommendations/api.py index f33575b92cc9e8f5aa3aff3d6937990fef450448..4d035c26a02fe451401ecb05436425dcc325c53b 100644 --- a/representatives_recommendations/api.py +++ b/representatives_recommendations/api.py @@ -9,14 +9,14 @@ from .models import ( DossierScore, Recommendation, RepresentativeScore, - ScoredVote + VoteScore ) from .serializers import ( DossierScoreSerializer, RecommendationSerializer, RepresentativeScoreSerializer, - ScoredVoteSerializer + VoteScoreSerializer ) @@ -85,12 +85,12 @@ class RepresentativeScoreViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = RepresentativeScoreSerializer -class ScoredVoteViewSet(viewsets.ReadOnlyModelViewSet): +class VoteScoreViewSet(viewsets.ReadOnlyModelViewSet): """ API endpoint to view votes with their score impact. This endpoint only shows votes that have a matching recommendation. """ - queryset = ScoredVote.objects.select_related( + queryset = VoteScore.objects.select_related( 'representative', 'proposal', 'proposal__dossier', @@ -112,4 +112,4 @@ class ScoredVoteViewSet(viewsets.ReadOnlyModelViewSet): } pagination_class = DefaultWebPagination - serializer_class = ScoredVoteSerializer + serializer_class = VoteScoreSerializer diff --git a/representatives_recommendations/management/commands/update_score.py b/representatives_recommendations/management/commands/update_score.py deleted file mode 100644 index cbf2452fac8922d1bbaf040d4eee5a07f66d85ef..0000000000000000000000000000000000000000 --- a/representatives_recommendations/management/commands/update_score.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.core.management.base import BaseCommand - -from representatives_recommendations.models import (RepresentativeScore, - calculate_representative_score) - - -class Command(BaseCommand): - def handle(self, *args, **options): - for score in RepresentativeScore.objects.all(): - score.score = calculate_representative_score( - score.representative) - score.save() diff --git a/representatives_recommendations/migrations/0003_votescore.py b/representatives_recommendations/migrations/0003_votescore.py new file mode 100644 index 0000000000000000000000000000000000000000..7fee085cf371c5afbb805b2d9f07f2a11f841e82 --- /dev/null +++ b/representatives_recommendations/migrations/0003_votescore.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('representatives_recommendations', '0002_dossierscore'), + ] + + operations = [ + migrations.CreateModel( + name='VoteScore', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('position', models.CharField(max_length=10)), + ('score', models.IntegerField(default=0)), + ], + options={ + 'ordering': ['proposal__datetime'], + 'db_table': 'representatives_recommendations_votescores', + 'managed': False, + }, + ), + migrations.DeleteModel( + name='ScoredVote', + ), + migrations.RunSQL( + """ + CREATE VIEW "representatives_recommendations_votescores" + AS SELECT + "representatives_votes_vote"."id", + "representatives_votes_vote"."position", + "representatives_votes_vote"."proposal_id", + "representatives_votes_vote"."representative_id", + CASE WHEN "representatives_votes_vote"."position" = ("representatives_recommendations_recommendation"."recommendation") + THEN "representatives_recommendations_recommendation"."weight" + ELSE (0 - "representatives_recommendations_recommendation"."weight") + END AS "score" + FROM "representatives_votes_vote" + INNER JOIN "representatives_votes_proposal" + ON ( "representatives_votes_vote"."proposal_id" = "representatives_votes_proposal"."id" ) + LEFT OUTER JOIN "representatives_recommendations_recommendation" + ON ( "representatives_votes_proposal"."id" = "representatives_recommendations_recommendation"."proposal_id" ) + WHERE "representatives_recommendations_recommendation"."id" IS NOT NULL + """ + ) + ] diff --git a/representatives_recommendations/migrations/0004_dossierscore_rewrite.py b/representatives_recommendations/migrations/0004_dossierscore_rewrite.py new file mode 100644 index 0000000000000000000000000000000000000000..6879a9660dbeaaae34614fcb96ff2e4c924e89ab --- /dev/null +++ b/representatives_recommendations/migrations/0004_dossierscore_rewrite.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('representatives_recommendations', '0003_votescore'), + ] + + operations = [ + migrations.RunSQL( + """ + DROP VIEW "representatives_recommendations_dossierscores" + """ + ), + migrations.RunSQL( + """ + CREATE VIEW "representatives_recommendations_dossierscores" + AS SELECT + "representatives_recommendations_votescores"."representative_id" || ':' || "representatives_votes_proposal"."dossier_id" AS "id", + "representatives_recommendations_votescores"."representative_id", + "representatives_votes_proposal"."dossier_id", + SUM("representatives_recommendations_votescores"."score") AS "score" + FROM "representatives_recommendations_votescores" + INNER JOIN "representatives_votes_proposal" + ON ( "representatives_recommendations_votescores"."proposal_id" = "representatives_votes_proposal"."id" ) + GROUP BY + "representatives_recommendations_votescores"."representative_id", + "representatives_votes_proposal"."dossier_id" + """ + ), + ] diff --git a/representatives_recommendations/migrations/0005_representativescore.py b/representatives_recommendations/migrations/0005_representativescore.py new file mode 100644 index 0000000000000000000000000000000000000000..292af629c52c6a9d97fe2e960454fa9ab5011c92 --- /dev/null +++ b/representatives_recommendations/migrations/0005_representativescore.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('representatives_recommendations', '0004_dossierscore_rewrite'), + ] + + operations = [ + migrations.AlterModelOptions( + name='representativescore', + options={ + 'managed': False, + 'db_table': 'representatives_recommendations_representativescore', + }, + ), + migrations.RunSQL( + """ + DROP TABLE "representatives_recommendations_representativescore" + """ + ), + migrations.RunSQL( + """ + CREATE VIEW "representatives_recommendations_representativescore" + AS SELECT + "representatives_representative"."id" as "representative_id", + COALESCE(SUM("representatives_recommendations_votescores"."score"), 0) AS "score" + FROM + "representatives_representative" + LEFT OUTER JOIN "representatives_recommendations_votescores" + ON "representatives_recommendations_votescores"."representative_id" = "representatives_representative"."id" + GROUP BY "representatives_representative"."id" + """ + ) + ] diff --git a/representatives_recommendations/models.py b/representatives_recommendations/models.py index 49a6ecf59c999f0f2102972bd39985cf3e60c609..5f6e64efe891a785f413500a25d479ba30d2a8c4 100644 --- a/representatives_recommendations/models.py +++ b/representatives_recommendations/models.py @@ -1,7 +1,5 @@ # coding: utf-8 from django.db import models -from django.db.models.signals import post_save -from django.utils.functional import cached_property from representatives_votes.contrib.parltrack.import_votes import \ vote_pre_import @@ -23,11 +21,29 @@ class DossierScore(models.Model): db_table = 'representatives_recommendations_dossierscores' +class VoteScore(models.Model): + proposal = models.ForeignKey(Proposal, related_name='votescores') + + representative = models.ForeignKey( + Representative, related_name='votescores', null=True) + position = models.CharField(max_length=10) + score = models.IntegerField(default=0) + + class Meta: + managed = False + ordering = ['proposal__datetime'] + db_table = 'representatives_recommendations_votescores' + + class RepresentativeScore(models.Model): representative = models.OneToOneField('representatives.representative', primary_key=True, related_name='score') score = models.IntegerField(default=0) + class Meta: + managed = False + db_table = 'representatives_recommendations_representativescore' + class Recommendation(models.Model): proposal = models.OneToOneField( @@ -44,20 +60,6 @@ class Recommendation(models.Model): ordering = ['proposal__datetime'] -class ScoredVote(Vote): - class Meta: - proxy = True - - @cached_property - def absolute_score(self): - recommendation = self.proposal.recommendation - - if self.position == recommendation.recommendation: - return recommendation.weight - else: - return -recommendation.weight - - def skip_votes(sender, vote_data=None, **kwargs): dossiers = getattr(sender, 'memopol_filters', None) @@ -75,28 +77,3 @@ def skip_representatives(sender, representative_data=None, **kwargs): if not representative_data.get('active', False): return False representative_pre_import.connect(skip_representatives) - - -def create_representative_vote_profile(sender, instance=None, created=None, - **kwargs): - - if not created: - return - - RepresentativeScore.objects.create(representative=instance) -post_save.connect(create_representative_vote_profile, sender=Representative) - - -def calculate_representative_score(representative): - score = 0 - - votes = representative.votes.exclude( - proposal__recommendation=None - ).select_related('proposal__recommendation') - - votes = ScoredVote.objects.filter(pk__in=votes.values_list('pk')) - - for vote in votes: - score += vote.absolute_score - - return score diff --git a/representatives_recommendations/serializers.py b/representatives_recommendations/serializers.py index 4a1ae0d0dc5fb94720d32bcd00a273ecb4969299..330e53e4424378b836bb6222d1d9b57cb0fd27e9 100644 --- a/representatives_recommendations/serializers.py +++ b/representatives_recommendations/serializers.py @@ -4,7 +4,7 @@ from .models import ( DossierScore, Recommendation, RepresentativeScore, - ScoredVote + VoteScore ) @@ -30,16 +30,8 @@ class RepresentativeScoreSerializer(serializers.HyperlinkedModelSerializer): fields = ('representative', 'score') -class ScoredVoteSerializer(serializers.HyperlinkedModelSerializer): - """ - Scored Vote serializer - """ +class VoteScoreSerializer(serializers.HyperlinkedModelSerializer): class Meta: - model = ScoredVote - fields = ( - 'proposal', - 'representative', - 'position', - 'absolute_score' - ) + model = VoteScore + fields = ('proposal', 'representative', 'position', 'score') diff --git a/templates/representatives/representative_detail.haml b/templates/representatives/representative_detail.haml index 2775cb9d5444361574f4bfbf0947358dd11052b9..94629ffda1bc816dceb3cb36c4007f2b58942510 100644 --- a/templates/representatives/representative_detail.haml +++ b/templates/representatives/representative_detail.haml @@ -34,7 +34,7 @@ %td.icon-cell = vote.position|position_icon %td.icon-cell - = vote.absolute_score|score_label + = vote.score|score_label %h2 Mandates