├── .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 | [](https://travis-ci.com/opencivicdata/python-opencivicdata)
5 | [](https://coveralls.io/r/opencivicdata/python-opencivicdata?branch=master)
6 | [](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 |
--------------------------------------------------------------------------------