From efb3dcdc2841d0d4a005a834cd1138f6eec036df Mon Sep 17 00:00:00 2001
From: Nicolas Joyard <joyard.nicolas@gmail.com>
Date: Wed, 29 Jun 2016 20:35:25 +0200
Subject: [PATCH] Change score calculation to exponential decay

---
 memopol_settings/fixtures/score_settings.json | 30 ++++++++
 .../migrations/0002_score_settings.py         | 34 +++++++++
 .../migrations/0006_score_formula.py          | 76 +++++++++++++++++++
 representatives_recommendations/models.py     |  8 +-
 4 files changed, 144 insertions(+), 4 deletions(-)
 create mode 100644 memopol_settings/fixtures/score_settings.json
 create mode 100644 memopol_settings/migrations/0002_score_settings.py
 create mode 100644 representatives_recommendations/migrations/0006_score_formula.py

diff --git a/memopol_settings/fixtures/score_settings.json b/memopol_settings/fixtures/score_settings.json
new file mode 100644
index 00000000..01ecf76b
--- /dev/null
+++ b/memopol_settings/fixtures/score_settings.json
@@ -0,0 +1,30 @@
+[{
+  "model": "memopol_settings.setting",
+  "pk": "SCORE_DECAY_NUM",
+  "fields": {
+    "value": "0",
+    "comment": "Numerator for decay rate in score formula. Set to 0 for no decay, 1 otherwise. Score formula is base_score * exp( -(vote_age*DECAY_NUM/DECAY_DENOM)^(2*EXPONENT) )."
+  }
+},
+{
+  "model": "memopol_settings.setting",
+  "pk": "SCORE_DECAY_DENOM",
+  "fields": {
+    "value": "1",
+    "comment": "Denominator for decay rate in score formula. Must be nonzero. Set to higher value to delay the score decay. Score formula is base_score * exp( -(vote_age*DECAY_NUM/DECAY_DENOM)^(2*EXPONENT) )."
+  }
+},{
+  "model": "memopol_settings.setting",
+  "pk": "SCORE_EXPONENT",
+  "fields": {
+    "value": "1",
+    "comment": "Exponent for score formula. Set to higher value for a steeper decay around the cutoff. Score formula is base_score * exp( -(vote_age*DECAY_NUM/DECAY_DENOM)^(2*EXPONENT) )."
+  }
+},{
+  "model": "memopol_settings.setting",
+  "pk": "SCORE_DECIMALS",
+  "fields": {
+    "value": "0",
+    "comment": "Number of score decimals to display. Use 0 for integers."
+  }
+}]
\ No newline at end of file
diff --git a/memopol_settings/migrations/0002_score_settings.py b/memopol_settings/migrations/0002_score_settings.py
new file mode 100644
index 00000000..00c7098b
--- /dev/null
+++ b/memopol_settings/migrations/0002_score_settings.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import os
+
+from django.core import serializers
+from django.db import migrations
+
+fixture_dir = os.path.abspath(
+    os.path.join(
+        os.path.dirname(__file__),
+        '../fixtures'))
+fixture_filename = 'score_settings.json'
+
+
+def load_fixture(apps, schema_editor):
+    fixture_file = os.path.join(fixture_dir, fixture_filename)
+
+    fixture = open(fixture_file, 'rb')
+    objects = serializers.deserialize('json', fixture, ignorenonexistent=True)
+    for obj in objects:
+        obj.save()
+    fixture.close()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('memopol_settings', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.RunPython(load_fixture),
+    ]
diff --git a/representatives_recommendations/migrations/0006_score_formula.py b/representatives_recommendations/migrations/0006_score_formula.py
new file mode 100644
index 00000000..52039c8b
--- /dev/null
+++ b/representatives_recommendations/migrations/0006_score_formula.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('representatives_recommendations', '0005_representativescore'),
+    ]
+
+    operations = [
+        migrations.RunSQL(
+            """
+            DROP VIEW "representatives_recommendations_votescores" CASCADE;
+            """
+        ),
+        migrations.AlterField(
+            model_name='recommendation',
+            name='weight',
+            field=models.FloatField(default=0),
+        ),
+        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,
+                ROUND(CAST(EXP(-((decay_num.value * EXTRACT(days FROM CURRENT_DATE - representatives_votes_proposal.datetime) / decay_denom.value) ^ (2 * exponent.value)))
+                    * (CASE
+                        WHEN representatives_votes_vote."position"::text = representatives_recommendations_recommendation.recommendation::text THEN representatives_recommendations_recommendation.weight
+                        ELSE 0 - representatives_recommendations_recommendation.weight
+                    END) AS NUMERIC), decimals.value) AS score
+            FROM representatives_votes_vote
+                JOIN (SELECT CAST(TO_NUMBER(value, '99999') AS FLOAT) AS value FROM memopol_settings_setting WHERE key = 'SCORE_DECAY_NUM') decay_num ON 1=1
+                JOIN (SELECT CAST(TO_NUMBER(value, '99999') AS FLOAT) AS value FROM memopol_settings_setting WHERE key = 'SCORE_DECAY_DENOM') decay_denom ON 1=1
+                JOIN (SELECT CAST(TO_NUMBER(value, '99999') AS FLOAT) AS value FROM memopol_settings_setting WHERE key = 'SCORE_EXPONENT') exponent ON 1=1
+                JOIN (SELECT CAST(TO_NUMBER(value, '99999') AS INTEGER) AS value FROM memopol_settings_setting WHERE key = 'SCORE_DECIMALS') decimals ON 1=1
+                JOIN representatives_votes_proposal ON representatives_votes_vote.proposal_id = representatives_votes_proposal.id
+                LEFT JOIN representatives_recommendations_recommendation ON representatives_votes_proposal.id = representatives_recommendations_recommendation.proposal_id
+            WHERE representatives_recommendations_recommendation.id IS NOT NULL;
+            """
+        ),
+        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"
+            """
+        ),
+        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 7e887e03..c9a18951 100644
--- a/representatives_recommendations/models.py
+++ b/representatives_recommendations/models.py
@@ -12,7 +12,7 @@ class DossierScore(models.Model):
     representative = models.ForeignKey(Representative,
         on_delete=models.DO_NOTHING)
     dossier = models.ForeignKey(Dossier, on_delete=models.DO_NOTHING)
-    score = models.IntegerField(default=0)
+    score = models.FloatField(default=0)
 
     class Meta:
         managed = False
@@ -25,7 +25,7 @@ class VoteScore(models.Model):
     representative = models.ForeignKey(
         Representative, related_name='votescores', null=True)
     position = models.CharField(max_length=10)
-    score = models.IntegerField(default=0)
+    score = models.FloatField(default=0)
 
     class Meta:
         managed = False
@@ -36,7 +36,7 @@ class VoteScore(models.Model):
 class RepresentativeScore(models.Model):
     representative = models.OneToOneField('representatives.representative',
         primary_key=True, related_name='score')
-    score = models.IntegerField(default=0)
+    score = models.FloatField(default=0)
 
     class Meta:
         managed = False
@@ -52,7 +52,7 @@ class Recommendation(models.Model):
     recommendation = models.CharField(max_length=10, choices=Vote.VOTECHOICES)
     title = models.CharField(max_length=1000, blank=True)
     description = models.TextField(blank=True)
-    weight = models.IntegerField(default=0)
+    weight = models.FloatField(default=0)
 
     class Meta:
         ordering = ['proposal__datetime']
-- 
GitLab