├── popolo ├── tests │ ├── __init__.py │ ├── test_utils.py │ └── factories.py ├── migrations │ ├── __init__.py │ ├── 0002_auto_20210407_1722.py │ ├── 0003_auto_20210407_1910.py │ └── 0001_initial.py ├── templates │ ├── area_detail.html │ ├── post_detail.html │ ├── membership_detail.html │ ├── organization_detail.html │ ├── electoral_event_detail.html │ └── person_detail.html ├── behaviors │ ├── __init__.py │ ├── admin.py │ ├── models.py │ └── tests.py ├── locale │ └── it │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── __init__.py ├── validators.py ├── exceptions.py ├── apps.py ├── urls.py ├── views.py ├── managers.py ├── signals.py ├── admin.py ├── querysets.py ├── utils.py └── mixins.py ├── AUTHORS ├── MANIFEST.in ├── setup.cfg ├── pyproject.toml ├── tox.ini ├── LICENSE ├── .gitlab-ci.yml ├── Makefile ├── setup.py ├── README.md ├── .gitignore ├── schema_parser.py └── CHANGELOG.md /popolo/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /popolo/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /popolo/templates/area_detail.html: -------------------------------------------------------------------------------- 1 | Area -------------------------------------------------------------------------------- /popolo/templates/post_detail.html: -------------------------------------------------------------------------------- 1 | Post -------------------------------------------------------------------------------- /popolo/templates/membership_detail.html: -------------------------------------------------------------------------------- 1 | Membership -------------------------------------------------------------------------------- /popolo/behaviors/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = "guglielmo" 2 | -------------------------------------------------------------------------------- /popolo/templates/organization_detail.html: -------------------------------------------------------------------------------- 1 | Organizzazione -------------------------------------------------------------------------------- /popolo/templates/electoral_event_detail.html: -------------------------------------------------------------------------------- 1 | Electoral Event -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Guglielmo Celata at Openpolis 2 | Juha Yrjölä at Code4Europe 3 | -------------------------------------------------------------------------------- /popolo/templates/person_detail.html: -------------------------------------------------------------------------------- 1 | Persona: {{ person.slug }} 2 | 3 | Nome: {{ person.name }} -------------------------------------------------------------------------------- /popolo/locale/it/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openpolis/django-popolo/HEAD/popolo/locale/it/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include LICENSE 3 | include README.rst 4 | recursive-include popolo/static * 5 | recursive-include popolo/templates * 6 | -------------------------------------------------------------------------------- /popolo/__init__.py: -------------------------------------------------------------------------------- 1 | """A Django-based implementation of the OpenGovernment context, compliant with the Popolo data specifications.""" 2 | 3 | __version__ = "3.0.5" 4 | 5 | default_app_config = "popolo.apps.PopoloConfig" 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 3.0.5 3 | commit = True 4 | tag = True 5 | tag_name = v{new_version} 6 | 7 | [bumpversion:file:popolo/__init__.py] 8 | 9 | [flake8] 10 | max-line-length = 120 11 | exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv 12 | 13 | -------------------------------------------------------------------------------- /popolo/validators.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ValidationError 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | 5 | def validate_percentage(value): 6 | if value < 0.0 or value > 100.0: 7 | raise ValidationError(_("%(value)s is not a percentage"), params={"value": value}) 8 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 118 3 | target-version = ['py36', 'py37', 'py38'] 4 | include = '\.pyi?$' 5 | exclude = ''' 6 | /( 7 | \.eggs 8 | | \.git 9 | | \.hg 10 | | \.mypy_cache 11 | | \.tox 12 | | \.venv 13 | | _build 14 | | buck-out 15 | | build 16 | | dist 17 | | migrations 18 | )/ 19 | ''' -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /popolo/exceptions.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | 4 | class PartialDateException(Exception): 5 | """Exception used in the context of the PartialDate class""" 6 | 7 | pass 8 | 9 | 10 | class OverlappingDateIntervalException(Exception): 11 | """Raised when date intervals overlap 12 | 13 | Attributes: 14 | overlapping -- the first entity whose date interval overlaps 15 | message -- the extended description of the error 16 | 17 | """ 18 | 19 | def __init__(self, overlapping: Any, message: str): 20 | self.overlapping = overlapping 21 | self.message = message 22 | 23 | def __str__(self) -> str: 24 | return repr(self.message) 25 | -------------------------------------------------------------------------------- /popolo/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PopoloConfig(AppConfig): 5 | """ 6 | Default "popolo" app configuration. 7 | """ 8 | 9 | name = "popolo" 10 | verbose_name = "Popolo" 11 | 12 | def ready(self): 13 | from popolo import admin 14 | from popolo import signals 15 | 16 | admin.register() # Register models in admin site 17 | signals.connect() # Connect the signals 18 | 19 | 20 | class PopoloNoAdminConfig(PopoloConfig): 21 | """ 22 | Alternate "popolo" app configuration which doesn't load the admin site. 23 | 24 | Useful in tests. 25 | """ 26 | 27 | def ready(self): 28 | from popolo import signals 29 | 30 | signals.connect() # Just connect the signals 31 | -------------------------------------------------------------------------------- /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/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from popolo.views import ( 3 | OrganizationDetailView, 4 | PersonDetailView, 5 | MembershipDetailView, 6 | PostDetailView, 7 | KeyEventDetailView, 8 | AreaDetailView, 9 | ) 10 | 11 | __author__ = "guglielmo" 12 | 13 | urlpatterns = [ 14 | url(r"^person/(?P[-\w]+)/$", PersonDetailView.as_view(), name="person-detail"), 15 | url(r"^organization/(?P[-\w]+)/$", OrganizationDetailView.as_view(), name="organization-detail"), 16 | url(r"^membership/(?P[-\w]+)/$", MembershipDetailView.as_view(), name="membership-detail"), 17 | url(r"^post/(?P[-\w]+)/$", PostDetailView.as_view(), name="post-detail"), 18 | url(r"^key-event/(?P[-\w]+)/$", KeyEventDetailView.as_view(), name="electoral-event-detail"), 19 | url(r"^area/(?P[-\w]+)/$", AreaDetailView.as_view(), name="area-detail"), 20 | ] 21 | -------------------------------------------------------------------------------- /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 6 | 7 | 8 | class LinkRelAdmin(GenericTabularInline): 9 | model = models.LinkRel 10 | extra = 0 11 | 12 | 13 | class SourceRelAdmin(GenericTabularInline): 14 | model = models.SourceRel 15 | extra = 0 16 | 17 | 18 | class IdentifierAdmin(GenericTabularInline): 19 | model = models.Identifier 20 | extra = 0 21 | 22 | 23 | class ContactDetailAdmin(GenericStackedInline): 24 | model = models.ContactDetail 25 | extra = 0 26 | 27 | 28 | class OtherNameAdmin(GenericTabularInline): 29 | model = models.OtherName 30 | extra = 0 31 | 32 | 33 | BASE_INLINES = [ 34 | LinkRelAdmin, 35 | SourceRelAdmin, 36 | IdentifierAdmin, 37 | ContactDetailAdmin, 38 | OtherNameAdmin, 39 | ] 40 | -------------------------------------------------------------------------------- /popolo/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import DetailView 2 | from popolo.models import Organization, Person, Membership, Post, KeyEvent, Area 3 | 4 | 5 | class PersonDetailView(DetailView): 6 | model = Person 7 | context_object_name = "person" 8 | template_name = "person_detail.html" 9 | 10 | 11 | class OrganizationDetailView(DetailView): 12 | model = Organization 13 | context_object_name = "organization" 14 | template_name = "organization_detail.html" 15 | 16 | 17 | class MembershipDetailView(DetailView): 18 | model = Membership 19 | context_object_name = "membership" 20 | template_name = "membership_detail.html" 21 | 22 | 23 | class PostDetailView(DetailView): 24 | model = Post 25 | context_object_name = "post" 26 | template_name = "post_detail.html" 27 | 28 | 29 | class KeyEventDetailView(DetailView): 30 | model = KeyEvent 31 | context_object_name = "event" 32 | template_name = "electoral_event_detail.html" 33 | 34 | 35 | class AreaDetailView(DetailView): 36 | model = Area 37 | context_object_name = "area" 38 | template_name = "area_detail.html" 39 | -------------------------------------------------------------------------------- /popolo/migrations/0002_auto_20210407_1722.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-04-07 15:22 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('popolo', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='area', 15 | name='is_provincial_capital', 16 | field=models.BooleanField(blank=True, help_text='If the city is a provincial capital.Takes the Null value if not a municipality.', null=True, verbose_name='Is provincial capital'), 17 | ), 18 | migrations.AlterField( 19 | model_name='contactdetail', 20 | name='contact_type', 21 | field=models.CharField(choices=[('ADDRESS', 'Address'), ('EMAIL', 'Email'), ('URL', 'Url'), ('MAIL', 'Snail mail'), ('TWITTER', 'Twitter'), ('FACEBOOK', 'Facebook'), ('PHONE', 'Telephone'), ('MOBILE', 'Mobile'), ('TEXT', 'Text'), ('VOICE', 'Voice'), ('FAX', 'Fax'), ('CELL', 'Cell'), ('VIDEO', 'Video'), ('INSTAGRAM', 'Instagram'), ('YOUTUBE', 'Youtube'), ('PAGER', 'Pager'), ('TEXTPHONE', 'Textphone'), ('LINKEDIN', 'LinkedIn')], help_text="A type of medium, e.g. 'fax' or 'email'", max_length=12, verbose_name='type'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - test 3 | - release 4 | 5 | variables: 6 | DOCKER_IMAGE_PYTHON: python:3.8-buster 7 | PYPI_TEST_TOKEN: ${PYPI_TEST_TOKEN} 8 | PYPI_TOKEN: ${PYPI_TOKEN} 9 | 10 | flake8: 11 | image: $DOCKER_IMAGE_PYTHON 12 | stage: test 13 | script: 14 | - pip install -U flake8 15 | - flake8 16 | allow_failure: true 17 | 18 | 19 | pypi_test_release: 20 | image: $DOCKER_IMAGE_PYTHON 21 | stage: release 22 | cache: {} 23 | script: 24 | - pip install -U twine wheel 25 | - python setup.py sdist bdist_wheel 26 | - twine check dist/* 27 | - twine upload --verbose --repository-url https://test.pypi.org/legacy/ --username __token__ --password ${PYPI_TEST_TOKEN} dist/* 28 | - pip install --no-deps --index-url https://test.pypi.org/simple/ django-popolo 29 | - pip uninstall django-popolo -y 30 | only: 31 | - tags 32 | 33 | pypi_official_release: 34 | image: $DOCKER_IMAGE_PYTHON 35 | stage: release 36 | cache: {} 37 | script: 38 | - pip install -U twine wheel 39 | - python setup.py sdist bdist_wheel 40 | - twine check dist/* 41 | - twine upload --verbose --username __token__ --password ${PYPI_TOKEN} dist/* 42 | - pip install django-popolo 43 | - pip uninstall django-popolo -y 44 | only: 45 | - tags 46 | when: manual 47 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PHONY: clean clean-test clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | 4 | django-admin-args = --pythonpath=. --settings=test_settings 5 | 6 | help: ## Display this help 7 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} \ 8 | /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } \ 9 | /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST) 10 | 11 | ##@ Manage 12 | 13 | migrations: ## create a new migration 14 | django-admin makemigrations $(django-admin-args) popolo 15 | 16 | ##@ Dependencies 17 | 18 | install-requirements: ## Install development dependecies 19 | pip install ".[test,dev]" 20 | 21 | ##@ Cleanup 22 | 23 | clean: clean-pyc clean-test ## Remove all build, test, coverage and Python artifacts 24 | 25 | clean-pyc: ## Remove Python file artifacts 26 | find . -name '*.pyc' -exec rm -f {} + 27 | find . -name '*.pyo' -exec rm -f {} + 28 | find . -name '*~' -exec rm -f {} + 29 | find . -name '__pycache__' -exec rm -fr {} + 30 | 31 | clean-test: ## Remove test and coverage artifacts 32 | rm -fr .tox/ 33 | rm -f .coverage 34 | rm -fr htmlcov/ 35 | rm -fr .pytest_cache 36 | 37 | ##@ Linting and formatting 38 | 39 | black: ## format all Python code using black 40 | black popolo 41 | 42 | lint: ## check code style with flake8 43 | flake8 popolo 44 | 45 | ##@ Testing 46 | 47 | test: ## run tests quickly with the default Python 48 | django-admin test $(django-admin-args) popolo.tests 49 | 50 | coverage: ## check code coverage quickly with the default Python 51 | coverage run `which django-admin` test $(django-admin-args) popolo.tests 52 | coverage report -m 53 | 54 | coverage-html: | coverage 55 | coverage html 56 | $(BROWSER) htmlcov/index.html 57 | -------------------------------------------------------------------------------- /popolo/migrations/0003_auto_20210407_1910.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-04-07 17:10 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('popolo', '0002_auto_20210407_1722'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='membership', 16 | name='appointed_by', 17 | field=models.ForeignKey(blank=True, help_text='The Membership that officially has appointed this one.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appointees', to='popolo.membership', verbose_name='Appointed by'), 18 | ), 19 | migrations.AlterField( 20 | model_name='membership', 21 | name='area', 22 | field=models.ForeignKey(blank=True, help_text='The geographic area to which the membership is related', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='memberships', to='popolo.area', verbose_name='Area'), 23 | ), 24 | migrations.AlterField( 25 | model_name='membership', 26 | name='electoral_event', 27 | field=models.ForeignKey(blank=True, help_text='The electoral event that assigned this membership', limit_choices_to={'event_type__contains': 'ELE'}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='memberships_assigned', to='popolo.keyevent', verbose_name='Electoral event'), 28 | ), 29 | migrations.AlterField( 30 | model_name='membership', 31 | name='on_behalf_of', 32 | field=models.ForeignKey(blank=True, help_text='The organization on whose behalf the person is a member of the organization', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='memberships_on_behalf_of', to='popolo.organization', verbose_name='On behalf of'), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup 3 | 4 | import popolo 5 | 6 | setup( 7 | name="django-popolo", 8 | version=popolo.__version__, 9 | author="Guglielmo Celata", 10 | author_email="guglielmo@openpolis.it", 11 | packages=["popolo"], 12 | include_package_data=True, 13 | url="http://github.com/openpolis/django-popolo", 14 | license="AGPL-3.0", 15 | description=popolo.__doc__, 16 | classifiers=[ 17 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 18 | "Intended Audience :: Developers", 19 | "Programming Language :: Python", 20 | "Programming Language :: Python :: 3 :: Only", 21 | "Programming Language :: Python :: 3.5", 22 | "Programming Language :: Python :: 3.6", 23 | "Programming Language :: Python :: 3.7", 24 | "Programming Language :: Python :: 3.8", 25 | "Programming Language :: Python :: 3.9", 26 | "Framework :: Django", 27 | "Framework :: Django :: 1.11", 28 | "Framework :: Django :: 2.2", 29 | "Framework :: Django :: 3.2", 30 | "License :: OSI Approved :: GNU Affero General Public License v3", 31 | "Development Status :: 4 - Beta", 32 | "Operating System :: OS Independent", 33 | ], 34 | long_description=open("./README.md").read(), 35 | long_description_content_type="text/markdown", 36 | zip_safe=False, 37 | install_requires=( 38 | # Package requirements (pin major versions) 39 | "django<3.3", # Django 3.2 LTS - https://github.com/django/django 40 | "django-autoslug<2", # https://github.com/justinmayer/django-autoslug 41 | "django-model-utils<5", # https://github.com/jazzband/django-model-utils 42 | ), 43 | extras_require={ 44 | "test": ( 45 | # Test requirements 46 | "faker", # https://github.com/joke2k/faker 47 | "factory_boy", # https://github.com/FactoryBoy/factory_boy 48 | ), 49 | "dev": ( 50 | # Development requirements 51 | "black", # https://github.com/psf/black 52 | "coverage", # https://github.com/nedbat/coveragepy 53 | ), 54 | }, 55 | ) 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-popolo 2 | 3 | Welcome to the documentation for django-popolo! 4 | 5 | **django-popolo** is a django-based implementation of the 6 | [Popolo's open government data specifications](http://popoloproject.com/). 7 | 8 | It is developed as a Django reusable app to be deployed directly within Django projects. 9 | 10 | It will allow web developers using it to manage and store data according to Popolo's specifications. 11 | 12 | The standard sql-oriented Django ORM will be used. 13 | 14 | From release 1.2.0, django-popolo includes classes that extend the model, although compatibility 15 | with the standard is kept. 16 | 17 | Release 2.0.0 introduces a change in how Sources and Links are referred to, 18 | that breaks compatibility with the popit importer. 19 | 20 | Release 3.0.0 main focuses have been to keep up with latest Django 21 | versions and to "modernize" the code base, adopting latest Python features (like type hinting), 22 | and doing some serious housekeeping. Python 2 is no longer supported. 23 | This release also implements a lot of new models which are not part of the Popolo specification (mildly out of scope), 24 | but we needed them in some projects which make use this package. Those new models can be safely ignored, and they could 25 | also be removed in the future, as we are considering the option of entirely decoupling them from `django-popolo`. 26 | 27 | 28 | See the [CHANGELOG.md](./CHANGELOG.md) file for more details. 29 | 30 | ## Installation 31 | 32 | To install `django-popolo` as a third party app within a django project, 33 | you need to add it to the django project's requirements.txt. 34 | You can do this from GitHub in the usual way, or using the 35 | `django-popolo` package on PyPI. 36 | 37 | ## Notes on mysociety's fork 38 | 39 | [mysociety/django-popolo](https://github.com/mysociety/django-popolo) is a fork of this project where integer IDs are used 40 | instead of slugs. 41 | 42 | Our packages, since version 1.1 also uses numerical ids as primary keys for all entities. 43 | Slugs are available as non-primary fields, for the 4 main classes (`Person`, `Organization`, `Post`, `Membership`). 44 | Slugs are used through the `Permalinkable` behavior, that adds the `slug` field to the class, populating it according to rules defined in each class. 45 | 46 | 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Linux template 3 | *~ 4 | 5 | # temporary files which can be created if a process still has a handle open of a deleted file 6 | .fuse_hidden* 7 | 8 | # KDE directory preferences 9 | .directory 10 | 11 | # Linux trash folder which might appear on any partition or disk 12 | .Trash-* 13 | 14 | # .nfs files are created when an open file is removed but is still being accessed 15 | .nfs* 16 | 17 | ### JetBrains template 18 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 19 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 20 | 21 | .idea/* 22 | 23 | # CMake 24 | cmake-build-*/ 25 | 26 | # Mongo Explorer plugin 27 | .idea/**/mongoSettings.xml 28 | 29 | # File-based project format 30 | *.iws 31 | 32 | # IntelliJ 33 | out/ 34 | 35 | # mpeltonen/sbt-idea plugin 36 | .idea_modules/ 37 | 38 | # JIRA plugin 39 | atlassian-ide-plugin.xml 40 | 41 | # Cursive Clojure plugin 42 | .idea/replstate.xml 43 | 44 | # Crashlytics plugin (for Android Studio and IntelliJ) 45 | com_crashlytics_export_strings.xml 46 | crashlytics.properties 47 | crashlytics-build.properties 48 | fabric.properties 49 | 50 | # Editor-based Rest Client 51 | .idea/httpRequests 52 | 53 | # Android studio 3.1+ serialized cache file 54 | .idea/caches/build_file_checksums.ser 55 | 56 | ### Windows template 57 | # Windows thumbnail cache files 58 | Thumbs.db 59 | Thumbs.db:encryptable 60 | ehthumbs.db 61 | ehthumbs_vista.db 62 | 63 | # Dump file 64 | *.stackdump 65 | 66 | # Folder config file 67 | [Dd]esktop.ini 68 | 69 | # Recycle Bin used on file shares 70 | $RECYCLE.BIN/ 71 | 72 | # Windows Installer files 73 | *.cab 74 | *.msi 75 | *.msix 76 | *.msm 77 | *.msp 78 | 79 | # Windows shortcuts 80 | *.lnk 81 | 82 | ### Python template 83 | # Byte-compiled / optimized / DLL files 84 | __pycache__/ 85 | *.py[cod] 86 | *$py.class 87 | 88 | # C extensions 89 | *.so 90 | 91 | # Distribution / packaging 92 | .Python 93 | build/ 94 | develop-eggs/ 95 | dist/ 96 | downloads/ 97 | eggs/ 98 | .eggs/ 99 | lib/ 100 | lib64/ 101 | parts/ 102 | sdist/ 103 | var/ 104 | wheels/ 105 | pip-wheel-metadata/ 106 | share/python-wheels/ 107 | *.egg-info/ 108 | .installed.cfg 109 | *.egg 110 | MANIFEST 111 | 112 | # PyInstaller 113 | # Usually these files are written by a python script from a template 114 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 115 | *.manifest 116 | *.spec 117 | 118 | # Installer logs 119 | pip-log.txt 120 | pip-delete-this-directory.txt 121 | 122 | # Unit test / coverage reports 123 | htmlcov/ 124 | .tox/ 125 | .nox/ 126 | .coverage 127 | .coverage.* 128 | .cache 129 | nosetests.xml 130 | coverage.xml 131 | *.cover 132 | *.py,cover 133 | .hypothesis/ 134 | .pytest_cache/ 135 | 136 | # Translations 137 | *.mo 138 | *.pot 139 | 140 | # Django stuff: 141 | *.log 142 | local_settings.py 143 | db.sqlite3 144 | db.sqlite3-journal 145 | 146 | # Flask stuff: 147 | instance/ 148 | .webassets-cache 149 | 150 | # Scrapy stuff: 151 | .scrapy 152 | 153 | # Sphinx documentation 154 | docs/_build/ 155 | 156 | # PyBuilder 157 | target/ 158 | 159 | # Jupyter Notebook 160 | .ipynb_checkpoints 161 | 162 | # IPython 163 | profile_default/ 164 | ipython_config.py 165 | 166 | # pyenv 167 | .python-version 168 | 169 | # pipenv 170 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 171 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 172 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 173 | # install all needed dependencies. 174 | #Pipfile.lock 175 | 176 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 177 | __pypackages__/ 178 | 179 | # Celery stuff 180 | celerybeat-schedule 181 | celerybeat.pid 182 | 183 | # SageMath parsed files 184 | *.sage.py 185 | 186 | # Environments 187 | .env 188 | .venv 189 | env/ 190 | venv/ 191 | ENV/ 192 | env.bak/ 193 | venv.bak/ 194 | 195 | # Spyder project settings 196 | .spyderproject 197 | .spyproject 198 | 199 | # Rope project settings 200 | .ropeproject 201 | 202 | # mkdocs documentation 203 | /site 204 | 205 | # mypy 206 | .mypy_cache/ 207 | .dmypy.json 208 | dmypy.json 209 | 210 | # Pyre type checker 211 | .pyre/ 212 | 213 | ### macOS template 214 | # General 215 | .DS_Store 216 | .AppleDouble 217 | .LSOverride 218 | 219 | # Icon must end with two \r 220 | Icon 221 | 222 | # Thumbnails 223 | ._* 224 | 225 | # Files that might appear in the root of a volume 226 | .DocumentRevisions-V100 227 | .fseventsd 228 | .Spotlight-V100 229 | .TemporaryItems 230 | .Trashes 231 | .VolumeIcon.icns 232 | .com.apple.timemachine.donotpresent 233 | 234 | # Directories potentially created on remote AFP share 235 | .AppleDB 236 | .AppleDesktop 237 | Network Trash Folder 238 | Temporary Items 239 | .apdisk 240 | 241 | -------------------------------------------------------------------------------- /schema_parser.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | from collections import OrderedDict 4 | import requests 5 | 6 | 7 | __author__ = 'guglielmo' 8 | 9 | """ 10 | This script can be used to extract the details of a json schemas, 11 | given its URL. 12 | The script only requires the `requests` package 13 | 14 | 15 | To install `requests`:: 16 | 17 | pip install requests 18 | 19 | 20 | In order to get the content of the schema (title, descr, properties) in a readable form:: 21 | 22 | python schema_parser.py \ 23 | --url http://www.popoloproject.com/schemas/event.json# 24 | 25 | 26 | In order to generate the class definitions:: 27 | 28 | python schema_parser.py \ 29 | --url http://www.popoloproject.com/schemas/event.json# \ 30 | --generate 31 | 32 | """ 33 | 34 | 35 | def main(): 36 | parser = argparse.ArgumentParser( 37 | description='Parse a remote popolo schema.') 38 | parser.add_argument('--url', dest='url', type=str, nargs='?', 39 | help='Json URL') 40 | parser.add_argument('--generate', action='store_true', 41 | help='Generate class or fields definition') 42 | 43 | args = parser.parse_args() 44 | url = args.url 45 | generate = args.generate 46 | 47 | resp = requests.get(url) 48 | if resp.status_code != 200 or 'OpenDNS' in resp.headers['server']: 49 | print("URL not found") 50 | exit() 51 | 52 | try: 53 | schema = json.loads( 54 | resp.content, 55 | object_pairs_hook=OrderedDict 56 | ) 57 | except ValueError: 58 | schema = None 59 | print("No JSON at {0}".format(url)) 60 | exit() 61 | 62 | if generate: 63 | print(""" 64 | class {0}(models.Model): 65 | \"\"\"{1} 66 | 67 | \"\"\" 68 | """.format(schema['title'], schema['description'])) 69 | generate_fields(schema['properties']) 70 | else: 71 | print("Title: {0}".format(schema['title'])) 72 | print("Description: {0}".format(schema['description'])) 73 | print("Properties:") 74 | for p, v in schema['properties'].items(): 75 | if isinstance(v, OrderedDict): 76 | v = json.loads(json.dumps(v)) 77 | print( 78 | " {0} => {1}".format(p, v) 79 | ) 80 | 81 | 82 | def generate_fields(properties): 83 | """ 84 | Generate representations for all fields 85 | """ 86 | for p, v in properties.items(): 87 | generate_field(p, v) 88 | 89 | 90 | def generate_field(key, value): 91 | """ 92 | Generate representation for a single field 93 | """ 94 | 95 | # determine object type 96 | obj_type = None 97 | default = None 98 | if 'type' in value: 99 | if isinstance(value['type'], list): 100 | obj_type = value['type'][0] 101 | default = value['type'][1] 102 | else: 103 | obj_type = value['type'] 104 | default = None 105 | else: 106 | if '$ref' in value: 107 | print(" # reference to '{0}'".format(value['$ref'])) 108 | 109 | # convert key into label ('_' => ' ') 110 | label = " ".join(key.split("_")) 111 | 112 | required = value['required'] if 'required' in value else False 113 | nullable = not required and (default is None or default == 'null') 114 | 115 | model_class = None 116 | field_validator = None 117 | if obj_type == 'string': 118 | if 'format' in value: 119 | if value['format'] == 'email': 120 | model_class = "models.EmailField" 121 | elif value['format'] == 'date-time': 122 | model_class = "models.DateTimeField" 123 | elif value['format'] == 'uri': 124 | model_class = "models.URLField" 125 | 126 | else: 127 | model_class = "models.CharField" 128 | if 'pattern' in value: 129 | field_validator = """ 130 | RegexValidator( 131 | regex='{0}', 132 | message='{1} must follow the given pattern: {2}', 133 | code='invalid_{3}' 134 | ) 135 | """.format(value['pattern'], label, value['pattern'], key) 136 | 137 | elif obj_type == 'array': 138 | referenced_objects_type = value['items']['$ref'] 139 | print( 140 | " # add '{0}' property to get array of items " 141 | "referencing '{1}'".format( 142 | key, referenced_objects_type 143 | ) 144 | ) 145 | 146 | if model_class: 147 | # build field representation 148 | field_repr = ' {0} = {1}(_("{2}")'.format(key, model_class, label) 149 | if model_class == 'models.CharField': 150 | field_repr += ', max_length=128' 151 | if nullable: 152 | field_repr += ', blank=True' 153 | if model_class != 'models.CharField': 154 | field_repr += ', null=True' 155 | if field_validator: 156 | field_repr += ', validators=[{0}]'.format(field_validator) 157 | 158 | field_repr += ', help_text=_("{0}")'.format(value['description']) 159 | field_repr += ')' 160 | print(field_repr) 161 | 162 | 163 | if __name__ == "__main__": 164 | main() 165 | -------------------------------------------------------------------------------- /popolo/behaviors/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from autoslug import AutoSlugField 4 | from django.contrib.contenttypes.fields import GenericForeignKey 5 | from django.contrib.contenttypes.models import ContentType 6 | from django.core.exceptions import ValidationError 7 | from django.core.validators import RegexValidator 8 | from django.db import models 9 | from django.db.models import DateTimeField 10 | from django.utils.translation import ugettext_lazy as _ 11 | 12 | __author__ = "guglielmo" 13 | 14 | 15 | def get_slug_source(instance): 16 | """For use in AutoSlugField's populate_from""" 17 | return instance.slug_source 18 | 19 | 20 | class GenericRelatable(models.Model): 21 | """ 22 | An abstract class that provides the possibility of generic relations 23 | """ 24 | 25 | content_type = models.ForeignKey(ContentType, blank=True, null=True, db_index=True, on_delete=models.CASCADE,) 26 | object_id = models.PositiveIntegerField(null=True, db_index=True) 27 | content_object = GenericForeignKey("content_type", "object_id") 28 | 29 | class Meta: 30 | abstract = True 31 | 32 | 33 | def validate_partial_date(value): 34 | """ 35 | Validate a partial date, it can be partial, but it must yet be a valid date. 36 | Accepted formats are: YYYY-MM-DD, YYYY-MM, YYYY. 37 | 2013-22 must rais a ValidationError, as 2013-13-12, or 2013-11-55. 38 | """ 39 | try: 40 | datetime.strptime(value, "%Y-%m-%d") 41 | except ValueError: 42 | try: 43 | datetime.strptime(value, "%Y-%m") 44 | except ValueError: 45 | try: 46 | datetime.strptime(value, "%Y") 47 | except ValueError: 48 | raise ValidationError("date seems not to be correct %s" % value) 49 | 50 | 51 | class Dateframeable(models.Model): 52 | """ 53 | An abstract base class model that provides a start and an end dates to 54 | the class. 55 | Uncomplete dates can be used. The validation pattern is: "^[0-9]{4}(-[ 56 | 0-9]{2}){0,2}$" 57 | """ 58 | 59 | partial_date_validator = RegexValidator(regex="^[0-9]{4}(-[0-9]{2}){0,2}$", message="Date has wrong format") 60 | 61 | start_date = models.CharField( 62 | _("start date"), 63 | max_length=10, 64 | blank=True, 65 | null=True, 66 | validators=[partial_date_validator, validate_partial_date], 67 | help_text=_("The date when the validity of the item starts"), 68 | ) 69 | end_date = models.CharField( 70 | _("end date"), 71 | max_length=10, 72 | blank=True, 73 | null=True, 74 | validators=[partial_date_validator, validate_partial_date], 75 | help_text=_("The date when the validity of the item ends"), 76 | ) 77 | end_reason = models.CharField( 78 | _("end reason"), 79 | max_length=255, 80 | null=True, 81 | blank=True, 82 | help_text=_("The reason why the entity isn't valid any longer (eg: merge)"), 83 | ) 84 | 85 | @property 86 | def is_active_now(self): 87 | """Return the current status of the item, whether active or not 88 | 89 | :return: boolean 90 | """ 91 | return self.is_active() 92 | 93 | def is_active(self, moment=datetime.strftime(datetime.now(), "%Y-%m-%d")): 94 | """Return the status of the item at the given moment 95 | 96 | :param moment: date in '%Y-%m-%d' format 97 | :return: boolean 98 | """ 99 | return self.end_date is None or self.end_date >= moment 100 | 101 | def close(self, moment=datetime.strftime(datetime.now(), "%Y-%m-%d"), reason=None): 102 | """closes the validity of the entity, specifying a reason 103 | 104 | :param moment: the moment the validity ends, in %Y-%m-%d format 105 | :param reason: the reason whi the validity ends 106 | :return: 107 | """ 108 | self.end_date = moment 109 | if reason: 110 | self.end_reason = reason 111 | self.save() 112 | 113 | class Meta: 114 | abstract = True 115 | 116 | 117 | class Timestampable(models.Model): 118 | """ 119 | An abstract base class model that provides self-updating 120 | ``created`` and ``modified`` fields. 121 | """ 122 | 123 | created_at = DateTimeField(_("creation time"), auto_now_add=True) 124 | updated_at = DateTimeField(_("last modification time"), auto_now=True) 125 | 126 | class Meta: 127 | abstract = True 128 | 129 | 130 | class Permalinkable(models.Model): 131 | """ 132 | An abstract base class model that provides a unique slug, 133 | and the methods necessary to handle the permalink 134 | """ 135 | 136 | from django.utils.text import slugify 137 | 138 | slug = AutoSlugField(populate_from=get_slug_source, max_length=255, unique=True, slugify=slugify) 139 | 140 | class Meta: 141 | abstract = True 142 | 143 | 144 | class PrioritizedModel(models.Model): 145 | """ 146 | An abstract base class that provides an optional priority field, 147 | to impose a custom sorting order. 148 | """ 149 | 150 | priority = models.IntegerField( 151 | _("Priority"), null=True, blank=True, default=0, help_text=_("Sort order in case ambiguities arise") 152 | ) 153 | 154 | class Meta: 155 | abstract = True 156 | -------------------------------------------------------------------------------- /popolo/managers.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from datetime import datetime 3 | 4 | from popolo import models as popolo_models 5 | 6 | 7 | class HistoricAreaManager(models.Manager): 8 | def get_queryset(self): 9 | return super().get_queryset() 10 | 11 | def comuni_with_prov_and_istat_identifiers(self, d=None, filters=None, excludes=None): 12 | """Return the list of comuni active at a given date, 13 | with the name and identifier, plus the ISTAT_CODE_COM identifier valid at a given time 14 | and province and region information 15 | 16 | :param d: the date 17 | :param filters: a list of filters, in the form of dict 18 | :param excludes: a list of exclude in the form of dict 19 | :return: a list of Area objects, annotated with these fields: 20 | - id 21 | - identifier 22 | - name 23 | - istat_identifier 24 | - prov_id 25 | - prov_name 26 | - prov_identifier 27 | - reg_id 28 | - reg_name 29 | - reg_identifier 30 | """ 31 | if d is None: 32 | d = datetime.strftime(datetime.now(), '%Y-%m-%d') 33 | 34 | ar_qs = popolo_models.AreaRelationship.objects.filter(classification="FIP") 35 | ar_qs = ( 36 | ar_qs.filter(start_date__lte=d, end_date__gte=d) 37 | | ar_qs.filter(start_date__lte=d, end_date__isnull=True) 38 | | ar_qs.filter(start_date__isnull=True, end_date__gte=d) 39 | | ar_qs.filter(start_date__isnull=True, end_date__isnull=True) 40 | ) 41 | 42 | prev_provs = { 43 | a["source_area_id"]: { 44 | "prov_id": a["dest_area_id"], 45 | "prov": a["dest_area__identifier"], 46 | "prov_name": a["dest_area__name"], 47 | } 48 | for a in ar_qs.values( 49 | "source_area_id", "source_area__name", "dest_area_id", "dest_area__name", "dest_area__identifier" 50 | ) 51 | } 52 | 53 | current_regs = { 54 | a["id"]: { 55 | "reg_id": a["parent__parent_id"], 56 | "reg_identifier": a["parent__parent__identifier"], 57 | "reg_name": a["parent__parent__name"], 58 | } 59 | for a in popolo_models.Area.objects.comuni() 60 | .current(d) 61 | .values("id", "parent__parent_id", "parent__parent__name", "parent__parent__identifier") 62 | } 63 | 64 | current_provs = { 65 | a["id"]: {"prov_id": a["parent_id"], "prov": a["parent__identifier"], "prov_name": a["parent__name"]} 66 | for a in popolo_models.Area.objects.comuni() 67 | .current(d) 68 | .values("id", "parent_id", "parent__name", "parent__identifier") 69 | } 70 | 71 | provs = {} 72 | for a_id, prov_info in current_provs.items(): 73 | if a_id in prev_provs: 74 | prov_id = prev_provs[a_id]["prov_id"] 75 | prov = prev_provs[a_id]["prov"] 76 | prov_name = prev_provs[a_id]["prov_name"] 77 | else: 78 | prov_id = current_provs[a_id]["prov_id"] 79 | prov = current_provs[a_id]["prov"] 80 | prov_name = current_provs[a_id]["prov_name"] 81 | 82 | provs[a_id] = {"prov_id": prov_id, "prov": prov, "prov_name": prov_name} 83 | 84 | current_comuni_qs = popolo_models.Area.objects.comuni().current(moment=d) 85 | 86 | current_comuni_qs = ( 87 | current_comuni_qs.filter( 88 | identifiers__scheme="ISTAT_CODE_COM", identifiers__start_date__lte=d, identifiers__end_date__gte=d 89 | ) 90 | | current_comuni_qs.filter( 91 | identifiers__scheme="ISTAT_CODE_COM", 92 | identifiers__start_date__lte=d, 93 | identifiers__end_date__isnull=True, 94 | ) 95 | | current_comuni_qs.filter( 96 | identifiers__scheme="ISTAT_CODE_COM", 97 | identifiers__start_date__isnull=True, 98 | identifiers__end_date__isnull=True, 99 | ) 100 | | current_comuni_qs.filter( 101 | identifiers__scheme="ISTAT_CODE_COM", 102 | identifiers__start_date__isnull=True, 103 | identifiers__end_date__gte=d, 104 | ) 105 | ) 106 | 107 | if filters: 108 | for f in filters: 109 | current_comuni_qs = current_comuni_qs.filter(**f) 110 | if excludes: 111 | for e in excludes: 112 | current_comuni_qs = current_comuni_qs.exclude(**e) 113 | 114 | current_comuni = current_comuni_qs.values("id", "name", "identifier", "identifiers__identifier") 115 | 116 | results_list = [] 117 | for com in current_comuni: 118 | c = self.model(id=com["id"], name=com["name"], identifier=com["identifier"]) 119 | c.istat_identifier = com["identifiers__identifier"] 120 | c.prov_name = provs[com["id"]]["prov_name"] 121 | c.prov_id = provs[com["id"]]["prov_id"] 122 | c.prov_identifier = provs[com["id"]]["prov"] 123 | c.reg_name = current_regs[com["id"]]["reg_name"] 124 | c.reg_id = current_regs[com["id"]]["reg_id"] 125 | c.reg_identifier = current_regs[com["id"]]["reg_identifier"] 126 | 127 | results_list.append(c) 128 | 129 | return results_list 130 | -------------------------------------------------------------------------------- /popolo/signals.py: -------------------------------------------------------------------------------- 1 | from django.apps import apps 2 | from django.db import IntegrityError 3 | from django.db.models import Model 4 | from django.db.models.signals import pre_save, post_save 5 | from django.utils.translation import ugettext_lazy as _ 6 | 7 | from popolo.behaviors.models import Dateframeable 8 | from popolo.models import Organization, Person, Membership, Ownership, OriginalEducationLevel, Post, Area, KeyEvent 9 | 10 | 11 | def copy_organization_date_fields(sender, instance: Organization, **kwargs): 12 | """ 13 | Copy founding and dissolution dates into start and end dates, 14 | so that Organization can extend the abstract Dateframeable behavior. 15 | 16 | :param sender: The model class 17 | :param instance: The actual instance being saved. 18 | :param kwargs: Other args. See: https://docs.djangoproject.com/en/dev/ref/signals/#pre-save 19 | """ 20 | if instance.founding_date: 21 | instance.start_date = instance.founding_date 22 | if instance.dissolution_date: 23 | instance.end_date = instance.dissolution_date 24 | 25 | 26 | def copy_person_date_fields(sender, instance: Person, **kwargs): 27 | """ 28 | Copy birth and death dates into start and end dates, 29 | so that Person can extend the abstract Dateframeable behavior. 30 | 31 | :param sender: The model class 32 | :param instance: The actual instance being saved. 33 | :param kwargs: Other args. See: https://docs.djangoproject.com/en/dev/ref/signals/#pre-save 34 | """ 35 | 36 | if instance.birth_date: 37 | instance.start_date = instance.birth_date 38 | if instance.death_date: 39 | instance.end_date = instance.death_date 40 | 41 | 42 | def verify_start_end_dates_order(sender, instance: Dateframeable, **kwargs): 43 | """ 44 | All Dateframeable instances need to have proper dates. 45 | 46 | :param sender: The model class 47 | :param instance: The actual instance being saved. 48 | :param kwargs: Other args. See: https://docs.djangoproject.com/en/dev/ref/signals/#pre-save 49 | :raises: IntegrityError 50 | """ 51 | 52 | if instance.start_date and instance.end_date and instance.start_date > instance.end_date: 53 | raise IntegrityError(_("Initial date must precede end date")) 54 | 55 | 56 | def verify_start_end_dates_non_blank(sender, instance: Dateframeable, **kwargs): 57 | """ 58 | Memberships dates must be non-blank. 59 | 60 | :param sender: The model class 61 | :param instance: The actual instance being saved. 62 | :param kwargs: Other args. See: https://docs.djangoproject.com/en/dev/ref/signals/#pre-save 63 | :raises: IntegrityError 64 | """ 65 | 66 | if instance.end_date == "" or instance.start_date == "": 67 | raise IntegrityError( 68 | _( 69 | f"Dates should not be blank for " 70 | f"{type(instance)} (id:{instance.id}): <{instance.start_date}> - <{instance.end_date}>" 71 | ) 72 | ) 73 | 74 | 75 | def verify_membership_has_org_and_member(sender, instance: Membership, **kwargs): 76 | """ 77 | A proper memberships has at least an organisation and a person member 78 | 79 | :param sender: The model class 80 | :param instance: The actual instance being saved. 81 | :param kwargs: Other args. See: https://docs.djangoproject.com/en/dev/ref/signals/#pre-save 82 | :raises: IntegrityError 83 | """ 84 | if instance.person is None and instance.member_organization is None: 85 | raise IntegrityError(_("A member, either a Person or an Organization, must be specified.")) 86 | if instance.organization is None: 87 | raise IntegrityError(_("An Organization, must be specified.")) 88 | 89 | 90 | def verify_ownership_has_org_and_owner(sender, instance: Ownership, **kwargs): 91 | """ 92 | A proper ownership has at least an owner and an owned organisations. 93 | 94 | :param sender: The model class 95 | :param instance: The actual instance being saved. 96 | :param kwargs: Other args. See: https://docs.djangoproject.com/en/dev/ref/signals/#pre-save 97 | :raises: IntegrityError 98 | """ 99 | if instance.owner_person is None and instance.owner_organization is None: 100 | raise IntegrityError(_("An owner, either a Person or an Organization, must be specified.")) 101 | if instance.owned_organization is None: 102 | raise IntegrityError(_("An owned Organization must be specified.")) 103 | 104 | 105 | def update_education_levels(sender, instance, **kwargs): 106 | """ 107 | Updates persons education_level when the mapping between 108 | the original education_level and the normalized one is touched. 109 | 110 | :param sender: The model class 111 | :param instance: The actual instance being saved. 112 | :param kwargs: Other args. See: https://docs.djangoproject.com/en/dev/ref/signals/#post-save 113 | """ 114 | if instance.normalized_education_level: 115 | instance.persons_with_this_original_education_level.exclude( 116 | education_level=instance.normalized_education_level 117 | ).update(education_level=instance.normalized_education_level) 118 | 119 | 120 | def validate_fields(sender, instance: Model, **kwargs): 121 | """ 122 | Main instances are always validated before being saved. 123 | 124 | :param sender: 125 | :param instance: 126 | :param kwargs: 127 | """ 128 | instance.full_clean() 129 | 130 | 131 | def connect(): 132 | """ 133 | Connect all the signals. 134 | """ 135 | 136 | for model_class in [Person, Organization, Post, Membership, Ownership, KeyEvent, Area]: 137 | pre_save.connect(receiver=validate_fields, sender=model_class) 138 | 139 | pre_save.connect(receiver=copy_organization_date_fields, sender=Organization) 140 | 141 | pre_save.connect(receiver=copy_person_date_fields, sender=Person) 142 | 143 | # Connect a pre-save signal to all models subclassing Dateframeable 144 | for _dummy, model_class in apps.all_models.get("popolo").items(): 145 | if issubclass(model_class, Dateframeable): 146 | pre_save.connect(receiver=verify_start_end_dates_order, sender=model_class) 147 | pre_save.connect(receiver=verify_start_end_dates_non_blank, sender=model_class) 148 | 149 | pre_save.connect(receiver=verify_membership_has_org_and_member, sender=Membership) 150 | 151 | pre_save.connect(receiver=verify_ownership_has_org_and_owner, sender=Ownership) 152 | 153 | post_save.connect(receiver=update_education_levels, sender=OriginalEducationLevel) 154 | -------------------------------------------------------------------------------- /popolo/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from datetime import datetime, timedelta 4 | from unittest import TestCase 5 | 6 | from faker import Factory 7 | from popolo.exceptions import PartialDateException 8 | from popolo.utils import PartialDate, PartialDatesInterval 9 | 10 | faker = Factory.create("it_IT") # a factory to create fake names for tests 11 | 12 | 13 | class PartialDateTestCase(TestCase): 14 | def create_instance(self, ds=None, pattern=PartialDate.d_fmt): 15 | if not ds: 16 | ds = faker.date(pattern=pattern) 17 | return PartialDate(ds) 18 | 19 | def test_new_instance(self): 20 | ds = faker.date(PartialDate.d_fmt) 21 | d = self.create_instance(ds) 22 | 23 | self.assertEqual(isinstance(d.date, str), True) 24 | self.assertEqual(isinstance(d.date_as_dt, datetime), True) 25 | 26 | def test_new_instance_using_d_fmt(self): 27 | ds = faker.date(PartialDate.d_fmt) 28 | d = self.create_instance(ds) 29 | 30 | self.assertEqual(d.date, ds) 31 | self.assertEqual(d.date_as_dt, datetime.strptime(ds, PartialDate.d_fmt)) 32 | 33 | def test_new_instance_using_m_fmt(self): 34 | ds = faker.date(PartialDate.m_fmt) 35 | d = self.create_instance(ds) 36 | 37 | self.assertEqual(d.date_as_dt, datetime.strptime(ds, PartialDate.m_fmt)) 38 | 39 | def test_new_instance_using_y_fmt(self): 40 | ds = faker.date(PartialDate.y_fmt) 41 | d = self.create_instance(ds) 42 | 43 | self.assertEqual(d.date_as_dt, datetime.strptime(ds, PartialDate.y_fmt)) 44 | 45 | def test_new_instance_null(self): 46 | d = PartialDate(None) 47 | self.assertEqual(d.date, None) 48 | self.assertEqual(d.date_as_dt, None) 49 | 50 | def test_sub_partialdate(self): 51 | dt = timedelta(100) 52 | 53 | da = faker.date() 54 | a = PartialDate(da) 55 | b = a + dt 56 | 57 | self.assertTrue(b - a, dt) 58 | 59 | def test_sub_timedelta(self): 60 | dt = timedelta(100) 61 | 62 | da = faker.date() 63 | a = PartialDate(da) 64 | b = PartialDate(da) + dt 65 | 66 | self.assertTrue(b - dt, a) 67 | 68 | def test_sum_timedelta(self): 69 | da = faker.date() 70 | a = PartialDate(da) 71 | 72 | dt = timedelta(100) 73 | 74 | db = datetime.strftime(faker.date_time_between(a.date_as_dt + timedelta(days=100)), "%Y-%m-%d") 75 | b = PartialDate(db) 76 | self.assertTrue(b + dt, a) 77 | 78 | def test_eq_comparison(self): 79 | da = db = faker.date() 80 | a = self.create_instance(da) 81 | b = self.create_instance(db) 82 | 83 | self.assertEqual(a, b) 84 | 85 | def test_eq_comparison_null(self): 86 | a = PartialDate(None) 87 | b = PartialDate(None) 88 | 89 | self.assertEqual(a, b) 90 | 91 | def test_gt_comparison(self): 92 | dt = timedelta(100) 93 | 94 | da = faker.date() 95 | a = PartialDate(da) 96 | b = PartialDate(da) + dt 97 | 98 | self.assertGreater(b, a) 99 | 100 | def test_gt_comparison_null(self): 101 | da = faker.date() 102 | 103 | a = PartialDate(da) 104 | b = PartialDate(None) 105 | with self.assertRaises(PartialDateException): 106 | self.assertGreater(b, a) 107 | 108 | def test_lt_comparison(self): 109 | dt = timedelta(100) 110 | 111 | da = faker.date() 112 | a = PartialDate(da) 113 | b = PartialDate(da) + dt 114 | 115 | self.assertLess(a, b) 116 | 117 | def test_lt_comparison_null(self): 118 | da = faker.date() 119 | 120 | a = PartialDate(da) 121 | b = PartialDate(None) 122 | with self.assertRaises(PartialDateException): 123 | self.assertLess(a, b) 124 | 125 | def test_ge_comparison(self): 126 | dt = timedelta(100) 127 | 128 | da = faker.date() 129 | a = PartialDate(da) 130 | b = PartialDate(da) + dt 131 | c = PartialDate(da) 132 | 133 | self.assertGreaterEqual(b, a) 134 | self.assertGreaterEqual(c, a) 135 | 136 | def test_intervals_overlap(self): 137 | da_start = faker.date() 138 | a_start = PartialDate(da_start) 139 | 140 | da_end = datetime.strftime(faker.date_time_between(a_start.date_as_dt), "%Y-%m-%d") 141 | a_end = PartialDate(da_end) 142 | 143 | db_start = datetime.strftime(faker.date_time_between(a_start.date_as_dt, a_end.date_as_dt), "%Y-%m-%d") 144 | b_start = PartialDate(db_start) 145 | 146 | db_end = datetime.strftime(faker.date_time_between(b_start.date_as_dt), "%Y-%m-%d") 147 | b_end = PartialDate(db_end) 148 | 149 | self.assertLessEqual(b_start, a_end) 150 | 151 | a = PartialDatesInterval(start=a_start, end=a_end) 152 | b = PartialDatesInterval(start=b_start, end=b_end) 153 | 154 | overlap = PartialDate.intervals_overlap(a, b) 155 | self.assertGreater(overlap, 0) 156 | 157 | def test_intervals_do_not_overlap(self): 158 | da_start = faker.date() 159 | a_start = PartialDate(da_start) 160 | 161 | da_end = datetime.strftime(faker.date_time_between(a_start.date_as_dt), "%Y-%m-%d") 162 | a_end = PartialDate(da_end) 163 | 164 | db_start = datetime.strftime(faker.date_time_between(a_end.date_as_dt), "%Y-%m-%d") 165 | b_start = PartialDate(db_start) 166 | 167 | db_end = datetime.strftime(faker.date_time_between(b_start.date_as_dt), "%Y-%m-%d") 168 | b_end = PartialDate(db_end) 169 | 170 | a = PartialDatesInterval(start=a_start, end=a_end) 171 | b = PartialDatesInterval(start=b_start, end=b_end) 172 | 173 | overlap = PartialDate.intervals_overlap(a, b) 174 | self.assertLessEqual(overlap, 0) 175 | 176 | def test_intervals_overlap_null_start_end(self): 177 | a_start = PartialDate(None) 178 | 179 | da_end = faker.date() 180 | a_end = PartialDate(da_end) 181 | 182 | db_start = datetime.strftime( 183 | faker.date_time_between(a_end.date_as_dt - timedelta(days=50), a_end.date_as_dt), "%Y-%m-%d" 184 | ) 185 | b_start = PartialDate(db_start) 186 | b_end = PartialDate(None) 187 | 188 | a = PartialDatesInterval(start=a_start, end=a_end) 189 | b = PartialDatesInterval(start=b_start, end=b_end) 190 | 191 | overlap = PartialDate.intervals_overlap(a, b) 192 | self.assertGreater(overlap, 0) 193 | 194 | def test_intervals_do_not_overlap_null_start_end(self): 195 | a_start = PartialDate(None) 196 | 197 | da_end = faker.date() 198 | a_end = PartialDate(da_end) 199 | 200 | db_start = datetime.strftime(faker.date_time_between(a_end.date_as_dt + timedelta(days=10)), "%Y-%m-%d") 201 | b_start = PartialDate(db_start) 202 | 203 | b_end = PartialDate(None) 204 | 205 | a = PartialDatesInterval(start=a_start, end=a_end) 206 | b = PartialDatesInterval(start=b_start, end=b_end) 207 | 208 | overlap = PartialDate.intervals_overlap(a, b) 209 | self.assertLessEqual(overlap, 0) 210 | -------------------------------------------------------------------------------- /popolo/behaviors/tests.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from datetime import timedelta 3 | from time import sleep 4 | 5 | from django.core.exceptions import ValidationError 6 | 7 | 8 | class BehaviorTestCaseMixin(object): 9 | def get_model(self): 10 | return getattr(self, "model") 11 | 12 | def create_instance(self, **kwargs): 13 | raise NotImplementedError("Implement me") 14 | 15 | 16 | class DateframeableTests(BehaviorTestCaseMixin): 17 | """ 18 | Dateframeable tests. 19 | 20 | Are dates valid? Are invalid dates blocked? 21 | Are querysets to filter past, present and future items correct? 22 | """ 23 | 24 | def test_new_instance_has_valid_dates(self): 25 | """Test complete or incomplete dates, 26 | according to the "^[0-9]{4}(-[0-9]{2}){0,2}$" pattern (incomplete 27 | dates)""" 28 | obj = self.create_instance(start_date="2012") 29 | self.assertRegexpMatches(obj.start_date, "^[0-9]{4}(-[0-9]{2}){0,2}$", "date does not match pattern") 30 | obj = self.create_instance(end_date="2012") 31 | self.assertRegexpMatches(obj.end_date, "^[0-9]{4}(-[0-9]{2}){0,2}$", "date does not match pattern") 32 | 33 | obj = self.create_instance(start_date="2012-01") 34 | self.assertRegexpMatches(obj.start_date, "^[0-9]{4}(-[0-9]{2}){0,2}$", "date does not match pattern") 35 | obj = self.create_instance(end_date="2012-02") 36 | self.assertRegexpMatches(obj.end_date, "^[0-9]{4}(-[0-9]{2}){0,2}$", "date does not match pattern") 37 | 38 | obj = self.create_instance(start_date="2012-10-12") 39 | self.assertRegexpMatches(obj.start_date, "^[0-9]{4}(-[0-9]{2}){0,2}$", "date does not match pattern") 40 | obj = self.create_instance(end_date="2012-12-10") 41 | self.assertRegexpMatches(obj.end_date, "^[0-9]{4}(-[0-9]{2}){0,2}$", "date does not match pattern") 42 | 43 | with self.assertRaises(Exception): 44 | self.create_instance(end_date="") 45 | 46 | def test_invalid_dates_are_blocked(self): 47 | """Test if dates are valid (months and days range are tested)""" 48 | # test invalid start dates 49 | with self.assertRaises(ValidationError): 50 | self.create_instance(start_date="YESTERDAY") 51 | 52 | with self.assertRaises(ValidationError): 53 | self.create_instance(start_date="2012-1210") 54 | 55 | with self.assertRaises(ValidationError): 56 | self.create_instance(start_date="2012-13") 57 | 58 | with self.assertRaises(ValidationError): 59 | self.create_instance(start_date="2012-12-34") 60 | 61 | # test invalid end dates 62 | with self.assertRaises(ValidationError): 63 | self.create_instance(end_date="YESTERDAY") 64 | 65 | with self.assertRaises(ValidationError): 66 | self.create_instance(end_date="2012-1210") 67 | 68 | with self.assertRaises(ValidationError): 69 | self.create_instance(end_date="2012-13") 70 | 71 | with self.assertRaises(ValidationError): 72 | self.create_instance(end_date="2012-12-34") 73 | 74 | def test_querysets_filters(self): 75 | """Test current, past and future querysets""" 76 | self.create_instance( 77 | start_date=datetime.strftime(datetime.now() - timedelta(days=10), "%Y-%m-%d"), 78 | end_date=datetime.strftime(datetime.now() - timedelta(days=5), "%Y-%m-%d"), 79 | ) 80 | self.create_instance( 81 | start_date=datetime.strftime(datetime.now() - timedelta(days=5), "%Y-%m-%d"), 82 | end_date=datetime.strftime(datetime.now() + timedelta(days=5), "%Y-%m-%d"), 83 | ) 84 | self.create_instance( 85 | start_date=datetime.strftime(datetime.now() + timedelta(days=5), "%Y-%m-%d"), 86 | end_date=datetime.strftime(datetime.now() + timedelta(days=10), "%Y-%m-%d"), 87 | ) 88 | 89 | self.assertEqual(self.get_model().objects.all().count(), 3, "Something really bad is going on") 90 | self.assertEqual(self.get_model().objects.past().count(), 1, "One past object should have been fetched") 91 | self.assertEqual(self.get_model().objects.current().count(), 1, "One current object should have been fetched") 92 | self.assertEqual(self.get_model().objects.future().count(), 1, "One future object should have been fetched") 93 | 94 | def test_is_active_now(self): 95 | i = self.create_instance() 96 | self.assertEqual(i.is_active_now, True) 97 | 98 | i = self.create_instance(start_date="2012-05-24", end_date="2017") 99 | self.assertEqual(i.is_active_now, False) 100 | 101 | i = self.create_instance(start_date="2014-04-23", end_date="2050") 102 | self.assertEqual(i.is_active_now, True) 103 | 104 | i = self.create_instance(start_date="2013-06-22") 105 | self.assertEqual(i.is_active_now, True) 106 | 107 | def test_is_active(self): 108 | i = self.create_instance(start_date="2012", end_date="2017") 109 | self.assertEqual(i.is_active("2015-04-23"), True) 110 | 111 | 112 | class TimestampableTests(BehaviorTestCaseMixin): 113 | """ 114 | Timestampable tests. 115 | 116 | Tests whether objects are assigned timestamps at creation time, and 117 | whether a successive modification changes the update timestamp only. 118 | """ 119 | 120 | def test_new_instance_has_equal_timestamps(self): 121 | """Object is assigned timestamps when created""" 122 | obj = self.create_instance() 123 | self.assertIsNotNone(obj.created_at) 124 | self.assertIsNotNone(obj.updated_at) 125 | 126 | # created_at and updated_at are actually different, but still within 127 | # 30 millisec 128 | # that's because of the pre-save signal validation 129 | self.assertTrue( 130 | (obj.updated_at - obj.created_at) < timedelta(microseconds=30000), 131 | obj.updated_at - obj.created_at 132 | ) 133 | 134 | def test_updated_instance_has_different_timestamps(self): 135 | """Modified object has different created_at and updated_at timestamps 136 | """ 137 | obj = self.create_instance() 138 | creation_ts = obj.created_at 139 | update_ts = obj.updated_at 140 | # save object after 30K microsecs and check again 141 | sleep(0.03) 142 | obj.save() 143 | self.assertEqual(obj.created_at, creation_ts) 144 | self.assertNotEqual(obj.updated_at, update_ts) 145 | 146 | # created_at and updated_at are actually different, well outside 20 147 | # millisecs 148 | self.assertFalse( 149 | (obj.updated_at - obj.created_at) < timedelta(microseconds=30000), 150 | obj.updated_at - obj.created_at 151 | ) 152 | 153 | 154 | class PermalinkableTests(BehaviorTestCaseMixin): 155 | """ 156 | Permalinkable tests. 157 | 158 | Tests whether objects are assigned slugs and have the correct absolute_url 159 | """ 160 | 161 | def test_instance_has_slug(self): 162 | """The instance has been assigned a non-null slug""" 163 | i = self.create_instance() 164 | self.assertIsNotNone(i.slug) 165 | -------------------------------------------------------------------------------- /popolo/tests/factories.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import random 3 | 4 | import factory 5 | from faker import Factory 6 | from popolo.models import Area 7 | 8 | faker = Factory.create("it_IT") # a factory to create fake names for tests 9 | 10 | 11 | class PersonFactory(factory.django.DjangoModelFactory): 12 | class Meta: 13 | model = "popolo.Person" 14 | 15 | gender = random.choice(["M", "F"]) 16 | given_name = factory.Faker("first_name") 17 | family_name = factory.Faker("last_name") 18 | name = factory.LazyAttribute(lambda o: o.given_name + " " + o.family_name) 19 | birth_date = factory.Faker("date", pattern="%Y-%m-%d", end_datetime="-27y") 20 | birth_location = factory.Faker("city") 21 | additional_name = factory.Faker("first_name") 22 | email = factory.Faker("ascii_safe_email") 23 | biography = factory.Faker("paragraph", nb_sentences=7, variable_nb_sentences=True, ext_word_list=None) 24 | 25 | 26 | class AreaFactory(factory.django.DjangoModelFactory): 27 | class Meta: 28 | model = "popolo.Area" 29 | 30 | name = factory.Faker("city") 31 | identifier = factory.Faker("pystr", max_chars=4) 32 | classification = factory.Faker("pystr", max_chars=5) 33 | inhabitants = factory.Faker("pyint") 34 | 35 | @factory.lazy_attribute 36 | def istat_classification(self): 37 | return random.choice([a[0] for a in Area.ISTAT_CLASSIFICATIONS]) 38 | 39 | 40 | class OrganizationFactory(factory.django.DjangoModelFactory): 41 | class Meta: 42 | model = "popolo.Organization" 43 | 44 | name = factory.Faker("company") 45 | identifier = factory.Faker("pystr", max_chars=11) 46 | 47 | 48 | class OrganizationRelationshipFactory(factory.django.DjangoModelFactory): 49 | class Meta: 50 | model = "popolo.OrganizationRelationship" 51 | 52 | weight = factory.Faker("pyint", min_value=-2, max_value=2) 53 | descr = factory.Faker("sentence", nb_words=8) 54 | 55 | @factory.lazy_attribute 56 | def source_organization(self): 57 | return OrganizationFactory.create() 58 | 59 | @factory.lazy_attribute 60 | def dest_organization(self): 61 | return OrganizationFactory.create() 62 | 63 | @factory.lazy_attribute 64 | def classification(self): 65 | return ClassificationFactory.create( 66 | scheme='OP_TIPO_RELAZIONE_ORG', 67 | code='OT_01', 68 | descr='Vigilanza' 69 | ) 70 | 71 | @factory.lazy_attribute 72 | def start_date(self): 73 | return faker.date_between(start_date="-3y", end_date="-2y").strftime("%Y-%m-%d") 74 | 75 | @factory.lazy_attribute 76 | def end_date(self): 77 | return faker.date_between(start_date="-2y", end_date="-1y").strftime("%Y-%m-%d") 78 | 79 | 80 | class ElectoralEventFactory(factory.django.DjangoModelFactory): 81 | class Meta: 82 | model = "popolo.KeyEvent" 83 | 84 | name = factory.Faker("sentence", nb_words=3) 85 | identifier = factory.Faker("pystr", max_chars=11) 86 | event_type = "ELE" 87 | start_date = factory.Faker("date", pattern="%Y-%m-%d", end_datetime="-27y") 88 | 89 | 90 | class LegislatureEventFactory(factory.django.DjangoModelFactory): 91 | class Meta: 92 | model = "popolo.KeyEvent" 93 | 94 | name = factory.Faker("sentence") 95 | identifier = factory.Faker("pystr", max_chars=11) 96 | event_type = "ITL" 97 | start_date = factory.Faker("date", pattern="%Y-%m-%d", end_datetime="-27y") 98 | 99 | 100 | class XadmEventFactory(factory.django.DjangoModelFactory): 101 | class Meta: 102 | model = "popolo.KeyEvent" 103 | 104 | name = factory.Faker("sentence") 105 | identifier = factory.Faker("pystr", max_chars=11) 106 | event_type = "XAD" 107 | start_date = factory.Faker("date", pattern="%Y-%m-%d", end_datetime="-27y") 108 | 109 | 110 | class MembershipFactory(factory.django.DjangoModelFactory): 111 | class Meta: 112 | model = "popolo.Membership" 113 | 114 | label = factory.Faker("sentence", nb_words=8) 115 | role = factory.Faker("sentence", nb_words=8) 116 | 117 | @factory.lazy_attribute 118 | def person(self): 119 | return PersonFactory.create() 120 | 121 | @factory.lazy_attribute 122 | def organization(self): 123 | return OrganizationFactory.create() 124 | 125 | @factory.lazy_attribute 126 | def start_date(self): 127 | return faker.date_between(start_date="-3y", end_date="-2y").strftime("%Y-%m-%d") 128 | 129 | @factory.lazy_attribute 130 | def end_date(self): 131 | return faker.date_between(start_date="-2y", end_date="-1y").strftime("%Y-%m-%d") 132 | 133 | 134 | class PostFactory(factory.django.DjangoModelFactory): 135 | class Meta: 136 | model = "popolo.Post" 137 | 138 | label = factory.Faker("sentence", nb_words=8) 139 | role = factory.Faker("sentence", nb_words=8) 140 | 141 | @factory.lazy_attribute 142 | def start_date(self): 143 | return faker.date_between(start_date="-3y", end_date="-2y").strftime("%Y-%m-%d") 144 | 145 | @factory.lazy_attribute 146 | def end_date(self): 147 | return faker.date_between(start_date="-2y", end_date="-1y").strftime("%Y-%m-%d") 148 | 149 | 150 | class ClassificationFactory(factory.django.DjangoModelFactory): 151 | class Meta: 152 | model = "popolo.Classification" 153 | django_get_or_create = ('scheme', 'code') 154 | 155 | scheme = factory.Faker("pystr", max_chars=16) 156 | code = factory.Faker("pystr", max_chars=8) 157 | descr = factory.Faker("sentence", nb_words=8) 158 | 159 | 160 | class IdentifierFactory(factory.django.DjangoModelFactory): 161 | class Meta: 162 | model = "popolo.Identifier" 163 | 164 | scheme = factory.Faker("pystr", max_chars=32) 165 | identifier = factory.Faker("pystr", max_chars=64) 166 | source = factory.Faker("url") 167 | 168 | 169 | class LinkFactory(factory.django.DjangoModelFactory): 170 | class Meta: 171 | model = "popolo.Link" 172 | 173 | url = factory.Faker("url") 174 | note = factory.Faker("sentence", nb_words=10) 175 | 176 | 177 | class SourceFactory(factory.django.DjangoModelFactory): 178 | class Meta: 179 | model = "popolo.Source" 180 | 181 | url = factory.Faker("url") 182 | note = factory.Faker("sentence", nb_words=10) 183 | 184 | 185 | class OriginalProfessionFactory(factory.django.DjangoModelFactory): 186 | class Meta: 187 | model = "popolo.OriginalProfession" 188 | 189 | name = factory.Faker("sentence", nb_words=7) 190 | 191 | 192 | class ProfessionFactory(factory.django.DjangoModelFactory): 193 | class Meta: 194 | model = "popolo.Profession" 195 | 196 | name = factory.Faker("sentence", nb_words=7) 197 | 198 | 199 | class OriginalEducationLevelFactory(factory.django.DjangoModelFactory): 200 | class Meta: 201 | model = "popolo.OriginalEducationLevel" 202 | 203 | name = factory.Faker("sentence", nb_words=7) 204 | 205 | 206 | class EducationLevelFactory(factory.django.DjangoModelFactory): 207 | class Meta: 208 | model = "popolo.EducationLevel" 209 | 210 | name = factory.Faker("sentence", nb_words=7) 211 | 212 | 213 | class RoleTypeFactory(factory.django.DjangoModelFactory): 214 | class Meta: 215 | model = "popolo.RoleType" 216 | 217 | label = factory.Faker("sentence", nb_words=7) 218 | priority = factory.Faker("pyint") 219 | 220 | @factory.lazy_attribute 221 | def classification(self): 222 | c = ClassificationFactory.create() 223 | c.scheme = "FORMA_GIURIDICA_OP" 224 | return c 225 | -------------------------------------------------------------------------------- /popolo/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.admin import SimpleListFilter 3 | from django.contrib.contenttypes.admin import GenericTabularInline 4 | from django.contrib.gis import forms 5 | from django.contrib.gis.db.models import MultiPolygonField 6 | from django.db import models 7 | from django.forms import TextInput 8 | 9 | from popolo import models as popolo_models 10 | 11 | 12 | class NullListFilter(SimpleListFilter): 13 | def lookups(self, request, model_admin): 14 | return ("1", "Null"), ("0", "!= Null") 15 | 16 | def queryset(self, request, queryset): 17 | if self.value() in ("0", "1"): 18 | kwargs = {"{0}__isnull".format(self.parameter_name): self.value() == "1"} 19 | return queryset.filter(**kwargs) 20 | return queryset 21 | 22 | 23 | class EndDateNullListFilter(NullListFilter): 24 | title = "End date" 25 | parameter_name = "end_date" 26 | 27 | 28 | class ClassificationAdmin(admin.ModelAdmin): 29 | model = popolo_models.Classification 30 | list_display = ("scheme", "code", "descr") 31 | list_filter = ("scheme",) 32 | search_fields = ("code", "descr") 33 | raw_id_fields = ("parent",) 34 | 35 | 36 | def set_appointables(modeladmin, request, queryset): 37 | for item in queryset: 38 | item.is_appointable = True 39 | item.save() 40 | 41 | 42 | set_appointables.short_description = "Set RoleTypes as appointables" 43 | 44 | 45 | def unset_appointables(modeladmin, request, queryset): 46 | for item in queryset: 47 | item.is_appointable = False 48 | item.save() 49 | 50 | 51 | unset_appointables.short_description = "Set RoleTypes as not appointables" 52 | 53 | 54 | class RoleTypeAdmin(admin.ModelAdmin): 55 | model = popolo_models.RoleType 56 | list_display = ("label", "classification", "priority", "other_label", "is_appointer", "is_appointable") 57 | list_filter = (("classification", admin.RelatedOnlyFieldListFilter),) 58 | list_select_related = ("classification",) 59 | actions = (set_appointables, unset_appointables) 60 | search_fields = ("label", "other_label") 61 | 62 | def formfield_for_foreignkey(self, db_field, request, **kwargs): 63 | if db_field.name == "classification": 64 | kwargs["queryset"] = popolo_models.Classification.objects.filter(scheme="FORMA_GIURIDICA_OP").order_by( 65 | "code" 66 | ) 67 | return super(RoleTypeAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) 68 | 69 | 70 | class PostAdmin(admin.ModelAdmin): 71 | model = popolo_models.Post 72 | list_display = ("label", "role_type", "appointed_by") 73 | list_filter = (("role_type", admin.RelatedOnlyFieldListFilter),) 74 | list_select_related = ("role_type", "organization", "appointed_by") 75 | search_fields = ("label", "other_label", "role", "role_type__label") 76 | raw_id_fields = ("role_type", "organization", "appointed_by") 77 | exclude = ("start_date", "end_date", "end_reason", "other_label", "area", "role") 78 | readonly_fields = ("label", "role_type", "organization") 79 | 80 | 81 | class MembershipAdmin(admin.ModelAdmin): 82 | model = popolo_models.Membership 83 | list_display = ("person", "label", "start_date", "end_date", "appointed_by") 84 | list_display_links = ("label",) 85 | list_select_related = ("person", "appointed_by") 86 | list_filter = (EndDateNullListFilter,) 87 | search_fields = ("label", "role", "person__name", "organization__name") 88 | raw_id_fields = ("person", "organization", "appointed_by") 89 | readonly_fields = ("person", "organization", "role", "post", "start_date", "end_date", "end_reason") 90 | fields = readonly_fields + ("appointed_by", "is_appointment_locked", "appointment_note") 91 | 92 | 93 | class OwnershipAdmin(admin.ModelAdmin): 94 | model = popolo_models.Ownership 95 | list_display = ("owner", "percentage", "owned_organization", "start_date", "end_date") 96 | list_filter = (EndDateNullListFilter,) 97 | list_display_links = ("percentage",) 98 | list_select_related = ("owned_organization", "owner_person", "owner_organization") 99 | search_fields = ("owned_organization__name", "owner_person__name", "owner_organization__name") 100 | raw_id_fields = ("owned_organization", "owner_organization", "owner_person") 101 | fields = ( 102 | "owned_organization", 103 | "owner_person", 104 | "owner_organization", 105 | "percentage", 106 | "start_date", 107 | "end_date", 108 | "end_reason", 109 | ) 110 | 111 | 112 | class IdentifiersInline(GenericTabularInline): 113 | model = popolo_models.Identifier 114 | extra = 0 115 | max_num = 5 116 | 117 | 118 | class OriginalEducationInline(admin.TabularInline): 119 | model = popolo_models.OriginalEducationLevel 120 | show_change_link = True 121 | extra = 0 122 | max_num = 10 123 | readonly_fields = ("name",) 124 | 125 | def has_add_permission(self, request, obj=None): 126 | return False 127 | 128 | 129 | class EducationLevelAdmin(admin.ModelAdmin): 130 | model = popolo_models.EducationLevel 131 | list_display = ("name",) 132 | inlines = (IdentifiersInline, OriginalEducationInline) 133 | 134 | 135 | class OriginalEducationLevelAdmin(admin.ModelAdmin): 136 | model = popolo_models.OriginalEducationLevel 137 | list_display = ("name", "normalized_education_level") 138 | list_filter = ("normalized_education_level",) 139 | search_fields = ("name", "normalized_education_level__name") 140 | formfield_overrides = {models.CharField: {"widget": TextInput(attrs={"size": "120"})}} 141 | 142 | 143 | class OriginalProfessionInline(admin.TabularInline): 144 | model = popolo_models.OriginalProfession 145 | show_change_link = True 146 | extra = 0 147 | max_num = 10 148 | readonly_fields = ("name",) 149 | 150 | def has_add_permission(self, request, obj=None): 151 | return False 152 | 153 | 154 | class ProfessionAdmin(admin.ModelAdmin): 155 | model = popolo_models.Profession 156 | list_display = ("name",) 157 | inlines = (IdentifiersInline, OriginalProfessionInline) 158 | 159 | 160 | class OriginalProfessionAdmin(admin.ModelAdmin): 161 | model = popolo_models.OriginalProfession 162 | list_display = ("name", "normalized_profession") 163 | list_filter = ("normalized_profession",) 164 | search_fields = ("name", "normalized_profession__name") 165 | formfield_overrides = {models.CharField: {"widget": TextInput(attrs={"size": "120"})}} 166 | 167 | 168 | class AreaAdmin(admin.ModelAdmin): 169 | model = popolo_models.Area 170 | list_display = ("name", "identifier", "classification", "inhabitants", "start_date", "end_date") 171 | fields = ( 172 | "name", "identifier", "classification", "istat_classification", 173 | "start_date", "end_date", ("gps_lat", "gps_lon"), "geometry", 174 | ) 175 | readonly_fields = ("gps_lat", "gps_lon", ) 176 | list_filter = ("classification", "istat_classification") 177 | search_fields = ("name", "identifier", "identifiers__identifier") 178 | 179 | formfield_overrides = { 180 | MultiPolygonField: {"widget": forms.OSMWidget(attrs={"map_width": 600, "map_height": 400})} 181 | } 182 | 183 | 184 | class PersonAdmin(admin.ModelAdmin): 185 | model = popolo_models.Person 186 | list_display = ("name", "birth_date", "birth_location") 187 | search_fields = ("name", "identifiers__identifier") 188 | exclude = ("original_profession", "original_education_level", "birth_location_area") 189 | 190 | 191 | class OrganizationAdmin(admin.ModelAdmin): 192 | model = popolo_models.Organization 193 | list_display = ("name", "start_date") 194 | search_fields = ("name", "identifiers__identifier") 195 | ordering = ['name', ] 196 | exclude = ("area", "parent", "new_orgs") 197 | readonly_fields = fields = ( 198 | "name", 199 | "start_date", 200 | "end_date", 201 | "end_reason", 202 | "identifier", 203 | "classification", 204 | "thematic_classification", 205 | "abstract", 206 | "description", 207 | "image", 208 | ) 209 | 210 | 211 | class OrganizationRelationshipAdmin(admin.ModelAdmin): 212 | model = popolo_models.OrganizationRelationship 213 | list_display = ("source_organization", "dest_organization", "weight") 214 | search_fields = ("source_organization__name", "dest_organization__name") 215 | raw_id_fields = ("source_organization", "dest_organization") 216 | list_filter = ("classification",) 217 | 218 | 219 | def register(): 220 | """Register all the admin classes. """ 221 | for model_class, admin_class in { 222 | (popolo_models.Area, AreaAdmin), 223 | (popolo_models.Person, PersonAdmin), 224 | (popolo_models.Organization, OrganizationAdmin), 225 | (popolo_models.RoleType, RoleTypeAdmin), 226 | (popolo_models.Post, PostAdmin), 227 | (popolo_models.Membership, MembershipAdmin), 228 | (popolo_models.Ownership, OwnershipAdmin), 229 | (popolo_models.Classification, ClassificationAdmin), 230 | (popolo_models.EducationLevel, EducationLevelAdmin), 231 | (popolo_models.OriginalEducationLevel, OriginalEducationLevelAdmin), 232 | (popolo_models.Profession, ProfessionAdmin), 233 | (popolo_models.OriginalProfession, OriginalProfessionAdmin), 234 | (popolo_models.OrganizationRelationship, OrganizationRelationshipAdmin), 235 | }: 236 | admin.site.register(model_class, admin_class) 237 | 238 | admin.site.register(popolo_models.Language) 239 | -------------------------------------------------------------------------------- /popolo/querysets.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Tuple 2 | 3 | from django.db.models import Q 4 | 5 | __author__ = "guglielmo" 6 | 7 | from django.db import models, transaction 8 | from datetime import datetime 9 | 10 | 11 | class PopoloQueryset(models.query.QuerySet): 12 | def strategic_update_or_create( 13 | self, defaults: Dict = None, strategy: str = "overwrite", **kwargs 14 | ) -> Tuple[models.Model, bool]: 15 | 16 | """ 17 | Look up an object with the given kwargs, updating one with defaults 18 | if it exists, otherwise create a new one. 19 | 20 | Return a tuple (object, created), where created is a boolean 21 | specifying whether an object was created. 22 | 23 | Patched version of `update_or_create` which allows different update strategy. 24 | 25 | :param defaults: a dictionary of (field, value) pairs used to update the object 26 | :param strategy: 27 | - `overwrite`: (default) Normal `update_or_create` behaviour. 28 | - `keep_old`: Only update fields which do not have an already set-value. 29 | In other words, just update fields which are non-null. 30 | :param kwargs: Fields used to fetch the object. 31 | May be empty if your model has defaults for all fields. 32 | :return: a tuple (object, created), where created is a boolean 33 | specifying whether an object was created. 34 | """ 35 | 36 | self._for_write = True 37 | with transaction.atomic(using=self.db): 38 | try: 39 | obj = self.select_for_update().get(**kwargs) 40 | except self.model.DoesNotExist: 41 | params = self._extract_model_params(defaults, **kwargs) 42 | # Lock the row so that a concurrent update is blocked until 43 | # after update_or_create() has performed its save. 44 | obj, created = self._create_object_from_params(kwargs, params, lock=True) 45 | if created: 46 | return obj, created 47 | for k, v in defaults.items(): 48 | if strategy == "keep_old" and getattr(obj, k): 49 | continue # Do not update already set attribute 50 | 51 | setattr(obj, k, v() if callable(v) else v) 52 | obj.save(using=self.db) 53 | return obj, False 54 | 55 | 56 | class DateframeableQuerySet(models.query.QuerySet): 57 | """ 58 | A custom ``QuerySet`` allowing easy retrieval of current, past and future 59 | instances 60 | of a Dateframeable model. 61 | 62 | Here, a *Dateframeable model* denotes a model class having an associated 63 | date range. 64 | 65 | We assume that the date range is described by two ``Char`` fields 66 | named ``start_date`` and ``end_date``, respectively, 67 | whose validation pattern is: "^[0-9]{4}(-[0-9]{2}){0,2}$", 68 | in order to represent partial dates. 69 | """ 70 | 71 | def past(self, moment=None): 72 | """ 73 | Return a QuerySet containing the *past* instances of the model 74 | (i.e. those having an end date which is in the past). 75 | """ 76 | if moment is None: 77 | moment = datetime.strftime(datetime.now(), "%Y-%m-%d") 78 | return self.filter(end_date__lte=moment) 79 | 80 | def future(self, moment=None): 81 | """ 82 | Return a QuerySet containing the *future* instances of the model 83 | (i.e. those having a start date which is in the future). 84 | """ 85 | if moment is None: 86 | moment = datetime.strftime(datetime.now(), "%Y-%m-%d") 87 | return self.filter(start_date__gte=moment) 88 | 89 | def current(self, moment=None): 90 | """ 91 | Return a QuerySet containing the *current* instances of the model 92 | at the given moment in time, if the parameter is spcified 93 | now if it is not 94 | @moment - is a string, representing a date in the YYYY-MM-DD format 95 | (i.e. those for which the moment date-time lies within their 96 | associated time range). 97 | """ 98 | if moment is None: 99 | moment = datetime.strftime(datetime.now(), "%Y-%m-%d") 100 | 101 | return self.filter( 102 | (Q(start_date__lte=moment) | Q(start_date__isnull=True)) 103 | & (Q(end_date__gte=moment) | Q(end_date__isnull=True)) 104 | ) 105 | 106 | 107 | class PersonQuerySet(DateframeableQuerySet): 108 | pass 109 | 110 | 111 | class OrganizationQuerySet(DateframeableQuerySet): 112 | def municipalities(self): 113 | return self.filter( 114 | classifications__classification__scheme="FORMA_GIURIDICA_OP", 115 | classifications__classification__descr="Comune", 116 | ) 117 | 118 | def comuni(self): 119 | return self.municipalities() 120 | 121 | def metropolitan_areas(self): 122 | return self.filter( 123 | classifications__classification__scheme="FORMA_GIURIDICA_OP", 124 | classifications__classification__descr="Città metropolitana", 125 | ) 126 | 127 | def metropoli(self): 128 | return self.metropolitan_areas() 129 | 130 | def provinces(self): 131 | return self.filter( 132 | classifications__classification__scheme="FORMA_GIURIDICA_OP", 133 | classifications__classification__descr="Provincia", 134 | ) 135 | 136 | def province(self): 137 | return self.provinces() 138 | 139 | def provincie(self): 140 | return self.provinces() 141 | 142 | def regions(self): 143 | return self.filter( 144 | classifications__classification__scheme="FORMA_GIURIDICA_OP", 145 | classifications__classification__descr="Regione", 146 | ) 147 | 148 | def regioni(self): 149 | return self.regions() 150 | 151 | def giunte_regionali(self): 152 | return self.filter( 153 | classifications__classification__scheme="FORMA_GIURIDICA_OP", 154 | classifications__classification__descr="Giunta regionale", 155 | ) 156 | 157 | def giunte_provinciali(self): 158 | return self.filter( 159 | classifications__classification__scheme="FORMA_GIURIDICA_OP", 160 | classifications__classification__descr="Giunta provinciale", 161 | ) 162 | 163 | def giunte_comunali(self): 164 | return self.filter( 165 | classifications__classification__scheme="FORMA_GIURIDICA_OP", 166 | classifications__classification__descr="Giunta comunale", 167 | ) 168 | 169 | def conferenze_metropolitane(self): 170 | return self.filter( 171 | classifications__classification__scheme="FORMA_GIURIDICA_OP", 172 | classifications__classification__descr="Conferenza metropolitana", 173 | ) 174 | 175 | def consigli_regionali(self): 176 | return self.filter( 177 | classifications__classification__scheme="FORMA_GIURIDICA_OP", 178 | classifications__classification__descr="Consiglio regionale", 179 | ) 180 | 181 | def consigli_provinciali(self): 182 | return self.filter( 183 | classifications__classification__scheme="FORMA_GIURIDICA_OP", 184 | classifications__classification__descr="Consiglio provinciale", 185 | ) 186 | 187 | def consigli_comunali(self): 188 | return self.filter( 189 | classifications__classification__scheme="FORMA_GIURIDICA_OP", 190 | classifications__classification__descr="Consiglio comunale", 191 | ) 192 | 193 | def consigli_metropolitane(self): 194 | return self.filter( 195 | classifications__classification__scheme="FORMA_GIURIDICA_OP", 196 | classifications__classification__descr="Consiglio metropolitano", 197 | ) 198 | 199 | 200 | class PostQuerySet(DateframeableQuerySet): 201 | pass 202 | 203 | 204 | class MembershipQuerySet(DateframeableQuerySet): 205 | pass 206 | 207 | 208 | class OwnershipQuerySet(DateframeableQuerySet): 209 | pass 210 | 211 | 212 | class ContactDetailQuerySet(DateframeableQuerySet): 213 | pass 214 | 215 | 216 | class OtherNameQuerySet(DateframeableQuerySet): 217 | pass 218 | 219 | 220 | class PersonalRelationshipQuerySet(DateframeableQuerySet): 221 | pass 222 | 223 | 224 | class OrganizationRelationshipQuerySet(DateframeableQuerySet): 225 | pass 226 | 227 | 228 | class KeyEventQuerySet(PopoloQueryset, DateframeableQuerySet): 229 | pass 230 | 231 | 232 | class AreaQuerySet(DateframeableQuerySet): 233 | def municipalities(self): 234 | return self.filter(istat_classification=self.model.ISTAT_CLASSIFICATIONS.comune) 235 | 236 | def comuni(self): 237 | return self.municipalities() 238 | 239 | def metropolitan_areas(self): 240 | return self.filter(istat_classification=self.model.ISTAT_CLASSIFICATIONS.metro) 241 | 242 | def metropoli(self): 243 | return self.metropolitan_areas() 244 | 245 | def provinces(self): 246 | return self.filter(istat_classification=self.model.ISTAT_CLASSIFICATIONS.provincia) 247 | 248 | def province(self): 249 | return self.provinces() 250 | 251 | def regions(self): 252 | return self.filter(istat_classification=self.model.ISTAT_CLASSIFICATIONS.regione) 253 | 254 | def regioni(self): 255 | return self.regions() 256 | 257 | def macro_areas(self): 258 | return self.filter(istat_classification=self.model.ISTAT_CLASSIFICATIONS.ripartizione) 259 | 260 | def ripartizioni(self): 261 | return self.macro_areas() 262 | 263 | 264 | class AreaRelationshipQuerySet(DateframeableQuerySet): 265 | pass 266 | 267 | 268 | class IdentifierQuerySet(DateframeableQuerySet): 269 | pass 270 | 271 | 272 | class ClassificationQuerySet(DateframeableQuerySet): 273 | pass 274 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ### Changed 10 | - internal areas added to choices in Area and AreaRelationship models 11 | 12 | 13 | ## [3.0.3] 14 | 15 | ### Changed 16 | Added Linkedin contact type 17 | 18 | ## [3.0.2] 19 | ### Changed 20 | Arearelationships can now be of type DEP, to account for countries depending on other countries (and areas dependencies, in general) 21 | 22 | ## [3.0.1] 23 | 24 | ### Changed 25 | Migrations reset to __initial__ only. 26 | 27 | ### Added 28 | ``.gitlab-ci-yml`` added with flake8 syntax tests and release on pypi (with test) 29 | 30 | ## [3.0.0] 31 | The main development focuses have been to keep up with latest Django 32 | versions and to "modernize" the code base, adopting latest Python features (like type hinting), and doing some serious 33 | housekeeping. Python 2 is no longer supported. 34 | This release also implements a lot of new models which are not part of the Popolo specification (mildly out of scope), 35 | but we needed them in some projects which make use this package. Those new models can be safely ignored, and they could 36 | also be removed in the future, as we are considering the option of entirely decoupling them from `django-popolo`. 37 | That would probably be the right choice, but would also require a lot of development time which we currently don't have. 38 | 39 | Below a semi-complete list of changes. 40 | 41 | ### Added 42 | - Django 2.x support. 43 | - Mild type hinting 44 | - `Area.geometry` geo-spatial field (requires GeoDjango). 45 | - `Area.coordinates` property. 46 | - `RoleType` model to map structured roles for posts (off-spec). 47 | - Shortcuts in Organization QuerySets to extract institutions and organs (IT locale only). 48 | - Add `Makefile` 49 | 50 | 51 | ### Changed 52 | - `add_classification` can now accept `allow_same_scheme` parameter, to allow multiple classifications with the same scheme 53 | - Person can now receive `classifications`. 54 | - Target latest 2 Django LTS versions (1.11 and 2.2 at the moment) 55 | - Target the second-last Python version (3.7 at the moment) 56 | - Require GeoDjango 57 | - Use F-strings when possible 58 | - Tests now cover all methods implemented in the models (possibly). 59 | - original and normalized education levels and profession names are unique. 60 | - original and normalized education levels and profession are now storable. 61 | - `Organization` has a `thematic_classification` field (off-spec). 62 | - `add_classification` and `add_classification_rel` methods decoupled. 63 | - `LinkShortcutsMixin.update_links` method implemented. 64 | - `SourceShortcutsMixin.update_sources` method implemented. 65 | - `ContactDetailShortcutsMixin.update_contact_details` method implemented. 66 | - Add `AKAs` choice to `OtherName.othername_type` choices. 67 | - Adopted `factory_boy` package to generate model instances in tests. 68 | - Implemented test "factories" for the main models (see `popolo.tests.factories` module). 69 | - ClassificationShortcutMixin methods adjusted to work with nested Classification objects. 70 | - Re-worked `update_classifications` method in ClassificationMixin. 71 | - `update_other_names` and `update_identifiers` mixins implemented. 72 | - `Membership.label` field max length increased to 512 characters. 73 | - `Membership` class got methods to compute related apical roles and electoral events. 74 | - ``get_apicals`` 75 | - ``get_electoral_event`` 76 | - ``this_and_next_electoral_events``, 77 | 78 | ### Removed 79 | - **Drop Python 2 support** 80 | - `behaviors.models.Permalink.get_absolute_url` method removed. It was unused and relied on deprecated Django APIs. 81 | - `@python_2_unicode_compatible` decorator in model classes (also removed from version 3.0 of Django). 82 | - `management` sub-package (including Popit importer, which was broken anyway) is removed. 83 | 84 | ### Deprecated 85 | - `Area.geom` field (superseded by `Area.geometry`). 86 | Read-only backward compatibility is provided by `geom` property. 87 | - `Area.gps_lat` field (superseded by `Area.geometry`). 88 | Read-only backward compatibility is provided by `gps_lat` property. 89 | - `Area.gps_lon` field (superseded by `Area.geometry`). 90 | Read-only backward compatibility is provided by `gps_lon` property. 91 | 92 | 93 | ## [2.4.0] 94 | 95 | ### Added 96 | - `electoral_result` foreign key added to `Membership`. 97 | 98 | ## [2.3.0] 99 | 100 | ### Added 101 | - `Profession` and `EducationLevel` models added. 102 | - `profession` and `education_level` foreign keys added to `Person`, referring to `Profession` and `EducationLevel`. 103 | 104 | ## [2.2.1] 105 | 106 | ### Fixed 107 | - Classification code and descr fields can now be null, in order to use this class for tagging. 108 | - constituency_descr_tmp and electoral_list_descr_tmp moved from Area to Membership. 109 | - role fields max_length in Membership and Post objects increased to 512. 110 | - get_former_parents and get_former_children moment_date parameter can be null. 111 | - get_former_parents and get_former_children FIP classification_type corrected. 112 | 113 | ## [2.2.0] 114 | 115 | ### Changed 116 | - `constituency_descr` and `electoral_list_descr` fields 117 | **temporarily** added to Membership in order to store relevant 118 | information contained in the Openpolitici dataset (will be removed 119 | when the whole subject will be refactored). 120 | - Multiple overlapping memberships are possible if the `allow_overlap` 121 | flag is specified. 122 | 123 | ## [2.1.0] 124 | 125 | ### Added 126 | - `birth_location` and `birth_location_area` fields added to Person. 127 | - person helper methods to add roles and memberships. 128 | - helper methods of the previous pointcheck for overlapping dates, in order 129 | to allow duplicate roles or memberships for the same Organizations and Posts. 130 | 131 | ## 2.0.1 [YANKED] 132 | 133 | ### Fixed 134 | - `str` method added to LinkRel, SourceRel and ClassificationRel classes. 135 | - fixed ordering of queryset results in determining overlapping dates 136 | in `add_other_names`, that resulted in tests failing on some 137 | platforms. 138 | - `str` for ClassificationRel, LinkRel and SourceRel now correctly 139 | ouput a string, not a tuple. 140 | 141 | 142 | ## [2.0.0] 143 | 144 | Compatibility with Popit importer broken! 145 | Due to changes in how Links and Sources are modeled, the Popit 146 | importer is not working any longer. 147 | 148 | ### Added 149 | - Area class refined 150 | - Area class shortcuts methods implemented and tested 151 | - AreaRelationship class added to map generic relationships among Areas 152 | - Classification added for 153 | - Links, Sources and Classifications are related to generic objects through *Rel…classes, in order to minimize 154 | unnecessary repetitions in the tables. 155 | - Shortcuts to filter type of areas added to AreaQuerySet. 156 | 157 | ### Changed 158 | - Common methods and testcases oved into Dateframeable and DateframeableTestCase 159 | - Unicity of ContactDetail, OtherName and Identifier is enforced in the `add_x` shortcut methods. Identifiers validation 160 | take into account overlapping dates intervals. Overlapping identical values are merged into a single identifier whose 161 | start and end dates are extended. 162 | - IdentifierQueryset added to handle date filters for identifiers 163 | - `popolo.utils.PartialDate` and `popolo.utils.PartialDatesInterval` added to handle partial dates computations and 164 | comparisons. 165 | - opdm-service project layout now follows our template at https://gitlab.depp.it/openpolis/django-project-template. 166 | - `add_identifiers` tests now encompass many use cases. 167 | 168 | ### Removed 169 | - The importers were removed, due to broken backward compatibility introduced in the models. 170 | 171 | ## 1.2.1 - 2017-09-20 172 | 173 | ### Added 174 | - ElectoralEvent shortcuts to create ElectoralResults added. 175 | 176 | ## 1.2.0 - 2017-09-20 177 | 178 | ### Added 179 | - ElectoralEvent and ElectoralResult classes added. 180 | - personal relationships modelling added. 181 | 182 | ### Changed 183 | - models and tests refactored in order to distribute shortcuts and tests through mixins. 184 | - source added to Identifier. 185 | - tests now encompass on_behalf_of relations. 186 | 187 | ## 1.1.0 - 2017-09-16 188 | 189 | ### Added 190 | - Event class added to instances. 191 | 192 | ### Changed 193 | - Main entities primary keys are now numerical IDs. 194 | - django-popolo models aligned to popolo schemas. 195 | 196 | ## 1.0.1 - 2017-09-14 197 | 198 | ### Added 199 | - Italian translation in models and admin. 200 | 201 | ### Changed 202 | - Area and inline AreaI18Names admin classes. 203 | - `models.py` code readability increased. 204 | 205 | ## 1.0.0 - 2017-09-13 206 | 207 | ### Added 208 | - added tests for importer. 209 | 210 | ### Changed 211 | - `popit` importer substituted by the `popolo_json` importer. 212 | - simplified `popolo_create_from_popit` management task. 213 | - updated travis matrix to latest python (3.6) and django (1.11) releases. 214 | 215 | ### Fixed 216 | - `urls.py` is now compatible with django 1.8>, and does not cause errors in django 1.11. 217 | 218 | [Unreleased]: https://gitlab.depp.it/openpolis/django-popolo/compare/v3.0.0...master 219 | [3.0.0]: https://gitlab.depp.it/openpolis/django-popolo/compare/v2.4.0...v3.0.0 220 | [2.4.0]: https://gitlab.depp.it/openpolis/django-popolo/compare/v2.3.0...v2.4.0 221 | [2.3.0]: https://gitlab.depp.it/openpolis/django-popolo/compare/v2.2.1...v2.3.0 222 | [2.2.1]: https://gitlab.depp.it/openpolis/django-popolo/compare/v2.2.0...v2.2.1 223 | [2.2.0]: https://gitlab.depp.it/openpolis/django-popolo/compare/v2.1.0...v2.2.0 224 | [2.1.0]: https://gitlab.depp.it/openpolis/django-popolo/compare/v2.0.0...v2.1.0 225 | [2.0.0]: https://gitlab.depp.it/openpolis/django-popolo/compare/v1.0...v2.0.0 226 | -------------------------------------------------------------------------------- /popolo/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime as dt 2 | from datetime import timedelta 3 | import operator 4 | import sys 5 | 6 | from django.utils.translation import ugettext_lazy as _ 7 | from popolo.exceptions import PartialDateException 8 | 9 | 10 | class PartialDatesInterval(object): 11 | """Class used to represent an interval among two ``PartialDate`` instances 12 | """ 13 | 14 | def __init__(self, start, end): 15 | """Initialize the instance. 16 | 17 | The interval can be instantiated both with 18 | `PartialDate` instances and with strings, using the formats 19 | allowed in `PartialDate`. 20 | 21 | :param start: the starting inteval date 22 | :param end: the ending interval date 23 | :type start Union[PartialDate, str]: 24 | :type end Union[PartialDate, str]: 25 | """ 26 | if sys.version_info >= (3, 0, 0): 27 | 28 | if isinstance(start, PartialDate): 29 | self.start = start 30 | elif isinstance(start, str) or isinstance(start, bytes) or start is None: 31 | self.start = PartialDate(start) 32 | else: 33 | raise PartialDateException(f"Class {type(start)} not allowed here") 34 | 35 | if isinstance(end, PartialDate): 36 | self.end = end 37 | elif isinstance(end, str) or isinstance(end, bytes) or end is None: 38 | self.end = PartialDate(end) 39 | else: 40 | raise PartialDateException(f"Class {type(end)} not allowed here") 41 | 42 | else: 43 | 44 | if isinstance(start, PartialDate): 45 | self.start = start 46 | elif isinstance(start, str) or start is None: 47 | self.start = PartialDate(start) 48 | else: 49 | raise PartialDateException(f"Class {type(start)} not allowed here") 50 | 51 | if isinstance(end, PartialDate): 52 | self.end = end 53 | elif isinstance(end, str) or end is None: 54 | self.end = PartialDate(end) 55 | else: 56 | raise PartialDateException(f"Class {type(end)} not allowed here") 57 | 58 | def __eq__(self, other): 59 | """Equality operator for PartialDateInterval 60 | 61 | :param other: 62 | :return: 63 | """ 64 | 65 | if self.start == other.start and self.end == other.end: 66 | return True 67 | else: 68 | return False 69 | 70 | def __repr__(self): 71 | return self.__str__() 72 | 73 | def __str__(self): 74 | if self.start is None: 75 | start = _("forever") 76 | else: 77 | start = self.start 78 | if self.end is None: 79 | end = _("forever") 80 | else: 81 | end = self.end 82 | 83 | return f"{start} => {end}" 84 | 85 | 86 | class PartialDate(object): 87 | """Class that holds partial date and is able to compare among them. 88 | 89 | An instance is created by passing the string representation:: 90 | 91 | a = PartialDate('2010-01') 92 | 93 | a.date 94 | > '2010-01' 95 | 96 | During the initialization, the attribute ``date_as_dt`` is instantiated:: 97 | 98 | a.date_as_dt 99 | > datetime.datetime(2010, 1, 1, 0, 0) 100 | 101 | The class allows to switch seamlessly, between the partial textual 102 | representations of the date (2017-01-04, 2016-04, 2015) and 103 | their datetime instances. 104 | 105 | Null values are considered during comparisons, raising 106 | ``PartialDateException`` whenever comparisons are meaningless. 107 | 108 | Partial dates are pointed to the first days in the month, or year. 109 | 110 | Comparison operators are overrided, so that the textual representations 111 | are always compared. 112 | 113 | The ``intervals_overlap`` class method allows to compare two 114 | ``PartialDatesInterval`` instances and return the n. of days of overlap. 115 | 116 | 117 | """ 118 | 119 | d_fmt = "%Y-%m-%d" 120 | m_fmt = "%Y-%m" 121 | y_fmt = "%Y" 122 | 123 | HUGE_OVERLAP = 999999 124 | 125 | @classmethod 126 | def intervals_overlap(cls, a: PartialDatesInterval, b: PartialDatesInterval): 127 | """Return the number of overlapping days between two intervals. 128 | 129 | Intervals are expressed as instances of the ``PartialDatesInterval`` 130 | namedtuple. 131 | 132 | Null start and end dates hold special meaning and the overlapping 133 | computation takes it into account. 134 | 135 | Two interval are overlapping when the returned value is greater than 0. 136 | 137 | When the two starting dates are both null, then the two intervals 138 | return a ``HUGE_OVERLAP`` value (999999). 139 | 140 | :param a: PartialDatesInterval 141 | :param b: PartialDatesInterval 142 | :return: integer 143 | """ 144 | 145 | if not a.start.date and not b.start.date: 146 | return cls.HUGE_OVERLAP 147 | elif a.start.date and b.start.date: 148 | latest_start = max(a.start, b.start) 149 | elif not a.start.date: 150 | latest_start = b.start 151 | elif not b.start.date: 152 | latest_start = a.start 153 | 154 | if not a.end.date and not b.end.date: 155 | return cls.HUGE_OVERLAP 156 | elif a.end.date and b.end.date: 157 | earliest_end = min(a.end, b.end) 158 | elif not a.end.date: 159 | earliest_end = b.end 160 | elif not b.end.date: 161 | earliest_end = a.end 162 | 163 | overlap = (earliest_end - latest_start).days 164 | 165 | return overlap 166 | 167 | def __init__(self, date_string): 168 | """Initialize the instance, trying the various allowed format. 169 | 170 | If the string is not in one of the allowed format, then a 171 | ``PartialDateException`` is raised. 172 | 173 | The converted datetime instance is stored in the 174 | ``date_as_dt`` attribute. 175 | 176 | :param date_string: the date in one of the allowed formats. 177 | """ 178 | 179 | self.date = date_string 180 | 181 | if self.date: 182 | try: 183 | self.date_as_dt = dt.strptime(self.date, self.d_fmt) 184 | except ValueError: 185 | try: 186 | self.date_as_dt = dt.strptime(self.date, self.m_fmt) 187 | except ValueError: 188 | try: 189 | self.date_as_dt = dt.strptime(self.date, self.y_fmt) 190 | except ValueError: 191 | raise PartialDateException("Could not convert {0} into datetime".format(self.date)) 192 | else: 193 | self.date_as_dt = None 194 | 195 | def __sub__(self, other): 196 | """Overrides the `-` operator, so that: 197 | 198 | - in case ``other`` is a ``PartialDate`` instance, 199 | then a ``datetime.timedelta``, 200 | - in case ``other`` is a ``datetime.timedelta`` instance, then 201 | a ``PartialDate`` instance is returned. 202 | 203 | a = PartialDate('2010-01') 204 | b = PartialDate('2008-01') 205 | a - b 206 | > datetime.timedelta(731) 207 | 208 | :param other: the subtrahend 209 | :return: the PartialDate resulting from the subtraction 210 | :type other: Union[PartialDate, timedelta] 211 | :rtype: timedelta 212 | """ 213 | if isinstance(other, PartialDate): 214 | return self.date_as_dt - other.date_as_dt 215 | elif isinstance(other, timedelta): 216 | return self.date_as_dt - other 217 | else: 218 | raise PartialDateException("Instance not allowed for the subtrahend") 219 | 220 | def __add__(self, other): 221 | """override the *add* operation, 222 | so that a ``datetime.timedelta` addendum can be added 223 | 224 | :param other: the timedelta to be added 225 | :return: the result of the add operation 226 | :type other timedelta 227 | :rtype PartialDate 228 | """ 229 | if isinstance(other, timedelta): 230 | res_as_dt = self.date_as_dt + other 231 | return PartialDate(dt.strftime(res_as_dt, self.d_fmt)) 232 | else: 233 | raise PartialDateException("Instance not allowed for the addendum") 234 | 235 | def __eq__(self, other): 236 | """ 237 | 238 | :param other: 239 | :return: 240 | """ 241 | 242 | if other: 243 | return self.date == other.date 244 | else: 245 | return self.date is None 246 | 247 | def _compare(self, other, op): 248 | """ 249 | Overrides comparison operators. 250 | 251 | Raises an exception if one or both dates are null, 252 | as null dates are used with different meanings 253 | for start and end dates and the comparison does not make sense 254 | 255 | :param other: the PartialDate instance to be compared with 256 | :return: boolean 257 | """ 258 | if self.date and other.date: 259 | return op(self.date, other.date) 260 | else: 261 | raise PartialDateException("Could not compare null dates") 262 | 263 | def __gt__(self, other): 264 | return self._compare(other, operator.gt) 265 | 266 | def __ge__(self, other): 267 | return self._compare(other, operator.ge) 268 | 269 | def __lt__(self, other): 270 | return self._compare(other, operator.lt) 271 | 272 | def __le__(self, other): 273 | return self._compare(other, operator.le) 274 | 275 | def __repr__(self): 276 | return self.__str__() 277 | 278 | def __str__(self): 279 | if self.date is None: 280 | return "None" 281 | else: 282 | return self.date 283 | -------------------------------------------------------------------------------- /popolo/locale/it/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2017-09-16 01:20+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: Guglielmo Celata \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: admin.py:24 22 | msgid "Biography" 23 | msgstr "Dati biografici" 24 | 25 | #: admin.py:28 26 | msgid "Honorifics" 27 | msgstr "Titoli onorifici" 28 | 29 | #: admin.py:32 30 | msgid "Demography" 31 | msgstr "Dati demografici" 32 | 33 | #: admin.py:36 34 | msgid "Special Names" 35 | msgstr "Nomi speciali" 36 | 37 | #: admin.py:41 admin.py:88 38 | msgid "Advanced options" 39 | msgstr "Opzioni avanzate" 40 | 41 | #: admin.py:50 42 | msgid "Member" 43 | msgstr "Membro" 44 | 45 | #: admin.py:51 46 | msgid "Members of this organization" 47 | msgstr "Membro di questa organizzazione" 48 | 49 | #: admin.py:56 50 | msgid "Proxy member" 51 | msgstr "Membro che agisce per conto di questa organizzazione" 52 | 53 | #: admin.py:57 54 | msgid "Members acting on behalf of this organization" 55 | msgstr "Membri che agiscono per conto di questa organizzazione" 56 | 57 | #: admin.py:67 admin.py:84 admin.py:114 58 | msgid "Details" 59 | msgstr "Dettagli" 60 | 61 | #: behaviors/models.py:66 models.py:966 62 | msgid "start date" 63 | msgstr "data inizio" 64 | 65 | #: behaviors/models.py:68 66 | msgid "The date when the validity of the item starts" 67 | msgstr "Data di inizio validità del dato" 68 | 69 | #: behaviors/models.py:71 models.py:989 70 | msgid "end date" 71 | msgstr "data fine" 72 | 73 | #: behaviors/models.py:73 74 | msgid "The date when the validity of the item ends" 75 | msgstr "Data di fine validità del dato" 76 | 77 | #: behaviors/models.py:85 78 | msgid "creation time" 79 | msgstr "momento creazione" 80 | 81 | #: behaviors/models.py:86 82 | msgid "last modification time" 83 | msgstr "momento ultima modifica" 84 | 85 | #: models.py:55 models.py:238 models.py:692 models.py:820 models.py:847 86 | #: models.py:929 models.py:950 87 | msgid "name" 88 | msgstr "nome" 89 | 90 | #: models.py:57 91 | msgid "A person's preferred full name" 92 | msgstr "Il nome completo di una persona" 93 | 94 | #: models.py:64 95 | msgid "Alternate or former names" 96 | msgstr "Nomi alternativi o usati in precedenza" 97 | 98 | #: models.py:71 99 | msgid "Issued identifiers" 100 | msgstr "Identificativi riconosciuti" 101 | 102 | #: models.py:75 103 | msgid "family name" 104 | msgstr "cognome" 105 | 106 | #: models.py:77 107 | msgid "One or more family names" 108 | msgstr "Uno o più cognomi" 109 | 110 | #: models.py:81 111 | msgid "given name" 112 | msgstr "Nome" 113 | 114 | #: models.py:83 115 | msgid "One or more primary given names" 116 | msgstr "Uno o più nomi" 117 | 118 | #: models.py:87 119 | msgid "additional name" 120 | msgstr "Nome aggiuntivo" 121 | 122 | #: models.py:89 123 | msgid "One or more secondary given names" 124 | msgstr "Uno o più nomi aggiuntivi" 125 | 126 | #: models.py:93 127 | msgid "honorific prefix" 128 | msgstr "titolo onorifico (prefisso)" 129 | 130 | #: models.py:95 131 | msgid "One or more honorifics preceding a person's name" 132 | msgstr "" 133 | "Uno o più prefissi di titoli onorifici che precedono il nome della persona" 134 | 135 | #: models.py:99 136 | msgid "honorific suffix" 137 | msgstr "titolo onorifico (suffisso)" 138 | 139 | #: models.py:101 140 | msgid "One or more honorifics following a person's name" 141 | msgstr "Uno o più suffissi di titoli onorifici" 142 | 143 | #: models.py:105 144 | msgid "patronymic name" 145 | msgstr "nome patronimico" 146 | 147 | #: models.py:107 148 | msgid "One or more patronymic names" 149 | msgstr "Uno o più nomi patronimici" 150 | 151 | #: models.py:111 152 | msgid "sort name" 153 | msgstr "nome per ordinamento" 154 | 155 | #: models.py:114 156 | msgid "A name to use in an lexicographically ordered list" 157 | msgstr "Nome da usare in una lista ordinata alfabeticamente" 158 | 159 | #: models.py:120 160 | msgid "email" 161 | msgstr "" 162 | 163 | #: models.py:122 164 | msgid "A preferred email address" 165 | msgstr "Indirizzo email principale" 166 | 167 | #: models.py:126 168 | msgid "gender" 169 | msgstr "genere" 170 | 171 | #: models.py:128 172 | msgid "A gender" 173 | msgstr "Il genere, es: Uomo, Donna, Agender, Transgender, ..." 174 | 175 | #: models.py:132 176 | msgid "birth date" 177 | msgstr "data di nascita" 178 | 179 | #: models.py:134 180 | msgid "A date of birth" 181 | msgstr "La data di nascita" 182 | 183 | #: models.py:138 184 | msgid "death date" 185 | msgstr "data del decesso" 186 | 187 | #: models.py:140 188 | msgid "A date of death" 189 | msgstr "La data dell'eventuale decesso" 190 | 191 | #: models.py:144 models.py:327 192 | msgid "image" 193 | msgstr "immagine" 194 | 195 | #: models.py:146 196 | msgid "A URL of a head shot" 197 | msgstr "La URL dell'immagine del volto" 198 | 199 | #: models.py:150 200 | msgid "summary" 201 | msgstr "riassunto" 202 | 203 | #: models.py:152 204 | msgid "A one-line account of a person's life" 205 | msgstr "La vita della persona in una linea" 206 | 207 | #: models.py:156 models.py:291 208 | msgid "biography" 209 | msgstr "biografia" 210 | 211 | #: models.py:159 212 | msgid "An extended account of a person's life" 213 | msgstr "Racconto più dettagliato della vita della persona" 214 | 215 | #: models.py:164 216 | msgid "national identity" 217 | msgstr "identità nazionale" 218 | 219 | #: models.py:166 220 | msgid "A national identity" 221 | msgstr "L'identità nazionale" 222 | 223 | #: models.py:191 models.py:510 models.py:519 224 | msgid "Person" 225 | msgstr "Persona" 226 | 227 | #: models.py:192 228 | msgid "People" 229 | msgstr "Persone" 230 | 231 | #: models.py:240 232 | msgid "A primary name, e.g. a legally recognized name" 233 | msgstr "Il nome principale, o quello legalmente valido" 234 | 235 | #: models.py:257 models.py:859 models.py:1040 236 | msgid "classification" 237 | msgstr "classificazione" 238 | 239 | #: models.py:259 240 | msgid "An organization category, e.g. committee" 241 | msgstr "" 242 | "La categoria di organizzazione (esempi: assemblea elettiva, partito " 243 | "politico, commissione, società privata)" 244 | 245 | #: models.py:267 models.py:879 models.py:1066 246 | msgid "Parent" 247 | msgstr "Genitore" 248 | 249 | #: models.py:269 250 | msgid "The organization that contains this organization" 251 | msgstr "L'organizzazione che contiene questa organizzazione" 252 | 253 | #: models.py:280 254 | msgid "The geographic area to which this organization is related" 255 | msgstr "L'area geografica cui questa organizzazione è collegata" 256 | 257 | #: models.py:285 258 | msgid "abstract" 259 | msgstr "sommario" 260 | 261 | #: models.py:287 262 | msgid "A one-line description of an organization" 263 | msgstr "La descrizione dell'organizzazion in una linea" 264 | 265 | #: models.py:293 266 | msgid "An extended description of an organization" 267 | msgstr "La descrizione dettagliata dell'organizzazione" 268 | 269 | #: models.py:297 270 | msgid "founding date" 271 | msgstr "data di fondazione" 272 | 273 | #: models.py:308 274 | msgid "A date of founding" 275 | msgstr "La data di fondazione" 276 | 277 | #: models.py:312 278 | msgid "dissolution date" 279 | msgstr "data di scioglimento" 280 | 281 | #: models.py:323 282 | msgid "A date of dissolution" 283 | msgstr "La data di scioglimento eventuale" 284 | 285 | #: models.py:331 286 | msgid "A URL of an image, to identify the organization visually" 287 | msgstr "" 288 | "URL dell'immagine che identifica visualmente l'organizzazione (logo, stemma)" 289 | 290 | #: models.py:339 291 | msgid "Means of contacting the organization" 292 | msgstr "Riferimenti per contattare l'organizzazione" 293 | 294 | #: models.py:346 295 | msgid "URLs to documents about the organization" 296 | msgstr "URL a documenti online che riguardano l'organizzazione" 297 | 298 | #: models.py:352 models.py:1073 299 | msgid "URLs to source documents about the organization" 300 | msgstr "URL a fonti online che riguardano l'organizzazione" 301 | 302 | #: models.py:358 models.py:425 models.py:537 303 | msgid "Organization" 304 | msgstr "Organizzazione" 305 | 306 | #: models.py:359 307 | msgid "Organizations" 308 | msgstr "Organizzazioni" 309 | 310 | #: models.py:400 models.py:494 models.py:638 311 | msgid "label" 312 | msgstr "denominazione" 313 | 314 | #: models.py:402 315 | msgid "A label describing the post" 316 | msgstr "La denominazione che descrive l'impiego" 317 | 318 | #: models.py:406 319 | msgid "other label" 320 | msgstr "altre denominazioni" 321 | 322 | #: models.py:409 323 | msgid "An alternate label, such as an abbreviation" 324 | msgstr "La denominazione alternativa, es: abbreviazione (AD)" 325 | 326 | #: models.py:414 models.py:500 327 | msgid "role" 328 | msgstr "ruolo" 329 | 330 | #: models.py:417 331 | msgid "The function that the holder of the post fulfills" 332 | msgstr "La funzione che il detentore dell'impiego adempie" 333 | 334 | #: models.py:426 335 | msgid "The organization in which the post is held" 336 | msgstr "L'organizzazione in cui l'impiego è detenuto" 337 | 338 | #: models.py:434 models.py:571 339 | msgid "Area" 340 | msgstr "" 341 | 342 | #: models.py:435 models.py:572 343 | msgid "The geographic area to which the post is related" 344 | msgstr "L'area geografica cui l'impiego è collegato" 345 | 346 | #: models.py:442 347 | msgid "Means of contacting the holder of the post" 348 | msgstr "Mezi per contattare la persona che detiene l'impiego" 349 | 350 | #: models.py:448 351 | msgid "URLs to documents about the post" 352 | msgstr "URL a documenti online che riguardano l'impiego" 353 | 354 | #: models.py:454 355 | msgid "URLs to source documents about the post" 356 | msgstr "URL a fonti online che riguardano l'impiego" 357 | 358 | #: models.py:460 models.py:559 359 | msgid "Post" 360 | msgstr "Impiego" 361 | 362 | #: models.py:461 363 | msgid "Posts" 364 | msgstr "Impieghi" 365 | 366 | #: models.py:496 367 | msgid "A label describing the membership" 368 | msgstr "La denominazione che descrive la membership" 369 | 370 | #: models.py:502 371 | msgid "The role that the member fulfills in the organization" 372 | msgstr "Il ruolo ricoperto dalla persona nell'organizzazione" 373 | 374 | #: models.py:511 models.py:520 375 | msgid "The person who is a member of the organization" 376 | msgstr "La persona membro dell'organizzazione" 377 | 378 | #: models.py:539 379 | msgid "The organization in which the person or organization is a member" 380 | msgstr "L'organizzazione di cui la persona o organizzazion è membro" 381 | 382 | #: models.py:547 383 | msgid "On behalf of" 384 | msgstr "Per conto di" 385 | 386 | #: models.py:549 387 | msgid "" 388 | "The organization on whose behalf the person is a member of the organization" 389 | msgstr "" 390 | "L'organizzazion per conto della quale la persona è parte dell'organizzazione" 391 | 392 | #: models.py:561 393 | msgid "The post held by the person in the organization through this membership" 394 | msgstr "" 395 | "L'impiego detenuto dalla persona nell'organizzazione attraverso questa " 396 | "relazione" 397 | 398 | #: models.py:579 399 | msgid "Means of contacting the member of the organization" 400 | msgstr "Riferimenti per contattare la persona " 401 | 402 | #: models.py:585 403 | msgid "URLs to documents about the membership" 404 | msgstr "URL a documenti online che riguardano la membership" 405 | 406 | #: models.py:591 407 | msgid "URLs to source documents about the membership" 408 | msgstr "URL a fonti online che riguardano la membership" 409 | 410 | #: models.py:597 411 | msgid "Membership" 412 | msgstr "" 413 | 414 | #: models.py:598 415 | msgid "Memberships" 416 | msgstr "Membership" 417 | 418 | #: models.py:620 419 | msgid "Address" 420 | msgstr "Indirizzo" 421 | 422 | #: models.py:621 423 | msgid "Email" 424 | msgstr "" 425 | 426 | #: models.py:622 427 | msgid "Url" 428 | msgstr "" 429 | 430 | #: models.py:623 431 | msgid "Snail mail" 432 | msgstr "Indirizzo Postale" 433 | 434 | #: models.py:624 435 | msgid "Twitter" 436 | msgstr "" 437 | 438 | #: models.py:625 439 | msgid "Facebook" 440 | msgstr "" 441 | 442 | #: models.py:626 443 | msgid "Telephone" 444 | msgstr "Telefono fisso" 445 | 446 | #: models.py:627 447 | msgid "Mobile" 448 | msgstr "Telefono cellulare" 449 | 450 | #: models.py:628 451 | msgid "Text" 452 | msgstr "Testo" 453 | 454 | #: models.py:629 455 | msgid "Voice" 456 | msgstr "Voce" 457 | 458 | #: models.py:630 459 | msgid "Fax" 460 | msgstr "" 461 | 462 | #: models.py:631 463 | msgid "Cell" 464 | msgstr "Cellulare" 465 | 466 | #: models.py:632 467 | msgid "Video" 468 | msgstr "" 469 | 470 | #: models.py:633 471 | msgid "Pager" 472 | msgstr "" 473 | 474 | #: models.py:634 475 | msgid "Textphone" 476 | msgstr "Telefono testuale" 477 | 478 | #: models.py:640 479 | msgid "A human-readable label for the contact detail" 480 | msgstr "Denominazione leggibile per il contatto" 481 | 482 | #: models.py:644 483 | msgid "type" 484 | msgstr "tipo" 485 | 486 | #: models.py:647 487 | msgid "A type of medium, e.g. 'fax' or 'email'" 488 | msgstr "Il tipo di riferimento, es: 'fax' o 'email'" 489 | 490 | #: models.py:651 491 | msgid "value" 492 | msgstr "valore" 493 | 494 | #: models.py:653 495 | msgid "A value, e.g. a phone number or email address" 496 | msgstr "Il valore, es: il numero di telefono o l'indirizzo email" 497 | 498 | #: models.py:657 models.py:698 models.py:763 models.py:789 499 | msgid "note" 500 | msgstr "nota" 501 | 502 | #: models.py:660 503 | msgid "A note, e.g. for grouping contact details by physical location" 504 | msgstr "Una nota, es: per raggruppare contatti per località" 505 | 506 | #: models.py:667 models.py:901 507 | msgid "URLs to source documents about the contact detail" 508 | msgstr "URL a documenti online che riguardano il contatto" 509 | 510 | #: models.py:671 511 | msgid "Contact detail" 512 | msgstr "Contatto" 513 | 514 | #: models.py:672 515 | msgid "Contact details" 516 | msgstr "Contatti" 517 | 518 | #: models.py:694 519 | msgid "An alternate or former name" 520 | msgstr "Un nome alternativo o usato in precedenza" 521 | 522 | #: models.py:700 523 | msgid "A note, e.g. 'Birth name'" 524 | msgstr "Una nota, es: 'Nome alla nascita'" 525 | 526 | #: models.py:704 527 | msgid "source" 528 | msgstr "fonte" 529 | 530 | #: models.py:706 531 | msgid "The URL of the source where this information comes from" 532 | msgstr "La URL della fonte da dove è stata presa questa informazione" 533 | 534 | #: models.py:710 535 | msgid "Other name" 536 | msgstr "Altra denominazione" 537 | 538 | #: models.py:711 539 | msgid "Other names" 540 | msgstr "Altre denominazioni" 541 | 542 | #: models.py:731 models.py:853 543 | msgid "identifier" 544 | msgstr "identificativo" 545 | 546 | #: models.py:733 547 | msgid "An issued identifier, e.g. a DUNS number" 548 | msgstr "Un identificativo, es: il Codice Fiscale" 549 | 550 | #: models.py:737 551 | msgid "scheme" 552 | msgstr "schema" 553 | 554 | #: models.py:739 555 | msgid "An identifier scheme, e.g. DUNS" 556 | msgstr "Lo schema dell'identificativo, es: CODICE FISCALE" 557 | 558 | #: models.py:743 559 | msgid "Identifier" 560 | msgstr "identificativo" 561 | 562 | #: models.py:744 563 | msgid "Identifiers" 564 | msgstr "identificativo" 565 | 566 | #: models.py:757 models.py:783 567 | msgid "url" 568 | msgstr "" 569 | 570 | #: models.py:759 models.py:785 571 | msgid "A URL" 572 | msgstr "La URL" 573 | 574 | #: models.py:765 575 | msgid "A note, e.g. 'Wikipedia page'" 576 | msgstr "Una nota, es: 'pagina Wikipedia'" 577 | 578 | #: models.py:769 579 | msgid "Link" 580 | msgstr "Collegamento" 581 | 582 | #: models.py:770 583 | msgid "Links" 584 | msgstr "Collegamenti" 585 | 586 | #: models.py:791 587 | msgid "A note, e.g. 'Parliament website'" 588 | msgstr "Una nota, es: 'Sito del Parlamento'" 589 | 590 | #: models.py:795 591 | msgid "Source" 592 | msgstr "Fonte" 593 | 594 | #: models.py:796 595 | msgid "Sources" 596 | msgstr "Fonti" 597 | 598 | #: models.py:809 599 | msgid "dbpedia resource" 600 | msgstr "Risorsa DBPedia" 601 | 602 | #: models.py:811 603 | msgid "DbPedia URI of the resource" 604 | msgstr "URI DBPedia della risorsa" 605 | 606 | #: models.py:815 607 | msgid "iso639_1 code" 608 | msgstr "Codice iso639_1" 609 | 610 | #: models.py:817 611 | msgid "ISO 639_1 code, ex: en, it, de, fr, es, ..." 612 | msgstr "Codice ISO 639_1 della lingua, es: en, it, de, fr, es, ..." 613 | 614 | #: models.py:822 615 | msgid "English name of the language" 616 | msgstr "Denominazione della lingua in italiano" 617 | 618 | #: models.py:826 models.py:925 619 | msgid "Language" 620 | msgstr "Lingua" 621 | 622 | #: models.py:827 623 | msgid "Languages" 624 | msgstr "Lingue" 625 | 626 | #: models.py:849 627 | msgid "A primary name" 628 | msgstr "Il nome principale" 629 | 630 | #: models.py:855 631 | msgid "An issued identifier" 632 | msgstr "L'identificativo principale" 633 | 634 | #: models.py:861 635 | msgid "An area category, e.g. city" 636 | msgstr "La categoria di area, es: città" 637 | 638 | #: models.py:870 639 | msgid "Other issued identifiers (zip code, other useful codes, ...)" 640 | msgstr "Altri identificativi (CAP, Codice Catastale, Codice MinInt ...)" 641 | 642 | #: models.py:880 643 | msgid "The area that contains this area" 644 | msgstr "L'area che cointiene questa area" 645 | 646 | #: models.py:885 647 | msgid "geom" 648 | msgstr "areale geometrico" 649 | 650 | #: models.py:887 651 | msgid "A geometry" 652 | msgstr "La geometria (coordinate)" 653 | 654 | #: models.py:892 655 | msgid "inhabitants" 656 | msgstr "abitanti" 657 | 658 | #: models.py:894 659 | msgid "The total number of inhabitants" 660 | msgstr "Il numero totale di abitanti" 661 | 662 | #: models.py:905 663 | msgid "Geographic Area" 664 | msgstr "Area geografica" 665 | 666 | #: models.py:906 667 | msgid "Geographic Areas" 668 | msgstr "Aree geografiche" 669 | 670 | #: models.py:938 671 | msgid "I18N Name" 672 | msgstr "Nome I18N" 673 | 674 | #: models.py:939 675 | msgid "I18N Names" 676 | msgstr "Nomi I18N" 677 | 678 | #: models.py:952 679 | msgid "The event's name" 680 | msgstr "Il nome dell'evento" 681 | 682 | #: models.py:956 683 | msgid "description" 684 | msgstr "descrizione" 685 | 686 | #: models.py:958 687 | msgid "The event's description" 688 | msgstr "La descrizione dell'evento" 689 | 690 | #: models.py:986 691 | msgid "The time at which the event starts" 692 | msgstr "Momento di inizio dell'evento" 693 | 694 | #: models.py:1009 695 | msgid "The time at which the event ends" 696 | msgstr "Momento in cui l'evento finisce" 697 | 698 | #: models.py:1014 699 | msgid "location" 700 | msgstr "località" 701 | 702 | #: models.py:1016 703 | msgid "The event's location" 704 | msgstr "L'indirizzo completo, con la località" 705 | 706 | #: models.py:1023 707 | msgid "The Area the Event is related to" 708 | msgstr "L'area che cui l'evento è correlato" 709 | 710 | #: models.py:1027 711 | msgid "status" 712 | msgstr "" 713 | 714 | #: models.py:1029 715 | msgid "The event's status" 716 | msgstr "Lo status dell'evento" 717 | 718 | #: models.py:1036 719 | msgid "Issued identifiers for this event" 720 | msgstr "Identificativi riconosciuti per questo evento" 721 | 722 | #: models.py:1042 723 | msgid "The event's category" 724 | msgstr "La categoria dell'evento" 725 | 726 | #: models.py:1050 727 | msgid "The organization organizing the event" 728 | msgstr "L'organizzazione che organizza questo evento" 729 | 730 | #: models.py:1058 731 | msgid "People attending the event" 732 | msgstr "Persone che partecipano all'evento" 733 | 734 | #: models.py:1067 735 | msgid "The Event that this event is part of" 736 | msgstr "L'area che cointiene questa area" 737 | 738 | #: models.py:1080 739 | msgid "Event" 740 | msgstr "Evento" 741 | 742 | #: models.py:1081 743 | msgid "Events" 744 | msgstr "Eventi" 745 | -------------------------------------------------------------------------------- /popolo/mixins.py: -------------------------------------------------------------------------------- 1 | from django.contrib.contenttypes.fields import ReverseGenericManyToOneDescriptor 2 | from django.db.models.fields.related_descriptors import ReverseManyToOneDescriptor 3 | 4 | from popolo import models as popolo_models 5 | from popolo.exceptions import OverlappingDateIntervalException 6 | from popolo.utils import PartialDatesInterval, PartialDate 7 | 8 | 9 | class ContactDetailsShortcutsMixin: 10 | contact_details: ReverseGenericManyToOneDescriptor 11 | 12 | def add_contact_detail(self, **kwargs): 13 | value = kwargs.pop("value") 14 | obj, created = self.contact_details.get_or_create(value=value, defaults=kwargs) 15 | return obj 16 | 17 | def add_contact_details(self, contacts): 18 | for obj in contacts: 19 | self.add_contact_detail(**obj) 20 | 21 | def update_contact_details(self, new_contacts): 22 | """update contact_details, 23 | removing those not present in new_contacts 24 | overwriting those present and existing, 25 | adding those present and not existing 26 | 27 | :param new_contacts: the new list of contact_details 28 | :return: 29 | """ 30 | existing_ids = set(self.contact_details.values_list("id", flat=True)) 31 | new_ids = set(n["id"] for n in new_contacts if "id" in n) 32 | 33 | # remove objects 34 | delete_ids = existing_ids - new_ids 35 | self.contact_details.filter(id__in=delete_ids).delete() 36 | 37 | # update objects 38 | for id_ in new_ids & existing_ids: 39 | u_name = list(filter(lambda x: x.get("id", None) == id_, new_contacts))[0].copy() 40 | 41 | self.contact_details.filter(pk=u_name.pop("id")).update(**u_name) 42 | 43 | # add objects 44 | for new_contact in new_contacts: 45 | if "id" not in new_contact: 46 | self.add_contact_detail(**new_contact) 47 | 48 | 49 | class OtherNamesShortcutsMixin: 50 | other_names: ReverseGenericManyToOneDescriptor 51 | 52 | def add_other_name( 53 | self, name, othername_type="ALT", overwrite_overlapping=False, extend_overlapping=True, **kwargs 54 | ): 55 | """Add other_name to the instance inheriting the mixin 56 | 57 | Names without dates specification may be added freely. 58 | 59 | A check for date interval overlaps is performed between the 60 | name that's to be added and all existing names that 61 | use the same scheme. Existing names are extracted ordered by 62 | ascending `start_date`. 63 | 64 | As soon as an overlap is found (first match): 65 | * if the name has the same value, 66 | then it is by default treated as an extension and date intervals 67 | are *extended*; 68 | this behaviour can be turned off by setting the 69 | `extend_overlapping` parameter to False, in which case the 70 | interval is not added and an exception is raised 71 | * if the name has a different value, 72 | then the interval is not added and an Exception is raised; 73 | this behaviour can be changed by setting `overwrite_overlapping` to 74 | True, in which case the new identifier overwrites the old one, both 75 | by value and dates 76 | 77 | Since **order matters**, multiple intervals need to be sorted by 78 | start date before being inserted. 79 | 80 | :param name: 81 | :param othername_type: 82 | :param overwrite_overlapping: overwrite first overlapping 83 | :param extend_overlapping: extend first overlapping (touching) 84 | :param kwargs: 85 | :return: the instance just added if successful 86 | """ 87 | 88 | # names with no date interval specified are added immediately 89 | # the tuple (othername_type, name) must be unique 90 | if kwargs.get("start_date", None) is None and kwargs.get("end_date", None) is None: 91 | i, created = self.other_names.get_or_create(othername_type=othername_type, name=name, defaults=kwargs) 92 | return i 93 | else: 94 | # the type was used 95 | # check if dates intervals overlap 96 | from popolo.utils import PartialDatesInterval 97 | from popolo.utils import PartialDate 98 | 99 | # get names having the same type 100 | same_type_names = self.other_names.filter(othername_type=othername_type).order_by("-end_date") 101 | 102 | # new name dates interval as PartialDatesInterval instance 103 | new_int = PartialDatesInterval(start=kwargs.get("start_date", None), end=kwargs.get("end_date", None)) 104 | 105 | is_overlapping_or_extending = False 106 | 107 | # loop over names of the same type 108 | for n in same_type_names: 109 | # existing name dates interval as PartialDatesInterval instance 110 | n_int = PartialDatesInterval(start=n.start_date, end=n.end_date) 111 | 112 | # compute overlap days 113 | # > 0 means crossing 114 | # == 0 means touching 115 | # < 0 meand not overlapping 116 | overlap = PartialDate.intervals_overlap(new_int, n_int) 117 | 118 | if overlap >= 0: 119 | # detect "overlaps" and "extensions", 120 | 121 | if n.name != name: 122 | # only crossing dates is a proper overlap 123 | if overlap > 0: 124 | if overwrite_overlapping: 125 | # overwrites existing identifier 126 | n.start_date = kwargs.get("start_date", None) 127 | n.end_date = kwargs.get("end_date", None) 128 | n.note = kwargs.get("note", None) 129 | n.source = kwargs.get("source", None) 130 | 131 | # save i and exit the loop 132 | n.save() 133 | is_overlapping_or_extending = True 134 | break 135 | else: 136 | # block insertion 137 | raise OverlappingDateIntervalException( 138 | n, 139 | "Name could not be created, " 140 | "due to overlapping dates ({0} : {1})".format(new_int, n_int), 141 | ) 142 | 143 | else: 144 | if extend_overlapping: 145 | # extension, get new dates for i 146 | if new_int.start.date is None or n_int.start.date is None: 147 | n.start_date = None 148 | else: 149 | n.start_date = min(n.start_date, new_int.start.date) 150 | 151 | if new_int.end.date is None or n_int.end.date is None: 152 | n.end_date = None 153 | else: 154 | n.end_date = max(n.end_date, new_int.end.date) 155 | 156 | # save i and exit the loop 157 | n.save() 158 | is_overlapping_or_extending = True 159 | break 160 | else: 161 | # block insertion 162 | raise OverlappingDateIntervalException( 163 | n, 164 | "Name could not be created, " 165 | "due to overlapping dates ({0} : {1})".format(new_int, n_int), 166 | ) 167 | 168 | # no overlaps, nor extensions, the identifier can be created 169 | if not is_overlapping_or_extending: 170 | return self.other_names.create(othername_type=othername_type, name=name, **kwargs) 171 | 172 | def add_other_names(self, names): 173 | """add other static names 174 | 175 | :param names: list of dicts containing names' parameters 176 | :return: 177 | """ 178 | for n in names: 179 | self.add_other_name(**n) 180 | 181 | def update_other_names(self, new_names): 182 | """update other_names, 183 | removing those not present in new_names 184 | overwriting those present and existing, 185 | adding those present and not existing 186 | 187 | :param new_names: the new list of other_names 188 | :return: 189 | """ 190 | existing_ids = set(self.other_names.values_list("id", flat=True)) 191 | new_ids = set(n["id"] for n in new_names if "id" in n) 192 | 193 | # remove objects 194 | delete_ids = existing_ids - new_ids 195 | self.other_names.filter(id__in=delete_ids).delete() 196 | 197 | # update objects 198 | for id_ in new_ids & existing_ids: 199 | u_name = list(filter(lambda x: x.get("id", None) == id_, new_names))[0].copy() 200 | 201 | self.other_names.filter(pk=u_name.pop("id")).update(**u_name) 202 | 203 | # add objects 204 | for new_name in new_names: 205 | if "id" not in new_name: 206 | self.add_other_name(**new_name) 207 | 208 | 209 | class IdentifierShortcutsMixin: 210 | identifiers: ReverseGenericManyToOneDescriptor 211 | 212 | def add_identifier( 213 | self, 214 | identifier, 215 | scheme, 216 | overwrite_overlapping=False, 217 | merge_overlapping=False, 218 | extend_overlapping=True, 219 | same_scheme_values_criterion=False, 220 | **kwargs, 221 | ): 222 | """Add identifier to the instance inheriting the mixin 223 | 224 | A check for date interval overlaps is performed between the 225 | identifier that's to be added and all existing identifiers that 226 | use the same scheme. Existing identifiers are extracted ordered by 227 | ascending `start_date`. 228 | 229 | As soon as an overlap is found (first match): 230 | * if the identifier has the same value, 231 | then it is by default treated as an extension and date intervals 232 | are *extended*; 233 | this behaviour can be turned off by setting the 234 | `extend_overlapping` parameter to False, in which case the 235 | interval is not added and an exception is raised 236 | * if the identifier has a different value, 237 | then the interval is not added and an Exception is raised; 238 | this behaviour can be changed by setting `overwrite_overlapping` to 239 | True, in which case the new identifier overwrites the old one, both 240 | by value and dates 241 | 242 | Since **order matters**, multiple intervals need to be sorted by 243 | start date before being inserted. 244 | 245 | :param identifier: 246 | :param scheme: 247 | :param overwrite_overlapping: overwrite first overlapping 248 | :param extend_overlapping: extend first overlapping (touching) 249 | :param merge_overlapping: get last start_date and first end_date 250 | :parma same_scheme_values_criterion: True if overlap is computed 251 | only for identifiers with same scheme and values 252 | If set to False (default) overlapping is computed for 253 | identifiers having the same scheme. 254 | :param kwargs: 255 | :return: the instance just added if successful 256 | """ 257 | 258 | # get identifiers having the same scheme, 259 | same_scheme_identifiers = self.identifiers.filter(scheme=scheme) 260 | 261 | # add identifier if the scheme is new 262 | if same_scheme_identifiers.count() == 0: 263 | i, created = self.identifiers.get_or_create(scheme=scheme, identifier=identifier, **kwargs) 264 | return i 265 | else: 266 | # the scheme is used 267 | # check if dates intervals overlap 268 | from popolo.utils import PartialDatesInterval 269 | from popolo.utils import PartialDate 270 | 271 | # new identifiere dates interval as PartialDatesInterval instance 272 | new_int = PartialDatesInterval(start=kwargs.get("start_date", None), end=kwargs.get("end_date", None)) 273 | 274 | is_overlapping_or_extending = False 275 | 276 | # loop over identifiers belonging to the same scheme 277 | for i in same_scheme_identifiers: 278 | 279 | # existing identifier interval as PartialDatesInterval instance 280 | i_int = PartialDatesInterval(start=i.start_date, end=i.end_date) 281 | 282 | # compute overlap days 283 | # > 0 means crossing 284 | # == 0 means touching 285 | # < 0 meand not overlapping 286 | overlap = PartialDate.intervals_overlap(new_int, i_int) 287 | 288 | if overlap >= 0: 289 | # detect "overlaps" and "extensions" 290 | 291 | if i.identifier != identifier: 292 | 293 | # only crossing dates is a proper overlap 294 | # when same 295 | if not same_scheme_values_criterion and overlap > 0: 296 | if overwrite_overlapping: 297 | # overwrites existing identifier 298 | i.start_date = kwargs.get("start_date", None) 299 | i.end_date = kwargs.get("end_date", None) 300 | i.identifier = identifier 301 | i.source = kwargs.get("source", None) 302 | 303 | # save i and exit the loop 304 | i.save() 305 | 306 | is_overlapping_or_extending = True 307 | break 308 | else: 309 | # block insertion 310 | if new_int.start.date is None and new_int.end.date is None and i_int == new_int: 311 | return 312 | else: 313 | raise OverlappingDateIntervalException( 314 | i, 315 | "Identifier could not be created, " 316 | "due to overlapping dates ({0} : {1})".format(new_int, i_int), 317 | ) 318 | else: 319 | # same values 320 | 321 | # same scheme, same date intervals, skip 322 | if i_int == new_int: 323 | is_overlapping_or_extending = True 324 | continue 325 | 326 | # we can extend, merge or block 327 | if extend_overlapping: 328 | # extension, get new dates for i 329 | if new_int.start.date is None or i_int.start.date is None: 330 | i.start_date = None 331 | else: 332 | i.start_date = min(i.start_date, new_int.start.date) 333 | 334 | if new_int.end.date is None or i_int.end.date is None: 335 | i.end_date = None 336 | else: 337 | i.end_date = max(i.end_date, new_int.end.date) 338 | 339 | # save i and break the loop 340 | i.save() 341 | is_overlapping_or_extending = True 342 | break 343 | elif merge_overlapping: 344 | nonnull_start_dates = list( 345 | filter(lambda x: x is not None, [new_int.start.date, i_int.start.date]) 346 | ) 347 | if len(nonnull_start_dates): 348 | i.start_date = min(nonnull_start_dates) 349 | 350 | nonnull_end_dates = list( 351 | filter(lambda x: x is not None, [new_int.end.date, i_int.end.date]) 352 | ) 353 | if len(nonnull_end_dates): 354 | i.end_date = max(nonnull_end_dates) 355 | 356 | i.save() 357 | is_overlapping_or_extending = True 358 | else: 359 | # block insertion 360 | if new_int.start.date is None and new_int.end.date is None and i_int == new_int: 361 | return 362 | else: 363 | raise OverlappingDateIntervalException( 364 | i, 365 | "Identifier with same scheme could not be created, " 366 | "due to overlapping dates ({0} : {1})".format(new_int, i_int), 367 | ) 368 | 369 | # no overlaps, nor extensions, the identifier can be created 370 | if not is_overlapping_or_extending: 371 | return self.identifiers.get_or_create(scheme=scheme, identifier=identifier, **kwargs) 372 | 373 | def add_identifiers(self, identifiers, update=True): 374 | """ add identifiers and skip those that generate exceptions 375 | 376 | Exceptions generated when dates overlap are gathered in a 377 | pipe-separated array and returned. 378 | 379 | :param identifiers: 380 | :param update: 381 | :return: 382 | """ 383 | exceptions = [] 384 | for i in identifiers: 385 | try: 386 | self.add_identifier(**i) 387 | except Exception as e: 388 | exceptions.append(str(e)) 389 | pass 390 | 391 | if len(exceptions): 392 | raise Exception(" | ".join(exceptions)) 393 | 394 | def update_identifiers(self, new_identifiers): 395 | """update identifiers, 396 | removing those not present in new_identifiers 397 | overwriting those present and existing, 398 | adding those present and not existing 399 | 400 | :param new_identifiers: the new list of identifiers 401 | :return: 402 | """ 403 | existing_ids = set(self.identifiers.values_list("id", flat=True)) 404 | new_ids = set(n["id"] for n in new_identifiers if "id" in n) 405 | 406 | # remove objects 407 | delete_ids = existing_ids - new_ids 408 | self.identifiers.filter(id__in=delete_ids).delete() 409 | 410 | # update objects 411 | for id_ in new_ids & existing_ids: 412 | u_name = list(filter(lambda x: x.get("id", None) == id_, new_identifiers))[0].copy() 413 | 414 | self.identifiers.filter(pk=u_name.pop("id")).update(**u_name) 415 | 416 | # add objects 417 | for new_identifier in new_identifiers: 418 | if "id" not in new_identifier: 419 | self.add_identifier(**new_identifier) 420 | 421 | 422 | class ClassificationShortcutsMixin: 423 | classifications: ReverseGenericManyToOneDescriptor 424 | 425 | def add_classification(self, scheme, code=None, descr=None, allow_same_scheme=False, **kwargs): 426 | """Add classification to the instance inheriting the mixin 427 | :param scheme: classification scheme (ATECO, LEGAL_FORM_IPA, ...) 428 | :param code: classification code, internal to the scheme 429 | :param descr: classification textual description (brief) 430 | :param allow_same_scheme: allows same scheme multiple classifications (for labels) 431 | :param kwargs: other params as source, start_date, end_date, ... 432 | :return: the classification instance just added 433 | """ 434 | # classifications having the same scheme, code and descr are considered 435 | # overlapping and will not be added 436 | if code is None and descr is None: 437 | raise Exception("At least one between descr " "and code must take value") 438 | 439 | # first create the Classification object, 440 | # or fetch an already existing one 441 | c, created = popolo_models.Classification.objects.get_or_create( 442 | scheme=scheme, code=code, descr=descr, defaults=kwargs 443 | ) 444 | 445 | # then add the ClassificationRel to classifications 446 | self.add_classification_rel(c, allow_same_scheme, **kwargs) 447 | 448 | def add_classification_rel(self, classification, allow_same_scheme=False, **kwargs): 449 | """Add classification (rel) to the instance inheriting the mixin 450 | 451 | :param classification: existing Classification instance or ID 452 | :param allow_same_scheme: allows same scheme multiple classifications (for labels) 453 | :param kwargs: other params: start_date, end_date, end_reason 454 | :return: the ClassificationRel instance just added 455 | """ 456 | # then add the ClassificationRel to classifications 457 | if not isinstance(classification, int) and not isinstance(classification, popolo_models.Classification): 458 | raise Exception("classification needs to be an integer ID or a Classification instance") 459 | 460 | multiple_values_schemes = getattr(self, 'MULTIPLE_CLASSIFICATIONS_SCHEMES', []) 461 | 462 | if isinstance(classification, int): 463 | # add classification_rel only if self is not already classified with classification of the same scheme 464 | cl = popolo_models.Classification.objects.get(id=classification) 465 | same_scheme_classifications = self.classifications.filter(classification__scheme=cl.scheme) 466 | if allow_same_scheme or not same_scheme_classifications or cl.scheme in multiple_values_schemes: 467 | c, created = self.classifications.get_or_create(classification_id=classification, **kwargs) 468 | return c 469 | else: 470 | # add classification_rel only if self is not already classified with classification of the same scheme 471 | same_scheme_classifications = self.classifications.filter(classification__scheme=classification.scheme) 472 | if allow_same_scheme or not same_scheme_classifications or classification.scheme in multiple_values_schemes: 473 | c, created = self.classifications.get_or_create(classification=classification, **kwargs) 474 | return c 475 | 476 | # return None if no classification was added 477 | return None 478 | 479 | def add_classifications(self, new_classifications, allow_same_scheme=False): 480 | """ add multiple classifications 481 | :param new_classifications: classification ids to be added 482 | :param allow_same_scheme: allows same scheme multiple classifications (for labels) 483 | :return: 484 | """ 485 | # add objects 486 | for new_classification in new_classifications: 487 | if "classification" in new_classification: 488 | self.add_classification_rel(**new_classification, allow_same_scheme=allow_same_scheme) 489 | else: 490 | self.add_classification(**new_classification, allow_same_scheme=allow_same_scheme) 491 | 492 | def update_classifications(self, new_classifications): 493 | """update classifications, 494 | removing those not present in new_classifications 495 | overwriting those present and existing, 496 | adding those present and not existing 497 | 498 | :param new_classifications: the new list of classification_rels 499 | :return: 500 | """ 501 | 502 | existing_ids = set(self.classifications.values_list("classification", flat=True)) 503 | new_ids = set(n.get("classification", None) for n in new_classifications) 504 | 505 | # remove objects 506 | delete_ids = existing_ids - set(new_ids) 507 | self.classifications.filter(classification__in=delete_ids).delete() 508 | 509 | # update objects (reference to already existing only) 510 | self.add_classifications([{"classification": c_id["classification"]} for c_id in new_classifications]) 511 | 512 | # update or create objects 513 | # for id in new_ids: 514 | # u = list(filter(lambda x: x['classification'].id == id, new_classifications))[0].copy() 515 | # u.pop('classification_id', None) 516 | # u.pop('content_type_id', None) 517 | # u.pop('object_id', None) 518 | # self.classifications.update_or_create( 519 | # classification_id=id, 520 | # content_type_id=ContentType.objects.get_for_model(self).pk, 521 | # object_id=self.id, 522 | # defaults=u 523 | # ) 524 | 525 | 526 | class LinkShortcutsMixin: 527 | links: ReverseGenericManyToOneDescriptor 528 | 529 | def add_link(self, url, **kwargs): 530 | note = kwargs.pop("note", "") 531 | link, created = popolo_models.Link.objects.get_or_create(url=url, note=note, defaults=kwargs) 532 | 533 | # then add the LinkRel to links 534 | self.links.get_or_create(link=link) 535 | 536 | return link 537 | 538 | def add_links(self, links): 539 | """TODO: clarify usage""" 540 | for link in links: 541 | if "link" in link: 542 | self.add_link(**link["link"]) 543 | else: 544 | self.add_link(**link) 545 | 546 | def update_links(self, new_links): 547 | """update links, (link_rels, actually) 548 | removing those not present in new_links 549 | overwriting those present and existing, 550 | adding those present and not existing 551 | 552 | :param new_links: the new list of link_rels 553 | :return: 554 | """ 555 | existing_ids = set(self.links.values_list("id", flat=True)) 556 | new_ids = set(link.get("id", None) for link in new_links) 557 | 558 | # remove objects 559 | delete_ids = existing_ids - set(new_ids) 560 | self.links.filter(id__in=delete_ids).delete() 561 | 562 | # update or create objects 563 | for id_ in new_ids: 564 | ul = list(filter(lambda x: x.get("id", None) == id_, new_links)).copy() 565 | for u in ul: 566 | u.pop("id", None) 567 | u.pop("content_type_id", None) 568 | u.pop("object_id", None) 569 | 570 | # update underlying link 571 | u_link = u["link"] 572 | l, created = popolo_models.Link.objects.get_or_create(url=u_link["url"], note=u_link["note"]) 573 | 574 | # update link_rel 575 | self.links.update_or_create(link=l) 576 | 577 | 578 | class SourceShortcutsMixin: 579 | sources: ReverseGenericManyToOneDescriptor 580 | 581 | def add_source(self, url, **kwargs): 582 | note = kwargs.pop("note", "") 583 | s, created = popolo_models.Source.objects.get_or_create(url=url, note=note, defaults=kwargs) 584 | 585 | # then add the SourceRel to sources 586 | self.sources.get_or_create(source=s) 587 | 588 | return s 589 | 590 | def add_sources(self, sources): 591 | """TODO: clarify usage""" 592 | for s in sources: 593 | if "source" in s: 594 | self.add_source(**s["source"]) 595 | else: 596 | self.add_source(**s) 597 | 598 | def update_sources(self, new_sources): 599 | """update sources, 600 | removing those not present in new_sources 601 | overwriting those present and existing, 602 | adding those present and not existing 603 | 604 | :param new_sources: the new list of link_rels 605 | :return: 606 | """ 607 | existing_ids = set(self.sources.values_list("id", flat=True)) 608 | new_ids = set(link.get("id", None) for link in new_sources) 609 | 610 | # remove objects 611 | delete_ids = existing_ids - new_ids 612 | self.sources.filter(id__in=delete_ids).delete() 613 | 614 | # update or create objects 615 | for id_ in new_ids: 616 | ul = list(filter(lambda x: x.get("id", None) == id_, new_sources)).copy() 617 | for u in ul: 618 | u.pop("id", None) 619 | u.pop("content_type_id", None) 620 | u.pop("object_id", None) 621 | 622 | # update underlying source 623 | u_source = u["source"] 624 | l, created = popolo_models.Source.objects.get_or_create(url=u_source["url"], note=u_source["note"]) 625 | 626 | # update source_rel 627 | self.sources.update_or_create(source=l) 628 | 629 | 630 | class OwnerShortcutsMixin: 631 | ownerships: ReverseManyToOneDescriptor 632 | 633 | def add_ownership( 634 | self, 635 | organization: "popolo_models.Organization", 636 | allow_overlap: bool = False, 637 | percentage: float = 0.0, 638 | **kwargs, 639 | ) -> "popolo_models.Ownership": 640 | """ 641 | Add this instance as "owner" of the given `Organization` 642 | 643 | Multiple ownerships to the same organization can be added 644 | only when start/end dates are not overlapping, or if overlap is explicitly allowed 645 | through the `allow_overlap` parameter. 646 | 647 | :param organization: The owned `Organization`. 648 | :param allow_overlap: Allow start/end date overlap of already existing ownerships. 649 | :param percentage: The ownership share. 650 | :param kwargs: Additional args to be passed when creating the `Ownership`. 651 | :return: The created `Ownership`. 652 | """ 653 | 654 | # New dates interval as PartialDatesInterval instance 655 | new_interval = PartialDatesInterval(start=kwargs.get("start_date", None), end=kwargs.get("end_date", None)) 656 | 657 | is_overlapping = False 658 | 659 | # Loop over already existing ownerships of the same organization 660 | same_org_ownerships = self.ownerships.filter(owned_organization=organization, percentage=percentage) 661 | for ownership in same_org_ownerships: 662 | 663 | # Existing identifier interval as PartialDatesInterval instance 664 | interval = PartialDatesInterval(start=ownership.start_date, end=ownership.end_date) 665 | 666 | # Get overlap days: 667 | # > 0 means crossing 668 | # == 0 means touching (considered non overlapping) 669 | # < 0 means not overlapping 670 | overlap = PartialDate.intervals_overlap(new_interval, interval) 671 | 672 | if overlap > 0: 673 | is_overlapping = True 674 | 675 | if not is_overlapping or allow_overlap: 676 | obj = self.ownerships.create(owned_organization=organization, percentage=percentage, **kwargs) 677 | return obj 678 | -------------------------------------------------------------------------------- /popolo/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-10-01 15:51 2 | 3 | import autoslug.fields 4 | import django.contrib.gis.db.models.fields 5 | import django.core.validators 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | import django.utils.text 9 | import popolo.behaviors.models 10 | import popolo.mixins 11 | import popolo.validators 12 | 13 | 14 | class Migration(migrations.Migration): 15 | 16 | initial = True 17 | 18 | dependencies = [ 19 | ('contenttypes', '0002_remove_content_type_name'), 20 | ] 21 | 22 | operations = [ 23 | migrations.CreateModel( 24 | name='Area', 25 | fields=[ 26 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 27 | ('start_date', models.CharField(blank=True, help_text='The date when the validity of the item starts', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='start date')), 28 | ('end_date', models.CharField(blank=True, help_text='The date when the validity of the item ends', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='end date')), 29 | ('end_reason', models.CharField(blank=True, help_text="The reason why the entity isn't valid any longer (eg: merge)", max_length=255, null=True, verbose_name='end reason')), 30 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='creation time')), 31 | ('updated_at', models.DateTimeField(auto_now=True, verbose_name='last modification time')), 32 | ('slug', autoslug.fields.AutoSlugField(editable=False, max_length=255, populate_from=popolo.behaviors.models.get_slug_source, slugify=django.utils.text.slugify, unique=True)), 33 | ('name', models.CharField(blank=True, help_text='The official, issued name', max_length=256, verbose_name='name')), 34 | ('identifier', models.CharField(blank=True, help_text='The main issued identifier', max_length=128, verbose_name='identifier')), 35 | ('classification', models.CharField(blank=True, help_text='An area category, according to GEONames definitions: http://www.geonames.org/export/codes.html', max_length=128, verbose_name='classification')), 36 | ('istat_classification', models.CharField(blank=True, choices=[('NAZ', 'Country'), ('RIP', 'Geographic partition'), ('REG', 'Region'), ('PROV', 'Province'), ('CM', 'Metropolitan area'), ('COM', 'Municipality'), ('MUN', 'Submunicipality'), ('ZU', 'Zone')], help_text='An area category, according to ISTAT: Ripartizione Geografica, Regione, Provincia, Città Metropolitana, Comune', max_length=4, null=True, verbose_name='ISTAT classification')), 37 | ('is_provincial_capital', models.NullBooleanField(help_text='If the city is a provincial capital.Takes the Null value if not a municipality.', verbose_name='Is provincial capital')), 38 | ('geometry', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, help_text='The geometry of the area', null=True, srid=4326, verbose_name='Geometry')), 39 | ('inhabitants', models.PositiveIntegerField(blank=True, help_text='The total number of inhabitants', null=True, verbose_name='inhabitants')), 40 | ('new_places', models.ManyToManyField(blank=True, help_text='Link to area(s) after date_end', related_name='old_places', to='popolo.Area')), 41 | ('parent', models.ForeignKey(blank=True, help_text='The area that contains this area, as for the main administrative subdivision.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='popolo.Area', verbose_name='Main parent')), 42 | ], 43 | options={ 44 | 'verbose_name': 'Geographic Area', 45 | 'verbose_name_plural': 'Geographic Areas', 46 | }, 47 | bases=(popolo.mixins.SourceShortcutsMixin, popolo.mixins.LinkShortcutsMixin, popolo.mixins.IdentifierShortcutsMixin, popolo.mixins.OtherNamesShortcutsMixin, models.Model), 48 | ), 49 | migrations.CreateModel( 50 | name='Classification', 51 | fields=[ 52 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 53 | ('start_date', models.CharField(blank=True, help_text='The date when the validity of the item starts', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='start date')), 54 | ('end_date', models.CharField(blank=True, help_text='The date when the validity of the item ends', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='end date')), 55 | ('end_reason', models.CharField(blank=True, help_text="The reason why the entity isn't valid any longer (eg: merge)", max_length=255, null=True, verbose_name='end reason')), 56 | ('scheme', models.CharField(blank=True, help_text='A classification scheme, e.g. ATECO, or FORMA_GIURIDICA', max_length=128, verbose_name='scheme')), 57 | ('code', models.CharField(blank=True, help_text='An alphanumerical code in use within the scheme', max_length=128, null=True, verbose_name='code')), 58 | ('descr', models.CharField(blank=True, help_text='The extended, textual description of the classification', max_length=512, null=True, verbose_name='description')), 59 | ('parent', models.ForeignKey(blank=True, help_text='The parent classification.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='popolo.Classification')), 60 | ], 61 | options={ 62 | 'verbose_name': 'Classification', 63 | 'verbose_name_plural': 'Classifications', 64 | }, 65 | bases=(popolo.mixins.SourceShortcutsMixin, models.Model), 66 | ), 67 | migrations.CreateModel( 68 | name='EducationLevel', 69 | fields=[ 70 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 71 | ('name', models.CharField(help_text='Education level name', max_length=256, unique=True, verbose_name='name')), 72 | ], 73 | options={ 74 | 'verbose_name': 'Normalized education level', 75 | 'verbose_name_plural': 'Normalized education levels', 76 | }, 77 | bases=(popolo.mixins.IdentifierShortcutsMixin, models.Model), 78 | ), 79 | migrations.CreateModel( 80 | name='KeyEvent', 81 | fields=[ 82 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 83 | ('start_date', models.CharField(blank=True, help_text='The date when the validity of the item starts', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='start date')), 84 | ('end_date', models.CharField(blank=True, help_text='The date when the validity of the item ends', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='end date')), 85 | ('end_reason', models.CharField(blank=True, help_text="The reason why the entity isn't valid any longer (eg: merge)", max_length=255, null=True, verbose_name='end reason')), 86 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='creation time')), 87 | ('updated_at', models.DateTimeField(auto_now=True, verbose_name='last modification time')), 88 | ('slug', autoslug.fields.AutoSlugField(editable=False, max_length=255, populate_from=popolo.behaviors.models.get_slug_source, slugify=django.utils.text.slugify, unique=True)), 89 | ('name', models.CharField(blank=True, help_text='A primary, generic name, e.g.: Local elections 2016', max_length=256, null=True, verbose_name='name')), 90 | ('event_type', models.CharField(choices=[('ELE', 'Election round'), ('ELE-POL', 'National election'), ('ELE-EU', 'European election'), ('ELE-REG', 'Regional election'), ('ELE-METRO', 'Metropolitan election'), ('ELE-PROV', 'Provincial election'), ('ELE-COM', 'Comunal election'), ('ITL', 'IT legislature'), ('EUL', 'EU legislature'), ('XAD', 'External administration')], default='ELE', help_text='The electoral type, e.g.: election, legislature, ...', max_length=12, verbose_name='event type')), 91 | ('identifier', models.CharField(blank=True, help_text='An issued identifier', max_length=512, null=True, verbose_name='identifier')), 92 | ], 93 | options={ 94 | 'verbose_name': 'Key event', 95 | 'verbose_name_plural': 'Key events', 96 | 'unique_together': {('start_date', 'event_type')}, 97 | }, 98 | ), 99 | migrations.CreateModel( 100 | name='Language', 101 | fields=[ 102 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 103 | ('name', models.CharField(help_text='English name of the language', max_length=128, verbose_name='name')), 104 | ('iso639_1_code', models.CharField(help_text='ISO 639_1 code, ex: en, it, de, fr, es, ...', max_length=2, unique=True, verbose_name='iso639_1 code')), 105 | ('dbpedia_resource', models.CharField(blank=True, help_text='DbPedia URI of the resource', max_length=255, null=True, unique=True, verbose_name='dbpedia resource')), 106 | ], 107 | options={ 108 | 'verbose_name': 'Language', 109 | 'verbose_name_plural': 'Languages', 110 | }, 111 | ), 112 | migrations.CreateModel( 113 | name='Link', 114 | fields=[ 115 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 116 | ('url', models.URLField(help_text='A URL', max_length=350, verbose_name='url')), 117 | ('note', models.CharField(blank=True, help_text="A note, e.g. 'Wikipedia page'", max_length=512, verbose_name='note')), 118 | ], 119 | options={ 120 | 'verbose_name': 'Link', 121 | 'verbose_name_plural': 'Links', 122 | 'unique_together': {('url', 'note')}, 123 | }, 124 | ), 125 | migrations.CreateModel( 126 | name='Membership', 127 | fields=[ 128 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 129 | ('start_date', models.CharField(blank=True, help_text='The date when the validity of the item starts', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='start date')), 130 | ('end_date', models.CharField(blank=True, help_text='The date when the validity of the item ends', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='end date')), 131 | ('end_reason', models.CharField(blank=True, help_text="The reason why the entity isn't valid any longer (eg: merge)", max_length=255, null=True, verbose_name='end reason')), 132 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='creation time')), 133 | ('updated_at', models.DateTimeField(auto_now=True, verbose_name='last modification time')), 134 | ('slug', autoslug.fields.AutoSlugField(editable=False, max_length=255, populate_from=popolo.behaviors.models.get_slug_source, slugify=django.utils.text.slugify, unique=True)), 135 | ('label', models.CharField(blank=True, help_text='A label describing the membership', max_length=512, null=True, verbose_name='label')), 136 | ('role', models.CharField(blank=True, help_text='The role that the member fulfills in the organization', max_length=512, null=True, verbose_name='role')), 137 | ('appointment_note', models.TextField(blank=True, help_text='A textual note for this appointment, if needed.', null=True, verbose_name='appointment note')), 138 | ('is_appointment_locked', models.BooleanField(default=False, help_text='A flag that shows if this appointment is locked (set to true when manually creating the appointment)')), 139 | ('constituency_descr_tmp', models.CharField(blank=True, max_length=128, null=True, verbose_name='Constituency location description')), 140 | ('electoral_list_descr_tmp', models.CharField(blank=True, max_length=512, null=True, verbose_name='Electoral list description')), 141 | ('appointed_by', models.ForeignKey(blank=True, help_text='The Membership that officially has appointed this one.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='appointees', to='popolo.Membership', verbose_name='Appointed by')), 142 | ('area', models.ForeignKey(blank=True, help_text='The geographic area to which the membership is related', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to='popolo.Area', verbose_name='Area')), 143 | ('electoral_event', models.ForeignKey(blank=True, help_text='The electoral event that assigned this membership', limit_choices_to={'event_type__contains': 'ELE'}, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='memberships_assigned', to='popolo.KeyEvent', verbose_name='Electoral event')), 144 | ], 145 | options={ 146 | 'verbose_name': 'Membership', 147 | 'verbose_name_plural': 'Memberships', 148 | }, 149 | bases=(popolo.mixins.ContactDetailsShortcutsMixin, popolo.mixins.LinkShortcutsMixin, popolo.mixins.SourceShortcutsMixin, popolo.mixins.ClassificationShortcutsMixin, models.Model), 150 | ), 151 | migrations.CreateModel( 152 | name='Organization', 153 | fields=[ 154 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 155 | ('start_date', models.CharField(blank=True, help_text='The date when the validity of the item starts', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='start date')), 156 | ('end_date', models.CharField(blank=True, help_text='The date when the validity of the item ends', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='end date')), 157 | ('end_reason', models.CharField(blank=True, help_text="The reason why the entity isn't valid any longer (eg: merge)", max_length=255, null=True, verbose_name='end reason')), 158 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='creation time')), 159 | ('updated_at', models.DateTimeField(auto_now=True, verbose_name='last modification time')), 160 | ('slug', autoslug.fields.AutoSlugField(editable=False, max_length=255, populate_from=popolo.behaviors.models.get_slug_source, slugify=django.utils.text.slugify, unique=True)), 161 | ('name', models.CharField(help_text='A primary name, e.g. a legally recognized name', max_length=512, verbose_name='name')), 162 | ('identifier', models.CharField(blank=True, help_text='The main issued identifier, or fiscal code, for organization', max_length=128, null=True, verbose_name='identifier')), 163 | ('classification', models.CharField(blank=True, help_text='The nature of the organization, legal form in many cases', max_length=256, null=True, verbose_name='classification')), 164 | ('thematic_classification', models.CharField(blank=True, help_text='What the organization does, in what fields, ...', max_length=256, null=True, verbose_name='thematic classification')), 165 | ('abstract', models.CharField(blank=True, help_text='A one-line description of an organization', max_length=256, null=True, verbose_name='abstract')), 166 | ('description', models.TextField(blank=True, help_text='An extended description of an organization', null=True, verbose_name='biography')), 167 | ('founding_date', models.CharField(blank=True, help_text='A date of founding', max_length=10, null=True, validators=[django.core.validators.RegexValidator(code='invalid_founding_date', message='founding date must follow the given pattern: ^[0-9]{4}(-[0-9]{2}){0,2}$', regex='^[0-9]{4}(-[0-9]{2}){0,2}$')], verbose_name='founding date')), 168 | ('dissolution_date', models.CharField(blank=True, help_text='A date of dissolution', max_length=10, null=True, validators=[django.core.validators.RegexValidator(code='invalid_dissolution_date', message='dissolution date must follow the given pattern: ^[0-9]{4}(-[0-9]{2}){0,2}$', regex='^[0-9]{4}(-[0-9]{2}){0,2}$')], verbose_name='dissolution date')), 169 | ('image', models.URLField(blank=True, help_text='A URL of an image, to identify the organization visually', max_length=255, null=True, verbose_name='image')), 170 | ('area', models.ForeignKey(blank=True, help_text='The geographic area to which this organization is related', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='organizations', to='popolo.Area')), 171 | ('new_orgs', models.ManyToManyField(blank=True, help_text='Link to organization(s) after dissolution_date, needed to track mergers, acquisition, splits.', related_name='old_orgs', to='popolo.Organization')), 172 | ('organization_members', models.ManyToManyField(related_name='organizations_memberships', through='popolo.Membership', to='popolo.Organization')), 173 | ], 174 | options={ 175 | 'verbose_name': 'Organization', 176 | 'verbose_name_plural': 'Organizations', 177 | }, 178 | bases=(popolo.mixins.ContactDetailsShortcutsMixin, popolo.mixins.OtherNamesShortcutsMixin, popolo.mixins.IdentifierShortcutsMixin, popolo.mixins.ClassificationShortcutsMixin, popolo.mixins.LinkShortcutsMixin, popolo.mixins.SourceShortcutsMixin, popolo.mixins.OwnerShortcutsMixin, models.Model), 179 | ), 180 | migrations.CreateModel( 181 | name='OriginalEducationLevel', 182 | fields=[ 183 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 184 | ('name', models.CharField(help_text='Education level name', max_length=512, unique=True, verbose_name='name')), 185 | ('normalized_education_level', models.ForeignKey(blank=True, help_text='The normalized education_level', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='original_education_levels', to='popolo.EducationLevel')), 186 | ], 187 | options={ 188 | 'verbose_name': 'Original education level', 189 | 'verbose_name_plural': 'Original education levels', 190 | }, 191 | ), 192 | migrations.CreateModel( 193 | name='OriginalProfession', 194 | fields=[ 195 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 196 | ('name', models.CharField(help_text='The original profession name', max_length=512, unique=True, verbose_name='name')), 197 | ], 198 | options={ 199 | 'verbose_name': 'Original profession', 200 | 'verbose_name_plural': 'Original professions', 201 | }, 202 | ), 203 | migrations.CreateModel( 204 | name='Person', 205 | fields=[ 206 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 207 | ('start_date', models.CharField(blank=True, help_text='The date when the validity of the item starts', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='start date')), 208 | ('end_date', models.CharField(blank=True, help_text='The date when the validity of the item ends', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='end date')), 209 | ('end_reason', models.CharField(blank=True, help_text="The reason why the entity isn't valid any longer (eg: merge)", max_length=255, null=True, verbose_name='end reason')), 210 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='creation time')), 211 | ('updated_at', models.DateTimeField(auto_now=True, verbose_name='last modification time')), 212 | ('slug', autoslug.fields.AutoSlugField(editable=False, max_length=255, populate_from=popolo.behaviors.models.get_slug_source, slugify=django.utils.text.slugify, unique=True)), 213 | ('name', models.CharField(blank=True, db_index=True, help_text="A person's preferred full name", max_length=512, null=True, verbose_name='name')), 214 | ('family_name', models.CharField(blank=True, db_index=True, help_text='One or more family names', max_length=128, null=True, verbose_name='family name')), 215 | ('given_name', models.CharField(blank=True, db_index=True, help_text='One or more primary given names', max_length=128, null=True, verbose_name='given name')), 216 | ('additional_name', models.CharField(blank=True, help_text='One or more secondary given names', max_length=128, null=True, verbose_name='additional name')), 217 | ('honorific_prefix', models.CharField(blank=True, help_text="One or more honorifics preceding a person's name", max_length=32, null=True, verbose_name='honorific prefix')), 218 | ('honorific_suffix', models.CharField(blank=True, help_text="One or more honorifics following a person's name", max_length=32, null=True, verbose_name='honorific suffix')), 219 | ('patronymic_name', models.CharField(blank=True, help_text='One or more patronymic names', max_length=128, null=True, verbose_name='patronymic name')), 220 | ('sort_name', models.CharField(blank=True, db_index=True, help_text='A name to use in an lexicographically ordered list', max_length=128, null=True, verbose_name='sort name')), 221 | ('email', models.EmailField(blank=True, help_text='A preferred email address', max_length=254, null=True, verbose_name='email')), 222 | ('gender', models.CharField(blank=True, db_index=True, help_text='A gender', max_length=32, null=True, verbose_name='gender')), 223 | ('birth_date', models.CharField(blank=True, db_index=True, help_text='A date of birth', max_length=10, null=True, verbose_name='birth date')), 224 | ('birth_location', models.CharField(blank=True, help_text='Birth location as a string', max_length=128, null=True, verbose_name='birth location')), 225 | ('death_date', models.CharField(blank=True, db_index=True, help_text='A date of death', max_length=10, null=True, verbose_name='death date')), 226 | ('is_identity_verified', models.BooleanField(default=False, help_text='If tax_id was verified formally', verbose_name='identity verified')), 227 | ('image', models.URLField(blank=True, help_text='A URL of a head shot', null=True, verbose_name='image')), 228 | ('summary', models.CharField(blank=True, help_text="A one-line account of a person's life", max_length=1024, null=True, verbose_name='summary')), 229 | ('biography', models.TextField(blank=True, help_text="An extended account of a person's life", null=True, verbose_name='biography')), 230 | ('national_identity', models.CharField(blank=True, help_text='A national identity', max_length=128, null=True, verbose_name='national identity')), 231 | ('birth_location_area', models.ForeignKey(blank=True, help_text='The geographic area corresponding to the birth location', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='persons_born_here', to='popolo.Area', verbose_name='birth location Area')), 232 | ('education_level', models.ForeignKey(blank=True, help_text='The education level of this person', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='persons_with_this_education_level', to='popolo.EducationLevel', verbose_name='Normalized education level')), 233 | ('original_education_level', models.ForeignKey(blank=True, help_text='The education level of this person, non normalized', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='persons_with_this_original_education_level', to='popolo.OriginalEducationLevel', verbose_name='Non normalized education level')), 234 | ('original_profession', models.ForeignKey(blank=True, help_text='The profession of this person, non normalized', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='persons_with_this_original_profession', to='popolo.OriginalProfession', verbose_name='Non normalized profession')), 235 | ], 236 | options={ 237 | 'verbose_name': 'Person', 238 | 'verbose_name_plural': 'People', 239 | }, 240 | bases=(popolo.mixins.ContactDetailsShortcutsMixin, popolo.mixins.OtherNamesShortcutsMixin, popolo.mixins.IdentifierShortcutsMixin, popolo.mixins.ClassificationShortcutsMixin, popolo.mixins.LinkShortcutsMixin, popolo.mixins.SourceShortcutsMixin, popolo.mixins.OwnerShortcutsMixin, models.Model), 241 | ), 242 | migrations.CreateModel( 243 | name='Profession', 244 | fields=[ 245 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 246 | ('name', models.CharField(help_text='Normalized profession name', max_length=512, unique=True, verbose_name='name')), 247 | ], 248 | options={ 249 | 'verbose_name': 'Normalized profession', 250 | 'verbose_name_plural': 'Normalized professions', 251 | }, 252 | bases=(popolo.mixins.IdentifierShortcutsMixin, models.Model), 253 | ), 254 | migrations.CreateModel( 255 | name='Source', 256 | fields=[ 257 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 258 | ('url', models.URLField(help_text='A URL', max_length=350, verbose_name='url')), 259 | ('note', models.CharField(blank=True, help_text="A note, e.g. 'Parliament website'", max_length=512, verbose_name='note')), 260 | ], 261 | options={ 262 | 'verbose_name': 'Source', 263 | 'verbose_name_plural': 'Sources', 264 | 'unique_together': {('url', 'note')}, 265 | }, 266 | ), 267 | migrations.CreateModel( 268 | name='SourceRel', 269 | fields=[ 270 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 271 | ('object_id', models.PositiveIntegerField(db_index=True, null=True)), 272 | ('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), 273 | ('source', models.ForeignKey(help_text='A Source instance assigned to this object', on_delete=django.db.models.deletion.CASCADE, related_name='related_objects', to='popolo.Source')), 274 | ], 275 | options={ 276 | 'abstract': False, 277 | }, 278 | ), 279 | migrations.CreateModel( 280 | name='RoleType', 281 | fields=[ 282 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 283 | ('label', models.CharField(help_text='A label describing the post, better keep it unique and put the classification descr into it', max_length=512, unique=True, verbose_name='label')), 284 | ('other_label', models.CharField(blank=True, help_text='An alternate label, such as an abbreviation', max_length=32, null=True, verbose_name='other label')), 285 | ('is_appointer', models.BooleanField(default=False, help_text='Whether this is a role able to appoint other roles', verbose_name='is appointer')), 286 | ('is_appointable', models.BooleanField(default=False, help_text='Whether this is role can be appointed (by appointers)', verbose_name='is appointable')), 287 | ('priority', models.IntegerField(blank=True, help_text='The priority of this role type, within the same classification group', null=True, verbose_name='priority')), 288 | ('classification', models.ForeignKey(help_text='The OP_FORMA_GIURIDICA classification this role type is related to', limit_choices_to={'scheme': 'FORMA_GIURIDICA_OP'}, on_delete=django.db.models.deletion.CASCADE, related_name='role_types', to='popolo.Classification')), 289 | ], 290 | options={ 291 | 'verbose_name': 'Role type', 292 | 'verbose_name_plural': 'Role types', 293 | 'unique_together': {('classification', 'label')}, 294 | }, 295 | ), 296 | migrations.CreateModel( 297 | name='Post', 298 | fields=[ 299 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 300 | ('start_date', models.CharField(blank=True, help_text='The date when the validity of the item starts', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='start date')), 301 | ('end_date', models.CharField(blank=True, help_text='The date when the validity of the item ends', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='end date')), 302 | ('end_reason', models.CharField(blank=True, help_text="The reason why the entity isn't valid any longer (eg: merge)", max_length=255, null=True, verbose_name='end reason')), 303 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='creation time')), 304 | ('updated_at', models.DateTimeField(auto_now=True, verbose_name='last modification time')), 305 | ('slug', autoslug.fields.AutoSlugField(editable=False, max_length=255, populate_from=popolo.behaviors.models.get_slug_source, slugify=django.utils.text.slugify, unique=True)), 306 | ('label', models.CharField(blank=True, help_text='A label describing the post', max_length=512, verbose_name='label')), 307 | ('other_label', models.CharField(blank=True, help_text='An alternate label, such as an abbreviation', max_length=32, null=True, verbose_name='other label')), 308 | ('role', models.CharField(blank=True, help_text='The function that the holder of the post fulfills', max_length=512, null=True, verbose_name='role')), 309 | ('priority', models.FloatField(blank=True, help_text='The absolute priority of this specific post, with respect to all others.', null=True, verbose_name='priority')), 310 | ('appointment_note', models.TextField(blank=True, help_text='A textual note for this appointment rule, if needed', null=True, verbose_name='appointment note')), 311 | ('is_appointment_locked', models.BooleanField(default=False, help_text='A flag that shows if this appointment rule is locked (set to true when manually creating the rule)')), 312 | ('appointed_by', models.ForeignKey(blank=True, help_text='The Post that officially appoints members to this one (appointment rule), ex: Secr. of Defence is appointed by POTUS', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='appointees', to='popolo.Post', verbose_name='Appointed by')), 313 | ('area', models.ForeignKey(blank=True, help_text='The geographic area to which the post is related', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='posts', to='popolo.Area', verbose_name='Area')), 314 | ('holders', models.ManyToManyField(related_name='roles_held', through='popolo.Membership', to='popolo.Person')), 315 | ('organization', models.ForeignKey(blank=True, help_text='The organization in which the post is held', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='posts', to='popolo.Organization', verbose_name='Organization')), 316 | ('organizations', models.ManyToManyField(related_name='posts_available', through='popolo.Membership', to='popolo.Organization')), 317 | ('role_type', models.ForeignKey(blank=True, help_text='The structured role type for this post', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='posts', to='popolo.RoleType', verbose_name='Role type')), 318 | ], 319 | options={ 320 | 'verbose_name': 'Post', 321 | 'verbose_name_plural': 'Posts', 322 | }, 323 | bases=(popolo.mixins.ContactDetailsShortcutsMixin, popolo.mixins.LinkShortcutsMixin, popolo.mixins.SourceShortcutsMixin, models.Model), 324 | ), 325 | migrations.CreateModel( 326 | name='PersonalRelationship', 327 | fields=[ 328 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 329 | ('start_date', models.CharField(blank=True, help_text='The date when the validity of the item starts', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='start date')), 330 | ('end_date', models.CharField(blank=True, help_text='The date when the validity of the item ends', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='end date')), 331 | ('end_reason', models.CharField(blank=True, help_text="The reason why the entity isn't valid any longer (eg: merge)", max_length=255, null=True, verbose_name='end reason')), 332 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='creation time')), 333 | ('updated_at', models.DateTimeField(auto_now=True, verbose_name='last modification time')), 334 | ('weight', models.IntegerField(choices=[(-1, 'Strongly negative'), (-2, 'Negative'), (0, 'Neutral'), (1, 'Positive'), (2, 'Strongly positive')], default=0, help_text='The relationship weight, from strongly negative, to strongly positive', verbose_name='weight')), 335 | ('descr', models.CharField(blank=True, help_text='Some details on the relationship (not much, though)', max_length=512, null=True, verbose_name='Description')), 336 | ('classification', models.ForeignKey(help_text='The classification for this personal relationship', limit_choices_to={'scheme': 'OP_TIPO_RELAZIONE_PERS'}, on_delete=django.db.models.deletion.CASCADE, related_name='personal_relationships', to='popolo.Classification')), 337 | ('dest_person', models.ForeignKey(help_text='The Person the relationship ends to', on_delete=django.db.models.deletion.CASCADE, related_name='from_relationships', to='popolo.Person', verbose_name='Destination person')), 338 | ('source_person', models.ForeignKey(help_text='The Person the relation starts from', on_delete=django.db.models.deletion.CASCADE, related_name='to_relationships', to='popolo.Person', verbose_name='Source person')), 339 | ], 340 | options={ 341 | 'verbose_name': 'Personal relationship', 342 | 'verbose_name_plural': 'Personal relationships', 343 | 'unique_together': {('source_person', 'dest_person', 'classification')}, 344 | }, 345 | bases=(popolo.mixins.SourceShortcutsMixin, models.Model), 346 | ), 347 | migrations.AddField( 348 | model_name='person', 349 | name='profession', 350 | field=models.ForeignKey(blank=True, help_text='The profession of this person', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='persons_with_this_profession', to='popolo.Profession', verbose_name='Normalized profession'), 351 | ), 352 | migrations.AddField( 353 | model_name='person', 354 | name='related_persons', 355 | field=models.ManyToManyField(through='popolo.PersonalRelationship', to='popolo.Person'), 356 | ), 357 | migrations.CreateModel( 358 | name='Ownership', 359 | fields=[ 360 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 361 | ('start_date', models.CharField(blank=True, help_text='The date when the validity of the item starts', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='start date')), 362 | ('end_date', models.CharField(blank=True, help_text='The date when the validity of the item ends', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='end date')), 363 | ('end_reason', models.CharField(blank=True, help_text="The reason why the entity isn't valid any longer (eg: merge)", max_length=255, null=True, verbose_name='end reason')), 364 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='creation time')), 365 | ('updated_at', models.DateTimeField(auto_now=True, verbose_name='last modification time')), 366 | ('slug', autoslug.fields.AutoSlugField(editable=False, max_length=255, populate_from=popolo.behaviors.models.get_slug_source, slugify=django.utils.text.slugify, unique=True)), 367 | ('percentage', models.FloatField(help_text='The *required* percentage ownership, expressed as a floating number, from 0 to 1', validators=[popolo.validators.validate_percentage], verbose_name='percentage ownership')), 368 | ('owned_organization', models.ForeignKey(help_text='The owned organization', on_delete=django.db.models.deletion.CASCADE, related_name='ownerships_as_owned', to='popolo.Organization', verbose_name='Owned organization')), 369 | ('owner_organization', models.ForeignKey(blank=True, help_text='An organization owning part of this organization', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='ownerships', to='popolo.Organization', verbose_name='Owning organization')), 370 | ('owner_person', models.ForeignKey(blank=True, help_text='A person owning part of this organization.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='ownerships', to='popolo.Person', verbose_name='Person')), 371 | ], 372 | options={ 373 | 'verbose_name': 'Ownership', 374 | 'verbose_name_plural': 'Ownerships', 375 | }, 376 | bases=(popolo.mixins.SourceShortcutsMixin, models.Model), 377 | ), 378 | migrations.CreateModel( 379 | name='OtherName', 380 | fields=[ 381 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 382 | ('object_id', models.PositiveIntegerField(db_index=True, null=True)), 383 | ('start_date', models.CharField(blank=True, help_text='The date when the validity of the item starts', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='start date')), 384 | ('end_date', models.CharField(blank=True, help_text='The date when the validity of the item ends', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='end date')), 385 | ('end_reason', models.CharField(blank=True, help_text="The reason why the entity isn't valid any longer (eg: merge)", max_length=255, null=True, verbose_name='end reason')), 386 | ('name', models.CharField(help_text='An alternate or former name', max_length=512, verbose_name='name')), 387 | ('othername_type', models.CharField(choices=[('FOR', 'Former name'), ('ALT', 'Alternate name'), ('AKA', 'Also Known As'), ('NIC', 'Nickname'), ('ACR', 'Acronym')], default='ALT', help_text='Type of other name, e.g. FOR: former, ALT: alternate, ...', max_length=3, verbose_name='scheme')), 388 | ('note', models.CharField(blank=True, help_text="An extended note, e.g. 'Birth name used before marrige'", max_length=1024, null=True, verbose_name='note')), 389 | ('source', models.URLField(blank=True, help_text='The URL of the source where this information comes from', max_length=256, null=True, verbose_name='source')), 390 | ('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), 391 | ], 392 | options={ 393 | 'verbose_name': 'Other name', 394 | 'verbose_name_plural': 'Other names', 395 | }, 396 | ), 397 | migrations.AddField( 398 | model_name='originalprofession', 399 | name='normalized_profession', 400 | field=models.ForeignKey(blank=True, help_text='The normalized profession', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='original_professions', to='popolo.Profession'), 401 | ), 402 | migrations.AddField( 403 | model_name='organization', 404 | name='organization_owners', 405 | field=models.ManyToManyField(related_name='organization_ownerships', through='popolo.Ownership', to='popolo.Organization'), 406 | ), 407 | migrations.AddField( 408 | model_name='organization', 409 | name='parent', 410 | field=models.ForeignKey(blank=True, help_text='The organization that contains this organization', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='popolo.Organization', verbose_name='Parent'), 411 | ), 412 | migrations.AddField( 413 | model_name='organization', 414 | name='person_members', 415 | field=models.ManyToManyField(related_name='organizations_memberships', through='popolo.Membership', to='popolo.Person'), 416 | ), 417 | migrations.AddField( 418 | model_name='organization', 419 | name='person_owners', 420 | field=models.ManyToManyField(related_name='organizations_ownerships', through='popolo.Ownership', to='popolo.Person'), 421 | ), 422 | migrations.AddField( 423 | model_name='membership', 424 | name='member_organization', 425 | field=models.ForeignKey(blank=True, help_text='The organization who is a member of the organization', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='memberships_as_member', to='popolo.Organization', verbose_name='Organization'), 426 | ), 427 | migrations.AddField( 428 | model_name='membership', 429 | name='on_behalf_of', 430 | field=models.ForeignKey(blank=True, help_text='The organization on whose behalf the person is a member of the organization', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='memberships_on_behalf_of', to='popolo.Organization', verbose_name='On behalf of'), 431 | ), 432 | migrations.AddField( 433 | model_name='membership', 434 | name='organization', 435 | field=models.ForeignKey(blank=True, help_text='The organization in which the person or organization is a member', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to='popolo.Organization', verbose_name='Organization'), 436 | ), 437 | migrations.AddField( 438 | model_name='membership', 439 | name='person', 440 | field=models.ForeignKey(blank=True, help_text='The person who is a member of the organization', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to='popolo.Person', verbose_name='Person'), 441 | ), 442 | migrations.AddField( 443 | model_name='membership', 444 | name='post', 445 | field=models.ForeignKey(blank=True, help_text='The post held by the person in the organization through this membership', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to='popolo.Post', verbose_name='Post'), 446 | ), 447 | migrations.CreateModel( 448 | name='LinkRel', 449 | fields=[ 450 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 451 | ('object_id', models.PositiveIntegerField(db_index=True, null=True)), 452 | ('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), 453 | ('link', models.ForeignKey(help_text='A relation to a Link instance assigned to this object', on_delete=django.db.models.deletion.CASCADE, related_name='related_objects', to='popolo.Link')), 454 | ], 455 | options={ 456 | 'abstract': False, 457 | }, 458 | ), 459 | migrations.CreateModel( 460 | name='KeyEventRel', 461 | fields=[ 462 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 463 | ('object_id', models.PositiveIntegerField(db_index=True, null=True)), 464 | ('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), 465 | ('key_event', models.ForeignKey(help_text='A relation to a KeyEvent instance assigned to this object', on_delete=django.db.models.deletion.CASCADE, related_name='related_objects', to='popolo.KeyEvent')), 466 | ], 467 | options={ 468 | 'abstract': False, 469 | }, 470 | ), 471 | migrations.CreateModel( 472 | name='Identifier', 473 | fields=[ 474 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 475 | ('object_id', models.PositiveIntegerField(db_index=True, null=True)), 476 | ('start_date', models.CharField(blank=True, help_text='The date when the validity of the item starts', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='start date')), 477 | ('end_date', models.CharField(blank=True, help_text='The date when the validity of the item ends', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='end date')), 478 | ('end_reason', models.CharField(blank=True, help_text="The reason why the entity isn't valid any longer (eg: merge)", max_length=255, null=True, verbose_name='end reason')), 479 | ('identifier', models.CharField(help_text='An issued identifier, e.g. a DUNS number', max_length=512, verbose_name='identifier')), 480 | ('scheme', models.CharField(blank=True, help_text='An identifier scheme, e.g. DUNS', max_length=128, verbose_name='scheme')), 481 | ('source', models.URLField(blank=True, help_text='The URL of the source where this information comes from', max_length=256, null=True, verbose_name='source')), 482 | ('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), 483 | ], 484 | options={ 485 | 'verbose_name': 'Identifier', 486 | 'verbose_name_plural': 'Identifiers', 487 | }, 488 | ), 489 | migrations.CreateModel( 490 | name='Event', 491 | fields=[ 492 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 493 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='creation time')), 494 | ('updated_at', models.DateTimeField(auto_now=True, verbose_name='last modification time')), 495 | ('name', models.CharField(help_text="The event's name", max_length=128, verbose_name='name')), 496 | ('description', models.CharField(blank=True, help_text="The event's description", max_length=512, null=True, verbose_name='description')), 497 | ('start_date', models.CharField(blank=True, help_text='The time at which the event starts', max_length=20, null=True, validators=[django.core.validators.RegexValidator(code='invalid_start_date', message='start date must follow the given pattern: ^[0-9]{4}((-[0-9]{2}){0,2}|(-[0-9]{2}){2}T[0-9]{2}(:[0-9]{2}){0,2}(Z|[+-][0-9]{2}(:[0-9]{2})?))$', regex='^[0-9]{4}((-[0-9]{2}){0,2}|(-[0-9]{2}){2}T[0-9]{2}(:[0-9]{2}){0,2}(Z|[+-][0-9]{2}(:[0-9]{2})?))$')], verbose_name='start date')), 498 | ('end_date', models.CharField(blank=True, help_text='The time at which the event ends', max_length=20, null=True, validators=[django.core.validators.RegexValidator(code='invalid_end_date', message='end date must follow the given pattern: ^[0-9]{4}((-[0-9]{2}){0,2}|(-[0-9]{2}){2}T[0-9]{2}(:[0-9]{2}){0,2}(Z|[+-][0-9]{2}(:[0-9]{2})?))$', regex='^[0-9]{4}((-[0-9]{2}){0,2}|(-[0-9]{2}){2}T[0-9]{2}(:[0-9]{2}){0,2}(Z|[+-][0-9]{2}(:[0-9]{2})?))$')], verbose_name='end date')), 499 | ('location', models.CharField(blank=True, help_text="The event's location", max_length=255, null=True, verbose_name='location')), 500 | ('status', models.CharField(blank=True, help_text="The event's status", max_length=128, null=True, verbose_name='status')), 501 | ('classification', models.CharField(blank=True, help_text="The event's category", max_length=128, null=True, verbose_name='classification')), 502 | ('area', models.ForeignKey(blank=True, help_text='The Area the Event is related to', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='events', to='popolo.Area')), 503 | ('attendees', models.ManyToManyField(blank=True, help_text='People attending the event', related_name='attended_events', to='popolo.Person')), 504 | ('organization', models.ForeignKey(blank=True, help_text='The organization organizing the event', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='events', to='popolo.Organization')), 505 | ('parent', models.ForeignKey(blank=True, help_text='The Event that this event is part of', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='popolo.Event', verbose_name='Parent')), 506 | ], 507 | options={ 508 | 'verbose_name': 'Event', 509 | 'verbose_name_plural': 'Events', 510 | }, 511 | bases=(popolo.mixins.SourceShortcutsMixin, models.Model), 512 | ), 513 | migrations.CreateModel( 514 | name='ContactDetail', 515 | fields=[ 516 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 517 | ('object_id', models.PositiveIntegerField(db_index=True, null=True)), 518 | ('start_date', models.CharField(blank=True, help_text='The date when the validity of the item starts', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='start date')), 519 | ('end_date', models.CharField(blank=True, help_text='The date when the validity of the item ends', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='end date')), 520 | ('end_reason', models.CharField(blank=True, help_text="The reason why the entity isn't valid any longer (eg: merge)", max_length=255, null=True, verbose_name='end reason')), 521 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='creation time')), 522 | ('updated_at', models.DateTimeField(auto_now=True, verbose_name='last modification time')), 523 | ('label', models.CharField(blank=True, help_text='A human-readable label for the contact detail', max_length=256, verbose_name='label')), 524 | ('contact_type', models.CharField(choices=[('ADDRESS', 'Address'), ('EMAIL', 'Email'), ('URL', 'Url'), ('MAIL', 'Snail mail'), ('TWITTER', 'Twitter'), ('FACEBOOK', 'Facebook'), ('PHONE', 'Telephone'), ('MOBILE', 'Mobile'), ('TEXT', 'Text'), ('VOICE', 'Voice'), ('FAX', 'Fax'), ('CELL', 'Cell'), ('VIDEO', 'Video'), ('INSTAGRAM', 'Instagram'), ('YOUTUBE', 'Youtube'), ('PAGER', 'Pager'), ('TEXTPHONE', 'Textphone')], help_text="A type of medium, e.g. 'fax' or 'email'", max_length=12, verbose_name='type')), 525 | ('value', models.CharField(help_text='A value, e.g. a phone number or email address', max_length=256, verbose_name='value')), 526 | ('note', models.CharField(blank=True, help_text='A note, e.g. for grouping contact details by physical location', max_length=512, null=True, verbose_name='note')), 527 | ('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), 528 | ], 529 | options={ 530 | 'verbose_name': 'Contact detail', 531 | 'verbose_name_plural': 'Contact details', 532 | }, 533 | bases=(popolo.mixins.SourceShortcutsMixin, models.Model), 534 | ), 535 | migrations.CreateModel( 536 | name='ClassificationRel', 537 | fields=[ 538 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 539 | ('object_id', models.PositiveIntegerField(db_index=True, null=True)), 540 | ('start_date', models.CharField(blank=True, help_text='The date when the validity of the item starts', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='start date')), 541 | ('end_date', models.CharField(blank=True, help_text='The date when the validity of the item ends', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='end date')), 542 | ('end_reason', models.CharField(blank=True, help_text="The reason why the entity isn't valid any longer (eg: merge)", max_length=255, null=True, verbose_name='end reason')), 543 | ('classification', models.ForeignKey(help_text='A Classification instance assigned to this object', on_delete=django.db.models.deletion.CASCADE, related_name='related_objects', to='popolo.Classification')), 544 | ('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), 545 | ], 546 | options={ 547 | 'abstract': False, 548 | }, 549 | ), 550 | migrations.CreateModel( 551 | name='AreaRelationship', 552 | fields=[ 553 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 554 | ('start_date', models.CharField(blank=True, help_text='The date when the validity of the item starts', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='start date')), 555 | ('end_date', models.CharField(blank=True, help_text='The date when the validity of the item ends', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='end date')), 556 | ('end_reason', models.CharField(blank=True, help_text="The reason why the entity isn't valid any longer (eg: merge)", max_length=255, null=True, verbose_name='end reason')), 557 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='creation time')), 558 | ('updated_at', models.DateTimeField(auto_now=True, verbose_name='last modification time')), 559 | ('classification', models.CharField(choices=[('FIP', 'Former ISTAT parent'), ('AMP', 'Alternate mountain community parent'), ('ACP', 'Alternate consortium of municipality parent'), ('DEP', 'Area depends on another area')], help_text='The relationship classification, ex: Former ISTAT parent, ...', max_length=3)), 560 | ('note', models.TextField(blank=True, help_text='Additional info about the relationship', null=True)), 561 | ('dest_area', models.ForeignKey(help_text='The Area the relationship ends to', on_delete=django.db.models.deletion.CASCADE, related_name='to_relationships', to='popolo.Area', verbose_name='Destination area')), 562 | ('source_area', models.ForeignKey(help_text='The Area the relation starts from', on_delete=django.db.models.deletion.CASCADE, related_name='from_relationships', to='popolo.Area', verbose_name='Source area')), 563 | ], 564 | options={ 565 | 'verbose_name': 'Area relationship', 566 | 'verbose_name_plural': 'Area relationships', 567 | }, 568 | bases=(popolo.mixins.SourceShortcutsMixin, models.Model), 569 | ), 570 | migrations.CreateModel( 571 | name='AreaI18Name', 572 | fields=[ 573 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 574 | ('name', models.CharField(max_length=255, verbose_name='name')), 575 | ('area', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='i18n_names', to='popolo.Area')), 576 | ('language', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='popolo.Language', verbose_name='Language')), 577 | ], 578 | options={ 579 | 'verbose_name': 'I18N Name', 580 | 'verbose_name_plural': 'I18N Names', 581 | }, 582 | ), 583 | migrations.AddField( 584 | model_name='area', 585 | name='related_areas', 586 | field=models.ManyToManyField(help_text='Relationships between areas', related_name='inversely_related_areas', through='popolo.AreaRelationship', to='popolo.Area'), 587 | ), 588 | migrations.CreateModel( 589 | name='OrganizationRelationship', 590 | fields=[ 591 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 592 | ('start_date', models.CharField(blank=True, help_text='The date when the validity of the item starts', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='start date')), 593 | ('end_date', models.CharField(blank=True, help_text='The date when the validity of the item ends', max_length=10, null=True, validators=[django.core.validators.RegexValidator(message='Date has wrong format', regex='^[0-9]{4}(-[0-9]{2}){0,2}$'), popolo.behaviors.models.validate_partial_date], verbose_name='end date')), 594 | ('end_reason', models.CharField(blank=True, help_text="The reason why the entity isn't valid any longer (eg: merge)", max_length=255, null=True, verbose_name='end reason')), 595 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='creation time')), 596 | ('updated_at', models.DateTimeField(auto_now=True, verbose_name='last modification time')), 597 | ('weight', models.IntegerField(choices=[(-1, 'Strongly negative'), (-2, 'Negative'), (0, 'Neutral'), (1, 'Positive'), (2, 'Strongly positive')], default=0, help_text='The relationship weight, from strongly negative, to strongly positive', verbose_name='weight')), 598 | ('descr', models.CharField(blank=True, help_text='Some details on the relationship (not much, though)', max_length=512, null=True, verbose_name='Description')), 599 | ('classification', models.ForeignKey(help_text='The classification for this organization relationship', limit_choices_to={'scheme': 'OP_TIPO_RELAZIONE_ORG'}, on_delete=django.db.models.deletion.CASCADE, related_name='organization_relationships', to='popolo.Classification')), 600 | ('dest_organization', models.ForeignKey(help_text='The Organization the relationship ends to', on_delete=django.db.models.deletion.CASCADE, related_name='from_relationships', to='popolo.Organization', verbose_name='Destination organization')), 601 | ('source_organization', models.ForeignKey(help_text='The Organization the relation starts from', on_delete=django.db.models.deletion.CASCADE, related_name='to_relationships', to='popolo.Organization', verbose_name='Source organization')), 602 | ], 603 | options={ 604 | 'verbose_name': 'Organization relationship', 605 | 'verbose_name_plural': 'Organization relationships', 606 | 'unique_together': {('source_organization', 'dest_organization', 'classification')}, 607 | }, 608 | bases=(popolo.mixins.SourceShortcutsMixin, models.Model), 609 | ), 610 | migrations.AlterUniqueTogether( 611 | name='organization', 612 | unique_together={('name', 'identifier', 'start_date')}, 613 | ), 614 | migrations.AddIndex( 615 | model_name='identifier', 616 | index=models.Index(fields=['identifier'], name='popolo_iden_identif_ace930_idx'), 617 | ), 618 | migrations.AlterUniqueTogether( 619 | name='event', 620 | unique_together={('name', 'start_date')}, 621 | ), 622 | migrations.AlterUniqueTogether( 623 | name='classification', 624 | unique_together={('scheme', 'code', 'descr')}, 625 | ), 626 | migrations.AlterUniqueTogether( 627 | name='areai18name', 628 | unique_together={('area', 'language', 'name')}, 629 | ), 630 | migrations.AlterUniqueTogether( 631 | name='area', 632 | unique_together={('identifier', 'istat_classification')}, 633 | ), 634 | ] 635 | --------------------------------------------------------------------------------