├── popolo ├── importers │ ├── __init__.py │ └── popit.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── popolo_create_from_popit.py ├── migrations │ ├── __init__.py │ ├── 0002_update_models_from_upstream.py │ └── 0001_initial.py ├── templates │ ├── post_detail.html │ ├── person_detail.html │ ├── membership_detail.html │ └── organization_detail.html ├── behaviors │ ├── __init__.py │ ├── admin.py │ ├── models.py │ └── tests.py ├── __init__.py ├── urls.py ├── views.py ├── querysets.py ├── admin.py ├── tests.py └── models.py ├── tests_requirements.txt ├── AUTHORS ├── MANIFEST.in ├── .gitignore ├── .hgignore ├── CHANGES.txt ├── tox.ini ├── .travis.yml ├── LICENSE ├── runtests.py ├── setup.py ├── README.rst └── schema_parser.py /popolo/importers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /popolo/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /popolo/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /popolo/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /popolo/templates/post_detail.html: -------------------------------------------------------------------------------- 1 | Post -------------------------------------------------------------------------------- /popolo/templates/person_detail.html: -------------------------------------------------------------------------------- 1 | Persona -------------------------------------------------------------------------------- /popolo/templates/membership_detail.html: -------------------------------------------------------------------------------- 1 | Membership -------------------------------------------------------------------------------- /popolo/behaviors/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'guglielmo' 2 | -------------------------------------------------------------------------------- /popolo/templates/organization_detail.html: -------------------------------------------------------------------------------- 1 | Organizzazione -------------------------------------------------------------------------------- /tests_requirements.txt: -------------------------------------------------------------------------------- 1 | Django 2 | fake-factory==0.3.2 3 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Guglielmo Celata at Openpolis 2 | Juha Yrjölä at Code4Europe 3 | -------------------------------------------------------------------------------- /popolo/__init__.py: -------------------------------------------------------------------------------- 1 | "A Django-based implementation of the Popolo data specifications." 2 | 3 | __version__ = '0.0.5' 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include LICENSE 3 | include README.rst 4 | recursive-include popolo/static * 5 | recursive-include popolo/templates * 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | .project 3 | .pydevproject 4 | *~ 5 | *.db 6 | *.orig 7 | *.DS_Store 8 | .coverage 9 | .tox 10 | .idea 11 | *.egg-info/* 12 | docs/_build/* 13 | dist/* 14 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | # use glob syntax. 2 | syntax: glob 3 | *.py[co] 4 | .project 5 | .pydevproject 6 | *~ 7 | *.db 8 | *.orig 9 | *.DS_Store 10 | .coverage 11 | .tox/* 12 | .idea/* 13 | *.egg-info/* 14 | docs/_build/* 15 | dist/* 16 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | 0.0.5 2 | * Sync with upstream (at 3a34d5cb, plus PR #28). 3 | 0.0.2 4 | * Python 3 compatibility changes (James McKinney). 5 | 0.0.1 6 | * Initial release of fork on PyPI. The only difference with upstream is the 7 | removal of slugs and autoslug, so something using this app can decide how 8 | it handles them. 9 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | downloadcache = {toxworkdir}/_download/ 3 | envlist = py26-trunk,py26-1.4.X,py26-1.3.X 4 | 5 | [testenv] 6 | commands = {envpython} runtests.py 7 | 8 | [testenv:py26-trunk] 9 | basepython = python2.6 10 | deps = https://github.com/django/django/zipball/master 11 | 12 | [testenv:py26-1.4.X] 13 | basepython = python2.6 14 | deps = django>=1.4,<1.5 15 | 16 | [testenv:py26-1.3.X] 17 | basepython = python2.6 18 | deps = django>=1.3,<1.4 19 | 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.3" 5 | - "3.4" 6 | - "3.5" 7 | env: 8 | - DJANGO_VERSION='>=1.7,<1.8' 9 | - DJANGO_VERSION='>=1.8,<1.9' 10 | - DJANGO_VERSION='>=1.9,<1.10' 11 | matrix: 12 | exclude: 13 | - python: "3.3" 14 | env: DJANGO_VERSION='>=1.9,<1.10' 15 | - python: "3.5" 16 | env: DJANGO_VERSION='>=1.7,<1.8' 17 | install: 18 | - "pip install Django$DJANGO_VERSION" 19 | - "pip install -r tests_requirements.txt" 20 | - "python setup.py install" 21 | script: 22 | - "python runtests.py" 23 | -------------------------------------------------------------------------------- /popolo/urls.py: -------------------------------------------------------------------------------- 1 | from popolo.views import OrganizationDetailView, PersonDetailView, MembershipDetailView, PostDetailView 2 | 3 | __author__ = 'guglielmo' 4 | from django.conf.urls import patterns, url 5 | 6 | urlpatterns = patterns('', 7 | # organization 8 | url(r'^person/(?P[-\w]+)/$', PersonDetailView.as_view(), name='person-detail'), 9 | url(r'^organization/(?P[-\w]+)/$', OrganizationDetailView.as_view(), name='organization-detail'), 10 | url(r'^membership/(?P[-\w]+)/$', MembershipDetailView.as_view(), name='membership-detail'), 11 | url(r'^post/(?P[-\w]+)/$', PostDetailView.as_view(), name='post-detail'), 12 | ) 13 | 14 | -------------------------------------------------------------------------------- /popolo/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import DetailView 2 | from popolo.models import Organization, Person, Membership, Post 3 | 4 | 5 | class PersonDetailView(DetailView): 6 | model = Person 7 | context_object_name = 'person' 8 | template_name='person_detail.html' 9 | 10 | class OrganizationDetailView(DetailView): 11 | model = Organization 12 | context_object_name = 'organization' 13 | template_name='organization_detail.html' 14 | 15 | 16 | class MembershipDetailView(DetailView): 17 | model = Membership 18 | context_object_name = 'membership' 19 | template_name='membership_detail.html' 20 | 21 | 22 | class PostDetailView(DetailView): 23 | model = Post 24 | context_object_name = 'post' 25 | template_name='post_detail.html' -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | django-popolo is a django implementation of the Popolo's open government data specifications 2 | 3 | Copyright (C) 2013 Guglielmo Celata, Juha Yrjölä 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | -------------------------------------------------------------------------------- /popolo/behaviors/admin.py: -------------------------------------------------------------------------------- 1 | try: 2 | from django.contrib.contenttypes.admin import GenericTabularInline,GenericStackedInline 3 | except ImportError: 4 | from django.contrib.contenttypes.generic import GenericTabularInline,GenericStackedInline 5 | from popolo import models, behaviors 6 | 7 | class LinkAdmin(GenericTabularInline): 8 | model = models.Link 9 | extra = 0 10 | class IdentifierAdmin(GenericTabularInline): 11 | model = models.Identifier 12 | extra = 0 13 | class ContactDetailAdmin(GenericStackedInline): 14 | model = models.ContactDetail 15 | extra = 0 16 | class OtherNameAdmin(GenericTabularInline): 17 | model = models.OtherName 18 | extra = 0 19 | class SourceAdmin(GenericTabularInline): 20 | model = models.Source 21 | extra = 0 22 | 23 | BASE_INLINES = [ 24 | LinkAdmin,IdentifierAdmin,ContactDetailAdmin,OtherNameAdmin,SourceAdmin 25 | ] 26 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | 4 | import django 5 | from django.conf import settings 6 | 7 | 8 | if not settings.configured: 9 | settings.configure( 10 | DATABASES={ 11 | 'default': { 12 | 'ENGINE': 'django.db.backends.sqlite3', 13 | 'NAME': ':memory:', 14 | } 15 | }, 16 | INSTALLED_APPS=( 17 | 'django.contrib.contenttypes', 18 | 'popolo', 19 | ), 20 | SITE_ID=1, 21 | SECRET_KEY='this-is-just-for-tests-so-not-that-secret', 22 | ROOT_URLCONF='popolo.urls', 23 | ) 24 | 25 | 26 | from django.test.utils import get_runner 27 | 28 | 29 | def runtests(): 30 | if django.VERSION[:2] >= (1, 7): 31 | django.setup() 32 | TestRunner = get_runner(settings) 33 | test_runner = TestRunner(verbosity=1, interactive=True, failfast=False) 34 | failures = test_runner.run_tests(['popolo', ]) 35 | sys.exit(failures) 36 | 37 | 38 | if __name__ == '__main__': 39 | runtests() 40 | 41 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | 5 | def read_file(filename): 6 | """Read a file into a string""" 7 | path = os.path.abspath(os.path.dirname(__file__)) 8 | filepath = os.path.join(path, filename) 9 | try: 10 | return open(filepath).read() 11 | except IOError: 12 | return '' 13 | 14 | 15 | setup( 16 | name='mysociety-django-popolo', 17 | version=__import__('popolo').__version__, 18 | author='Guglielmo Celata', 19 | author_email='guglielmo@openpolis.it', 20 | maintainer='Matthew Somerville', 21 | maintainer_email='matthew@mysociety.org', 22 | packages=find_packages(), 23 | include_package_data=True, 24 | url='http://github.com/mysociety/django-popolo', 25 | license='Affero', 26 | description=u' '.join(__import__('popolo').__doc__.splitlines()).strip(), 27 | classifiers=[ 28 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 29 | 'Intended Audience :: Developers', 30 | 'Programming Language :: Python', 31 | 'Programming Language :: Python :: 2', 32 | 'Programming Language :: Python :: 3', 33 | 'Framework :: Django', 34 | 'Development Status :: 5 - Production/Stable', 35 | 'Operating System :: OS Independent', 36 | ], 37 | long_description=read_file('README.rst'), 38 | test_suite="runtests.runtests", 39 | zip_safe=False, 40 | tests_require=['fake-factory'], 41 | install_requires=[ 42 | "django-model-utils", 43 | ], 44 | ) 45 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | popolo 2 | ======================== 3 | 4 | 5 | .. image:: https://travis-ci.org/mysociety/django-popolo.svg?branch=master 6 | :target: https://travis-ci.org/mysociety/django-popolo 7 | 8 | .. image:: https://coveralls.io/repos/mysociety/django-popolo/badge.svg?branch=master&service=github 9 | :target: https://coveralls.io/github/mysociety/django-popolo?branch=master 10 | 11 | 12 | Welcome to the documentation for django-popolo! 13 | 14 | 15 | This fork of django-popolo is the same as the `upstream code 16 | `_ except that the models use 17 | integer IDs rather than slugs, and the models have no slug field. This is to 18 | enable projects using this package to pick their own (perhaps different) slug 19 | behaviour. 20 | 21 | 22 | **django-popolo** is a django-based implementation of the 23 | `Popolo's open government data specifications `_. 24 | 25 | It is developed as a django application to be deployed directly within django projects. 26 | 27 | It will allow web developers using it to manage and store data according to Popolo's specifications. 28 | 29 | The standard sql-oriented django ORM will be used. 30 | 31 | Project is under way and any help is welcome. 32 | 33 | 34 | Installation 35 | ------------ 36 | 37 | To install ``django-popolo`` as a third party app within a Django project, you 38 | need to add it to the Django project's requirements.txt. You can do this from 39 | GitHub in the usual way, or using the ``mysociety-django-popolo`` package on 40 | PyPI. 41 | 42 | Running the Tests 43 | ------------------------------------ 44 | 45 | Set up the tests with: 46 | 47 | pip install -r tests_requirements.txt 48 | python setup.py install 49 | 50 | You can run the tests with:: 51 | 52 | python setup.py test 53 | 54 | or:: 55 | 56 | python runtests.py 57 | -------------------------------------------------------------------------------- /popolo/querysets.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Q 2 | 3 | __author__ = 'guglielmo' 4 | 5 | from django.db import models 6 | from datetime import datetime 7 | 8 | class DateframeableQuerySet(models.query.QuerySet): 9 | """ 10 | A custom ``QuerySet`` allowing easy retrieval of current, past and future instances 11 | of a Dateframeable model. 12 | 13 | Here, a *Dateframeable model* denotes a model class having an associated date range. 14 | 15 | We assume that the date range is described by two ``Char`` fields 16 | named ``start_date`` and ``end_date``, respectively, 17 | whose validation pattern is: "^[0-9]{4}(-[0-9]{2}){0,2}$", 18 | in order to represent partial dates. 19 | """ 20 | def past(self, moment=None): 21 | """ 22 | Return a QuerySet containing the *past* instances of the model 23 | (i.e. those having an end date which is in the past). 24 | """ 25 | if moment is None: 26 | moment = datetime.strftime(datetime.now(), '%Y-%m-%d') 27 | return self.filter(end_date__lte=moment) 28 | 29 | def future(self, moment=None): 30 | """ 31 | Return a QuerySet containing the *future* instances of the model 32 | (i.e. those having a start date which is in the future). 33 | """ 34 | if moment is None: 35 | moment = datetime.strftime(datetime.now(), '%Y-%m-%d') 36 | return self.filter(start_date__gte=moment) 37 | 38 | def current(self, moment=None): 39 | """ 40 | Return a QuerySet containing the *current* instances of the model 41 | at the given moment in time, if the parameter is spcified 42 | now if it is not 43 | @moment - is a string, representing a date in the YYYY-MM-DD format 44 | (i.e. those for which the moment date-time lies within their associated time range). 45 | """ 46 | if moment is None: 47 | moment = datetime.strftime(datetime.now(), '%Y-%m-%d') 48 | 49 | return self.filter(Q(start_date__lte=moment) & 50 | (Q(end_date__gte=moment) | Q(end_date__isnull=True))) 51 | 52 | 53 | 54 | 55 | class PersonQuerySet(DateframeableQuerySet): 56 | pass 57 | 58 | class OrganizationQuerySet(DateframeableQuerySet): 59 | pass 60 | 61 | class PostQuerySet(DateframeableQuerySet): 62 | pass 63 | 64 | class MembershipQuerySet(DateframeableQuerySet): 65 | pass 66 | 67 | class ContactDetailQuerySet(DateframeableQuerySet): 68 | pass 69 | 70 | class OtherNameQuerySet(DateframeableQuerySet): 71 | pass 72 | -------------------------------------------------------------------------------- /popolo/management/commands/popolo_create_from_popit.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import re 4 | 5 | from django.core.management.base import BaseCommand, CommandError 6 | 7 | from popolo.importers.popit import PopItImporter 8 | 9 | class Command(PopItImporter, BaseCommand): 10 | 11 | # This is so that this command works on Django 1.8 as well as 12 | # earlier versions. See "Changed in Django 1.8" here: 13 | # https://docs.djangoproject.com/en/1.8/howto/custom-management-commands/ 14 | def add_arguments(self, parser): 15 | parser.add_argument('args', nargs='+') 16 | 17 | def handle(self, *args, **options): 18 | 19 | if len(args) != 1: 20 | message = "You must supply a filename with exported PopIt JSON" 21 | raise CommandError(message) 22 | 23 | popit_export_filename = args[0] 24 | 25 | self.import_from_export_json(popit_export_filename) 26 | 27 | # ------------------------------------------------------------------------ 28 | # These overridden methods deal with common incompatabilities 29 | # between what PopIt and django-popolo allow. Those that truncate 30 | # fields that are too long (the majority of these things) should 31 | # be removed if the max_length of those fields are increased in 32 | # django-popolo in the future. 33 | 34 | def update_person(self, person_data): 35 | new_person_data = person_data.copy() 36 | # There are quite a lot of summary fields in PopIt that are 37 | # way longer than 1024 characters. 38 | new_person_data['summary'] = (person_data.get('summary') or '')[:1024] 39 | # Surprisingly, quite a lot of PopIt email addresses have 40 | # extraneous whitespace in them, so strip any out to avoid 41 | # the 'Enter a valid email address' ValidationError on saving: 42 | email = person_data.get('email') or None 43 | if email: 44 | email = re.sub(r'\s*', '', email) 45 | new_person_data['email'] = email 46 | return super(Command, self).update_person(new_person_data) 47 | 48 | def make_contact_detail_dict(self, contact_detail_data): 49 | new_contact_detail_data = contact_detail_data.copy() 50 | # There are some contact types that are used in PopIt that are 51 | # longer than 12 characters... 52 | new_contact_detail_data['type'] = contact_detail_data['type'][:12] 53 | return super(Command, self).make_contact_detail_dict(new_contact_detail_data) 54 | 55 | def make_link_dict(self, link_data): 56 | new_link_data = link_data.copy() 57 | # There are some really long URLs in PopIt, which exceed the 58 | # 200 character limit in django-popolo. 59 | new_link_data['url'] = new_link_data['url'][:200] 60 | return super(Command, self).make_link_dict(new_link_data) 61 | -------------------------------------------------------------------------------- /popolo/behaviors/models.py: -------------------------------------------------------------------------------- 1 | try: 2 | from django.contrib.contenttypes.fields import GenericForeignKey 3 | except ImportError: 4 | # This fallback import is the version that was deprecated in 5 | # Django 1.7 and is removed in 1.9: 6 | from django.contrib.contenttypes.generic import GenericForeignKey 7 | from django.contrib.contenttypes.models import ContentType 8 | from django.core.exceptions import ValidationError 9 | from django.core.validators import RegexValidator 10 | from django.db import models 11 | from django.utils.translation import ugettext_lazy as _ 12 | from model_utils.fields import AutoCreatedField, AutoLastModifiedField 13 | from datetime import datetime 14 | 15 | __author__ = 'guglielmo' 16 | 17 | 18 | class GenericRelatable(models.Model): 19 | """ 20 | An abstract class that provides the possibility of generic relations 21 | """ 22 | content_type = models.ForeignKey(ContentType, blank=True, null=True) 23 | object_id = models.PositiveIntegerField(blank=True, null=True) 24 | content_object = GenericForeignKey('content_type', 'object_id') 25 | 26 | class Meta: 27 | abstract = True 28 | 29 | 30 | def validate_partial_date(value): 31 | """ 32 | Validate a partial date, it can be partial, but it must yet be a valid date. 33 | Accepted formats are: YYYY-MM-DD, YYYY-MM, YYYY. 34 | 2013-22 must rais a ValidationError, as 2013-13-12, or 2013-11-55. 35 | """ 36 | try: 37 | datetime.strptime(value, '%Y-%m-%d') 38 | except ValueError: 39 | try: 40 | datetime.strptime(value, '%Y-%m') 41 | except ValueError: 42 | try: 43 | datetime.strptime(value, '%Y') 44 | except ValueError: 45 | raise ValidationError(u'date seems not to be correct %s' % value) 46 | 47 | 48 | class Dateframeable(models.Model): 49 | """ 50 | An abstract base class model that provides a start and an end dates to the class. 51 | Uncomplete dates can be used. The validation pattern is: "^[0-9]{4}(-[0-9]{2}){0,2}$" 52 | """ 53 | partial_date_validator = RegexValidator(regex="^[0-9]{4}(-[0-9]{2}){0,2}$", message="Date has wrong format") 54 | 55 | start_date = models.CharField( 56 | _("start date"), max_length=10, blank=True, null=True, 57 | validators=[partial_date_validator, validate_partial_date], 58 | help_text=_("The date when the validity of the item starts"), 59 | ) 60 | end_date = models.CharField( 61 | _("end date"), max_length=10, blank=True, null=True, 62 | validators=[partial_date_validator, validate_partial_date], 63 | help_text=_("The date when the validity of the item ends") 64 | ) 65 | 66 | class Meta: 67 | abstract = True 68 | 69 | 70 | class Timestampable(models.Model): 71 | """ 72 | An abstract base class model that provides self-updating 73 | ``created`` and ``modified`` fields. 74 | """ 75 | created_at = AutoCreatedField(_('creation time')) 76 | updated_at = AutoLastModifiedField(_('last modification time')) 77 | 78 | class Meta: 79 | abstract = True 80 | -------------------------------------------------------------------------------- /popolo/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | try: 3 | from django.contrib.contenttypes.admin import GenericTabularInline 4 | except ImportError: 5 | from django.contrib.contenttypes.generic import GenericTabularInline 6 | 7 | from popolo import models 8 | from .behaviors import admin as generics 9 | from django.utils.translation import ugettext_lazy as _ 10 | 11 | 12 | class MembershipInline(admin.StackedInline): 13 | extra = 0 14 | model = models.Membership 15 | 16 | class PersonAdmin(admin.ModelAdmin): 17 | fieldsets = ( 18 | (None, { 19 | 'fields': ('name', 'gender', 'birth_date', 'death_date') 20 | }), 21 | ('Biography', { 22 | 'classes': ('collapse',), 23 | 'fields': ('summary', 'image', 'biography') 24 | }), 25 | ('Honorifics', { 26 | 'classes': ('collapse',), 27 | 'fields': ('honorific_prefix', 'honorific_suffix') 28 | }), 29 | ('Demography', { 30 | 'classes': ('collapse',), 31 | 'fields': ('national_identity',) 32 | }), 33 | ('Special Names', { 34 | 'classes': ('collapse',), 35 | 'fields': ('family_name', 'given_name', 'additional_name','patronymic_name','sort_name') 36 | }), 37 | ('Advanced options', { 38 | 'classes': ('collapse',), 39 | 'fields': ('start_date', 'end_date') 40 | }), 41 | ) 42 | inlines = generics.BASE_INLINES + [MembershipInline] 43 | 44 | class OrganizationMembersInline(MembershipInline): 45 | verbose_name = _("Member") 46 | verbose_name_plural = _("Members of this organization") 47 | fk_name = 'organization' 48 | class OrganizationOnBehalfInline(MembershipInline): 49 | verbose_name = "Proxy member" 50 | verbose_name_plural = "Members acting on behalf of this organization" 51 | fk_name = 'on_behalf_of' 52 | 53 | class PostAdmin(admin.ModelAdmin): 54 | model = models.Post 55 | fieldsets = ( 56 | (None, { 57 | 'fields': ('label','role', 'start_date', 'end_date') 58 | }), 59 | ('Details', { 60 | 'classes': ('collapse',), 61 | 'fields': ('other_label', 'area', 'organization') 62 | }), 63 | ) 64 | inlines = [ 65 | generics.LinkAdmin,generics.ContactDetailAdmin,generics.SourceAdmin 66 | ] 67 | 68 | class OrganizationAdmin(admin.ModelAdmin): 69 | fieldsets = ( 70 | (None, { 71 | 'fields': ('name', 'founding_date', 'dissolution_date') 72 | }), 73 | ('Details', { 74 | 'classes': ('collapse',), 75 | 'fields': ('summary', 'image', 'description') 76 | }), 77 | ('Advanced options', { 78 | 'classes': ('collapse',), 79 | 'fields': ('classification','start_date', 'end_date') 80 | }), 81 | ) 82 | inlines = generics.BASE_INLINES + [OrganizationMembersInline,OrganizationOnBehalfInline] 83 | 84 | 85 | admin.site.register(models.Post,PostAdmin) 86 | admin.site.register(models.Person,PersonAdmin) 87 | admin.site.register(models.Organization,OrganizationAdmin) 88 | -------------------------------------------------------------------------------- /schema_parser.py: -------------------------------------------------------------------------------- 1 | from django.core.validators import RegexValidator 2 | 3 | __author__ = 'guglielmo' 4 | 5 | import argparse 6 | import requests 7 | 8 | 9 | def main(): 10 | parser = argparse.ArgumentParser(description='Parse a remote popolo schema.') 11 | parser.add_argument('commands', metavar='C', type=str, nargs='+', 12 | help='One or more commands') 13 | parser.add_argument('--url', dest='url', type=str, nargs='?', 14 | help='Json URL') 15 | parser.add_argument('--generate', action='store_true', 16 | help='Generate class or field definition') 17 | 18 | args = parser.parse_args() 19 | commands = args.commands 20 | url = args.url 21 | generate = args.generate 22 | 23 | resp = requests.get(url) 24 | if resp.status_code != 200 or 'OpenDNS' in resp.headers['server']: 25 | print "URL not found" 26 | exit() 27 | 28 | try: 29 | schema = resp.json() 30 | except ValueError: 31 | print "No JSON at {0}".format(url) 32 | exit() 33 | 34 | if commands[0] == 'description': 35 | print "Description: {0}".format(schema['description']) 36 | elif commands[0] == 'title': 37 | print "Title: {0}".format(schema['title']) 38 | elif commands[0] == 'properties': 39 | if len(commands) == 1: 40 | if generate: 41 | generate_fields(schema['properties']) 42 | else: 43 | print "Properties:" 44 | for p in sorted(schema['properties'].keys()): 45 | print "{0} => {1}".format(p, schema['properties'][p]) 46 | else: 47 | p = commands[1] 48 | if p in schema['properties']: 49 | if generate: 50 | generate_field(p, schema['properties'][p]) 51 | else: 52 | print "{0} => {1}".format(p, schema['properties'][p]) 53 | else: 54 | print "No such property: {0}".format(p) 55 | 56 | 57 | def generate_fields(properties): 58 | """ 59 | Generate representations for all fields 60 | """ 61 | for k,v in sorted(properties.iteritems()): 62 | generate_field(k, v) 63 | 64 | def generate_field(key, value): 65 | """ 66 | Generate representation for a single field 67 | """ 68 | import types 69 | 70 | 71 | ## determine object type 72 | obj_type = None 73 | default = None 74 | if 'type' in value: 75 | if isinstance(value['type'], types.ListType): 76 | obj_type = value['type'][0] 77 | default = value['type'][1] 78 | else: 79 | obj_type = value['type'] 80 | default = None 81 | else: 82 | if '$ref' in value: 83 | print ' # reference to "{0}"'.format(value['$ref']) 84 | 85 | # convert key into label ('_' => ' ') 86 | label = " ".join(key.split("_")) 87 | 88 | 89 | required = value['required'] if 'required' in value else False 90 | nullable = not required and (default is None or default == 'null') 91 | 92 | model_class = None 93 | field_validator = None 94 | if obj_type == 'string': 95 | if 'format' in value: 96 | if value['format'] == 'email': 97 | model_class = "models.EmailField" 98 | elif value['format'] == 'date-time': 99 | model_class = "models.DateTimeField" 100 | elif value['format'] == 'uri': 101 | model_class = "models.URLField" 102 | 103 | else: 104 | model_class = "models.CharField" 105 | if 'pattern' in value: 106 | field_validator = """ 107 | RegexValidator( 108 | regex='{0}', 109 | message='{1} must follow the given pattern: {2}', 110 | code='invalid_{3}' 111 | ) 112 | """.format(value['pattern'], label, value['pattern'], key) 113 | 114 | elif obj_type == 'array': 115 | referenced_objects_type = value['items']['$ref'] 116 | print ' # add "{0}" property to get array of items referencing "{1}"'.format( 117 | key, referenced_objects_type 118 | ) 119 | 120 | if model_class: 121 | ## build field representation 122 | field_repr = ' {0} = {1}(_("{2}")'.format(key, model_class, label) 123 | if model_class == 'models.CharField': 124 | field_repr += ', max_length=128' 125 | if nullable: 126 | field_repr += ', blank=True' 127 | if model_class != 'models.CharField': 128 | field_repr += ', null=True' 129 | if field_validator: 130 | field_repr += ', validators=[{0}]'.format(field_validator) 131 | 132 | field_repr += ', help_text=_("{0}")'.format(value['description']) 133 | field_repr += ')' 134 | print field_repr 135 | 136 | 137 | if __name__ == "__main__": 138 | main() 139 | 140 | -------------------------------------------------------------------------------- /popolo/behaviors/tests.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | from datetime import timedelta 3 | from datetime import datetime 4 | from django.core.exceptions import ValidationError 5 | 6 | 7 | class BehaviorTestCaseMixin(object): 8 | def get_model(self): 9 | return getattr(self, 'model') 10 | 11 | def create_instance(self, **kwargs): 12 | raise NotImplementedError("Implement me") 13 | 14 | 15 | class DateframeableTests(BehaviorTestCaseMixin): 16 | """ 17 | Dateframeable tests. 18 | 19 | Are dates valid? Are invalid dates blocked? 20 | Are querysets to filter past, present and future items correct? 21 | """ 22 | 23 | def test_new_instance_has_valid_dates(self): 24 | """Test complete or incomplete dates, 25 | according to the "^[0-9]{4}(-[0-9]{2}){0,2}$" pattern (incomplete dates)""" 26 | obj = self.create_instance(start_date='2012') 27 | self.assertRegexpMatches(obj.start_date, "^[0-9]{4}(-[0-9]{2}){0,2}$", "date does not match pattern") 28 | obj = self.create_instance(end_date='2012') 29 | self.assertRegexpMatches(obj.end_date, "^[0-9]{4}(-[0-9]{2}){0,2}$", "date does not match pattern") 30 | 31 | obj = self.create_instance(start_date='2012-01') 32 | self.assertRegexpMatches(obj.start_date, "^[0-9]{4}(-[0-9]{2}){0,2}$", "date does not match pattern") 33 | obj = self.create_instance(end_date='2012-02') 34 | self.assertRegexpMatches(obj.end_date, "^[0-9]{4}(-[0-9]{2}){0,2}$", "date does not match pattern") 35 | 36 | obj = self.create_instance(start_date='2012-10-12') 37 | self.assertRegexpMatches(obj.start_date, "^[0-9]{4}(-[0-9]{2}){0,2}$", "date does not match pattern") 38 | obj = self.create_instance(end_date='2012-12-10') 39 | self.assertRegexpMatches(obj.end_date, "^[0-9]{4}(-[0-9]{2}){0,2}$", "date does not match pattern") 40 | 41 | 42 | def test_invalid_dates_are_blocked(self): 43 | """Test if dates are valid (months and days range are tested)""" 44 | # test invalid start dates 45 | with self.assertRaises(ValidationError): 46 | obj = self.create_instance(start_date='YESTERDAY') 47 | 48 | with self.assertRaises(ValidationError): 49 | obj = self.create_instance(start_date='2012-1210') 50 | 51 | with self.assertRaises(ValidationError): 52 | obj = self.create_instance(start_date='2012-13') 53 | 54 | with self.assertRaises(ValidationError): 55 | obj = self.create_instance(start_date='2012-12-34') 56 | 57 | # test invalid end dates 58 | with self.assertRaises(ValidationError): 59 | obj = self.create_instance(end_date='YESTERDAY') 60 | 61 | with self.assertRaises(ValidationError): 62 | obj = self.create_instance(end_date='2012-1210') 63 | 64 | with self.assertRaises(ValidationError): 65 | obj = self.create_instance(end_date='2012-13') 66 | 67 | with self.assertRaises(ValidationError): 68 | obj = self.create_instance(end_date='2012-12-34') 69 | 70 | 71 | def test_querysets_filters(self): 72 | """Test current, past and future querysets""" 73 | past_obj = self.create_instance(start_date=datetime.strftime(datetime.now()-timedelta(days=10), '%Y-%m-%d'), 74 | end_date=datetime.strftime(datetime.now()-timedelta(days=5), '%Y-%m-%d')) 75 | current_obj = self.create_instance(start_date=datetime.strftime(datetime.now()-timedelta(days=5), '%Y-%m-%d'), 76 | end_date=datetime.strftime(datetime.now()+timedelta(days=5), '%Y-%m-%d')) 77 | future_obj = self.create_instance(start_date=datetime.strftime(datetime.now()+timedelta(days=5), '%Y-%m-%d'), 78 | end_date=datetime.strftime(datetime.now()+timedelta(days=10), '%Y-%m-%d')) 79 | 80 | self.assertEqual(self.get_model().objects.all().count(), 3, "Something really bad is going on") 81 | self.assertEqual(self.get_model().objects.past().count(), 1, "One past object should have been fetched") 82 | self.assertEqual(self.get_model().objects.current().count(), 1, "One current object should have been fetched") 83 | self.assertEqual(self.get_model().objects.future().count(), 1, "One future object should have been fetched") 84 | 85 | 86 | class TimestampableTests(BehaviorTestCaseMixin): 87 | """ 88 | Timestampable tests. 89 | 90 | Tests whether objects are assigned timestamps at creation time, and 91 | whether a successive modification changes the update timestamp only. 92 | """ 93 | def test_new_instance_has_equal_timestamps(self): 94 | """Object is assigned timestamps when created""" 95 | obj = self.create_instance() 96 | self.assertIsNotNone(obj.created_at) 97 | self.assertIsNotNone(obj.updated_at) 98 | 99 | # created_at and updated_at are actually different, but still within 2 millisec 100 | # that's because of the pre-save signal validation 101 | self.assertTrue((obj.updated_at - obj.created_at) < timedelta(microseconds=10000)) 102 | 103 | 104 | def test_updated_instance_has_different_timestamps(self): 105 | """Modified object has different created_at and updated_at timestamps """ 106 | obj = self.create_instance() 107 | creation_ts = obj.created_at 108 | update_ts = obj.updated_at 109 | # save object after 30K microsecs and check again 110 | sleep(0.03) 111 | obj.save() 112 | self.assertEqual(obj.created_at, creation_ts) 113 | self.assertNotEqual(obj.updated_at, update_ts) 114 | 115 | # created_at and updated_at are actually different, well outside 10 millisecs 116 | self.assertFalse((obj.updated_at - obj.created_at) < timedelta(microseconds=10000)) 117 | -------------------------------------------------------------------------------- /popolo/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implements tests specific to the popolo module. 3 | Run with "manage.py test popolo, or with python". 4 | """ 5 | 6 | from django.test import TestCase 7 | from popolo.behaviors.tests import TimestampableTests, DateframeableTests 8 | from popolo.models import Person, Organization, Post, ContactDetail 9 | from faker import Factory 10 | 11 | faker = Factory.create('it_IT') # a factory to create fake names for tests 12 | 13 | 14 | class PersonTestCase(DateframeableTests, TimestampableTests, TestCase): 15 | model = Person 16 | object_name = 'person' 17 | 18 | def create_instance(self, **kwargs): 19 | if 'name' not in kwargs: 20 | kwargs.update({'name': u'test instance'}) 21 | return Person.objects.create(**kwargs) 22 | 23 | def test_add_membership(self): 24 | p = self.create_instance(name=faker.name(), birth_date=faker.year()) 25 | o = Organization.objects.create(name=faker.company()) 26 | p.add_membership(o) 27 | self.assertEqual(p.memberships.count(), 1) 28 | 29 | def test_add_memberships(self): 30 | p = self.create_instance(name=faker.name(), birth_date=faker.year()) 31 | os = [ 32 | Organization.objects.create(name=faker.company()) 33 | for i in range(3) 34 | ] 35 | p.add_memberships(os) 36 | self.assertEqual(p.memberships.count(), 3) 37 | 38 | def test_add_role(self): 39 | p = self.create_instance(name=faker.name(), birth_date=faker.year()) 40 | o = Organization.objects.create(name=faker.company()) 41 | r = Post.objects.create(label=u'CEO', organization=o) 42 | p.add_role(r) 43 | self.assertEqual(p.memberships.count(), 1) 44 | 45 | def test_add_contact_detail(self): 46 | p = self.create_instance() 47 | p.add_contact_detail(contact_type=ContactDetail.CONTACT_TYPES.email, value=faker.email()) 48 | self.assertEqual(p.contact_details.count(), 1) 49 | 50 | def test_add_contact_details(self): 51 | p = self.create_instance() 52 | contacts = [ 53 | {'contact_type': ContactDetail.CONTACT_TYPES.email, 54 | 'value': faker.email()}, 55 | {'contact_type': ContactDetail.CONTACT_TYPES.phone, 56 | 'value': faker.phone_number()}, 57 | 58 | ] 59 | p.add_contact_details(contacts) 60 | self.assertEqual(p.contact_details.count(), 2) 61 | 62 | def test_it_copies_birth_date_after_saving(self): 63 | pr = Person(name=faker.name(), birth_date=faker.year()) 64 | self.assertIsNone(pr.start_date) 65 | pr.save() 66 | self.assertEqual(pr.start_date, pr.birth_date) 67 | 68 | def test_it_copies_death_date_after_saving(self): 69 | pr = Person(name=faker.name(), death_date=faker.year()) 70 | self.assertIsNone(pr.end_date) 71 | pr.save() 72 | self.assertEqual(pr.end_date, pr.death_date) 73 | 74 | 75 | def test_add_links_and_sources(self): 76 | p = self.create_instance() 77 | p.links.create( url='http://link.example.org/', note='Note' ) 78 | p.sources.create( url='http://source.example.org/', note='Source note' ) 79 | self.assertEqual(p.links.count(), 1) 80 | self.assertEqual(p.sources.filter(url='http://link.example.org/').count(), 0) 81 | 82 | class OrganizationTestCase(DateframeableTests, TimestampableTests, TestCase): 83 | model = Organization 84 | object_name = 'organization' 85 | 86 | def create_instance(self, **kwargs): 87 | if 'name' not in kwargs: 88 | kwargs.update({'name': u'test instance'}) 89 | return Organization.objects.create(**kwargs) 90 | 91 | def test_add_member(self): 92 | o = self.create_instance(name=faker.company()) 93 | p = Person.objects.create(name=faker.name(), birth_date=faker.year()) 94 | o.add_member(p) 95 | self.assertEqual(o.memberships.count(), 1) 96 | 97 | def test_add_members(self): 98 | o = self.create_instance(name=faker.company()) 99 | ps = [ 100 | Person.objects.create(name=faker.name(), birth_date=faker.year()), 101 | Person.objects.create(name=faker.name(), birth_date=faker.year()), 102 | Person.objects.create(name=faker.name(), birth_date=faker.year()), 103 | ] 104 | o.add_members(ps) 105 | self.assertEqual(o.memberships.count(), 3) 106 | 107 | def test_add_post(self): 108 | o = Organization.objects.create(name=faker.company()) 109 | o.add_post(label=u'CEO') 110 | self.assertEqual(o.posts.count(), 1) 111 | 112 | def test_add_posts(self): 113 | o = Organization.objects.create(name=faker.company()) 114 | o.add_posts([ 115 | {'label': u'Presidente'}, 116 | {'label': u'Vicepresidente'}, 117 | ]) 118 | self.assertEqual(o.posts.count(), 2) 119 | 120 | def test_it_copies_the_foundation_date_to_start_date(self): 121 | o = Organization(name=faker.company(), founding_date=faker.year()) 122 | # it is not set to start_date until saved 123 | self.assertIsNone(o.start_date) 124 | o.save() 125 | self.assertEqual(o.start_date, o.founding_date) 126 | 127 | def test_it_copies_the_dissolution_date_to_end_date(self): 128 | o = Organization(name=faker.company(), dissolution_date=faker.year()) 129 | # it is not set to start_date until saved 130 | self.assertIsNone(o.end_date) 131 | o.save() 132 | self.assertEqual(o.end_date, o.dissolution_date) 133 | 134 | 135 | class PostTestCase(DateframeableTests, TimestampableTests, TestCase): 136 | model = Post 137 | 138 | def create_instance(self, **kwargs): 139 | if 'label' not in kwargs: 140 | kwargs.update({'label': u'test instance'}) 141 | if 'other_label' not in kwargs: 142 | kwargs.update({'other_label': u'TI,TEST'}) 143 | 144 | if 'organization' not in kwargs: 145 | o = Organization.objects.create(name=faker.company()) 146 | kwargs.update({'organization': o}) 147 | return Post.objects.create(**kwargs) 148 | 149 | def test_add_person(self): 150 | o = Organization.objects.create(name=faker.company()) 151 | p = self.create_instance(label=u'Chief Executive Officer', other_label=u'CEO,AD', organization=o) 152 | pr = Person.objects.create(name=faker.name(), birth_date=faker.year()) 153 | p.add_person(pr) 154 | self.assertEqual(p.memberships.count(), 1) 155 | 156 | -------------------------------------------------------------------------------- /popolo/migrations/0002_update_models_from_upstream.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | import django.utils.timezone 6 | import model_utils.fields 7 | import django.core.validators 8 | import popolo.behaviors.models 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | dependencies = [ 14 | ('contenttypes', '0001_initial'), 15 | ('popolo', '0001_initial'), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='Area', 21 | fields=[ 22 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 23 | ('object_id', models.PositiveIntegerField(null=True, blank=True)), 24 | ('start_date', models.CharField(validators=[django.core.validators.RegexValidator(regex=b'^[0-9]{4}(-[0-9]{2}){0,2}$', message=b'Date has wrong format'), popolo.behaviors.models.validate_partial_date], max_length=10, blank=True, help_text='The date when the validity of the item starts', null=True, verbose_name='start date')), 25 | ('end_date', models.CharField(validators=[django.core.validators.RegexValidator(regex=b'^[0-9]{4}(-[0-9]{2}){0,2}$', message=b'Date has wrong format'), popolo.behaviors.models.validate_partial_date], max_length=10, blank=True, help_text='The date when the validity of the item ends', null=True, verbose_name='end date')), 26 | ('created_at', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='creation time', editable=False)), 27 | ('updated_at', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='last modification time', editable=False)), 28 | ('name', models.CharField(help_text='A primary name', max_length=256, verbose_name='name', blank=True)), 29 | ('identifier', models.CharField(help_text='An issued identifier', max_length=512, verbose_name='identifier', blank=True)), 30 | ('classification', models.CharField(help_text='An area category, e.g. city', max_length=512, verbose_name='identifier', blank=True)), 31 | ('geom', models.TextField(help_text='A geometry', null=True, verbose_name='geom', blank=True)), 32 | ('inhabitants', models.IntegerField(help_text='The total number of inhabitants', null=True, verbose_name='inhabitants', blank=True)), 33 | ('content_type', models.ForeignKey(blank=True, to='contenttypes.ContentType', null=True)), 34 | ('parent', models.ForeignKey(related_name='children', blank=True, to='popolo.Area', help_text='The area that contains this area', null=True)), 35 | ], 36 | options={ 37 | 'abstract': False, 38 | }, 39 | ), 40 | migrations.CreateModel( 41 | name='AreaI18Name', 42 | fields=[ 43 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 44 | ('name', models.CharField(max_length=255, verbose_name='name')), 45 | ('area', models.ForeignKey(related_name='i18n_names', to='popolo.Area')), 46 | ], 47 | options={ 48 | 'verbose_name': 'I18N Name', 49 | 'verbose_name_plural': 'I18N Names', 50 | }, 51 | ), 52 | migrations.CreateModel( 53 | name='Language', 54 | fields=[ 55 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 56 | ('dbpedia_resource', models.CharField(help_text='DbPedia URI of the resource', unique=True, max_length=255)), 57 | ('iso639_1_code', models.CharField(max_length=2)), 58 | ('name', models.CharField(help_text='English name of the language', max_length=128)), 59 | ], 60 | ), 61 | migrations.AlterModelOptions( 62 | name='person', 63 | options={'verbose_name_plural': 'People'}, 64 | ), 65 | migrations.AddField( 66 | model_name='organization', 67 | name='description', 68 | field=models.TextField(help_text='An extended description of an organization', verbose_name='biography', blank=True), 69 | ), 70 | migrations.AddField( 71 | model_name='organization', 72 | name='image', 73 | field=models.URLField(help_text='A URL of an image, to identify the organization visually', null=True, verbose_name='image', blank=True), 74 | ), 75 | migrations.AddField( 76 | model_name='organization', 77 | name='summary', 78 | field=models.CharField(help_text='A one-line description of an organization', max_length=1024, verbose_name='summary', blank=True), 79 | ), 80 | migrations.AddField( 81 | model_name='person', 82 | name='national_identity', 83 | field=models.CharField(help_text='A national identity', max_length=128, null=True, verbose_name='national identity', blank=True), 84 | ), 85 | migrations.AddField( 86 | model_name='post', 87 | name='other_label', 88 | field=models.CharField(help_text='An alternate label, such as an abbreviation', max_length=512, null=True, verbose_name='other label', blank=True), 89 | ), 90 | migrations.AlterField( 91 | model_name='contactdetail', 92 | name='contact_type', 93 | field=models.CharField(help_text="A type of medium, e.g. 'fax' or 'email'", max_length=12, verbose_name='type', choices=[(b'ADDRESS', 'Address'), (b'EMAIL', 'Email'), (b'URL', 'Url'), (b'MAIL', 'Snail mail'), (b'TWITTER', 'Twitter'), (b'FACEBOOK', 'Facebook'), (b'PHONE', 'Telephone'), (b'MOBILE', 'Mobile'), (b'TEXT', 'Text'), (b'VOICE', 'Voice'), (b'FAX', 'Fax'), (b'CELL', 'Cell'), (b'VIDEO', 'Video'), (b'PAGER', 'Pager'), (b'TEXTPHONE', 'Textphone')]), 94 | ), 95 | migrations.AlterField( 96 | model_name='contactdetail', 97 | name='content_type', 98 | field=models.ForeignKey(blank=True, to='contenttypes.ContentType', null=True), 99 | ), 100 | migrations.AlterField( 101 | model_name='contactdetail', 102 | name='label', 103 | field=models.CharField(help_text='A human-readable label for the contact detail', max_length=512, verbose_name='label', blank=True), 104 | ), 105 | migrations.AlterField( 106 | model_name='contactdetail', 107 | name='note', 108 | field=models.CharField(help_text='A note, e.g. for grouping contact details by physical location', max_length=512, verbose_name='note', blank=True), 109 | ), 110 | migrations.AlterField( 111 | model_name='contactdetail', 112 | name='object_id', 113 | field=models.PositiveIntegerField(null=True, blank=True), 114 | ), 115 | migrations.AlterField( 116 | model_name='contactdetail', 117 | name='value', 118 | field=models.CharField(help_text='A value, e.g. a phone number or email address', max_length=512, verbose_name='value'), 119 | ), 120 | migrations.AlterField( 121 | model_name='identifier', 122 | name='content_type', 123 | field=models.ForeignKey(blank=True, to='contenttypes.ContentType', null=True), 124 | ), 125 | migrations.AlterField( 126 | model_name='identifier', 127 | name='identifier', 128 | field=models.CharField(help_text='An issued identifier, e.g. a DUNS number', max_length=512, verbose_name='identifier'), 129 | ), 130 | migrations.AlterField( 131 | model_name='identifier', 132 | name='object_id', 133 | field=models.PositiveIntegerField(null=True, blank=True), 134 | ), 135 | migrations.AlterField( 136 | model_name='link', 137 | name='content_type', 138 | field=models.ForeignKey(blank=True, to='contenttypes.ContentType', null=True), 139 | ), 140 | migrations.AlterField( 141 | model_name='link', 142 | name='note', 143 | field=models.CharField(help_text="A note, e.g. 'Wikipedia page'", max_length=512, verbose_name='note', blank=True), 144 | ), 145 | migrations.AlterField( 146 | model_name='link', 147 | name='object_id', 148 | field=models.PositiveIntegerField(null=True, blank=True), 149 | ), 150 | migrations.AlterField( 151 | model_name='link', 152 | name='url', 153 | field=models.URLField(help_text='A URL', max_length=350, verbose_name='url'), 154 | ), 155 | migrations.AlterField( 156 | model_name='membership', 157 | name='label', 158 | field=models.CharField(help_text='A label describing the membership', max_length=512, verbose_name='label', blank=True), 159 | ), 160 | migrations.AlterField( 161 | model_name='membership', 162 | name='organization', 163 | field=models.ForeignKey(related_name='memberships', blank=True, to='popolo.Organization', help_text='The organization that is a party to the relationship', null=True), 164 | ), 165 | migrations.AlterField( 166 | model_name='membership', 167 | name='role', 168 | field=models.CharField(help_text='The role that the person fulfills in the organization', max_length=512, verbose_name='role', blank=True), 169 | ), 170 | migrations.AlterField( 171 | model_name='organization', 172 | name='classification', 173 | field=models.CharField(help_text='An organization category, e.g. committee', max_length=512, verbose_name='classification', blank=True), 174 | ), 175 | migrations.AlterField( 176 | model_name='organization', 177 | name='dissolution_date', 178 | field=models.CharField(validators=[django.core.validators.RegexValidator(regex=b'^[0-9]{4}(-[0-9]{2}){0,2}$', message=b'dissolution date must follow the given pattern: ^[0-9]{4}(-[0-9]{2}){0,2}$', code=b'invalid_dissolution_date')], max_length=10, blank=True, help_text='A date of dissolution', null=True, verbose_name='dissolution date'), 179 | ), 180 | migrations.AlterField( 181 | model_name='organization', 182 | name='founding_date', 183 | field=models.CharField(validators=[django.core.validators.RegexValidator(regex=b'^[0-9]{4}(-[0-9]{2}){0,2}$', message=b'founding date must follow the given pattern: ^[0-9]{4}(-[0-9]{2}){0,2}$', code=b'invalid_founding_date')], max_length=10, blank=True, help_text='A date of founding', null=True, verbose_name='founding date'), 184 | ), 185 | migrations.AlterField( 186 | model_name='organization', 187 | name='name', 188 | field=models.CharField(help_text='A primary name, e.g. a legally recognized name', max_length=512, verbose_name='name'), 189 | ), 190 | migrations.AlterField( 191 | model_name='othername', 192 | name='content_type', 193 | field=models.ForeignKey(blank=True, to='contenttypes.ContentType', null=True), 194 | ), 195 | migrations.AlterField( 196 | model_name='othername', 197 | name='name', 198 | field=models.CharField(help_text='An alternate or former name', max_length=512, verbose_name='name'), 199 | ), 200 | migrations.AlterField( 201 | model_name='othername', 202 | name='note', 203 | field=models.CharField(help_text="A note, e.g. 'Birth name'", max_length=1024, verbose_name='note', blank=True), 204 | ), 205 | migrations.AlterField( 206 | model_name='othername', 207 | name='object_id', 208 | field=models.PositiveIntegerField(null=True, blank=True), 209 | ), 210 | migrations.AlterField( 211 | model_name='person', 212 | name='name', 213 | field=models.CharField(help_text="A person's preferred full name", max_length=512, verbose_name='name'), 214 | ), 215 | migrations.AlterField( 216 | model_name='person', 217 | name='summary', 218 | field=models.CharField(help_text="A one-line account of a person's life", max_length=1024, verbose_name='summary', blank=True), 219 | ), 220 | migrations.AlterField( 221 | model_name='post', 222 | name='label', 223 | field=models.CharField(help_text='A label describing the post', max_length=512, verbose_name='label', blank=True), 224 | ), 225 | migrations.AlterField( 226 | model_name='post', 227 | name='role', 228 | field=models.CharField(help_text='The function that the holder of the post fulfills', max_length=512, verbose_name='role', blank=True), 229 | ), 230 | migrations.AlterField( 231 | model_name='source', 232 | name='content_type', 233 | field=models.ForeignKey(blank=True, to='contenttypes.ContentType', null=True), 234 | ), 235 | migrations.AlterField( 236 | model_name='source', 237 | name='note', 238 | field=models.CharField(help_text="A note, e.g. 'Parliament website'", max_length=512, verbose_name='note', blank=True), 239 | ), 240 | migrations.AlterField( 241 | model_name='source', 242 | name='object_id', 243 | field=models.PositiveIntegerField(null=True, blank=True), 244 | ), 245 | migrations.AddField( 246 | model_name='areai18name', 247 | name='language', 248 | field=models.ForeignKey(to='popolo.Language'), 249 | ), 250 | migrations.AddField( 251 | model_name='membership', 252 | name='area', 253 | field=models.ForeignKey(related_name='memberships', blank=True, to='popolo.Area', help_text='The geographic area to which the post is related', null=True), 254 | ), 255 | migrations.AddField( 256 | model_name='organization', 257 | name='area', 258 | field=models.ForeignKey(related_name='organizations', blank=True, to='popolo.Area', help_text='The geographic area to which this organization is related', null=True), 259 | ), 260 | migrations.AddField( 261 | model_name='post', 262 | name='area', 263 | field=models.ForeignKey(related_name='posts', blank=True, to='popolo.Area', help_text='The geographic area to which the post is related', null=True), 264 | ), 265 | migrations.AlterUniqueTogether( 266 | name='areai18name', 267 | unique_together=set([('area', 'language', 'name')]), 268 | ), 269 | ] 270 | -------------------------------------------------------------------------------- /popolo/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | import django.utils.timezone 6 | import model_utils.fields 7 | import django.core.validators 8 | import popolo.behaviors.models 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | dependencies = [ 14 | ('contenttypes', '0001_initial'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='ContactDetail', 20 | fields=[ 21 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 22 | ('object_id', models.PositiveIntegerField()), 23 | ('start_date', models.CharField(validators=[django.core.validators.RegexValidator(regex=b'^[0-9]{4}(-[0-9]{2}){0,2}$', message=b'Date has wrong format'), popolo.behaviors.models.validate_partial_date], max_length=10, blank=True, help_text='The date when the validity of the item starts', null=True, verbose_name='start date')), 24 | ('end_date', models.CharField(validators=[django.core.validators.RegexValidator(regex=b'^[0-9]{4}(-[0-9]{2}){0,2}$', message=b'Date has wrong format'), popolo.behaviors.models.validate_partial_date], max_length=10, blank=True, help_text='The date when the validity of the item ends', null=True, verbose_name='end date')), 25 | ('created_at', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='creation time', editable=False)), 26 | ('updated_at', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='last modification time', editable=False)), 27 | ('label', models.CharField(help_text='A human-readable label for the contact detail', max_length=128, verbose_name='label', blank=True)), 28 | ('contact_type', models.CharField(help_text="A type of medium, e.g. 'fax' or 'email'", max_length=12, verbose_name='type', choices=[(b'FAX', 'Fax'), (b'PHONE', 'Telephone'), (b'MOBILE', 'Mobile'), (b'EMAIL', 'Email'), (b'MAIL', 'Snail mail'), (b'TWITTER', 'Twitter'), (b'FACEBOOK', 'Facebook')])), 29 | ('value', models.CharField(help_text='A value, e.g. a phone number or email address', max_length=128, verbose_name='value')), 30 | ('note', models.CharField(help_text='A note, e.g. for grouping contact details by physical location', max_length=128, verbose_name='note', blank=True)), 31 | ('content_type', models.ForeignKey(to='contenttypes.ContentType')), 32 | ], 33 | options={ 34 | 'abstract': False, 35 | }, 36 | ), 37 | migrations.CreateModel( 38 | name='Identifier', 39 | fields=[ 40 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 41 | ('object_id', models.PositiveIntegerField()), 42 | ('identifier', models.CharField(help_text='An issued identifier, e.g. a DUNS number', max_length=128, verbose_name='identifier')), 43 | ('scheme', models.CharField(help_text='An identifier scheme, e.g. DUNS', max_length=128, verbose_name='scheme', blank=True)), 44 | ('content_type', models.ForeignKey(to='contenttypes.ContentType')), 45 | ], 46 | options={ 47 | 'abstract': False, 48 | }, 49 | ), 50 | migrations.CreateModel( 51 | name='Link', 52 | fields=[ 53 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 54 | ('object_id', models.PositiveIntegerField()), 55 | ('url', models.URLField(help_text='A URL', verbose_name='url')), 56 | ('note', models.CharField(help_text="A note, e.g. 'Wikipedia page'", max_length=128, verbose_name='note', blank=True)), 57 | ('content_type', models.ForeignKey(to='contenttypes.ContentType')), 58 | ], 59 | options={ 60 | 'abstract': False, 61 | }, 62 | ), 63 | migrations.CreateModel( 64 | name='Membership', 65 | fields=[ 66 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 67 | ('start_date', models.CharField(validators=[django.core.validators.RegexValidator(regex=b'^[0-9]{4}(-[0-9]{2}){0,2}$', message=b'Date has wrong format'), popolo.behaviors.models.validate_partial_date], max_length=10, blank=True, help_text='The date when the validity of the item starts', null=True, verbose_name='start date')), 68 | ('end_date', models.CharField(validators=[django.core.validators.RegexValidator(regex=b'^[0-9]{4}(-[0-9]{2}){0,2}$', message=b'Date has wrong format'), popolo.behaviors.models.validate_partial_date], max_length=10, blank=True, help_text='The date when the validity of the item ends', null=True, verbose_name='end date')), 69 | ('created_at', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='creation time', editable=False)), 70 | ('updated_at', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='last modification time', editable=False)), 71 | ('label', models.CharField(help_text='A label describing the membership', max_length=128, verbose_name='label', blank=True)), 72 | ('role', models.CharField(help_text='The role that the person fulfills in the organization', max_length=128, verbose_name='role', blank=True)), 73 | ], 74 | options={ 75 | 'abstract': False, 76 | }, 77 | ), 78 | migrations.CreateModel( 79 | name='Organization', 80 | fields=[ 81 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 82 | ('start_date', models.CharField(validators=[django.core.validators.RegexValidator(regex=b'^[0-9]{4}(-[0-9]{2}){0,2}$', message=b'Date has wrong format'), popolo.behaviors.models.validate_partial_date], max_length=10, blank=True, help_text='The date when the validity of the item starts', null=True, verbose_name='start date')), 83 | ('end_date', models.CharField(validators=[django.core.validators.RegexValidator(regex=b'^[0-9]{4}(-[0-9]{2}){0,2}$', message=b'Date has wrong format'), popolo.behaviors.models.validate_partial_date], max_length=10, blank=True, help_text='The date when the validity of the item ends', null=True, verbose_name='end date')), 84 | ('created_at', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='creation time', editable=False)), 85 | ('updated_at', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='last modification time', editable=False)), 86 | ('name', models.CharField(help_text='A primary name, e.g. a legally recognized name', max_length=128, verbose_name='name')), 87 | ('classification', models.CharField(help_text='An organization category, e.g. committee', max_length=128, verbose_name='classification', blank=True)), 88 | ('dissolution_date', models.CharField(blank=True, help_text='A date of dissolution', max_length=10, verbose_name='dissolution date', validators=[django.core.validators.RegexValidator(regex=b'^[0-9]{4}(-[0-9]{2}){0,2}$', message=b'dissolution date must follow the given pattern: ^[0-9]{4}(-[0-9]{2}){0,2}$', code=b'invalid_dissolution_date')])), 89 | ('founding_date', models.CharField(blank=True, help_text='A date of founding', max_length=10, verbose_name='founding date', validators=[django.core.validators.RegexValidator(regex=b'^[0-9]{4}(-[0-9]{2}){0,2}$', message=b'founding date must follow the given pattern: ^[0-9]{4}(-[0-9]{2}){0,2}$', code=b'invalid_founding_date')])), 90 | ('parent', models.ForeignKey(related_name='children', blank=True, to='popolo.Organization', help_text='The organization that contains this organization', null=True)), 91 | ], 92 | options={ 93 | 'abstract': False, 94 | }, 95 | ), 96 | migrations.CreateModel( 97 | name='OtherName', 98 | fields=[ 99 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 100 | ('object_id', models.PositiveIntegerField()), 101 | ('start_date', models.CharField(validators=[django.core.validators.RegexValidator(regex=b'^[0-9]{4}(-[0-9]{2}){0,2}$', message=b'Date has wrong format'), popolo.behaviors.models.validate_partial_date], max_length=10, blank=True, help_text='The date when the validity of the item starts', null=True, verbose_name='start date')), 102 | ('end_date', models.CharField(validators=[django.core.validators.RegexValidator(regex=b'^[0-9]{4}(-[0-9]{2}){0,2}$', message=b'Date has wrong format'), popolo.behaviors.models.validate_partial_date], max_length=10, blank=True, help_text='The date when the validity of the item ends', null=True, verbose_name='end date')), 103 | ('name', models.CharField(help_text='An alternate or former name', max_length=128, verbose_name='name')), 104 | ('note', models.CharField(help_text="A note, e.g. 'Birth name'", max_length=256, verbose_name='note', blank=True)), 105 | ('content_type', models.ForeignKey(to='contenttypes.ContentType')), 106 | ], 107 | options={ 108 | 'abstract': False, 109 | }, 110 | ), 111 | migrations.CreateModel( 112 | name='Person', 113 | fields=[ 114 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 115 | ('start_date', models.CharField(validators=[django.core.validators.RegexValidator(regex=b'^[0-9]{4}(-[0-9]{2}){0,2}$', message=b'Date has wrong format'), popolo.behaviors.models.validate_partial_date], max_length=10, blank=True, help_text='The date when the validity of the item starts', null=True, verbose_name='start date')), 116 | ('end_date', models.CharField(validators=[django.core.validators.RegexValidator(regex=b'^[0-9]{4}(-[0-9]{2}){0,2}$', message=b'Date has wrong format'), popolo.behaviors.models.validate_partial_date], max_length=10, blank=True, help_text='The date when the validity of the item ends', null=True, verbose_name='end date')), 117 | ('created_at', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='creation time', editable=False)), 118 | ('updated_at', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='last modification time', editable=False)), 119 | ('name', models.CharField(help_text="A person's preferred full name", max_length=128, verbose_name='name')), 120 | ('family_name', models.CharField(help_text='One or more family names', max_length=128, verbose_name='family name', blank=True)), 121 | ('given_name', models.CharField(help_text='One or more primary given names', max_length=128, verbose_name='given name', blank=True)), 122 | ('additional_name', models.CharField(help_text='One or more secondary given names', max_length=128, verbose_name='additional name', blank=True)), 123 | ('honorific_prefix', models.CharField(help_text="One or more honorifics preceding a person's name", max_length=128, verbose_name='honorific prefix', blank=True)), 124 | ('honorific_suffix', models.CharField(help_text="One or more honorifics following a person's name", max_length=128, verbose_name='honorific suffix', blank=True)), 125 | ('patronymic_name', models.CharField(help_text='One or more patronymic names', max_length=128, verbose_name='patronymic name', blank=True)), 126 | ('sort_name', models.CharField(help_text='A name to use in an lexicographically ordered list', max_length=128, verbose_name='sort name', blank=True)), 127 | ('email', models.EmailField(help_text='A preferred email address', max_length=254, null=True, verbose_name='email', blank=True)), 128 | ('gender', models.CharField(help_text='A gender', max_length=128, verbose_name='gender', blank=True)), 129 | ('birth_date', models.CharField(help_text='A date of birth', max_length=10, verbose_name='birth date', blank=True)), 130 | ('death_date', models.CharField(help_text='A date of death', max_length=10, verbose_name='death date', blank=True)), 131 | ('summary', models.CharField(help_text="A one-line account of a person's life", max_length=512, verbose_name='summary', blank=True)), 132 | ('biography', models.TextField(help_text="An extended account of a person's life", verbose_name='biography', blank=True)), 133 | ('image', models.URLField(help_text='A URL of a head shot', null=True, verbose_name='image', blank=True)), 134 | ], 135 | options={ 136 | 'abstract': False, 137 | }, 138 | ), 139 | migrations.CreateModel( 140 | name='Post', 141 | fields=[ 142 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 143 | ('start_date', models.CharField(validators=[django.core.validators.RegexValidator(regex=b'^[0-9]{4}(-[0-9]{2}){0,2}$', message=b'Date has wrong format'), popolo.behaviors.models.validate_partial_date], max_length=10, blank=True, help_text='The date when the validity of the item starts', null=True, verbose_name='start date')), 144 | ('end_date', models.CharField(validators=[django.core.validators.RegexValidator(regex=b'^[0-9]{4}(-[0-9]{2}){0,2}$', message=b'Date has wrong format'), popolo.behaviors.models.validate_partial_date], max_length=10, blank=True, help_text='The date when the validity of the item ends', null=True, verbose_name='end date')), 145 | ('created_at', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='creation time', editable=False)), 146 | ('updated_at', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='last modification time', editable=False)), 147 | ('label', models.CharField(help_text='A label describing the post', max_length=128, verbose_name='label')), 148 | ('role', models.CharField(help_text='The function that the holder of the post fulfills', max_length=128, verbose_name='role', blank=True)), 149 | ('organization', models.ForeignKey(related_name='posts', to='popolo.Organization', help_text='The organization in which the post is held')), 150 | ], 151 | options={ 152 | 'abstract': False, 153 | }, 154 | ), 155 | migrations.CreateModel( 156 | name='Source', 157 | fields=[ 158 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 159 | ('object_id', models.PositiveIntegerField()), 160 | ('url', models.URLField(help_text='A URL', verbose_name='url')), 161 | ('note', models.CharField(help_text="A note, e.g. 'Parliament website'", max_length=128, verbose_name='note', blank=True)), 162 | ('content_type', models.ForeignKey(to='contenttypes.ContentType')), 163 | ], 164 | options={ 165 | 'abstract': False, 166 | }, 167 | ), 168 | migrations.AddField( 169 | model_name='membership', 170 | name='on_behalf_of', 171 | field=models.ForeignKey(related_name='memberships_on_behalf_of', blank=True, to='popolo.Organization', help_text='The organization on whose behalf the person is a party to the relationship', null=True), 172 | ), 173 | migrations.AddField( 174 | model_name='membership', 175 | name='organization', 176 | field=models.ForeignKey(related_name='memberships', to='popolo.Organization', help_text='The organization that is a party to the relationship'), 177 | ), 178 | migrations.AddField( 179 | model_name='membership', 180 | name='person', 181 | field=models.ForeignKey(related_name='memberships', to='popolo.Person', help_text='The person who is a party to the relationship'), 182 | ), 183 | migrations.AddField( 184 | model_name='membership', 185 | name='post', 186 | field=models.ForeignKey(related_name='memberships', blank=True, to='popolo.Post', help_text='The post held by the person in the organization through this membership', null=True), 187 | ), 188 | ] 189 | -------------------------------------------------------------------------------- /popolo/importers/popit.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | from contextlib import contextmanager 4 | import json 5 | import sys 6 | 7 | from django.apps import apps 8 | 9 | NEW_COLLECTIONS = ('organization', 'post', 'person', 'membership', 'area') 10 | 11 | @contextmanager 12 | def show_data_on_error(variable_name, data): 13 | """A context manager to output problematic data on any exception 14 | 15 | If there's an error when importing a particular person, says, it's 16 | useful to have in the error output that particular structure that 17 | caused problems. If you wrap the code that processes some data 18 | structure (a dictionary called 'my_data', say) with this: 19 | 20 | with show_data_on_error('my_data', my_data'): 21 | ... 22 | process(my_data) 23 | ... 24 | 25 | ... then if any exception is thrown in the 'with' block you'll see 26 | the data that was being processed when it was thrown.""" 27 | 28 | try: 29 | yield 30 | except: 31 | message = 'An exception was thrown while processing {0}:' 32 | print(message.format(variable_name), file=sys.stderr) 33 | print(json.dumps(data, indent=4, sort_keys=True)) 34 | raise 35 | 36 | class PopItImporter(object): 37 | 38 | """This class helps you to import data from PopIt into django-popolo 39 | 40 | If you instantiate this class, you can call 41 | import_from_export_json on that importer object with data from 42 | PopIt's /api/v0.1/export.json API endpoint. That will import the 43 | core Popolo data from that PopIt export into django-popolo's 44 | Django models. 45 | 46 | This is designed to be easy to subclass to extend its 47 | behaviour. For example: 48 | 49 | * If you need to use this importer in a migration, you can 50 | override the initializer and get_popolo_model_class to return 51 | the unfrozen model classes instead of the real current model 52 | classes. 53 | 54 | * If you need to preprocess the data being added you can override 55 | the methods that transform PopIt to django-popolo data (e.g. you 56 | could override make_link_dict to truncate excessively long 57 | URLs.) 58 | 59 | * If you're using multi-table inheritance or explicit one-to-one 60 | fields to add extra attributes to the django-popolo models, then 61 | you can add that data by overriding one or more of 62 | update_person, update_membership, etc. 63 | """ 64 | 65 | def get_popolo_model_class(self, model_name): 66 | """A default implementation for getting the Popolo model class""" 67 | return self.get_model_class('popolo', model_name) 68 | 69 | def get_model_class(self, app_label, model_name): 70 | return apps.get_model(app_label, model_name) 71 | 72 | def import_from_export_json(self, json_filename): 73 | """Update or create django-popolo models from a PopIt export 74 | 75 | You can run this multiple times to update the django-popolo 76 | models after the initial import.""" 77 | 78 | with open(json_filename) as f: 79 | data = json.load(f) 80 | 81 | # Keep track of all areas that are found, so that we can later 82 | # iterate over them and make sure their 'parent' property is 83 | # correctly set. 84 | area_id_to_django_object = {} 85 | area_id_to_parent_area_id = {} 86 | def update_optional_area(object_data): 87 | area_data = object_data.get('area') 88 | area = None 89 | if area_data: 90 | if not area_data.get('id'): 91 | return None 92 | area_parent_id = area_data.get('parent_id') 93 | if area_parent_id: 94 | area_id_to_parent_area_id 95 | with show_data_on_error('area_data', area_data): 96 | area_id, area = self.update_area(area_data) 97 | area_id_to_django_object[area_id] = area 98 | return area 99 | 100 | # Do one pass through the organizations: 101 | org_id_to_django_object = {} 102 | for org_data in data['organizations']: 103 | with show_data_on_error('org_data', org_data): 104 | area = update_optional_area(org_data) 105 | popit_id, organization = self.update_organization(org_data, area) 106 | org_id_to_django_object[popit_id] = organization 107 | # Then go through the organizations again to set the parent 108 | # organization: 109 | for org_data in data['organizations']: 110 | with show_data_on_error('org_data', org_data): 111 | org = org_id_to_django_object[org_data['id']] 112 | parent_id = org_data.get('parent_id') 113 | if parent_id: 114 | org_parent = org_id_to_django_object[parent_id] 115 | org.parent = org_parent 116 | org.save() 117 | # Create all posts (dependent on organizations already existing) 118 | post_id_to_django_object = {} 119 | for post_data in data.get('posts', []): 120 | with show_data_on_error('post_data', post_data): 121 | area = update_optional_area(post_data) 122 | popit_id, post = \ 123 | self.update_post(post_data, area, org_id_to_django_object) 124 | post_id_to_django_object[popit_id] = post 125 | # Create all people: 126 | person_id_to_django_object = {} 127 | for person_data in data['persons']: 128 | with show_data_on_error('person_data', person_data): 129 | popit_id, person = \ 130 | self.update_person(person_data) 131 | person_id_to_django_object[popit_id] = person 132 | # Now create all memberships to tie the people, organizations 133 | # and posts together: 134 | membership_id_to_django_object = {} 135 | for membership_data in data['memberships']: 136 | with show_data_on_error('membership_data', membership_data): 137 | area = update_optional_area(membership_data) 138 | membership_id, membership = \ 139 | self.update_membership( 140 | membership_data, 141 | area, 142 | org_id_to_django_object, 143 | post_id_to_django_object, 144 | person_id_to_django_object, 145 | ) 146 | membership_id_to_django_object[membership_id] = membership 147 | 148 | # Finally set any parent area relationships on areas: 149 | for area_id, parent_area_id in area_id_to_parent_area_id.items(): 150 | area = area_id_to_parent_area_id[area_id] 151 | parent_area = area_id_to_parent_area_id[parent_area_id] 152 | area.parent = parent_area 153 | area.save() 154 | 155 | def get_existing_django_object(self, popit_collection, popit_id): 156 | Identifier = self.get_popolo_model_class('Identifier') 157 | if popit_collection not in NEW_COLLECTIONS: 158 | raise Exception("Unknown collection '{collection}'".format( 159 | collection=popit_collection 160 | )) 161 | try: 162 | i = Identifier.objects.get( 163 | scheme=('popit-' + popit_collection), 164 | identifier=popit_id 165 | ) 166 | # Following i.content_object doesn't work in a migration, so use 167 | # a slightly more long-winded way to find the referenced object: 168 | model_class = self.get_popolo_model_class(i.content_type.model) 169 | return model_class.objects.get(pk=i.object_id) 170 | except Identifier.DoesNotExist: 171 | return None 172 | 173 | def update_organization(self, org_data, area): 174 | Organization = self.get_popolo_model_class('Organization') 175 | existing = self.get_existing_django_object('organization', org_data['id']) 176 | if existing is None: 177 | result = Organization() 178 | else: 179 | result = existing 180 | result.name = org_data['name'] 181 | result.classification = org_data.get('classification', '') 182 | result.dissolution_date = org_data.get('dissolution_date', '') 183 | result.founding_date = org_data.get('founding_date', '') 184 | result.image = org_data.get('image') or None 185 | result.area = area 186 | result.save() 187 | # Create an identifier with the PopIt ID: 188 | if not existing: 189 | self.create_identifier('organization', org_data['id'], result) 190 | 191 | # Update other identifiers: 192 | self.update_related_objects( 193 | Organization, 194 | self.get_popolo_model_class('Identifier'), 195 | self.make_identifier_dict, 196 | org_data['identifiers'], 197 | result, 198 | preserve_predicate=lambda i: i.scheme == 'popit-organization', 199 | ) 200 | # Update contact details: 201 | self.update_related_objects( 202 | Organization, 203 | self.get_popolo_model_class('ContactDetail'), 204 | self.make_contact_detail_dict, 205 | org_data.get('contact_details', []), 206 | result 207 | ) 208 | # Update links: 209 | self.update_related_objects( 210 | Organization, 211 | self.get_popolo_model_class('Link'), 212 | self.make_link_dict, 213 | org_data.get('links', []), 214 | result 215 | ) 216 | # Update sources: 217 | self.update_related_objects( 218 | Organization, 219 | self.get_popolo_model_class('Source'), 220 | self.make_source_dict, 221 | org_data.get('sources', []), 222 | result 223 | ) 224 | # Update other names: 225 | self.update_related_objects( 226 | Organization, 227 | self.get_popolo_model_class('OtherName'), 228 | self.make_other_name_dict, 229 | org_data.get('other_names', []), 230 | result 231 | ) 232 | return org_data['id'], result 233 | 234 | def update_post(self, post_data, area, org_id_to_django_object): 235 | Post = self.get_popolo_model_class('Post') 236 | existing = self.get_existing_django_object('post', post_data['id']) 237 | if existing is None: 238 | result = Post() 239 | else: 240 | result = existing 241 | result.label = post_data['label'] 242 | result.role = post_data['role'] 243 | result.organization = org_id_to_django_object[post_data['organization_id']] 244 | result.area = area 245 | result.save() 246 | # Create an identifier with the PopIt ID: 247 | if not existing: 248 | self.create_identifier('post', post_data['id'], result) 249 | # Update contact details: 250 | self.update_related_objects( 251 | Post, 252 | self.get_popolo_model_class('ContactDetail'), 253 | self.make_contact_detail_dict, 254 | post_data.get('contact_details', []), 255 | result 256 | ) 257 | # Update links: 258 | self.update_related_objects( 259 | Post, 260 | self.get_popolo_model_class('Link'), 261 | self.make_link_dict, 262 | post_data.get('links', []), 263 | result 264 | ) 265 | # Update sources: 266 | self.update_related_objects( 267 | Post, 268 | self.get_popolo_model_class('Source'), 269 | self.make_source_dict, 270 | post_data.get('sources', []), 271 | result 272 | ) 273 | return post_data['id'], result 274 | 275 | def update_person(self, person_data): 276 | Person = self.get_popolo_model_class('Person') 277 | existing = self.get_existing_django_object('person', person_data['id']) 278 | if existing is None: 279 | result = Person() 280 | else: 281 | result = existing 282 | result.name = person_data['name'] 283 | result.family_name = person_data.get('family_name') or '' 284 | result.given_name = person_data.get('given_name') or '' 285 | result.additional_name = person_data.get('additional_name') or '' 286 | result.honorific_prefix = person_data.get('honorific_prefix') or '' 287 | result.honorific_suffix = person_data.get('honorific_suffix') or '' 288 | result.patronymic_name = person_data.get('patronymic_name') or '' 289 | result.sort_name = person_data.get('sort_name') or '' 290 | result.email = person_data.get('email') or None 291 | result.gender = person_data.get('gender') or '' 292 | result.birth_date = person_data.get('birth_date') or '' 293 | result.death_date = person_data.get('death_date') or '' 294 | result.summary = person_data.get('summary') or '' 295 | result.biography = person_data.get('biography') or '' 296 | result.national_identitiy = person_data.get('national_identity') or None 297 | result.image = person_data.get('image') or None 298 | result.save() 299 | # Create an identifier with the PopIt ID: 300 | if not existing: 301 | self.create_identifier('person', person_data['id'], result) 302 | 303 | # Update other_names: 304 | self.update_related_objects( 305 | Person, 306 | self.get_popolo_model_class('OtherName'), 307 | self.make_other_name_dict, 308 | person_data.get('other_names', []), 309 | result 310 | ) 311 | # Update other identifiers: 312 | self.update_related_objects( 313 | Person, 314 | self.get_popolo_model_class('Identifier'), 315 | self.make_identifier_dict, 316 | person_data['identifiers'], 317 | result, 318 | preserve_predicate=lambda i: i.scheme == 'popit-person', 319 | ) 320 | # Update contact details: 321 | self.update_related_objects( 322 | Person, 323 | self.get_popolo_model_class('ContactDetail'), 324 | self.make_contact_detail_dict, 325 | person_data.get('contact_details', []), 326 | result 327 | ) 328 | # Update links: 329 | self.update_related_objects( 330 | Person, 331 | self.get_popolo_model_class('Link'), 332 | self.make_link_dict, 333 | person_data.get('links', []), 334 | result 335 | ) 336 | # Update sources: 337 | self.update_related_objects( 338 | Person, 339 | self.get_popolo_model_class('Source'), 340 | self.make_source_dict, 341 | person_data.get('sources', []), 342 | result 343 | ) 344 | return person_data['id'], result 345 | 346 | def update_membership( 347 | self, 348 | membership_data, 349 | area, 350 | org_id_to_django_object, 351 | post_id_to_django_object, 352 | person_id_to_django_object, 353 | ): 354 | Membership = self.get_popolo_model_class('Membership') 355 | existing = self.get_existing_django_object('membership', membership_data['id']) 356 | if existing is None: 357 | result = Membership() 358 | else: 359 | result = existing 360 | result.label = membership_data.get('label') or '' 361 | result.role = membership_data.get('role') or '' 362 | result.person = person_id_to_django_object[membership_data['person_id']] 363 | organization_id = membership_data.get('organization_id') 364 | if organization_id: 365 | result.organization = org_id_to_django_object[organization_id] 366 | on_behalf_of_id = membership_data.get('on_behalf_of_id') 367 | if on_behalf_of_id: 368 | result.on_behalf_of = org_id_to_django_object[on_behalf_of_id] 369 | post_id = membership_data.get('post_id') 370 | if post_id: 371 | result.post = post_id_to_django_object[post_id] 372 | result.area = area 373 | result.start_date = membership_data.get('start_date', '') 374 | result.end_date = membership_data.get('end_date', '') 375 | result.save() 376 | # Create an identifier with the PopIt ID: 377 | if not existing: 378 | self.create_identifier('membership', membership_data['id'], result) 379 | 380 | # Update contact details: 381 | self.update_related_objects( 382 | Membership, 383 | self.get_popolo_model_class('ContactDetail'), 384 | self.make_contact_detail_dict, 385 | membership_data.get('contact_details', []), 386 | result 387 | ) 388 | # Update links: 389 | self.update_related_objects( 390 | Membership, 391 | self.get_popolo_model_class('Link'), 392 | self.make_link_dict, 393 | membership_data.get('links', []), 394 | result 395 | ) 396 | # Update sources: 397 | self.update_related_objects( 398 | Membership, 399 | self.get_popolo_model_class('Source'), 400 | self.make_source_dict, 401 | membership_data.get('sources', []), 402 | result 403 | ) 404 | return membership_data['id'], result 405 | 406 | def update_area(self, area_data): 407 | Area = self.get_popolo_model_class('Area') 408 | existing = self.get_existing_django_object('area', area_data['id']) 409 | if existing is None: 410 | result = Area() 411 | else: 412 | result = existing 413 | result.name = area_data.get('name') or '' 414 | result.identifier = area_data.get('identifier') or '' 415 | result.classification = area_data.get('classification') or '' 416 | result.geom = area_data.get('geom') or None 417 | result.inhabitants = area_data.get('inhabitants') 418 | result.save() 419 | # Create an identifier with the PopIt ID: 420 | if not existing: 421 | self.create_identifier('area', area_data['id'], result) 422 | # Update other_identifiers: 423 | self.update_related_objects( 424 | Area, 425 | self.get_popolo_model_class('Identifier'), 426 | self.make_identifier_dict, 427 | area_data.get('other_identifiers', []), 428 | result, 429 | preserve_predicate=lambda i: i.scheme == 'popit-area', 430 | ) 431 | # Update sources: 432 | self.update_related_objects( 433 | Area, 434 | self.get_popolo_model_class('Source'), 435 | self.make_source_dict, 436 | area_data.get('sources', []), 437 | result 438 | ) 439 | return area_data['id'], result 440 | 441 | def create_identifier(self, popit_collection, popit_id, django_object): 442 | if popit_collection not in NEW_COLLECTIONS: 443 | raise Exception("Unknown collection '{collection}'".format( 444 | collection=popit_collection 445 | )) 446 | ContentType = self.get_model_class('contenttypes', 'ContentType') 447 | content_type = ContentType.objects.get_for_model(django_object) 448 | self.get_popolo_model_class('Identifier').objects.create( 449 | object_id=django_object.id, 450 | content_type_id=content_type.id, 451 | scheme=('popit-' + popit_collection), 452 | identifier=popit_id, 453 | ) 454 | 455 | def update_related_objects( 456 | self, 457 | django_main_model, 458 | django_related_model, 459 | popit_to_django_attributes_method, 460 | popit_related_objects_data, 461 | django_object, 462 | preserve_predicate=lambda o: False, 463 | ): 464 | # Find the unchanged related objects so we don't unnecessarily 465 | # recreate objects. 466 | ContentType = self.get_model_class('contenttypes', 'ContentType') 467 | main_content_type = ContentType.objects.get_for_model(django_main_model) 468 | new_objects = [] 469 | old_objects_to_preserve = [ 470 | o for o in django_related_model.objects.filter( 471 | content_type_id=main_content_type.id, 472 | object_id=django_object.id 473 | ) 474 | if preserve_predicate(o) 475 | ] 476 | for object_data in popit_related_objects_data: 477 | wanted_attributes = popit_to_django_attributes_method( 478 | object_data 479 | ) 480 | wanted_attributes['content_type_id'] = main_content_type.id 481 | wanted_attributes['object_id'] = django_object.id 482 | existing = django_related_model.objects.filter(**wanted_attributes) 483 | if existing.exists(): 484 | old_objects_to_preserve += existing 485 | else: 486 | new_objects.append( 487 | django_related_model.objects.create(**wanted_attributes) 488 | ) 489 | object_ids_to_preserve = set(c.id for c in new_objects) 490 | object_ids_to_preserve.update(c.id for c in old_objects_to_preserve) 491 | django_related_model.objects.filter( 492 | content_type_id=main_content_type.id, 493 | object_id=django_object.id 494 | ).exclude(pk__in=object_ids_to_preserve).delete() 495 | 496 | def make_contact_detail_dict(self, contact_detail_data): 497 | return { 498 | 'label': contact_detail_data.get('label') or '', 499 | 'contact_type': contact_detail_data['type'], 500 | 'value': contact_detail_data['value'], 501 | 'note': contact_detail_data.get('note') or '', 502 | 'start_date': contact_detail_data.get('valid_from') or '', 503 | 'end_date': contact_detail_data.get('valid_until') or '', 504 | } 505 | 506 | def make_link_dict(self, link_data): 507 | return { 508 | 'note': link_data['note'], 509 | 'url': link_data['url'], 510 | } 511 | 512 | def make_source_dict(self, source_data): 513 | return { 514 | 'url': source_data['url'], 515 | 'note': source_data['note'], 516 | } 517 | 518 | def make_other_name_dict(self, other_name_data): 519 | return { 520 | 'name': other_name_data.get('name') or '', 521 | 'note': other_name_data.get('note') or '', 522 | 'start_date': other_name_data.get('start_date') or '', 523 | 'end_date': other_name_data.get('end_date') or '', 524 | } 525 | 526 | def make_identifier_dict(self, identifier_data): 527 | return { 528 | 'identifier': identifier_data['identifier'], 529 | 'scheme': identifier_data['scheme'], 530 | } 531 | -------------------------------------------------------------------------------- /popolo/models.py: -------------------------------------------------------------------------------- 1 | try: 2 | from django.contrib.contenttypes.fields import GenericRelation 3 | except ImportError: 4 | # This fallback import is the version that was deprecated in 5 | # Django 1.7 and is removed in 1.9: 6 | from django.contrib.contenttypes.generic import GenericRelation 7 | 8 | try: 9 | # PassTrhroughManager was removed in django-model-utils 2.4 10 | # see issue #22 at https://github.com/openpolis/django-popolo/issues/22 11 | from model_utils.managers import PassThroughManager 12 | except ImportError: 13 | pass 14 | 15 | from django.core.validators import RegexValidator 16 | from django.db import models 17 | from model_utils import Choices 18 | from django.utils.encoding import python_2_unicode_compatible 19 | from django.utils.translation import ugettext_lazy as _ 20 | from django.db.models.signals import pre_save 21 | from django.dispatch import receiver 22 | 23 | from .behaviors.models import Timestampable, Dateframeable, GenericRelatable 24 | from .querysets import PostQuerySet, OtherNameQuerySet, ContactDetailQuerySet, MembershipQuerySet, OrganizationQuerySet, PersonQuerySet 25 | 26 | 27 | @python_2_unicode_compatible 28 | class Person(Dateframeable, Timestampable, models.Model): 29 | """ 30 | A real person, alive or dead 31 | see schema at http://popoloproject.com/schemas/person.json# 32 | """ 33 | 34 | json_ld_context = "http://popoloproject.com/contexts/person.jsonld" 35 | json_ld_type = "http://www.w3.org/ns/person#Person" 36 | 37 | name = models.CharField(_("name"), max_length=512, help_text=_("A person's preferred full name")) 38 | 39 | # array of items referencing "http://popoloproject.com/schemas/other_name.json#" 40 | other_names = GenericRelation('OtherName', help_text="Alternate or former names") 41 | 42 | # array of items referencing "http://popoloproject.com/schemas/identifier.json#" 43 | identifiers = GenericRelation('Identifier', help_text="Issued identifiers") 44 | 45 | family_name = models.CharField(_("family name"), max_length=128, blank=True, help_text=_("One or more family names")) 46 | given_name = models.CharField(_("given name"), max_length=128, blank=True, help_text=_("One or more primary given names")) 47 | additional_name = models.CharField(_("additional name"), max_length=128, blank=True, help_text=_("One or more secondary given names")) 48 | honorific_prefix = models.CharField(_("honorific prefix"), max_length=128, blank=True, help_text=_("One or more honorifics preceding a person's name")) 49 | honorific_suffix = models.CharField(_("honorific suffix"), max_length=128, blank=True, help_text=_("One or more honorifics following a person's name")) 50 | patronymic_name = models.CharField(_("patronymic name"), max_length=128, blank=True, help_text=_("One or more patronymic names")) 51 | sort_name = models.CharField(_("sort name"), max_length=128, blank=True, help_text=_("A name to use in an lexicographically ordered list")) 52 | email = models.EmailField(_("email"), blank=True, null=True, help_text=_("A preferred email address")) 53 | gender = models.CharField(_('gender'), max_length=128, blank=True, help_text=_("A gender")) 54 | birth_date = models.CharField(_("birth date"), max_length=10, blank=True, help_text=_("A date of birth")) 55 | death_date = models.CharField(_("death date"), max_length=10, blank=True, help_text=_("A date of death")) 56 | image = models.URLField(_("image"), blank=True, null=True, help_text=_("A URL of a head shot")) 57 | summary = models.CharField(_("summary"), max_length=1024, blank=True, help_text=_("A one-line account of a person's life")) 58 | biography = models.TextField(_("biography"), blank=True, help_text=_("An extended account of a person's life")) 59 | national_identity = models.CharField(_("national identity"), max_length=128, blank=True, null=True, help_text=_("A national identity")) 60 | 61 | # array of items referencing "http://popoloproject.com/schemas/contact_detail.json#" 62 | contact_details = GenericRelation('ContactDetail', help_text="Means of contacting the person") 63 | 64 | # array of items referencing "http://popoloproject.com/schemas/link.json#" 65 | links = GenericRelation('Link', help_text="URLs to documents related to the person") 66 | 67 | # array of items referencing "http://popoloproject.com/schemas/link.json#" 68 | sources = GenericRelation('Source', help_text="URLs to source documents about the person") 69 | 70 | class Meta: 71 | verbose_name_plural="People" 72 | 73 | try: 74 | # PassTrhroughManager was removed in django-model-utils 2.4, see issue #22 75 | objects = PassThroughManager.for_queryset_class(PersonQuerySet)() 76 | except: 77 | objects = PersonQuerySet.as_manager() 78 | 79 | def add_membership(self, organization): 80 | m = Membership(person=self, organization=organization) 81 | m.save() 82 | 83 | def add_memberships(self, organizations): 84 | for o in organizations: 85 | self.add_membership(o) 86 | 87 | def add_role(self, post): 88 | m = Membership(person=self, post=post, organization=post.organization) 89 | m.save() 90 | 91 | def add_contact_detail(self, **kwargs): 92 | c = ContactDetail(content_object=self, **kwargs) 93 | c.save() 94 | 95 | def add_contact_details(self, contacts): 96 | for c in contacts: 97 | self.add_contact_detail(**c) 98 | 99 | def __str__(self): 100 | return self.name 101 | 102 | @python_2_unicode_compatible 103 | class Organization(Dateframeable, Timestampable, models.Model): 104 | """ 105 | A group with a common purpose or reason for existence that goes beyond the set of people belonging to it 106 | see schema at http://popoloproject.com/schemas/organization.json# 107 | """ 108 | 109 | name = models.CharField(_("name"), max_length=512, help_text=_("A primary name, e.g. a legally recognized name")) 110 | summary = models.CharField(_("summary"), max_length=1024, blank=True, help_text=_("A one-line description of an organization")) 111 | description = models.TextField(_("biography"), blank=True, help_text=_("An extended description of an organization")) 112 | 113 | # array of items referencing "http://popoloproject.com/schemas/other_name.json#" 114 | other_names = GenericRelation('OtherName', help_text="Alternate or former names") 115 | 116 | # array of items referencing "http://popoloproject.com/schemas/identifier.json#" 117 | identifiers = GenericRelation('Identifier', help_text="Issued identifiers") 118 | classification = models.CharField(_("classification"), max_length=512, blank=True, help_text=_("An organization category, e.g. committee")) 119 | 120 | # reference to "http://popoloproject.com/schemas/organization.json#" 121 | parent = models.ForeignKey('Organization', blank=True, null=True, related_name='children', 122 | help_text=_("The organization that contains this organization")) 123 | 124 | # reference to "http://popoloproject.com/schemas/area.json#" 125 | area = models.ForeignKey('Area', blank=True, null=True, related_name='organizations', 126 | help_text=_("The geographic area to which this organization is related")) 127 | 128 | founding_date = models.CharField(_("founding date"), max_length=10, null=True, blank=True, validators=[ 129 | RegexValidator( 130 | regex='^[0-9]{4}(-[0-9]{2}){0,2}$', 131 | message='founding date must follow the given pattern: ^[0-9]{4}(-[0-9]{2}){0,2}$', 132 | code='invalid_founding_date' 133 | ) 134 | ], help_text=_("A date of founding")) 135 | dissolution_date = models.CharField(_("dissolution date"), max_length=10, null=True, blank=True, validators=[ 136 | RegexValidator( 137 | regex='^[0-9]{4}(-[0-9]{2}){0,2}$', 138 | message='dissolution date must follow the given pattern: ^[0-9]{4}(-[0-9]{2}){0,2}$', 139 | code='invalid_dissolution_date' 140 | ) 141 | ], help_text=_("A date of dissolution")) 142 | image = models.URLField(_("image"), blank=True, null=True, help_text=_("A URL of an image, to identify the organization visually")) 143 | 144 | # array of items referencing "http://popoloproject.com/schemas/contact_detail.json#" 145 | contact_details = GenericRelation('ContactDetail', help_text="Means of contacting the organization") 146 | 147 | # array of items referencing "http://popoloproject.com/schemas/link.json#" 148 | links = GenericRelation('Link', help_text="URLs to documents about the organization") 149 | 150 | # array of items referencing "http://popoloproject.com/schemas/link.json#" 151 | sources = GenericRelation('Source', help_text="URLs to source documents about the organization") 152 | 153 | try: 154 | # PassTrhroughManager was removed in django-model-utils 2.4, see issue #22 155 | objects = PassThroughManager.for_queryset_class(OrganizationQuerySet)() 156 | except: 157 | objects = OrganizationQuerySet.as_manager() 158 | 159 | def add_member(self, person): 160 | m = Membership(organization=self, person=person) 161 | m.save() 162 | 163 | def add_members(self, persons): 164 | for p in persons: 165 | self.add_member(p) 166 | 167 | def add_post(self, **kwargs): 168 | p = Post(organization=self, **kwargs) 169 | p.save() 170 | 171 | def add_posts(self, posts): 172 | for p in posts: 173 | self.add_post(**p) 174 | 175 | def __str__(self): 176 | return self.name 177 | 178 | @python_2_unicode_compatible 179 | class Post(Dateframeable, Timestampable, models.Model): 180 | """ 181 | A position that exists independent of the person holding it 182 | see schema at http://popoloproject.com/schemas/json# 183 | """ 184 | 185 | label = models.CharField(_("label"), max_length=512, blank=True, help_text=_("A label describing the post")) 186 | other_label = models.CharField(_("other label"), max_length=512, blank=True, null=True, help_text=_("An alternate label, such as an abbreviation")) 187 | 188 | role = models.CharField(_("role"), max_length=512, blank=True, help_text=_("The function that the holder of the post fulfills")) 189 | 190 | # reference to "http://popoloproject.com/schemas/organization.json#" 191 | organization = models.ForeignKey('Organization', related_name='posts', 192 | help_text=_("The organization in which the post is held")) 193 | 194 | # reference to "http://popoloproject.com/schemas/area.json#" 195 | area = models.ForeignKey('Area', blank=True, null=True, related_name='posts', 196 | help_text=_("The geographic area to which the post is related")) 197 | 198 | # array of items referencing "http://popoloproject.com/schemas/contact_detail.json#" 199 | contact_details = GenericRelation('ContactDetail', help_text="Means of contacting the holder of the post") 200 | 201 | # array of items referencing "http://popoloproject.com/schemas/link.json#" 202 | links = GenericRelation('Link', help_text="URLs to documents about the post") 203 | 204 | # array of items referencing "http://popoloproject.com/schemas/link.json#" 205 | sources = GenericRelation('Source', help_text="URLs to source documents about the post") 206 | 207 | try: 208 | # PassTrhroughManager was removed in django-model-utils 2.4, see issue #22 209 | objects = PassThroughManager.for_queryset_class(PostQuerySet)() 210 | except: 211 | objects = PostQuerySet.as_manager() 212 | 213 | def add_person(self, person): 214 | m = Membership(post=self, person=person, organization=self.organization) 215 | m.save() 216 | 217 | def __str__(self): 218 | return self.label 219 | 220 | @python_2_unicode_compatible 221 | class Membership(Dateframeable, Timestampable, models.Model): 222 | """ 223 | A relationship between a person and an organization 224 | see schema at http://popoloproject.com/schemas/membership.json# 225 | """ 226 | 227 | label = models.CharField(_("label"), max_length=512, blank=True, help_text=_("A label describing the membership")) 228 | role = models.CharField(_("role"), max_length=512, blank=True, help_text=_("The role that the person fulfills in the organization")) 229 | 230 | # reference to "http://popoloproject.com/schemas/person.json#" 231 | person = models.ForeignKey('Person', to_field="id", related_name='memberships', 232 | help_text=_("The person who is a party to the relationship")) 233 | 234 | # reference to "http://popoloproject.com/schemas/organization.json#" 235 | organization = models.ForeignKey('Organization', blank=True, null=True, 236 | related_name='memberships', 237 | help_text=_("The organization that is a party to the relationship")) 238 | on_behalf_of = models.ForeignKey('Organization', blank=True, null=True, 239 | related_name='memberships_on_behalf_of', 240 | help_text=_("The organization on whose behalf the person is a party to the relationship")) 241 | 242 | # reference to "http://popoloproject.com/schemas/post.json#" 243 | post = models.ForeignKey('Post', blank=True, null=True, related_name='memberships', 244 | help_text=_("The post held by the person in the organization through this membership")) 245 | 246 | # reference to "http://popoloproject.com/schemas/area.json#" 247 | area = models.ForeignKey('Area', blank=True, null=True, related_name='memberships', 248 | help_text=_("The geographic area to which the post is related")) 249 | 250 | # array of items referencing "http://popoloproject.com/schemas/contact_detail.json#" 251 | contact_details = GenericRelation('ContactDetail', help_text="Means of contacting the member of the organization") 252 | 253 | # array of items referencing "http://popoloproject.com/schemas/link.json#" 254 | links = GenericRelation('Link', help_text="URLs to documents about the membership") 255 | 256 | # array of items referencing "http://popoloproject.com/schemas/link.json#" 257 | sources = GenericRelation('Source', help_text="URLs to source documents about the membership") 258 | 259 | try: 260 | # PassTrhroughManager was removed in django-model-utils 2.4, see issue #22 261 | objects = PassThroughManager.for_queryset_class(MembershipQuerySet)() 262 | except: 263 | objects = MembershipQuerySet.as_manager() 264 | 265 | def __str__(self): 266 | return self.label 267 | 268 | @python_2_unicode_compatible 269 | class ContactDetail(Timestampable, Dateframeable, GenericRelatable, models.Model): 270 | """ 271 | A means of contacting an entity 272 | see schema at http://popoloproject.com/schemas/contact-detail.json# 273 | """ 274 | 275 | CONTACT_TYPES = Choices( 276 | ('ADDRESS', 'address', _('Address')), 277 | ('EMAIL', 'email', _('Email')), 278 | ('URL', 'url', _('Url')), 279 | ('MAIL', 'mail', _('Snail mail')), 280 | ('TWITTER', 'twitter', _('Twitter')), 281 | ('FACEBOOK', 'facebook', _('Facebook')), 282 | ('PHONE', 'phone', _('Telephone')), 283 | ('MOBILE', 'mobile', _('Mobile')), 284 | ('TEXT', 'text', _('Text')), 285 | ('VOICE', 'voice', _('Voice')), 286 | ('FAX', 'fax', _('Fax')), 287 | ('CELL', 'cell', _('Cell')), 288 | ('VIDEO', 'video', _('Video')), 289 | ('PAGER', 'pager', _('Pager')), 290 | ('TEXTPHONE', 'textphone', _('Textphone')), 291 | ) 292 | 293 | label = models.CharField(_("label"), max_length=512, blank=True, help_text=_("A human-readable label for the contact detail")) 294 | contact_type = models.CharField(_("type"), max_length=12, choices=CONTACT_TYPES, help_text=_("A type of medium, e.g. 'fax' or 'email'")) 295 | value = models.CharField(_("value"), max_length=512, help_text=_("A value, e.g. a phone number or email address")) 296 | note = models.CharField(_("note"), max_length=512, blank=True, help_text=_("A note, e.g. for grouping contact details by physical location")) 297 | 298 | # array of items referencing "http://popoloproject.com/schemas/link.json#" 299 | sources = GenericRelation('Source', help_text="URLs to source documents about the contact detail") 300 | 301 | try: 302 | # PassTrhroughManager was removed in django-model-utils 2.4, see issue #22 303 | objects = PassThroughManager.for_queryset_class(ContactDetailQuerySet)() 304 | except: 305 | objects = ContactDetailQuerySet.as_manager() 306 | 307 | def __str__(self): 308 | return u"{0} - {1}".format(self.value, self.contact_type) 309 | 310 | 311 | @python_2_unicode_compatible 312 | class OtherName(Dateframeable, GenericRelatable, models.Model): 313 | """ 314 | An alternate or former name 315 | see schema at http://popoloproject.com/schemas/name-component.json# 316 | """ 317 | name = models.CharField(_("name"), max_length=512, help_text=_("An alternate or former name")) 318 | note = models.CharField(_("note"), max_length=1024, blank=True, help_text=_("A note, e.g. 'Birth name'")) 319 | 320 | try: 321 | # PassTrhroughManager was removed in django-model-utils 2.4, see issue #22 322 | objects = PassThroughManager.for_queryset_class(OtherNameQuerySet)() 323 | except: 324 | objects = OtherNameQuerySet.as_manager() 325 | 326 | def __str__(self): 327 | return self.name 328 | 329 | 330 | @python_2_unicode_compatible 331 | class Identifier(GenericRelatable, models.Model): 332 | """ 333 | An issued identifier 334 | see schema at http://popoloproject.com/schemas/identifier.json# 335 | """ 336 | identifier = models.CharField(_("identifier"), max_length=512, help_text=_("An issued identifier, e.g. a DUNS number")) 337 | scheme = models.CharField(_("scheme"), max_length=128, blank=True, help_text=_("An identifier scheme, e.g. DUNS")) 338 | 339 | def __str__(self): 340 | return "{0}: {1}".format(self.scheme, self.identifier) 341 | 342 | 343 | @python_2_unicode_compatible 344 | class Link(GenericRelatable, models.Model): 345 | """ 346 | A URL 347 | see schema at http://popoloproject.com/schemas/link.json# 348 | """ 349 | url = models.URLField(_("url"), max_length=350, help_text=_("A URL")) 350 | note = models.CharField(_("note"), max_length=512, blank=True, help_text=_("A note, e.g. 'Wikipedia page'")) 351 | 352 | def __str__(self): 353 | return self.url 354 | 355 | 356 | @python_2_unicode_compatible 357 | class Source(GenericRelatable, models.Model): 358 | """ 359 | A URL for referring to sources of information 360 | see schema at http://popoloproject.com/schemas/link.json# 361 | """ 362 | url = models.URLField(_("url"), help_text=_("A URL")) 363 | note = models.CharField(_("note"), max_length=512, blank=True, help_text=_("A note, e.g. 'Parliament website'")) 364 | 365 | def __str__(self): 366 | return self.url 367 | 368 | 369 | 370 | @python_2_unicode_compatible 371 | class Language(models.Model): 372 | """ 373 | Maps languages, with names and 2-char iso 639-1 codes. 374 | Taken from http://dbpedia.org, using a sparql query 375 | """ 376 | dbpedia_resource = models.CharField(max_length=255, 377 | help_text=_("DbPedia URI of the resource"), unique=True) 378 | iso639_1_code = models.CharField(max_length=2) 379 | name = models.CharField(max_length=128, 380 | help_text=_("English name of the language")) 381 | 382 | def __str__(self): 383 | return u"{0} ({1})".format(self.name, self.iso639_1_code) 384 | 385 | @python_2_unicode_compatible 386 | class Area(GenericRelatable, Dateframeable, Timestampable, models.Model): 387 | """ 388 | An area is a geographic area whose geometry may change over time. 389 | see schema at http://popoloproject.com/schemas/area.json# 390 | """ 391 | 392 | name = models.CharField(_("name"), max_length=256, blank=True, help_text=_("A primary name")) 393 | identifier = models.CharField(_("identifier"), max_length=512, blank=True, help_text=_("An issued identifier")) 394 | classification = models.CharField(_("identifier"), max_length=512, blank=True, help_text=_("An area category, e.g. city")) 395 | 396 | # array of items referencing "http://popoloproject.com/schemas/identifier.json#" 397 | other_identifiers = GenericRelation('Identifier', blank=True, null=True, help_text="Other issued identifiers (zip code, other useful codes, ...)") 398 | 399 | # reference to "http://popoloproject.com/schemas/area.json#" 400 | parent = models.ForeignKey('Area', blank=True, null=True, related_name='children', 401 | help_text=_("The area that contains this area")) 402 | 403 | # geom property, as text (GeoJson, KML, GML) 404 | geom = models.TextField(_("geom"), null=True, blank=True, help_text=_("A geometry")) 405 | 406 | # inhabitants, can be useful for some queries 407 | inhabitants = models.IntegerField(_("inhabitants"), null=True, blank=True, help_text=_("The total number of inhabitants")) 408 | 409 | # array of items referencing "http://popoloproject.com/schemas/link.json#" 410 | sources = GenericRelation('Source', blank=True, null=True, help_text="URLs to source documents about the contact detail") 411 | 412 | def __str__(self): 413 | return self.name 414 | 415 | @python_2_unicode_compatible 416 | class AreaI18Name(models.Model): 417 | """ 418 | Internationalized name for an Area. 419 | Contains references to language and area. 420 | """ 421 | area = models.ForeignKey('Area', related_name='i18n_names') 422 | language = models.ForeignKey('Language') 423 | name = models.CharField(_("name"), max_length=255) 424 | 425 | def __str__(self): 426 | return "{0} - {1}".format(self.language, self.name) 427 | 428 | class Meta: 429 | verbose_name = 'I18N Name' 430 | verbose_name_plural = 'I18N Names' 431 | unique_together = ('area', 'language', 'name') 432 | 433 | 434 | 435 | ## 436 | ## signals 437 | ## 438 | 439 | ## copy founding and dissolution dates into start and end dates, 440 | ## so that Organization can extend the abstract Dateframeable behavior 441 | ## (it's way easier than dynamic field names) 442 | @receiver(pre_save, sender=Organization) 443 | def copy_organization_date_fields(sender, **kwargs): 444 | obj = kwargs['instance'] 445 | 446 | if obj.founding_date: 447 | obj.start_date = obj.founding_date 448 | if obj.dissolution_date: 449 | obj.end_date = obj.dissolution_date 450 | 451 | ## copy birth and death dates into start and end dates, 452 | ## so that Person can extend the abstract Dateframeable behavior 453 | ## (it's way easier than dynamic field names) 454 | @receiver(pre_save, sender=Person) 455 | def copy_person_date_fields(sender, **kwargs): 456 | obj = kwargs['instance'] 457 | 458 | if obj.birth_date: 459 | obj.start_date = obj.birth_date 460 | if obj.death_date: 461 | obj.end_date = obj.death_date 462 | 463 | 464 | ## all instances are validated before being saved 465 | @receiver(pre_save, sender=Person) 466 | @receiver(pre_save, sender=Organization) 467 | @receiver(pre_save, sender=Post) 468 | def validate_date_fields(sender, **kwargs): 469 | obj = kwargs['instance'] 470 | obj.full_clean() 471 | --------------------------------------------------------------------------------