Commit 2a3afb72 authored by njoyard's avatar njoyard

Merge branch 'multi-rep-positions' into 'master'

Allow multiple reps on a position, fixes #159



See merge request !173
parents f18ce532 b9364962
This diff is collapsed.
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
<div class="row"> <div class="row">
<div class="col-sm-6"> <div class="col-sm-6">
{% bootstrap_field position_form.representative layout='horizontal' %} {% bootstrap_field position_form.representatives layout='horizontal' %}
{% bootstrap_field position_form.datetime layout='horizontal' %} {% bootstrap_field position_form.datetime layout='horizontal' %}
{% bootstrap_field position_form.link layout='horizontal' %} {% bootstrap_field position_form.link layout='horizontal' %}
{% bootstrap_field position_form.title layout='horizontal' %} {% bootstrap_field position_form.title layout='horizontal' %}
......
...@@ -38,9 +38,15 @@ ...@@ -38,9 +38,15 @@
<td> <td>
{% for position in timeframe %} {% for position in timeframe %}
<button class="btn btn-default position-button" id="position-button-{{ position.pk }}" type="button" data-toggle="modal" data-target="#position-modal-{{ position.pk }}" aria-expanded="false" aria-controls="position-modal-{{ position.pk }}"> <button class="btn btn-default position-button" id="position-button-{{ position.pk }}" type="button" data-toggle="modal" data-target="#position-modal-{{ position.pk }}" aria-expanded="false" aria-controls="position-modal-{{ position.pk }}">
{% if show_representatives %} <h5>
<h5>{{ position.representative }}</h5> {% if position.representatives.count > 1 %}
{% blocktrans with count=position.representatives.count %}
{{ count }} representatives
{% endblocktrans %}
{% else %}
{{ position.representatives.first }}
{% endif %} {% endif %}
</h5>
<div class="text-center">{{ position.datetime|naturalday }}</div> <div class="text-center">{{ position.datetime|naturalday }}</div>
{% include "blocks/_themetags.html" with themes=position.themes.all exclude=theme.pk %} {% include "blocks/_themetags.html" with themes=position.themes.all exclude=theme.pk %}
...@@ -59,13 +65,10 @@ ...@@ -59,13 +65,10 @@
</h4> </h4>
{% endif %} {% endif %}
<{% if position.title %}h5{% else %}h4{% endif %} class="modal-title"> <{% if position.title %}h5{% else %}h4{% endif %} class="modal-title">
{% if show_representative %} {% trans "Public position by" %}
{% trans "Public position" %} {% for rep in position.representatives.all %}
{% else %} {{ rep}}{% if not forloop.last %}, {% endif %}
{% blocktrans with rep=position.representative %} {% endfor %}
Public position by {{ rep }}
{% endblocktrans %}
{% endif %}
</{% if position.title %}h5{% else %}h4{% endif %}> </{% if position.title %}h5{% else %}h4{% endif %}>
</div> </div>
......
...@@ -15,11 +15,10 @@ class BaseTest(ResponseDiffTestMixin, test.TestCase): ...@@ -15,11 +15,10 @@ class BaseTest(ResponseDiffTestMixin, test.TestCase):
- 1 for parties - 1 for parties
- 1 for committees - 1 for committees
- 1 for delegations - 1 for delegations
- 2 for the position form - 1 for the position form
- 1 for representatives
- 1 for themes - 1 for themes
""" """
left_pane_queries = 8 left_pane_queries = 7
def setUp(self): def setUp(self):
RepresentativeScore.refresh() RepresentativeScore.refresh()
...@@ -47,12 +46,12 @@ class RepresentativeBaseTest(BaseTest): ...@@ -47,12 +46,12 @@ class RepresentativeBaseTest(BaseTest):
- 1 for chamber websites - 1 for chamber websites
- 1 for other websites - 1 for other websites
- 1 for addresses - 1 for addresses
- 1 for address country - 2 for phone numbers related to addresses
- 1 for phone numbers related to addresses
- 1 for other phone numbers - 1 for other phone numbers
- 2 for themes and theme scores - 2 for themes and theme scores
- 1 for DAL to fetch its initial value in the position form
""" """
queries = BaseTest.left_pane_queries + 14 queries = BaseTest.left_pane_queries + 15
@property @property
def url(self): def url(self):
......
<option selected="selected" value="4899">Olivier Dussopt</option>
\ No newline at end of file
<button aria-controls="position-modal-36" aria-expanded="false" class="btn btn-default position-button" data-target="#position-modal-36" data-toggle="modal" id="position-button-36" type="button">
<h5>
4 representatives
</h5>
<div class="text-center">July 21, 2010</div>
<span class="badge badge-primary">0</span>
</button>
---
<button aria-controls="position-modal-566" aria-expanded="false" class="btn btn-default position-button" data-target="#position-modal-566" data-toggle="modal" id="position-button-566" type="button"> <button aria-controls="position-modal-566" aria-expanded="false" class="btn btn-default position-button" data-target="#position-modal-566" data-toggle="modal" id="position-button-566" type="button">
<h5>
Olivier Dussopt
</h5>
<div class="text-center">Oct. 3, 2009</div> <div class="text-center">Oct. 3, 2009</div>
...@@ -8,7 +27,11 @@ ...@@ -8,7 +27,11 @@
</button> </button>
--- ---
<button aria-controls="position-modal-567" aria-expanded="false" class="btn btn-default position-button" data-target="#position-modal-567" data-toggle="modal" id="position-button-567" type="button"> <button aria-controls="position-modal-567" aria-expanded="false" class="btn btn-default position-button" data-target="#position-modal-567" data-toggle="modal" id="position-button-567" type="button">
<h5>
Olivier Dussopt
</h5>
<div class="text-center">June 17, 2008</div> <div class="text-center">June 17, 2008</div>
......
...@@ -4,10 +4,72 @@ ...@@ -4,10 +4,72 @@
<button aria-label="Close" class="close" data-dismiss="modal" type="button"><span aria-hidden="true">×</span></button> <button aria-label="Close" class="close" data-dismiss="modal" type="button"><span aria-hidden="true">×</span></button>
<h4 class="modal-title"> <h4 class="modal-title">
Public position by
Albert DESS,
Public position by Olivier Dussopt Olivier Dussopt,
Annie SCHREIJER-PIERIK,
Kerstin WESTPHAL
</h4>
</div>
<div class="modal-body">
<div class="row">
<dl class="dl-horizontal col-sm-6 text-left">
<dt>Date</dt>
<dd>
July 21, 2010
</dd>
<dt>Kind</dt>
<dd>
other
</dd>
</dl>
<dl class="dl-horizontal col-sm-6 text-left">
<dt>Themes</dt>
<dd>
</dd>
<dt>Score</dt>
<dd>
<span class="badge badge-primary">0</span>
</dd>
</dl>
</div>
<div class="row">
<div class="col-sm-12 text-justify">
<blockquote class="position-text">
<p>Déclaration écrite 12/2010 (ACTA/ACAC)
A signé la déclaration 12/2010 sur l’absence d’un processus transparent et la présence d’un contenu potentiellement controversé concernant l’accord commercial anti-contrefaçon (ACAC).</p>
</blockquote>
</div>
</div>
</div>
<div class="modal-footer">
<a class="btn btn-primary" href="http://www.laquadrature.net/wiki/Written_Declaration_12/2010_signatories_list" target="_blank">Check the source »</a>
<button class="btn btn-default" data-dismiss="modal" type="button">Close</button>
</div>
</div>
</div>
---
<div class="modal-dialog modal-lg position-details" role="document">
<div class="modal-content">
<div class="modal-header">
<button aria-label="Close" class="close" data-dismiss="modal" type="button"><span aria-hidden="true">×</span></button>
<h4 class="modal-title">
Public position by
Olivier Dussopt
</h4> </h4>
</div> </div>
...@@ -69,10 +131,9 @@ Olivier Dussopt n'a pas fait de réponse aux commentaires sur son blog.</p> ...@@ -69,10 +131,9 @@ Olivier Dussopt n'a pas fait de réponse aux commentaires sur son blog.</p>
<button aria-label="Close" class="close" data-dismiss="modal" type="button"><span aria-hidden="true">×</span></button> <button aria-label="Close" class="close" data-dismiss="modal" type="button"><span aria-hidden="true">×</span></button>
<h4 class="modal-title"> <h4 class="modal-title">
Public position by
Olivier Dussopt
Public position by Olivier Dussopt
</h4> </h4>
</div> </div>
......
...@@ -10,7 +10,7 @@ class PositionFormTest(BaseTest): ...@@ -10,7 +10,7 @@ class PositionFormTest(BaseTest):
url = '/' url = '/'
create_url = RepresentativeBaseTest.base_url % 'none' create_url = RepresentativeBaseTest.base_url % 'none'
position_fixture = { position_fixture = {
'position-representative': 1, 'position-representatives': 1,
'position-datetime': '2016-09-01', 'position-datetime': '2016-09-01',
'position-link': 'http://example.com/test', 'position-link': 'http://example.com/test',
'position-kind': 'other', 'position-kind': 'other',
...@@ -20,12 +20,6 @@ class PositionFormTest(BaseTest): ...@@ -20,12 +20,6 @@ class PositionFormTest(BaseTest):
'position-themes': '1' 'position-themes': '1'
} }
def test_select_representative(self):
self.selector_test(
'#add-position-form #id_position-representative option[selected]',
RepresentativeBaseTest.base_url % 'none'
)
def test_select_theme(self): def test_select_theme(self):
self.selector_test( self.selector_test(
'#add-position-form #id_position-themes input[checked]', '#add-position-form #id_position-themes input[checked]',
...@@ -39,15 +33,15 @@ class PositionFormTest(BaseTest): ...@@ -39,15 +33,15 @@ class PositionFormTest(BaseTest):
position = Position.objects.get(text='position test text') position = Position.objects.get(text='position test text')
assert position.datetime == datetime.date(2016, 9, 1) assert position.datetime == datetime.date(2016, 9, 1)
assert position.representative.pk == \ assert position.representatives.all()[0].pk == \
self.position_fixture['position-representative'] self.position_fixture['position-representatives']
assert position.link == self.position_fixture['position-link'] assert position.link == self.position_fixture['position-link']
assert ''.join(['%s' % t.pk for t in position.themes.all()]) == '1' assert ''.join(['%s' % t.pk for t in position.themes.all()]) == '1'
assert position.published is False assert position.published is False
def test_create_position_without_representative(self): def test_create_position_without_representative(self):
fixture = copy.copy(self.position_fixture) fixture = copy.copy(self.position_fixture)
fixture.pop('position-representative') fixture.pop('position-representatives')
response = self.client.post(self.create_url, fixture) response = self.client.post(self.create_url, fixture)
self.assertResponseDiffEmpty(response, self.assertResponseDiffEmpty(response,
......
...@@ -8,8 +8,9 @@ class RepresentativePositionsTest(RepresentativeBaseTest): ...@@ -8,8 +8,9 @@ class RepresentativePositionsTest(RepresentativeBaseTest):
- One for positions - One for positions
- One for position scores - One for position scores
- One for position themes - One for position themes
- One for position representatives
""" """
queries = RepresentativeBaseTest.queries + 3 queries = RepresentativeBaseTest.queries + 4
def test_queries(self): def test_queries(self):
self.do_query_test() self.do_query_test()
......
...@@ -67,6 +67,6 @@ class RepresentativeDetailBase(RepresentativeViewMixin, PositionFormMixin, ...@@ -67,6 +67,6 @@ class RepresentativeDetailBase(RepresentativeViewMixin, PositionFormMixin,
c = super(RepresentativeDetailBase, self).get_context_data(**kwargs) c = super(RepresentativeDetailBase, self).get_context_data(**kwargs)
self.add_representative_country_and_main_mandate(c['object']) self.add_representative_country_and_main_mandate(c['object'])
c['position_form'].fields['representative'].initial = c['object'].pk c['position_form'].fields['representatives'].initial = [c['object'].pk]
return c return c
...@@ -24,6 +24,7 @@ class RepresentativeDetailPositions(RepresentativeDetailBase): ...@@ -24,6 +24,7 @@ class RepresentativeDetailPositions(RepresentativeDetailBase):
queryset=positions_qs.order_by('-datetime', 'pk') queryset=positions_qs.order_by('-datetime', 'pk')
), ),
'positions__themes', 'positions__themes',
'positions__representatives',
'positions__position_score' 'positions__position_score'
) )
......
...@@ -8,7 +8,7 @@ class ThemeDetailPositions(ThemeDetailBase): ...@@ -8,7 +8,7 @@ class ThemeDetailPositions(ThemeDetailBase):
def get_queryset(self): def get_queryset(self):
qs = super(ThemeDetailPositions, self).get_queryset() qs = super(ThemeDetailPositions, self).get_queryset()
qs = qs.prefetch_related('positions__representative', qs = qs.prefetch_related('positions__representatives',
'positions__position_score') 'positions__position_score')
return qs return qs
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('memopol_scores', '0001_initial')
]
operations = [
migrations.RunSQL(
"""
DROP FUNCTION refresh_scores();
"""
),
migrations.RunSQL(
"""
DROP VIEW memopol_scores_v_representative_score;
"""
),
migrations.RunSQL(
"""
DROP VIEW memopol_scores_v_theme_score;
"""
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('memopol_scores', '0002_pre_multi_rep_positions'),
('representatives_positions', '0002_multi_rep_positions')
]
operations = [
migrations.RunSQL(
"""
CREATE OR REPLACE VIEW "memopol_scores_v_representative_score"
AS SELECT
"source"."representative_id" AS "representative_id" ,
SUM("source"."score") AS "score"
FROM
(
SELECT
"memopol_scores_dossierscore"."representative_id" AS "representative_id",
"memopol_scores_dossierscore"."score" AS "score"
FROM "memopol_scores_dossierscore"
UNION ALL
SELECT
"representatives_positions_position_representatives"."representative_id" AS "representative_id",
"memopol_scores_positionscore"."score" AS "score"
FROM
"memopol_scores_positionscore"
INNER JOIN "representatives_positions_position_representatives"
ON "representatives_positions_position_representatives"."position_id" = "memopol_scores_positionscore"."position_id"
) "source"
GROUP BY
"source"."representative_id"
"""
),
migrations.RunSQL(
"""
CREATE OR REPLACE VIEW "memopol_scores_v_theme_score"
AS SELECT
"scoresource"."representative_id" AS "representative_id",
"scoresource"."theme_id" AS "theme_id",
SUM("scoresource"."score") AS "score"
FROM
(
-- Score contribution for proposals
SELECT
"representatives_votes_vote"."representative_id" AS "representative_id",
"proposal_themes"."theme_id" AS "theme_id",
"memopol_scores_votescore"."score" AS "score"
FROM
"memopol_scores_votescore"
INNER JOIN "representatives_votes_vote"
ON "representatives_votes_vote"."id" = "memopol_scores_votescore"."vote_id"
INNER JOIN (
-- Proposals with a theme
SELECT
"representatives_votes_proposal"."id" AS "proposal_id",
"memopol_themes_theme_proposals"."theme_id" AS "theme_id"
FROM
"representatives_votes_proposal"
INNER JOIN "memopol_themes_theme_proposals"
ON "representatives_votes_proposal"."id" = "memopol_themes_theme_proposals"."proposal_id"
UNION
-- Proposals in a dossier with a theme
SELECT
"representatives_votes_proposal"."id" AS "proposal_id",
"memopol_themes_theme_dossiers"."theme_id" AS "theme_id"
FROM
"representatives_votes_proposal"
INNER JOIN "representatives_votes_dossier"
ON "representatives_votes_dossier"."id" = "representatives_votes_proposal"."dossier_id"
INNER JOIN "memopol_themes_theme_dossiers"
ON "memopol_themes_theme_dossiers"."dossier_id" = "representatives_votes_dossier"."id"
) "proposal_themes"
ON "proposal_themes"."proposal_id" = "representatives_votes_vote"."proposal_id"
UNION ALL
-- Score contribution for positions
SELECT
"representatives_positions_position_representatives"."representative_id" AS "representative_id",
"memopol_themes_theme_positions"."theme_id" AS "theme_id",
"memopol_scores_positionscore"."score" AS "score"
FROM
"memopol_scores_positionscore"
INNER JOIN "representatives_positions_position_representatives"
ON "representatives_positions_position_representatives"."position_id" = "memopol_scores_positionscore"."position_id"
INNER JOIN "memopol_themes_theme_positions"
ON "memopol_themes_theme_positions"."position_id" = "memopol_scores_positionscore"."position_id"
) "scoresource"
GROUP BY
"scoresource"."representative_id",
"scoresource"."theme_id"
"""
),
migrations.RunSQL(
"""
CREATE OR REPLACE FUNCTION refresh_scores()
RETURNS VOID AS $$
BEGIN
TRUNCATE TABLE "memopol_scores_representativescore";
TRUNCATE TABLE "memopol_scores_dossierscore";
TRUNCATE TABLE "memopol_scores_votescore";
INSERT INTO "memopol_scores_votescore" ("vote_id", "score")
SELECT "vote_id", "score" FROM "memopol_scores_v_vote_score";
INSERT INTO "memopol_scores_dossierscore" ("representative_id", "dossier_id", "score")
SELECT "representative_id", "dossier_id", "score" FROM "memopol_scores_v_dossier_score";
TRUNCATE TABLE "memopol_scores_positionscore";
INSERT INTO "memopol_scores_positionscore" ("position_id", "score")
SELECT "position_id", "score" FROM "memopol_scores_v_position_score";
TRUNCATE TABLE "memopol_scores_themescore";
INSERT INTO "memopol_scores_themescore" ("representative_id", "theme_id", "score")
SELECT
"representatives_representative"."id",
"memopol_themes_theme"."id",
COALESCE("memopol_scores_v_theme_score"."score", 0)
FROM
"representatives_representative"
INNER JOIN "memopol_themes_theme" ON 1=1
LEFT OUTER JOIN "memopol_scores_v_theme_score"
ON "memopol_scores_v_theme_score"."representative_id" = "representatives_representative"."id"
AND "memopol_scores_v_theme_score"."theme_id" = "memopol_themes_theme"."id";
INSERT INTO "memopol_scores_representativescore" ("representative_id", "score")
SELECT
"representatives_representative"."id",
COALESCE("memopol_scores_v_representative_score"."score", 0)
FROM
"representatives_representative"
LEFT OUTER JOIN "memopol_scores_v_representative_score"
ON "memopol_scores_v_representative_score"."representative_id" = "representatives_representative"."id";
END;
$$ LANGUAGE PLPGSQL;
"""
),
migrations.RunSQL(
"""
SELECT refresh_scores();
"""
)
]
...@@ -69,7 +69,6 @@ class ComputeTest(test.TestCase): ...@@ -69,7 +69,6 @@ class ComputeTest(test.TestCase):
def create_position(self, when, score): def create_position(self, when, score):
pos = Position( pos = Position(
representative=self.representative,
datetime=when, datetime=when,
kind='other', kind='other',
title='TEST', title='TEST',
...@@ -79,6 +78,8 @@ class ComputeTest(test.TestCase): ...@@ -79,6 +78,8 @@ class ComputeTest(test.TestCase):
published=True published=True
) )
pos.save() pos.save()
pos.representatives.add(self.representative)
pos.save()
return pos return pos
def test_no_score(self): def test_no_score(self):
......
from dal import autocomplete
from django import forms from django import forms
from django.contrib import admin from django.contrib import admin
from representatives_votes.admin import DossierAdmin, ProposalAdmin from representatives_votes.admin import DossierAdmin, ProposalAdmin
from representatives_votes.models import Dossier, Proposal from representatives_votes.models import Dossier, Proposal
from representatives_positions.admin import PositionAdmin from representatives_positions.admin import PositionAdmin, PositionAdminForm
from representatives_positions.models import Position from representatives_positions.models import Position
from .models import Theme, ThemeLink from .models import Theme, ThemeLink
...@@ -73,16 +71,11 @@ class ThemedProposalAdminForm(ThemedAdminForm): ...@@ -73,16 +71,11 @@ class ThemedProposalAdminForm(ThemedAdminForm):
'themes') 'themes')
class ThemedPositionAdminForm(ThemedAdminForm): class ThemedPositionAdminForm(ThemedAdminForm, PositionAdminForm):
class Meta: class Meta:
model = Position model = Position
fields = ('representative', 'datetime', 'kind', 'title', 'score', fields = ('representatives', 'datetime', 'kind', 'title', 'score',
'text', 'link', 'published', 'themes') 'text', 'link', 'published', 'themes')