Commit 0731c495 authored by Nicolas Joyard's avatar Nicolas Joyard

Add working position form

parent f6d6bd9a
......@@ -213,7 +213,7 @@ TEMPLATE_LOADERS = (
TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + (
'django.template.context_processors.request',
'memopol.context_processors.search_form_options'
'memopol.context_processors.search_form_options',
)
# Static files finders
......
......@@ -7,13 +7,17 @@ class BaseTest(ResponseDiffTestMixin, test.TestCase):
"""
Common queries
- 1 for chambers
- 1 for countries
- 1 for parties
- 1 for committees
- 1 for delegations
- 5 for search forms
- 1 for chambers
- 1 for countries
- 1 for parties
- 1 for committees
- 1 for delegations
- 2 for the position form
- 1 for representatives
- 1 for themes
"""
left_pane_queries = 5
left_pane_queries = 7
def request_test(self, url=None):
self.assertResponseDiffEmpty(self.client.get(url or self.url))
......
<form action="" method="post">
<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">Add a representative public position</h4>
</div>
<div class="modal-body">
<input name="csrfmiddlewaretoken" type="hidden" value="csrftoken"/>
<div class="row">
<div class="col-sm-12">
<div class="well well-sm text-justify">
<p>
Use this form to submit a public position taken by a representative and
related to one of the themes followed on this instance of Political Memory.
Public positions may include blog or social network posts, interviews,
parliament interventions...
</p>
<p>
Be sure to include a relevant excerpt from the public position as well as
a valid link that refers to it. Note that positions will be reviewed by
the staff before publication.
</p>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group"><label class="col-md-3 control-label" for="id_position-representative">Representative</label><div class="col-md-9"><select class="form-control" id="id_position-representative" name="position-representative" required="required" title="">
<option selected="selected" value="">---------</option>
<option value="4902">François Asensi</option>
<option value="4893">Thierry Benoit</option>
<option value="4898">Marcel Bonnot</option>
<option value="4891">Jean-Claude Bouchet</option>
<option value="22">Paul BRANNEN</option>
<option value="12">Udo BULLMANN</option>
<option value="4914">Jean-Paul Chanteguet</option>
<option value="4912">Jean-Louis Christ</option>
<option value="4900">Jean-Michel Couve</option>
<option value="21">Esther de LANGE</option>
<option value="9">Albert DESS</option>
<option value="4896">Marc Dolez</option>
<option value="4889">Dominique Dord</option>
<option value="4899">Olivier Dussopt</option>
<option value="4890">Daniel Fasquelle</option>
<option value="24">María Teresa GIMÉNEZ BARBAT</option>
<option value="4894">Claude Goasguen</option>
<option value="4885">Pascale Got</option>
<option value="13">Bolesław G. PIECHA</option>
<option value="20">Iveta GRIGULE</option>
<option value="1">Czesław HOC</option>
<option value="4910">Philippe Houillon</option>
<option value="7">Sylvia-Yvonne KAUFMANN</option>
<option value="18">Jan KELLER</option>
<option value="3">Dietmar KÖSTER</option>
<option value="29">Werner LANGEN</option>
<option value="23">Jo LEINEN</option>
<option value="4903">Pierre Lellouche</option>
<option value="4886">Annick Lepetit</option>
<option value="4895">Pierre Lequiller</option>
<option value="8">Arne LIETZ</option>
<option value="19">Verónica LOPE FONTAGNÉ</option>
<option value="4908">Jacqueline Maquet</option>
<option value="4907">Philippe Martin</option>
<option value="25">Gesine MEISSNER</option>
<option value="4904">Hervé Morin</option>
<option value="4913">Alain Moyne-Bressand</option>
<option value="16">Angelika NIEBLER</option>
<option value="14">Paul NUTTALL</option>
<option value="5">Patrick O'FLYNN</option>
<option value="4905">Martine Pinville</option>
<option value="15">Mirosław PIOTROWSKI</option>
<option value="4906">François Pupponi</option>
<option value="4911">Jean-Luc Reitzer</option>
<option value="4909">Franck Reynier</option>
<option value="4888">Marcel Rogemont</option>
<option value="4901">André Santini</option>
<option value="10">Annie SCHREIJER-PIERIK</option>
<option value="28">Joachim SCHUSTER</option>
<option value="26">Helga STEVENS</option>
<option value="17">László TŐKÉS</option>
<option value="4887">Jean-Jacques Urvoas</option>
<option value="4892">Alain Vidalies</option>
<option value="4897">Philippe Vigier</option>
<option value="6">Axel VOSS</option>
<option value="2">Renate WEBER</option>
<option value="11">Kerstin WESTPHAL</option>
<option value="4">Hermann WINKLER</option>
<option value="27">Damiano ZOFFOLI</option>
</select></div></div>
<div class="form-group"><label class="col-md-3 control-label" for="id_position-datetime">Datetime</label><div class="col-md-9">
<div class="input-group date" id="id_position-datetime">
<input class="form-control" id="id_position-datetime" name="position-datetime" placeholder="Datetime" readonly="" required="required" title="" type="text"/>
<span class="input-group-addon"><span class="glyphicon glyphicon-remove"></span></span>
<span class="input-group-addon"><span class="glyphicon glyphicon-calendar"></span></span>
</div>
<script type="text/javascript">
$("#id_position-datetime").datetimepicker({minView: 2,
autoclose: true,
language: 'en',
startView: 2,
format: 'yyyy-mm-dd'}).find('input').addClass("form-control");
</script>
</div></div>
<div class="form-group"><label class="col-md-3 control-label" for="id_position-link">Link</label><div class="col-md-9"><input class="form-control" id="id_position-link" maxlength="500" name="position-link" placeholder="Link" required="required" title="" type="url"/></div></div>
</div>
<div class="col-sm-6">
<div class="form-group"><label class="col-md-3 control-label" for="id_position-themes_0">Themes</label><div class="col-md-9"><div id="id_position-themes"><div class="checkbox"><label for="id_position-themes_0"><input class="" id="id_position-themes_0" name="position-themes" title="" type="checkbox" value="1"/> Etat d'urgence</label></div>
<div class="checkbox"><label for="id_position-themes_1"><input class="" id="id_position-themes_1" name="position-themes" title="" type="checkbox" value="2"/> ACTA</label></div></div></div></div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="col-sm-12">
<div class="form-group"><label class="control-label" for="id_position-text">Text</label><textarea class="form-control" cols="40" id="id_position-text" name="position-text" placeholder="Text" required="required" rows="10" title=""></textarea></div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-default" data-dismiss="modal" type="button">Close</button>
<button class="btn btn-primary" type="submit">Submit public position</button>
</div>
</form>
\ No newline at end of file
<option selected="selected" value="4899">Olivier Dussopt</option>
\ No newline at end of file
<input checked="checked" class="" id="id_position-themes_0" name="position-themes" title="" type="checkbox" value="1"/>
\ No newline at end of file
from .base import BaseTest, RepresentativeBaseTest, ThemeBaseTest
class PositionFormTest(BaseTest):
url = '/'
def test_position_form(self):
self.client.cookies['csrftoken'] = 'csrftoken'
self.selector_test('#add-position-form form')
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]',
ThemeBaseTest.base_url % 'none'
)
# coding: utf-8
from django.conf.urls import include, url
from django.contrib import admin
from django.views import generic
from views.home import HomeView
from views.dossier_ac import DossierAutocomplete, ProposalAutocomplete
from views.dossier_detail_base import DossierDetailBase
......@@ -184,8 +185,6 @@ urlpatterns = [
),
url(r'^admin/', include(admin.site.urls)),
url(r'^positions/', include('representatives_positions.urls',
namespace='representatives_positions')),
url(r'^api/', include(api.router.urls)),
url(r'^$', generic.TemplateView.as_view(template_name='home.html')),
url(r'^$', HomeView.as_view()),
]
......@@ -4,8 +4,10 @@ from django.views import generic
from representatives_votes.models import Dossier
from representatives_positions.views import PositionFormMixin
class DossierDetailBase(generic.DetailView):
class DossierDetailBase(PositionFormMixin, generic.DetailView):
template_name = 'representatives_votes/dossier_detail.html'
queryset = Dossier.objects.prefetch_related('themes')
......@@ -9,8 +9,11 @@ from representatives_votes.models import Dossier
from ..filters import DossierFilter
from representatives_positions.views import PositionFormMixin
class DossierList(PaginationMixin, SortMixin, generic.ListView):
class DossierList(PaginationMixin, SortMixin, PositionFormMixin,
generic.ListView):
current_filter = None
queryset = Dossier.objects.prefetch_related(
......
# coding: utf-8
from django.views import generic
from representatives_positions.views import PositionFormMixin
class HomeView(PositionFormMixin, generic.TemplateView):
template_name = 'home.html'
......@@ -8,8 +8,11 @@ from representatives.models import Chamber, Representative, Address, Phone, \
from .representative_mixin import RepresentativeViewMixin
from representatives_positions.views import PositionFormMixin
class RepresentativeDetailBase(RepresentativeViewMixin, generic.DetailView):
class RepresentativeDetailBase(RepresentativeViewMixin, PositionFormMixin,
generic.DetailView):
queryset = Representative.objects.select_related('score')
......@@ -56,5 +59,6 @@ class RepresentativeDetailBase(RepresentativeViewMixin, generic.DetailView):
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
return c
......@@ -10,10 +10,12 @@ from representatives.models import Representative
from ..filters import RepresentativeFilter
from .representative_mixin import RepresentativeViewMixin
from representatives_positions.views import PositionFormMixin
class RepresentativeList(CSVDownloadMixin, GridListMixin, PaginationMixin,
RepresentativeViewMixin, ActiveLegislatureMixin,
SortMixin, generic.ListView):
SortMixin, PositionFormMixin, generic.ListView):
csv_name = 'representatives'
queryset = Representative.objects.select_related('score')
......
......@@ -4,8 +4,16 @@ from django.views import generic
from memopol_themes.models import Theme
from representatives_positions.views import PositionFormMixin
class ThemeDetailBase(generic.DetailView):
class ThemeDetailBase(PositionFormMixin, generic.DetailView):
template_name = 'memopol_themes/theme_detail.html'
queryset = Theme.objects.all()
def get_context_data(self, **kwargs):
c = super(ThemeDetailBase, self).get_context_data(**kwargs)
c['position_form'].fields['themes'].initial = [c['object']]
return c
......@@ -9,8 +9,11 @@ from memopol_themes.models import Theme
from ..filters import ThemeFilter
from representatives_positions.views import PositionFormMixin
class ThemeList(PaginationMixin, SortMixin, generic.ListView):
class ThemeList(PaginationMixin, SortMixin, PositionFormMixin,
generic.ListView):
current_filter = None
queryset = Theme.objects.all().annotate(
......
......@@ -2,21 +2,36 @@ from django import forms
from datetimewidget.widgets import DateWidget
from memopol_themes.models import Theme
from .models import Position
class PositionForm(forms.ModelForm):
themes = forms.models.ModelMultipleChoiceField(
queryset=Theme.objects.all(),
required=False,
widget=forms.CheckboxSelectMultiple
)
class Meta:
model = Position
fields = ['datetime', 'text', 'link', 'representative']
fields = ['representative', 'link', 'datetime', 'themes', 'text']
widgets = {
# Use localization and bootstrap 3
'datetime': DateWidget(
attrs={
'id': 'yourdatetimeid'
},
usel10n=True,
bootstrap_version=3,
),
'representative': forms.HiddenInput
bootstrap_version=3
)
}
def save(self, commit=True):
position = super(PositionForm, self).save(commit=False)
if commit:
position.save()
if position.pk:
position.themes = self.cleaned_data.get('themes')
self.save_m2m()
return position
from django.db import models
from django.core.urlresolvers import reverse
from django.template.defaultfilters import truncatewords
from representatives.models import Representative
......@@ -21,7 +20,3 @@ class Position(models.Model):
def unpublish(self):
self.published = False
def get_absolute_url(self):
return reverse('representatives_positions:position-detail',
args=(self.pk,))
from django.conf.urls import url
import views
urlpatterns = [
url(
r'^position/create/$',
views.PositionCreate.as_view(),
name='position-create'
),
url(
r'^position/(?P<pk>\d+)/$',
views.PositionDetail.as_view(),
name='position-detail'
),
]
from django.views import generic
from django.db import models
from django.core.urlresolvers import reverse
from memopol.views.representative_mixin import RepresentativeViewMixin
from representatives.models import Mandate
from .models import Position
from .forms import PositionForm
class PositionCreate(generic.CreateView):
model = Position
form_class = PositionForm
class PositionFormMixin(generic.View):
"""
Mixin for class views that handle a position form (should be all full-page
views, ie. all template views that use a templates that extends base.html).
We don't use a FormView here to allow usage of this mixin in views that
have their own form.
"""
position_form = None
position_created = False
def get_success_url(self):
return reverse('representative-detail',
args=(self.object.representative.slug,))
def post(self, request, *args, **kwargs):
if 'position-representative' in request.POST:
self.position_form = PositionForm(request.POST, prefix='position')
if self.position_form.is_valid():
self.position_form.save()
self.position_form = None
self.position_created = True
return self.get(request, args, kwargs)
class PositionDetail(RepresentativeViewMixin, generic.DetailView):
queryset = Position.objects.filter(published=True).select_related(
'representative__score')
def get_context_data(self, **kwargs):
c = super(PositionFormMixin, self).get_context_data(**kwargs)
def get_queryset(self):
qs = super(PositionDetail, self).get_queryset()
qs = qs.prefetch_related(models.Prefetch(
'representative__mandates',
Mandate.objects.select_related('constituency__country', 'group')
))
return qs
c['position_form'] = \
self.position_form or PositionForm(prefix='position')
c['position_created'] = self.position_created
def get_object(self):
obj = super(PositionDetail, self).get_object()
self.add_representative_country_and_main_mandate(obj.representative)
return obj
return c
......@@ -253,18 +253,6 @@ a:hover .custom-thumbnail-details {
}
/***************************************************************
List / Résultat de la recherche
***************************************************************/
thead th {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAMAAACelLz8AAAAJ1BMVEVmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmaP/QSjAAAADHRSTlMAAgMJC0uWpKa6wMxMdjkoAAAANUlEQVR4AeXJyQEAERAAsNl7Hf3X6xt0QL6JpZWq30pdvdadme+0PMdzvHm8YThHcT1H7K0BtOMDniZhWOgAAAAASUVORK5CYII=);
-webkit-background-size: 13px 13px;
background-size: 13px;
background-repeat: no-repeat;
background-position: right center;
}
/***************************************************************
MEPs
***************************************************************/
......
......@@ -3,25 +3,27 @@
{% load staticfiles %}
<div id="intro">
<h1 class="text-center">
<img id="logo" src="{% static 'images/logo.png' %}">
<br>
{% trans "Political memory" %}
</h1>
<p class="lead text-center hidden-xs">{% trans "What is Memopol ?" %}</p>
<p class="text-justify hidden-xs">{% trans "Political Memory is a tool designed by La Quadrature du Net to help citizens reach their representative and track their voting records on issues related to fundamental freedoms online." %}</p>
<div class="container-fluid hidden-xs">
<div class="row">
<div class="col-sm-6">
<a class="btn btn-primary hidden-print" data-toggle="modal" data-target="#addposition" aria-expanded="false" aria-controls="addposition">
{% bootstrap_icon "bullhorn" %}
Add a public position
</a>
<h1 class="text-center">
<img id="logo" src="{% static 'images/logo.png' %}">
<br>
{% trans "Political memory" %}
</h1>
<p class="lead text-center hidden-xs">{% trans "What is Memopol ?" %}</p>
<p class="text-justify hidden-xs">{% trans "Political Memory is a tool designed by La Quadrature du Net to help citizens reach their representative and track their voting records on issues related to fundamental freedoms online." %}</p>
<div class="container-fluid hidden-xs">
<div class="row">
<div class="col-sm-6">
{% if position_form %}
<a class="btn btn-primary hidden-print" data-toggle="modal" data-target="#add-position-form" aria-expanded="false" aria-controls="add-position-form">
{% bootstrap_icon "bullhorn" %}
Add a public position
</a>
{% endif %}
</div>
<div class="col-sm-6 text-right">
<a class="btn btn-default">{% trans "More on our blog" %}</a>
</div>
</div>
</div>
<div class="col-sm-6 text-right">
<a class="btn btn-default">{% trans "More on our blog" %}</a>
</div>
</div>
</div>
</div>
\ No newline at end of file
<div class="modal fade" id="addposition" tabindex="-1" role="dialog" aria-labelledby="btn-position">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="myModalLabel">Add a MEP's public position</h4>
{% load i18n %}
{% load bootstrap3 %}
<div class="modal fade" id="add-position-form" tabindex="-1" role="dialog" aria-labelledby="btn-position">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<form method="post" action="">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="{% trans 'Close' %}"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">{% trans "Add a representative public position" %}</h4>
</div>
<div class="modal-body">
{% csrf_token %}
<div class="row">
<div class="col-sm-12">
<div class="well well-sm text-justify">{% include "text/position_info.html" %}</div>
</div>
<div class="modal-body">
<p class="text-justify">Cronut pop-up gluten-free, cliche Carles chillwave salvia roof party Blue Bottle. Craft beer post-ironic photo booth whatever literally DIY lomo, shabby chic fashion axe. Cardigan meh master cleanse, McSweeney's hashtag Etsy Pinterest forage viral XOXO freegan narwhal. Pour-over Brooklyn asymmetrical flannel pork belly four loko. Crucifix tote bag you probably haven't heard of them, organic synth stumptown occupy. Seitan plaid Williamsburg selvage single-origin coffee, tattooed PBR occupy art party whatever Banksy. Tumblr kale chips food truck tofu kitsch, chambray Etsy pork belly.</p>
<form class="form-horizontal">
<div class="col-sm-6">
<div class="form-group">
<label class="col-sm-2 control-label" for="MEPname">MEP</label>
<div class="col-sm-10">
<select class="form-control" id="MEPname">
<option></option>
<option>Jan Philipp ALBRECHT</option>
<option>Martina ANDERSON</option>
<option>Sergio Gaetano COFFERATI</option>
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="date">Date</label>
<div class="col-sm-10">
<input type="date" class="form-control" id="date" placeholder="Today">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="points">Points</label>
<div class="col-sm-10">
<input type="number" class="form-control" id="points" placeholder="0">
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label class="col-sm-2 control-label" for="theme">Theme</label>
<div class="col-sm-10">
<select class="form-control" id="theme">
<option>Any</option>
<option>Personal data protection: processing of data for the purposes of prevention, investigation, detection or prosecution of criminal offences or execution of criminal penalties, and free movement of data </option>
<option>ACTA </option>
<option>Personal data protection: processing and free movement of data </option>
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="dossier">Dossier</label>
<div class="col-sm-10">
<select class="form-control" id="dossier">
<option>Any</option>
<option>Personal data protection: processing of data for the purposes of prevention, investigation, detection or prosecution of criminal offences or execution of criminal penalties, and free movement of data </option>
<option>ACTA </option>
<option>Personal data protection: processing and free movement of data </option>
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="tags">Tags</label>
<div class="col-sm-10">
<input type="url" class="form-control" id="source" placeholder="ACTA, Net neutrality, ...">
</div>