Skip to content

Commit cacdcec

Browse files
Make url_for work with all endpoints (#1070)
## Description of Changes Some of the `ModelView` based endpoints were missing a distinct name that could be used for `url_for`, and instead string concatenation was used (mostly in templates). I also moved some logic out of templates (e.g. calculation of total pay), added helper methods and made some other changed to reduce the code needed in the templates and improve readability a little bit. I also added an anonymous user class that is useful to call things like `.is_admin_or_coordinator(department)` on any `current_user` object, without first making sure the user is not anonymous. ## 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.
1 parent aa8db33 commit cacdcec

36 files changed

+233
-222
lines changed

OpenOversight/app/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from OpenOversight.app.filters import instantiate_filters
1818
from OpenOversight.app.models.config import config
1919
from OpenOversight.app.models.database import db
20+
from OpenOversight.app.models.users import AnonymousUser
2021
from OpenOversight.app.utils.constants import MEGABYTE
2122

2223

@@ -25,6 +26,7 @@
2526

2627
login_manager = LoginManager()
2728
login_manager.session_protection = "strong"
29+
login_manager.anonymous_user = AnonymousUser
2830
login_manager.login_view = "auth.login"
2931

3032
limiter = Limiter(

OpenOversight/app/filters.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ def thousands_separator(value: int) -> str:
9292
return f"{value:,}"
9393

9494

95+
def display_currency(value: float) -> str:
96+
return f"${value:,.2f}"
97+
98+
9599
def instantiate_filters(app: Flask):
96100
"""Instantiate all template filters"""
97101
app.template_filter("capfirst")(capfirst_filter)
@@ -104,3 +108,4 @@ def instantiate_filters(app: Flask):
104108
app.template_filter("display_time")(display_time)
105109
app.template_filter("local_time")(local_time)
106110
app.template_filter("thousands_separator")(thousands_separator)
111+
app.template_filter("display_currency")(display_currency)

OpenOversight/app/main/views.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -332,12 +332,10 @@ def officer_profile(officer_id: int):
332332
.all()
333333
)
334334
assignments = Assignment.query.filter_by(officer_id=officer_id).all()
335-
face_paths = []
336-
for face in faces:
337-
face_paths.append(serve_image(face.image.filepath))
335+
face_paths = [(face, serve_image(face.image.filepath)) for face in faces]
338336
if not face_paths:
339337
# Add in the placeholder image if no faces are found
340-
face_paths = [url_for("static", filename="images/placeholder.png")]
338+
face_paths = [(None, url_for("static", filename="images/placeholder.png"))]
341339
except: # noqa: E722
342340
exception_type, value, full_traceback = sys.exc_info()
343341
error_str = " ".join([str(exception_type), str(value), format_exc()])
@@ -355,8 +353,7 @@ def officer_profile(officer_id: int):
355353
return render_template(
356354
"officer.html",
357355
officer=officer,
358-
paths=face_paths,
359-
faces=faces,
356+
face_paths=face_paths,
360357
assignments=assignments,
361358
form=form,
362359
)
@@ -2085,24 +2082,31 @@ def populate_obj(self, form: FlaskForm, obj: Incident):
20852082
main.add_url_rule(
20862083
"/incidents/",
20872084
defaults={"obj_id": None},
2085+
endpoint="incident_api",
20882086
view_func=incident_view,
20892087
methods=[HTTPMethod.GET],
20902088
)
20912089
main.add_url_rule(
20922090
"/incidents/new",
2091+
endpoint="incident_api_new",
20932092
view_func=incident_view,
20942093
methods=[HTTPMethod.GET, HTTPMethod.POST],
20952094
)
20962095
main.add_url_rule(
2097-
"/incidents/<int:obj_id>", view_func=incident_view, methods=[HTTPMethod.GET]
2096+
"/incidents/<int:obj_id>",
2097+
endpoint="incident_api",
2098+
view_func=incident_view,
2099+
methods=[HTTPMethod.GET],
20982100
)
20992101
main.add_url_rule(
21002102
"/incidents/<int:obj_id>/edit",
2103+
endpoint="incident_api_edit",
21012104
view_func=incident_view,
21022105
methods=[HTTPMethod.GET, HTTPMethod.POST],
21032106
)
21042107
main.add_url_rule(
21052108
"/incidents/<int:obj_id>/delete",
2109+
endpoint="incident_api_delete",
21062110
view_func=incident_view,
21072111
methods=[HTTPMethod.GET, HTTPMethod.POST],
21082112
)
@@ -2189,7 +2193,7 @@ def redirect_get_notes(officer_id: int, obj_id=None):
21892193
def redirect_edit_note(officer_id: int, obj_id=None):
21902194
flash(FLASH_MSG_PERMANENT_REDIRECT)
21912195
return redirect(
2192-
f"{url_for('main.note_api', officer_id=officer_id, obj_id=obj_id)}/edit",
2196+
url_for("main.note_api_edit", officer_id=officer_id, obj_id=obj_id),
21932197
code=HTTPStatus.PERMANENT_REDIRECT,
21942198
)
21952199

@@ -2199,14 +2203,15 @@ def redirect_edit_note(officer_id: int, obj_id=None):
21992203
def redirect_delete_note(officer_id: int, obj_id=None):
22002204
flash(FLASH_MSG_PERMANENT_REDIRECT)
22012205
return redirect(
2202-
f"{url_for('main.note_api', officer_id=officer_id, obj_id=obj_id)}/delete",
2206+
url_for("main.note_api_delete", officer_id=officer_id, obj_id=obj_id),
22032207
code=HTTPStatus.PERMANENT_REDIRECT,
22042208
)
22052209

22062210

22072211
note_view = NoteApi.as_view("note_api")
22082212
main.add_url_rule(
22092213
"/officers/<int:officer_id>/notes/new",
2214+
endpoint="note_api",
22102215
view_func=note_view,
22112216
methods=[HTTPMethod.GET, HTTPMethod.POST],
22122217
)
@@ -2217,6 +2222,7 @@ def redirect_delete_note(officer_id: int, obj_id=None):
22172222
)
22182223
main.add_url_rule(
22192224
"/officers/<int:officer_id>/notes/<int:obj_id>",
2225+
endpoint="note_api",
22202226
view_func=note_view,
22212227
methods=[HTTPMethod.GET],
22222228
)
@@ -2227,6 +2233,7 @@ def redirect_delete_note(officer_id: int, obj_id=None):
22272233
)
22282234
main.add_url_rule(
22292235
"/officers/<int:officer_id>/notes/<int:obj_id>/edit",
2236+
endpoint="note_api_edit",
22302237
view_func=note_view,
22312238
methods=[HTTPMethod.GET, HTTPMethod.POST],
22322239
)
@@ -2237,6 +2244,7 @@ def redirect_delete_note(officer_id: int, obj_id=None):
22372244
)
22382245
main.add_url_rule(
22392246
"/officers/<int:officer_id>/notes/<int:obj_id>/delete",
2247+
endpoint="note_api_delete",
22402248
view_func=note_view,
22412249
methods=[HTTPMethod.GET, HTTPMethod.POST],
22422250
)
@@ -2252,7 +2260,7 @@ def redirect_delete_note(officer_id: int, obj_id=None):
22522260
def redirect_new_description(officer_id: int):
22532261
flash(FLASH_MSG_PERMANENT_REDIRECT)
22542262
return redirect(
2255-
url_for("main.description_api", officer_id=officer_id),
2263+
url_for("main.description_api_new", officer_id=officer_id),
22562264
code=HTTPStatus.PERMANENT_REDIRECT,
22572265
)
22582266

