Commit 22643a22 authored by Nicolas Joyard's avatar Nicolas Joyard

Allow multiple reps on a position, fixes #159

parent f18ce532
This diff is collapsed.
......@@ -24,7 +24,7 @@
<div class="row">
<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.link layout='horizontal' %}
{% bootstrap_field position_form.title layout='horizontal' %}
......
......@@ -38,9 +38,15 @@
<td>
{% 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 }}">
{% if show_representatives %}
<h5>{{ position.representative }}</h5>
{% endif %}
<h5>
{% if position.representatives.count > 1 %}
{% blocktrans with count=position.representatives.count %}
{{ count }} representatives
{% endblocktrans %}
{% else %}
{{ position.representatives.first }}
{% endif %}
</h5>
<div class="text-center">{{ position.datetime|naturalday }}</div>
{% include "blocks/_themetags.html" with themes=position.themes.all exclude=theme.pk %}
......@@ -59,13 +65,10 @@
</h4>
{% endif %}
<{% if position.title %}h5{% else %}h4{% endif %} class="modal-title">
{% if show_representative %}
{% trans "Public position" %}
{% else %}
{% blocktrans with rep=position.representative %}
Public position by {{ rep }}
{% endblocktrans %}
{% endif %}
{% trans "Public position by" %}
{% for rep in position.representatives.all %}
{{ rep}}{% if not forloop.last %}, {% endif %}
{% endfor %}
</{% if position.title %}h5{% else %}h4{% endif %}>
</div>
......
......@@ -15,11 +15,10 @@ class BaseTest(ResponseDiffTestMixin, test.TestCase):
- 1 for parties
- 1 for committees
- 1 for delegations
- 2 for the position form
- 1 for representatives
- 1 for the position form
- 1 for themes
"""
left_pane_queries = 8
left_pane_queries = 7
def setUp(self):
RepresentativeScore.refresh()
......@@ -47,8 +46,7 @@ class RepresentativeBaseTest(BaseTest):
- 1 for chamber websites
- 1 for other websites
- 1 for addresses
- 1 for address country
- 1 for phone numbers related to addresses
- 2 for phone numbers related to addresses
- 1 for other phone numbers
- 2 for themes and theme scores
"""
......
<option selected="selected" value="4899">Olivier Dussopt</option>
\ No newline at end of file
......@@ -4,10 +4,9 @@
<button aria-label="Close" class="close" data-dismiss="modal" type="button"><span aria-hidden="true">×</span></button>
<h4 class="modal-title">
Public position by
Public position by Olivier Dussopt
Olivier Dussopt
</h4>
</div>
......@@ -69,10 +68,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>
<h4 class="modal-title">
Public position by
Public position by Olivier Dussopt
Olivier Dussopt
</h4>
</div>
......
......@@ -10,7 +10,7 @@ class PositionFormTest(BaseTest):
url = '/'
create_url = RepresentativeBaseTest.base_url % 'none'
position_fixture = {
'position-representative': 1,
'position-representatives': 1,
'position-datetime': '2016-09-01',
'position-link': 'http://example.com/test',
'position-kind': 'other',
......@@ -20,12 +20,6 @@ class PositionFormTest(BaseTest):
'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):
self.selector_test(
'#add-position-form #id_position-themes input[checked]',
......@@ -39,15 +33,15 @@ class PositionFormTest(BaseTest):
position = Position.objects.get(text='position test text')
assert position.datetime == datetime.date(2016, 9, 1)
assert position.representative.pk == \
self.position_fixture['position-representative']
assert position.representatives.all()[0].pk == \
self.position_fixture['position-representatives']
assert position.link == self.position_fixture['position-link']
assert ''.join(['%s' % t.pk for t in position.themes.all()]) == '1'
assert position.published is False
def test_create_position_without_representative(self):
fixture = copy.copy(self.position_fixture)
fixture.pop('position-representative')
fixture.pop('position-representatives')
response = self.client.post(self.create_url, fixture)
self.assertResponseDiffEmpty(response,
......
......@@ -67,6 +67,6 @@ class RepresentativeDetailBase(RepresentativeViewMixin, PositionFormMixin,
c = super(RepresentativeDetailBase, self).get_context_data(**kwargs)
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
......@@ -24,6 +24,7 @@ class RepresentativeDetailPositions(RepresentativeDetailBase):
queryset=positions_qs.order_by('-datetime', 'pk')
),
'positions__themes',
'positions__representatives',
'positions__position_score'
)
......
......@@ -8,7 +8,7 @@ class ThemeDetailPositions(ThemeDetailBase):
def get_queryset(self):
qs = super(ThemeDetailPositions, self).get_queryset()
qs = qs.prefetch_related('positions__representative',
qs = qs.prefetch_related('positions__representatives',
'positions__position_score')
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):
def create_position(self, when, score):
pos = Position(
representative=self.representative,
datetime=when,
kind='other',
title='TEST',
......@@ -79,6 +78,8 @@ class ComputeTest(test.TestCase):
published=True
)
pos.save()
pos.representatives.add(self.representative)
pos.save()
return pos
def test_no_score(self):
......
from dal import autocomplete
from django import forms
from django.contrib import admin
from representatives_votes.admin import DossierAdmin, ProposalAdmin
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 .models import Theme, ThemeLink
......@@ -73,16 +71,11 @@ class ThemedProposalAdminForm(ThemedAdminForm):
'themes')
class ThemedPositionAdminForm(ThemedAdminForm):
class ThemedPositionAdminForm(ThemedAdminForm, PositionAdminForm):
class Meta:
model = Position
fields = ('representative', 'datetime', 'kind', 'title', 'score',
fields = ('representatives', 'datetime', 'kind', 'title', 'score',
'text', 'link', 'published', 'themes')
widgets = {
'representative': autocomplete.ModelSelect2(
url='representative-autocomplete',
)
}
class ThemedDossierAdmin(DossierAdmin):
......
from django import forms
from django.contrib import admin
from dal.autocomplete import ModelSelect2Multiple
from representatives.models import Representative
from .models import Position
......@@ -17,9 +21,35 @@ def unpublish_positions(modeladmin, request, queryset):
unpublish_positions.short_description = 'Unpublish selected positions'
class PositionAdminForm(forms.ModelForm):
representatives = forms.ModelMultipleChoiceField(
queryset=Representative.objects.all(),
required=False,
widget=ModelSelect2Multiple(
url='representative-autocomplete',
)
)
def __init__(self, *args, **kwargs):
super(PositionAdminForm, self).__init__(*args, **kwargs)
if self.instance and self.instance.pk:
self.fields['representatives'].initial = \
self.instance.representatives.all()
def save(self, commit=True):
item = super(PositionAdminForm, self).save(commit=False)
item.save()
item.representatives = self.cleaned_data['representatives']
if commit:
self.save_m2m()
return item
class PositionAdmin(admin.ModelAdmin):
list_display = (
'representative',
'kind',
'short_title',
'short_text',
......@@ -32,4 +62,7 @@ class PositionAdmin(admin.ModelAdmin):
list_filter = ('published',)
actions = (publish_positions, unpublish_positions)
form = PositionAdminForm
admin.site.register(Position, PositionAdmin)
from django import forms
from datetimewidget.widgets import DateWidget
from dal.autocomplete import ModelSelect2Multiple
from memopol_themes.models import Theme
from .models import Position
......@@ -15,13 +16,16 @@ class PositionForm(forms.ModelForm):
class Meta:
model = Position
fields = ['representative', 'link', 'datetime', 'themes', 'title',
fields = ['representatives', 'link', 'datetime', 'themes', 'title',
'kind', 'text']
widgets = {
'datetime': DateWidget(
usel10n=True,
bootstrap_version=3
),
'representatives': ModelSelect2Multiple(
url='representative-autocomplete',
)
}
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
def migrate_position_representatives(apps, schema_editor):
Position = apps.get_model("representatives_positions", "Position")
for pos in Position.objects.all():
pos.representatives = [pos.representative]
pos.save()
class Migration(migrations.Migration):
dependencies = [
('memopol_scores', '0002_pre_multi_rep_positions'),
('representatives_positions', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='position',
name='representatives',
field=models.ManyToManyField(to='representatives.Representative'),
),
migrations.RunPython(migrate_position_representatives),
migrations.RemoveField(
model_name='position',
name='representative',
),
migrations.AlterField(
model_name='position',
name='representatives',
field=models.ManyToManyField(related_name='positions', to='representatives.Representative'),
),
]
......@@ -9,12 +9,13 @@ KIND_CHOICES = (
('social', 'Social network'),
('press', 'Press interview'),
('parliament', 'Parliament debate'),
('amendment', 'Amendment'),
)
class Position(models.Model):
representative = models.ForeignKey(Representative,
related_name='positions')
representatives = models.ManyToManyField(Representative,
related_name='positions')
datetime = models.DateField()
kind = models.CharField(max_length=64, choices=KIND_CHOICES,
default='other')
......
......@@ -16,7 +16,7 @@ class PositionFormMixin(generic.View):
position_created = False
def post(self, request, *args, **kwargs):
if 'position-representative' in request.POST:
if 'position-representatives' in request.POST:
self.position_form = PositionForm(request.POST, prefix='position')
if self.position_form.is_valid():
self.position_form.save()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment