diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000000000000000000000000000000000000..3e2e9dad9ff8b53e883b6999029b1ed15fec0022 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +[run] +omit = representatives_votes/tests/* +omit = representatives_votes/migrations/* diff --git a/.travis.yml b/.travis.yml index 6f9bd2eb272b00d65af7fe60f90f3c534a9ebaeb..f4e45ee0d66d8b975400ac0fd5c8372b8f5b73a2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,20 @@ sudo: false language: python +env: +- DJANGO="django>1.8,<1.9" DJANGO_SETTINGS_MODULE=representatives_votes.tests.settings python: - - "2.7" +- "2.7" +before_install: +- pip install codecov install: - - pip install django - - pip install -e git+https://github.com/political-memory/django-representatives.git#egg=representatives - - pip install -e . +- pip install $DJANGO pep8 flake8 pytest-django pytest-cov codecov +- pip install https://github.com/political-memory/django-representatives/archive/parltrack.tar.gz#egg=django-representatives +- pip install -e . script: - - test_project/manage.py migrate +- django-admin migrate +- flake8 representatives_votes/ --exclude migrations --ignore E128 +- py.test +- cat representatives_votes/contrib/parltrack/tests/dossiers_fixture.json | parltrack_import_dossiers +- cat representatives_votes/contrib/parltrack/tests/votes_fixture.json | parltrack_import_votes +after_success: +- codecov diff --git a/COPYING b/LICENSE similarity index 100% rename from COPYING rename to LICENSE diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000000000000000000000000000000000000..702ef56e1c75ee0b46882b0654728637f048f891 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include *.rst *.txt README LICENSE AUTHORS CHANGELOG +recursive-include representatives_votes *.html *.css *.js *.py *.po *.mo *.json *.png *.gif diff --git a/README.md b/README.md index c366f37b2174b9d4256cf3197d02663a771e01f6..e4acb9bbc6190726d9e701678bd50b86b3b7852a 100644 --- a/README.md +++ b/README.md @@ -1 +1,2 @@ [](https://travis-ci.org/political-memory/django-representatives-votes) +[](https://codecov.io/github/political-memory/django-representatives-votes?branch=master) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000000000000000000000000000000000000..77a3ed6a813b1cc24d8cbc98c15e02fbc15769d3 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +DJANGO_SETTINGS_MODULE=representatives_votes.tests.settings +addopts = --cov=representatives_votes --create-db diff --git a/representatives_votes/admin.py b/representatives_votes/admin.py index 6d185c496c71bf6e0717dbbe01b8e66df8c02ded..8f17f7a757bed93a48d15d99f3de6437f1f82ec0 100644 --- a/representatives_votes/admin.py +++ b/representatives_votes/admin.py @@ -1,6 +1,7 @@ # coding: utf-8 from django.contrib import admin + from .models import Dossier, Proposal, Vote @@ -10,8 +11,19 @@ class DossierAdmin(admin.ModelAdmin): class ProposalAdmin(admin.ModelAdmin): - list_display = ('id', 'fingerprint', 'reference', 'dossier_reference', 'title', 'datetime', 'kind', 'total_abstain', 'total_against', 'total_for') + list_display = ( + 'id', + 'fingerprint', + 'reference', + 'dossier_reference', + 'title', + 'datetime', + 'kind', + 'total_abstain', + 'total_against', + 'total_for') search_fields = ('reference', 'dossier__reference', 'title', 'fingerprint') + def dossier_reference(self, obj): return obj.dossier.reference @@ -22,18 +34,23 @@ class NoneMatchingFilter(admin.SimpleListFilter): def lookups(self, request, model_admin): return [('None', 'Unknown')] - + def queryset(self, request, queryset): if self.value() == 'None': return queryset.filter(representative=None) else: - return queryset + return queryset class VoteAdmin(admin.ModelAdmin): - list_display = ('id', 'proposal_reference', 'position', 'representative', 'representative_name') + list_display = ( + 'id', + 'proposal_reference', + 'position', + 'representative', + 'representative_name') list_filter = (NoneMatchingFilter,) - + def proposal_reference(self, obj): return obj.proposal.reference diff --git a/representatives_votes/management/__init__.py b/representatives_votes/contrib/__init__.py similarity index 100% rename from representatives_votes/management/__init__.py rename to representatives_votes/contrib/__init__.py diff --git a/representatives_votes/management/commands/__init__.py b/representatives_votes/contrib/parltrack/__init__.py similarity index 100% rename from representatives_votes/management/commands/__init__.py rename to representatives_votes/contrib/parltrack/__init__.py diff --git a/representatives_votes/contrib/parltrack/import_dossiers.py b/representatives_votes/contrib/parltrack/import_dossiers.py new file mode 100644 index 0000000000000000000000000000000000000000..e688633cb279ef0be0008028b4472beb940be60b --- /dev/null +++ b/representatives_votes/contrib/parltrack/import_dossiers.py @@ -0,0 +1,56 @@ +# coding: utf-8 +import logging +import sys + +import ijson +import django +from django.apps import apps + +from representatives_votes.models import Dossier + +logger = logging.getLogger(__name__) + +URL = 'http://parltrack.euwiki.org/dumps/ep_dossiers.json.xz' +LOCAL_PATH = 'ep_dossiers.json.xz' + + +def parse_dossier_data(data): + """Parse data from parltarck dossier export (1 dossier) Update dossier + if it existed before, this function goal is to import and update a + dossier, not to import all parltrack data + """ + changed = False + ref = data['procedure']['reference'] + + logger.debug('Processing dossier %s', ref) + + try: + dossier = Dossier.objects.get(reference=ref) + except Dossier.DoesNotExist: + dossier = Dossier(reference=ref) + logger.debug('Dossier did not exist') + changed = True + + if dossier.title != data['procedure']['title']: + logger.debug('Title changed from "%s" to "%s"', dossier.title, + data['procedure']['title']) + dossier.title = data['procedure']['title'] + changed = True + + source = data['meta']['source'].replace('&l=en', '') + if dossier.link != source: + logger.debug('Source changed from "%s" to "%s"', dossier.link, source) + dossier.link = source + changed = True + + if changed: + logger.info('Updated dossier %s', ref) + dossier.save() + + +def main(stream=None): + if not apps.ready: + django.setup() + + for data in ijson.items(stream or sys.stdin, 'item'): + parse_dossier_data(data) diff --git a/representatives_votes/contrib/parltrack/import_votes.py b/representatives_votes/contrib/parltrack/import_votes.py new file mode 100644 index 0000000000000000000000000000000000000000..068830d761c6451ac0341690c1349b04c79e08a2 --- /dev/null +++ b/representatives_votes/contrib/parltrack/import_votes.py @@ -0,0 +1,186 @@ +# coding: utf-8 +import logging +import sys +from os.path import join + +import django.dispatch +import ijson +import django +from django.apps import apps +from dateutil.parser import parse as date_parse +from django.db import transaction +from django.utils.timezone import make_aware as date_make_aware +from pytz import timezone as date_timezone + +from representatives.models import Representative +from representatives_votes.models import Dossier, Proposal, Vote + + +logger = logging.getLogger(__name__) +vote_pre_import = django.dispatch.Signal(providing_args=['vote_data']) + + +def _parse_date(date_str): + return date_make_aware( + date_parse(date_str), + date_timezone('Europe/Brussels')) + +JSON_URL = 'http://parltrack.euwiki.org/dumps/ep_votes.json.xz' +DESTINATION = join('/tmp', 'ep_votes.json') + + +class Command(object): + def init_cache(self): + self.cache = dict() + self.index_representatives() + self.index_dossiers() + + def parse_vote_data(self, vote_data): + """ + Parse data from parltrack votes db dumps (1 proposal) + """ + if 'epref' not in vote_data.keys(): + logger.debug('Could not import data without epref %s', + vote_data['title']) + return + + dossier_pk = self.get_dossier(vote_data['epref']) + + if not dossier_pk: + logger.debug('Cannot find dossier with remote id %s', + vote_data['epref']) + return + + return self.parse_proposal_data( + proposal_data=vote_data, + dossier_pk=dossier_pk + ) + + @transaction.atomic + def parse_proposal_data(self, proposal_data, dossier_pk): + """Get or Create a proposal model from raw data""" + proposal_display = '{} ({})'.format(proposal_data['title'].encode( + 'utf-8'), proposal_data.get('report', '').encode('utf-8')) + + if 'issue_type' not in proposal_data.keys(): + logger.debug('This proposal data without issue_type: %s', + proposal_data['epref']) + return + + changed = False + try: + proposal = Proposal.objects.get(title=proposal_data['title']) + except Proposal.DoesNotExist: + proposal = Proposal(title=proposal_data['title']) + changed = True + + data_map = dict( + title=proposal_data['title'], + datetime=_parse_date(proposal_data['ts']), + dossier_id=dossier_pk, + reference=proposal_data.get('report'), + kind=proposal_data.get('issue_type') + ) + + for position in ('For', 'Abstain', 'Against'): + position_data = proposal_data.get(position, {}) + position_total = position_data.get('total', 0) + + if isinstance(position_total, str) and position_total.isdigit(): + position_total = int(position_total) + + data_map['total_%s' % position.lower()] = position_total + + for key, value in data_map.items(): + if value != getattr(proposal, key, None): + setattr(proposal, key, value) + changed = True + + if changed: + proposal.save() + + responses = vote_pre_import.send(sender=self, vote_data=proposal_data) + + for receiver, response in responses: + if response is False: + logger.debug( + 'Skipping dossier %s', proposal_data.get( + 'epref', proposal_data['title'])) + return + + positions = ['For', 'Abstain', 'Against'] + logger.info( + 'Looking for votes in proposal {}'.format(proposal_display)) + for position in positions: + for group_vote_data in proposal_data.get( + position, + {}).get( + 'groups', + {}): + for vote_data in group_vote_data['votes']: + if not isinstance(vote_data, dict): + logger.error('Skipping vote data %s for proposal %s', + vote_data, proposal_data['_id']) + continue + + representative_pk = self.get_representative(vote_data) + + if representative_pk is None: + logger.error('Could not find mep for %s', vote_data) + continue + + representative_name = vote_data.get('orig', '') + + changed = False + try: + vote = Vote.objects.get( + representative_id=representative_pk, + proposal_id=proposal.pk) + except Vote.DoesNotExist: + vote = Vote(proposal_id=proposal.pk, + representative_id=representative_pk) + changed = True + + if vote.position != position.lower(): + changed = True + vote.position = position.lower() + + if vote.representative_name != representative_name: + changed = True + vote.representative_name = representative_name + + if changed: + vote.save() + logger.debug('Save vote %s for MEP %s on %s #%s to %s', + vote.pk, representative_pk, proposal_data['title'], + proposal.pk, position) + + return proposal + + def index_dossiers(self): + self.cache['dossiers'] = { + d[0]: d[1] for d in Dossier.objects.values_list('reference', 'pk') + } + + def get_dossier(self, reference): + return self.cache['dossiers'].get(reference, None) + + def index_representatives(self): + self.cache['meps'] = {int(l[0]): l[1] for l in + Representative.objects.values_list('remote_id', 'pk')} + + def get_representative(self, vote_data): + if vote_data.get('ep_id', None) is None: + return + return self.cache['meps'].get(int(vote_data['ep_id']), None) + + +def main(stream=None): + if not apps.ready: + django.setup() + + command = Command() + command.init_cache() + + for vote_data in ijson.items(stream or sys.stdin, 'item'): + command.parse_vote_data(vote_data) diff --git a/representatives_votes/contrib/parltrack/tests/dossiers_expected.json b/representatives_votes/contrib/parltrack/tests/dossiers_expected.json new file mode 100644 index 0000000000000000000000000000000000000000..1cb58271b4e7cd7757d91064e563036df31a0230 --- /dev/null +++ b/representatives_votes/contrib/parltrack/tests/dossiers_expected.json @@ -0,0 +1,67 @@ +[ +{ + "fields": { + "updated": "2015-12-13T10:11:31.369Z", + "reference": "2012/2002(INI)", + "title": "Agenda for change: the future of EU development policy", + "text": "", + "created": "2015-12-13T10:11:31.369Z", + "link": "http://www.europarl.europa.eu/oeil/popups/ficheprocedure.do?reference=2012/2002(INI)", + "fingerprint": "9e2cccdc5f6d22afd008af8b5b55dc193c27c5d6" + }, + "model": "representatives_votes.dossier", + "pk": 1 +}, +{ + "fields": { + "updated": "2015-12-13T10:11:31.378Z", + "reference": "2015/2132(BUD)", + "title": "2016 general budget: all sections", + "text": "", + "created": "2015-12-13T10:11:31.378Z", + "link": "http://www.europarl.europa.eu/oeil/popups/ficheprocedure.do?reference=2015/2132(BUD)", + "fingerprint": "e6856e0880e701c1022f23d595cc37a9a1cdcca8" + }, + "model": "representatives_votes.dossier", + "pk": 2 +}, +{ + "fields": { + "updated": "2015-12-13T10:11:31.388Z", + "reference": "2013/2857(DEA)", + "title": "Scheme of control and enforcement applicable in the area covered by the Convention on future multilateral cooperation in the North-East Atlantic fisheries", + "text": "", + "created": "2015-12-13T10:11:31.388Z", + "link": "http://www.europarl.europa.eu/oeil/popups/ficheprocedure.do?reference=2013/2857(DEA)", + "fingerprint": "50745a1a6e47b8db097c55ef21a4f11fc1ef0d97" + }, + "model": "representatives_votes.dossier", + "pk": 3 +}, +{ + "fields": { + "updated": "2015-12-13T10:11:31.398Z", + "reference": "2015/2623(DEA)", + "title": "Scheme of control and enforcement applicable in the area covered by the Convention on future multilateral cooperation in the North-East Atlantic fisheries", + "text": "", + "created": "2015-12-13T10:11:31.398Z", + "link": "http://www.europarl.europa.eu/oeil/popups/ficheprocedure.do?reference=2015/2623(DEA)", + "fingerprint": "65fce7af2e020ff58849d1663f2c30ab0b1a35db" + }, + "model": "representatives_votes.dossier", + "pk": 4 +}, +{ + "fields": { + "updated": "2015-12-13T10:11:31.408Z", + "reference": "2009/0051(COD)", + "title": "Scheme of control and enforcement applicable in the area covered by the Convention on future multilateral cooperation in the North-East Atlantic fisheries", + "text": "", + "created": "2015-12-13T10:11:31.408Z", + "link": "http://www.europarl.europa.eu/oeil/popups/ficheprocedure.do?reference=2009/0051(COD)", + "fingerprint": "f2abba201c10df8a7cec1d734f91b7988eec8260" + }, + "model": "representatives_votes.dossier", + "pk": 5 +} +] diff --git a/representatives_votes/contrib/parltrack/tests/dossiers_fixture.json b/representatives_votes/contrib/parltrack/tests/dossiers_fixture.json new file mode 100644 index 0000000000000000000000000000000000000000..2ac515ce21a513a00dfca5952d70400fd7acc7f6 --- /dev/null +++ b/representatives_votes/contrib/parltrack/tests/dossiers_fixture.json @@ -0,0 +1,118 @@ +[ + { + "meta": { + "source": "http://www.europarl.europa.eu/oeil/popups/ficheprocedure.do?reference=2012/2002(INI)", + "updated": "2012-10-25T02:02:36.882000" + }, + "procedure": { + "dossier_of_the_committee": "DEVE/7/08564", + "legal_basis": [ + "Rules of Procedure of the European Parliament EP 048" + ], + "reference": "2012/2002(INI)", + "stage_reached": "Procedure completed", + "subject": [ + "6.30 Development cooperation" + ], + "subtype": "Initiative", + "title": "Agenda for change: the future of EU development policy", + "type": "INI - Own-initiative procedure" + } + }, + { + "meta": { + "source": "http://www.europarl.europa.eu/oeil/popups/ficheprocedure.do?reference=2015/2132(BUD)&l=en", + "updated": "2015-11-18T02:23:18.933000" + }, + "procedure": { + "dossier_of_the_committee": "BUDE/8/04900;BUDG/8/03789", + "reference": "2015/2132(BUD)", + "stage_reached": "Budgetary conciliation committee convened", + "subject": [ + "8.70.56 2016 budget" + ], + "subtype": "Budget", + "title": "2016 general budget: all sections", + "type": "BUD - Budgetary procedure" + } + }, + { + "meta": { + "source": "http://www.europarl.europa.eu/oeil/popups/ficheprocedure.do?reference=2013/2857(DEA)&l=en", + "updated": "2014-12-06T03:56:21.811000" + }, + "procedure": { + "dossier_of_the_committee": "PECH/7/14126", + "geographical_area": [ + "Atlantic Ocean area" + ], + "reference": "2013/2857(DEA)", + "stage_reached": "Awaiting Council decision on delegated act", + "subject": [ + "3.15.01 Fish stocks, conservation of fishery resources", + "3.15.07 Fisheries inspectorate, surveillance of fishing vessels and areas", + "3.15.15 Fisheries agreements and cooperation" + ], + "subtype": "Examination of delegated act", + "summary": [ + "Supplementing" + ], + "title": "Scheme of control and enforcement applicable in the area covered by the Convention on future multilateral cooperation in the North-East Atlantic fisheries", + "type": "DEA - Delegated acts procedure" + } + }, + { + "meta": { + "source": "http://www.europarl.europa.eu/oeil/popups/ficheprocedure.do?reference=2015/2623(DEA)&l=en", + "updated": "2015-07-22T00:46:11.603000" + }, + "procedure": { + "dossier_of_the_committee": "PECH/8/03020", + "reference": "2015/2623(DEA)", + "stage_reached": "Awaiting Council decision on delegated act", + "subject": [ + "3.15.01 Fish stocks, conservation of fishery resources", + "3.15.07 Fisheries inspectorate, surveillance of fishing vessels and areas", + "3.15.15 Fisheries agreements and cooperation" + ], + "subtype": "Examination of delegated act", + "summary": [ + "Supplementing" + ], + "title": "Scheme of control and enforcement applicable in the area covered by the Convention on future multilateral cooperation in the North-East Atlantic fisheries", + "type": "DEA - Delegated acts procedure" + } + }, + { + "meta": { + "source": "http://www.europarl.europa.eu/oeil/popups/ficheprocedure.do?reference=2009/0051(COD)&l=en", + "updated": "2015-11-04T17:01:33.812000" + }, + "procedure": { + "Modified legal basis": "Rules of Procedure of the European Parliament EP 150", + "dossier_of_the_committee": "PECH/7/00288", + "final": { + "title": "Regulation 2010/1236", + "url": "http://eur-lex.europa.eu/smartapi/cgi/sga_doc?smartapi!celexplus!prod!CELEXnumdoc&lg=EN&numdoc=32010R1236" + }, + "instrument": "Regulation", + "legal_basis": [ + "Treaty on the Functioning of the EU TFEU 043-p2" + ], + "reference": "2009/0051(COD)", + "stage_reached": "Procedure completed", + "subject": [ + "3.15.01 Fish stocks, conservation of fishery resources", + "3.15.07 Fisheries inspectorate, surveillance of fishing vessels and areas", + "3.15.15 Fisheries agreements and cooperation" + ], + "subtype": "Legislation", + "summary": [ + "Amended by", + "Repealing Regulation (EC) No 2791/1999" + ], + "title": "Scheme of control and enforcement applicable in the area covered by the Convention on future multilateral cooperation in the North-East Atlantic fisheries", + "type": "COD - Ordinary legislative procedure (ex-codecision procedure)" + } + } +] diff --git a/representatives_votes/contrib/parltrack/tests/test_import.py b/representatives_votes/contrib/parltrack/tests/test_import.py new file mode 100644 index 0000000000000000000000000000000000000000..1ea818211adf5c002db113b14d8daaeb2a00f6ce --- /dev/null +++ b/representatives_votes/contrib/parltrack/tests/test_import.py @@ -0,0 +1,52 @@ +import pytest +import os +import copy + +from django.core.serializers.json import Deserializer +from django.core.management import call_command +from representatives_votes.contrib.parltrack import import_dossiers +from representatives_votes.contrib.parltrack import import_votes +from representatives_votes.models import Dossier, Proposal, Vote +from representatives.models import Representative +from representatives.contrib import parltrack + + +def _test_import(scenario, callback): + fixture = os.path.join(os.path.dirname(__file__), + '%s_fixture.json' % scenario) + expected = os.path.join(os.path.dirname(__file__), + '%s_expected.json' % scenario) + + # Disable django auto fields + exclude = ('id', '_state', 'created', 'updated') + + with open(fixture, 'r') as f: + callback(f) + + with open(expected, 'r') as f: + for obj in Deserializer(f.read()): + compare = copy.copy(obj.object.__dict__) + + for f in exclude: + if f in compare: + compare.pop(f) + + type(obj.object).objects.get(**compare) + + +@pytest.mark.django_db +def test_parltrack_import_dossiers(): + _test_import('dossiers', import_dossiers.main) + + +@pytest.mark.django_db +def test_parltrack_import_votes(): + for model in (Representative, Dossier, Proposal, Vote): + model.objects.all().delete() + + call_command('loaddata', os.path.join(os.path.abspath( + parltrack.__path__[0]), 'tests', 'representatives_expected.json')) + call_command('loaddata', os.path.join(os.path.dirname(__file__), + 'dossiers_expected.json')) + + _test_import('votes', import_votes.main) diff --git a/representatives_votes/contrib/parltrack/tests/votes_expected.json b/representatives_votes/contrib/parltrack/tests/votes_expected.json new file mode 100644 index 0000000000000000000000000000000000000000..5ab0956427f577425ef3a1441e494a37a9baa96c --- /dev/null +++ b/representatives_votes/contrib/parltrack/tests/votes_expected.json @@ -0,0 +1,112 @@ +[ +{ + "fields": { + "representative_name": "", + "position": "abstain", + "proposal": 1, + "representative": 2 + }, + "model": "representatives_votes.vote", + "pk": 1 +}, +{ + "fields": { + "representative_name": "", + "position": "abstain", + "proposal": 1, + "representative": 1 + }, + "model": "representatives_votes.vote", + "pk": 2 +}, +{ + "fields": { + "representative_name": "", + "position": "for", + "proposal": 2, + "representative": 2 + }, + "model": "representatives_votes.vote", + "pk": 3 +}, +{ + "fields": { + "representative_name": "", + "position": "for", + "proposal": 2, + "representative": 1 + }, + "model": "representatives_votes.vote", + "pk": 4 +}, +{ + "fields": { + "representative_name": "", + "position": "against", + "proposal": 3, + "representative": 2 + }, + "model": "representatives_votes.vote", + "pk": 5 +}, +{ + "fields": { + "representative_name": "", + "position": "against", + "proposal": 3, + "representative": 1 + }, + "model": "representatives_votes.vote", + "pk": 6 +}, +{ + "fields": { + "representative_name": "", + "position": "for", + "proposal": 4, + "representative": 1 + }, + "model": "representatives_votes.vote", + "pk": 7 +}, +{ + "fields": { + "representative_name": "", + "position": "against", + "proposal": 4, + "representative": 2 + }, + "model": "representatives_votes.vote", + "pk": 8 +}, +{ + "fields": { + "representative_name": "", + "position": "abstain", + "proposal": 5, + "representative": 1 + }, + "model": "representatives_votes.vote", + "pk": 9 +}, +{ + "fields": { + "representative_name": "", + "position": "against", + "proposal": 5, + "representative": 2 + }, + "model": "representatives_votes.vote", + "pk": 10 +}, +{ + "fields": { + "representative_name": "", + "position": "for", + "proposal": 6, + "representative": 1 + }, + "model": "representatives_votes.vote", + "pk": 11 +} +] diff --git a/representatives_votes/contrib/parltrack/tests/votes_fixture.json b/representatives_votes/contrib/parltrack/tests/votes_fixture.json new file mode 100644 index 0000000000000000000000000000000000000000..cabfcbe739ca6a25f6150abf4d6739ab41484c2e --- /dev/null +++ b/representatives_votes/contrib/parltrack/tests/votes_fixture.json @@ -0,0 +1,285 @@ +[ + { + "Abstain": { + "groups": [ + { + "group": "PPE", + "votes": [ + { + "ep_id": 96673, + "name": "Ludvigsson", + "userid": 5860 + }, + { + "ep_id": 2307, + "name": "Pirker", + "userid": 4611 + } + ] + } + ], + "total": "2" + }, + "_id": "56617c15ecc52ed712fc7901", + "dossierid": "4f33d5ddb819f2756a000009", + "epref": "2012/2002(INI)", + "eptitle": "Agenda for change: the future of EU development policy", + "issue_type": "\u00a7 31", + "rapporteur": [ + { + "name": "Charles Goerens", + "ref": 840 + } + ], + "report": "A7-0234/2012", + "title": "A7-0234/2012 - Charles Goerens - \u00a7 31", + "ts": "2012-10-23T18:31:10", + "url": "http://www.europarl.europa.eu/RegData/seance_pleniere/proces_verbal/2012/10-23/votes_nominaux/xml/P7_PV(2012)10-23(RCV)_XC.xml", + "voteid": "17393" + }, + { + "For": { + "groups": [ + { + "group": "PPE", + "votes": [ + { + "ep_id": 96673, + "name": "Ludvigsson", + "userid": 5860 + } + ] + }, + { + "group": "PPE", + "votes": [ + { + "ep_id": 2307, + "name": "Pirker", + "userid": 4611 + } + ] + } + ], + "total": "2" + }, + "_id": "56617c15ecc52ed712fc7902", + "dossierid": "4f33d5ddb819f2756a000009", + "epref": "2012/2002(INI)", + "eptitle": "Agenda for change: the future of EU development policy", + "issue_type": "R\u00e9solution", + "rapporteur": [ + { + "name": "Charles Goerens", + "ref": 840 + } + ], + "report": "A7-0234/2012", + "title": "A7-0234/2012 - Charles Goerens - R\u00e9solution", + "ts": "2012-10-23T18:34:32", + "url": "http://www.europarl.europa.eu/RegData/seance_pleniere/proces_verbal/2012/10-23/votes_nominaux/xml/P7_PV(2012)10-23(RCV)_XC.xml", + "voteid": "17394" + }, + { + "Against": { + "groups": [ + { + "group": "PPE", + "votes": [ + { + "ep_id": 96673, + "name": "Ludvigsson", + "userid": 5860 + } + ] + }, + { + "group": "PPE", + "votes": [ + { + "ep_id": 2307, + "name": "Pirker", + "userid": 4611 + } + ] + } + ], + "total": "2" + }, + "_id": "56617a11ecc52ed712fc6b14", + "dossierid": "55a4580ed1d1c57c1e000002", + "epref": "2015/2132(BUD)", + "eptitle": "2016 general budget: all sections", + "issue_type": "Am 4", + "rapporteur": [ + { + "name": "Jos\u00e9 Manuel Fernandes", + "ref": 96899 + }, + { + "name": " G\u00e9rard Deprez", + "ref": 1473 + } + ], + "report": "A8-0298/2015", + "title": "A8-0298/2015 - Jos\u00e9 Manuel Fernandes et G\u00e9rard Deprez - Am 4", + "ts": "2015-10-28T12:59:35", + "url": "http://www.europarl.europa.eu/RegData/seance_pleniere/proces_verbal/2015/10-28/votes_nominaux/xml/P8_PV(2015)10-28(RCV)_XC.xml", + "voteid": "59145" + }, + { + "For": { + "groups": [ + { + "group": "PPE", + "votes": [ + { + "ep_id": 2307, + "name": "Pirker", + "userid": 4611 + } + ] + } + ], + "total": "1" + }, + "Against": { + "groups": [ + { + "group": "PPE", + "votes": [ + { + "ep_id": 96673, + "name": "Ludvigsson", + "userid": 5860 + } + ] + } + ], + "total": "1" + }, + "_id": "56617a11ecc52ed712fc6b15", + "dossierid": "55a4580ed1d1c57c1e000002", + "epref": "2015/2132(BUD)", + "eptitle": "2016 general budget: all sections", + "issue_type": "Am 29", + "rapporteur": [ + { + "name": "Jos\u00e9 Manuel Fernandes", + "ref": 96899 + }, + { + "name": " G\u00e9rard Deprez", + "ref": 1473 + } + ], + "report": "A8-0298/2015", + "title": "A8-0298/2015 - Jos\u00e9 Manuel Fernandes et G\u00e9rard Deprez - Am 29", + "ts": "2015-10-28T13:00:12", + "url": "http://www.europarl.europa.eu/RegData/seance_pleniere/proces_verbal/2015/10-28/votes_nominaux/xml/P8_PV(2015)10-28(RCV)_XC.xml", + "voteid": "59146" + }, + { + "Abstain": { + "groups": [ + { + "group": "PPE", + "votes": [ + { + "ep_id": 2307, + "name": "Pirker", + "userid": 4611 + } + ] + } + ], + "total": "1" + }, + "Against": { + "groups": [ + { + "group": "PPE", + "votes": [ + { + "ep_id": 96673, + "name": "Ludvigsson", + "userid": 5860 + } + ] + } + ], + "total": "1" + }, + "_id": "56617a11ecc52ed712fc6b16", + "dossierid": "55a4580ed1d1c57c1e000002", + "epref": "2015/2132(BUD)", + "eptitle": "2016 general budget: all sections", + "issue_type": "Am 31", + "rapporteur": [ + { + "name": "Jos\u00e9 Manuel Fernandes", + "ref": 96899 + }, + { + "name": " G\u00e9rard Deprez", + "ref": 1473 + } + ], + "report": "A8-0298/2015", + "title": "A8-0298/2015 - Jos\u00e9 Manuel Fernandes et G\u00e9rard Deprez - Am 31", + "ts": "2015-10-28T13:00:42", + "url": "http://www.europarl.europa.eu/RegData/seance_pleniere/proces_verbal/2015/10-28/votes_nominaux/xml/P8_PV(2015)10-28(RCV)_XC.xml", + "voteid": "59147" + }, + { + "For": { + "groups": [ + { + "group": "PPED", + "votes": [ + { + "ep_id": 12323196673, + "name": "FAILLLLL", + "userid": 5860 + }, + { + "name": "FAILLLLL", + "userid": 5860 + } + ] + }, + { + "group": "PPE", + "votes": [ + { + "ep_id": 2307, + "name": "Pirker", + "userid": 4611 + } + ] + } + ], + "total": "2" + }, + "_id": "56617a11ecc52ed712fc6b17", + "dossierid": "55a4580ed1d1c57c1e000002", + "epref": "2015/2132(BUD)", + "eptitle": "2016 general budget: all sections", + "issue_type": "Am 30", + "rapporteur": [ + { + "name": "Jos\u00e9 Manuel Fernandes", + "ref": 96899 + }, + { + "name": " G\u00e9rard Deprez", + "ref": 1473 + } + ], + "report": "A8-0298/2015", + "title": "A8-0298/2015 - Jos\u00e9 Manuel Fernandes et G\u00e9rard Deprez - Am 30", + "ts": "2015-10-28T13:01:09", + "url": "http://www.europarl.europa.eu/RegData/seance_pleniere/proces_verbal/2015/10-28/votes_nominaux/xml/P8_PV(2015)10-28(RCV)_XC.xml", + "voteid": "59151" + } +] diff --git a/representatives_votes/contrib/parltrack/tests/votes_initial.json b/representatives_votes/contrib/parltrack/tests/votes_initial.json new file mode 100644 index 0000000000000000000000000000000000000000..090fc8e8005d25934099f1c66e47d07da368fa3c --- /dev/null +++ b/representatives_votes/contrib/parltrack/tests/votes_initial.json @@ -0,0 +1,68 @@ +[ +{ + "fields": { + "updated": "2015-12-13T01:10:09.683Z", + "reference": "2012/2002(INI)", + "title": "Agenda for change: the future of EU development policy", + "text": "", + "created": "2015-12-13T01:10:09.683Z", + "link": "http://www.europarl.europa.eu/oeil/popups/ficheprocedure.do?reference=2012/2002(INI)", + "fingerprint": "9e2cccdc5f6d22afd008af8b5b55dc193c27c5d6" + }, + "model": "representatives_votes.dossier", + "pk": 64 +}, +{ + "fields": { + "updated": "2015-12-13T01:10:09.698Z", + "reference": "2015/2132(BUD)", + "title": "2016 general budget: all sections", + "text": "", + "created": "2015-12-13T01:10:09.698Z", + "link": "http://www.europarl.europa.eu/oeil/popups/ficheprocedure.do?reference=2015/2132(BUD)", + "fingerprint": "e6856e0880e701c1022f23d595cc37a9a1cdcca8" + }, + "model": "representatives_votes.dossier", + "pk": 65 +}, +{ + "fields": { + "updated": "2015-12-12T23:06:32.305Z", + "last_name": "PIRKER", + "photo": "http://www.europarl.europa.eu/mepphoto/2307.jpg", + "created": "2015-12-12T22:56:14.401Z", + "gender": 2, + "remote_id": "2307", + "first_name": "Hubert", + "cv": "Transport and security spokesman, \u00d6VP Delegation, European Parliament;\nsecurity spokesman, \u00d6VP Delegation, European Parliament (2006-2009); security spokesman (coordinator), EPP Group (1999-2004); Deputy Head of \u00d6VP Delegation, European Parliament (1996-2004);", + "active": false, + "birth_place": "Gries", + "full_name": "Hubert PIRKER", + "fingerprint": "2a3c90346d40e9c540050534d832ceb3e0d25a49", + "birth_date": "1948-10-03", + "slug": "hubert-pirker" + }, + "model": "representatives.representative", + "pk": 1 +}, +{ + "fields": { + "updated": "2015-12-12T23:06:32.656Z", + "last_name": "LUDVIGSSON", + "photo": "http://www.europarl.europa.eu/mepphoto/96673.jpg", + "created": "2015-12-12T22:56:14.757Z", + "gender": 2, + "remote_id": "96673", + "first_name": "Olle", + "cv": "", + "active": true, + "birth_place": "H\u00e4ls\u00f6", + "full_name": "Olle LUDVIGSSON", + "fingerprint": "314d0f4c25af31bfa2a6b286838367994b902615", + "birth_date": "1948-10-28", + "slug": "olle-ludvigsson" + }, + "model": "representatives.representative", + "pk": 2 +} +] diff --git a/representatives_votes/management/commands/import_dossier_from_toutatis.py b/representatives_votes/management/commands/import_dossier_from_toutatis.py deleted file mode 100644 index 14eb2cd4c39486fbeb1e09f741d82b66d6069b28..0000000000000000000000000000000000000000 --- a/representatives_votes/management/commands/import_dossier_from_toutatis.py +++ /dev/null @@ -1,39 +0,0 @@ -# coding: utf-8 - -# This file is part of toutatis. -# -# toutatis 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. -# -# toutatis 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 Foobar. -# If not, see <http://www.gnu.org/licenses/>. -# -# Copyright (C) 2013 Laurent Peuch <cortex@worlddomination.be> -# Copyright (C) 2015 Arnaud Fabre <af@laquadrature.net> - -from django.core.management.base import BaseCommand -from representatives_votes.tasks import import_a_dossier_from_toutatis - -class Command(BaseCommand): - """ - Command to import a dossier from a toutatis server - """ - - def add_arguments(self, parser): - parser.add_argument('--celery', action='store_true', default=False) - parser.add_argument('fingerprint') - - def handle(self, *args, **options): - fingerprint = options['fingerprint'] - if options['celery']: - import_a_dossier_from_toutatis.delay(fingerprint) - else: - import_a_dossier_from_toutatis(fingerprint) diff --git a/representatives_votes/management/commands/import_proposal_from_toutatis.py b/representatives_votes/management/commands/import_proposal_from_toutatis.py deleted file mode 100644 index 7e2966b2edc31830575222c02b16126bd648a99d..0000000000000000000000000000000000000000 --- a/representatives_votes/management/commands/import_proposal_from_toutatis.py +++ /dev/null @@ -1,39 +0,0 @@ -# coding: utf-8 - -# This file is part of toutatis. -# -# toutatis 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. -# -# toutatis 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 Foobar. -# If not, see <http://www.gnu.org/licenses/>. -# -# Copyright (C) 2013 Laurent Peuch <cortex@worlddomination.be> -# Copyright (C) 2015 Arnaud Fabre <af@laquadrature.net> - -from django.core.management.base import BaseCommand -from representatives_votes.tasks import import_a_proposal_from_toutatis - -class Command(BaseCommand): - """ - Command to import a dossier from a toutatis server - """ - - def add_arguments(self, parser): - parser.add_argument('--celery', action='store_true', default=False) - parser.add_argument('fingerprint') - - def handle(self, *args, **options): - fingerprint = options['fingerprint'] - if options['celery']: - import_a_proposal_from_toutatis.delay(fingerprint, delay=True) - else: - import_a_proposal_from_toutatis(fingerprint, delay=False) diff --git a/representatives_votes/migrations/0001_initial.py b/representatives_votes/migrations/0001_initial.py index 22a3618c75602404fce40f4959c87093d775a771..ebf94c0218b8b770a54c30122494ff08f4af8911 100644 --- a/representatives_votes/migrations/0001_initial.py +++ b/representatives_votes/migrations/0001_initial.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/representatives_votes/migrations/0002_auto_20150707_1611.py b/representatives_votes/migrations/0002_auto_20150707_1611.py index 5b553b537b692a96a77850964df2960fced2f9ab..5565949b07f35180fa29b601d6ee12ac627dc1be 100644 --- a/representatives_votes/migrations/0002_auto_20150707_1611.py +++ b/representatives_votes/migrations/0002_auto_20150707_1611.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): @@ -19,17 +19,25 @@ class Migration(migrations.Migration): migrations.AddField( model_name='vote', name='representative', - field=models.ForeignKey(related_name='votes', to='representatives.Representative', null=True), + field=models.ForeignKey( + related_name='votes', + to='representatives.Representative', + null=True), ), migrations.AddField( model_name='vote', name='representative_fingerprint', - field=models.CharField(max_length=200, blank=True), + field=models.CharField( + max_length=200, + blank=True), ), migrations.AlterField( model_name='vote', name='representative_name', - field=models.CharField(default='', max_length=200, blank=True), + field=models.CharField( + default='', + max_length=200, + blank=True), preserve_default=False, ), ] diff --git a/representatives_votes/migrations/0003_auto_20150708_1358.py b/representatives_votes/migrations/0003_auto_20150708_1358.py index 2939234f98be0a32e3a2172fee36e08ebb2bd6e6..390f1ec7088986dc9f30062d34985a3d361f038c 100644 --- a/representatives_votes/migrations/0003_auto_20150708_1358.py +++ b/representatives_votes/migrations/0003_auto_20150708_1358.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): @@ -19,6 +19,8 @@ class Migration(migrations.Migration): migrations.AddField( model_name='proposal', name='representatives', - field=models.ManyToManyField(to='representatives.Representative', through='representatives_votes.Vote'), + field=models.ManyToManyField( + to='representatives.Representative', + through='representatives_votes.Vote'), ), ] diff --git a/representatives_votes/migrations/0004_auto_20150709_0819.py b/representatives_votes/migrations/0004_auto_20150709_0819.py index 1f897df1b20c95ebf878b9546bbac092b867f11e..9437c238f90e35b5ec885a142d45f606e37dab4a 100644 --- a/representatives_votes/migrations/0004_auto_20150709_0819.py +++ b/representatives_votes/migrations/0004_auto_20150709_0819.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): @@ -13,15 +13,20 @@ class Migration(migrations.Migration): operations = [ migrations.AlterModelOptions( name='proposal', - options={'ordering': ['datetime']}, + options={ + 'ordering': ['datetime']}, ), migrations.AlterModelOptions( name='vote', - options={'ordering': ['proposal__datetime']}, + options={ + 'ordering': ['proposal__datetime']}, ), migrations.AlterField( model_name='proposal', name='representatives', - field=models.ManyToManyField(related_name='proposals', through='representatives_votes.Vote', to='representatives.Representative'), + field=models.ManyToManyField( + related_name='proposals', + through='representatives_votes.Vote', + to='representatives.Representative'), ), ] diff --git a/representatives_votes/migrations/0005_make_dossier_reference_unique.py b/representatives_votes/migrations/0005_make_dossier_reference_unique.py new file mode 100644 index 0000000000000000000000000000000000000000..57f4f832304ed5890a4d30d2d425873ea1761b12 --- /dev/null +++ b/representatives_votes/migrations/0005_make_dossier_reference_unique.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('representatives_votes', '0004_auto_20150709_0819'), + ] + + operations = [ + migrations.AlterField( + model_name='dossier', + name='reference', + field=models.CharField(unique=True, max_length=200), + ), + ] diff --git a/representatives_votes/migrations/0006_duplicates.py b/representatives_votes/migrations/0006_duplicates.py new file mode 100644 index 0000000000000000000000000000000000000000..9a13adf5cb8c1babafbcccbd55da256aaaeb9ee8 --- /dev/null +++ b/representatives_votes/migrations/0006_duplicates.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +from django.db.models import Count + + +def remove_duplicate(apps, schema_editor): + Vote = apps.get_model('representatives_votes', 'Vote') + duplicates = Vote.objects.values('proposal_id', + 'representative_id').annotate(Count('id')).filter(id__count__gt=1) + + for duplicate in duplicates: + remove = Vote.objects.filter( + proposal_id=duplicate['proposal_id'], + representative_id=duplicate['representative_id']) + + for i in remove.values_list('pk')[1:]: + Vote.objects.get(pk=i[0]).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('representatives_votes', '0005_make_dossier_reference_unique'), + ] + + operations = [ + migrations.RunPython(remove_duplicate) + ] diff --git a/representatives_votes/migrations/0007_vote_unique_together_proposal_representative.py b/representatives_votes/migrations/0007_vote_unique_together_proposal_representative.py new file mode 100644 index 0000000000000000000000000000000000000000..1ffb38decc069586f310187267e5d633a58aea25 --- /dev/null +++ b/representatives_votes/migrations/0007_vote_unique_together_proposal_representative.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('representatives_votes', '0006_duplicates'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='vote', + unique_together=set([('proposal', 'representative')]), + ), + ] diff --git a/representatives_votes/migrations/0008_unique_proposal_title.py b/representatives_votes/migrations/0008_unique_proposal_title.py new file mode 100644 index 0000000000000000000000000000000000000000..faae4734ba89fbe8bbdebf77a481f78f9aa351e8 --- /dev/null +++ b/representatives_votes/migrations/0008_unique_proposal_title.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('representatives_votes', '0007_vote_unique_together_proposal_representative'), + ] + + operations = [ + migrations.AlterField( + model_name='proposal', + name='title', + field=models.CharField(unique=True, max_length=1000), + ), + ] diff --git a/representatives_votes/models.py b/representatives_votes/models.py index e2292fd19cb5b48c8c3f4d68342da5a8abe65902..41514fde5bc57fa4d219984e0e41faa2132a85a2 100644 --- a/representatives_votes/models.py +++ b/representatives_votes/models.py @@ -18,24 +18,25 @@ from django.db import models -from representatives.models import TimeStampedModel, HashableModel, Representative +from representatives.models import (HashableModel, Representative, + TimeStampedModel) class Dossier(HashableModel, TimeStampedModel): title = models.CharField(max_length=1000) - reference = models.CharField(max_length=200) + reference = models.CharField(max_length=200, unique=True) text = models.TextField(blank=True, default='') link = models.URLField() hashable_fields = ['title', 'reference'] - + def __unicode__(self): return unicode(self.title) class Proposal(HashableModel, TimeStampedModel): dossier = models.ForeignKey(Dossier, related_name='proposals') - title = models.CharField(max_length=1000) + title = models.CharField(max_length=1000, unique=True) description = models.TextField(blank=True, default='') reference = models.CharField(max_length=200, blank=True, null=True) datetime = models.DateTimeField() @@ -47,22 +48,21 @@ class Proposal(HashableModel, TimeStampedModel): representatives = models.ManyToManyField( Representative, through='Vote', related_name='proposals' ) - + hashable_fields = ['dossier', 'title', 'reference', 'kind', 'total_abstain', 'total_against', 'total_for'] class Meta: ordering = ['datetime'] - - + @property def status(self): if self.total_for > self.total_against: return 'adopted' else: return 'rejected' - + def __unicode__(self): return unicode(self.title) @@ -75,8 +75,9 @@ class Vote(models.Model): ) proposal = models.ForeignKey(Proposal, related_name='votes') - - representative = models.ForeignKey(Representative, related_name='votes', null=True) + + representative = models.ForeignKey( + Representative, related_name='votes', null=True) # Save representative name in case of we don't find the representative representative_name = models.CharField(max_length=200, blank=True) @@ -84,3 +85,4 @@ class Vote(models.Model): class Meta: ordering = ['proposal__datetime'] + unique_together = (('proposal', 'representative')) diff --git a/representatives_votes/serializers.py b/representatives_votes/serializers.py deleted file mode 100644 index d2b1a3cde0b47668f947e9484acb2da6bad8a094..0000000000000000000000000000000000000000 --- a/representatives_votes/serializers.py +++ /dev/null @@ -1,166 +0,0 @@ -# coding: utf-8 - -# This file is part of toutatis. -# -# toutatis 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. -# -# toutatis 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> - -import representatives_votes.models as models -from representatives.models import Representative -from rest_framework import serializers - - -class VoteSerializer(serializers.ModelSerializer): - """ - Vote serializer - """ - proposal = serializers.CharField( - source='proposal.fingerprint' - ) - representative = serializers.CharField( - source='representative.fingerprint', - allow_null=True - ) - - class Meta: - model = models.Vote - fields = ( - 'id', - 'proposal', - 'representative', - 'representative_name', - 'position' - ) - - def to_internal_value(self, data): - data = super(VoteSerializer, self).to_internal_value(data) - data['proposal'] = models.Proposal.objects.get( - fingerprint=data['proposal']['fingerprint'] - ) - if data['representative']['fingerprint']: - data['representative'] = Representative.objects.get( - fingerprint=data['representative']['fingerprint'] - ) - else: - data['representative'] = None - - return data - - -class ProposalSerializer(serializers.ModelSerializer): - dossier = serializers.CharField( - source='dossier.fingerprint' - ) - - dossier_title = serializers.CharField( - source='dossier.title', - read_only=True - ) - - dossier_reference = serializers.CharField( - source='dossier.reference', - read_only=True - ) - - class Meta: - model = models.Proposal - fields = ( - 'id', - 'fingerprint', - 'dossier', - 'dossier_title', - 'dossier_reference', - 'title', - 'description', - 'reference', - 'datetime', - 'kind', - 'total_abstain', - 'total_against', - 'total_for', - 'url', - ) - - def to_internal_value(self, data): - validated_data = super(ProposalSerializer, self).to_internal_value(data) - validated_data['dossier'] = models.Dossier.objects.get( - fingerprint=validated_data['dossier']['fingerprint'] - ) - validated_data['votes'] = data['votes'] - return validated_data - - def _create_votes(self, votes_data, proposal): - for vote in votes_data: - serializer = VoteSerializer(data=vote) - if serializer.is_valid(): - serializer.save() - else: - raise Exception(serializer.errors) - - def create(self, validated_data): - votes_data = validated_data.pop('votes') - proposal = models.Proposal.objects.create( - **validated_data - ) - self._create_votes(votes_data, proposal) - return proposal - - def update(self, instance, validated_data): - validated_data.pop('votes') - for attr, value in validated_data.iteritems(): - setattr(instance, attr, value) - instance.save() - return instance - - -class ProposalDetailSerializer(ProposalSerializer): - """ Proposal serializer that includes votes """ - votes = VoteSerializer(many=True) - - class Meta(ProposalSerializer.Meta): - fields = ProposalSerializer.Meta.fields + ( - 'votes', - ) - - -class DossierSerializer(serializers.ModelSerializer): - """ Base dossier serializer """ - class Meta: - model = models.Dossier - fields = ( - 'id', - 'fingerprint', - 'title', - 'reference', - 'text', - 'link', - 'url', - ) - - -class DossierDetailSerializer(DossierSerializer): - """ - Dossier serializer that includes proposals - and votes - """ - proposals = ProposalDetailSerializer( - many = True, - ) - - class Meta(DossierSerializer.Meta): - fields = DossierSerializer.Meta.fields + ( - 'proposals', - ) diff --git a/representatives_votes/tasks.py b/representatives_votes/tasks.py deleted file mode 100644 index b9004e698ce29ffee44595bedc08635a56c6dca9..0000000000000000000000000000000000000000 --- a/representatives_votes/tasks.py +++ /dev/null @@ -1,100 +0,0 @@ -# coding: utf-8 - -# This file is part of toutatis. -# -# toutatis 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. -# -# toutatis 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 - -import logging -import json - -from django.conf import settings - -import redis -from celery import shared_task -from urllib2 import urlopen - -from representatives_votes.models import Dossier, Proposal, Vote -from representatives.tasks import import_a_model -from representatives_votes.serializers import DossierSerializer, ProposalSerializer, VoteSerializer - -logger = logging.getLogger(__name__) - - -def import_a_dossier_from_toutatis(fingerprint): - ''' - Import a complete dossier from a toutatis server - ''' - toutatis_server = settings.TOUTATIS_SERVER - search_url = '{}/api/dossiers/?fingerprint={}'.format( - toutatis_server, - fingerprint - ) - logger.info('Import dossier with fingerprint {} from {}'.format( - fingerprint, - search_url - )) - data = json.load(urlopen(search_url)) - if data['count'] != 1: - raise Exception('Search should return one and only one result') - detail_url = data['results'][0]['url'] - data = json.load(urlopen(detail_url)) - dossier = import_a_model(data, Dossier, DossierSerializer) - for proposal in data['proposals']: - logger.info('Import proposal {}'.format(proposal['title'])) - import_a_model(proposal, Proposal, ProposalSerializer) - return dossier - -def import_a_proposal_from_toutatis(fingerprint): - ''' - Import a partial dossier from a toutatis server - ''' - toutatis_server = settings.TOUTATIS_SERVER - search_url = '{}/api/proposals/?fingerprint={}'.format( - toutatis_server, - fingerprint - ) - logger.info('Import proposal with fingerprint {} from {}'.format( - fingerprint, - search_url - )) - proposal_data = json.load(urlopen(search_url)) - if proposal_data['count'] != 1: - raise Exception('Search should return one and only one result') - detail_url = proposal_data['results'][0]['url'] - proposal_data = json.load(urlopen(detail_url)) - search_url = '{}/api/dossiers/?fingerprint={}'.format( - toutatis_server, - proposal_data['dossier'] - ) - dossier_data = json.load(urlopen(search_url)) - if dossier_data['count'] != 1: - raise Exception('Search should return one and only one result') - import_a_model(dossier_data['results'][0], Dossier, DossierSerializer) - return import_a_model(proposal_data, Proposal, ProposalSerializer) - - -def export_a_dossier(dossier): - serialized = DossierDetailSerializer(dossier) - return serialized.data - -def export_dossiers(filters={}): - return [export_a_dossier(dossier) for dossier in Dossier.objects.filter(**filters)] - -def export_all_dossier(): - return export_dossiers() diff --git a/test_project/test_project/__init__.py b/representatives_votes/tests/__init__.py similarity index 100% rename from test_project/test_project/__init__.py rename to representatives_votes/tests/__init__.py diff --git a/representatives_votes/tests/settings.py b/representatives_votes/tests/settings.py new file mode 100644 index 0000000000000000000000000000000000000000..8623f2e7e725a4f1e5c3e2e1e4e6fa4022d8f426 --- /dev/null +++ b/representatives_votes/tests/settings.py @@ -0,0 +1,57 @@ +import os + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + +SECRET_KEY = 'notsecret' + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'test.db'), + } +} + +INSTALLED_APPS = ( + 'representatives', + 'representatives_votes', +) + +DEBUG = True +LANGUAGE_CODE = 'en-us' +TIME_ZONE = 'UTC' +ROOT_URLCONF = 'representatives_votes.tests.urls' + +USE_I18N = True +USE_L10N = True +USE_TZ = True + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'simple': { + 'format': '%(levelname)s[%(module)s]: %(message)s' + }, + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'simple' + }, + }, + 'loggers': { + 'django': { + 'handlers': ['console'], + 'level': 'INFO' + }, + 'representatives': { + 'handlers': ['console'], + 'level': 'DEBUG' + }, + 'representatives_votes': { + 'handlers': ['console'], + 'level': 'DEBUG' + }, + }, +} diff --git a/representatives_votes/tests/urls.py b/representatives_votes/tests/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..637600f58aa4445293fcc008d547253d34470ffe --- /dev/null +++ b/representatives_votes/tests/urls.py @@ -0,0 +1 @@ +urlpatterns = [] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index ca5b397947409c6a867560fbc89e7f7526655a48..0000000000000000000000000000000000000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -django>=1.8,<1.9 -djangorestframework diff --git a/setup.py b/setup.py index 473a8101fdeb7400486d57f94ae741ead2b61bf0..1d839e01b1b4b0140fc7e61629d7bd14cc9753fd 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,10 @@ -from setuptools import setup, find_packages - +from setuptools import find_packages, setup setup( name='django-representatives-votes', - version='0.0.1', + version='0.0.7', description='Base app for government representative votes', - author='Olivier Le Thanh Duong', + author='Olivier Le Thanh Duong, Laurent Peuch, Arnaud Fabre, James Pic', author_email='olivier@lethanh.be', url='http://github.com/political-memory/django-representatives-votes', packages=find_packages(), @@ -14,9 +13,18 @@ setup( keywords='django government parliament votes', install_requires=[ 'django-representatives', + 'py-dateutil', + 'pytz', + 'ijson', ], + entry_points={ + 'console_scripts': [ + 'parltrack_import_dossiers = representatives_votes.contrib.parltrack.import_dossiers:main', + 'parltrack_import_votes = representatives_votes.contrib.parltrack.import_votes:main', + ] + }, classifiers=[ - 'Development Status :: 1 - Alpha/Planning', + 'Development Status :: 4 - Beta', 'Environment :: Web Environment', 'Framework :: Django', 'Intended Audience :: Developers', diff --git a/test_project/manage.py b/test_project/manage.py deleted file mode 100755 index 0fc36a34c27f8863de0560f451ddbfa5fa957f46..0000000000000000000000000000000000000000 --- a/test_project/manage.py +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env python -import os -import sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") - - from django.core.management import execute_from_command_line - - execute_from_command_line(sys.argv) diff --git a/test_project/test_project/settings.py b/test_project/test_project/settings.py deleted file mode 100644 index d5f18520906f732e2efd660c3c97f4d0b43f29cc..0000000000000000000000000000000000000000 --- a/test_project/test_project/settings.py +++ /dev/null @@ -1,104 +0,0 @@ -""" -Django settings for test_project project. - -Generated by 'django-admin startproject' using Django 1.8.5. - -For more information on this file, see -https://docs.djangoproject.com/en/1.8/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/1.8/ref/settings/ -""" - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -import os - -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'hul=ds-wua5*3r+o2k-%&jlleub99q7dot+lb(((v*^-@yk%g7' - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = [] - - -# Application definition - -INSTALLED_APPS = ( - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'representatives', - 'representatives_votes', -) - -MIDDLEWARE_CLASSES = ( - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.middleware.security.SecurityMiddleware', -) - -ROOT_URLCONF = 'test_project.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'test_project.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/1.8/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), - } -} - - -# Internationalization -# https://docs.djangoproject.com/en/1.8/topics/i18n/ - -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.8/howto/static-files/ - -STATIC_URL = '/static/' diff --git a/test_project/test_project/urls.py b/test_project/test_project/urls.py deleted file mode 100644 index 7dc47ea440461eab941518b8e3061d43506f38b9..0000000000000000000000000000000000000000 --- a/test_project/test_project/urls.py +++ /dev/null @@ -1,21 +0,0 @@ -"""test_project URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/1.8/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') -Including another URLconf - 1. Add an import: from blog import urls as blog_urls - 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) -""" -from django.conf.urls import include, url -from django.contrib import admin - -urlpatterns = [ - url(r'^admin/', include(admin.site.urls)), -] diff --git a/test_project/test_project/wsgi.py b/test_project/test_project/wsgi.py deleted file mode 100644 index cb26c81145664287d53b3820f8e0c2807cd3826d..0000000000000000000000000000000000000000 --- a/test_project/test_project/wsgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -WSGI config for test_project project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") - -application = get_wsgi_application()