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): ...@@ -12,7 +12,7 @@ class RepresentativeDetailTest(UrlGetTestMixin, TestCase):
# Ensure one-time cached queries occur before the actual test # Ensure one-time cached queries occur before the actual test
self.client.get(self.url) self.client.get(self.url)
with self.assertNumQueries(12): with self.assertNumQueries(11):
""" """
- One query for chambers - One query for chambers
- One query for the rep details and foreign key (profile) - One query for the rep details and foreign key (profile)
...@@ -24,7 +24,6 @@ class RepresentativeDetailTest(UrlGetTestMixin, TestCase): ...@@ -24,7 +24,6 @@ class RepresentativeDetailTest(UrlGetTestMixin, TestCase):
- One query for reverse relation on votes - One query for reverse relation on votes
- One query for reverse relation on mandates - One query for reverse relation on mandates
- One query for reverse relation positions - One query for reverse relation positions
- One query for reverse relation tags on positions
""" """
self.client.get(self.url) 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 ...@@ -11,6 +11,8 @@ from views.group_list import GroupList
from views.representative_detail import RepresentativeDetail from views.representative_detail import RepresentativeDetail
from views.representative_list import RepresentativeList from views.representative_list import RepresentativeList
from views.redirects import RedirectGroupList, RedirectGroupRepresentativeList from views.redirects import RedirectGroupList, RedirectGroupRepresentativeList
from views.theme_detail import ThemeDetail
from views.theme_list import ThemeList
import api import api
...@@ -84,6 +86,16 @@ urlpatterns = [ ...@@ -84,6 +86,16 @@ urlpatterns = [
ProposalAutocomplete.as_view(), ProposalAutocomplete.as_view(),
name='proposal-autocomplete', 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'^admin/', include(admin.site.urls)),
url(r'^positions/', include('representatives_positions.urls', url(r'^positions/', include('representatives_positions.urls',
......
...@@ -70,7 +70,6 @@ class RepresentativeDetail(RepresentativeViewMixin, generic.DetailView): ...@@ -70,7 +70,6 @@ class RepresentativeDetail(RepresentativeViewMixin, generic.DetailView):
c['votes'] = c['object'].votes.all() c['votes'] = c['object'].votes.all()
c['mandates'] = c['object'].mandates.all() c['mandates'] = c['object'].mandates.all()
c['positions'] = c['object'].positions.filter(published=True) \ c['positions'] = c['object'].positions.filter(published=True) \
.prefetch_related('tags') \
.order_by('-datetime', 'pk') .order_by('-datetime', 'pk')
c['position_form'] = PositionForm( 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 ...@@ -8,7 +8,7 @@ from .models import Position
class PositionForm(forms.ModelForm): class PositionForm(forms.ModelForm):
class Meta: class Meta:
model = Position model = Position
fields = ['tags', 'datetime', 'text', 'link', 'representative'] fields = ['datetime', 'text', 'link', 'representative']
widgets = { widgets = {
# Use localization and bootstrap 3 # Use localization and bootstrap 3
'datetime': DateWidget( '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.db import models
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.template.defaultfilters import truncatewords from django.template.defaultfilters import truncatewords
from taggit.managers import TaggableManager
from representatives.models import Representative from representatives.models import Representative
class Position(models.Model): class Position(models.Model):
representative = models.ForeignKey(Representative, representative = models.ForeignKey(Representative,
related_name='positions') related_name='positions')
datetime = models.DateField() datetime = models.DateField()
text = models.TextField() text = models.TextField()
link = models.URLField(max_length=500) link = models.URLField(max_length=500)
published = models.BooleanField(default=False) published = models.BooleanField(default=False)
tags = TaggableManager()
@property @property
def short_text(self): def short_text(self):
...@@ -26,4 +24,4 @@ class Position(models.Model): ...@@ -26,4 +24,4 @@ class Position(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('representatives_positions:position-detail', return reverse('representatives_positions:position-detail',
args=(self.pk,)) args=(self.pk,))
...@@ -11,10 +11,7 @@ ...@@ -11,10 +11,7 @@
.long-quote .long-quote
= object.text|linebreaks = object.text|linebreaks
%p.tags
- for tag in object.tags.all
%span.label.label-default
= tag
%p %p
Source : Source :
%a{:href => '{{object.link}}'} %a{:href => '{{object.link}}'}
......
...@@ -13,13 +13,11 @@ class PositionTest(TestCase): ...@@ -13,13 +13,11 @@ class PositionTest(TestCase):
def setUp(self): def setUp(self):
self.client = Client() self.client = Client()
self.tags = [u'foo', u'bar']
self.create_url = reverse('representatives_positions:position-create') self.create_url = reverse('representatives_positions:position-create')
self.mep = Representative.objects.get(pk=160) self.mep = Representative.objects.get(pk=160)
self.fixture = { self.fixture = {
'tags': ','.join(self.tags),
'datetime': '2015-12-11', 'datetime': '2015-12-11',
'text': '%stext' % self.id(), 'text': '%stext' % self.id(),
'link': 'http://example.com/%slink' % self.id(), 'link': 'http://example.com/%slink' % self.id(),
...@@ -34,8 +32,6 @@ class PositionTest(TestCase): ...@@ -34,8 +32,6 @@ class PositionTest(TestCase):
assert response['Location'] == expected assert response['Location'] == expected
result = Position.objects.get(text='%stext' % self.id()) 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.datetime == datetime.date(2015, 12, 11)
assert result.link == self.fixture['link'] assert result.link == self.fixture['link']
assert result.representative.pk == self.mep.pk assert result.representative.pk == self.mep.pk
...@@ -73,20 +69,16 @@ class PositionTest(TestCase): ...@@ -73,20 +69,16 @@ class PositionTest(TestCase):
representative=self.mep representative=self.mep
) )
position.tags.add('%stag' % self.id())
# Trigger irrelevant queries that happen only once ie. constance before # Trigger irrelevant queries that happen only once ie. constance before
# testing actual page queries. # testing actual page queries.
self.client.get(position.get_absolute_url()) self.client.get(position.get_absolute_url())
with self.assertNumQueries(4): with self.assertNumQueries(3):
# One for position and rep and score # One for position and rep and score
# One for rep mandates # One for rep mandates
# One for rep chamber # One for rep chamber
# One for position tags
response = self.client.get(position.get_absolute_url()) response = self.client.get(position.get_absolute_url())
assert 'Dec. 11, 2015' in response.content assert 'Dec. 11, 2015' in response.content
assert '%stag' % self.id() in response.content
assert self.fixture['link'] in response.content assert self.fixture['link'] in response.content
assert self.fixture['text'] in response.content assert self.fixture['text'] in response.content
assert self.mep.full_name in response.content assert self.mep.full_name in response.content
...@@ -10,6 +10,7 @@ setup(name='political-memory', ...@@ -10,6 +10,7 @@ setup(name='political-memory',
url='http://github.com/political-memory/political_memory/', url='http://github.com/political-memory/political_memory/',
install_requires=[ install_requires=[
'django-autocomplete-light>=3.0,<4.0', 'django-autocomplete-light>=3.0,<4.0',
'django-autoslug>=1.9,<1.10',
'django-bootstrap3>=6,<7', 'django-bootstrap3>=6,<7',
'django-coffeescript>=0.7,<0.8', 'django-coffeescript>=0.7,<0.8',
'django-compressor>=1,<2', 'django-compressor>=1,<2',
......
...@@ -32,6 +32,9 @@ ...@@ -32,6 +32,9 @@
%li %li
%a{href: "{% url 'group-list' kind='committee' %}"} %a{href: "{% url 'group-list' kind='committee' %}"}
- trans 'Committees' - trans 'Committees'
%li
%a{href: "{% url 'theme-list' %}"}
- trans 'Themes'
%li %li
%a{href: "{% url 'dossier-list' %}"} %a{href: "{% url 'dossier-list' %}"}
- trans 'Dossiers' - 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