@@ -2270,7 +2278,7 @@ def redirect_get_description(officer_id: int, obj_id=None):
22702278
def redirect_edit_description(officer_id: int, obj_id=None):
22712279
flash(FLASH_MSG_PERMANENT_REDIRECT)
22722280
return redirect(
2273-
f"{url_for('main.description_api', officer_id=officer_id, obj_id=obj_id)}/edit",
2281+
url_for("main.description_api_edit", officer_id=officer_id, obj_id=obj_id),
22742282
code=HTTPStatus.PERMANENT_REDIRECT,
22752283
)
22762284

@@ -2280,14 +2288,15 @@ def redirect_edit_description(officer_id: int, obj_id=None):
22802288
def redirect_delete_description(officer_id: int, obj_id=None):
22812289
flash(FLASH_MSG_PERMANENT_REDIRECT)
22822290
return redirect(
2283-
f"{url_for('main.description_api', officer_id=officer_id, obj_id=obj_id)}/delete",
2291+
url_for("main.description_api_delete", officer_id=officer_id, obj_id=obj_id),
22842292
code=HTTPStatus.PERMANENT_REDIRECT,
22852293
)
22862294

22872295

22882296
description_view = DescriptionApi.as_view("description_api")
22892297
main.add_url_rule(
22902298
"/officers/<int:officer_id>/descriptions/new",
2299+
endpoint="description_api_new",
22912300
view_func=description_view,
22922301
methods=[HTTPMethod.GET, HTTPMethod.POST],
22932302
)
@@ -2298,6 +2307,7 @@ def redirect_delete_description(officer_id: int, obj_id=None):
22982307
)
22992308
main.add_url_rule(
23002309
"/officers/<int:officer_id>/descriptions/<int:obj_id>",
2310+
endpoint="description_api",
23012311
view_func=description_view,
23022312
methods=[HTTPMethod.GET],
23032313
)
@@ -2308,6 +2318,7 @@ def redirect_delete_description(officer_id: int, obj_id=None):
23082318
)
23092319
main.add_url_rule(
23102320
"/officers/<int:officer_id>/descriptions/<int:obj_id>/edit",
2321+
endpoint="description_api_edit",
23112322
view_func=description_view,
23122323
methods=[HTTPMethod.GET, HTTPMethod.POST],
23132324
)
@@ -2318,6 +2329,7 @@ def redirect_delete_description(officer_id: int, obj_id=None):
23182329
)
23192330
main.add_url_rule(
23202331
"/officers/<int:officer_id>/descriptions/<int:obj_id>/delete",
2332+
endpoint="description_api_delete",
23212333
view_func=description_view,
23222334
methods=[HTTPMethod.GET, HTTPMethod.POST],
23232335
)

