Commit 365ec31a authored by njoyard's avatar njoyard
Browse files

Merge pull request #78 from political-memory/rewrite-scores

Rewrite score computing, fixes #69
parents 922c2f74 fcb1226b
......@@ -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:
......
......@@ -10,7 +10,3 @@ bin/update_dossiers
sleep 120
bin/update_votes
sleep 120
bin/update_scores
#!/bin/bash
set -ex
source ${OPENSHIFT_REPO_DIR}bin/lib.sh
[ -n "$OPENSHIFT_REPO_DIR" ] && cd $OPENSHIFT_REPO_DIR
./manage.py update_score
......@@ -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``
......
......@@ -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)
......@@ -1096,13 +1096,6 @@
"model": "representatives_votes.vote",
"pk": 25535
},
{
"fields": {
"score": -7
},
"model": "representatives_recommendations.representativescore",
"pk": 160
},
{
"fields": {
"proposal": 5744,
......
......@@ -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,
......
......@@ -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))
......
......@@ -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
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()
# -*- 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
"""
)
]
# -*- 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"
"""
),
]
# -*- 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"
"""
)
]
# 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