Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API endpoints for departments #1138

Open
wants to merge 87 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 83 commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
7057311
Create __init__.py
michplunkett Dec 14, 2024
549ec84
Base API files
michplunkett Dec 14, 2024
5f43d63
Update views.py
michplunkett Dec 17, 2024
bb0aab4
Update views.py
michplunkett Dec 17, 2024
eb02d88
Lint
michplunkett Dec 18, 2024
ddb8233
Remove Flask-Sitemap
michplunkett Dec 18, 2024
04976b8
Move limiter to utils
michplunkett Dec 18, 2024
b9414e8
Add back sitemap
michplunkett Dec 18, 2024
e7c02be
Update views.py
michplunkett Dec 18, 2024
cf5f7a4
Update views.py
michplunkett Dec 18, 2024
ada954b
Update views.py
michplunkett Dec 18, 2024
2742f80
Update views.py
michplunkett Dec 18, 2024
07fe24c
Modify Blueprints
michplunkett Dec 19, 2024
bc57312
Delete officers.py
michplunkett Dec 19, 2024
c1388c4
Delete incidents.py
michplunkett Dec 19, 2024
066f294
Delete departments.py
michplunkett Dec 19, 2024
ff35e1f
Create api.py
michplunkett Dec 19, 2024
5e00b0b
Create api.py
michplunkett Dec 19, 2024
fcb3dd7
Update __init__.py
michplunkett Dec 19, 2024
2ca4be7
Update api.py
michplunkett Dec 19, 2024
bcd27c2
Update api.py
michplunkett Dec 19, 2024
6485464
Update views.py
michplunkett Dec 19, 2024
659993a
Update api.py
michplunkett Dec 19, 2024
6505ce7
Create test_v1_api.py
michplunkett Dec 19, 2024
5a39961
Restructure
michplunkett Dec 19, 2024
8af766a
Delete api.py
michplunkett Dec 19, 2024
88fd11c
Update __init__.py
michplunkett Dec 19, 2024
9a17b02
Create v1.py
michplunkett Dec 19, 2024
0c9b088
Create test_api_v1.py
michplunkett Dec 19, 2024
bd2971f
Update test_api_v1.py
michplunkett Dec 19, 2024
f73b203
Update test_api_v1.py
michplunkett Dec 19, 2024
8a21eca
Update v1.py
michplunkett Dec 19, 2024
55d3d0c
Update test_api_v1.py
michplunkett Dec 19, 2024
1efde38
Update v1.py
michplunkett Dec 19, 2024
60a9e49
Update test_api_v1.py
michplunkett Dec 19, 2024
668b1b6
Update test_api_v1.py
michplunkett Dec 19, 2024
b56d070
Update v1.py
michplunkett Dec 19, 2024
8e3f44f
Update database.py
michplunkett Dec 19, 2024
cb66a22
Update db.py
michplunkett Dec 20, 2024
9a1fe2f
Update __init__.py
michplunkett Dec 20, 2024
2f50bb9
Update test_api_v1.py
michplunkett Dec 20, 2024
fd5f406
Update database.py
michplunkett Dec 20, 2024
e74eff2
Update db.py
michplunkett Dec 20, 2024
6ba9269
repr tests
michplunkett Dec 20, 2024
2fcdca8
Update db.py
michplunkett Dec 20, 2024
7c8dcb1
Update __init__.py
michplunkett Dec 20, 2024
11443db
Update database.py
michplunkett Dec 20, 2024
f610941
Update database.py
michplunkett Dec 20, 2024
74f20d2
Update v1.py
michplunkett Dec 20, 2024
e8535cb
Update database.py
michplunkett Dec 20, 2024
013f4bb
Update views.py
michplunkett Dec 20, 2024
c6b7f52
Update test_models.py
michplunkett Dec 20, 2024
0bacc74
Update database.py
michplunkett Dec 20, 2024
112fd35
Revert "Update views.py"
michplunkett Dec 20, 2024
8f6a4e3
Update database.py
michplunkett Dec 20, 2024
0ba2ec8
Add tests
michplunkett Dec 20, 2024
6d0f577
Update db.py
michplunkett Dec 20, 2024
731085b
Update database.py
michplunkett Dec 20, 2024
6ebd1de
Update views.py
michplunkett Dec 20, 2024
9b95c66
Update database.py
michplunkett Dec 21, 2024
bc618af
Merge branch 'develop' into build-api
michplunkett Jan 2, 2025
88be2c3
Update database.py
michplunkett Jan 2, 2025
42f8e6f
Merge branch 'develop' into build-api
michplunkett Jan 3, 2025
cca977a
Merge branch 'develop' into build-api
michplunkett Jan 6, 2025
d2d897c
Merge branch 'develop' into build-api
michplunkett Jan 15, 2025
ca3fdd4
Merge branch 'develop' into build-api
michplunkett Jan 16, 2025
9435fc3
Update database.py
michplunkett Jan 16, 2025
e1f6c54
Merge branch 'develop' into build-api
michplunkett Jan 16, 2025
1385597
Update database.py
michplunkett Jan 16, 2025
024d8fa
Update v1.py
michplunkett Jan 16, 2025
b14f0dd
Update v1.py
michplunkett Jan 16, 2025
5905525
Update v1.py
michplunkett Jan 16, 2025
4d2acd7
Add Department.get_officers
michplunkett Jan 19, 2025
74f06f0
Update database.py
michplunkett Jan 19, 2025
4259fcd
Add Department.get_assignments
michplunkett Jan 19, 2025
0b4f477
Add Department.get_salaries
michplunkett Jan 19, 2025
0b5d2e2
Add Department.get_incidents
michplunkett Jan 19, 2025
731d97a
Add Department.get_descriptions
michplunkett Jan 19, 2025
87c45fd
Add Department.get_links
michplunkett Jan 19, 2025
c7a1f64
Update v1.py
michplunkett Jan 19, 2025
496006c
Update test_singular_redirects.py
michplunkett Jan 19, 2025
ed667e1
Update test_singular_redirects.py
michplunkett Jan 19, 2025
c88c076
Update database.py
michplunkett Jan 19, 2025
a4c9f8d
Update database.py
michplunkett Jan 19, 2025
a7615f1
Update database.py
michplunkett Jan 19, 2025
84ad6c8
Update database.py
michplunkett Jan 21, 2025
d5826f6
Update database.py
michplunkett Jan 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions OpenOversight/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from flask_migrate import Migrate
from flask_wtf.csrf import CSRFProtect

