├── 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 |
--------------------------------------------------------------------------------