Skip to content

Commit 95c107f

Browse files
Merge upstream commits (#534)
* Add invalid ID tests for `main.views` (lucyparsons#1124) There were a handful of routes where we did not validate the initial ID parameters. Additionally, I standardized how object querying was done within the `main.views` file. - [x] This branch is up-to-date with the `develop` branch. - [x] `pytest` passes on my local development environment. - [x] `pre-commit` passes on my local development environment. * Remove unnecessary `mockdata` parameters (lucyparsons#1126) lucyparsons#1125 Removed the `mockdata` parameter from test functions that don't require it. - [x] This branch is up-to-date with the `develop` branch. - [x] `pytest` passes on my local development environment. - [x] `pre-commit` passes on my local development environment. * Finish audit fields for `users` table (lucyparsons#1129) lucyparsons#1004 Added `disabled_by`, `disabled_at`, `approved_at`, `approved_by`, and `confirmed_at`, and `confirmed_by` fields to the `users` table so that we could keep track of users actions on the platform at a more granular level. I also removed the previously mentioned columns corresponding boolean columns. - [x] This branch is up-to-date with the `develop` branch. - [x] `pytest` passes on my local development environment. - [x] `pre-commit` passes on my local development environment. - [x] The `flask db upgrade` and `flask db downgrade` functions work for both migrations. ```console I have no name!@c38d30934aa7:/usr/src/app$ % (env) michaelp@MacBook-Air-18 OpenOversight % docker exec -it openoversight-web-1 bash I have no name!@5027495d31c0:/usr/src/app$ flask db downgrade ... INFO [alembic.runtime.migration] Running downgrade 99c50fc8d294 -> bf254c0961ca, complete audit field addition to users I have no name!@5027495d31c0:/usr/src/app$ flask db downgrade ... INFO [alembic.runtime.migration] Running downgrade bf254c0961ca -> 5865f488470c, add remaining audit fields for users table I have no name!@5027495d31c0:/usr/src/app$ flask db downgrade ... INFO [alembic.runtime.migration] Running downgrade 5865f488470c -> 939ea0f2b26d, change salary column types I have no name!@5027495d31c0:/usr/src/app$ flask db upgrade ... INFO [alembic.runtime.migration] Running upgrade 939ea0f2b26d -> 5865f488470c, change salary column types INFO [alembic.runtime.migration] Running upgrade 5865f488470c -> bf254c0961ca, add remaining audit fields for users table INFO [alembic.runtime.migration] Running upgrade bf254c0961ca -> 99c50fc8d294, complete audit field addition to users I have no name!@5027495d31c0:/usr/src/app$ ``` * Fix the `/find` department sort (lucyparsons#1133) ## Fixes issue Addresses TO-DO in code. ## Description of Changes The `/find` route was using an unsorted list of departments unlike the rest of the application. Before: <img width="1334" alt="Screenshot 2024-11-06 at 10 13 13 PM" src="https://github.com/user-attachments/assets/8c6b832a-109e-465a-a6ec-c102524eb43b"> After: <img width="1324" alt="Screenshot 2024-11-06 at 10 13 36 PM" src="https://github.com/user-attachments/assets/1f639518-a750-498e-8a26-6f5784ad052b"> ## Tests and Linting - [x] This branch is up-to-date with the `develop` branch. - [x] `pytest` passes on my local development environment. - [x] `pre-commit` passes on my local development environment. * Standardize model `__repr__` functions (lucyparsons#1140) ## Description of Changes Addressed inconsistencies with the model `__repr__` functions. Mostly making this because this PR got a bit out of control: lucyparsons#1138 ## Tests and Linting - [x] This branch is up-to-date with the `develop` branch. - [x] `pytest` passes on my local development environment. - [x] `pre-commit` passes on my local development environment. * Restructure application routing (lucyparsons#1141) Sets the groundwork for this issue: lucyparsons#385 Simplifying the import logic for the `auth` and `main` routes of the application. This also sets up the work for this PR: lucyparsons#1138 - [x] This branch is up-to-date with the `develop` branch. - [x] `pytest` passes on my local development environment. - [x] `pre-commit` passes on my local development environment. * Add generic `__repr__` and `to_dict` functions (lucyparsons#1143) Adds a generic `__repr__` function and and a generic `to_dict` function so that objects can be serialized in the future. The reason I didn't use the SQLAlchemy class like we talked about in the previous PR was because that would require a complete rework of most object instantiations and how properties are ordered. For a greater view into that, you can look at this PR: lucyparsons#1142 - [x] This branch is up-to-date with the `develop` branch. - [x] `pytest` passes on my local development environment. - [x] `pre-commit` passes on my local development environment. * Add static `Department` functions (lucyparsons#1150) Moved `Department`-related methods to the `Department` object. - [x] This branch is up-to-date with the `develop` branch. - [x] Ran `make create_db_diagram` command. - [x] `pytest` passes on my local development environment. - [x] `pre-commit` passes on my local development environment. * Remove audit fields from `Description` download (lucyparsons#1151) ## Description of Changes Removed audit fields from the `Description` download function. They leak user information and provide data that isn't particularly useful outside of internal auditing. ## Tests and Linting - [x] This branch is up-to-date with the `develop` branch. - [x] Ran `make create_db_diagram` command. - [x] `pytest` passes on my local development environment. - [x] `pre-commit` passes on my local development environment. * UX Re-design (lucyparsons#1155) * Fix submit image page and python sqlite bug (lucyparsons#1157) ## Description of Changes _Changes carried over from https://github.com/OrcaCollective/OpenOversight/pull/533_ Fix bug where submit image page department selector was not submitting images to the correct department. It looks like the current implementation was using `selectedIndex` (the index of the selected `<option>` element in the `<select>`) to determine the initial department id, which assumes the departments are loaded in order: https://github.com/OrcaCollective/OpenOversight/blob/2c5ad9a74687943eac5e71a874c0f4ceb4784fa0/OpenOversight/app/templates/submit_image.html#L79-L82 ## Notes for Deployment None! ## Screenshots (if appropriate) N/A ## Testing instructions 1. Log in as admin user. 2. Create a new department "Peoria Police Department (PPD)" at http://localhost:3000/departments/new. 3. Update the user's preferred department to PPD at http://localhost:3000/auth/change-dept/. 4. Visit the Submit Image page (http://localhost:3000/submit) and confirm that the populated department is BPD. 5. Upload an image. 6. Verify in devtools that an image was submitted to http://localhost:3000/upload/departments/4. 7. Update the department selector to Springfield Police Department. 8. Upload an image. 9. Verify in devtools that an image was submitted to http://localhost:3000/upload/departments/1. 10. Change the preferred department to Springfield Police Department at http://localhost:3000/auth/change-dept/. 11. Visit the Submit Image page (http://localhost:3000/submit) and confirm that the populated department is now SPD. 12. Upload an image. 13. Verify in devtools that an image was submitted to http://localhost:3000/upload/departments/1. ## Checks - [x] I have rebased my changes on `main` - [x] `just lint` passes - [x] `just test` passes * Default to "N/A" if dept state is invalid (lucyparsons#1159) ## Description of Changes Default to "N/A" if department state is invalid In [the migration](https://github.com/lucyparsons/OpenOversight/blob/develop/OpenOversight/migrations/versions/2023-07-26-1551_18f43ac4622f_add_state_column_to_departments.py) where we introduced the department state column, we defaulted to empty string for existing departments. We think this may have broken the browse page when trying to load the browse page. ## Notes for Deployment None! ## Screenshots (if appropriate) N/A ## Tests and Linting Reproduced the internal server error by setting the department state to empty string ``` openoversight-dev=# update departments set state='' where id = 1; ``` Checked that the error was no longer displayed once this change was implemented - [x] This branch is up-to-date with the `develop` branch. - [ ] Ran `make create_db_diagram` command. - [x] `pytest` passes on my local development environment. - [x] `pre-commit` passes on my local development environment. * Fix department selector on submit image page * Re-add dropzone * Update contact email --------- Co-authored-by: Michael Plunkett <[email protected]>
1 parent 2c5ad9a commit 95c107f

File tree

125 files changed

+3609
-6946
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

125 files changed

+3609
-6946
lines changed

OpenOversight/app/__init__.py

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,19 @@
66
from flask import Flask, jsonify, render_template, request
77
from flask_bootstrap import Bootstrap5
88
from flask_compress import Compress
9-
from flask_limiter import Limiter
10-
from flask_limiter.util import get_remote_address
119
from flask_login import LoginManager
1210
from flask_migrate import Migrate
13-
from flask_sitemap import Sitemap
1411
from flask_wtf.csrf import CSRFProtect
1512

13+
from OpenOversight.app.auth.views import auth as auth_blueprint
1614
from OpenOversight.app.email_client import EmailClient
1715
from OpenOversight.app.filters import instantiate_filters
16+
from OpenOversight.app.main.views import main as main_blueprint
1817
from OpenOversight.app.models.config import config
1918
from OpenOversight.app.models.database import db
2019
from OpenOversight.app.models.users import AnonymousUser
2120
from OpenOversight.app.utils.constants import MEGABYTE
21+
from OpenOversight.app.utils.flask import limiter, sitemap
2222

2323

2424
bootstrap = Bootstrap5()
@@ -29,11 +29,6 @@
2929
login_manager.anonymous_user = AnonymousUser
3030
login_manager.login_view = "auth.login"
3131

32-
limiter = Limiter(
33-
key_func=get_remote_address, default_limits=["100 per minute", "5 per second"]
34-
)
35-
36-
sitemap = Sitemap()
3732
csrf = CSRFProtect()
3833

3934

@@ -52,15 +47,11 @@ def create_app(config_name="default"):
5247
sitemap.init_app(app)
5348
compress.init_app(app)
5449

55-
from OpenOversight.app.main import main as main_blueprint
56-
50+
# Register Blueprints for application routes
51+
app.register_blueprint(auth_blueprint)
5752
app.register_blueprint(main_blueprint)
5853

59-
from OpenOversight.app.auth import auth as auth_blueprint
60-
61-
app.register_blueprint(auth_blueprint, url_prefix="/auth")
62-
63-
max_log_size = 10 * MEGABYTE # start new log file after 10 MB
54+
max_log_size = 10 * MEGABYTE # Start new log file after 10 MB
6455
num_logs_to_keep = 5
6556
file_handler = RotatingFileHandler(
6657
"/tmp/openoversight.log", "a", max_log_size, num_logs_to_keep
@@ -77,7 +68,7 @@ def create_app(config_name="default"):
7768
app.logger.addHandler(file_handler)
7869
app.logger.info("OpenOversight startup")
7970

80-
# Also log when endpoints are getting hit hard
71+
# Log when endpoints are getting hit hard
8172
limiter.logger.addHandler(file_handler)
8273

8374
# Define error handlers

OpenOversight/app/auth/__init__.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +0,0 @@
1-
from flask import Blueprint
2-
3-
4-
auth = Blueprint("auth", __name__)
5-
6-
from . import views # noqa: F401, E402

OpenOversight/app/auth/views.py

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
from datetime import datetime, timezone
12
from http import HTTPMethod, HTTPStatus
23

34
from flask import (
5+
Blueprint,
46
current_app,
57
flash,
68
redirect,
@@ -11,8 +13,6 @@
1113
)
1214
from flask_login import current_user, login_required, login_user, logout_user
1315

14-
from OpenOversight.app import sitemap
15-
from OpenOversight.app.auth import auth
1616
from OpenOversight.app.auth.forms import (
1717
ChangeDefaultDepartmentForm,
1818
ChangeEmailForm,
@@ -34,10 +34,13 @@
3434
ResetPasswordEmail,
3535
)
3636
from OpenOversight.app.utils.auth import admin_required
37+
from OpenOversight.app.utils.constants import KEY_APPROVE_REGISTRATIONS
38+
from OpenOversight.app.utils.flask import sitemap
3739
from OpenOversight.app.utils.forms import set_dynamic_default
3840
from OpenOversight.app.utils.general import validate_redirect_url
3941

4042

43+
auth = Blueprint("auth", __name__, url_prefix="/auth")
4144
js_loads = ["js/zxcvbn.js", "js/password.js"]
4245
sitemap_endpoints = []
4346

@@ -57,7 +60,8 @@ def static_routes():
5760
def before_request():
5861
if (
5962
current_user.is_authenticated
60-
and not current_user.confirmed
63+
and not current_user.confirmed_at
64+
and not current_user.confirmed_by
6165
and request.endpoint
6266
and request.endpoint[:5] != "auth."
6367
and request.endpoint not in ["static", "bootstrap.static"]
@@ -67,9 +71,15 @@ def before_request():
6771

6872
@auth.route("/unconfirmed")
6973
def unconfirmed():
70-
if current_user.is_anonymous or current_user.confirmed:
74+
if current_user.is_anonymous or (
75+
current_user.confirmed_at and current_user.confirmed_by
76+
):
7177
return redirect(url_for("main.index"))
72-
if current_app.config["APPROVE_REGISTRATIONS"] and not current_user.approved:
78+
if (
79+
current_app.config[KEY_APPROVE_REGISTRATIONS]
80+
and not current_user.approved_at
81+
and not current_user.approved_by
82+
):
7383
return render_template("auth/unapproved.html")
7484
else:
7585
return render_template("auth/unconfirmed.html")
@@ -112,11 +122,16 @@ def register():
112122
email=form.email.data,
113123
username=form.username.data,
114124
password=form.password.data,
115-
approved=False if current_app.config["APPROVE_REGISTRATIONS"] else True,
125+
approved_at=None
126+
if current_app.config[KEY_APPROVE_REGISTRATIONS]
127+
else datetime.now(timezone.utc),
128+
approved_by=None
129+
if current_app.config[KEY_APPROVE_REGISTRATIONS]
130+
else User.query.filter_by(is_administrator=True).first().id,
116131
)
117132
db.session.add(user)
118133
db.session.commit()
119-
if current_app.config["APPROVE_REGISTRATIONS"]:
134+
if current_app.config[KEY_APPROVE_REGISTRATIONS]:
120135
admins = User.query.filter_by(is_administrator=True).all()
121136
for admin in admins:
122137
EmailClient.send_email(
@@ -141,9 +156,11 @@ def register():
141156
@auth.route("/confirm/<token>", methods=[HTTPMethod.GET])
142157
@login_required
143158
def confirm(token):
144-
if current_user.confirmed:
159+
if current_user.confirmed_at and current_user.confirmed_by:
145160
return redirect(url_for("main.index"))
146-
if current_user.confirm(token):
161+
if current_user.confirm(
162+
token, User.query.filter_by(is_administrator=True).first().id
163+
):
147164
admins = User.query.filter_by(is_administrator=True).all()
148165
for admin in admins:
149166
EmailClient.send_email(
@@ -313,18 +330,31 @@ def edit_user(user_id):
313330
flash("You cannot edit your own account!")
314331
form = EditUserForm(obj=user)
315332
return render_template("auth/user.html", user=user, form=form)
316-
already_approved = user.approved
333+
already_approved = (
334+
user.approved_at is not None and user.approved_by is not None
335+
)
336+
if form.approved.data:
337+
user.approve_user(current_user.id)
338+
339+
if form.confirmed.data:
340+
user.confirm_user(current_user.id)
341+
342+
if form.is_disabled.data:
343+
user.disable_user(current_user.id)
344+
317345
form.populate_obj(user)
318346
db.session.add(user)
319347
db.session.commit()
320348

321349
# automatically send a confirmation email when approving an
322350
# unconfirmed user
323351
if (
324-
current_app.config["APPROVE_REGISTRATIONS"]
352+
current_app.config[KEY_APPROVE_REGISTRATIONS]
325353
and not already_approved
326-
and user.approved
327-
and not user.confirmed
354+
and user.approved_at
355+
and user.approved_by
356+
and not user.confirmed_at
357+
and not user.confirmed_by
328358
):
329359
admin_resend_confirmation(user)
330360

@@ -353,7 +383,7 @@ def delete_user(user_id):
353383

354384

355385
def admin_resend_confirmation(user):
356-
if user.confirmed:
386+
if user.confirmed_at and current_user.confirmed_by:
357387
flash(f"User {user.username} is already confirmed.")
358388
else:
359389
token = user.generate_confirmation_token()

OpenOversight/app/commands.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,13 @@ def make_admin_user(
9393
username=username,
9494
email=email,
9595
password=password,
96-
confirmed=True,
9796
is_administrator=True,
9897
)
9998
db.session.add(u)
99+
db.session.flush()
100+
101+
u.confirmed_at = datetime.now()
102+
u.confirmed_by = u.id
100103
db.session.commit()
101104
print(f"Administrator {username} successfully added")
102105
current_app.logger.info(f"Administrator {username} added with email {email}")

OpenOversight/app/filters.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from datetime import datetime
44
from zoneinfo import ZoneInfo
55

6+
import us.states
67
from flask import Flask, current_app, session
78

89
from OpenOversight.app.utils.constants import (
@@ -87,6 +88,16 @@ def display_currency(value: float) -> str:
8788
return f"${value:,.2f}"
8889

8990

91+
def get_state_full_name(abbrev: str) -> str:
92+
if abbrev == "FA":
93+
return "Federal"
94+
95+
state = us.states.lookup(abbrev)
96+
if state is None:
97+
return "N/A"
98+
return us.states.lookup(abbrev).name
99+
100+
90101
def instantiate_filters(app: Flask):
91102
"""Instantiate all template filters"""
92103
app.template_filter("capfirst")(capfirst_filter)
@@ -99,3 +110,4 @@ def instantiate_filters(app: Flask):
99110
app.template_filter("local_time")(local_time)
100111
app.template_filter("thousands_separator")(thousands_separator)
101112
app.template_filter("display_currency")(display_currency)
113+
app.template_filter("get_state_full_name")(get_state_full_name)

OpenOversight/app/main/__init__.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +0,0 @@
1-
from flask import Blueprint
2-
3-
4-
main = Blueprint("main", __name__)
5-
6-
from OpenOversight.app.main import views # noqa: E402,F401

OpenOversight/app/main/downloads.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from typing import Any, Callable, Dict, List, TypeVar
66

77
from flask import Response, abort
8-
from sqlalchemy.orm import Query
98

109
from OpenOversight.app.models.database import (
1110
Assignment,
@@ -40,7 +39,7 @@ def check_output(output_str):
4039

4140

4241
def make_downloadable_csv(
43-
query: Query,
42+
query: List,
4443
department_id: int,
4544
csv_suffix: str,
4645
field_names: List[str],
@@ -161,8 +160,5 @@ def descriptions_record_maker(description: Description) -> _Record:
161160
return {
162161
"id": description.id,
163162
"text_contents": description.text_contents,
164-
"created_by": description.created_by,
165163
"officer_id": description.officer_id,
166-
"created_at": description.created_at,
167-
"last_updated_at": description.last_updated_at,
168164
}

OpenOversight/app/main/forms.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
STATE_CHOICES,
4141
SUFFIX_CHOICES,
4242
)
43-
from OpenOversight.app.utils.db import dept_choices, unit_choices, unsorted_dept_choices
43+
from OpenOversight.app.utils.db import dept_choices, unit_choices
4444
from OpenOversight.app.widgets import BootstrapListWidget, FormFieldWidget
4545

4646

@@ -102,8 +102,8 @@ class FindOfficerForm(Form):
102102
dept = QuerySelectField(
103103
"dept",
104104
validators=[DataRequired()],
105-
query_factory=unsorted_dept_choices,
106-
get_label="display_name",
105+
query_factory=dept_choices,
106+
get_label="name",
107107
)
108108
unit = StringField("unit", default="Not Sure", validators=[Optional()])
109109
current_job = BooleanField("current_job", default=None, validators=[Optional()])
@@ -122,12 +122,8 @@ class FindOfficerForm(Form):
122122
choices=GENDER_CHOICES,
123123
validators=[AnyOf(allowed_values(GENDER_CHOICES))],
124124
)
125-
min_age = IntegerField(
126-
"min_age", default=16, validators=[NumberRange(min=16, max=100)]
127-
)
128-
max_age = IntegerField(
129-
"max_age", default=85, validators=[NumberRange(min=16, max=100)]
130-
)
125+
min_age = IntegerField("min_age", validators=[NumberRange(min=16, max=100)])
126+
max_age = IntegerField("max_age", validators=[NumberRange(min=16, max=100)])
131127
require_photo = BooleanField(
132128
"require_photo", default=False, validators=[Optional()]
133129
)
@@ -287,7 +283,7 @@ class AddOfficerForm(Form):
287283
"Department",
288284
validators=[DataRequired()],
289285
query_factory=dept_choices,
290-
get_label="display_name",
286+
get_label="name",
291287
)
292288
first_name = StringField(
293289
"First name",
@@ -409,7 +405,7 @@ class EditOfficerForm(Form):
409405
"Department",
410406
validators=[Optional()],
411407
query_factory=dept_choices,
412-
get_label="display_name",
408+
get_label="name",
413409
)
414410
submit = SubmitField(label="Update")
415411

@@ -424,7 +420,7 @@ class AddUnitForm(Form):
424420
"Department",
425421
validators=[DataRequired()],
426422
query_factory=dept_choices,
427-
get_label="display_name",
423+
get_label="name",
428424
)
429425
submit = SubmitField(label="Add")
430426

@@ -434,7 +430,7 @@ class AddImageForm(Form):
434430
"Department",
435431
validators=[DataRequired()],
436432
query_factory=dept_choices,
437-
get_label="display_name",
433+
get_label="name",
438434
)
439435

440436

@@ -535,7 +531,7 @@ class IncidentForm(DateFieldForm):
535531
"Department*",
536532
validators=[DataRequired()],
537533
query_factory=dept_choices,
538-
get_label="display_name",
534+
get_label="name",
539535
)
540536
address = FormField(LocationForm)
541537
officers = FieldList(

0 commit comments

Comments
 (0)