from OpenOversight.app.api.v1 import v1 as api_v1_blueprint
from OpenOversight.app.auth.views import auth as auth_blueprint
from OpenOversight.app.email_client import EmailClient
from OpenOversight.app.filters import instantiate_filters
Expand Down Expand Up @@ -48,6 +49,7 @@ def create_app(config_name="default"):
compress.init_app(app)

# Register Blueprints for application routes
app.register_blueprint(api_v1_blueprint)
app.register_blueprint(auth_blueprint)
app.register_blueprint(main_blueprint)

Expand Down
Empty file.
56 changes: 56 additions & 0 deletions OpenOversight/app/api/v1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from http import HTTPMethod

from flask import Blueprint, Response, jsonify
from sqlalchemy.orm import Query

from OpenOversight.app.models.database import Department
from OpenOversight.app.utils.flask import limiter


v1 = Blueprint("v1", __name__, url_prefix="/api/v1")


def objs_to_dicts_jsonify(obj_list: Query) -> Response:
return jsonify([o.to_dict() for o in obj_list])


@v1.route("/departments/<int:department_id>/assignments", methods=[HTTPMethod.GET])
@limiter.limit("5/minute")
def get_dept_assignments(department_id: int) -> Response:
assignments = Department.get_assignments(department_id)
return objs_to_dicts_jsonify(assignments)
Comment on lines +13 to +21
Copy link
Collaborator

@sea-kelp sea-kelp Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know we already vend a full copy our department data for CSV exports but I think we may want to consider adding pagination and filtering to reduce the amount of data we vend out per request.



@v1.route("/departments/<int:department_id>/descriptions", methods=[HTTPMethod.GET])
@limiter.limit("5/minute")
def get_dept_descriptions(department_id: int) -> Response:
descriptions = Department.get_descriptions(department_id)
return objs_to_dicts_jsonify(descriptions)
michplunkett marked this conversation as resolved.
Show resolved Hide resolved


@v1.route("/departments/<int:department_id>/incidents", methods=[HTTPMethod.GET])
@limiter.limit("5/minute")
def get_dept_incidents(department_id: int) -> Response:
incidents = Department.get_incidents(department_id)
return objs_to_dicts_jsonify(incidents)


@v1.route("/departments/<int:department_id>/links", methods=[HTTPMethod.GET])
@limiter.limit("5/minute")
def get_dept_links(department_id: int) -> Response:
links = Department.get_links(department_id)
return objs_to_dicts_jsonify(links)


@v1.route("/departments/<int:department_id>/officers", methods=[HTTPMethod.GET])
@limiter.limit("5/minute")
def get_dept_officers(department_id: int) -> Response:
officers = Department.get_officers(department_id)
return objs_to_dicts_jsonify(officers)