OpenOversight/app/models/database.py

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import operator
12
import re
23
import time
34
import uuid
45
from datetime import date, datetime
5-
from typing import List
6+
from typing import List, Optional
67

78
from authlib.jose import JoseError, JsonWebToken
89
from cachetools import cached
@@ -294,21 +295,27 @@ def gender_label(self):
294295
def job_title(self):
295296
if self.assignments:
296297
return max(
297-
self.assignments, key=lambda x: x.start_date or date.min
298+
self.assignments, key=operator.attrgetter("start_date_or_min")
298299
).job.job_title
299300

300301
def unit_description(self):
301302
if self.assignments:
302-
unit = max(self.assignments, key=lambda x: x.start_date or date.min).unit
303+
unit = max(
304+
self.assignments, key=operator.attrgetter("start_date_or_min")
305+
).unit
303306
return unit.description if unit else None
304307

305308
def badge_number(self):
306309
if self.assignments:
307-
return max(self.assignments, key=lambda x: x.start_date or date.min).star_no
310+
return max(
311+
self.assignments, key=operator.attrgetter("start_date_or_min")
312+
).star_no
308313

309314
def currently_on_force(self):
310315
if self.assignments:
311-
most_recent = max(self.assignments, key=lambda x: x.start_date or date.min)
316+
most_recent = max(
317+
self.assignments, key=operator.attrgetter("start_date_or_min")
318+
)
312319
return "Yes" if most_recent.resign_date is None else "No"
313320
return "Uncertain"
314321

@@ -340,6 +347,16 @@ class Salary(BaseModel, TrackUpdates):
340347
def __repr__(self):
341348
return f"<Salary: ID {self.officer_id} : {self.salary}"
342349

350+
@property
351+
def total_pay(self) -> float:
352+
return self.salary + self.overtime_pay
353+
354+
@property
355+
def year_repr(self) -> str:
356+
if self.is_fiscal_year:
357+
return f"FY{self.year}"
358+
return str(self.year)
359+
343360

344361
class Assignment(BaseModel, TrackUpdates):
345362
__tablename__ = "assignments"
@@ -371,6 +388,14 @@ class Assignment(BaseModel, TrackUpdates):
371388
def __repr__(self):
372389
return f"<Assignment: ID {self.officer_id} : {self.star_no}>"
373390

391+
@property
392+
def start_date_or_min(self):
393+
return self.start_date or date.min
394+
395+
@property
396+
def start_date_or_max(self):
397+
return self.start_date or date.max
398+
374399

375400
class Unit(BaseModel, TrackUpdates):
376401
__tablename__ = "unit_types"
@@ -702,6 +727,12 @@ class User(UserMixin, BaseModel):
702727
unique=False,
703728
)
704729

730+
def is_admin_or_coordinator(self, department: Optional[Department]) -> bool:
731+
return self.is_administrator or (
732+
department is not None
733+
and (self.is_area_coordinator and self.ac_department_id == department.id)
734+
)
735+
705736
def _jwt_encode(self, payload, expiration):
706737
secret = current_app.config["SECRET_KEY"]
707738
header = {"alg": SIGNATURE_ALGORITHM}

OpenOversight/app/models/users.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from flask_login import AnonymousUserMixin
2+
3+
from OpenOversight.app.models.database import Department
4+
5+
6+
class AnonymousUser(AnonymousUserMixin):
7+
def is_admin_or_coordinator(self, department: Department) -> bool:
8+
return False

