Commit 66980e59 authored by Jamesie Pic's avatar Jamesie Pic

Quality

parent 9bf87cb4
...@@ -7,12 +7,8 @@ set -xe ...@@ -7,12 +7,8 @@ set -xe
source ${OPENSHIFT_HOMEDIR}app-root/runtime/dependencies/python/virtenv/bin/activate source ${OPENSHIFT_HOMEDIR}app-root/runtime/dependencies/python/virtenv/bin/activate
cat ${OPENSHIFT_REPO_DIR}requirements.txt
pip install -U pip pip install -U pip
pip install -r ${OPENSHIFT_REPO_DIR}requirements.txt
# We don't have sentry yet # We don't have sentry yet
# python ${OPENSHIFT_REPO_DIR}manage.py raven test # python ${OPENSHIFT_REPO_DIR}manage.py raven test
......
sudo: false sudo: false
env: env:
matrix: - DJANGO_DEBUG=True DJANGO_SETTINGS_MODULE=memopol.settings
- DEBUG=True
language: python language: python
python: python:
- '2.7' - '2.7'
install: install:
- pip install django - pip install -e .
- pip install -r requirements.txt - pip install flake8 pep8
before_script: before_script:
- npm install -g bower - npm install -g bower
- bower install - bower install
- npm install - npm install
script: script:
- ./manage.py migrate - pep8 . --exclude '*/migrations,docs' --ignore E128
- flake8 . --exclude '*/migrations,docs' --ignore E128
- django-admin migrate
- django-admin update_score
- node_modules/gulp/bin/gulp.js less - node_modules/gulp/bin/gulp.js less
deploy: deploy:
- provider: openshift - provider: openshift
......
How to install memopol
1. Create a directory containing the whole project (say ~memopol)
2. Create a python virtualenv in ~memopol : ~memopol/ve
3. Clone memopol main repository (http://git.laquadrature.net/memopol/memopol_political_memory) in ~memopol/memopol_political_memory
4. Clone django-representative directory (http://git.laquadrature.net/memopol/compotista_django-representatives) in ~memopol/django-representatives
5. Clone the last version of django-chronograph (https://bitbucket.org/wnielson/django-chronograph) in ~memopol/django-chronograph
(It seems that the version in pip is not up to date…)
6. Symlink ~memopol/memopol_political_memory/representatives -> ~memopol/django-representatives/representatives
7. Symlink ~memopol/memopol_political_memory/chronograph -> ~memopol/django-chronograph/chronograph
8. Install requierements in the virtualenv with pip install -r ~memopol//memopol_political_memory/requierements
9. Config your database in ~memopol/memopol_political_memory/settings.py
10. Sync the database python manage.py syncdb
11. Import mep from a compotista instance : python manage.py update update_eu_mps
12. Run the server
-# Pagination block display pagination for the `object_list`
`object_list` could be generated with core.view_utils.render_paginate_list
.pagination-block .pagination-block
%nav %nav
%ul.pagination.pagination-sm %ul.pagination.pagination-sm
- if object_list.has_previous - if page.has_previous
%li %li
%a{'href': '?={queries.urlencode}&page=={object_list.previous_page_number}', %a{'href': '?={queries.urlencode}&page=={page.previous_page_number}',
'aria-label': 'Previous'} 'aria-label': 'Previous'}
<i aria-hidden="true" class="fa fa-chevron-left"></i> <i aria-hidden="true" class="fa fa-chevron-left"></i>
- for page_num in object_list.page_range - for p in page.pages
- if not page_num - if p
%li.disabled - if p == page.number
%a{'href': ''}
- elif page_num == object_list.number
%li.active %li.active
%a{'href': ''} %a{'href': ''}
{{ page_num }} {{ p }}
- else - else
%li %li
%a{'href': '?={queries.urlencode}&page=={page_num}'} %a{'href': '?={queries.urlencode}&page=={p}'}
{{ page_num }} {{ p }}
- else
%li.disabled
%a{'href': ''}
- if object_list.has_next - if page.has_next
%li %li
%a{'href': '?={queries.urlencode}&page=={object_list.next_page_number}', %a{'href': '?={queries.urlencode}&page=={page.next_page_number}',
'aria-label': 'Next'} 'aria-label': 'Next'}
<i aria-hidden="true" class="fa fa-chevron-right"></i> <i aria-hidden="true" class="fa fa-chevron-right"></i>
......
from django.test import TestCase
# Create your tests here. # Create your tests here.
# coding: utf-8
# This file is part of memopol.
#
# memopol is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of
# the License, or any later version.
#
# memopol is distributed in the hope that it will
# be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU General Affero Public
# License along with django-representatives.
# If not, see <http://www.gnu.org/licenses/>.
#
# Copyright (C) 2015 Arnaud Fabre <af@laquadrature.net>
from __future__ import absolute_import from __future__ import absolute_import
from pure_pagination import EmptyPage
from pure_pagination import Paginator
from django.shortcuts import render
def create_child_instance_from_parent(child_cls, parent_instance): def create_child_instance_from_parent(child_cls, parent_instance):
""" """
...@@ -34,3 +19,31 @@ def create_child_instance_from_parent(child_cls, parent_instance): ...@@ -34,3 +19,31 @@ def create_child_instance_from_parent(child_cls, parent_instance):
child_instance.__dict__.update(parent_instance.__dict__) child_instance.__dict__.update(parent_instance.__dict__)
child_instance.save() child_instance.save()
return child_instance return child_instance
def render_paginate_list(request, object_list, template_name):
"""
Render a paginated list of representatives
"""
pagination_limits = (10, 20, 50, 100)
num_by_page = request.GET.get('limit', 30)
paginator = Paginator(object_list, num_by_page)
number = request.GET.get('page', '1')
number = int(number) if number.isdigit() else 1
try:
page = paginator.page(number)
except EmptyPage:
page = paginator.page(paginator.num_pages)
context = {}
context['paginator'] = paginator
context['page'] = page
context['object_list'] = context['page'].object_list
context['pagination_limits'] = pagination_limits
return render(
request,
template_name,
context
)
# coding: utf-8
# This file is part of memopol.
#
# memopol is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of
# the License, or any later version.
#
# memopol is distributed in the hope that it will
# be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU General Affero Public
# License along with django-representatives.
# If not, see <http://www.gnu.org/licenses/>.
#
# Copyright (C) 2015 Arnaud Fabre <af@laquadrature.net>
from create_child_instance_from_parent import create_child_instance_from_parent
from render_paginate_list import render_paginate_list
This diff is collapsed.
# coding: utf-8
# This file is part of memopol.
#
# memopol is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of
# the License, or any later version.
#
# memopol is distributed in the hope that it will
# be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU General Affero Public
# License along with django-representatives.
# If not, see <http://www.gnu.org/licenses/>.
#
# Copyright (C) 2015 Arnaud Fabre <af@laquadrature.net>
from __future__ import absolute_import
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from .digg_paginator import DiggPaginator
from django.shortcuts import render
def render_paginate_list(request, object_list, template_name):
"""
Render a paginated list of representatives
"""
pagination_limits = (10, 20, 50, 100)
num_by_page = request.GET.get('limit', 30)
paginator = DiggPaginator(object_list, num_by_page, body=5)
# paginator = Paginator(object_list, num_by_page)
page = request.GET.get('page', 1)
try:
objects = paginator.page(page)
except PageNotAnInteger:
objects = paginator.page(1)
except EmptyPage:
objects = paginator.page(paginator.num_pages)
context = {}
queries_without_page = request.GET.copy()
if 'page' in queries_without_page:
del queries_without_page['page']
context['queries'] = queries_without_page
context['object_list'] = objects
context['paginator'] = paginator
context['pagination_limits'] = pagination_limits
return render(
request,
template_name,
context
)
Administrator guide
~~~~~~~~~~~~~~~~~~~
This guide targets the administrator who has deployed
the website in production.
Authentication in the admin backend
===================================
If you haven't already, create a super-administrator
account with command::
./manage.py createsuperuser
Then use this account to authenticate in the
administration backend located in ``/admin``.
User groups
===========
You should create a user group with all permissions
on:
- vote | recommendation
- positions | position
Creating a user
===============
To create a content administrator, create a user with:
- "Staff status": checked, otherwise the user won't be
able to authenticate in the administration backend,
- groups: at least the group created above.
Then, send the credentials to the user along with a link to :doc:`usage`.
...@@ -12,9 +12,6 @@ ...@@ -12,9 +12,6 @@
# All configuration values have a default; values that are commented out # All configuration values have a default; values that are commented out
# serve to show the default. # serve to show the default.
import sys
import os
import shlex
# If extensions (or modules to document with autodoc) are in another directory, # If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
......
...@@ -91,3 +91,5 @@ to be executed, either do it manually:: ...@@ -91,3 +91,5 @@ to be executed, either do it manually::
OpenShift is fun, login with ssh and look around if you're curious, you'll be OpenShift is fun, login with ssh and look around if you're curious, you'll be
able to recreate your app without much effort if you break it anyway. able to recreate your app without much effort if you break it anyway.
Continue to :doc:`administration`.
...@@ -67,18 +67,6 @@ Then, install the package for development:: ...@@ -67,18 +67,6 @@ Then, install the package for development::
Running setup.py develop for political-memory Running setup.py develop for political-memory
Successfully installed django-1.9 django-coffeescript-0.7.2 django-debug-toolbar-1.4 django-extensions-1.5.9 django-pdb-0.4.2 hamlpy-0.82.2 ijson-2.2 markdown-2.6.5 political-memory pygments-2.0.2 python-dateutil-2.4.2 pytz-2015.7 six-1.10.0 south-1.0.2 sqlparse-0.1.18 werkzeug-0.11.2 Successfully installed django-1.9 django-coffeescript-0.7.2 django-debug-toolbar-1.4 django-extensions-1.5.9 django-pdb-0.4.2 hamlpy-0.82.2 ijson-2.2 markdown-2.6.5 political-memory pygments-2.0.2 python-dateutil-2.4.2 pytz-2015.7 six-1.10.0 south-1.0.2 sqlparse-0.1.18 werkzeug-0.11.2
And install the requirements::
$ pip install -r requirements.txt
Collecting django<1.9,>=1.8 (from -r requirements.txt (line 1))
[output snipped for readability]
Using cached Django-1.8.7-py2.py3-none-any.whl
Running setup.py develop for django-representatives
Running setup.py develop for django-representatives-votes
Successfully installed amqp-1.4.8 anyjson-0.3.3 billiard-3.3.0.22 celery-3.1.19 django-1.8.7 django-adminplus-0.5 django-appconf-1.0.1 django-autocomplete-light-2.2.10 django-bootstrap3-6.2.2 django-celery-3.1.17 django-compressor-1.6 django-constance-1.1.1 django-datetime-widget-0.9.3 django-denorm-0.2.0 django-filter-0.11.0 django-picklefield-0.3.2 django-representatives django-representatives-votes django-taggit-0.17.5 django-uuidfield-0.5.0 djangorestframework-3.3.1 kombu-3.0.30 py-dateutil-2.2 pyprind-2.9.3 requests-2.8.1 slugify-0.0.1
Install NodeJS dependencies Install NodeJS dependencies
=========================== ===========================
...@@ -124,14 +112,14 @@ Build the static files with gulp:: ...@@ -124,14 +112,14 @@ Build the static files with gulp::
have gulp watching for changes and rebuilding static files have gulp watching for changes and rebuilding static files
automatically. automatically.
Activate ``DEBUG`` Activate ``DJANGO_DEBUG``
================== =========================
``DEBUG`` is disabled by default, the development server won't run properly by ``DEBUG`` is disabled by default, the development server
default thnen, to enable it export the ``DEBUG`` variable in the current won't run properly by default thnen, to enable it export
shell:: the ``DJANGO_DEBUG`` variable in the current shell::
$ export DEBUG=True $ export DJANGO_DEBUG=True
Database migrations Database migrations
=================== ===================
...@@ -178,3 +166,5 @@ Provision with data ...@@ -178,3 +166,5 @@ Provision with data
To provision it with data (takes a while):: To provision it with data (takes a while)::
$ bin/update_all $ bin/update_all
Continue to :doc:`administration`.
Hacker guide
~~~~~~~~~~~~
Adding random recommendations
=============================
::
$ ./manage.py shell
In [1]: from representatives_votes.models import Proposal
In [2]: from votes.models import Recommendation
In [3]: import random
In [4]: for p in Proposal.objects.all(): Recommendation.objects.create(proposal=p, recommendation='for', weight=random.randint(1,10))
...@@ -11,8 +11,11 @@ Contents: ...@@ -11,8 +11,11 @@ Contents:
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
development usage
deployment deployment
administration
development
hacker
Indices and tables Indices and tables
================== ==================
......
User guide
~~~~~~~~~~
Authentication in the admin backend
===================================
As a content-editor, you should be able to connect to the administration
interface with the credentials and link that your administrator sent you. If
they haven't, please email them with a request and eventually a link to
:doc:`administration`.
Managing representative positions
=================================
Our dear representatives make promises to be elected. These can be submitted by
visitors on the front-end so one of your tasks is to review them and publish
them if they are appropriate.
In "Positions -> Position -> Change" (url should be
``/admin/positions/position/``), above the list table, click the "Published"
select box and choose the "Published: No" option, then click "Search".
Alternatively, you may bookmark
``/admin/positions/position/?published__exact=0``.
Click a "Position" and if it's appropriate then check the "Published" checkbox
and click "Save". The position will now appear in the representative detail
page.
Managing vote recommendations
=============================
A recommendation represents the vote we want representatives to make on a
proposal: representatives doing the "recommended" vote will have their score
increased, others will have their score decreased. Some recommendations may be
more important than others, you can change the number of score points a
recommendation is worth by changing its "weight" (must be a positive integer).
In "Votes -> Recommendations -> Change" (url should be
``/admin/votes/recommendation/``), you can create, update or remove
recommendations.
Your change won't be visible on score as soon as you make it.
Data updates
============
Data updates daily:
- representatives details are updated daily,
- dossiers are synchronised daily,
- proposals are synchronised daily, when proposals are synchronised, they
become available to use in "Recommendations".
- votes and scores are synchronised daily for proposals which have a
"Recommendation",
This means that if a proposal arrives today in the database, you may add a
recommendation for it. The next day, votes will be imported and scores will be
updated as well.
...@@ -21,7 +21,7 @@ from __future__ import absolute_import ...@@ -21,7 +21,7 @@ from __future__ import absolute_import
from django.contrib import admin from django.contrib import admin
from representatives.models import Email, WebSite, Address, Phone, Country from representatives.models import Address, Country, Email, Phone, WebSite
from .models import MemopolRepresentative from .models import MemopolRepresentative
...@@ -63,17 +63,4 @@ class MemopolRepresentativeAdmin(admin.ModelAdmin): ...@@ -63,17 +63,4 @@ class MemopolRepresentativeAdmin(admin.ModelAdmin):
] ]
# class MandateAdmin(admin.ModelAdmin):
# list_display = ('representative', 'group', 'role', 'constituency', 'begin_date', 'end_date', 'active')
# search_fields = ('representative', 'group', 'constituency')
# list_filter = ('role',)
# admin.site.register(Representative, RepresentativeAdmin)
admin.site.register(MemopolRepresentative, MemopolRepresentativeAdmin) admin.site.register(MemopolRepresentative, MemopolRepresentativeAdmin)
# admin.site.register(Country)
# admin.site.register(MemopolMandate, MandateAdmin)
# admin.site.register(MemopolGroup)
# admin.site.register(Constituency)
...@@ -26,6 +26,7 @@ from .models import MemopolRepresentative ...@@ -26,6 +26,7 @@ from .models import MemopolRepresentative
class RepresentativeFilter(django_filters.FilterSet): class RepresentativeFilter(django_filters.FilterSet):
class Meta: class Meta:
model = MemopolRepresentative model = MemopolRepresentative
# fields = ['full_name', 'country', 'score'] # fields = ['full_name', 'country', 'score']
......
from __future__ import absolute_import
from django.db import transaction
from django.core.management.base import BaseCommand
from legislature.models import MemopolRepresentative
class Command(BaseCommand):
@transaction.atomic
def handle(self, *args, **options):
for representative in MemopolRepresentative.objects.all():
representative.update_all()
import pyprind
from django.core.management.base import BaseCommand
from legislature.models import MemopolRepresentative
class Command(BaseCommand):
def handle(self, *args, **options):
bar = pyprind.ProgBar(MemopolRepresentative.objects.all().count())
for representative in MemopolRepresentative.objects.all():
representative.update_score()
bar.update(item_id = str(representative))
...@@ -14,9 +14,11 @@ class Migration(migrations.Migration): ...@@ -14,9 +14,11 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='MemopolRepresentative', name='MemopolRepresentative',
fields=[ fields=[
('representative_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='representatives.Representative')), ('representative_ptr', models.OneToOneField(parent_link=True, auto_created=True,
primary_key=True, serialize=False, to='representatives.Representative')),
('score', models.IntegerField(default=0)), ('score', models.IntegerField(default=0)),
('country', models.ForeignKey(to='representatives.Country', null=True)), ('country', models.ForeignKey(
to='representatives.Country', null=True)),
], ],
options={ options={
'abstract': False, 'abstract': False,
......
...@@ -15,6 +15,7 @@ class Migration(migrations.Migration): ...@@ -15,6 +15,7 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='memopolrepresentative', model_name='memopolrepresentative',
name='main_mandate', name='main_mandate',
field=models.ForeignKey(default=True, to='representatives.Mandate', null=True), field=models.ForeignKey(
default=True, to='representatives.Mandate', null=True),
), ),
] ]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('legislature', '0002_memopolrepresentative_main_mandate'),
]
operations = [
migrations.AlterField(
model_name='memopolrepresentative',
name='main_mandate',
field=models.ForeignKey(
default=None, to='representatives.Mandate', null=True),
),
]
...@@ -21,16 +21,14 @@ ...@@ -21,16 +21,14 @@
from datetime import datetime from datetime import datetime
from django.db import models from django.db import models
from django.core.exceptions import ObjectDoesNotExist
from django.db.models.signals import post_save
from django.dispatch import receiver
# from django.utils.functional import cached_property
from representatives.models import Representative, Mandate, Country from representatives.contrib.parltrack.import_representatives import \
from representatives.management.commands import ( representative_post_save
parltrack_import_representatives,) from representatives.models import Country, Mandate, Representative
from votes.models import MemopolVote from votes.models import MemopolVote
from core.utils import create_child_instance_from_parent
# from django.utils.functional import cached_property
class MemopolRepresentative(Representative): class MemopolRepresentative(Representative):
...@@ -66,7 +64,7 @@ class MemopolRepresentative(Representative): ...@@ -66,7 +64,7 @@ class MemopolRepresentative(Representative):
).filter(representative=self) ).filter(representative=self)
def parltrack_representative_post_save(sender, representative, data, **kwargs): def mempol_representative(sender, representative, data, **kwargs):
update = False update = False
try: try:
memopol_representative = MemopolRepresentative.objects.get( memopol_representative = MemopolRepresentative.objects.get(
...@@ -106,4 +104,4 @@ def parltrack_representative_post_save(sender, representative, data, **kwargs): ...@@ -106,4 +104,4 @@ def parltrack_representative_post_save(sender, representative, data, **kwargs):
if update: if update:
memopol_representative.save() memopol_representative.save()
parltrack_import_representatives.ParltrackImporter.representative_post_save.connect(parltrack_representative_post_save) representative_post_save.connect(mempol_representative)
...@@ -19,13 +19,14 @@ ...@@ -19,13 +19,14 @@
from datetime import datetime from datetime import datetime
from django.http import Http404 from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.shortcuts import render, get_object_or_404
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.db.models import Q from django.db.models import Q
from django.http import Http404
from django.shortcuts import get_object_or_404, render
from representatives.models import Group
from legislature.models import MemopolRepresentative from legislature.models import MemopolRepresentative
from representatives.models import Group
def retrieve(request, pk=None, name=None): def retrieve(request, pk=None, name=None):
if pk: if pk:
...@@ -47,6 +48,7 @@ def retrieve(request, pk=None, name=None): ...@@ -47,6 +48,7 @@ def retrieve(request, pk=None, name=None):
{'representative': representative} {'representative': representative}
) )
def representatives_by_group(request, group_kind, group_abbr=None, def representatives_by_group(request, group_kind, group_abbr=None,
group_name=None, search=None, group_id=None): group_name=None, search=None, group_id=None):
if group_id: if group_id:
...@@ -58,14 +60,14 @@ def representatives_by_group(request, group_kind, group_abbr=None, ...@@ -58,14 +60,14 @@ def representatives_by_group(request, group_kind, group_abbr=None,
representative_list = MemopolRepresentative.objects.filter( representative_list = MemopolRepresentative.objects.filter(
mandates__group__abbreviation=group_abbr, mandates__group__abbreviation=group_abbr,
mandates__group__kind=group_kind, mandates__group__kind=group_kind,
mandates__end_date__gte = datetime.now() mandates__end_date__gte=datetime.now()