@v1.route("/departments/<int:department_id>/salaries", methods=[HTTPMethod.GET])
@limiter.limit("5/minute")
def get_dept_salaries(department_id: int) -> Response:
salaries = Department.get_salaries(department_id)
return objs_to_dicts_jsonify(salaries)
112 changes: 31 additions & 81 deletions OpenOversight/app/main/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from flask_login import current_user, login_required, login_user
from flask_wtf import FlaskForm
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import contains_eager, joinedload, selectinload
from sqlalchemy.orm import selectinload
from sqlalchemy.orm.exc import NoResultFound

from OpenOversight.app.auth.forms import LoginForm
Expand Down Expand Up @@ -71,20 +71,14 @@
User,
db,
)
from OpenOversight.app.models.database_cache import (
get_database_cache_entry,
put_database_cache_entry,
)
from OpenOversight.app.utils.auth import ac_or_admin_required, admin_required
from OpenOversight.app.utils.choices import AGE_CHOICES, GENDER_CHOICES, RACE_CHOICES
from OpenOversight.app.utils.cloud import crop_image, save_image_to_s3_and_db
from OpenOversight.app.utils.constants import (
ENCODING_UTF_8,
FLASH_MSG_PERMANENT_REDIRECT,
KEY_DEPT_ALL_ASSIGNMENTS,
KEY_DEPT_ALL_INCIDENTS,
KEY_DEPT_ALL_LINKS,
KEY_DEPT_ALL_NOTES,
KEY_DEPT_ALL_OFFICERS,
KEY_DEPT_ALL_SALARIES,
KEY_DEPT_TOTAL_ASSIGNMENTS,
Expand Down Expand Up @@ -217,7 +211,8 @@ def get_officer():
def redirect_get_started_labeling():
flash(FLASH_MSG_PERMANENT_REDIRECT)
return redirect(
url_for("main.get_started_labeling"), code=HTTPStatus.PERMANENT_REDIRECT
url_for("main.get_started_labeling"),
code=HTTPStatus.PERMANENT_REDIRECT,
)


Expand Down Expand Up @@ -422,7 +417,9 @@ def redirect_edit_assignment(officer_id: int, assignment_id: int):
flash(FLASH_MSG_PERMANENT_REDIRECT)
return redirect(
url_for(
"main.edit_assignment", officer_id=officer_id, assignment_id=assignment_id
"main.edit_assignment",
officer_id=officer_id,
assignment_id=assignment_id,
),
code=HTTPStatus.PERMANENT_REDIRECT,
)
Expand Down Expand Up @@ -632,7 +629,9 @@ def display_tag(tag_id: int):
def redirect_classify_submission(image_id: int, contains_cops: int):
return redirect(
url_for(
"main.classify_submission", image_id=image_id, contains_cops=contains_cops
"main.classify_submission",
image_id=image_id,
contains_cops=contains_cops,
),
code=HTTPStatus.PERMANENT_REDIRECT,
)
Expand Down Expand Up @@ -816,7 +815,10 @@ def edit_department(department_id: int):
f"You attempted to delete a rank, {rank}, that is still in use"
)
return redirect(
url_for("main.edit_department", department_id=department_id)
url_for(
"main.edit_department",
department_id=department_id,
)
)

for new_rank, order in new_ranks:
Expand Down Expand Up @@ -1572,18 +1574,7 @@ def redirect_download_dept_officers_csv(department_id: int):
)
@limiter.limit("5/minute")
def download_dept_officers_csv(department_id: int):
cache_params = (Department(id=department_id), KEY_DEPT_ALL_OFFICERS)
officers = get_database_cache_entry(*cache_params)
if officers is None:
officers = (
db.session.query(Officer)
.options(joinedload(Officer.assignments).joinedload(Assignment.job))
.options(joinedload(Officer.salaries))
.filter_by(department_id=department_id)
.all()
)
put_database_cache_entry(*cache_params, officers)

officers = Department.get_officers(department_id)
field_names = [
"id",
"unique identifier",
Expand All @@ -1599,6 +1590,7 @@ def download_dept_officers_csv(department_id: int):
"job title",
"most recent salary",
]

return make_downloadable_csv(
officers, department_id, "Officers", field_names, officer_record_maker
)
Expand All @@ -1620,20 +1612,7 @@ def redirect_download_dept_assignments_csv(department_id: int):
)
@limiter.limit("5/minute")
def download_dept_assignments_csv(department_id: int):
cache_params = Department(id=department_id), KEY_DEPT_ALL_ASSIGNMENTS
assignments = get_database_cache_entry(*cache_params)
if assignments is None:
assignments = (
db.session.query(Assignment)
.join(Assignment.base_officer)
.filter(Officer.department_id == department_id)
.options(contains_eager(Assignment.base_officer))
.options(joinedload(Assignment.unit))
.options(joinedload(Assignment.job))
.all()
)
put_database_cache_entry(*cache_params, assignments)

