Commit ea84bcb7 authored by njoyard's avatar njoyard Committed by GitHub
Browse files

Merge pull request #123 from political-memory/themes

Add themes
parents 69861a94 b6432187
......@@ -12,7 +12,7 @@ class RepresentativeDetailTest(UrlGetTestMixin, TestCase):
# Ensure one-time cached queries occur before the actual test
self.client.get(self.url)
with self.assertNumQueries(12):
with self.assertNumQueries(11):
"""
- One query for chambers
- One query for the rep details and foreign key (profile)
......@@ -24,7 +24,6 @@ class RepresentativeDetailTest(UrlGetTestMixin, TestCase):
- One query for reverse relation on votes
- One query for reverse relation on mandates
- One query for reverse relation positions
- One query for reverse relation tags on positions
"""
self.client.get(self.url)
......
# -*- coding: utf8 -*-
from django.test import TestCase
from memopol_themes.models import Theme
from .base import ResponseDiffMixin
class ThemesTest(ResponseDiffMixin, TestCase):
fixtures = ['smaller_sample.json']
def test_theme_list(self):
# session setup
self.client.get('/theme/')
# 1 query for theme count
# 1 query for themes
self.responsediff_test('/theme/', 2)
def test_theme_search(self):
# session setup
self.client.get('/theme/')
# 1 query for theme count
# 1 query for themes
q = 'acta'
self.responsediff_test('/theme/?search=%s' % q, 2)
def test_theme_search_noresults(self):
# session setup
self.client.get('/theme/')
# 1 query for theme count
# nothing else since count = 0
q = 'no-theme-will-have-that-title-ever'
self.responsediff_test('/theme/?search=%s' % q, 1)
def test_theme_detail(self):
# Get 1st theme in dataset
theme = Theme.objects.order_by('pk')[0]
# session setup
self.client.get('/theme/%s/' % theme.pk)
# 1 query for the theme
# 1 query for links
# 1 query for dossiers
# 1 query for dossier documents
# 1 query for dossier document chambers
# 1 query for proposals
# 1 query for proposals dossiers
# 1 query for proposals dossier documents
# 1 query for proposals dossier document chambers
# 1 query for positions
# 1 query for position representativs
self.responsediff_test('/theme/%s/' % theme.slug, 11)
......@@ -11,6 +11,8 @@ from views.group_list import GroupList
from views.representative_detail import RepresentativeDetail
from views.representative_list import RepresentativeList
from views.redirects import RedirectGroupList, RedirectGroupRepresentativeList
from views.theme_detail import ThemeDetail
from views.theme_list import ThemeList
import api
......@@ -84,6 +86,16 @@ urlpatterns = [
ProposalAutocomplete.as_view(),
name='proposal-autocomplete',
),
url(
r'^theme/$',
ThemeList.as_view(),
name='theme-list'
),
url(
r'^theme/(?P<slug>[-\w]+)/$',
ThemeDetail.as_view(),
name='theme-detail'
),
url(r'^admin/', include(admin.site.urls)),
url(r'^positions/', include('representatives_positions.urls',
......
......@@ -70,7 +70,6 @@ class RepresentativeDetail(RepresentativeViewMixin, generic.DetailView):
c['votes'] = c['object'].votes.all()
c['mandates'] = c['object'].mandates.all()
c['positions'] = c['object'].positions.filter(published=True) \
.prefetch_related('tags') \
.order_by('-datetime', 'pk')
c['position_form'] = PositionForm(
......
# coding: utf-8
from django.views import generic
from memopol_themes.models import Theme
class ThemeDetail(generic.DetailView):
queryset = Theme.objects.prefetch_related(
'links',
'dossiers__documents__chamber',
'proposals__recommendation',
'proposals__dossier__documents__chamber',
'positions__representative',
)
# coding: utf-8
from core.views import PaginationMixin
from django.views import generic
from memopol_themes.models import Theme
from ..filters import ThemeFilter
class ThemeList(PaginationMixin, generic.ListView):
current_filter = None
queryset = Theme.objects.all()
def theme_filter(self, qs):
f = ThemeFilter(self.request.GET, queryset=qs)
self.current_filter = f
return f.qs
def get_queryset(self):
qs = super(ThemeList, self).get_queryset()
qs = self.theme_filter(qs)
return qs
def get_context_data(self, **kwargs):
c = super(ThemeList, self).get_context_data(**kwargs)
c['filter'] = self.current_filter
return c
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.models import Position
from .models import Theme, ThemeLink
class LinkInline(admin.StackedInline):
model = ThemeLink
extra = 0
class ThemeAdmin(admin.ModelAdmin):
list_display = ('name', 'description')
list_editable = ('name', 'description')
list_filter = ('name',)
fields = ('name', 'description')
inlines = [
LinkInline
]
class ThemeLinkAdmin(admin.ModelAdmin):
list_display = ('title', 'datetime', 'link')
list_editable = ('title', 'datetime', 'link')
list_filter = ('title', 'datetime', 'link')
class ThemedAdminForm(forms.ModelForm):
themes = forms.ModelMultipleChoiceField(
queryset=Theme.objects.all(),
required=False,
widget=admin.widgets.FilteredSelectMultiple(
verbose_name='Themes',
is_stacked=False
)
)
def __init__(self, *args, **kwargs):
super(ThemedAdminForm, self).__init__(*args, **kwargs)
if self.instance and self.instance.pk:
self.fields['themes'].initial = self.instance.themes.all()
def save(self, commit=True):
item = super(ThemedAdminForm, self).save(commit=False)
if commit:
item.save()
if item.pk:
item.themes = self.cleaned_data['themes']
self.save_m2m()
return item
class ThemedDossierAdminForm(ThemedAdminForm):
class Meta:
model = Dossier
fields = ('title', 'reference', 'text', 'themes')
class ThemedProposalAdminForm(ThemedAdminForm):
class Meta:
model = Proposal
fields = ('dossier', 'title', 'description', 'reference', 'datetime',
'kind', 'total_abstain', 'total_against', 'total_for',
'themes')
class ThemedPositionAdminForm(ThemedAdminForm):
class Meta:
model = Position
fields = ('representative', 'datetime', 'text', 'link', 'published',
'themes')
class ThemedDossierAdmin(DossierAdmin):
form = ThemedDossierAdminForm
class ThemedProposalAdmin(ProposalAdmin):
form = ThemedProposalAdminForm
class ThemedPositionAdmin(PositionAdmin):
form = ThemedPositionAdminForm
admin.site.register(Theme, ThemeAdmin)
admin.site.register(ThemeLink, ThemeLinkAdmin)
admin.site.unregister(Dossier)
admin.site.register(Dossier, ThemedDossierAdmin)
admin.site.unregister(Proposal)
admin.site.register(Proposal, ThemedProposalAdmin)
admin.site.unregister(Position)
admin.site.register(Position, ThemedPositionAdmin)
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import autoslug.fields
class Migration(migrations.Migration):
dependencies = [
('representatives_votes', '0012_document'),
('representatives_positions', '0003_remove_position_tags'),
]
operations = [
migrations.CreateModel(
name='Theme',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(unique=True, max_length=255)),
('slug', autoslug.fields.AutoSlugField(populate_from=b'name', editable=False)),
('description', models.TextField()),
('dossiers', models.ManyToManyField(related_name='themes', to='representatives_votes.Dossier')),
('positions', models.ManyToManyField(related_name='themes', to='representatives_positions.Position')),
('proposals', models.ManyToManyField(related_name='themes', to='representatives_votes.Proposal')),
],
),
migrations.CreateModel(
name='ThemeLink',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('title', models.CharField(max_length=511)),
('datetime', models.DateField()),
('link', models.URLField(max_length=500)),
('theme', models.ForeignKey(related_name='links', to='memopol_themes.Theme')),
],
),
]
from django.db import models
from django.utils.encoding import smart_unicode
from autoslug import AutoSlugField
from representatives_votes.models import Dossier, Proposal
from representatives_positions.models import Position
class Theme(models.Model):
name = models.CharField(max_length=255, unique=True)
slug = AutoSlugField(populate_from='name')
description = models.TextField()
dossiers = models.ManyToManyField(Dossier, related_name='themes')
proposals = models.ManyToManyField(Proposal, related_name='themes')
positions = models.ManyToManyField(Position, related_name='themes')
def __unicode__(self):
return smart_unicode(self.name)
class ThemeLink(models.Model):
title = models.CharField(max_length=511)
datetime = models.DateField()
link = models.URLField(max_length=500)
theme = models.ForeignKey(Theme, related_name='links')
def __unicode__(self):
return smart_unicode('%s (%s)' % (self.title, self.link))
......@@ -8,7 +8,7 @@ from .models import Position
class PositionForm(forms.ModelForm):
class Meta:
model = Position
fields = ['tags', 'datetime', 'text', 'link', 'representative']
fields = ['datetime', 'text', 'link', 'representative']
widgets = {
# Use localization and bootstrap 3
'datetime': DateWidget(
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('representatives_positions', '0002_increase_link_length'),
]
operations = [
migrations.RemoveField(
model_name='position',
name='tags',
),
]
from django.db import models
from django.core.urlresolvers import reverse
from django.template.defaultfilters import truncatewords
from taggit.managers import TaggableManager
from representatives.models import Representative
class Position(models.Model):
representative = models.ForeignKey(Representative,
related_name='positions')
related_name='positions')
datetime = models.DateField()
text = models.TextField()
link = models.URLField(max_length=500)
published = models.BooleanField(default=False)
tags = TaggableManager()
@property
def short_text(self):
......@@ -26,4 +24,4 @@ class Position(models.Model):
def get_absolute_url(self):
return reverse('representatives_positions:position-detail',
args=(self.pk,))
args=(self.pk,))
......@@ -11,10 +11,7 @@
.long-quote
= object.text|linebreaks
%p.tags
- for tag in object.tags.all
%span.label.label-default
= tag
%p
Source :
%a{:href => '{{object.link}}'}
......
......@@ -13,13 +13,11 @@ class PositionTest(TestCase):
def setUp(self):
self.client = Client()
self.tags = [u'foo', u'bar']
self.create_url = reverse('representatives_positions:position-create')
self.mep = Representative.objects.get(pk=160)
self.fixture = {
'tags': ','.join(self.tags),
'datetime': '2015-12-11',
'text': '%stext' % self.id(),
'link': 'http://example.com/%slink' % self.id(),
......@@ -34,8 +32,6 @@ class PositionTest(TestCase):
assert response['Location'] == expected
result = Position.objects.get(text='%stext' % self.id())
assert list(result.tags.order_by('pk').values_list('name',
flat=True)) == self.tags
assert result.datetime == datetime.date(2015, 12, 11)
assert result.link == self.fixture['link']
assert result.representative.pk == self.mep.pk
......@@ -73,20 +69,16 @@ class PositionTest(TestCase):
representative=self.mep
)
position.tags.add('%stag' % self.id())
# Trigger irrelevant queries that happen only once ie. constance before
# testing actual page queries.
self.client.get(position.get_absolute_url())
with self.assertNumQueries(4):
with self.assertNumQueries(3):
# One for position and rep and score
# One for rep mandates
# One for rep chamber
# One for position tags
response = self.client.get(position.get_absolute_url())
assert 'Dec. 11, 2015' in response.content
assert '%stag' % self.id() in response.content
assert self.fixture['link'] in response.content
assert self.fixture['text'] in response.content
assert self.mep.full_name in response.content
......@@ -10,6 +10,7 @@ setup(name='political-memory',
url='http://github.com/political-memory/political_memory/',
install_requires=[
'django-autocomplete-light>=3.0,<4.0',
'django-autoslug>=1.9,<1.10',
'django-bootstrap3>=6,<7',
'django-coffeescript>=0.7,<0.8',
'django-compressor>=1,<2',
......
......@@ -32,6 +32,9 @@
%li
%a{href: "{% url 'group-list' kind='committee' %}"}
- trans 'Committees'
%li
%a{href: "{% url 'theme-list' %}"}
- trans 'Themes'
%li
%a{href: "{% url 'dossier-list' %}"}
- trans 'Dossiers'
- extends "base.html"
- load i18n
- load memopol_tags
- block content
%h1 {{ theme.name }}
%h2
- trans "Description"
%p.description
{{ theme.description }}
%h2
- trans "Links"
%table.table
- for link in theme.links.all
%tr
%td
%a.external{'href': '{{ link.link }}', target:'_blank'}
= link.title
%td
= link.datetime
%h2
- trans "Dossiers"
%table.table
%tr
%th
- trans "Chambers"
%th
- trans "Dossier"
%th
- trans "Reference"
- for dossier in theme.dossiers.all
%tr
%td
- for chamber in dossier.chambers
= chamber | chamber_small_icon
%td
%a{'href': "{% url 'dossier-detail' dossier.pk %}"}
{{ dossier.title }}
%td= dossier.reference
%h2
- trans "Proposals"
%table.table
%tr
%th
- trans "Chambers"
%th
- trans "Dossier"
%th
- trans "Reference"
%th
- trans "Proposal"
%th
- trans "Title"
%th
- trans "Recommendation"
%th
- trans "Status"
%th= "for" | position_icon
%th= "against" | position_icon
%th= "abstain" | position_icon
- for proposal in theme.proposals.all
%tr
%td
- for chamber in proposal.dossier.chambers
= chamber | chamber_small_icon
%td
%a{'href': "{% url 'dossier-detail' proposal.dossier.pk %}"}
{{ proposal.dossier.title }}
%td= proposal.dossier.reference
%td
= proposal.title
%br
%small
{{ proposal.reference }}
- if proposal.recommendation
%td= proposal.recommendation.title
%td= proposal.recommendation.recommendation | position_icon
- else
%td.no-recommendation{colspan:2}= "No recommendation"
%td= proposal.status
%td= proposal.total_for
%td= proposal.total_against
%td= proposal.total_abstain
%h2
- trans "Positions"
%table.table
%tr
%th
- trans "Representative"
%th
- trans "Date"
%th
- trans "Position"
%th
- trans "Link"
- for position in theme.positions.all
%tr.position
%td
= position.representative
%td= position.datetime
%td
%a{:href => '{{ position.get_absolute_url }}'}
=position.text|truncatewords:8
%td
%a{:href => '{{ position.link }}'}
= position.link
- extends "base.html"
- load i18n
- load memopol_tags
- block head