├── .gitmodules ├── opencivicdata ├── __init__.py ├── core │ ├── __init__.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── loaddivisions.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0006_merge_20200103_1432.py │ │ ├── 0002_post_maximum_memberships.py │ │ ├── 0005_auto_20191124_1658.py │ │ ├── 0005_auto_20191031_1507.py │ │ ├── 0009_auto_20241111_1450.py │ │ ├── 0008_auto_20221215_1132.py │ │ ├── 0007_jurisdiction_last_seen_membership_last_seen_and_more.py │ │ └── 0004_auto_20171005_2028.py │ ├── admin │ │ ├── __init__.py │ │ ├── base.py │ │ ├── other.py │ │ ├── person.py │ │ └── organization.py │ ├── apps.py │ └── models │ │ ├── __init__.py │ │ ├── jurisdiction.py │ │ ├── base.py │ │ └── division.py ├── tests │ ├── __init__.py │ ├── test_division.py │ ├── test_settings.py │ ├── test_loaddivisions.py │ ├── test_elections.py │ ├── fixtures │ │ └── country-in-subset.csv │ └── conftest.py ├── elections │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0003_election_fk.py │ │ ├── 0002_auto_20170731_2047.py │ │ ├── 0011_auto_20241111_1450.py │ │ ├── 0010_auto_20221215_1132.py │ │ ├── 0008_auto_20181029_1527.py │ │ ├── 0009_auto_20221208_1041.py │ │ ├── 0006_auto_20171005_2029.py │ │ ├── 0005_auto_20170823_1648.py │ │ ├── 0007_auto_20171022_0234.py │ │ └── 0004_field_docs.py │ ├── admin │ │ ├── contests │ │ │ ├── __init__.py │ │ │ ├── party.py │ │ │ ├── candidate.py │ │ │ └── ballot_measure.py │ │ ├── __init__.py │ │ ├── election.py │ │ └── candidacy.py │ ├── models │ │ ├── contests │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── party.py │ │ │ ├── candidate.py │ │ │ └── ballot_measure.py │ │ ├── __init__.py │ │ ├── election.py │ │ └── candidacy.py │ └── apps.py ├── legislative │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0008_longer_event_name.py │ │ ├── 0013_auto_20200326_0458.py │ │ ├── 0012_billdocument_extras.py │ │ ├── 0006_billversion_extras.py │ │ ├── 0007_auto_20181029_1527.py │ │ ├── 0011_auto_20191124_1658.py │ │ ├── 0002_more_extras.py │ │ ├── 0015_auto_20221215_1132.py │ │ ├── 0014_bill_last_seen_event_last_seen_voteevent_last_seen.py │ │ ├── 0016_auto_20241111_1450.py │ │ ├── 0003_time_changes.py │ │ ├── 0010_auto_20191031_1507.py │ │ ├── 0009_searchablebill.py │ │ ├── 0004_auto_20171005_2027.py │ │ └── 0005_auto_20171005_2028.py │ ├── admin │ │ ├── __init__.py │ │ ├── vote.py │ │ ├── event.py │ │ └── bill.py │ ├── apps.py │ └── models │ │ ├── __init__.py │ │ ├── session.py │ │ ├── vote.py │ │ ├── event.py │ │ └── bill.py ├── divisions.py └── common.py ├── .git-blame-ignore-revs ├── setup.cfg ├── MANIFEST.in ├── .coveragerc ├── .gitignore ├── run-tests.sh ├── tox.ini ├── .pre-commit-config.yaml ├── README.md ├── setup.py ├── LICENSE ├── .github └── workflows │ └── package.yml └── CHANGELOG.md /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /opencivicdata/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /opencivicdata/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /opencivicdata/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /opencivicdata/elections/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /opencivicdata/legislative/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /opencivicdata/core/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /opencivicdata/core/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /opencivicdata/elections/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /opencivicdata/legislative/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /opencivicdata/core/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /opencivicdata/elections/admin/contests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /opencivicdata/elections/models/contests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | 622e48bdfb95a80198eb95b8e65c37d2501ecbe1 2 | -------------------------------------------------------------------------------- /opencivicdata/legislative/admin/__init__.py: -------------------------------------------------------------------------------- 1 | from . import bill, vote # noqa 2 | -------------------------------------------------------------------------------- /opencivicdata/core/admin/__init__.py: -------------------------------------------------------------------------------- 1 | from . import other, organization, person # noqa 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | [flake8] 4 | max-line-length=100 5 | exclude = */migrations 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include opencivicdata/core/templates * 2 | recursive-include opencivicdata/legislative/templates * 3 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = opencivicdata/tests/* 3 | opencivicdata/*/migrations/* 4 | opencivicdata/*/admin/* 5 | -------------------------------------------------------------------------------- /opencivicdata/elections/admin/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: NOQA 2 | from . import election, candidacy 3 | from .contests import ballot_measure, candidate, party 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | .tox 3 | *.pyc 4 | dist/ 5 | build/ 6 | __pycache__ 7 | htmlcov 8 | .coverage 9 | graph 10 | *swp 11 | .cache 12 | .python-version -------------------------------------------------------------------------------- /run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | export PYTHONPATH=.; py.test --cov opencivicdata --ds=opencivicdata.tests.test_settings --cov-report html --cov-config=.coveragerc 4 | coverage report -m 5 | -------------------------------------------------------------------------------- /opencivicdata/core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | import os 3 | 4 | 5 | class BaseConfig(AppConfig): 6 | name = "opencivicdata.core" 7 | verbose_name = "Open Civic Data - Core" 8 | path = os.path.dirname(__file__) 9 | -------------------------------------------------------------------------------- /opencivicdata/elections/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | import os 3 | 4 | 5 | class BaseConfig(AppConfig): 6 | name = "opencivicdata.elections" 7 | verbose_name = "Open Civic Data - Elections" 8 | path = os.path.dirname(__file__) 9 | -------------------------------------------------------------------------------- /opencivicdata/legislative/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | import os 3 | 4 | 5 | class BaseConfig(AppConfig): 6 | name = "opencivicdata.legislative" 7 | verbose_name = "Open Civic Data - Legislative" 8 | path = os.path.dirname(__file__) 9 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py37-django{22},flake8 3 | [testenv] 4 | deps = 5 | 0jango22: Django==2.2 6 | commands = 7 | pip install -e .[dev] 8 | py.test opencivicdata --ds=opencivicdata.tests.test_settings 9 | 10 | [testenv:flake8] 11 | deps = flake8 12 | commands = flake8 opencivicdata 13 | -------------------------------------------------------------------------------- /opencivicdata/core/migrations/0006_merge_20200103_1432.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.2 on 2020-01-03 14:32 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('core', '0005_auto_20191124_1658'), 10 | ('core', '0005_auto_20191031_1507'), 11 | ] 12 | 13 | operations = [ 14 | ] 15 | -------------------------------------------------------------------------------- /opencivicdata/legislative/migrations/0008_longer_event_name.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | 3 | 4 | class Migration(migrations.Migration): 5 | 6 | dependencies = [("legislative", "0007_auto_20181029_1527")] 7 | 8 | operations = [ 9 | migrations.AlterField( 10 | model_name="event", name="name", field=models.CharField(max_length=1000) 11 | ) 12 | ] 13 | -------------------------------------------------------------------------------- /opencivicdata/tests/test_division.py: -------------------------------------------------------------------------------- 1 | #!/u/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from opencivicdata.divisions import Division 4 | 5 | 6 | def test_get(): 7 | div = Division.get("ocd-division/country:de/state:by/cd:248") 8 | assert div.name == "Bad Kissingen" 9 | assert div.name in str(div) 10 | 11 | 12 | def test_children(): 13 | us = Division.get("ocd-division/country:ua") 14 | assert len(list(us.children("region", duplicates=False))) == 25 15 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3.7 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v2.2.3 # Use the ref you want to point at 6 | hooks: 7 | - id: check-merge-conflict 8 | - id: debug-statements 9 | # - id: flake8 10 | # args: [--exclude, 'opencivicdata/*/migrations'] 11 | - repo: https://github.com/ambv/black 12 | rev: stable 13 | hooks: 14 | - id: black 15 | -------------------------------------------------------------------------------- /opencivicdata/elections/migrations/0003_election_fk.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.3 on 2017-08-17 19:01 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [("elections", "0002_auto_20170731_2047")] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name="electionsource", old_name="event", new_name="election" 15 | ) 16 | ] 17 | -------------------------------------------------------------------------------- /opencivicdata/core/migrations/0002_post_maximum_memberships.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.2 on 2017-07-19 09:36 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [("core", "0001_initial")] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="post", 15 | name="maximum_memberships", 16 | field=models.PositiveIntegerField(default=1), 17 | ) 18 | ] 19 | -------------------------------------------------------------------------------- /opencivicdata/legislative/migrations/0013_auto_20200326_0458.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.10 on 2020-03-26 04:58 2 | 3 | import django.contrib.postgres.indexes 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [("legislative", "0012_billdocument_extras")] 10 | 11 | operations = [ 12 | migrations.AddIndex( 13 | model_name="searchablebill", 14 | index=django.contrib.postgres.indexes.GinIndex( 15 | fields=["search_vector"], name="search_index" 16 | ), 17 | ) 18 | ] 19 | -------------------------------------------------------------------------------- /opencivicdata/core/models/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: NOQA 2 | from .jurisdiction import Jurisdiction 3 | from .division import Division 4 | from .people_orgs import ( 5 | Organization, 6 | OrganizationIdentifier, 7 | OrganizationName, 8 | OrganizationContactDetail, 9 | OrganizationLink, 10 | OrganizationSource, 11 | Person, 12 | PersonIdentifier, 13 | PersonName, 14 | PersonContactDetail, 15 | PersonLink, 16 | PersonSource, 17 | Post, 18 | PostContactDetail, 19 | PostLink, 20 | Membership, 21 | MembershipContactDetail, 22 | MembershipLink, 23 | ) 24 | -------------------------------------------------------------------------------- /opencivicdata/legislative/migrations/0012_billdocument_extras.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.2 on 2020-01-03 14:33 2 | 3 | import django.contrib.postgres.fields.jsonb 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('legislative', '0011_auto_20191124_1658'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='billdocument', 16 | name='extras', 17 | field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /opencivicdata/legislative/migrations/0006_billversion_extras.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1a1 on 2018-07-01 22:52 2 | 3 | import django.contrib.postgres.fields.jsonb 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [("legislative", "0005_auto_20171005_2028")] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="billversion", 14 | name="extras", 15 | field=django.contrib.postgres.fields.jsonb.JSONField( 16 | blank=True, default=dict 17 | ), 18 | ) 19 | ] 20 | -------------------------------------------------------------------------------- /opencivicdata/legislative/migrations/0007_auto_20181029_1527.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.9 on 2018-10-29 15:27 2 | 3 | import django.contrib.postgres.fields.jsonb 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [("legislative", "0006_billversion_extras")] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="voteevent", 14 | name="extras", 15 | field=django.contrib.postgres.fields.jsonb.JSONField( 16 | blank=True, default=dict 17 | ), 18 | ) 19 | ] 20 | -------------------------------------------------------------------------------- /opencivicdata/legislative/migrations/0011_auto_20191124_1658.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.3 on 2019-11-24 16:58 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [("legislative", "0010_auto_20191031_1507")] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="searchablebill", 14 | name="version_link", 15 | field=models.OneToOneField( 16 | null=True, 17 | on_delete=django.db.models.deletion.CASCADE, 18 | related_name="searchable", 19 | to="legislative.BillVersionLink", 20 | ), 21 | ) 22 | ] 23 | -------------------------------------------------------------------------------- /opencivicdata/tests/test_settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # not tests, just Django settings 4 | SECRET_KEY = "test" 5 | INSTALLED_APPS = ( 6 | "opencivicdata.core.apps.BaseConfig", 7 | "opencivicdata.legislative.apps.BaseConfig", 8 | "opencivicdata.elections.apps.BaseConfig", 9 | ) 10 | DATABASES = { 11 | "default": { 12 | "ENGINE": "django.contrib.gis.db.backends.postgis", 13 | "NAME": os.getenv("POSTGRES_DB", "test"), 14 | "USER": os.getenv("POSTGRES_USER", "test"), 15 | "PASSWORD": os.getenv("POSTGRES_PASSWORD", "test"), 16 | "HOST": "localhost", 17 | } 18 | } 19 | MIDDLEWARE_CLASSES = () 20 | GDAL_LIBRARY_PATH = os.getenv("GDAL_LIBRARY_PATH") 21 | GEOS_LIBRARY_PATH = os.getenv('GEOS_LIBRARY_PATH') 22 | -------------------------------------------------------------------------------- /opencivicdata/elections/migrations/0002_auto_20170731_2047.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.3 on 2017-07-31 20:47 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [("elections", "0001_initial")] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name="ballotmeasurecontest", options={"ordering": ("election", "name")} 15 | ), 16 | migrations.AlterModelOptions( 17 | name="candidatecontest", options={"ordering": ("election", "name")} 18 | ), 19 | migrations.AlterModelOptions( 20 | name="retentioncontest", options={"ordering": ("election", "name")} 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /opencivicdata/elections/models/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: NOQA 2 | from .election import Election, ElectionIdentifier, ElectionSource 3 | from .candidacy import Candidacy, CandidacySource 4 | from .contests.base import ContestBase 5 | from .contests.ballot_measure import ( 6 | BallotMeasureContest, 7 | BallotMeasureContestOption, 8 | BallotMeasureContestIdentifier, 9 | BallotMeasureContestSource, 10 | RetentionContest, 11 | RetentionContestOption, 12 | RetentionContestIdentifier, 13 | RetentionContestSource, 14 | ) 15 | from .contests.candidate import ( 16 | CandidateContest, 17 | CandidateContestPost, 18 | CandidateContestIdentifier, 19 | CandidateContestSource, 20 | ) 21 | from .contests.party import ( 22 | PartyContest, 23 | PartyContestOption, 24 | PartyContestIdentifier, 25 | PartyContestSource, 26 | ) 27 | -------------------------------------------------------------------------------- /opencivicdata/legislative/models/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: NOQA 2 | from .session import LegislativeSession 3 | from .bill import ( 4 | Bill, 5 | BillAbstract, 6 | BillTitle, 7 | BillIdentifier, 8 | RelatedBill, 9 | BillSponsorship, 10 | BillDocument, 11 | BillVersion, 12 | BillDocumentLink, 13 | BillVersionLink, 14 | BillSource, 15 | BillActionRelatedEntity, 16 | BillAction, 17 | SearchableBill, 18 | ) 19 | from .vote import VoteEvent, VoteCount, PersonVote, VoteSource 20 | from .event import ( 21 | Event, 22 | EventLocation, 23 | EventMedia, 24 | EventMediaLink, 25 | EventDocument, 26 | EventLink, 27 | EventSource, 28 | EventParticipant, 29 | EventAgendaItem, 30 | EventRelatedEntity, 31 | EventAgendaMedia, 32 | EventAgendaMediaLink, 33 | EventDocumentLink, 34 | ) 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | python-opencivicdata 2 | ==================== 3 | 4 | [![Build Status](https://travis-ci.com/opencivicdata/python-opencivicdata.svg?branch=master)](https://travis-ci.com/opencivicdata/python-opencivicdata) 5 | [![Coverage Status](https://coveralls.io/repos/opencivicdata/python-opencivicdata/badge.png?branch=master)](https://coveralls.io/r/opencivicdata/python-opencivicdata?branch=master) 6 | [![PyPI](https://img.shields.io/pypi/v/opencivicdata.svg)](https://pypi.python.org/pypi/opencivicdata) 7 | 8 | Python utilities (including Django models) for implementing the 9 | Open Civic Data specification. 10 | 11 | **Requires Django >=2.2 and Python >= 3.6, pin to <3.0 for older versions** 12 | 13 | The Organization, Person, Membership, Post, and VoteEvent models and related models are based on the [Popolo specification](http://popoloproject.com/). 14 | 15 | To run tests on this project: ./run-tests.sh 16 | -------------------------------------------------------------------------------- /opencivicdata/legislative/migrations/0002_more_extras.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.1 on 2017-05-27 02:21 3 | from __future__ import unicode_literals 4 | 5 | import django.contrib.postgres.fields.jsonb 6 | from django.db import migrations 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [("legislative", "0001_initial")] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="billaction", 16 | name="extras", 17 | field=django.contrib.postgres.fields.jsonb.JSONField( 18 | blank=True, default=dict 19 | ), 20 | ), 21 | migrations.AddField( 22 | model_name="eventagendaitem", 23 | name="extras", 24 | field=django.contrib.postgres.fields.jsonb.JSONField( 25 | blank=True, default=dict 26 | ), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /opencivicdata/legislative/models/session.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from opencivicdata.core.models.base import RelatedBase 3 | from opencivicdata.core.models import Jurisdiction 4 | from ...common import SESSION_CLASSIFICATION_CHOICES 5 | 6 | 7 | class LegislativeSession(RelatedBase): 8 | jurisdiction = models.ForeignKey( 9 | Jurisdiction, 10 | related_name="legislative_sessions", 11 | # should be hard to delete Jurisdiction 12 | on_delete=models.PROTECT, 13 | ) 14 | identifier = models.CharField(max_length=100) 15 | name = models.CharField(max_length=300) 16 | classification = models.CharField( 17 | max_length=100, choices=SESSION_CLASSIFICATION_CHOICES, blank=True 18 | ) 19 | start_date = models.CharField(max_length=10) # YYYY[-MM[-DD]] 20 | end_date = models.CharField(max_length=10) # YYYY[-MM[-DD]] 21 | 22 | def __str__(self): 23 | return "{} {}".format(self.jurisdiction, self.name) 24 | 25 | class Meta: 26 | db_table = "opencivicdata_legislativesession" 27 | -------------------------------------------------------------------------------- /opencivicdata/legislative/migrations/0015_auto_20221215_1132.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.8 on 2022-12-15 11:32 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("legislative", "0014_bill_last_seen_event_last_seen_voteevent_last_seen"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="bill", 15 | name="updated_at", 16 | field=models.DateTimeField( 17 | auto_now_add=True, help_text="The date and time of the last update." 18 | ), 19 | ), 20 | migrations.AlterField( 21 | model_name="event", 22 | name="updated_at", 23 | field=models.DateTimeField( 24 | auto_now_add=True, help_text="The date and time of the last update." 25 | ), 26 | ), 27 | migrations.AlterField( 28 | model_name="voteevent", 29 | name="updated_at", 30 | field=models.DateTimeField( 31 | auto_now_add=True, help_text="The date and time of the last update." 32 | ), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup, find_packages 3 | 4 | install_requires = ["Django>=3.2", "psycopg2-binary"] 5 | 6 | extras_require = { 7 | "dev": ["pytest>=3.6", "pytest-cov", "pytest-django", "coveralls==3.2.0", "flake8"] 8 | } 9 | 10 | setup( 11 | name="opencivicdata", 12 | version="3.4.0", 13 | author="James Turk", 14 | author_email="james@openstates.org", 15 | license="BSD", 16 | description="python opencivicdata library", 17 | long_description="", 18 | url="", 19 | packages=find_packages(), 20 | include_package_data=True, 21 | install_requires=install_requires, 22 | extras_require=extras_require, 23 | platforms=["any"], 24 | classifiers=[ 25 | "Development Status :: 5 - Production/Stable", 26 | "Intended Audience :: Developers", 27 | "License :: OSI Approved :: BSD License", 28 | "Natural Language :: English", 29 | "Operating System :: OS Independent", 30 | "Programming Language :: Python :: 3.9", 31 | "Programming Language :: Python :: 3.10", 32 | "Programming Language :: Python :: 3.11", 33 | "Programming Language :: Python :: 3.12", 34 | ], 35 | ) 36 | -------------------------------------------------------------------------------- /opencivicdata/core/migrations/0005_auto_20191124_1658.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.3 on 2019-11-24 16:58 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("core", "0004_auto_20171005_2028")] 9 | 10 | operations = [ 11 | migrations.AlterField( 12 | model_name="organization", 13 | name="classification", 14 | field=models.CharField( 15 | blank=True, 16 | choices=[ 17 | ("legislature", "Legislature"), 18 | ("executive", "Executive"), 19 | ("upper", "Upper Chamber"), 20 | ("lower", "Lower Chamber"), 21 | ("party", "Party"), 22 | ("committee", "Committee"), 23 | ("commission", "Commission"), 24 | ("corporation", "Corporation"), 25 | ("agency", "Agency"), 26 | ("department", "Department"), 27 | ("judiciary", "Judiciary"), 28 | ], 29 | help_text="The type of Organization being defined.", 30 | max_length=100, 31 | ), 32 | ) 33 | ] 34 | -------------------------------------------------------------------------------- /opencivicdata/legislative/migrations/0014_bill_last_seen_event_last_seen_voteevent_last_seen.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.3 on 2022-11-30 12:18 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("legislative", "0013_auto_20200326_0458"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="bill", 15 | name="last_seen", 16 | field=models.DateTimeField( 17 | auto_now=True, 18 | help_text="The last time this object was seen in a scrape.", 19 | ), 20 | ), 21 | migrations.AddField( 22 | model_name="event", 23 | name="last_seen", 24 | field=models.DateTimeField( 25 | auto_now=True, 26 | help_text="The last time this object was seen in a scrape.", 27 | ), 28 | ), 29 | migrations.AddField( 30 | model_name="voteevent", 31 | name="last_seen", 32 | field=models.DateTimeField( 33 | auto_now=True, 34 | help_text="The last time this object was seen in a scrape.", 35 | ), 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /opencivicdata/core/migrations/0005_auto_20191031_1507.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.8 on 2019-10-31 15:07 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("core", "0004_auto_20171005_2028"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="organization", 15 | name="classification", 16 | field=models.CharField( 17 | blank=True, 18 | choices=[ 19 | ("legislature", "Legislature"), 20 | ("executive", "Executive"), 21 | ("upper", "Upper Chamber"), 22 | ("lower", "Lower Chamber"), 23 | ("party", "Party"), 24 | ("committee", "Committee"), 25 | ("commission", "Commission"), 26 | ("corporation", "Corporation"), 27 | ("agency", "Agency"), 28 | ("department", "Department"), 29 | ("judiciary", "Judiciary"), 30 | ], 31 | help_text="The type of Organization being defined.", 32 | max_length=100, 33 | ), 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /opencivicdata/elections/admin/election.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Custom administration panels for Election-related models. 5 | """ 6 | from django.contrib import admin 7 | from opencivicdata.core.admin import base 8 | from .. import models 9 | 10 | 11 | class ElectionSourceInline(base.LinkInline): 12 | """ 13 | Custom inline administrative panely for ElectionSource model. 14 | """ 15 | 16 | model = models.ElectionSource 17 | 18 | 19 | class ElectionIdentifierInline(base.IdentifierInline): 20 | """ 21 | Custom inline administrative panel for the ElectionIdentifier model. 22 | """ 23 | 24 | model = models.ElectionIdentifier 25 | 26 | 27 | @admin.register(models.Election) 28 | class ElectionAdmin(base.ModelAdmin): 29 | """ 30 | Custom inline administrative panel for the Election model. 31 | """ 32 | 33 | readonly_fields = ("created_at", "updated_at") 34 | raw_id_fields = ("division",) 35 | fields = ( 36 | ("name", "date", "administrative_organization", "extras") 37 | + raw_id_fields 38 | + readonly_fields 39 | ) 40 | search_fields = ("name",) 41 | list_filter = ("updated_at",) 42 | date_hierarchy = "date" 43 | list_display = ("name", "date", "id", "updated_at") 44 | ordering = ("-date",) 45 | 46 | inlines = [ElectionIdentifierInline, ElectionSourceInline] 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-, Open Civic Data contributors 2 | Copyright (c) 2014, Sunlight Labs 3 | 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, 7 | are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, 10 | this list of conditions and the following disclaimer. 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | * Neither the name of Open Civic Data nor the names of its contributors may be 15 | used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 22 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 23 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 24 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /opencivicdata/elections/admin/contests/party.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Custom administration panels for OpenCivicData election contest models. 5 | """ 6 | from django.contrib import admin 7 | from opencivicdata.core.admin import base 8 | from ... import models 9 | 10 | 11 | class PartyContestIdentifierInline(base.IdentifierInline): 12 | """ 13 | Custom inline administrative panel for PartyContestIdentifier model. 14 | """ 15 | 16 | model = models.PartyContestIdentifier 17 | 18 | 19 | class PartyContestSourceInline(base.LinkInline): 20 | """ 21 | Custom inline administrative panel for the PartyContestSource model. 22 | """ 23 | 24 | model = models.PartyContestSource 25 | 26 | 27 | class PartyContestOptionInline(admin.TabularInline): 28 | """ 29 | Custom administrative panel for PartyContestOption model. 30 | """ 31 | 32 | model = models.PartyContestOption 33 | extra = 0 34 | 35 | 36 | @admin.register(models.PartyContest) 37 | class PartyContestAdmin(base.ModelAdmin): 38 | """ 39 | Custom administrative panel for the PartyContest model. 40 | """ 41 | 42 | readonly_fields = ("id", "created_at", "updated_at") 43 | raw_id_fields = ("division", "runoff_for_contest") 44 | fields = ("name", "election") + raw_id_fields + readonly_fields 45 | list_display = ("name", "election", "id", "updated_at") 46 | search_fields = ("name", "election__name") 47 | list_filter = ("updated_at",) 48 | date_hierarchy = "election__date" 49 | 50 | inlines = [ 51 | PartyContestOptionInline, 52 | PartyContestIdentifierInline, 53 | PartyContestSourceInline, 54 | ] 55 | -------------------------------------------------------------------------------- /opencivicdata/core/migrations/0009_auto_20241111_1450.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2024-11-11 14:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('core', '0008_auto_20221215_1132'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='jurisdiction', 15 | name='extras', 16 | field=models.JSONField(blank=True, default=dict, help_text='A key-value store for storing arbitrary information not covered elsewhere.'), 17 | ), 18 | migrations.AlterField( 19 | model_name='membership', 20 | name='extras', 21 | field=models.JSONField(blank=True, default=dict, help_text='A key-value store for storing arbitrary information not covered elsewhere.'), 22 | ), 23 | migrations.AlterField( 24 | model_name='organization', 25 | name='extras', 26 | field=models.JSONField(blank=True, default=dict, help_text='A key-value store for storing arbitrary information not covered elsewhere.'), 27 | ), 28 | migrations.AlterField( 29 | model_name='person', 30 | name='extras', 31 | field=models.JSONField(blank=True, default=dict, help_text='A key-value store for storing arbitrary information not covered elsewhere.'), 32 | ), 33 | migrations.AlterField( 34 | model_name='post', 35 | name='extras', 36 | field=models.JSONField(blank=True, default=dict, help_text='A key-value store for storing arbitrary information not covered elsewhere.'), 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /opencivicdata/elections/admin/candidacy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Custom administration panels for Candidacy-related models. 5 | """ 6 | from django.contrib import admin 7 | from opencivicdata.core.admin import base 8 | from .. import models 9 | 10 | 11 | class CandidacySourceInline(base.LinkInline): 12 | """ 13 | Custom inline administrative panel for the CandidacySource model. 14 | """ 15 | 16 | model = models.CandidacySource 17 | 18 | 19 | @admin.register(models.Candidacy) 20 | class CandidacyAdmin(base.ModelAdmin): 21 | """ 22 | Custom inline administrative panel for the Candidacy model. 23 | """ 24 | 25 | raw_id_fields = ("person", "contest", "top_ticket_candidacy") 26 | fields = ( 27 | "candidate_name", 28 | "post", 29 | "filed_date", 30 | "is_incumbent", 31 | "registration_status", 32 | "party", 33 | ) + raw_id_fields 34 | list_display = ( 35 | "candidate_name", 36 | "contest", 37 | "is_incumbent", 38 | "registration_status", 39 | "id", 40 | "party_name", 41 | "updated_at", 42 | ) 43 | 44 | search_fields = ("candidate_name", "contest__name", "post__label") 45 | list_filter = ("party__name", "is_incumbent", "registration_status", "updated_at") 46 | date_hierarchy = "contest__election__date" 47 | 48 | inlines = [CandidacySourceInline] 49 | 50 | def party_name(self, obj): 51 | """ 52 | Return the name of the Party associated with the Candidacy. 53 | """ 54 | if obj.party: 55 | name = obj.party.name 56 | else: 57 | name = None 58 | return name 59 | -------------------------------------------------------------------------------- /opencivicdata/core/migrations/0008_auto_20221215_1132.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.8 on 2022-12-15 11:32 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("core", "0007_jurisdiction_last_seen_membership_last_seen_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="jurisdiction", 15 | name="updated_at", 16 | field=models.DateTimeField( 17 | auto_now_add=True, help_text="The date and time of the last update." 18 | ), 19 | ), 20 | migrations.AlterField( 21 | model_name="membership", 22 | name="updated_at", 23 | field=models.DateTimeField( 24 | auto_now_add=True, help_text="The date and time of the last update." 25 | ), 26 | ), 27 | migrations.AlterField( 28 | model_name="organization", 29 | name="updated_at", 30 | field=models.DateTimeField( 31 | auto_now_add=True, help_text="The date and time of the last update." 32 | ), 33 | ), 34 | migrations.AlterField( 35 | model_name="person", 36 | name="updated_at", 37 | field=models.DateTimeField( 38 | auto_now_add=True, help_text="The date and time of the last update." 39 | ), 40 | ), 41 | migrations.AlterField( 42 | model_name="post", 43 | name="updated_at", 44 | field=models.DateTimeField( 45 | auto_now_add=True, help_text="The date and time of the last update." 46 | ), 47 | ), 48 | ] 49 | -------------------------------------------------------------------------------- /opencivicdata/tests/test_loaddivisions.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from django.core.management import call_command 5 | 6 | from opencivicdata.divisions import Division as FileDivision 7 | from opencivicdata.core.models import Division 8 | 9 | 10 | @pytest.mark.django_db 11 | def test_loaddivisions(capsys): 12 | assert Division.objects.count() == 0 13 | 14 | call_command("loaddivisions", "in") 15 | 16 | assert Division.objects.count() > 0 17 | assert Division.objects.count() == Division.objects.filter(country="in").count() 18 | assert ( 19 | Division.objects.filter(country="in", subtype1="state", subtype2="").count() 20 | == 29 21 | ) 22 | 23 | # Include a (very weak) check for idempotency 24 | call_command("loaddivisions", "in") 25 | 26 | assert ( 27 | Division.objects.filter(country="in", subtype1="state", subtype2="").count() 28 | == 29 29 | ) 30 | 31 | # The FileDivision's cache is a mutable class attribute, which is shared 32 | # between instances. Reset it, so calling the command with a CSV specified 33 | # does not use divisions cached from previous runs. 34 | FileDivision._cache = {} 35 | 36 | test_dir = os.path.abspath(os.path.dirname(__file__)) 37 | os.environ["OCD_DIVISION_CSV"] = os.path.join( 38 | test_dir, "fixtures", "country-in-subset.csv" 39 | ) 40 | 41 | call_command("loaddivisions", "in") 42 | 43 | out, _ = capsys.readouterr() 44 | last_message = out.splitlines()[-1] 45 | 46 | assert last_message == "The DB contains all CSV contents; no work to be done!" 47 | 48 | # Unset the CSV environment variable so subsequent division tests reload 49 | # divisions from source instead of breaking. 50 | os.environ.pop("OCD_DIVISION_CSV") 51 | -------------------------------------------------------------------------------- /opencivicdata/legislative/migrations/0016_auto_20241111_1450.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2024-11-11 14:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('legislative', '0015_auto_20221215_1132'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='bill', 15 | name='extras', 16 | field=models.JSONField(blank=True, default=dict, help_text='A key-value store for storing arbitrary information not covered elsewhere.'), 17 | ), 18 | migrations.AlterField( 19 | model_name='billaction', 20 | name='extras', 21 | field=models.JSONField(blank=True, default=dict), 22 | ), 23 | migrations.AlterField( 24 | model_name='billdocument', 25 | name='extras', 26 | field=models.JSONField(blank=True, default=dict), 27 | ), 28 | migrations.AlterField( 29 | model_name='billversion', 30 | name='extras', 31 | field=models.JSONField(blank=True, default=dict), 32 | ), 33 | migrations.AlterField( 34 | model_name='event', 35 | name='extras', 36 | field=models.JSONField(blank=True, default=dict, help_text='A key-value store for storing arbitrary information not covered elsewhere.'), 37 | ), 38 | migrations.AlterField( 39 | model_name='eventagendaitem', 40 | name='extras', 41 | field=models.JSONField(blank=True, default=dict), 42 | ), 43 | migrations.AlterField( 44 | model_name='voteevent', 45 | name='extras', 46 | field=models.JSONField(blank=True, default=dict), 47 | ), 48 | ] 49 | -------------------------------------------------------------------------------- /opencivicdata/core/migrations/0007_jurisdiction_last_seen_membership_last_seen_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.3 on 2022-11-30 12:18 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("core", "0006_merge_20200103_1432"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="jurisdiction", 15 | name="last_seen", 16 | field=models.DateTimeField( 17 | auto_now=True, 18 | help_text="The last time this object was seen in a scrape.", 19 | ), 20 | ), 21 | migrations.AddField( 22 | model_name="membership", 23 | name="last_seen", 24 | field=models.DateTimeField( 25 | auto_now=True, 26 | help_text="The last time this object was seen in a scrape.", 27 | ), 28 | ), 29 | migrations.AddField( 30 | model_name="organization", 31 | name="last_seen", 32 | field=models.DateTimeField( 33 | auto_now=True, 34 | help_text="The last time this object was seen in a scrape.", 35 | ), 36 | ), 37 | migrations.AddField( 38 | model_name="person", 39 | name="last_seen", 40 | field=models.DateTimeField( 41 | auto_now=True, 42 | help_text="The last time this object was seen in a scrape.", 43 | ), 44 | ), 45 | migrations.AddField( 46 | model_name="post", 47 | name="last_seen", 48 | field=models.DateTimeField( 49 | auto_now=True, 50 | help_text="The last time this object was seen in a scrape.", 51 | ), 52 | ), 53 | ] 54 | -------------------------------------------------------------------------------- /opencivicdata/core/models/jurisdiction.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.postgres.fields import ArrayField 3 | 4 | from ...common import JURISDICTION_CLASSIFICATION_CHOICES 5 | from .base import OCDBase, OCDIDField 6 | from .division import Division 7 | 8 | 9 | class Jurisdiction(OCDBase): 10 | """ 11 | A Jurisdiction represents a logical unit of governance. 12 | 13 | Examples would include: the United States Federal Government, the Government 14 | of the District of Columbia, the Lexington-Fayette Urban County Government, 15 | or the Wake County Public School System. 16 | """ 17 | 18 | id = OCDIDField(ocd_type="jurisdiction") 19 | name = models.CharField( 20 | max_length=300, 21 | help_text="The common name of the Jurisdiction, such as 'Wyoming.'", 22 | ) 23 | url = models.URLField( 24 | max_length=2000, help_text="The primary website of the Jurisdiction." 25 | ) 26 | classification = models.CharField( 27 | max_length=50, 28 | choices=JURISDICTION_CLASSIFICATION_CHOICES, 29 | default="government", 30 | db_index=True, 31 | help_text="The type of Jurisdiction being defined.", 32 | ) 33 | feature_flags = ArrayField( 34 | base_field=models.TextField(), 35 | blank=True, 36 | default=list, 37 | help_text="A list of features that are present for data in this jurisdiction.", 38 | ) 39 | division = models.ForeignKey( 40 | Division, 41 | related_name="jurisdictions", 42 | db_index=True, 43 | help_text="A link to a Division related to this Jurisdiction.", 44 | # don't allow deletion of a division that a Jurisdiction depends upon 45 | on_delete=models.PROTECT, 46 | ) 47 | 48 | class Meta: 49 | db_table = "opencivicdata_jurisdiction" 50 | 51 | def __str__(self): 52 | return self.name 53 | -------------------------------------------------------------------------------- /opencivicdata/core/admin/base.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Helpers ########## 4 | 5 | 6 | class ModelAdmin(admin.ModelAdmin): 7 | """ deletion of top level objects is evil """ 8 | 9 | actions = None 10 | 11 | def has_delete_permission(self, request, obj=None): 12 | return False 13 | 14 | # we probably don't want to add anything through the interface 15 | def has_add_permission(self, request, obj=None): 16 | return False 17 | 18 | # To ignore `DisallowedModelAdminLookup` error because of non 19 | # registered models 20 | def lookup_allowed(self, request, key): 21 | return True 22 | 23 | 24 | class ReadOnlyTabularInline(admin.TabularInline): 25 | def has_add_permission(self, request, obj=None): 26 | return False 27 | 28 | can_delete = False 29 | 30 | 31 | class IdentifierInline(admin.TabularInline): 32 | fields = readonly_fields = ("identifier", "scheme") 33 | extra = 0 34 | can_delete = False 35 | verbose_name = "ID from another system" 36 | verbose_name_plural = "IDs from other systems" 37 | 38 | def has_add_permission(self, request, obj=None): 39 | return False 40 | 41 | 42 | class LinkInline(admin.TabularInline): 43 | fields = ("url", "note") 44 | extra = 0 45 | 46 | 47 | class ContactDetailInline(admin.TabularInline): 48 | fields = ("type", "value", "note", "label") 49 | extra = 0 50 | verbose_name = "Piece of contact information" 51 | verbose_name_plural = "Contact information" 52 | 53 | 54 | class OtherNameInline(admin.TabularInline): 55 | extra = 0 56 | verbose_name = "Alternate name" 57 | verbose_name_plural = "Alternate names" 58 | 59 | 60 | class MimetypeLinkInline(admin.TabularInline): 61 | fields = ("media_type", "url") 62 | 63 | 64 | class RelatedEntityInline(admin.TabularInline): 65 | fields = ("name", "entity_type", "organization", "person") 66 | -------------------------------------------------------------------------------- /opencivicdata/legislative/migrations/0003_time_changes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [("legislative", "0002_more_extras")] 10 | 11 | operations = [ 12 | migrations.RunSQL( 13 | "SET CONSTRAINTS ALL IMMEDIATE", reverse_sql=migrations.RunSQL.noop 14 | ), 15 | migrations.AlterField( 16 | model_name="voteevent", 17 | name="end_date", 18 | field=models.CharField(blank=True, max_length=25), 19 | ), 20 | migrations.AlterField( 21 | model_name="voteevent", 22 | name="start_date", 23 | field=models.CharField(max_length=25), 24 | ), 25 | migrations.AlterField( 26 | model_name="billaction", name="date", field=models.CharField(max_length=25) 27 | ), 28 | migrations.AlterField( 29 | model_name="event", 30 | name="end_time", 31 | field=models.CharField(blank=True, default="", max_length=25), 32 | preserve_default=False, 33 | ), 34 | migrations.AlterField( 35 | model_name="event", name="start_time", field=models.CharField(max_length=25) 36 | ), 37 | migrations.RenameField( 38 | model_name="event", old_name="end_time", new_name="end_date" 39 | ), 40 | migrations.RenameField( 41 | model_name="event", old_name="start_time", new_name="start_date" 42 | ), 43 | migrations.AlterIndexTogether( 44 | name="event", index_together=set([("jurisdiction", "start_date", "name")]) 45 | ), 46 | migrations.RemoveField(model_name="event", name="timezone"), 47 | migrations.RunSQL( 48 | migrations.RunSQL.noop, reverse_sql="SET CONSTRAINTS ALL IMMEDIATE" 49 | ), 50 | ] 51 | -------------------------------------------------------------------------------- /opencivicdata/elections/models/contests/base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Base classes for contest-related models. 5 | """ 6 | from django.db import models 7 | from opencivicdata.core.models.base import OCDBase, OCDIDField 8 | from opencivicdata.core.models import Division 9 | from ..election import Election 10 | 11 | 12 | class ContestBase(OCDBase): 13 | """ 14 | A base class for representing a specific decision set before voters in an election. 15 | 16 | Includes properties shared by all contest types: BallotMeasureContest, 17 | CandidateContest, PartyContest and RetentionContest. 18 | """ 19 | 20 | id = OCDIDField( 21 | ocd_type="contest", 22 | help_text="Open Civic Data-style id in the format ``ocd-contest/{{uuid}}``.", 23 | ) 24 | name = models.CharField( 25 | max_length=300, 26 | help_text="Name of the contest, not necessarily as it appears on the ballot.", 27 | ) 28 | division = models.ForeignKey( 29 | Division, 30 | related_name="%(class)ss", 31 | related_query_name="%(class)ss", 32 | help_text="Reference to the Division that defines the political " 33 | "geography of the contest, e.g., a specific Congressional or " 34 | "State Senate district. Should be a subdivision of the Division " 35 | "referenced by the contest's Election.", 36 | on_delete=models.PROTECT, 37 | ) 38 | election = models.ForeignKey( 39 | Election, 40 | related_name="%(class)ss", 41 | related_query_name="%(class)ss", 42 | help_text="Reference to the Election in which the contest is decided.", 43 | on_delete=models.CASCADE, 44 | ) 45 | 46 | def __str__(self): 47 | return "{0} (in {1})".format(self.name, self.election.name) 48 | 49 | class Meta: 50 | """ 51 | Model options. 52 | """ 53 | 54 | ordering = ("election", "name") 55 | abstract = True 56 | -------------------------------------------------------------------------------- /opencivicdata/elections/migrations/0011_auto_20241111_1450.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2024-11-11 14:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('elections', '0010_auto_20221215_1132'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='ballotmeasurecontest', 15 | name='extras', 16 | field=models.JSONField(blank=True, default=dict, help_text='A key-value store for storing arbitrary information not covered elsewhere.'), 17 | ), 18 | migrations.AlterField( 19 | model_name='candidacy', 20 | name='extras', 21 | field=models.JSONField(blank=True, default=dict, help_text='A key-value store for storing arbitrary information not covered elsewhere.'), 22 | ), 23 | migrations.AlterField( 24 | model_name='candidatecontest', 25 | name='extras', 26 | field=models.JSONField(blank=True, default=dict, help_text='A key-value store for storing arbitrary information not covered elsewhere.'), 27 | ), 28 | migrations.AlterField( 29 | model_name='election', 30 | name='extras', 31 | field=models.JSONField(blank=True, default=dict, help_text='A key-value store for storing arbitrary information not covered elsewhere.'), 32 | ), 33 | migrations.AlterField( 34 | model_name='partycontest', 35 | name='extras', 36 | field=models.JSONField(blank=True, default=dict, help_text='A key-value store for storing arbitrary information not covered elsewhere.'), 37 | ), 38 | migrations.AlterField( 39 | model_name='retentioncontest', 40 | name='extras', 41 | field=models.JSONField(blank=True, default=dict, help_text='A key-value store for storing arbitrary information not covered elsewhere.'), 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /opencivicdata/elections/migrations/0010_auto_20221215_1132.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.8 on 2022-12-15 11:32 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("elections", "0009_auto_20221208_1041"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="ballotmeasurecontest", 15 | name="updated_at", 16 | field=models.DateTimeField( 17 | auto_now_add=True, help_text="The date and time of the last update." 18 | ), 19 | ), 20 | migrations.AlterField( 21 | model_name="candidacy", 22 | name="updated_at", 23 | field=models.DateTimeField( 24 | auto_now_add=True, help_text="The date and time of the last update." 25 | ), 26 | ), 27 | migrations.AlterField( 28 | model_name="candidatecontest", 29 | name="updated_at", 30 | field=models.DateTimeField( 31 | auto_now_add=True, help_text="The date and time of the last update." 32 | ), 33 | ), 34 | migrations.AlterField( 35 | model_name="election", 36 | name="updated_at", 37 | field=models.DateTimeField( 38 | auto_now_add=True, help_text="The date and time of the last update." 39 | ), 40 | ), 41 | migrations.AlterField( 42 | model_name="partycontest", 43 | name="updated_at", 44 | field=models.DateTimeField( 45 | auto_now_add=True, help_text="The date and time of the last update." 46 | ), 47 | ), 48 | migrations.AlterField( 49 | model_name="retentioncontest", 50 | name="updated_at", 51 | field=models.DateTimeField( 52 | auto_now_add=True, help_text="The date and time of the last update." 53 | ), 54 | ), 55 | ] 56 | -------------------------------------------------------------------------------- /opencivicdata/elections/admin/contests/candidate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Custom administration panels for OpenCivicData election contest models. 5 | """ 6 | from django.contrib import admin 7 | from opencivicdata.core.admin import base 8 | from ... import models 9 | 10 | 11 | class CandidateContestIdentifierInline(base.IdentifierInline): 12 | """ 13 | Custom inline administrative panel for CandidateContestIdentifier model. 14 | """ 15 | 16 | model = models.CandidateContestIdentifier 17 | 18 | 19 | class CandidateContestSourceInline(base.LinkInline): 20 | """ 21 | Custom inline administrative panel for the CandidateContestSource model. 22 | """ 23 | 24 | model = models.CandidateContestSource 25 | 26 | 27 | class CandidateContestPostInline(admin.TabularInline): 28 | """ 29 | Custom administrative panel for CandidateContestPost model. 30 | """ 31 | 32 | model = models.CandidateContestPost 33 | extra = 0 34 | 35 | 36 | @admin.register(models.CandidateContest) 37 | class CandidateContestAdmin(base.ModelAdmin): 38 | """ 39 | Custom administrative panel for the CandidateContest model. 40 | """ 41 | 42 | readonly_fields = ("id", "created_at", "updated_at") 43 | raw_id_fields = ("division", "runoff_for_contest") 44 | fields = ( 45 | ("name", "election", "party", "previous_term_unexpired", "number_elected") 46 | + raw_id_fields 47 | + readonly_fields 48 | ) 49 | list_display = ("name", "election", "division_name", "id", "updated_at") 50 | search_fields = ("name", "election__name") 51 | list_filter = ("updated_at",) 52 | date_hierarchy = "election__date" 53 | 54 | inlines = [ 55 | CandidateContestPostInline, 56 | CandidateContestIdentifierInline, 57 | CandidateContestSourceInline, 58 | ] 59 | 60 | def division_name(self, obj): 61 | """ 62 | Returns the name of the Division for the Contest. 63 | """ 64 | return obj.division.name 65 | -------------------------------------------------------------------------------- /opencivicdata/legislative/migrations/0010_auto_20191031_1507.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.8 on 2019-10-31 15:07 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("legislative", "0009_searchablebill")] 9 | 10 | operations = [ 11 | migrations.RemoveField(model_name="eventdocument", name="media_type"), 12 | migrations.RemoveField(model_name="eventdocument", name="text"), 13 | migrations.RemoveField(model_name="eventdocument", name="url"), 14 | migrations.AddField( 15 | model_name="eventdocument", 16 | name="classification", 17 | field=models.CharField( 18 | blank=True, 19 | choices=[ 20 | ("agenda", "Agenda"), 21 | ("minutes", "Minutes"), 22 | ("transcript", "Transcript"), 23 | ("testimony", "Testimony"), 24 | ], 25 | max_length=50, 26 | ), 27 | ), 28 | migrations.AddField( 29 | model_name="eventmedia", 30 | name="classification", 31 | field=models.CharField( 32 | blank=True, 33 | choices=[ 34 | ("audio recording", "Audio Recording"), 35 | ("video recording", "Video Recording"), 36 | ], 37 | max_length=50, 38 | ), 39 | ), 40 | migrations.AlterField( 41 | model_name="eventagendamedia", 42 | name="date", 43 | field=models.CharField(blank=True, max_length=25), 44 | ), 45 | migrations.AlterField( 46 | model_name="eventdocument", 47 | name="date", 48 | field=models.CharField(blank=True, max_length=25), 49 | ), 50 | migrations.AlterField( 51 | model_name="eventmedia", 52 | name="date", 53 | field=models.CharField(blank=True, max_length=25), 54 | ), 55 | ] 56 | -------------------------------------------------------------------------------- /opencivicdata/legislative/migrations/0009_searchablebill.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.13 on 2019-06-27 14:32 2 | 3 | import django.contrib.postgres.search 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [("legislative", "0008_longer_event_name")] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="SearchableBill", 15 | fields=[ 16 | ( 17 | "id", 18 | models.AutoField( 19 | auto_created=True, 20 | primary_key=True, 21 | serialize=False, 22 | verbose_name="ID", 23 | ), 24 | ), 25 | ( 26 | "search_vector", 27 | django.contrib.postgres.search.SearchVectorField(default=None), 28 | ), 29 | ("all_titles", models.TextField(default="")), 30 | ("raw_text", models.TextField(default="")), 31 | ("is_error", models.BooleanField(default=False)), 32 | ("created_at", models.DateTimeField(auto_now_add=True)), 33 | ( 34 | "bill", 35 | models.OneToOneField( 36 | null=True, 37 | on_delete=django.db.models.deletion.CASCADE, 38 | related_name="searchable", 39 | to="legislative.Bill", 40 | ), 41 | ), 42 | ( 43 | "version_link", 44 | models.OneToOneField( 45 | on_delete=django.db.models.deletion.CASCADE, 46 | related_name="searchable", 47 | to="legislative.BillVersionLink", 48 | ), 49 | ), 50 | ], 51 | options={"db_table": "opencivicdata_searchablebill"}, 52 | ) 53 | ] 54 | -------------------------------------------------------------------------------- /opencivicdata/legislative/admin/vote.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from opencivicdata.core.admin.base import ModelAdmin, ReadOnlyTabularInline 3 | from .. import models 4 | 5 | 6 | class VoteCountInline(ReadOnlyTabularInline): 7 | model = models.VoteCount 8 | fields = readonly_fields = ("option", "value") 9 | 10 | 11 | class PersonVoteInline(ReadOnlyTabularInline): 12 | model = models.PersonVote 13 | fields = readonly_fields = ("voter", "voter_name", "option") 14 | 15 | 16 | class VoteSourceInline(ReadOnlyTabularInline): 17 | model = models.VoteSource 18 | fields = readonly_fields = ("url", "note") 19 | 20 | 21 | @admin.register(models.VoteEvent) 22 | class VoteEventAdmin(ModelAdmin): 23 | readonly_fields = ( 24 | "bill", 25 | "organization", 26 | "legislative_session", 27 | "id", 28 | "identifier", 29 | "motion_text", 30 | "extras", 31 | ) 32 | fields = readonly_fields + ( 33 | "result", 34 | "motion_classification", 35 | "start_date", 36 | "end_date", 37 | ) 38 | 39 | list_selected_related = ( 40 | "sources", 41 | "legislative_session", 42 | "legislative_session__jurisdiction", 43 | "counts", 44 | ) 45 | 46 | def get_jurisdiction_name(self, obj): 47 | return obj.legislative_session.jurisdiction.name 48 | 49 | get_jurisdiction_name.short_description = "Jurisdiction" 50 | 51 | def get_vote_tally(self, obj): 52 | yes = no = other = 0 53 | for vc in obj.counts.all(): 54 | if vc.option == "yes": 55 | yes = vc.value 56 | elif vc.option == "no": 57 | no = vc.value 58 | else: 59 | other += vc.value 60 | return "{}-{}-{}".format(yes, no, other) 61 | 62 | get_vote_tally.short_description = "Vote Tally" 63 | 64 | list_display = ("get_jurisdiction_name", "identifier", "bill", "get_vote_tally") 65 | 66 | list_filter = ("legislative_session__jurisdiction__name",) 67 | 68 | inlines = [VoteCountInline, PersonVoteInline, VoteSourceInline] 69 | -------------------------------------------------------------------------------- /opencivicdata/elections/migrations/0008_auto_20181029_1527.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.9 on 2018-10-29 15:27 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [("elections", "0007_auto_20171022_0234")] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="candidacy", 14 | name="party", 15 | field=models.ForeignKey( 16 | help_text="Reference to the Organization representing the political party that nominated the candidate or would nominate the candidate (as in the case of a partisan primary).", # noqa 17 | limit_choices_to={"classification": "party"}, 18 | null=True, 19 | on_delete=django.db.models.deletion.SET_NULL, 20 | related_name="candidacies", 21 | to="core.Organization", 22 | ), 23 | ), 24 | migrations.AlterField( 25 | model_name="candidatecontest", 26 | name="party", 27 | field=models.ForeignKey( 28 | help_text="If the contest is among candidates of the same political party, e.g., a partisan primary election, reference to the Organization representing that party.", # noqa 29 | limit_choices_to={"classification": "party"}, 30 | null=True, 31 | on_delete=django.db.models.deletion.SET_NULL, 32 | related_name="candidate_contests", 33 | to="core.Organization", 34 | ), 35 | ), 36 | migrations.AlterField( 37 | model_name="partycontestoption", 38 | name="party", 39 | field=models.ForeignKey( 40 | help_text="Reference to an Organization representing a political party voters may choose in the contest.", # noqa 41 | limit_choices_to={"classification": "party"}, 42 | on_delete=django.db.models.deletion.CASCADE, 43 | related_name="party_contests", 44 | to="core.Organization", 45 | ), 46 | ), 47 | ] 48 | -------------------------------------------------------------------------------- /opencivicdata/elections/migrations/0009_auto_20221208_1041.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.8 on 2022-12-08 10:41 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("elections", "0008_auto_20181029_1527"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="ballotmeasurecontest", 15 | name="last_seen", 16 | field=models.DateTimeField( 17 | auto_now=True, 18 | help_text="The last time this object was seen in a scrape.", 19 | ), 20 | ), 21 | migrations.AddField( 22 | model_name="candidacy", 23 | name="last_seen", 24 | field=models.DateTimeField( 25 | auto_now=True, 26 | help_text="The last time this object was seen in a scrape.", 27 | ), 28 | ), 29 | migrations.AddField( 30 | model_name="candidatecontest", 31 | name="last_seen", 32 | field=models.DateTimeField( 33 | auto_now=True, 34 | help_text="The last time this object was seen in a scrape.", 35 | ), 36 | ), 37 | migrations.AddField( 38 | model_name="election", 39 | name="last_seen", 40 | field=models.DateTimeField( 41 | auto_now=True, 42 | help_text="The last time this object was seen in a scrape.", 43 | ), 44 | ), 45 | migrations.AddField( 46 | model_name="partycontest", 47 | name="last_seen", 48 | field=models.DateTimeField( 49 | auto_now=True, 50 | help_text="The last time this object was seen in a scrape.", 51 | ), 52 | ), 53 | migrations.AddField( 54 | model_name="retentioncontest", 55 | name="last_seen", 56 | field=models.DateTimeField( 57 | auto_now=True, 58 | help_text="The last time this object was seen in a scrape.", 59 | ), 60 | ), 61 | ] 62 | -------------------------------------------------------------------------------- /.github/workflows/package.yml: -------------------------------------------------------------------------------- 1 | name: Test and build Python package 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | tags: 7 | - v* 8 | pull_request: 9 | branches: [ master ] 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-20.04 14 | services: 15 | postgres: 16 | image: postgis/postgis:12-2.5 17 | env: 18 | POSTGRES_USER: test 19 | POSTGRES_DB: test 20 | POSTGRES_PASSWORD: test 21 | options: >- 22 | --health-cmd pg_isready 23 | --health-interval 10s 24 | --health-timeout 5s 25 | --health-retries 5 26 | ports: 27 | - 5432:5432 28 | strategy: 29 | matrix: 30 | python-version: ['3.9', '3.10', '3.11', '3.12'] 31 | django-series: ['3.2', '4.0', '4.2'] 32 | steps: 33 | - uses: actions/checkout@v4 34 | - name: Set up Python ${{ matrix.python-version }} 35 | uses: actions/setup-python@v5 36 | with: 37 | python-version: ${{ matrix.python-version }} 38 | - name: Install dependencies 39 | run: | 40 | sudo apt update 41 | sudo apt install -y gdal-bin 42 | pip install -U setuptools six 43 | pip install .[dev] --pre Django==${{ matrix.django-series }} 44 | - name: Lint with flake8 45 | run: | 46 | flake8 47 | - name: Test with pytest 48 | run: | 49 | ./run-tests.sh 50 | - name: Calculate test coverage 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | run: | 54 | coveralls --service=github 55 | 56 | build: 57 | needs: test 58 | name: Build package and upload to PyPI 59 | runs-on: ubuntu-20.04 60 | steps: 61 | - uses: actions/checkout@v4 62 | - name: Build and publish 63 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') 64 | env: 65 | TWINE_USERNAME: __token__ 66 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 67 | run: | 68 | pip install twine wheel 69 | pip wheel -w dist --no-deps . 70 | python setup.py sdist 71 | twine upload dist/* 72 | continue-on-error: true 73 | -------------------------------------------------------------------------------- /opencivicdata/core/admin/other.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.apps import apps 3 | from .. import models 4 | from .base import ModelAdmin, ReadOnlyTabularInline, ContactDetailInline, LinkInline 5 | 6 | 7 | @admin.register(models.Division) 8 | class DivisionAdmin(ModelAdmin): 9 | list_display = ("name", "id") 10 | search_fields = list_display 11 | fields = readonly_fields = ("id", "name", "redirect", "country") 12 | ordering = ("id",) 13 | 14 | 15 | # have to handle this special since LegislativeSession might not be present 16 | try: 17 | LegislativeSession = apps.get_model("legislative", "LegislativeSession") 18 | 19 | class LegislativeSessionInline(ReadOnlyTabularInline): 20 | model = LegislativeSession 21 | readonly_fields = ( 22 | "identifier", 23 | "name", 24 | "classification", 25 | "start_date", 26 | "end_date", 27 | ) 28 | ordering = ("-identifier",) 29 | 30 | jurisdiction_inlines = [LegislativeSessionInline] 31 | 32 | except LookupError: 33 | jurisdiction_inlines = [] 34 | 35 | 36 | @admin.register(models.Jurisdiction) 37 | class JurisdictionAdmin(ModelAdmin): 38 | list_display = ("name", "id") 39 | readonly_fields = fields = ( 40 | "id", 41 | "name", 42 | "division", 43 | "classification", 44 | "feature_flags", 45 | "extras", 46 | "url", 47 | ) 48 | ordering = ("id",) 49 | inlines = jurisdiction_inlines 50 | 51 | 52 | class PostContactDetailInline(ContactDetailInline): 53 | model = models.PostContactDetail 54 | 55 | 56 | class PostLinkInline(LinkInline): 57 | model = models.PostLink 58 | 59 | 60 | @admin.register(models.Post) 61 | class PostAdmin(ModelAdmin): 62 | readonly_fields = ("id", "label", "organization", "division", "extras", "role") 63 | fields = readonly_fields + (("start_date", "end_date"),) 64 | list_display = ("label", "organization", "division") 65 | list_filter = ("organization__jurisdiction__name",) 66 | ordering = ("organization__name",) 67 | inlines = [PostContactDetailInline, PostLinkInline] 68 | search_fields = ("organization__name", "label") 69 | -------------------------------------------------------------------------------- /opencivicdata/elections/migrations/0006_auto_20171005_2029.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.5 on 2017-10-05 20:29 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [("elections", "0005_auto_20170823_1648")] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="candidacy", 16 | name="party", 17 | field=models.ForeignKey( 18 | help_text="Reference to the Organization representing the political party that nominated the candidate or would nominate the candidate (as in the case of a partisan primary).", # noqa 19 | null=True, 20 | on_delete=django.db.models.deletion.SET_NULL, 21 | related_name="candidacies", 22 | to="core.Organization", 23 | ), 24 | ), 25 | migrations.AlterField( 26 | model_name="candidatecontest", 27 | name="party", 28 | field=models.ForeignKey( 29 | help_text="If the contest is among candidates of the same political party, e.g., a partisan primary election, reference to the Organization representing that party.", # noqa 30 | null=True, 31 | on_delete=django.db.models.deletion.SET_NULL, 32 | related_name="candidate_contests", 33 | to="core.Organization", 34 | ), 35 | ), 36 | migrations.AlterField( 37 | model_name="election", 38 | name="administrative_organization", 39 | field=models.ForeignKey( 40 | help_text="Reference to the Organization that administers the election.", 41 | null=True, 42 | on_delete=django.db.models.deletion.SET_NULL, 43 | related_name="elections", 44 | to="core.Organization", 45 | ), 46 | ), 47 | migrations.AlterField( 48 | model_name="election", 49 | name="division", 50 | field=models.ForeignKey( 51 | help_text="Reference to the Division that defines the broadest political geography of any contest to be decided by the election.", # noqa 52 | on_delete=django.db.models.deletion.PROTECT, 53 | related_name="elections", 54 | to="core.Division", 55 | ), 56 | ), 57 | ] 58 | -------------------------------------------------------------------------------- /opencivicdata/tests/test_elections.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.django_db 5 | def test_election_str(election): 6 | assert election.name in str(election) 7 | 8 | 9 | @pytest.mark.django_db 10 | def test_election_identifier_str(election_identifier): 11 | assert election_identifier.identifier in str(election_identifier) 12 | 13 | 14 | @pytest.mark.django_db 15 | def test_candidate_contest_str(candidate_contest): 16 | assert candidate_contest.name in str(candidate_contest) 17 | 18 | 19 | @pytest.mark.django_db 20 | def test_candidate_contest_post_str(candidate_contest_post): 21 | assert candidate_contest_post.post.label in str(candidate_contest_post) 22 | 23 | 24 | @pytest.mark.django_db 25 | def test_candidacy_str(candidacy): 26 | assert candidacy.candidate_name in str(candidacy) 27 | 28 | 29 | @pytest.mark.django_db 30 | def test_candidacy_election(candidacy, election): 31 | assert candidacy.election == election 32 | 33 | 34 | @pytest.mark.django_db 35 | def test_candidate_contest_post_candidacies(candidate_contest_post, person): 36 | assert candidate_contest_post.candidacies.count() == 0 37 | 38 | 39 | @pytest.mark.django_db 40 | def test_candidate_contest_identifier_str(candidate_contest_identifier): 41 | assert candidate_contest_identifier.identifier in str(candidate_contest_identifier) 42 | 43 | 44 | @pytest.mark.django_db 45 | def test_ballot_measure_contest_str(ballot_measure_contest): 46 | assert ballot_measure_contest.name in str(ballot_measure_contest) 47 | 48 | 49 | @pytest.mark.django_db 50 | def test_ballot_measure_contest_identifier_str(ballot_measure_contest_identifier): 51 | assert ballot_measure_contest_identifier.identifier in str( 52 | ballot_measure_contest_identifier 53 | ) 54 | 55 | 56 | @pytest.mark.django_db 57 | def test_ballot_measure_contest_option_str(ballot_measure_contest_option): 58 | assert ballot_measure_contest_option.text in str(ballot_measure_contest_option) 59 | 60 | 61 | @pytest.mark.django_db 62 | def test_retention_contest_str(retention_contest): 63 | assert retention_contest.name in str(retention_contest) 64 | 65 | 66 | @pytest.mark.django_db 67 | def test_retention_contest_identifier_str(retention_contest_identifier): 68 | assert retention_contest_identifier.identifier in str(retention_contest_identifier) 69 | 70 | 71 | @pytest.mark.django_db 72 | def test_retention_contest_option_str(retention_contest_option): 73 | assert retention_contest_option.text in str(retention_contest_option) 74 | 75 | 76 | @pytest.mark.django_db 77 | def test_party_contest_str(party_contest): 78 | assert party_contest.name in str(party_contest) 79 | 80 | 81 | @pytest.mark.django_db 82 | def test_party_contest_identifier_str(party_contest_identifier): 83 | assert party_contest_identifier.identifier in str(party_contest_identifier) 84 | 85 | 86 | @pytest.mark.django_db 87 | def test_party_contest_option_str(party_contest_option): 88 | assert party_contest_option.party.name in str(party_contest_option) 89 | -------------------------------------------------------------------------------- /opencivicdata/elections/models/election.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Election-related models. 5 | """ 6 | from django.db import models 7 | from opencivicdata.core.models.base import OCDBase, IdentifierBase, LinkBase, OCDIDField 8 | from opencivicdata.core.models import Division, Organization 9 | 10 | 11 | class Election(OCDBase): 12 | """ 13 | A collection of political contests set to be decided on the same date within a Division. 14 | """ 15 | 16 | id = OCDIDField( 17 | ocd_type="election", 18 | help_text="Open Civic Data-style id in the format ``ocd-election/{{uuid}}``.", 19 | ) 20 | name = models.CharField(max_length=300, help_text="Name of the Election.") 21 | date = models.DateField( 22 | help_text="Final or only date when eligible voters may cast their " 23 | "ballots in the Election. Typically this is also the same " 24 | "date when results of the election's contests are first " 25 | "publicly reported." 26 | ) 27 | division = models.ForeignKey( 28 | Division, 29 | related_name="elections", 30 | help_text="Reference to the Division that defines the broadest political " 31 | "geography of any contest to be decided by the election.", 32 | # divisions should be tough to delete 33 | on_delete=models.PROTECT, 34 | ) 35 | administrative_organization = models.ForeignKey( 36 | Organization, 37 | related_name="elections", 38 | null=True, 39 | help_text="Reference to the Organization that administers the election.", 40 | # shouldn't destroy election if org does go away 41 | on_delete=models.SET_NULL, 42 | ) 43 | 44 | def __str__(self): 45 | return "{0} ({1:%Y-%m-%d})".format(self.name, self.date) 46 | 47 | class Meta: 48 | """ 49 | Model options. 50 | """ 51 | 52 | db_table = "opencivicdata_election" 53 | ordering = ("-date",) 54 | 55 | 56 | class ElectionIdentifier(IdentifierBase): 57 | """ 58 | Upstream identifiers of a Election. 59 | 60 | For example, identfiers assigned by a Secretary of State, county or city 61 | elections office. 62 | """ 63 | 64 | election = models.ForeignKey( 65 | Election, 66 | related_name="identifiers", 67 | help_text="Reference to the Election identified by this alternative identifier.", 68 | on_delete=models.CASCADE, 69 | ) 70 | 71 | class Meta: 72 | db_table = "opencivicdata_electionidentifier" 73 | 74 | def __str__(self): 75 | tmpl = "%s identifies %s" 76 | return tmpl % (self.identifier, self.election) 77 | 78 | 79 | class ElectionSource(LinkBase): 80 | """ 81 | Source used in assembling a Election. 82 | """ 83 | 84 | election = models.ForeignKey( 85 | Election, 86 | related_name="sources", 87 | help_text="Reference to the Election this source verifies.", 88 | on_delete=models.CASCADE, 89 | ) 90 | 91 | class Meta: 92 | db_table = "opencivicdata_electionsource" 93 | -------------------------------------------------------------------------------- /opencivicdata/core/admin/person.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from django.contrib import admin 3 | from django.utils.safestring import mark_safe 4 | from .. import models 5 | from .base import ( 6 | ModelAdmin, 7 | ReadOnlyTabularInline, 8 | IdentifierInline, 9 | ContactDetailInline, 10 | OtherNameInline, 11 | ) 12 | 13 | 14 | class PersonIdentifierInline(IdentifierInline): 15 | model = models.PersonIdentifier 16 | 17 | 18 | class PersonNameInline(OtherNameInline): 19 | model = models.PersonName 20 | 21 | 22 | class PersonContactDetailInline(ContactDetailInline): 23 | model = models.PersonContactDetail 24 | 25 | 26 | class PersonLinkInline(ReadOnlyTabularInline): 27 | readonly_fields = ("url", "note") 28 | model = models.PersonLink 29 | 30 | 31 | class PersonSourceInline(ReadOnlyTabularInline): 32 | readonly_fields = ("url", "note") 33 | model = models.PersonSource 34 | 35 | 36 | class MembershipInline(ReadOnlyTabularInline): 37 | model = models.Membership 38 | readonly_fields = ("organization", "post", "label", "role", "start_date") 39 | fields = ("id",) + readonly_fields + ("end_date",) 40 | exclude = ("id",) 41 | extra = 0 42 | can_delete = False 43 | 44 | 45 | # TODO field locking 46 | @admin.register(models.Person) 47 | class PersonAdmin(ModelAdmin): 48 | search_fields = ["name"] 49 | readonly_fields = ("id", "name", "extras") 50 | fields = ( 51 | "name", 52 | "id", 53 | "image", 54 | ("birth_date", "death_date"), 55 | ("gender", "national_identity", "sort_name", "summary"), 56 | "biography", 57 | "extras", 58 | ) 59 | ordering = ("name",) 60 | list_filter = ("memberships__organization__jurisdiction__name",) 61 | 62 | inlines = [ 63 | PersonIdentifierInline, 64 | PersonNameInline, 65 | PersonContactDetailInline, 66 | PersonLinkInline, 67 | PersonSourceInline, 68 | MembershipInline, 69 | ] 70 | 71 | def get_memberships(self, obj): 72 | memberships = obj.memberships.select_related("organization__jurisdiction") 73 | html = [] 74 | SHOW_N = 5 75 | for memb in memberships[:SHOW_N]: 76 | org = memb.organization 77 | admin_url = reverse("admin:core_organization_change", args=(org.pk,)) 78 | tmpl = '%s%s\n' 79 | html.append( 80 | tmpl 81 | % ( 82 | admin_url, 83 | ( 84 | memb.organization.jurisdiction.name + ": " 85 | if memb.organization.jurisdiction 86 | else "" 87 | ), 88 | memb.organization.name, 89 | ) 90 | ) 91 | more = len(memberships) - SHOW_N 92 | if 0 < more: 93 | html.append("And %d more" % more) 94 | return mark_safe("
".join(html)) 95 | 96 | get_memberships.short_description = "Memberships" 97 | 98 | list_display = ("name", "id", "get_memberships") 99 | -------------------------------------------------------------------------------- /opencivicdata/core/management/commands/loaddivisions.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | from django.db import transaction 4 | from django.core.management.base import BaseCommand 5 | 6 | from opencivicdata.divisions import Division as FileDivision 7 | from ...models import Division 8 | 9 | 10 | def to_db(fd): 11 | """ convert a FileDivision to a Division """ 12 | args, _ = Division.subtypes_from_id(fd.id) 13 | if fd.sameAs: 14 | args["redirect_id"] = fd.sameAs 15 | return Division(id=fd.id, name=fd.name, **args) 16 | 17 | 18 | def load_divisions(country, bulk=False, sync=False): 19 | existing_divisions = Division.objects.filter(country=country) 20 | 21 | country_division = FileDivision.get("ocd-division/country:{}".format(country)) 22 | objects = [to_db(country_division)] 23 | 24 | for child in country_division.children(levels=100): 25 | objects.append(to_db(child)) 26 | 27 | print( 28 | "{} divisions found in the CSV, and {} already in the DB".format( 29 | len(objects), existing_divisions.count() 30 | ) 31 | ) 32 | 33 | objects_set = set(objects) 34 | existing_divisions_set = set(existing_divisions) 35 | 36 | if objects_set == existing_divisions_set: 37 | print("The CSV and the DB contents are exactly the same; no work to be done!") 38 | elif not sync and objects_set.issubset(existing_divisions_set): 39 | print("The DB contains all CSV contents; no work to be done!") 40 | else: 41 | if bulk: 42 | # delete old ids and add new ones all at once 43 | with transaction.atomic(): 44 | Division.objects.filter(country=country).delete() 45 | Division.objects.bulk_create(objects, batch_size=10000) 46 | print("{} divisions created".format(len(objects))) 47 | else: 48 | to_create = objects_set - existing_divisions_set 49 | to_delete = existing_divisions_set - objects_set 50 | # delete removed ids and add new ones all at once 51 | with transaction.atomic(): 52 | for division in to_delete: 53 | division.delete() 54 | for division in to_create: 55 | division.save() 56 | print("{} divisions deleted".format(len(to_delete))) 57 | print("{} divisions created".format(len(to_create))) 58 | 59 | 60 | class Command(BaseCommand): 61 | help = "initialize a pupa database" 62 | 63 | def add_arguments(self, parser): 64 | parser.add_argument("countries", nargs="+", type=str) 65 | parser.add_argument( 66 | "--bulk", 67 | action="store_true", 68 | help="Use bulk_create to add divisions. *Warning* This deletes any existing divisions.", 69 | ) 70 | parser.add_argument( 71 | "--sync", 72 | action="store_true", 73 | help="Add divisions from a CSV file, and delete existing divisions that are not in the " 74 | "CSV file. This option only makes sense with a single country.", 75 | ) 76 | 77 | def handle(self, *args, **options): 78 | for country in options["countries"]: 79 | load_divisions(country, options["bulk"], options["sync"]) 80 | -------------------------------------------------------------------------------- /opencivicdata/tests/fixtures/country-in-subset.csv: -------------------------------------------------------------------------------- 1 | id,name,sameAs,sameAsNote 2 | ocd-division/country:in,India,, 3 | ocd-division/country:in/cd:anglo-indian_reserved_seats_1,Nominated Lok Sabha constituency Anglo-Indian reserved seat 1,, 4 | ocd-division/country:in/cd:anglo-indian_reserved_seats_2,Nominated Lok Sabha constituency Anglo-Indian reserved seat 2,, 5 | ocd-division/country:in/lok_sabha:anglo-indian_reserved_seats_1,Nominated Lok Sabha constituency Anglo-Indian reserved seat 1,ocd-division/country:in/cd:anglo-indian_reserved_seats_1,Added for backwards compatibility with lok_sabha identifiers 6 | ocd-division/country:in/lok_sabha:anglo-indian_reserved_seats_2,Nominated Lok Sabha constituency Anglo-Indian reserved seat 2,ocd-division/country:in/cd:anglo-indian_reserved_seats_2,Added for backwards compatibility with lok_sabha identifiers 7 | ocd-division/country:in/state:ap,Andhra Pradesh,, 8 | ocd-division/country:in/state:ap/cd:amalapuram,Andhra Pradesh Lok Sabha constituency Amalapuram (SC),, 9 | ocd-division/country:in/state:ap/cd:anakapalli,Andhra Pradesh Lok Sabha constituency Anakapalli,, 10 | ocd-division/country:in/state:ap/cd:anantapur,Andhra Pradesh Lok Sabha constituency Anantapur,, 11 | ocd-division/country:in/state:ap/cd:araku,Andhra Pradesh Lok Sabha constituency Araku (ST),, 12 | ocd-division/country:in/state:ap/cd:bapatla,Andhra Pradesh Lok Sabha constituency Bapatla (SC),, 13 | ocd-division/country:in/state:ap/cd:chittoor,Andhra Pradesh Lok Sabha constituency Chittoor (SC),, 14 | ocd-division/country:in/state:ap/cd:eluru,Andhra Pradesh Lok Sabha constituency Eluru,, 15 | ocd-division/country:in/state:ap/cd:guntur,Andhra Pradesh Lok Sabha constituency Guntur,, 16 | ocd-division/country:in/state:ap/cd:hindupur,Andhra Pradesh Lok Sabha constituency Hindupur,, 17 | ocd-division/country:in/state:ap/cd:kadapa,Andhra Pradesh Lok Sabha constituency Kadapa,, 18 | ocd-division/country:in/state:ap/cd:kakinada,Andhra Pradesh Lok Sabha constituency Kakinada,, 19 | ocd-division/country:in/state:ap/cd:kurnool,Andhra Pradesh Lok Sabha constituency Kurnool,, 20 | ocd-division/country:in/state:ap/cd:machilipatnam,Andhra Pradesh Lok Sabha constituency Machilipatnam,, 21 | ocd-division/country:in/state:ap/cd:nandyal,Andhra Pradesh Lok Sabha constituency Nandyal,, 22 | ocd-division/country:in/state:ap/cd:narasapuram,Andhra Pradesh Lok Sabha constituency Narasapuram,, 23 | ocd-division/country:in/state:ap/cd:narasaraopet,Andhra Pradesh Lok Sabha constituency Narasaraopet,, 24 | ocd-division/country:in/state:ap/cd:nellore,Andhra Pradesh Lok Sabha constituency Nellore,, 25 | ocd-division/country:in/state:ap/cd:ongole,Andhra Pradesh Lok Sabha constituency Ongole,, 26 | ocd-division/country:in/state:ap/cd:rajahmundry,Andhra Pradesh Lok Sabha constituency Rajahmundry,, 27 | ocd-division/country:in/state:ap/cd:rajampet,Andhra Pradesh Lok Sabha constituency Rajampet,, 28 | ocd-division/country:in/state:ap/cd:srikakulam,Andhra Pradesh Lok Sabha constituency Srikakulam,, 29 | ocd-division/country:in/state:ap/cd:tirupati,Andhra Pradesh Lok Sabha constituency Tirupati (SC),, 30 | ocd-division/country:in/state:ap/cd:vijayawada,Andhra Pradesh Lok Sabha constituency Vijayawada,, 31 | ocd-division/country:in/state:ap/cd:visakhapatnam,Andhra Pradesh Lok Sabha constituency Visakhapatnam,, 32 | ocd-division/country:in/state:ap/cd:vizianagaram,Andhra Pradesh Lok Sabha constituency Vizianagaram,, 33 | -------------------------------------------------------------------------------- /opencivicdata/elections/migrations/0005_auto_20170823_1648.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-08-23 16:48 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [("elections", "0004_field_docs")] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="candidacy", 16 | name="party", 17 | field=models.ForeignKey( 18 | help_text="Reference to the Organization representing the political party that nominated the candidate or would nominate the candidate (as in the case of a partisan primary).", # noqa 19 | null=True, 20 | on_delete=django.db.models.deletion.CASCADE, 21 | related_name="candidacies", 22 | to="core.Organization", 23 | ), 24 | ), 25 | migrations.AlterField( 26 | model_name="candidacy", 27 | name="post", 28 | field=models.ForeignKey( 29 | help_text="Reference to Post representing the public office for which the candidate is seeking election.", # noqa 30 | on_delete=django.db.models.deletion.CASCADE, 31 | related_name="candidacies", 32 | to="core.Post", 33 | ), 34 | ), 35 | migrations.AlterField( 36 | model_name="candidatecontest", 37 | name="party", 38 | field=models.ForeignKey( 39 | help_text="If the contest is among candidates of the same political party, e.g., a partisan primary election, reference to the Organization representing that party.", # noqa 40 | null=True, 41 | on_delete=django.db.models.deletion.CASCADE, 42 | related_name="candidate_contests", 43 | to="core.Organization", 44 | ), 45 | ), 46 | migrations.AlterField( 47 | model_name="candidatecontestpost", 48 | name="post", 49 | field=models.ForeignKey( 50 | help_text="Reference to the Post representing a public office at stake in the CandidateContest.", # noqa 51 | on_delete=django.db.models.deletion.CASCADE, 52 | related_name="contests", 53 | to="core.Post", 54 | ), 55 | ), 56 | migrations.AlterField( 57 | model_name="partycontestoption", 58 | name="contest", 59 | field=models.ForeignKey( 60 | help_text="Reference to the PartyContest in which the party is an option.", 61 | on_delete=django.db.models.deletion.CASCADE, 62 | related_name="parties", 63 | to="elections.PartyContest", 64 | ), 65 | ), 66 | migrations.AlterField( 67 | model_name="partycontestoption", 68 | name="party", 69 | field=models.ForeignKey( 70 | help_text="Reference to an Organization representing a political party voters may choose in the contest.", # noqa 71 | on_delete=django.db.models.deletion.CASCADE, 72 | related_name="party_contests", 73 | to="core.Organization", 74 | ), 75 | ), 76 | ] 77 | -------------------------------------------------------------------------------- /opencivicdata/divisions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import re 5 | import io 6 | import csv 7 | from urllib.request import urlopen 8 | 9 | PWD = os.path.abspath(os.path.dirname(__file__)) 10 | OCD_REMOTE_URL = ( 11 | "https://raw.githubusercontent.com/opencivicdata/ocd-division-ids/master/" 12 | "identifiers/country-{}.csv" 13 | ) 14 | 15 | 16 | class Division(object): 17 | _cache = {} 18 | 19 | @classmethod 20 | def all(self, country, from_csv=None): 21 | file_handle = None 22 | 23 | # Load from CSV if `from_csv` or `OCD_DIVISION_CSV` are set. 24 | if from_csv or "OCD_DIVISION_CSV" in os.environ: 25 | if not from_csv: 26 | from_csv = os.environ.get("OCD_DIVISION_CSV").format(country) 27 | try: 28 | file_handle = io.open(from_csv, encoding="utf8") 29 | except FileNotFoundError: 30 | raise ValueError("Couldn't open CSV file {}".format(from_csv)) 31 | 32 | # Load from URL otherwise. 33 | if not file_handle: 34 | file_handle = io.StringIO( 35 | urlopen(OCD_REMOTE_URL.format(country)).read().decode("utf-8") 36 | ) 37 | 38 | for row in csv.DictReader(file_handle): 39 | yield Division(**row) 40 | 41 | @classmethod 42 | def get(self, division, from_csv=None): 43 | if division not in self._cache: 44 | # Figure out the country. 45 | if not re.match(r"ocd-division/country:\w{2}", division): 46 | raise ValueError("Invalid OCD format.") 47 | country = re.findall(r"country:(\w{2})", division)[0] 48 | 49 | # Load all divisions into cache. 50 | for d in self.all(country, from_csv): 51 | pass 52 | 53 | if division not in self._cache: 54 | raise ValueError("Division not found: {}".format(division)) 55 | 56 | return self._cache[division] 57 | 58 | def __init__(self, id, name, **kwargs): 59 | self._cache[id] = self 60 | self.id = id 61 | self.name = name 62 | self.sameAs = kwargs.pop("sameAs", None) 63 | valid_through = kwargs.pop("validThrough", None) 64 | if valid_through: 65 | self.valid_through = valid_through 66 | 67 | # set parent and _type 68 | parent, own_id = id.rsplit("/", 1) 69 | if parent == "ocd-division": 70 | self.parent = None 71 | else: 72 | self.parent = self._cache.get(parent) 73 | if self.parent: 74 | self.parent._children.append(self) 75 | else: 76 | # TODO: keep a list of unassigned parents for later reconciliation 77 | pass 78 | 79 | self._type = own_id.split(":")[0] 80 | 81 | # other attrs 82 | self.attrs = kwargs 83 | self.names = [] 84 | self._children = [] 85 | 86 | def children(self, _type=None, duplicates=True, levels=1): 87 | for d in self._children: 88 | if (not _type or d._type == _type) and (duplicates or not d.sameAs): 89 | yield d 90 | if levels > 1: 91 | for c in d.children(_type, duplicates, levels - 1): 92 | yield c 93 | 94 | def __str__(self): 95 | return "{} - {}".format(self.id, self.name) 96 | -------------------------------------------------------------------------------- /opencivicdata/legislative/admin/event.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.template import defaultfilters 3 | from opencivicdata.core.admin import base 4 | from .. import models 5 | 6 | 7 | @admin.register(models.EventLocation) 8 | class EventLocationAdmin(admin.ModelAdmin): 9 | pass 10 | 11 | 12 | class EventLinkInline(base.LinkInline): 13 | model = models.EventLink 14 | 15 | 16 | class EventSourceInline(base.LinkInline): 17 | model = models.EventSource 18 | 19 | 20 | class EventParticipantInline(base.RelatedEntityInline): 21 | model = models.EventParticipant 22 | readonly_fields = ("organization", "person") 23 | 24 | 25 | @admin.register(models.Event) 26 | class EventAdmin(admin.ModelAdmin): 27 | readonly_fields = ("jurisdiction", "location") 28 | fields = ( 29 | "name", 30 | "jurisdiction", 31 | "location", 32 | "description", 33 | "classification", 34 | "status", 35 | ("start_date", "end_date", "all_day"), 36 | ) 37 | 38 | def source_link(self, obj): 39 | source = obj.sources.filter(url__icontains="meetingdetail").get() 40 | tmpl = u'View source' 41 | return tmpl.format(source.url) 42 | 43 | source_link.short_description = "View source" 44 | source_link.allow_tags = True 45 | 46 | list_display = ("jurisdiction", "name", "start_date", "source_link") 47 | 48 | inlines = [EventLinkInline, EventSourceInline, EventParticipantInline] 49 | 50 | 51 | @admin.register(models.EventMedia) 52 | class EventMediaAdmin(admin.ModelAdmin): 53 | pass 54 | 55 | 56 | @admin.register(models.EventDocument) 57 | class EventDocumentAdmin(admin.ModelAdmin): 58 | readonly_fields = ("event",) 59 | list_display = ("event", "date", "note") 60 | 61 | 62 | # @admin.register(models.EventDocumentLink) 63 | # class EventDocumentLinkAdmin(base.MimetypeLinkAdmin): 64 | # readonly_fields = ('document',) 65 | # list_display = ('document', 'media_type', 'url',) 66 | 67 | 68 | @admin.register(models.EventSource) 69 | class EventSourceAdmin(admin.ModelAdmin): 70 | readonly_fields = ("event",) 71 | 72 | 73 | @admin.register(models.EventParticipant) 74 | class EventParticipantAdmin(admin.ModelAdmin): 75 | pass 76 | 77 | 78 | @admin.register(models.EventAgendaItem) 79 | class EventAgendaItemAdmin(admin.ModelAdmin): 80 | readonly_fields = ("event",) 81 | fields = ("event", "description", "classification", "order", "subjects", "notes") 82 | 83 | def get_truncated_description(self, obj): 84 | return defaultfilters.truncatewords(obj.description, 25) 85 | 86 | get_truncated_description.short_description = "Description" 87 | 88 | def get_truncated_event_name(self, obj): 89 | return defaultfilters.truncatewords(obj.event.name, 8) 90 | 91 | get_truncated_event_name.short_description = "Event Name" 92 | 93 | list_display = ("get_truncated_event_name", "get_truncated_description") 94 | 95 | 96 | @admin.register(models.EventRelatedEntity) 97 | class EventRelatedEntityAdmin(admin.ModelAdmin): 98 | pass 99 | 100 | 101 | @admin.register(models.EventAgendaMedia) 102 | class EventAgendaMediaAdmin(admin.ModelAdmin): 103 | pass 104 | 105 | 106 | @admin.register(models.EventAgendaMediaLink) 107 | class EventAgendaMediaLinkAdmin(admin.ModelAdmin): 108 | pass 109 | -------------------------------------------------------------------------------- /opencivicdata/core/admin/organization.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from django.contrib import admin 3 | from .. import models 4 | from .base import ( 5 | ModelAdmin, 6 | ReadOnlyTabularInline, 7 | IdentifierInline, 8 | LinkInline, 9 | ContactDetailInline, 10 | OtherNameInline, 11 | ) 12 | 13 | 14 | class OrganizationIdentifierInline(IdentifierInline): 15 | model = models.OrganizationIdentifier 16 | 17 | 18 | class OrganizationNameInline(OtherNameInline): 19 | model = models.OrganizationName 20 | 21 | 22 | class OrganizationContactDetailInline(ContactDetailInline): 23 | model = models.OrganizationContactDetail 24 | 25 | 26 | class OrganizationLinkInline(LinkInline): 27 | model = models.OrganizationLink 28 | 29 | 30 | class OrganizationSourceInline(LinkInline): 31 | model = models.OrganizationSource 32 | 33 | 34 | class PostInline(admin.TabularInline): 35 | """ a read-only inline for posts here, with links to the real thing """ 36 | 37 | model = models.Post 38 | extra = 0 39 | fields = readonly_fields = ("label", "role") 40 | ordering = ("label",) 41 | can_delete = False 42 | show_change_link = True 43 | 44 | def has_add_permission(self, request, obj=None): 45 | return False 46 | 47 | 48 | class OrgMembershipInline(ReadOnlyTabularInline): 49 | model = models.Membership 50 | fk_name = "organization" 51 | readonly_fields = ("id", "person", "post", "label", "role", "start_date") 52 | fields = readonly_fields + ("end_date",) 53 | extra = 0 54 | can_delete = False 55 | 56 | 57 | @admin.register(models.Organization) 58 | class OrganizationAdmin(ModelAdmin): 59 | readonly_fields = ( 60 | "id", 61 | "name", 62 | "classification", 63 | "parent", 64 | "jurisdiction", 65 | "extras", 66 | ) 67 | fields = readonly_fields + (("founding_date", "dissolution_date"), "image") 68 | search_fields = ("name",) 69 | list_filter = ("jurisdiction__name",) 70 | 71 | inlines = [ 72 | OrganizationIdentifierInline, 73 | OrganizationNameInline, 74 | OrganizationContactDetailInline, 75 | OrganizationLinkInline, 76 | OrganizationSourceInline, 77 | PostInline, 78 | OrgMembershipInline, 79 | ] 80 | 81 | def get_org_name(self, obj): 82 | parent = obj.parent 83 | if parent: 84 | return "{org} ({parent})".format(org=obj.name, parent=parent.name) 85 | return obj.name 86 | 87 | get_org_name.short_description = "Name" 88 | get_org_name.allow_tags = True 89 | get_org_name.admin_order_field = "name" 90 | 91 | def get_jurisdiction(self, obj): 92 | jurisdiction = obj.jurisdiction 93 | if jurisdiction: 94 | admin_url = reverse( 95 | "admin:core_jurisdiction_change", args=(jurisdiction.pk,) 96 | ) 97 | tmpl = '%s' 98 | return tmpl % (admin_url, jurisdiction.name) 99 | 100 | return "(none)" 101 | 102 | get_jurisdiction.short_description = "Jurisdiction" 103 | get_jurisdiction.allow_tags = True 104 | get_jurisdiction.admin_order_field = "jurisdiction__name" 105 | 106 | list_select_related = ("jurisdiction",) 107 | list_display = ("get_org_name", "get_jurisdiction", "classification") 108 | ordering = ("name",) 109 | -------------------------------------------------------------------------------- /opencivicdata/elections/models/contests/party.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | PartyContest-related models. 5 | """ 6 | from django.db import models 7 | from opencivicdata.core.models.base import IdentifierBase, LinkBase 8 | from opencivicdata.core.models import Organization 9 | from .base import ContestBase 10 | 11 | 12 | class PartyContest(ContestBase): 13 | """ 14 | A contest in which voters can vote directly for a political party. 15 | 16 | In these contests, voters can vote for a party in lieu of/in addition to 17 | voting for candidates endorsed by that party (as in the case of party-list 18 | proportional representation). 19 | """ 20 | 21 | runoff_for_contest = models.OneToOneField( 22 | "self", 23 | null=True, 24 | on_delete=models.SET_NULL, 25 | help_text="If this contest is a runoff to determine the outcome of a previously " 26 | "undecided contest, reference to that PartyContest.", 27 | ) 28 | 29 | class Meta: 30 | db_table = "opencivicdata_partycontest" 31 | 32 | 33 | class PartyContestOption(models.Model): 34 | """ 35 | A party (i.e., Organization) voters choose in a PartyContest. 36 | """ 37 | 38 | contest = models.ForeignKey( 39 | PartyContest, 40 | related_name="parties", 41 | on_delete=models.CASCADE, 42 | help_text="Reference to the PartyContest in which the party is an option.", 43 | ) 44 | party = models.ForeignKey( 45 | Organization, 46 | related_name="party_contests", 47 | on_delete=models.CASCADE, 48 | limit_choices_to={"classification": "party"}, 49 | help_text="Reference to an Organization representing a political party " 50 | "voters may choose in the contest.", 51 | ) 52 | is_incumbent = models.NullBooleanField( 53 | help_text="Indicates whether the party currently holds majority power." 54 | ) 55 | 56 | def __str__(self): 57 | return "{0} for {1}".format(self.party.name, self.contest) 58 | 59 | class Meta: 60 | """ 61 | Model options. 62 | """ 63 | 64 | db_table = "opencivicdata_partycontestoption" 65 | ordering = ("contest", "party") 66 | 67 | 68 | class PartyContestIdentifier(IdentifierBase): 69 | """ 70 | Upstream identifiers of a PartyMeasureContest. 71 | 72 | For example, identfiers assigned by a Secretary of State, county or city 73 | elections office. 74 | """ 75 | 76 | contest = models.ForeignKey( 77 | PartyContest, 78 | related_name="identifiers", 79 | on_delete=models.CASCADE, 80 | help_text="Reference to the PartyContest linked to the upstream " "identifier.", 81 | ) 82 | 83 | def __str__(self): 84 | tmpl = "%s identifies %s" 85 | return tmpl % (self.identifier, self.contest) 86 | 87 | class Meta: 88 | db_table = "opencivicdata_partyidentifier" 89 | 90 | 91 | class PartyContestSource(LinkBase): 92 | """ 93 | Source used in assembling a PartyContest. 94 | """ 95 | 96 | contest = models.ForeignKey( 97 | PartyContest, 98 | related_name="sources", 99 | on_delete=models.CASCADE, 100 | help_text="Reference to the PartyContest assembled from the source.", 101 | ) 102 | 103 | class Meta: 104 | db_table = "opencivicdata_partysource" 105 | -------------------------------------------------------------------------------- /opencivicdata/elections/admin/contests/ballot_measure.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Custom administration panels for OpenCivicData election contest models. 5 | """ 6 | from django.contrib import admin 7 | from opencivicdata.core.admin import base 8 | from ... import models 9 | 10 | 11 | class BallotMeasureContestIdentifierInline(base.IdentifierInline): 12 | """ 13 | Custom inline administrative panel for BallotMeasureContestIdentifier model. 14 | """ 15 | 16 | model = models.BallotMeasureContestIdentifier 17 | 18 | 19 | class BallotMeasureContestSourceInline(base.LinkInline): 20 | """ 21 | Custom inline administrative panel for the BallotMeasureContestSource model. 22 | """ 23 | 24 | model = models.BallotMeasureContestSource 25 | 26 | 27 | class BallotMeasureContestOptionInline(admin.TabularInline): 28 | """ 29 | Custom inline administrative panel for BallotMeasureContestOption model. 30 | """ 31 | 32 | model = models.BallotMeasureContestOption 33 | extra = 0 34 | 35 | 36 | @admin.register(models.BallotMeasureContest) 37 | class BallotMeasureContestAdmin(base.ModelAdmin): 38 | """ 39 | Custom administrative panel for the BallotMeasureContest model. 40 | """ 41 | 42 | readonly_fields = ("id", "created_at", "updated_at") 43 | raw_id_fields = ("division",) 44 | fields = ( 45 | ("name", "election", "description", "requirement", "classification") 46 | + raw_id_fields 47 | + readonly_fields 48 | ) 49 | list_display = ("name", "election", "division_name", "id", "updated_at") 50 | search_fields = ("name", "election__name") 51 | list_filter = ("updated_at", "classification") 52 | date_hierarchy = "election__date" 53 | 54 | def division_name(self, obj): 55 | """ 56 | Returns the name of the Division for the Contest. 57 | """ 58 | return obj.division.name 59 | 60 | inlines = [ 61 | BallotMeasureContestOptionInline, 62 | BallotMeasureContestIdentifierInline, 63 | BallotMeasureContestSourceInline, 64 | ] 65 | 66 | 67 | class RetentionContestIdentifierInline(base.IdentifierInline): 68 | """ 69 | Custom inline administrative panel for RetentionContestIdentifier model. 70 | """ 71 | 72 | model = models.RetentionContestIdentifier 73 | 74 | 75 | class RetentionContestSourceInline(base.LinkInline): 76 | """ 77 | Custom inline administrative panel for the RetentionContestSource model. 78 | """ 79 | 80 | model = models.RetentionContestSource 81 | 82 | 83 | class RetentionContestOptionInline(admin.TabularInline): 84 | """ 85 | Custom inline administrative panel for RetentionContestOption model. 86 | """ 87 | 88 | model = models.RetentionContestOption 89 | extra = 0 90 | 91 | 92 | @admin.register(models.RetentionContest) 93 | class RetentionContestBaseAdmin(base.ModelAdmin): 94 | """ 95 | Custom administrative panel for the RetentionContest model. 96 | """ 97 | 98 | readonly_fields = ("id", "created_at", "updated_at") 99 | raw_id_fields = ("membership", "division", "runoff_for_contest") 100 | fields = ( 101 | ("name", "description", "requirement", "election") 102 | + raw_id_fields 103 | + readonly_fields 104 | ) 105 | list_display = ("name", "membership", "election", "id", "updated_at") 106 | search_fields = ( 107 | "name", 108 | "membership__person__name", 109 | "membership__role", 110 | "election__name", 111 | ) 112 | list_filter = ("updated_at",) 113 | date_hierarchy = "election__date" 114 | 115 | inlines = [ 116 | RetentionContestOptionInline, 117 | RetentionContestIdentifierInline, 118 | RetentionContestSourceInline, 119 | ] 120 | -------------------------------------------------------------------------------- /opencivicdata/legislative/models/vote.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.postgres.fields import ArrayField 3 | 4 | from opencivicdata.core.models.base import OCDBase, LinkBase, OCDIDField, RelatedBase 5 | from opencivicdata.core.models import Organization, Person 6 | from .session import LegislativeSession 7 | from .bill import Bill, BillAction 8 | from ... import common 9 | 10 | 11 | class VoteEvent(OCDBase): 12 | id = OCDIDField(ocd_type="vote") 13 | identifier = models.CharField(max_length=300, blank=True) 14 | motion_text = models.TextField() 15 | # enum 16 | motion_classification = ArrayField( 17 | base_field=models.TextField(), blank=True, default=list 18 | ) 19 | start_date = models.CharField(max_length=25) # YYYY-MM-DD HH:MM:SS+HH:MM 20 | end_date = models.CharField(max_length=25, blank=True) # YYYY-MM-DD HH:MM:SS+HH:MM 21 | 22 | result = models.CharField(max_length=50, choices=common.VOTE_RESULT_CHOICES) 23 | organization = models.ForeignKey( 24 | Organization, 25 | related_name="votes", 26 | # make parent org hard to protect 27 | on_delete=models.PROTECT, 28 | ) 29 | legislative_session = models.ForeignKey( 30 | LegislativeSession, 31 | related_name="votes", 32 | # make legislative session hard to delete 33 | on_delete=models.PROTECT, 34 | ) 35 | bill = models.ForeignKey( 36 | Bill, 37 | related_name="votes", 38 | null=True, 39 | # if a bill was linked, the vote isn't meaningful without it 40 | on_delete=models.CASCADE, 41 | ) 42 | bill_action = models.OneToOneField( 43 | BillAction, 44 | related_name="vote", 45 | null=True, 46 | default=None, 47 | # if an action goes away - VoteEvent should stay 48 | on_delete=models.SET_NULL, 49 | ) 50 | 51 | extras = models.JSONField(default=dict, blank=True) 52 | 53 | def __str__(self): 54 | if self.identifier: 55 | return "{} in {}".format(self.identifier, self.legislative_session) 56 | else: 57 | return "{} on {}".format(self.motion_text, self.bill) 58 | 59 | class Meta: 60 | db_table = "opencivicdata_voteevent" 61 | index_together = [ 62 | ["legislative_session", "identifier", "bill"], 63 | ["legislative_session", "bill"], 64 | ] 65 | 66 | 67 | class VoteCount(RelatedBase): 68 | vote_event = models.ForeignKey( 69 | VoteEvent, related_name="counts", on_delete=models.CASCADE 70 | ) 71 | option = models.CharField(max_length=50, choices=common.VOTE_OPTION_CHOICES) 72 | value = models.PositiveIntegerField() 73 | 74 | def __str__(self): 75 | return "{0} for {1}".format(self.value, self.option) 76 | 77 | class Meta: 78 | db_table = "opencivicdata_votecount" 79 | 80 | 81 | class PersonVote(RelatedBase): 82 | vote_event = models.ForeignKey( 83 | VoteEvent, related_name="votes", on_delete=models.CASCADE 84 | ) 85 | option = models.CharField(max_length=50, choices=common.VOTE_OPTION_CHOICES) 86 | voter_name = models.CharField(max_length=300) 87 | voter = models.ForeignKey( 88 | Person, 89 | related_name="votes", 90 | null=True, 91 | # unresolve person if they go away 92 | on_delete=models.SET_NULL, 93 | ) 94 | note = models.TextField(blank=True) 95 | 96 | def __str__(self): 97 | return "{0} voted for {1}".format(self.voter_name, self.option) 98 | 99 | class Meta: 100 | db_table = "opencivicdata_personvote" 101 | 102 | 103 | class VoteSource(LinkBase): 104 | vote_event = models.ForeignKey( 105 | VoteEvent, related_name="sources", on_delete=models.CASCADE 106 | ) 107 | 108 | class Meta: 109 | db_table = "opencivicdata_votesource" 110 | -------------------------------------------------------------------------------- /opencivicdata/core/migrations/0004_auto_20171005_2028.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.5 on 2017-10-05 20:28 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [("core", "0003_field_docs")] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="division", 16 | name="redirect", 17 | field=models.ForeignKey( 18 | null=True, 19 | on_delete=django.db.models.deletion.SET_NULL, 20 | to="core.Division", 21 | ), 22 | ), 23 | migrations.AlterField( 24 | model_name="jurisdiction", 25 | name="division", 26 | field=models.ForeignKey( 27 | help_text="A link to a Division related to this Jurisdiction.", 28 | on_delete=django.db.models.deletion.PROTECT, 29 | related_name="jurisdictions", 30 | to="core.Division", 31 | ), 32 | ), 33 | migrations.AlterField( 34 | model_name="membership", 35 | name="on_behalf_of", 36 | field=models.ForeignKey( 37 | help_text="The Organization on whose behalf the Person is a member of the Organization.", # noqa 38 | null=True, 39 | on_delete=django.db.models.deletion.SET_NULL, 40 | related_name="memberships_on_behalf_of", 41 | to="core.Organization", 42 | ), 43 | ), 44 | migrations.AlterField( 45 | model_name="membership", 46 | name="person", 47 | field=models.ForeignKey( 48 | help_text="A link to the Person that is a member of the Organization.", 49 | null=True, 50 | on_delete=django.db.models.deletion.SET_NULL, 51 | related_name="memberships", 52 | to="core.Person", 53 | ), 54 | ), 55 | migrations.AlterField( 56 | model_name="membership", 57 | name="post", 58 | field=models.ForeignKey( 59 | help_text="\tThe Post held by the member in the Organization.", 60 | null=True, 61 | on_delete=django.db.models.deletion.SET_NULL, 62 | related_name="memberships", 63 | to="core.Post", 64 | ), 65 | ), 66 | migrations.AlterField( 67 | model_name="organization", 68 | name="jurisdiction", 69 | field=models.ForeignKey( 70 | help_text="A link to the Jurisdiction that contains this Organization.", 71 | null=True, 72 | on_delete=django.db.models.deletion.PROTECT, 73 | related_name="organizations", 74 | to="core.Jurisdiction", 75 | ), 76 | ), 77 | migrations.AlterField( 78 | model_name="organization", 79 | name="parent", 80 | field=models.ForeignKey( 81 | help_text="A link to another Organization that serves as this Organization's parent.", # noqa 82 | null=True, 83 | on_delete=django.db.models.deletion.SET_NULL, 84 | related_name="children", 85 | to="core.Organization", 86 | ), 87 | ), 88 | migrations.AlterField( 89 | model_name="post", 90 | name="division", 91 | field=models.ForeignKey( 92 | blank=True, 93 | default=None, 94 | help_text="The Division where the post exists.", 95 | null=True, 96 | on_delete=django.db.models.deletion.SET_NULL, 97 | related_name="posts", 98 | to="core.Division", 99 | ), 100 | ), 101 | ] 102 | -------------------------------------------------------------------------------- /opencivicdata/elections/models/candidacy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Candidacy-related models. 5 | """ 6 | from django.db import models 7 | from opencivicdata.core.models.base import OCDBase, LinkBase, OCDIDField 8 | from opencivicdata.core.models import Person, Post, Organization 9 | 10 | 11 | class Candidacy(OCDBase): 12 | """ 13 | A person seeking election to hold a specific public office for a term. 14 | """ 15 | 16 | id = OCDIDField( 17 | ocd_type="candidacy", 18 | help_text="Open Civic Data-style id in the format ``ocd-candidacy/{{uuid}}``.", 19 | ) 20 | person = models.ForeignKey( 21 | Person, 22 | related_name="candidacies", 23 | help_text="Reference to the Person who is the candidate.", 24 | on_delete=models.CASCADE, 25 | ) 26 | post = models.ForeignKey( 27 | Post, 28 | related_name="candidacies", 29 | help_text="Reference to Post representing the public office for which " 30 | "the candidate is seeking election.", 31 | on_delete=models.PROTECT, 32 | ) 33 | contest = models.ForeignKey( 34 | "elections.CandidateContest", 35 | related_name="candidacies", 36 | help_text="Reference to an OCD CandidateContest representing the contest " 37 | "in which the candidate is competing.", 38 | on_delete=models.CASCADE, 39 | ) 40 | candidate_name = models.CharField( 41 | max_length=300, 42 | help_text="For preserving the candidate's name as it was of the candidacy.", 43 | ) 44 | filed_date = models.DateField( 45 | null=True, help_text="Specifies when the candidate filed for the contest." 46 | ) 47 | is_incumbent = models.NullBooleanField( 48 | help_text="Indicates whether the candidate is seeking re-election to a " 49 | "public office he/she currently holds" 50 | ) 51 | party = models.ForeignKey( 52 | Organization, 53 | related_name="candidacies", 54 | limit_choices_to={"classification": "party"}, 55 | null=True, 56 | help_text="Reference to the Organization representing the political party " 57 | "that nominated the candidate or would nominate the candidate " 58 | "(as in the case of a partisan primary).", 59 | # survive party deletion 60 | on_delete=models.SET_NULL, 61 | ) 62 | REGISTRATION_STATUSES = ( 63 | ("filed", "Filed"), 64 | ("qualified", "Qualified"), 65 | ("withdrawn", "Withdrawn"), 66 | ("write-in", "Write-in"), 67 | ) 68 | registration_status = models.CharField( 69 | max_length=10, 70 | choices=REGISTRATION_STATUSES, 71 | null=True, 72 | help_text="Registration status of the candidate.", 73 | ) 74 | top_ticket_candidacy = models.ForeignKey( 75 | "self", 76 | related_name="ticket", 77 | null=True, 78 | on_delete=models.SET_NULL, 79 | help_text="If the candidate is running as part of ticket, e.g., a Vice " 80 | "Presidential candidate running with a Presidential candidate, " 81 | "reference to candidacy at the top of the ticket.", 82 | ) 83 | 84 | def __str__(self): 85 | return "{0.candidate_name} for {0.contest}".format(self) 86 | 87 | class Meta: 88 | """ 89 | Model options. 90 | """ 91 | 92 | db_table = "opencivicdata_candidacy" 93 | verbose_name_plural = "candidacies" 94 | ordering = ("contest", "post", "person") 95 | 96 | @property 97 | def election(self): 98 | """ 99 | Election in which the person is a candidate. 100 | """ 101 | return self.contest.election 102 | 103 | 104 | class CandidacySource(LinkBase): 105 | """ 106 | Source used in assembling a Candidacy. 107 | """ 108 | 109 | candidacy = models.ForeignKey( 110 | Candidacy, 111 | related_name="sources", 112 | on_delete=models.CASCADE, 113 | help_text="Reference to the assembed Candidacy.", 114 | ) 115 | 116 | class Meta: 117 | db_table = "opencivicdata_candidacysource" 118 | -------------------------------------------------------------------------------- /opencivicdata/legislative/admin/bill.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.template import defaultfilters 3 | from opencivicdata.core.admin.base import ( 4 | ModelAdmin, 5 | ReadOnlyTabularInline, 6 | IdentifierInline, 7 | ) 8 | from .. import models 9 | 10 | 11 | class BillAbstractInline(ReadOnlyTabularInline): 12 | model = models.BillAbstract 13 | readonly_fields = ("abstract", "note") 14 | can_delete = False 15 | 16 | 17 | class BillTitleInline(ReadOnlyTabularInline): 18 | model = models.BillTitle 19 | readonly_fields = ("title", "note") 20 | can_delete = False 21 | 22 | 23 | class BillIdentifierInline(IdentifierInline): 24 | model = models.BillIdentifier 25 | 26 | 27 | class BillActionInline(ReadOnlyTabularInline): 28 | model = models.BillAction 29 | 30 | def get_related_entities(self, obj): 31 | ents = obj.related_entities.all() 32 | ent_list = [e.name for e in ents] 33 | return ", ".join(ent_list) 34 | 35 | get_related_entities.short_description = "Related Entities" 36 | get_related_entities.allow_tags = True 37 | 38 | list_select_related = ("BillActionRelatedEntity",) 39 | readonly_fields = ("date", "organization", "description", "get_related_entities") 40 | 41 | 42 | class RelatedBillInline(ReadOnlyTabularInline): 43 | model = models.RelatedBill 44 | fk_name = "bill" 45 | readonly_fields = fields = ("identifier", "legislative_session", "relation_type") 46 | extra = 0 47 | 48 | 49 | class BillSponsorshipInline(ReadOnlyTabularInline): 50 | model = models.BillSponsorship 51 | readonly_fields = fields = ("name", "primary", "classification") 52 | extra = 0 53 | 54 | 55 | class DocVersionInline(ReadOnlyTabularInline): 56 | model = models.BillVersion 57 | 58 | def get_links(self, obj): 59 | return "
".join( 60 | '{0}'.format(link.url) for link in obj.links.all() 61 | ) 62 | 63 | get_links.short_description = "Links" 64 | get_links.allow_tags = True 65 | 66 | list_select_related = ("BillVersionLink",) 67 | readonly_fields = ("note", "date", "get_links") 68 | 69 | 70 | class BillVersionInline(DocVersionInline): 71 | model = models.BillVersion 72 | 73 | 74 | class BillDocumentInline(DocVersionInline): 75 | model = models.BillDocument 76 | 77 | 78 | class BillSourceInline(ReadOnlyTabularInline): 79 | readonly_fields = ("url", "note") 80 | model = models.BillSource 81 | 82 | 83 | @admin.register(models.Bill) 84 | class BillAdmin(ModelAdmin): 85 | readonly_fields = fields = ( 86 | "identifier", 87 | "legislative_session", 88 | "bill_classifications", 89 | "from_organization", 90 | "title", 91 | "id", 92 | "subject", 93 | "extras", 94 | ) 95 | search_fields = ["identifier", "title"] 96 | list_select_related = ("legislative_session", "legislative_session__jurisdiction") 97 | inlines = [ 98 | BillAbstractInline, 99 | BillTitleInline, 100 | BillIdentifierInline, 101 | BillActionInline, 102 | BillSponsorshipInline, 103 | BillSourceInline, 104 | RelatedBillInline, 105 | BillVersionInline, 106 | BillDocumentInline, 107 | ] 108 | 109 | def bill_classifications(self, obj): 110 | return ", ".join(obj.classification) 111 | 112 | def get_jurisdiction_name(self, obj): 113 | return obj.legislative_session.jurisdiction.name 114 | 115 | get_jurisdiction_name.short_description = "Jurisdiction" 116 | 117 | def get_session_name(self, obj): 118 | return obj.legislative_session.name 119 | 120 | get_session_name.short_description = "Session" 121 | get_session_name.admin_order_field = "legislative_session__name" 122 | 123 | def get_truncated_sponsors(self, obj): 124 | spons = ", ".join(s.name for s in obj.sponsorships.all()[:5]) 125 | return defaultfilters.truncatewords(spons, 10) 126 | 127 | get_truncated_sponsors.short_description = "Sponsors" 128 | 129 | def get_truncated_title(self, obj): 130 | return defaultfilters.truncatewords(obj.title, 25) 131 | 132 | get_truncated_title.short_description = "Title" 133 | 134 | list_display = ( 135 | "identifier", 136 | "get_jurisdiction_name", 137 | "get_session_name", 138 | "get_truncated_sponsors", 139 | "get_truncated_title", 140 | ) 141 | 142 | list_filter = ("legislative_session__jurisdiction__name",) 143 | -------------------------------------------------------------------------------- /opencivicdata/elections/models/contests/candidate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | CandidateContest-related models. 5 | """ 6 | from django.db import models 7 | from opencivicdata.core.models.base import IdentifierBase, LinkBase 8 | from opencivicdata.core.models import Organization, Post 9 | from .base import ContestBase 10 | 11 | 12 | class CandidateContest(ContestBase): 13 | """ 14 | A contest among candidates seeking election to one or more public offices. 15 | """ 16 | 17 | party = models.ForeignKey( 18 | Organization, 19 | related_name="candidate_contests", 20 | limit_choices_to={"classification": "party"}, 21 | null=True, 22 | # survive party deletion 23 | on_delete=models.SET_NULL, 24 | help_text="If the contest is among candidates of the same political party, " 25 | "e.g., a partisan primary election, reference to the Organization " 26 | "representing that party.", 27 | ) 28 | previous_term_unexpired = models.BooleanField( 29 | default=False, 30 | help_text="Indicates the previous public office holder vacated the post " 31 | "before serving a full term.", 32 | ) 33 | number_elected = models.IntegerField( 34 | default=1, 35 | help_text="Number of candidates that are elected in the contest, i.e. 'N' of N-of-M.", 36 | ) 37 | runoff_for_contest = models.OneToOneField( 38 | "self", 39 | related_name="runoff_contest", 40 | null=True, 41 | on_delete=models.SET_NULL, 42 | help_text="If this contest is a runoff to determine the outcome of a " 43 | "previously undecided contest, reference to that CandidateContest.", 44 | ) 45 | 46 | class Meta(ContestBase.Meta): 47 | db_table = "opencivicdata_candidatecontest" 48 | 49 | 50 | class CandidateContestPost(models.Model): 51 | """ 52 | A public office (i.e., Post) at stake in a CandidateContest. 53 | """ 54 | 55 | contest = models.ForeignKey( 56 | CandidateContest, 57 | related_name="posts", 58 | on_delete=models.CASCADE, 59 | help_text="Reference to the CandidateContest in which the Post is at stake.", 60 | ) 61 | post = models.ForeignKey( 62 | Post, 63 | related_name="contests", 64 | on_delete=models.CASCADE, 65 | help_text="Reference to the Post representing a public office at stake in " 66 | "the CandidateContest.", 67 | ) 68 | sort_order = models.IntegerField( 69 | default=0, 70 | help_text="Useful for sorting for contests where two or more public offices " 71 | "are at stake, e.g., in a U.S. presidential contest, the President " 72 | "post would have a lower sort order than the Vice President post.", 73 | ) 74 | 75 | def __str__(self): 76 | return "{0} in {1}".format(self.post.label, self.contest) 77 | 78 | @property 79 | def candidacies(self): 80 | """ 81 | List of candidacies for the Post in a CandidateContest. 82 | """ 83 | return self.contest.candidacies.filter(post=self.post) 84 | 85 | class Meta: 86 | """ 87 | Model options. 88 | """ 89 | 90 | ordering = ("contest", "sort_order") 91 | db_table = "opencivicdata_candidatecontestpost" 92 | 93 | 94 | class CandidateContestIdentifier(IdentifierBase): 95 | """ 96 | Upstream identifiers of a CandidateContest. 97 | 98 | For example, identfiers assigned by a Secretary of State, county or city 99 | elections office. 100 | """ 101 | 102 | contest = models.ForeignKey( 103 | CandidateContest, 104 | related_name="identifiers", 105 | on_delete=models.CASCADE, 106 | help_text="Reference to the CandidateContest linked to the upstream identifier.", 107 | ) 108 | 109 | def __str__(self): 110 | tmpl = "%s identifies %s" 111 | return tmpl % (self.identifier, self.contest) 112 | 113 | class Meta: 114 | db_table = "opencivicdata_candidatecontestidentifier" 115 | 116 | 117 | class CandidateContestSource(LinkBase): 118 | """ 119 | Source used in assembling a CandidateContest. 120 | """ 121 | 122 | contest = models.ForeignKey( 123 | CandidateContest, 124 | related_name="sources", 125 | on_delete=models.CASCADE, 126 | help_text="Reference to the CandidateContest assembled from the source.", 127 | ) 128 | 129 | class Meta: 130 | db_table = "opencivicdata_candidatecontestsource" 131 | -------------------------------------------------------------------------------- /opencivicdata/core/models/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import re 3 | import uuid 4 | from django.db import models 5 | from django.contrib.postgres.fields import ArrayField 6 | from django.core.validators import RegexValidator 7 | 8 | from ... import common 9 | 10 | 11 | class OCDIDField(models.CharField): 12 | def __init__(self, *args, **kwargs): 13 | self.ocd_type = kwargs.pop("ocd_type") 14 | if self.ocd_type != "jurisdiction": 15 | kwargs["default"] = lambda: "ocd-{}/{}".format(self.ocd_type, uuid.uuid4()) 16 | # len('ocd-') + len(ocd_type) + len('/') + len(uuid) 17 | # = 4 + len(ocd_type) + 1 + 36 18 | # = len(ocd_type) + 41 19 | kwargs["max_length"] = 41 + len(self.ocd_type) 20 | regex = ( 21 | "^ocd-" + self.ocd_type + "/[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$" 22 | ) 23 | else: 24 | kwargs["max_length"] = 300 25 | regex = common.JURISDICTION_ID_REGEX 26 | 27 | kwargs["primary_key"] = True 28 | # get pattern property if it exists, otherwise just return the object (hopefully a string) 29 | msg = "ID must match " + getattr(regex, "pattern", regex) 30 | kwargs["validators"] = [RegexValidator(regex=regex, message=msg, flags=re.U)] 31 | super(OCDIDField, self).__init__(*args, **kwargs) 32 | 33 | def deconstruct(self): 34 | name, path, args, kwargs = super(OCDIDField, self).deconstruct() 35 | if self.ocd_type != "jurisdiction": 36 | kwargs.pop("default") 37 | kwargs.pop("max_length") 38 | kwargs.pop("primary_key") 39 | kwargs["ocd_type"] = self.ocd_type 40 | return (name, path, args, kwargs) 41 | 42 | 43 | class OCDBase(models.Model): 44 | """ common base fields across all top-level models """ 45 | 46 | created_at = models.DateTimeField( 47 | auto_now_add=True, help_text="The date and time of creation." 48 | ) 49 | updated_at = models.DateTimeField( 50 | auto_now_add=True, help_text="The date and time of the last update." 51 | ) 52 | last_seen = models.DateTimeField( 53 | auto_now=True, 54 | help_text="The last time this object was seen in a scrape." 55 | ) 56 | extras = models.JSONField( 57 | default=dict, 58 | blank=True, 59 | help_text="A key-value store for storing arbitrary information not covered elsewhere.", 60 | ) 61 | locked_fields = ArrayField(base_field=models.TextField(), blank=True, default=list) 62 | 63 | class Meta: 64 | abstract = True 65 | 66 | 67 | class RelatedBase(models.Model): 68 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) 69 | 70 | class Meta: 71 | abstract = True 72 | 73 | 74 | class LinkBase(RelatedBase): 75 | note = models.CharField( 76 | max_length=300, 77 | blank=True, 78 | help_text="A short, optional note related to an object.", 79 | ) 80 | url = models.URLField( 81 | max_length=2000, help_text="A hyperlink related to an object." 82 | ) 83 | 84 | class Meta: 85 | abstract = True 86 | 87 | def __str__(self): 88 | return self.url 89 | 90 | 91 | class MimetypeLinkBase(RelatedBase): 92 | media_type = models.CharField(max_length=100) 93 | url = models.URLField(max_length=2000) 94 | text = models.TextField(default="", blank=True) 95 | 96 | class Meta: 97 | abstract = True 98 | 99 | 100 | class IdentifierBase(RelatedBase): 101 | identifier = models.CharField( 102 | max_length=300, 103 | help_text="A unique identifier developed by an upstream or third party source.", 104 | ) 105 | scheme = models.CharField( 106 | max_length=300, help_text="The name of the service that created the identifier." 107 | ) 108 | 109 | class Meta: 110 | abstract = True 111 | 112 | def __str__(self): 113 | return self.identifier 114 | 115 | 116 | class RelatedEntityBase(RelatedBase): 117 | name = models.CharField(max_length=2000) 118 | entity_type = models.CharField(max_length=20, blank=True) 119 | 120 | # optionally tied to an organization or person if it was linkable 121 | # for these two on_delete is SET_NULL so that deletion of a linked entity doesn't 122 | # delete this object- it should instead just become unresolved (NULL) 123 | organization = models.ForeignKey( 124 | "core.Organization", null=True, on_delete=models.SET_NULL 125 | ) 126 | person = models.ForeignKey("core.Person", null=True, on_delete=models.SET_NULL) 127 | 128 | @property 129 | def entity_name(self): 130 | if self.entity_type == "organization" and self.organization_id: 131 | return self.organization.name 132 | elif self.entity_type == "person" and self.person_id: 133 | return self.person.name 134 | else: 135 | return self.name 136 | 137 | @property 138 | def entity_id(self): 139 | if self.entity_type == "organization": 140 | return self.organization_id 141 | if self.entity_type == "person": 142 | return self.person_id 143 | return None 144 | 145 | class Meta: 146 | abstract = True 147 | -------------------------------------------------------------------------------- /opencivicdata/core/models/division.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class DivisionManager(models.Manager): 5 | def children_of(self, division_id, subtype=None, depth=1): 6 | query, n = Division.subtypes_from_id(division_id) 7 | q_objects = [] 8 | 9 | # only get children 10 | if subtype: 11 | query["subtype{0}".format(n)] = subtype 12 | else: 13 | q_objects.append(~models.Q(**{"subtype{0}".format(n): ""})) 14 | q_objects.append(~models.Q(**{"subid{0}".format(n): ""})) 15 | 16 | # allow for depth wildcards 17 | n += depth 18 | 19 | # ensure final field is null 20 | q_objects.append(models.Q(**{"subtype{0}".format(n): ""})) 21 | q_objects.append(models.Q(**{"subid{0}".format(n): ""})) 22 | 23 | return self.filter(*q_objects, **query) 24 | 25 | def create(self, id, name, redirect=None): 26 | return super(DivisionManager, self).create( 27 | id=id, name=name, redirect=redirect, **Division.subtypes_from_id(id)[0] 28 | ) 29 | 30 | 31 | class Division(models.Model): 32 | """ 33 | A political geography, which may have multiple boundaries over its lifetime. 34 | 35 | Types of divisions include, among others: 36 | * Governmental jurisdiction - A division that a government has jurisdiction over. 37 | (e.g. North Carolina) 38 | * Political district - A division that elects a representative to a legislature. 39 | (e.g. North Carolina Congressional District 4) 40 | * Service zone - An area to which a government provides a service. 41 | (e.g. Washington DC Police District 105) 42 | """ 43 | 44 | objects = DivisionManager() 45 | 46 | id = models.CharField(max_length=300, primary_key=True) 47 | name = models.CharField(max_length=300, help_text="The name of the division.") 48 | # cascade is SET_NULL, will un-redirect if deletion happens 49 | redirect = models.ForeignKey("self", null=True, on_delete=models.SET_NULL) 50 | country = models.CharField( 51 | max_length=2, 52 | help_text="An ISO-3166-1 alpha-2 code identifying the county where this division is found.", 53 | ) 54 | 55 | # up to 7 pieces of the id that are searchable 56 | subtype1 = models.CharField( 57 | max_length=50, 58 | blank=True, 59 | help_text="The first subtype in the unique identifier.", 60 | ) 61 | subid1 = models.CharField( 62 | max_length=100, 63 | blank=True, 64 | help_text="The first subidentifier in the unique identifer.", 65 | ) 66 | subtype2 = models.CharField( 67 | max_length=50, 68 | blank=True, 69 | help_text="The second subtype in the unique identifier.", 70 | ) 71 | subid2 = models.CharField( 72 | max_length=100, 73 | blank=True, 74 | help_text="The second subidentifier in the unique identifer.", 75 | ) 76 | subtype3 = models.CharField( 77 | max_length=50, 78 | blank=True, 79 | help_text="The third subtype in the unique identifier.", 80 | ) 81 | subid3 = models.CharField( 82 | max_length=100, 83 | blank=True, 84 | help_text="The third subidentifier in the unique identifer.", 85 | ) 86 | subtype4 = models.CharField( 87 | max_length=50, 88 | blank=True, 89 | help_text="The fourth subtype in the unique identifier.", 90 | ) 91 | subid4 = models.CharField( 92 | max_length=100, 93 | blank=True, 94 | help_text="The fourth subidentifier in the unique identifer.", 95 | ) 96 | subtype5 = models.CharField( 97 | max_length=50, 98 | blank=True, 99 | help_text="The fifth subtype in the unique identifier.", 100 | ) 101 | subid5 = models.CharField( 102 | max_length=100, 103 | blank=True, 104 | help_text="The fifth subidentifier in the unique identifer.", 105 | ) 106 | subtype6 = models.CharField( 107 | max_length=50, 108 | blank=True, 109 | help_text="The sixth subtype in the unique identifier.", 110 | ) 111 | subid6 = models.CharField( 112 | max_length=100, 113 | blank=True, 114 | help_text="The sixth subidentifier in the unique identifer.", 115 | ) 116 | subtype7 = models.CharField( 117 | max_length=50, 118 | blank=True, 119 | help_text="The seventh subtype in the unique identifier.", 120 | ) 121 | subid7 = models.CharField( 122 | max_length=100, 123 | blank=True, 124 | help_text="The seventh subidentifier in the unique identifer.", 125 | ) 126 | 127 | class Meta: 128 | db_table = "opencivicdata_division" 129 | 130 | def __str__(self): 131 | return "{0} ({1})".format(self.name, self.id) 132 | 133 | @staticmethod 134 | def subtypes_from_id(division_id): 135 | pieces = [piece.split(":", 1) for piece in division_id.split("/")] 136 | fields = {} 137 | 138 | # if it included the ocd-division bit, pop it off 139 | if pieces[0] == ["ocd-division"]: 140 | pieces.pop(0) 141 | 142 | if pieces[0][0] != "country": 143 | raise ValueError("OCD id must start with country") 144 | 145 | fields["country"] = pieces[0][1] 146 | 147 | # add the remaining pieces 148 | n = 1 149 | for stype, subid in pieces[1:]: 150 | fields["subtype{0}".format(n)] = stype 151 | fields["subid{0}".format(n)] = subid 152 | n += 1 153 | 154 | return fields, n 155 | -------------------------------------------------------------------------------- /opencivicdata/legislative/migrations/0004_auto_20171005_2027.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.5 on 2017-10-05 20:27 3 | from __future__ import unicode_literals 4 | 5 | import django.contrib.postgres.fields.jsonb 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [("legislative", "0003_time_changes")] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="bill", 16 | name="created_at", 17 | field=models.DateTimeField( 18 | auto_now_add=True, help_text="The date and time of creation." 19 | ), 20 | ), 21 | migrations.AlterField( 22 | model_name="bill", 23 | name="extras", 24 | field=django.contrib.postgres.fields.jsonb.JSONField( 25 | blank=True, 26 | default=dict, 27 | help_text="A key-value store for storing arbitrary information not covered elsewhere.", # noqa 28 | ), 29 | ), 30 | migrations.AlterField( 31 | model_name="bill", 32 | name="updated_at", 33 | field=models.DateTimeField( 34 | auto_now=True, help_text="The date and time of the last update." 35 | ), 36 | ), 37 | migrations.AlterField( 38 | model_name="billidentifier", 39 | name="identifier", 40 | field=models.CharField( 41 | help_text="A unique identifier developed by an upstream or third party source.", 42 | max_length=300, 43 | ), 44 | ), 45 | migrations.AlterField( 46 | model_name="billidentifier", 47 | name="scheme", 48 | field=models.CharField( 49 | help_text="The name of the service that created the identifier.", 50 | max_length=300, 51 | ), 52 | ), 53 | migrations.AlterField( 54 | model_name="billsource", 55 | name="note", 56 | field=models.CharField( 57 | blank=True, 58 | help_text="A short, optional note related to an object.", 59 | max_length=300, 60 | ), 61 | ), 62 | migrations.AlterField( 63 | model_name="billsource", 64 | name="url", 65 | field=models.URLField( 66 | help_text="A hyperlink related to an object.", max_length=2000 67 | ), 68 | ), 69 | migrations.AlterField( 70 | model_name="event", 71 | name="created_at", 72 | field=models.DateTimeField( 73 | auto_now_add=True, help_text="The date and time of creation." 74 | ), 75 | ), 76 | migrations.AlterField( 77 | model_name="event", 78 | name="extras", 79 | field=django.contrib.postgres.fields.jsonb.JSONField( 80 | blank=True, 81 | default=dict, 82 | help_text="A key-value store for storing arbitrary information not covered elsewhere.", # noqa 83 | ), 84 | ), 85 | migrations.AlterField( 86 | model_name="event", 87 | name="updated_at", 88 | field=models.DateTimeField( 89 | auto_now=True, help_text="The date and time of the last update." 90 | ), 91 | ), 92 | migrations.AlterField( 93 | model_name="eventlink", 94 | name="note", 95 | field=models.CharField( 96 | blank=True, 97 | help_text="A short, optional note related to an object.", 98 | max_length=300, 99 | ), 100 | ), 101 | migrations.AlterField( 102 | model_name="eventlink", 103 | name="url", 104 | field=models.URLField( 105 | help_text="A hyperlink related to an object.", max_length=2000 106 | ), 107 | ), 108 | migrations.AlterField( 109 | model_name="eventsource", 110 | name="note", 111 | field=models.CharField( 112 | blank=True, 113 | help_text="A short, optional note related to an object.", 114 | max_length=300, 115 | ), 116 | ), 117 | migrations.AlterField( 118 | model_name="eventsource", 119 | name="url", 120 | field=models.URLField( 121 | help_text="A hyperlink related to an object.", max_length=2000 122 | ), 123 | ), 124 | migrations.AlterField( 125 | model_name="voteevent", 126 | name="created_at", 127 | field=models.DateTimeField( 128 | auto_now_add=True, help_text="The date and time of creation." 129 | ), 130 | ), 131 | migrations.AlterField( 132 | model_name="voteevent", 133 | name="extras", 134 | field=django.contrib.postgres.fields.jsonb.JSONField( 135 | blank=True, 136 | default=dict, 137 | help_text="A key-value store for storing arbitrary information not covered elsewhere.", # noqa 138 | ), 139 | ), 140 | migrations.AlterField( 141 | model_name="voteevent", 142 | name="updated_at", 143 | field=models.DateTimeField( 144 | auto_now=True, help_text="The date and time of the last update." 145 | ), 146 | ), 147 | migrations.AlterField( 148 | model_name="votesource", 149 | name="note", 150 | field=models.CharField( 151 | blank=True, 152 | help_text="A short, optional note related to an object.", 153 | max_length=300, 154 | ), 155 | ), 156 | migrations.AlterField( 157 | model_name="votesource", 158 | name="url", 159 | field=models.URLField( 160 | help_text="A hyperlink related to an object.", max_length=2000 161 | ), 162 | ), 163 | ] 164 | -------------------------------------------------------------------------------- /opencivicdata/elections/migrations/0007_auto_20171022_0234.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-10-22 02:34 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [("elections", "0006_auto_20171005_2029")] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="ballotmeasurecontest", 16 | name="division", 17 | field=models.ForeignKey( 18 | help_text="Reference to the Division that defines the political geography of the contest, e.g., a specific Congressional or State Senate district. Should be a subdivision of the Division referenced by the contest's Election.", # noqa 19 | on_delete=django.db.models.deletion.PROTECT, 20 | related_name="ballotmeasurecontests", 21 | related_query_name="ballotmeasurecontests", 22 | to="core.Division", 23 | ), 24 | ), 25 | migrations.AlterField( 26 | model_name="ballotmeasurecontest", 27 | name="runoff_for_contest", 28 | field=models.OneToOneField( 29 | help_text="If this contest is a runoff to determine the outcome of a previously undecided contest, reference to that BallotMeasureContest.", # noqa 30 | null=True, 31 | on_delete=django.db.models.deletion.SET_NULL, 32 | related_name="runoff_contest", 33 | to="elections.BallotMeasureContest", 34 | ), 35 | ), 36 | migrations.AlterField( 37 | model_name="candidacy", 38 | name="post", 39 | field=models.ForeignKey( 40 | help_text="Reference to Post representing the public office for which the candidate is seeking election.", # noqa 41 | on_delete=django.db.models.deletion.PROTECT, 42 | related_name="candidacies", 43 | to="core.Post", 44 | ), 45 | ), 46 | migrations.AlterField( 47 | model_name="candidacy", 48 | name="top_ticket_candidacy", 49 | field=models.ForeignKey( 50 | help_text="If the candidate is running as part of ticket, e.g., a Vice Presidential candidate running with a Presidential candidate, reference to candidacy at the top of the ticket.", # noqa 51 | null=True, 52 | on_delete=django.db.models.deletion.SET_NULL, 53 | related_name="ticket", 54 | to="elections.Candidacy", 55 | ), 56 | ), 57 | migrations.AlterField( 58 | model_name="candidatecontest", 59 | name="division", 60 | field=models.ForeignKey( 61 | help_text="Reference to the Division that defines the political geography of the contest, e.g., a specific Congressional or State Senate district. Should be a subdivision of the Division referenced by the contest's Election.", # noqa 62 | on_delete=django.db.models.deletion.PROTECT, 63 | related_name="candidatecontests", 64 | related_query_name="candidatecontests", 65 | to="core.Division", 66 | ), 67 | ), 68 | migrations.AlterField( 69 | model_name="candidatecontest", 70 | name="runoff_for_contest", 71 | field=models.OneToOneField( 72 | help_text="If this contest is a runoff to determine the outcome of a previously undecided contest, reference to that CandidateContest.", # noqa 73 | null=True, 74 | on_delete=django.db.models.deletion.SET_NULL, 75 | related_name="runoff_contest", 76 | to="elections.CandidateContest", 77 | ), 78 | ), 79 | migrations.AlterField( 80 | model_name="partycontest", 81 | name="division", 82 | field=models.ForeignKey( 83 | help_text="Reference to the Division that defines the political geography of the contest, e.g., a specific Congressional or State Senate district. Should be a subdivision of the Division referenced by the contest's Election.", # noqa 84 | on_delete=django.db.models.deletion.PROTECT, 85 | related_name="partycontests", 86 | related_query_name="partycontests", 87 | to="core.Division", 88 | ), 89 | ), 90 | migrations.AlterField( 91 | model_name="partycontest", 92 | name="runoff_for_contest", 93 | field=models.OneToOneField( 94 | help_text="If this contest is a runoff to determine the outcome of a previously undecided contest, reference to that PartyContest.", # noqa 95 | null=True, 96 | on_delete=django.db.models.deletion.SET_NULL, 97 | to="elections.PartyContest", 98 | ), 99 | ), 100 | migrations.AlterField( 101 | model_name="retentioncontest", 102 | name="division", 103 | field=models.ForeignKey( 104 | help_text="Reference to the Division that defines the political geography of the contest, e.g., a specific Congressional or State Senate district. Should be a subdivision of the Division referenced by the contest's Election.", # noqa 105 | on_delete=django.db.models.deletion.PROTECT, 106 | related_name="retentioncontests", 107 | related_query_name="retentioncontests", 108 | to="core.Division", 109 | ), 110 | ), 111 | migrations.AlterField( 112 | model_name="retentioncontest", 113 | name="membership", 114 | field=models.ForeignKey( 115 | help_text="Reference to the Membership that represents the tenure of a person in a specific public office.", # noqa 116 | on_delete=django.db.models.deletion.PROTECT, 117 | to="core.Membership", 118 | ), 119 | ), 120 | migrations.AlterField( 121 | model_name="retentioncontest", 122 | name="runoff_for_contest", 123 | field=models.OneToOneField( 124 | help_text="If this contest is a runoff to determine the outcome of a previously undecided contest, reference to that RetentionContest.", # noqa 125 | null=True, 126 | on_delete=django.db.models.deletion.SET_NULL, 127 | related_name="runoff_contest", 128 | to="elections.RetentionContest", 129 | ), 130 | ), 131 | ] 132 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.4.0 (2024-11-11) 4 | 5 | * Add support for Python 3.9, 3.10, 3.11, 3.12 and Django 4.0, 4.2. 6 | * Drop support for Python 3.7, 3.8 and Django 2.2, 3.0. 7 | 8 | ## 3.3.1 (2024-05-15) 9 | 10 | * Add a --sync flag to the loaddivisions management command, to delete divisions that are in the DB but not the CSV, even if the DB contains the CSV. This flag is relevant if you synchronize with a single CSV. 11 | 12 | ## 3.3.0 (2023-05-08) 13 | 14 | * Add last_seen field to database objects 15 | * Pupa now has "pupa clean" CLI command 16 | * Change behavior of loaddivisions management command to do nothing if the DB contains the CSV. This means divisions that are in the DB but not the CSV are not deleted. This makes it easier to load multiple CSVs, but harder to synchronize with a single CSV. 17 | 18 | ## 3.2.0 (2020-03-26) 19 | 20 | * add GinIndex for search (requires migration) 21 | * add 'enrolled' bill status 22 | 23 | ## 3.1.0 (2020-01-28) 24 | 25 | * Add billaction.extras field (requires migration) 26 | 27 | ## 3.0.0 (2019-11-25) 28 | 29 | * Drops support for Python 2, Django versions before < 2.2 LTS. 30 | * Change behavior of loaddivisions management command to require --bulk if 31 | deletion & re-creation is desired. 32 | 33 | ## 2.5.0 (2019-11-24) 34 | 35 | Improvements requiring migrations: 36 | 37 | * added experimental SearchableBill model 38 | * bugfix changes to EventDocument and EventMediaBase 39 | 40 | Bugfixes: 41 | 42 | * Fix PersonAdmin memberships display 43 | 44 | Other: 45 | 46 | * add event classification choices 47 | 48 | ## 2.3.0 (2018-12-04) 49 | 50 | Improvements requiring migrations: 51 | 52 | * adjusted length of Event.name 53 | 54 | Other: 55 | 56 | * recommended Postgres version is now 10.x (>= 9.4 should still work but will no longer be tested) 57 | * removal of underdeveloped merge & unresolved legislators view 58 | 59 | ## 2.2.1 (2018-10-29) 60 | 61 | Bugfixes: 62 | 63 | * Fix missing migrations 64 | 65 | Improvements: 66 | 67 | * new 'bill of address' classification 68 | 69 | ## 2.2.0 (2018-10-09) 70 | 71 | Improvements requiring migrations: 72 | 73 | * add extras field to bill versions 74 | 75 | Improvements: 76 | 77 | * updated obsolete dependencies 78 | 79 | ## 2.1.2 (2018-05-08) 80 | 81 | Improvements: 82 | 83 | * Add 'became-law' action classification. 84 | 85 | Bugfixes: 86 | 87 | * Fix accidentally broken Python 2 compatibility 88 | 89 | ## 2.1.1 (2017-12-20) 90 | 91 | Improvements: 92 | 93 | * add post argument to Person.objects.memberOf 94 | 95 | Bugfixes: 96 | 97 | * fix urlresolvers.reverse import location for Django 2.0 98 | 99 | ## 2.1.0 (2017-12-04) 100 | 101 | Improvements requiring migrations: 102 | 103 | * added missing migration for help_text 104 | * update models to have explicit on_delete settings for Foreign Keys (required by Django 2.0) 105 | 106 | Bugfixes: 107 | 108 | * add missing help_text for opencivicdata.elections 109 | * fixes for some opencivicdata.elections migration issues 110 | 111 | ## 2.0.0 (2017-07-19) 112 | 113 | Backwards-incompatible changes: 114 | 115 | * Implementation of [OCDEP #101](http://docs.opencivicdata.org/en/latest/proposals/0101.html) - datetime fields are fuzzy and Event's start/end_time are now start/end_date. 116 | 117 | Improvments requiring migrations: 118 | 119 | * add extras to BillAction & EventAgendaItem 120 | * add Post.maximum_memberships for validating expected number of memberships, useful for multi-member seats 121 | 122 | Improvements: 123 | 124 | * jurisdiction specific merge tool 125 | * experimental introduction of opencivicdata.elections - provisional for now w/ future changes likely 126 | 127 | Bugfix: 128 | 129 | * fix usage of FileNotFoundError on Python 2.7 130 | 131 | ## 1.0.0 (2017-05-25) 132 | 133 | Backwards-incompatible changes: 134 | 135 | * This package is renamed to opencivicdata from opencivicdata-divisions and opencivicdata-django. 136 | This also means it is no longer split into opencivicdata-divisions and opencivicdata-django. This really shouldn't cause any issues, but you shouldn't be installing opencivicdata-divisions anymore, and doing so explicitly may cause some issues. 137 | * Your requirements.txt or other requirements definition should now use the opencivicdata name exclusively. 138 | * Instead of adding: 139 | ```'opencivicdata.apps.BaseConfig'`` to your ``INSTALLED_APPS`` you'll need to add: 140 | ``` 141 | 'opencivicdata.core.apps.BaseConfig', 142 | 'opencivicdata.legislative.apps.BaseConfig', 143 | ``` 144 | * If you already have models you'll need to run: ```./manage.py migrate --fake-initial`` to skip the initial migrations for the two new apps. 145 | 146 | Improvements requiring migrations: 147 | 148 | * Add `Membership.person_name` property, allowing unresolved people to be members of Organizations 149 | * Add `VoteEvent.bill_action`, allowing linking of VoteEvents to bill actions 150 | 151 | Improvements: 152 | 153 | * Add `amendment-deferral` to match `deferral` and other amendment actions. 154 | * Add `study request` and `concurrent study request` to to bill classifications. 155 | * Basic Python 2.7 support is restored. 156 | 157 | ## 0.9.0 (2017-02-19) 158 | 159 | Backwards-incompatible changes: 160 | 161 | * Make bill action classifications consistent: 162 | * `amendment-amended` is now `amendment-amendment` 163 | * `committee-referral` is now `referral-committee` 164 | * `executive-received` is now `executive-receipt` 165 | * `deferred` is now `deferral` 166 | 167 | Improvements requiring migrations: 168 | 169 | * Add `EventAgendaItem.classification` property, like Popolo's [Event](http://www.popoloproject.com/specs/event.html) class 170 | 171 | Improvements: 172 | 173 | * Add `transit_authority` to jurisdiction classifications 174 | * Add `corporation`, `agency`, `department` to organization classifications 175 | * Add `motion` to bill classifications 176 | * Add `receipt`, `referral` to bill action classifications 177 | 178 | Fixes: 179 | 180 | * Fix `EventRelatedEntity.entity_name` property when the entity is a vote event or bill 181 | 182 | ## 0.8.2 (2015-11-30) 183 | 184 | Fixes: 185 | 186 | * Fix package 187 | 188 | ## 0.8.0 (2015-11-13) 189 | 190 | Improvements: 191 | 192 | * Add admin views for merging objects 193 | 194 | ## 0.7.1, 0.7.2, 0.7.3 (2015-10-08/2015-10-12) 195 | 196 | Fixes: 197 | 198 | * Fix package 199 | 200 | ## 0.7.0 (2015-10-08) 201 | 202 | Backwards-incompatible changes: 203 | 204 | * Rename Vote to VoteEvent to align with Popolo's [VoteEvent](http://www.popoloproject.com/specs/vote-event.html) class, #27 205 | 206 | Improvements: 207 | 208 | * Various improvements to admin views 209 | * Add admin views for unresolved people 210 | * Add `Organization.get_current_members` method 211 | * Add `text` field to `MimetypeLinkBase` abstract class 212 | * Upgrade Django from 1.8 to 1.9 and remove `djorm-ext-pgarray`, `jsonfield`, `django-uuidfield` dependencies 213 | 214 | Fixes: 215 | 216 | * Fix package. 217 | 218 | ## 0.6.4 (2015-08-30) 219 | 220 | Start of changelog 221 | -------------------------------------------------------------------------------- /opencivicdata/elections/models/contests/ballot_measure.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | BallotMeasureContest-related models. 5 | """ 6 | from django.db import models 7 | from opencivicdata.core.models.base import IdentifierBase, LinkBase 8 | from opencivicdata.core.models import Membership 9 | from .base import ContestBase 10 | 11 | 12 | class BallotMeasureContest(ContestBase): 13 | """ 14 | A contest in which voters select from among options proposed in a ballot measure. 15 | """ 16 | 17 | description = models.TextField( 18 | help_text="Text describing the purpose and/or potential outcomes of the " 19 | "ballot measure, not necessarily as it appears on the ballot." 20 | ) 21 | requirement = models.CharField( 22 | max_length=300, 23 | blank=True, 24 | default="50% plus one vote", 25 | help_text="The threshold of votes the ballot measure needs in order to pass.", 26 | ) 27 | classification = models.CharField( 28 | max_length=300, 29 | blank=True, 30 | help_text="Describes the origin and/or potential outcome of the ballot " 31 | 'measure, e.g., "initiative statute", "legislative constitutional ' 32 | 'amendment".', 33 | ) 34 | runoff_for_contest = models.OneToOneField( 35 | "self", 36 | related_name="runoff_contest", 37 | null=True, 38 | on_delete=models.SET_NULL, 39 | help_text="If this contest is a runoff to determine the outcome of a " 40 | "previously undecided contest, reference to that " 41 | "BallotMeasureContest.", 42 | ) 43 | 44 | class Meta(ContestBase.Meta): 45 | db_table = "opencivicdata_ballotmeasurecontest" 46 | 47 | 48 | class BallotMeasureContestOption(models.Model): 49 | """ 50 | An option voters may choose in a BallotMeasureContest. 51 | """ 52 | 53 | contest = models.ForeignKey( 54 | BallotMeasureContest, 55 | related_name="options", 56 | on_delete=models.CASCADE, 57 | help_text="Reference to the BallotMeasureContest.", 58 | ) 59 | text = models.CharField( 60 | max_length=300, 61 | help_text="Text of the option, not necessarily as it appears on the ballot.", 62 | ) 63 | 64 | def __str__(self): 65 | return "{0} on {1}".format(self.text, self.contest) 66 | 67 | class Meta: 68 | db_table = "opencivicdata_ballotmeasurecontestoption" 69 | 70 | 71 | class BallotMeasureContestIdentifier(IdentifierBase): 72 | """ 73 | Upstream identifiers of a BallotMeasureContest. 74 | 75 | For example, identfiers assigned by a Secretary of State, county or city 76 | elections office. 77 | """ 78 | 79 | contest = models.ForeignKey( 80 | BallotMeasureContest, 81 | related_name="identifiers", 82 | on_delete=models.CASCADE, 83 | help_text="Reference to the BallotMeasureContest linked to the upstream " 84 | "identifier.", 85 | ) 86 | 87 | def __str__(self): 88 | tmpl = "%s identifies %s" 89 | return tmpl % (self.identifier, self.contest) 90 | 91 | class Meta: 92 | db_table = "opencivicdata_ballotmeasurecontestidentifier" 93 | 94 | 95 | class BallotMeasureContestSource(LinkBase): 96 | """ 97 | Source used in assembling a BallotMeasureContest. 98 | """ 99 | 100 | contest = models.ForeignKey( 101 | BallotMeasureContest, 102 | related_name="sources", 103 | on_delete=models.CASCADE, 104 | help_text="Reference to the BallotMeasureContest assembled from the source.", 105 | ) 106 | 107 | class Meta: 108 | db_table = "opencivicdata_ballotmeasurecontestsource" 109 | 110 | 111 | class RetentionContest(ContestBase): 112 | """ 113 | A contest where voters vote to retain or recall a current office holder. 114 | 115 | These contests include judicial retention or recall elections. 116 | """ 117 | 118 | description = models.TextField( 119 | help_text="Text describing the purpose and/or potential outcomes of the " 120 | "contest, not necessarily as it appears on the ballot." 121 | ) 122 | requirement = models.CharField( 123 | max_length=300, 124 | blank=True, 125 | default="50% plus one vote", 126 | help_text="The threshold of votes need in order to retain the officeholder.", 127 | ) 128 | runoff_for_contest = models.OneToOneField( 129 | "self", 130 | related_name="runoff_contest", 131 | null=True, 132 | on_delete=models.SET_NULL, 133 | help_text="If this contest is a runoff to determine the outcome of a previously " 134 | "undecided contest, reference to that RetentionContest.", 135 | ) 136 | membership = models.ForeignKey( 137 | Membership, 138 | help_text="Reference to the Membership that represents the tenure of a " 139 | "person in a specific public office.", 140 | on_delete=models.PROTECT, 141 | ) 142 | 143 | class Meta(ContestBase.Meta): 144 | db_table = "opencivicdata_retentioncontest" 145 | 146 | 147 | class RetentionContestOption(models.Model): 148 | """ 149 | An option voters may choose in a RetentionContest. 150 | """ 151 | 152 | contest = models.ForeignKey( 153 | RetentionContest, 154 | related_name="options", 155 | help_text="Reference to the RetentionContest.", 156 | on_delete=models.CASCADE, 157 | ) 158 | text = models.CharField( 159 | max_length=300, 160 | help_text="Text of the option, not necessarily as it appears on the ballot.", 161 | ) 162 | 163 | def __str__(self): 164 | return "{0} on {1}".format(self.text, self.contest) 165 | 166 | class Meta: 167 | db_table = "opencivicdata_retentioncontestoption" 168 | 169 | 170 | class RetentionContestIdentifier(IdentifierBase): 171 | """ 172 | Upstream identifiers of a RetentionContest. 173 | 174 | For example, identfiers assigned by a Secretary of State, county or city 175 | elections office. 176 | """ 177 | 178 | contest = models.ForeignKey( 179 | RetentionContest, 180 | related_name="identifiers", 181 | help_text="Reference to the RetentionContest linked to the upstream " 182 | "identifier.", 183 | on_delete=models.CASCADE, 184 | ) 185 | 186 | def __str__(self): 187 | tmpl = "%s identifies %s" 188 | return tmpl % (self.identifier, self.contest) 189 | 190 | class Meta: 191 | db_table = "opencivicdata_retentionidentifier" 192 | 193 | 194 | class RetentionContestSource(LinkBase): 195 | """ 196 | Source used in assembling a RetentionContest. 197 | """ 198 | 199 | contest = models.ForeignKey( 200 | RetentionContest, 201 | related_name="sources", 202 | help_text="Reference to the RetentionContest assembled from the source.", 203 | on_delete=models.CASCADE, 204 | ) 205 | 206 | class Meta: 207 | db_table = "opencivicdata_retentionsource" 208 | -------------------------------------------------------------------------------- /opencivicdata/common.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for declaration of common constants available throughout Open Civic Data code. 3 | """ 4 | 5 | DIVISION_ID_REGEX = r"^ocd-division/country:[a-z]{2}(/[^\W\d]+:[\w.~-]+)*$" 6 | JURISDICTION_ID_REGEX = r"^ocd-jurisdiction/country:[a-z]{2}(/[^\W\d]+:[\w.~-]+)*/\w+$" 7 | 8 | # helper for making options-only lists 9 | _keys = lambda allopts: [opt[0] for opt in allopts] # noqa 10 | 11 | """ 12 | Policy on addition of new types here: 13 | 14 | Because these lists are strictly enforced in lots of code for the purposes of data quality 15 | we have a fairly liberal policy on amendment. 16 | 17 | If a type is needed and is not duplicative of another type, it will be accepted. 18 | 19 | At the moment, because of this policy, no method exists to extend these lists, instead we will 20 | strive for them to be comprehensive. 21 | 22 | The only exception to this would be translations, which should simply exist as translations of 23 | the display name (2nd attribute). 24 | """ 25 | 26 | # NOTE: this list explicitly does not include RFC 6350s 'cell' as that is redundant with 27 | # voice and the distinction will only lead to confusion. contact_detail.note can be 28 | # used to indicate if something is a home, work, cell, etc. 29 | CONTACT_TYPE_CHOICES = ( 30 | ("address", "Postal Address"), 31 | ("email", "Email"), 32 | ("url", "URL"), 33 | ("fax", "Fax"), 34 | ("text", "Text Phone"), 35 | ("voice", "Voice Phone"), 36 | ("video", "Video Phone"), 37 | ("pager", "Pager"), 38 | ("textphone", "Device for people with hearing impairment"), 39 | ) 40 | CONTACT_TYPES = _keys(CONTACT_TYPE_CHOICES) 41 | 42 | 43 | JURISDICTION_CLASSIFICATION_CHOICES = ( 44 | ("government", "Government"), 45 | ("legislature", "Legislature"), 46 | ("executive", "Executive"), 47 | ("school", "School System"), 48 | ("park", "Park District"), 49 | ("sewer", "Sewer District"), 50 | ("forest", "Forest Preserve District"), 51 | ("transit_authority", "Transit Authority"), 52 | ) 53 | JURISDICTION_CLASSIFICATIONS = _keys(JURISDICTION_CLASSIFICATION_CHOICES) 54 | 55 | 56 | SESSION_CLASSIFICATION_CHOICES = (("primary", "Primary"), ("special", "Special")) 57 | SESSION_CLASSIFICATIONS = _keys(SESSION_CLASSIFICATION_CHOICES) 58 | 59 | 60 | ORGANIZATION_CLASSIFICATION_CHOICES = ( 61 | ("legislature", "Legislature"), 62 | ("executive", "Executive"), 63 | ("upper", "Upper Chamber"), 64 | ("lower", "Lower Chamber"), 65 | ("party", "Party"), 66 | ("committee", "Committee"), 67 | ("commission", "Commission"), 68 | ("corporation", "Corporation"), 69 | ("agency", "Agency"), 70 | ("department", "Department"), 71 | ("judiciary", "Judiciary"), 72 | ) 73 | ORGANIZATION_CLASSIFICATIONS = _keys(ORGANIZATION_CLASSIFICATION_CHOICES) 74 | 75 | BILL_CLASSIFICATION_CHOICES = ( 76 | ("bill", "Bill"), 77 | ("resolution", "Resolution"), 78 | ("concurrent resolution", "Concurrent Resolution"), 79 | ("joint resolution", "Joint Resolution"), 80 | ("memorial", "Memorial"), 81 | ("commemoration", "Commemoration"), 82 | ("concurrent memorial", "Concurrent Memorial"), 83 | ("joint memorial", "Joint Memorial"), 84 | ("proposed bill", "Proposed Bill"), 85 | ("proclamation", "Proclamation"), 86 | ("nomination", "Nomination"), 87 | ("contract", "Contract"), 88 | ("claim", "Claim"), 89 | ("appointment", "Appointment"), 90 | ("constitutional amendment", "Constitutional Amendment"), 91 | ("petition", "Petition"), 92 | ("order", "Order"), 93 | ("concurrent order", "Concurrent Order"), 94 | ("appropriation", "Appropriation"), 95 | ("ordinance", "Ordinance"), 96 | ("motion", "Motion"), 97 | ("study request", "Study Request"), 98 | ("concurrent study request", "Concurrent Study Request"), 99 | ("bill of address", "Bill of Address"), 100 | ) 101 | BILL_CLASSIFICATIONS = _keys(BILL_CLASSIFICATION_CHOICES) 102 | 103 | BILL_RELATION_TYPE_CHOICES = ( 104 | ("companion", "Companion"), # a companion in another chamber 105 | ("prior-session", "Prior Session"), # an introduction from a prior session 106 | ("replaced-by", "Replaced By"), # a bill has been replaced by another 107 | ("replaces", "Replaces"), # a bill that replaces another 108 | ) 109 | BILL_RELATION_TYPES = _keys(BILL_RELATION_TYPE_CHOICES) 110 | 111 | BILL_ACTION_CLASSIFICATION_CHOICES = ( 112 | ("filing", "Filing"), 113 | ("introduction", "Introduced"), 114 | ("enrolled", "Enrolled"), 115 | ("reading-1", "First Reading"), 116 | ("reading-2", "Second Reading"), 117 | ("reading-3", "Third Reading"), 118 | ("passage", "Passage"), 119 | ("failure", "Passage Failure"), 120 | ("withdrawal", "Withdrawal"), 121 | ("substitution", "Substitution"), 122 | ("amendment-introduction", "Amendment Introduction"), 123 | ("amendment-passage", "Amendment Passage"), 124 | ("amendment-withdrawal", "Amendment Withdrawal"), 125 | ("amendment-failure", "Amendment Failure"), 126 | ("amendment-amendment", "Amendment Amended"), 127 | ("amendment-deferral", "Amendment Deferred or Tabled"), 128 | ("committee-passage", "Passage from Committee"), 129 | ("committee-passage-favorable", "Favorable Passage from Committee"), 130 | ("committee-passage-unfavorable", "Unfavorable Passage from Committee"), 131 | ("committee-failure", "Failure in Committee"), 132 | ("executive-receipt", "Received By Executive"), 133 | ("executive-signature", "Signed By Executive"), 134 | ("executive-veto", "Veto By Executive"), 135 | ("executive-veto-line-item", "Line Item Veto By Executive"), 136 | ("became-law", "Became Law"), 137 | ("veto-override-passage", "Veto Override Passage"), 138 | ("veto-override-failure", "Veto Override Failure"), 139 | ("deferral", "Deferred or Tabled"), 140 | ("receipt", "Received"), 141 | ("referral", "Referred"), 142 | ("referral-committee", "Referred to Committee"), 143 | ) 144 | BILL_ACTION_CLASSIFICATIONS = _keys(BILL_ACTION_CLASSIFICATION_CHOICES) 145 | 146 | VOTE_CLASSIFICATION_CHOICES = ( 147 | ("bill-passage", "Bill Passage"), 148 | ("amendment-passage", "Amendment Passage"), 149 | ("veto-override", "Veto Override"), 150 | ) 151 | VOTE_CLASSIFICATIONS = _keys(VOTE_CLASSIFICATION_CHOICES) 152 | 153 | VOTE_OPTION_CHOICES = ( 154 | ("yes", "Yes"), 155 | ("no", "No"), 156 | ("absent", "Absent"), 157 | ("abstain", "Abstain"), 158 | ("not voting", "Not Voting"), 159 | ("paired", "Paired"), 160 | ("excused", "Excused"), 161 | # Only for open states. 162 | ("other", "Other"), 163 | ) 164 | VOTE_OPTIONS = _keys(VOTE_OPTION_CHOICES) 165 | 166 | VOTE_RESULT_CHOICES = (("pass", "Pass"), ("fail", "Fail")) 167 | VOTE_RESULTS = _keys(VOTE_RESULT_CHOICES) 168 | 169 | EVENT_MEDIA_CLASSIFICATION_CHOICES = ( 170 | ("audio recording", "Audio Recording"), 171 | ("video recording", "Video Recording"), 172 | ) 173 | 174 | EVENT_MEDIA_CLASSIFICATIONS = _keys(EVENT_MEDIA_CLASSIFICATION_CHOICES) 175 | 176 | EVENT_DOCUMENT_CLASSIFICATION_CHOICES = ( 177 | ("agenda", "Agenda"), 178 | ("minutes", "Minutes"), 179 | ("transcript", "Transcript"), 180 | ("testimony", "Testimony"), 181 | ) 182 | 183 | EVENT_DOCUMENT_CLASSIFICATIONS = _keys(EVENT_DOCUMENT_CLASSIFICATION_CHOICES) 184 | -------------------------------------------------------------------------------- /opencivicdata/legislative/migrations/0005_auto_20171005_2028.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.5 on 2017-10-05 20:28 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [("legislative", "0004_auto_20171005_2027")] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="bill", 16 | name="from_organization", 17 | field=models.ForeignKey( 18 | null=True, 19 | on_delete=django.db.models.deletion.PROTECT, 20 | related_name="bills", 21 | to="core.Organization", 22 | ), 23 | ), 24 | migrations.AlterField( 25 | model_name="bill", 26 | name="legislative_session", 27 | field=models.ForeignKey( 28 | on_delete=django.db.models.deletion.PROTECT, 29 | related_name="bills", 30 | to="legislative.LegislativeSession", 31 | ), 32 | ), 33 | migrations.AlterField( 34 | model_name="billaction", 35 | name="organization", 36 | field=models.ForeignKey( 37 | on_delete=django.db.models.deletion.PROTECT, 38 | related_name="actions", 39 | to="core.Organization", 40 | ), 41 | ), 42 | migrations.AlterField( 43 | model_name="billactionrelatedentity", 44 | name="organization", 45 | field=models.ForeignKey( 46 | null=True, 47 | on_delete=django.db.models.deletion.SET_NULL, 48 | to="core.Organization", 49 | ), 50 | ), 51 | migrations.AlterField( 52 | model_name="billactionrelatedentity", 53 | name="person", 54 | field=models.ForeignKey( 55 | null=True, 56 | on_delete=django.db.models.deletion.SET_NULL, 57 | to="core.Person", 58 | ), 59 | ), 60 | migrations.AlterField( 61 | model_name="billsponsorship", 62 | name="organization", 63 | field=models.ForeignKey( 64 | null=True, 65 | on_delete=django.db.models.deletion.SET_NULL, 66 | to="core.Organization", 67 | ), 68 | ), 69 | migrations.AlterField( 70 | model_name="billsponsorship", 71 | name="person", 72 | field=models.ForeignKey( 73 | null=True, 74 | on_delete=django.db.models.deletion.SET_NULL, 75 | to="core.Person", 76 | ), 77 | ), 78 | migrations.AlterField( 79 | model_name="event", 80 | name="jurisdiction", 81 | field=models.ForeignKey( 82 | on_delete=django.db.models.deletion.PROTECT, 83 | related_name="events", 84 | to="core.Jurisdiction", 85 | ), 86 | ), 87 | migrations.AlterField( 88 | model_name="event", 89 | name="location", 90 | field=models.ForeignKey( 91 | null=True, 92 | on_delete=django.db.models.deletion.SET_NULL, 93 | to="legislative.EventLocation", 94 | ), 95 | ), 96 | migrations.AlterField( 97 | model_name="eventparticipant", 98 | name="organization", 99 | field=models.ForeignKey( 100 | null=True, 101 | on_delete=django.db.models.deletion.SET_NULL, 102 | to="core.Organization", 103 | ), 104 | ), 105 | migrations.AlterField( 106 | model_name="eventparticipant", 107 | name="person", 108 | field=models.ForeignKey( 109 | null=True, 110 | on_delete=django.db.models.deletion.SET_NULL, 111 | to="core.Person", 112 | ), 113 | ), 114 | migrations.AlterField( 115 | model_name="eventrelatedentity", 116 | name="bill", 117 | field=models.ForeignKey( 118 | null=True, 119 | on_delete=django.db.models.deletion.SET_NULL, 120 | to="legislative.Bill", 121 | ), 122 | ), 123 | migrations.AlterField( 124 | model_name="eventrelatedentity", 125 | name="organization", 126 | field=models.ForeignKey( 127 | null=True, 128 | on_delete=django.db.models.deletion.SET_NULL, 129 | to="core.Organization", 130 | ), 131 | ), 132 | migrations.AlterField( 133 | model_name="eventrelatedentity", 134 | name="person", 135 | field=models.ForeignKey( 136 | null=True, 137 | on_delete=django.db.models.deletion.SET_NULL, 138 | to="core.Person", 139 | ), 140 | ), 141 | migrations.AlterField( 142 | model_name="eventrelatedentity", 143 | name="vote_event", 144 | field=models.ForeignKey( 145 | null=True, 146 | on_delete=django.db.models.deletion.SET_NULL, 147 | to="legislative.VoteEvent", 148 | ), 149 | ), 150 | migrations.AlterField( 151 | model_name="legislativesession", 152 | name="jurisdiction", 153 | field=models.ForeignKey( 154 | on_delete=django.db.models.deletion.PROTECT, 155 | related_name="legislative_sessions", 156 | to="core.Jurisdiction", 157 | ), 158 | ), 159 | migrations.AlterField( 160 | model_name="personvote", 161 | name="voter", 162 | field=models.ForeignKey( 163 | null=True, 164 | on_delete=django.db.models.deletion.SET_NULL, 165 | related_name="votes", 166 | to="core.Person", 167 | ), 168 | ), 169 | migrations.AlterField( 170 | model_name="relatedbill", 171 | name="related_bill", 172 | field=models.ForeignKey( 173 | null=True, 174 | on_delete=django.db.models.deletion.SET_NULL, 175 | related_name="related_bills_reverse", 176 | to="legislative.Bill", 177 | ), 178 | ), 179 | migrations.AlterField( 180 | model_name="voteevent", 181 | name="bill_action", 182 | field=models.OneToOneField( 183 | default=None, 184 | null=True, 185 | on_delete=django.db.models.deletion.SET_NULL, 186 | related_name="vote", 187 | to="legislative.BillAction", 188 | ), 189 | ), 190 | migrations.AlterField( 191 | model_name="voteevent", 192 | name="legislative_session", 193 | field=models.ForeignKey( 194 | on_delete=django.db.models.deletion.PROTECT, 195 | related_name="votes", 196 | to="legislative.LegislativeSession", 197 | ), 198 | ), 199 | migrations.AlterField( 200 | model_name="voteevent", 201 | name="organization", 202 | field=models.ForeignKey( 203 | on_delete=django.db.models.deletion.PROTECT, 204 | related_name="votes", 205 | to="core.Organization", 206 | ), 207 | ), 208 | ] 209 | -------------------------------------------------------------------------------- /opencivicdata/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from datetime import date, datetime 3 | from django.contrib.gis.geos import Point 4 | from opencivicdata.core.models import ( 5 | Jurisdiction, 6 | Division, 7 | Membership, 8 | Organization, 9 | Person, 10 | Post, 11 | ) 12 | from opencivicdata.legislative.models import ( 13 | LegislativeSession, 14 | Event, 15 | EventLocation, 16 | VoteEvent, 17 | Bill, 18 | ) 19 | from opencivicdata.elections.models import ( 20 | Election, 21 | ElectionIdentifier, 22 | Candidacy, 23 | CandidateContest, 24 | CandidateContestPost, 25 | CandidateContestIdentifier, 26 | BallotMeasureContest, 27 | BallotMeasureContestOption, 28 | BallotMeasureContestIdentifier, 29 | RetentionContest, 30 | RetentionContestOption, 31 | RetentionContestIdentifier, 32 | PartyContest, 33 | PartyContestOption, 34 | PartyContestIdentifier, 35 | ) 36 | 37 | 38 | @pytest.fixture 39 | def division(): 40 | div = Division.objects.create( 41 | id="ocd-division/country:us/state:mo", name="Missouri" 42 | ) 43 | return div 44 | 45 | 46 | @pytest.fixture 47 | def jurisdiction(division): 48 | juris = Jurisdiction.objects.create( 49 | id="ocd-division/country:us/state:mo", 50 | name="Missouri State Senate", 51 | url="http://www.senate.mo.gov", 52 | division=division, 53 | ) 54 | return juris 55 | 56 | 57 | @pytest.fixture 58 | def legislative_session(jurisdiction): 59 | l_s = LegislativeSession.objects.create( 60 | jurisdiction=jurisdiction, 61 | identifier=2017, 62 | name="2017 Session", 63 | start_date="2017-01-04", 64 | end_date="2017-05-25", 65 | ) 66 | return l_s 67 | 68 | 69 | @pytest.fixture 70 | def organization(): 71 | org = Organization.objects.create(name="Missouri State Senate") 72 | return org 73 | 74 | 75 | @pytest.fixture 76 | def bill(legislative_session): 77 | b = Bill.objects.create( 78 | legislative_session=legislative_session, 79 | identifier="HR 3590", 80 | title="The Patient Protection and Affordable Care Act", 81 | ) 82 | return b 83 | 84 | 85 | @pytest.fixture 86 | def vote_event(legislative_session, organization): 87 | v_e = VoteEvent.objects.create( 88 | motion_text="That the House do now proceed to the Orders of the Day.", 89 | start_date="2017-02-16", 90 | result="pass", 91 | organization=organization, 92 | legislative_session=legislative_session, 93 | ) 94 | return v_e 95 | 96 | 97 | @pytest.fixture 98 | def event_location(jurisdiction): 99 | loc = EventLocation.objects.create( 100 | name="State Legislative Building", 101 | coordinates=Point(33.448040, -112.097379), 102 | jurisdiction=jurisdiction, 103 | ) 104 | return loc 105 | 106 | 107 | @pytest.fixture 108 | def event(jurisdiction, event_location): 109 | e = Event.objects.create( 110 | name="Meeting of the Committee on Energy", 111 | jurisdiction=jurisdiction, 112 | description="To discuss the pros/cons of wind farming.", 113 | classification="committee-meeting", 114 | start_date=datetime.utcnow().isoformat().split(".")[0], 115 | status="passed", 116 | location=event_location, 117 | ) 118 | return e 119 | 120 | 121 | @pytest.fixture 122 | def party(): 123 | p = Organization.objects.create(name="Republican", classification="party") 124 | return p 125 | 126 | 127 | @pytest.fixture 128 | def person(): 129 | p = Person.objects.create( 130 | name="Arnold Schwarzenegger", sort_name="Schwarzenegger, Arnold" 131 | ) 132 | return p 133 | 134 | 135 | @pytest.fixture 136 | def post(organization): 137 | p = Post.objects.create(organization=organization, label="Governor") 138 | return p 139 | 140 | 141 | @pytest.fixture 142 | def membership(organization, post, person): 143 | m = Membership.objects.create(organization=organization, post=post, person=person) 144 | return m 145 | 146 | 147 | @pytest.fixture 148 | def election(division): 149 | elec = Election.objects.create( 150 | name="2016 General", date=date(2016, 11, 8), division=division 151 | ) 152 | return elec 153 | 154 | 155 | @pytest.fixture 156 | def election_identifier(election): 157 | elec_id = ElectionIdentifier.objects.create( 158 | election=election, scheme="calaccess_election_id", identifier="65" 159 | ) 160 | return elec_id 161 | 162 | 163 | @pytest.fixture 164 | def candidate_contest(election, division): 165 | cc = CandidateContest.objects.create( 166 | name="Governor", division=division, election=election, number_elected=1 167 | ) 168 | return cc 169 | 170 | 171 | @pytest.fixture 172 | def candidate_contest_post(candidate_contest, post): 173 | ccp = CandidateContestPost.objects.create(contest=candidate_contest, post=post) 174 | return ccp 175 | 176 | 177 | @pytest.fixture 178 | def candidacy(candidate_contest, post, person, party): 179 | cand = Candidacy.objects.create( 180 | person=person, 181 | contest=candidate_contest, 182 | post=post, 183 | candidate_name=person.name, 184 | party=party, 185 | ) 186 | return cand 187 | 188 | 189 | @pytest.fixture 190 | def candidate_contest_identifier(candidate_contest): 191 | cc_id = CandidateContestIdentifier.objects.create( 192 | contest=candidate_contest, scheme="calaccess_contest_id", identifier="GOV" 193 | ) 194 | return cc_id 195 | 196 | 197 | @pytest.fixture 198 | def ballot_measure_contest(election, division): 199 | bmc = BallotMeasureContest.objects.create( 200 | name="Proposition 060- Adult Films. Condoms. Health Requirements. Initiative Statute.", 201 | division=division, 202 | election=election, 203 | ) 204 | return bmc 205 | 206 | 207 | @pytest.fixture 208 | def ballot_measure_contest_identifier(ballot_measure_contest): 209 | bmc_id = BallotMeasureContestIdentifier.objects.create( 210 | contest=ballot_measure_contest, 211 | scheme="calaccess_measure_id", 212 | identifier="1376195", 213 | ) 214 | return bmc_id 215 | 216 | 217 | @pytest.fixture 218 | def ballot_measure_contest_option(ballot_measure_contest): 219 | opt = BallotMeasureContestOption.objects.create( 220 | contest=ballot_measure_contest, text="yes" 221 | ) 222 | return opt 223 | 224 | 225 | @pytest.fixture 226 | def retention_contest(election, division, membership): 227 | rc = RetentionContest.objects.create( 228 | name="2003 Recall Question", 229 | division=division, 230 | election=election, 231 | membership=membership, 232 | ) 233 | return rc 234 | 235 | 236 | @pytest.fixture 237 | def retention_contest_identifier(retention_contest): 238 | rc_id = RetentionContestIdentifier.objects.create( 239 | contest=retention_contest, scheme="calaccess_measure_id", identifier="1256382" 240 | ) 241 | return rc_id 242 | 243 | 244 | @pytest.fixture 245 | def retention_contest_option(retention_contest): 246 | opt = RetentionContestOption.objects.create(contest=retention_contest, text="yes") 247 | return opt 248 | 249 | 250 | @pytest.fixture 251 | def party_contest(election, division): 252 | pc = PartyContest.objects.create( 253 | name="Elections for the 20th Knesset", division=division, election=election 254 | ) 255 | return pc 256 | 257 | 258 | @pytest.fixture 259 | def party_contest_identifier(party_contest): 260 | pc_id = PartyContestIdentifier.objects.create( 261 | contest=party_contest, scheme="party_contest_id", identifier="pc09" 262 | ) 263 | return pc_id 264 | 265 | 266 | @pytest.fixture 267 | def party_contest_option(party_contest, party): 268 | opt = PartyContestOption.objects.create(contest=party_contest, party=party) 269 | return opt 270 | -------------------------------------------------------------------------------- /opencivicdata/legislative/models/event.py: -------------------------------------------------------------------------------- 1 | from django.contrib.gis.db import models 2 | from django.contrib.postgres.fields import ArrayField 3 | from opencivicdata.core.models.base import ( 4 | OCDBase, 5 | LinkBase, 6 | OCDIDField, 7 | RelatedBase, 8 | RelatedEntityBase, 9 | MimetypeLinkBase, 10 | ) 11 | from opencivicdata.core.models import Jurisdiction 12 | from .bill import Bill 13 | from .vote import VoteEvent 14 | from ...common import ( 15 | EVENT_MEDIA_CLASSIFICATION_CHOICES, 16 | EVENT_DOCUMENT_CLASSIFICATION_CHOICES, 17 | ) 18 | 19 | EVENT_STATUS_CHOICES = ( 20 | ("cancelled", "Cancelled"), 21 | ("tentative", "Tentative"), 22 | ("confirmed", "Confirmed"), 23 | ("passed", "Passed"), 24 | ) 25 | 26 | 27 | class EventMediaBase(RelatedBase): 28 | note = models.CharField(max_length=300) 29 | date = models.CharField(max_length=25, blank=True) # YYYY-MM-DD HH:MM:SS+HH:MM 30 | offset = models.PositiveIntegerField(null=True) 31 | 32 | class Meta: 33 | abstract = True 34 | 35 | 36 | class EventLocation(RelatedBase): 37 | name = models.CharField(max_length=200) 38 | url = models.URLField(blank=True, max_length=2000) 39 | coordinates = models.PointField(null=True) 40 | jurisdiction = models.ForeignKey( 41 | Jurisdiction, related_name="event_locations", on_delete=models.CASCADE 42 | ) 43 | 44 | def __str__(self): 45 | return self.name 46 | 47 | class Meta: 48 | db_table = "opencivicdata_eventlocation" 49 | 50 | 51 | class Event(OCDBase): 52 | id = OCDIDField(ocd_type="event") 53 | name = models.CharField(max_length=1000) 54 | jurisdiction = models.ForeignKey( 55 | Jurisdiction, 56 | related_name="events", 57 | # jurisdictions hard to delete 58 | on_delete=models.PROTECT, 59 | ) 60 | description = models.TextField() 61 | classification = models.CharField(max_length=100) 62 | start_date = models.CharField(max_length=25) # YYYY-MM-DD HH:MM:SS+HH:MM 63 | end_date = models.CharField(max_length=25, blank=True) # YYYY-MM-DD HH:MM:SS+HH:MM 64 | all_day = models.BooleanField(default=False) 65 | status = models.CharField(max_length=20, choices=EVENT_STATUS_CHOICES) 66 | location = models.ForeignKey(EventLocation, null=True, on_delete=models.SET_NULL) 67 | 68 | def __str__(self): 69 | return "{0} ({1})".format(self.name, self.start_date) 70 | 71 | class Meta: 72 | db_table = "opencivicdata_event" 73 | index_together = [["jurisdiction", "start_date", "name"]] 74 | 75 | 76 | class EventMedia(EventMediaBase): 77 | event = models.ForeignKey(Event, related_name="media", on_delete=models.CASCADE) 78 | classification = models.CharField( 79 | max_length=50, choices=EVENT_MEDIA_CLASSIFICATION_CHOICES, blank=True 80 | ) 81 | 82 | def __str__(self): 83 | return "%s for %s" % (self.note, self.event) 84 | 85 | class Meta: 86 | db_table = "opencivicdata_eventmedia" 87 | 88 | 89 | class EventMediaLink(MimetypeLinkBase): 90 | media = models.ForeignKey( 91 | EventMedia, related_name="links", on_delete=models.CASCADE 92 | ) 93 | 94 | def __str__(self): 95 | return "{0} for {1}".format(self.url, self.media.event) 96 | 97 | class Meta: 98 | db_table = "opencivicdata_eventmedialink" 99 | 100 | 101 | class EventDocument(RelatedBase): 102 | event = models.ForeignKey(Event, related_name="documents", on_delete=models.CASCADE) 103 | note = models.CharField(max_length=300) 104 | date = models.CharField(max_length=25, blank=True) # YYYY-MM-DD HH:MM:SS+HH:MM 105 | classification = models.CharField( 106 | max_length=50, choices=EVENT_DOCUMENT_CLASSIFICATION_CHOICES, blank=True 107 | ) 108 | 109 | def __str__(self): 110 | tmpl = "{doc.note} for event {doc.event}" 111 | return tmpl.format(doc=self) 112 | 113 | class Meta: 114 | db_table = "opencivicdata_eventdocument" 115 | 116 | 117 | class EventDocumentLink(MimetypeLinkBase): 118 | document = models.ForeignKey( 119 | EventDocument, related_name="links", on_delete=models.CASCADE 120 | ) 121 | 122 | def __str__(self): 123 | return "{0} for {1}".format(self.url, self.document) 124 | 125 | class Meta: 126 | db_table = "opencivicdata_eventdocumentlink" 127 | 128 | 129 | class EventLink(LinkBase): 130 | event = models.ForeignKey(Event, related_name="links", on_delete=models.CASCADE) 131 | 132 | class Meta: 133 | db_table = "opencivicdata_eventlink" 134 | 135 | 136 | class EventSource(LinkBase): 137 | event = models.ForeignKey(Event, related_name="sources", on_delete=models.CASCADE) 138 | 139 | class Meta: 140 | db_table = "opencivicdata_eventsource" 141 | 142 | 143 | class EventParticipant(RelatedEntityBase): 144 | event = models.ForeignKey( 145 | Event, related_name="participants", on_delete=models.CASCADE 146 | ) 147 | note = models.TextField() 148 | 149 | def __str__(self): 150 | tmpl = "%s at %s" 151 | return tmpl % (self.name, self.event) 152 | 153 | class Meta: 154 | db_table = "opencivicdata_eventparticipant" 155 | 156 | 157 | class EventAgendaItem(RelatedBase): 158 | description = models.TextField() 159 | classification = ArrayField(base_field=models.TextField(), blank=True, default=list) 160 | order = models.CharField(max_length=100, blank=True) 161 | subjects = ArrayField(base_field=models.TextField(), blank=True, default=list) 162 | notes = ArrayField(base_field=models.TextField(), blank=True, default=list) 163 | event = models.ForeignKey(Event, related_name="agenda", on_delete=models.CASCADE) 164 | extras = models.JSONField(default=dict, blank=True) 165 | 166 | def __str__(self): 167 | return "Agenda item {0} for {1}".format(self.order, self.event).replace( 168 | " ", " " 169 | ) 170 | 171 | class Meta: 172 | db_table = "opencivicdata_eventagendaitem" 173 | 174 | 175 | class EventRelatedEntity(RelatedEntityBase): 176 | agenda_item = models.ForeignKey( 177 | EventAgendaItem, related_name="related_entities", on_delete=models.CASCADE 178 | ) 179 | # will just unresolve if needed 180 | bill = models.ForeignKey(Bill, null=True, on_delete=models.SET_NULL) 181 | vote_event = models.ForeignKey(VoteEvent, null=True, on_delete=models.SET_NULL) 182 | note = models.TextField() 183 | 184 | def __str__(self): 185 | return "{0} related to {1}".format(self.entity_name, self.agenda_item) 186 | 187 | @property 188 | def entity_name(self): 189 | if self.entity_type == "vote" and self.vote_event_id: 190 | return self.vote_event.identifier 191 | elif self.entity_type == "bill" and self.bill_id: 192 | return self.bill.identifier 193 | else: 194 | return super(EventRelatedEntity, self).entity_name 195 | 196 | @property 197 | def entity_id(self): 198 | if self.entity_type == "vote": 199 | return self.vote_event_id 200 | if self.entity_type == "bill": 201 | return self.bill_id 202 | return super(EventRelatedEntity, self).entity_id 203 | 204 | class Meta: 205 | db_table = "opencivicdata_eventrelatedentity" 206 | 207 | 208 | class EventAgendaMedia(EventMediaBase): 209 | agenda_item = models.ForeignKey( 210 | EventAgendaItem, related_name="media", on_delete=models.CASCADE 211 | ) 212 | 213 | def __str__(self): 214 | return "{0} for {1}".format(self.note, self.agenda_item) 215 | 216 | class Meta: 217 | db_table = "opencivicdata_eventagendamedia" 218 | 219 | 220 | class EventAgendaMediaLink(MimetypeLinkBase): 221 | media = models.ForeignKey( 222 | EventAgendaMedia, related_name="links", on_delete=models.CASCADE 223 | ) 224 | 225 | def __str__(self): 226 | return "{0} for {1}".format(self.url, self.media) 227 | 228 | class Meta: 229 | db_table = "opencivicdata_eventagendamedialink" 230 | -------------------------------------------------------------------------------- /opencivicdata/legislative/models/bill.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from django.db import models 3 | from django.contrib.postgres.fields import ArrayField 4 | from django.contrib.postgres.search import SearchVectorField 5 | from django.contrib.postgres.indexes import GinIndex 6 | 7 | 8 | from opencivicdata.core.models.base import ( 9 | OCDBase, 10 | LinkBase, 11 | OCDIDField, 12 | RelatedBase, 13 | RelatedEntityBase, 14 | MimetypeLinkBase, 15 | IdentifierBase, 16 | ) 17 | from opencivicdata.core.models import Organization 18 | from .session import LegislativeSession 19 | from ... import common 20 | 21 | 22 | class Bill(OCDBase): 23 | id = OCDIDField(ocd_type="bill") 24 | legislative_session = models.ForeignKey( 25 | LegislativeSession, 26 | related_name="bills", 27 | # sessions should be hard to delete 28 | on_delete=models.PROTECT, 29 | ) 30 | identifier = models.CharField(max_length=100) 31 | 32 | title = models.TextField() 33 | 34 | from_organization = models.ForeignKey( 35 | Organization, 36 | related_name="bills", 37 | null=True, 38 | # chambers should be hard to delete 39 | on_delete=models.PROTECT, 40 | ) 41 | # check that array values are in enum? 42 | classification = ArrayField(base_field=models.TextField(), blank=True, default=list) 43 | subject = ArrayField(base_field=models.TextField(), blank=True, default=list) 44 | 45 | def __str__(self): 46 | return "{} in {}".format(self.identifier, self.legislative_session) 47 | 48 | class Meta: 49 | db_table = "opencivicdata_bill" 50 | index_together = [["from_organization", "legislative_session", "identifier"]] 51 | 52 | 53 | class BillAbstract(RelatedBase): 54 | bill = models.ForeignKey(Bill, related_name="abstracts", on_delete=models.CASCADE) 55 | abstract = models.TextField() 56 | note = models.TextField(blank=True) 57 | date = models.TextField(max_length=10, blank=True) # YYYY[-MM[-DD]] 58 | 59 | def __str__(self): 60 | return "{0} abstract".format(self.bill.identifier) 61 | 62 | class Meta: 63 | db_table = "opencivicdata_billabstract" 64 | 65 | 66 | class BillTitle(RelatedBase): 67 | bill = models.ForeignKey( 68 | Bill, related_name="other_titles", on_delete=models.CASCADE 69 | ) 70 | title = models.TextField() 71 | note = models.TextField(blank=True) 72 | 73 | def __str__(self): 74 | return "{0} ({1})".format(self.title, self.bill.identifier) 75 | 76 | class Meta: 77 | db_table = "opencivicdata_billtitle" 78 | 79 | 80 | class BillIdentifier(IdentifierBase): 81 | bill = models.ForeignKey( 82 | Bill, related_name="other_identifiers", on_delete=models.CASCADE 83 | ) 84 | note = models.TextField(blank=True) 85 | 86 | class Meta: 87 | db_table = "opencivicdata_billidentifier" 88 | 89 | 90 | class BillAction(RelatedBase): 91 | bill = models.ForeignKey(Bill, related_name="actions", on_delete=models.CASCADE) 92 | organization = models.ForeignKey( 93 | Organization, 94 | related_name="actions", 95 | # don't let an org delete wipe out a bunch of bill actions 96 | on_delete=models.PROTECT, 97 | ) 98 | description = models.TextField() 99 | date = models.CharField(max_length=25) # YYYY-MM-DD HH:MM:SS+HH:MM 100 | classification = ArrayField( 101 | base_field=models.TextField(), blank=True, default=list 102 | ) # enum 103 | order = models.PositiveIntegerField() 104 | extras = models.JSONField(default=dict, blank=True) 105 | 106 | class Meta: 107 | db_table = "opencivicdata_billaction" 108 | ordering = ["order"] 109 | 110 | def __str__(self): 111 | return "{0} action on {1}".format(self.bill.identifier, self.date) 112 | 113 | 114 | class BillActionRelatedEntity(RelatedEntityBase): 115 | action = models.ForeignKey( 116 | BillAction, related_name="related_entities", on_delete=models.CASCADE 117 | ) 118 | 119 | def __str__(self): 120 | return "{0} related to {1}".format(self.entity_name, self.action) 121 | 122 | class Meta: 123 | db_table = "opencivicdata_billactionrelatedentity" 124 | 125 | 126 | class RelatedBill(RelatedBase): 127 | bill = models.ForeignKey( 128 | Bill, related_name="related_bills", on_delete=models.CASCADE 129 | ) 130 | related_bill = models.ForeignKey( 131 | Bill, 132 | related_name="related_bills_reverse", 133 | null=True, 134 | # if related bill goes away, just unlink the relationship 135 | on_delete=models.SET_NULL, 136 | ) 137 | identifier = models.CharField(max_length=100) 138 | # not a FK in case we don't know the session yet 139 | legislative_session = models.CharField(max_length=100) 140 | relation_type = models.CharField( 141 | max_length=100, choices=common.BILL_RELATION_TYPE_CHOICES 142 | ) 143 | 144 | def __str__(self): 145 | return "relationship of {} to {} ({})".format( 146 | self.bill, self.related_bill, self.relation_type 147 | ) 148 | 149 | class Meta: 150 | db_table = "opencivicdata_relatedbill" 151 | 152 | 153 | class BillSponsorship(RelatedEntityBase): 154 | bill = models.ForeignKey( 155 | Bill, related_name="sponsorships", on_delete=models.CASCADE 156 | ) 157 | primary = models.BooleanField(default=False) 158 | classification = models.CharField(max_length=100) # enum? 159 | 160 | def __str__(self): 161 | return "{} ({}) sponsorship of {}".format( 162 | self.name, self.entity_type, self.bill 163 | ) 164 | 165 | class Meta: 166 | db_table = "opencivicdata_billsponsorship" 167 | 168 | 169 | class BillDocument(RelatedBase): 170 | bill = models.ForeignKey(Bill, related_name="documents", on_delete=models.CASCADE) 171 | note = models.CharField(max_length=300) 172 | date = models.CharField(max_length=10) # YYYY[-MM[-DD]] 173 | extras = models.JSONField(default=dict, blank=True) 174 | 175 | def __str__(self): 176 | return "{0} document of {1}".format(self.date, self.bill) 177 | 178 | class Meta: 179 | db_table = "opencivicdata_billdocument" 180 | 181 | 182 | class BillVersion(RelatedBase): 183 | bill = models.ForeignKey(Bill, related_name="versions", on_delete=models.CASCADE) 184 | note = models.CharField(max_length=300) 185 | date = models.CharField(max_length=10) # YYYY[-MM[-DD]] 186 | extras = models.JSONField(default=dict, blank=True) 187 | 188 | def __str__(self): 189 | return "{0} version of {1}".format(self.date, self.bill) 190 | 191 | class Meta: 192 | db_table = "opencivicdata_billversion" 193 | 194 | 195 | class BillDocumentLink(MimetypeLinkBase): 196 | document = models.ForeignKey( 197 | BillDocument, related_name="links", on_delete=models.CASCADE 198 | ) 199 | 200 | def __str__(self): 201 | return "{0} for {1}".format(self.url, self.document.bill) 202 | 203 | class Meta: 204 | db_table = "opencivicdata_billdocumentlink" 205 | 206 | 207 | class BillVersionLink(MimetypeLinkBase): 208 | version = models.ForeignKey( 209 | BillVersion, related_name="links", on_delete=models.CASCADE 210 | ) 211 | 212 | def __str__(self): 213 | return "{0} for {1}".format(self.url, self.version) 214 | 215 | class Meta: 216 | db_table = "opencivicdata_billversionlink" 217 | 218 | 219 | class BillSource(LinkBase): 220 | bill = models.ForeignKey(Bill, related_name="sources", on_delete=models.CASCADE) 221 | 222 | class Meta: 223 | db_table = "opencivicdata_billsource" 224 | 225 | 226 | class SearchableBill(models.Model): 227 | """ 228 | This model associates a single version's text with a given bill. 229 | 230 | This is done for a few reasons: 231 | * bills with multiple versions aren't weighted more heavily than others 232 | * this makes querying quite a bit more efficient (no need to deduplicate results) 233 | 234 | We'll also store error results, assuming that they're somewhat persistent. 235 | """ 236 | 237 | bill = models.OneToOneField( 238 | Bill, related_name="searchable", null=True, on_delete=models.CASCADE 239 | ) 240 | version_link = models.OneToOneField( 241 | BillVersionLink, related_name="searchable", null=True, on_delete=models.CASCADE 242 | ) 243 | 244 | search_vector = SearchVectorField(default=None) 245 | all_titles = models.TextField(default="") 246 | raw_text = models.TextField(default="") 247 | is_error = models.BooleanField(default=False) 248 | 249 | created_at = models.DateTimeField(auto_now_add=True) 250 | 251 | class Meta: 252 | db_table = "opencivicdata_searchablebill" 253 | indexes = [GinIndex(name="search_index", fields=["search_vector"])] 254 | -------------------------------------------------------------------------------- /opencivicdata/elections/migrations/0004_field_docs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.3 on 2017-08-17 18:45 3 | from __future__ import unicode_literals 4 | 5 | import django.contrib.postgres.fields.jsonb 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [("elections", "0003_election_fk")] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name="ballotmeasurecontest", 17 | name="created_at", 18 | field=models.DateTimeField( 19 | auto_now_add=True, help_text="The date and time of creation." 20 | ), 21 | ), 22 | migrations.AlterField( 23 | model_name="ballotmeasurecontest", 24 | name="extras", 25 | field=django.contrib.postgres.fields.jsonb.JSONField( 26 | blank=True, 27 | default=dict, 28 | help_text="A key-value store for storing arbitrary information not covered elsewhere.", # noqa 29 | ), 30 | ), 31 | migrations.AlterField( 32 | model_name="ballotmeasurecontest", 33 | name="updated_at", 34 | field=models.DateTimeField( 35 | auto_now=True, help_text="The date and time of the last update." 36 | ), 37 | ), 38 | migrations.AlterField( 39 | model_name="ballotmeasurecontestidentifier", 40 | name="identifier", 41 | field=models.CharField( 42 | help_text="A unique identifier developed by an upstream or third party source.", 43 | max_length=300, 44 | ), 45 | ), 46 | migrations.AlterField( 47 | model_name="ballotmeasurecontestidentifier", 48 | name="scheme", 49 | field=models.CharField( 50 | help_text="The name of the service that created the identifier.", 51 | max_length=300, 52 | ), 53 | ), 54 | migrations.AlterField( 55 | model_name="ballotmeasurecontestsource", 56 | name="note", 57 | field=models.CharField( 58 | blank=True, 59 | help_text="A short, optional note related to an object.", 60 | max_length=300, 61 | ), 62 | ), 63 | migrations.AlterField( 64 | model_name="ballotmeasurecontestsource", 65 | name="url", 66 | field=models.URLField( 67 | help_text="A hyperlink related to an object.", max_length=2000 68 | ), 69 | ), 70 | migrations.AlterField( 71 | model_name="candidacy", 72 | name="created_at", 73 | field=models.DateTimeField( 74 | auto_now_add=True, help_text="The date and time of creation." 75 | ), 76 | ), 77 | migrations.AlterField( 78 | model_name="candidacy", 79 | name="extras", 80 | field=django.contrib.postgres.fields.jsonb.JSONField( 81 | blank=True, 82 | default=dict, 83 | help_text="A key-value store for storing arbitrary information not covered elsewhere.", # noqa 84 | ), 85 | ), 86 | migrations.AlterField( 87 | model_name="candidacy", 88 | name="updated_at", 89 | field=models.DateTimeField( 90 | auto_now=True, help_text="The date and time of the last update." 91 | ), 92 | ), 93 | migrations.AlterField( 94 | model_name="candidacysource", 95 | name="note", 96 | field=models.CharField( 97 | blank=True, 98 | help_text="A short, optional note related to an object.", 99 | max_length=300, 100 | ), 101 | ), 102 | migrations.AlterField( 103 | model_name="candidacysource", 104 | name="url", 105 | field=models.URLField( 106 | help_text="A hyperlink related to an object.", max_length=2000 107 | ), 108 | ), 109 | migrations.AlterField( 110 | model_name="candidatecontest", 111 | name="created_at", 112 | field=models.DateTimeField( 113 | auto_now_add=True, help_text="The date and time of creation." 114 | ), 115 | ), 116 | migrations.AlterField( 117 | model_name="candidatecontest", 118 | name="extras", 119 | field=django.contrib.postgres.fields.jsonb.JSONField( 120 | blank=True, 121 | default=dict, 122 | help_text="A key-value store for storing arbitrary information not covered elsewhere.", # noqa 123 | ), 124 | ), 125 | migrations.AlterField( 126 | model_name="candidatecontest", 127 | name="updated_at", 128 | field=models.DateTimeField( 129 | auto_now=True, help_text="The date and time of the last update." 130 | ), 131 | ), 132 | migrations.AlterField( 133 | model_name="candidatecontestidentifier", 134 | name="identifier", 135 | field=models.CharField( 136 | help_text="A unique identifier developed by an upstream or third party source.", 137 | max_length=300, 138 | ), 139 | ), 140 | migrations.AlterField( 141 | model_name="candidatecontestidentifier", 142 | name="scheme", 143 | field=models.CharField( 144 | help_text="The name of the service that created the identifier.", 145 | max_length=300, 146 | ), 147 | ), 148 | migrations.AlterField( 149 | model_name="candidatecontestsource", 150 | name="note", 151 | field=models.CharField( 152 | blank=True, 153 | help_text="A short, optional note related to an object.", 154 | max_length=300, 155 | ), 156 | ), 157 | migrations.AlterField( 158 | model_name="candidatecontestsource", 159 | name="url", 160 | field=models.URLField( 161 | help_text="A hyperlink related to an object.", max_length=2000 162 | ), 163 | ), 164 | migrations.AlterField( 165 | model_name="election", 166 | name="created_at", 167 | field=models.DateTimeField( 168 | auto_now_add=True, help_text="The date and time of creation." 169 | ), 170 | ), 171 | migrations.AlterField( 172 | model_name="election", 173 | name="extras", 174 | field=django.contrib.postgres.fields.jsonb.JSONField( 175 | blank=True, 176 | default=dict, 177 | help_text="A key-value store for storing arbitrary information not covered elsewhere.", # noqa 178 | ), 179 | ), 180 | migrations.AlterField( 181 | model_name="election", 182 | name="updated_at", 183 | field=models.DateTimeField( 184 | auto_now=True, help_text="The date and time of the last update." 185 | ), 186 | ), 187 | migrations.AlterField( 188 | model_name="electionidentifier", 189 | name="election", 190 | field=models.ForeignKey( 191 | help_text="Reference to the Election identified by this alternative identifier.", 192 | on_delete=django.db.models.deletion.CASCADE, 193 | related_name="identifiers", 194 | to="elections.Election", 195 | ), 196 | ), 197 | migrations.AlterField( 198 | model_name="electionidentifier", 199 | name="identifier", 200 | field=models.CharField( 201 | help_text="A unique identifier developed by an upstream or third party source.", 202 | max_length=300, 203 | ), 204 | ), 205 | migrations.AlterField( 206 | model_name="electionidentifier", 207 | name="scheme", 208 | field=models.CharField( 209 | help_text="The name of the service that created the identifier.", 210 | max_length=300, 211 | ), 212 | ), 213 | migrations.AlterField( 214 | model_name="electionsource", 215 | name="election", 216 | field=models.ForeignKey( 217 | help_text="Reference to the Election this source verifies.", 218 | on_delete=django.db.models.deletion.CASCADE, 219 | related_name="sources", 220 | to="elections.Election", 221 | ), 222 | ), 223 | migrations.AlterField( 224 | model_name="electionsource", 225 | name="note", 226 | field=models.CharField( 227 | blank=True, 228 | help_text="A short, optional note related to an object.", 229 | max_length=300, 230 | ), 231 | ), 232 | migrations.AlterField( 233 | model_name="electionsource", 234 | name="url", 235 | field=models.URLField( 236 | help_text="A hyperlink related to an object.", max_length=2000 237 | ), 238 | ), 239 | migrations.AlterField( 240 | model_name="partycontest", 241 | name="created_at", 242 | field=models.DateTimeField( 243 | auto_now_add=True, help_text="The date and time of creation." 244 | ), 245 | ), 246 | migrations.AlterField( 247 | model_name="partycontest", 248 | name="extras", 249 | field=django.contrib.postgres.fields.jsonb.JSONField( 250 | blank=True, 251 | default=dict, 252 | help_text="A key-value store for storing arbitrary information not covered elsewhere.", # noqa 253 | ), 254 | ), 255 | migrations.AlterField( 256 | model_name="partycontest", 257 | name="updated_at", 258 | field=models.DateTimeField( 259 | auto_now=True, help_text="The date and time of the last update." 260 | ), 261 | ), 262 | migrations.AlterField( 263 | model_name="partycontestidentifier", 264 | name="identifier", 265 | field=models.CharField( 266 | help_text="A unique identifier developed by an upstream or third party source.", 267 | max_length=300, 268 | ), 269 | ), 270 | migrations.AlterField( 271 | model_name="partycontestidentifier", 272 | name="scheme", 273 | field=models.CharField( 274 | help_text="The name of the service that created the identifier.", 275 | max_length=300, 276 | ), 277 | ), 278 | migrations.AlterField( 279 | model_name="partycontestsource", 280 | name="note", 281 | field=models.CharField( 282 | blank=True, 283 | help_text="A short, optional note related to an object.", 284 | max_length=300, 285 | ), 286 | ), 287 | migrations.AlterField( 288 | model_name="partycontestsource", 289 | name="url", 290 | field=models.URLField( 291 | help_text="A hyperlink related to an object.", max_length=2000 292 | ), 293 | ), 294 | migrations.AlterField( 295 | model_name="retentioncontest", 296 | name="created_at", 297 | field=models.DateTimeField( 298 | auto_now_add=True, help_text="The date and time of creation." 299 | ), 300 | ), 301 | migrations.AlterField( 302 | model_name="retentioncontest", 303 | name="extras", 304 | field=django.contrib.postgres.fields.jsonb.JSONField( 305 | blank=True, 306 | default=dict, 307 | help_text="A key-value store for storing arbitrary information not covered elsewhere.", # noqa 308 | ), 309 | ), 310 | migrations.AlterField( 311 | model_name="retentioncontest", 312 | name="updated_at", 313 | field=models.DateTimeField( 314 | auto_now=True, help_text="The date and time of the last update." 315 | ), 316 | ), 317 | migrations.AlterField( 318 | model_name="retentioncontestidentifier", 319 | name="identifier", 320 | field=models.CharField( 321 | help_text="A unique identifier developed by an upstream or third party source.", 322 | max_length=300, 323 | ), 324 | ), 325 | migrations.AlterField( 326 | model_name="retentioncontestidentifier", 327 | name="scheme", 328 | field=models.CharField( 329 | help_text="The name of the service that created the identifier.", 330 | max_length=300, 331 | ), 332 | ), 333 | migrations.AlterField( 334 | model_name="retentioncontestsource", 335 | name="note", 336 | field=models.CharField( 337 | blank=True, 338 | help_text="A short, optional note related to an object.", 339 | max_length=300, 340 | ), 341 | ), 342 | migrations.AlterField( 343 | model_name="retentioncontestsource", 344 | name="url", 345 | field=models.URLField( 346 | help_text="A hyperlink related to an object.", max_length=2000 347 | ), 348 | ), 349 | ] 350 | --------------------------------------------------------------------------------