assignments = Department.get_assignments(department_id)
field_names = [
"id",
"officer id",
Expand All @@ -1645,6 +1624,7 @@ def download_dept_assignments_csv(department_id: int):
"unit id",
"unit description",
]

return make_downloadable_csv(
assignments,
department_id,
Expand All @@ -1670,12 +1650,7 @@ def redirect_download_incidents_csv(department_id: int):
)
@limiter.limit("5/minute")
def download_incidents_csv(department_id: int):
cache_params = (Department(id=department_id), KEY_DEPT_ALL_INCIDENTS)
incidents = get_database_cache_entry(*cache_params)
if incidents is None:
incidents = Incident.query.filter_by(department_id=department_id).all()
put_database_cache_entry(*cache_params, incidents)

incidents = Department.get_incidents(department_id)
field_names = [
"id",
"report_num",
Expand All @@ -1687,6 +1662,7 @@ def download_incidents_csv(department_id: int):
"links",
"officers",
]

return make_downloadable_csv(
incidents,
department_id,
Expand All @@ -1712,18 +1688,7 @@ def redirect_download_dept_salaries_csv(department_id: int):
)
@limiter.limit("5/minute")
def download_dept_salaries_csv(department_id: int):
cache_params = (Department(id=department_id), KEY_DEPT_ALL_SALARIES)
salaries = get_database_cache_entry(*cache_params)
if salaries is None:
salaries = (
db.session.query(Salary)
.join(Salary.officer)
.filter(Officer.department_id == department_id)
.options(contains_eager(Salary.officer))
.all()
)
put_database_cache_entry(*cache_params, salaries)

salaries = Department.get_salaries(department_id)
field_names = [
"id",
"officer id",
Expand All @@ -1734,6 +1699,7 @@ def download_dept_salaries_csv(department_id: int):
"year",
"is_fiscal_year",
]

return make_downloadable_csv(
salaries, department_id, "Salaries", field_names, salary_record_maker
)
Expand All @@ -1751,18 +1717,7 @@ def redirect_download_dept_links_csv(department_id: int):
@main.route("/download/departments/<int:department_id>/links", methods=[HTTPMethod.GET])
@limiter.limit("5/minute")
def download_dept_links_csv(department_id: int):
cache_params = (Department(id=department_id), KEY_DEPT_ALL_LINKS)
links = get_database_cache_entry(*cache_params)
if links is None:
links = (
db.session.query(Link)
.join(Link.officers)
.filter(Officer.department_id == department_id)
.options(contains_eager(Link.officers))
.all()
)
put_database_cache_entry(*cache_params, links)

links = Department.get_links(department_id)
field_names = [
"id",
"title",
Expand All @@ -1773,6 +1728,7 @@ def download_dept_links_csv(department_id: int):
"officers",
"incidents",
]

return make_downloadable_csv(
links, department_id, "Links", field_names, links_record_maker
)
Expand All @@ -1794,18 +1750,7 @@ def redirect_download_dept_descriptions_csv(department_id: int):
)
@limiter.limit("5/minute")
def download_dept_descriptions_csv(department_id: int):
cache_params = (Department(id=department_id), KEY_DEPT_ALL_NOTES)
notes = get_database_cache_entry(*cache_params)
if notes is None:
notes = (
db.session.query(Description)
.join(Description.officer)
.filter(Officer.department_id == department_id)
.options(contains_eager(Description.officer))
.all()
)
put_database_cache_entry(*cache_params, notes)

descriptions = Department.get_descriptions(department_id)
field_names = [
"id",
"text_contents",
Expand All @@ -1814,8 +1759,9 @@ def download_dept_descriptions_csv(department_id: int):
"created_at",
"last_updated_at",
]

return make_downloadable_csv(
notes, department_id, "Notes", field_names, descriptions_record_maker
descriptions, department_id, "Notes", field_names, descriptions_record_maker
)


Expand Down Expand Up @@ -2302,7 +2248,11 @@ def redirect_edit_description(officer_id: int, obj_id=None):
def redirect_delete_description(officer_id: int, obj_id=None):
flash(FLASH_MSG_PERMANENT_REDIRECT)
return redirect(
url_for("main.description_api_delete", officer_id=officer_id, obj_id=obj_id),
url_for(
"main.description_api_delete",
officer_id=officer_id,
obj_id=obj_id,
),
code=HTTPStatus.PERMANENT_REDIRECT,
)

Expand Down
Loading
Loading