OpenOversight/app/templates/cop_face.html

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
{% block content %}
1010
<div class="container theme-showcase" role="main">
1111
{% if current_user and current_user.is_authenticated %}
12-
{% if image and current_user.is_disabled == False %}
12+
{% if image and not current_user.is_disabled %}
1313
<div class="row">
1414
<div class="text-center">
1515
<h1>
@@ -138,18 +138,12 @@ <h2>
138138
</div>
139139
</div>
140140
<div class="col-sm-2 text-center skip-button">
141-
{% if department %}
142-
<a href="{{ url_for('main.label_data', department_id=department.id) }}"
143-
class="btn btn-lg btn-primary"
144-
role="button"><span class="glyphicon glyphicon-repeat" aria-hidden="true"></span> Next Photo</a>
145-
{% else %}
146-
<a href="{{ url_for("main.label_data") }}"
147-
class="btn btn-lg btn-primary"
148-
role="button"><span class="glyphicon glyphicon-repeat" aria-hidden="true"></span> Next Photo</a>
149-
{% endif %}
141+
<a href="{{ url_for('main.label_data', department_id=department.id if department else 0) }}"
142+
class="btn btn-lg btn-primary"
143+
role="button"><span class="glyphicon glyphicon-repeat" aria-hidden="true"></span> Next Photo</a>
150144
</div>
151145
<div class="col-sm-2 text-center done-button">
152-
<a href="{{ url_for('main.complete_tagging', image_id=image.id, department_id=department.id, contains_cops=0) }}"
146+
<a href="{{ url_for('main.complete_tagging', image_id=image.id, department_id=department.id if department else 0, contains_cops=0) }}"
153147
class="btn btn-sm btn-success">
154148
<span class="glyphicon glyphicon glyphicon-ok" aria-hidden="true"></span>
155149
All officers have been identified!
@@ -172,7 +166,7 @@ <h2>
172166
</div>
173167
</div>
174168
</div>
175-
{% elif current_user.is_disabled == True %}
169+
{% elif current_user.is_disabled %}
176170
<h3>Your account has been disabled due to too many incorrect classifications/tags!</h3>
177171
<p>
178172
<a href="mailto:info@lucyparsonslabs.com"

OpenOversight/app/templates/department_add_edit.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ <h5>Enter ranks in hierarchical order, from lowest to highest rank:</h5>
3535
</div>
3636
<div class="text-danger">{{ wtf.form_errors(form.jobs, hiddens="only") }}</div>
3737
{% if form.jobs|length > 1 %}
38-
{% for subfield in (form.jobs|rejectattr('data.is_sworn_officer','eq',False)|sort(attribute='data.order')|list) %}
38+
{% for subfield in (form.jobs|sort(attribute='data.order')|list) %}
3939
<fieldset>
4040
<div class="input-group {% if subfield.errors %} has-error{% endif -%} {%- if subfield.flags.required %} required{% endif -%}">
4141
<div class="input-group-addon">

OpenOversight/app/templates/description_delete.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ <h1>Delete Description of officer {{ obj.officer_id }}</h1>
88
<p class="lead">
99
Are you sure you want to delete this description?
1010
This cannot be undone.
11-
<form action="{{ '{}/delete'.format(url_for('main.description_api', obj_id=obj.id, officer_id=obj.officer_id) ) }}"
11+
<form action="{{ url_for('main.description_api_delete', obj_id=obj.id, officer_id=obj.officer_id) }}"
1212
method="post">
1313
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
1414
<button class="btn btn-danger" type="submit">Delete</button>

OpenOversight/app/templates/description_edit.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
{% if form.errors %}
88
{% set post_url = url_for('main.description_api', officer_id=obj.officer_id, obj_id=obj.id) %}
99
{% else %}
10-
{% set post_url = "{}/edit".format(url_for('main.description_api', officer_id=obj.officer_id, obj_id=obj.id)) %}
10+
{% set post_url = url_for('main.description_api_edit', officer_id=obj.officer_id, obj_id=obj.id) %}
1111
{% endif %}
1212
{{ wtf.quick_form(form, action=post_url, method='post', button_map={'submit':'primary'}) }}
1313
<br>

OpenOversight/app/templates/image.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,7 @@ <h3>Classification</h3>
8383
</tr>
8484
</tbody>
8585
</table>
86-
{% if current_user.is_administrator
87-
or (current_user.is_area_coordinator and current_user.ac_department_id == image.department.id) %}
86+
{% if current_user.is_admin_or_coordinator(image.department) %}
8887
<h3>
8988
Classify <small>Admin only</small>
9089
</h3>

0 commit comments

Comments
 (0)