-
Notifications
You must be signed in to change notification settings - Fork 79
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
michplunkett
wants to merge
72
commits into
develop
Choose a base branch
from
build-api
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+247
−9
Open
Changes from all commits
Commits
Show all changes
72 commits
Select commit
Hold shift + click to select a range
7057311
Create __init__.py
michplunkett 549ec84
Base API files
michplunkett 5f43d63
Update views.py
michplunkett bb0aab4
Update views.py
michplunkett eb02d88
Lint
michplunkett ddb8233
Remove Flask-Sitemap
michplunkett 04976b8
Move limiter to utils
michplunkett b9414e8
Add back sitemap
michplunkett e7c02be
Update views.py
michplunkett cf5f7a4
Update views.py
michplunkett ada954b
Update views.py
michplunkett 2742f80
Update views.py
michplunkett 07fe24c
Modify Blueprints
michplunkett bc57312
Delete officers.py
michplunkett c1388c4
Delete incidents.py
michplunkett 066f294
Delete departments.py
michplunkett ff35e1f
Create api.py
michplunkett 5e00b0b
Create api.py
michplunkett fcb3dd7
Update __init__.py
michplunkett 2ca4be7
Update api.py
michplunkett bcd27c2
Update api.py
michplunkett 6485464
Update views.py
michplunkett 659993a
Update api.py
michplunkett 6505ce7
Create test_v1_api.py
michplunkett 5a39961
Restructure
michplunkett 8af766a
Delete api.py
michplunkett 88fd11c
Update __init__.py
michplunkett 9a17b02
Create v1.py
michplunkett 0c9b088
Create test_api_v1.py
michplunkett bd2971f
Update test_api_v1.py
michplunkett f73b203
Update test_api_v1.py
michplunkett 8a21eca
Update v1.py
michplunkett 55d3d0c
Update test_api_v1.py
michplunkett 1efde38
Update v1.py
michplunkett 60a9e49
Update test_api_v1.py
michplunkett 668b1b6
Update test_api_v1.py
michplunkett b56d070
Update v1.py
michplunkett 8e3f44f
Update database.py
michplunkett cb66a22
Update db.py
michplunkett 9a1fe2f
Update __init__.py
michplunkett 2f50bb9
Update test_api_v1.py
michplunkett fd5f406
Update database.py
michplunkett e74eff2
Update db.py
michplunkett 6ba9269
repr tests
michplunkett 2fcdca8
Update db.py
michplunkett 7c8dcb1
Update __init__.py
michplunkett 11443db
Update database.py
michplunkett f610941
Update database.py
michplunkett 74f20d2
Update v1.py
michplunkett e8535cb
Update database.py
michplunkett 013f4bb
Update views.py
michplunkett c6b7f52
Update test_models.py
michplunkett 0bacc74
Update database.py
michplunkett 112fd35
Revert "Update views.py"
michplunkett 8f6a4e3
Update database.py
michplunkett 0ba2ec8
Add tests
michplunkett 6d0f577
Update db.py
michplunkett 731085b
Update database.py
michplunkett 6ebd1de
Update views.py
michplunkett 9b95c66
Update database.py
michplunkett bc618af
Merge branch 'develop' into build-api
michplunkett 88be2c3
Update database.py
michplunkett 42f8e6f
Merge branch 'develop' into build-api
michplunkett cca977a
Merge branch 'develop' into build-api
michplunkett d2d897c
Merge branch 'develop' into build-api
michplunkett ca3fdd4
Merge branch 'develop' into build-api
michplunkett 9435fc3
Update database.py
michplunkett e1f6c54
Merge branch 'develop' into build-api
michplunkett 1385597
Update database.py
michplunkett 024d8fa
Update v1.py
michplunkett b14f0dd
Update v1.py
michplunkett 5905525
Update v1.py
michplunkett File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
from http import HTTPMethod | ||
from typing import List | ||
|
||
from flask import Blueprint, Response, jsonify | ||
from sqlalchemy.orm import contains_eager, joinedload | ||
|
||
from OpenOversight.app.models.database import ( | ||
Assignment, | ||
BaseModel, | ||
Department, | ||
Description, | ||
Incident, | ||
Link, | ||
Officer, | ||
Salary, | ||
db, | ||
) | ||
from OpenOversight.app.models.database_cache import ( | ||
get_database_cache_entry, | ||
put_database_cache_entry, | ||
) | ||
from OpenOversight.app.utils.constants import ( | ||
KEY_DEPT_ALL_ASSIGNMENTS, | ||
KEY_DEPT_ALL_INCIDENTS, | ||
KEY_DEPT_ALL_LINKS, | ||
KEY_DEPT_ALL_NOTES, | ||
KEY_DEPT_ALL_OFFICERS, | ||
KEY_DEPT_ALL_SALARIES, | ||
) | ||
from OpenOversight.app.utils.flask import limiter | ||
|
||
|
||
v1 = Blueprint("v1", __name__, url_prefix="/api/v1") | ||
|
||
|
||
def objs_to_dicts_jsonify(obj_list: List[BaseModel]) -> Response: | ||
return jsonify([o.to_dict() for o in obj_list]) | ||
|
||
|
||
@v1.route("/departments/<int:department_id>/officers", methods=[HTTPMethod.GET]) | ||
@limiter.limit("5/minute") | ||
def get_dept_officers(department_id: int) -> Response: | ||
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) | ||
|
||
return objs_to_dicts_jsonify(officers) | ||
|
||
|
||
@v1.route("/departments/<int:department_id>/assignments", methods=[HTTPMethod.GET]) | ||
@limiter.limit("5/minute") | ||
def get_dept_assignments(department_id: int) -> Response: | ||
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) | ||
|
||
return objs_to_dicts_jsonify(assignments) | ||
|
||
|
||
@v1.route("/departments/<int:department_id>/incidents", methods=[HTTPMethod.GET]) | ||
@limiter.limit("5/minute") | ||
def get_dept_incidents(department_id: int) -> Response: | ||
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) | ||
|
||
return objs_to_dicts_jsonify(incidents) | ||
|
||
|
||
@v1.route("/departments/<int:department_id>/salaries", methods=[HTTPMethod.GET]) | ||
@limiter.limit("5/minute") | ||
def get_dept_salaries(department_id: int) -> Response: | ||
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) | ||
|
||
return objs_to_dicts_jsonify(salaries) | ||
|
||
|
||
@v1.route("/departments/<int:department_id>/links", methods=[HTTPMethod.GET]) | ||
@limiter.limit("5/minute") | ||
def get_dept_links(department_id: int) -> Response: | ||
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) | ||
|
||
return objs_to_dicts_jsonify(links) | ||
|
||
|
||
@v1.route("/departments/<int:department_id>/descriptions", methods=[HTTPMethod.GET]) | ||
@limiter.limit("5/minute") | ||
def get_dept_descriptions(department_id: int) -> Response: | ||
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) | ||
|
||
return objs_to_dicts_jsonify(notes) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import json | ||
from http import HTTPStatus | ||
|
||
import pytest | ||
from flask import current_app, url_for | ||
from sqlalchemy.orm import contains_eager, joinedload | ||
|
||
from OpenOversight.app.models.database import Assignment, Incident, Officer, Salary | ||
from OpenOversight.app.utils.constants import ENCODING_UTF_8 | ||
|
||
|
||
@pytest.mark.parametrize("department_id", [1, 3000]) | ||
def test_get_dept_attributes(client, session, department_id: int): | ||
with current_app.test_request_context(): | ||
# Get officers | ||
expected_officers = Officer.query.filter_by(department_id=department_id).count() | ||
|
||
resp_officers = client.get( | ||
url_for("v1.get_dept_officers", department_id=department_id), | ||
follow_redirects=True, | ||
) | ||
officers = json.loads(resp_officers.data.decode(ENCODING_UTF_8)) | ||
|
||
assert resp_officers.status_code == HTTPStatus.OK | ||
assert len(officers) == expected_officers | ||
|
||
# Get assignments | ||
expected_assignments = ( | ||
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() | ||
) | ||
|
||
resp_assignments = client.get( | ||
url_for("v1.get_dept_assignments", department_id=department_id), | ||
follow_redirects=True, | ||
) | ||
Comment on lines
+38
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I could probably get rid of |
||
assignments = json.loads(resp_assignments.data.decode(ENCODING_UTF_8)) | ||
|
||
assert resp_assignments.status_code == HTTPStatus.OK | ||
assert len(assignments) == len(expected_assignments) | ||
|
||
# Get incidents | ||
expected_incidents = Incident.query.filter_by( | ||
department_id=department_id | ||
).count() | ||
|
||
resp_incidents = client.get( | ||
url_for("v1.get_dept_incidents", department_id=department_id), | ||
follow_redirects=True, | ||
) | ||
incidents = json.loads(resp_incidents.data.decode(ENCODING_UTF_8)) | ||
|
||
assert resp_incidents.status_code == HTTPStatus.OK | ||
assert len(incidents) == expected_incidents | ||
|
||
# Get salaries | ||
expected_salaries = ( | ||
session.query(Salary) | ||
.join(Salary.officer) | ||
.filter(Officer.department_id == department_id) | ||
.options(contains_eager(Salary.officer)) | ||
.count() | ||
) | ||
|
||
resp_salaries = client.get( | ||
url_for("v1.get_dept_salaries", department_id=department_id), | ||
follow_redirects=True, | ||
) | ||
salaries = json.loads(resp_salaries.data.decode(ENCODING_UTF_8)) | ||
|
||
assert resp_salaries.status_code == HTTPStatus.OK | ||
assert len(salaries) == expected_salaries |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be worth moving these to static functions on each of the objects since they are used both here and in views.