Skip to content

Commit

Permalink
Add content warnings for videos/links (#457)
Browse files Browse the repository at this point in the history
  • Loading branch information
sea-kelp authored Jun 22, 2024
1 parent d796a55 commit 3c90393
Show file tree
Hide file tree
Showing 13 changed files with 216 additions and 10 deletions.
3 changes: 3 additions & 0 deletions OpenOversight/app/main/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,9 @@ class LinkForm(Form):
default="",
validators=[AnyOf(allowed_values(LINK_CHOICES))],
)
has_content_warning = BooleanField(
"Include content warning?", default=True, validators=[Optional()]
)

def validate(self, extra_validators=None):
success = super(LinkForm, self).validate(extra_validators=extra_validators)
Expand Down
1 change: 1 addition & 0 deletions OpenOversight/app/main/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2381,6 +2381,7 @@ def new(self, form: FlaskForm = None):
link_type=form.link_type.data,
description=form.description.data,
author=form.author.data,
has_content_warning=form.has_content_warning.data,
created_by=current_user.id,
last_updated_by=current_user.id,
)
Expand Down
1 change: 1 addition & 0 deletions OpenOversight/app/models/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,7 @@ class Link(BaseModel, TrackUpdates):
link_type = db.Column(db.String(100), index=True)
description = db.Column(db.Text(), nullable=True)
author = db.Column(db.String(255), nullable=True)
has_content_warning = db.Column(db.Boolean, nullable=False, default=False)

@validates("url")
def validate_url(self, key, url):
Expand Down
12 changes: 12 additions & 0 deletions OpenOversight/app/static/css/openoversight.css
Original file line number Diff line number Diff line change
Expand Up @@ -684,3 +684,15 @@ tr:hover .row-actions {
.bottom-margin {
margin-bottom: 2rem;
}

.video-container .overlay {
align-items: center;
background: black;
color: white;
display: flex;
height: 100%;
justify-content: center;
position: absolute;
top: 0;
width: 100%;
}
24 changes: 24 additions & 0 deletions OpenOversight/app/static/js/contentWarning.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
function createOverlay(container) {
const warningText = $(
"<span><h3>Content Warning</h3><p>This video may be disturbing for some viewers</p></span>"
)
const hide = $('<button type="button" class="btn btn-lg">Show video</button>')
hide.click(() => overlay.css("display", "none"))

const wrapper = $("<div>")
wrapper.append(warningText)
wrapper.append(hide)

const overlay = $('<div class="overlay">')
overlay.append(wrapper)
container.append(overlay)
}

$(document).ready(() => {
$(".video-container").each((index, element) => {
const container = $(element)
if (container.data("has-content-warning")) {
createOverlay(container)
}
})
})
1 change: 1 addition & 0 deletions OpenOversight/app/templates/incident_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,5 @@ <h4>Incident Description</h4>
</div>
{% endif %}
</main>
<script src="{{ url_for('static', filename='js/contentWarning.js') }}"></script>
{% endblock content %}
1 change: 1 addition & 0 deletions OpenOversight/app/templates/officer.html
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,5 @@ <h1>
</div>
{# end row #}
</div>
<script src="{{ url_for('static', filename='js/contentWarning.js') }}"></script>
{% endblock content %}
19 changes: 14 additions & 5 deletions OpenOversight/app/templates/partials/links_and_videos_row.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ <h3>Links</h3>
{% for link in list %}
<li class="list-group-item">
<a href="{{ link.url }}" rel="noopener noreferrer" target="_blank">{{ link.title or link.url }}</a>
{% if link.has_content_warning %}
<span class="label label-danger"
title="The linked page may be disturbing for some viewers">Content Warning</span>
{% endif %}
{% if officer and (is_admin_or_coordinator or link.created_by == current_user.id) %}
<a href="{{ url_for('main.link_api_edit', officer_id=officer.id, obj_id=link.id) }}">
<span class="sr-only">Edit</span>
Expand All @@ -30,10 +34,6 @@ <h3>Links</h3>
</ul>
{% endif %}
{% endfor %}
{% if officer and (current_user.is_admin_or_coordinator(officer.department)) %}
<a href="{{ url_for("main.link_api_new", officer_id=officer.id) }}"
class="btn btn-primary">New Link/Video</a>
{% endif %}
{% for type, list in obj.links | groupby("link_type") %}
{% if type == "video" %}
<h3>Videos</h3>
Expand All @@ -53,7 +53,8 @@ <h3>Videos</h3>
<i class="fa-solid fa-trash-can" aria-hidden="true"></i>
</a>
{% endif %}
<div class="video-container">
<div class="video-container"
data-has-content-warning="{{ link.has_content_warning | lower }}">
<iframe width="560"
height="315"
src="https://www.youtube.com/embed/{{ link_url }}"
Expand Down Expand Up @@ -81,6 +82,10 @@ <h3>Other videos</h3>
{% for link in list %}
<li class="list-group-item">
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer">{{ link.title or link.url }}</a>
{% if link.has_content_warning %}
<span class="label label-danger"
title="The linked video may be disturbing for some viewers">Content Warning</span>
{% endif %}
{% if officer and (current_user.is_admin_or_coordinator(officer.department)
or link.created_by == current_user.id) %}
<a href="{{ url_for('main.link_api_edit', officer_id=officer.id, obj_id=link.id) }}">
Expand All @@ -106,4 +111,8 @@ <h3>Other videos</h3>
</ul>
{% endif %}
{% endfor %}
{% if officer and (current_user.is_admin_or_coordinator(officer.department)) %}
<a href="{{ url_for("main.link_api_new", officer_id=officer.id) }}"
class="btn btn-primary">New Link/Video</a>
{% endif %}
{% endif %}
1 change: 1 addition & 0 deletions OpenOversight/app/utils/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ def get_or_create_link_from_form(link_form, user: User) -> Union[Link, None]:
link_type=if_exists_or_none(link_form["link_type"]),
title=if_exists_or_none(link_form["title"]),
url=if_exists_or_none(link_form["url"]),
has_content_warning=link_form["has_content_warning"],
created_by=user.id,
last_updated_by=user.id,
)
Expand Down
31 changes: 31 additions & 0 deletions OpenOversight/migrations/versions/2024-06-05-0203_939ea0f2b26d_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Add has_content_warning column to link model
Revision ID: 939ea0f2b26d
Revises: 52d3f6a21dd9
Create Date: 2024-06-05 02:03:29.168771
"""
import sqlalchemy as sa
from alembic import op


revision = "939ea0f2b26d"
down_revision = "52d3f6a21dd9"


def upgrade():
# This is not expected to impact performance: https://dba.stackexchange.com/a/216153
with op.batch_alter_table("links", schema=None) as batch_op:
batch_op.add_column(
sa.Column(
"has_content_warning",
sa.Boolean(),
nullable=False,
server_default="false",
)
)


def downgrade():
with op.batch_alter_table("links", schema=None) as batch_op:
batch_op.drop_column("has_content_warning")
16 changes: 12 additions & 4 deletions OpenOversight/tests/routes/route_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,20 +80,28 @@ def process_form_data(form_dict: dict) -> dict:
if type(value[0]) is dict:
for idx, item in enumerate(value):
for sub_key, sub_value in item.items():
new_dict[f"{key}-{idx}-{sub_key}"] = sub_value
if type(sub_value) is not bool:
new_dict[f"{key}-{idx}-{sub_key}"] = sub_value
elif sub_value:
new_dict[f"{key}-{idx}-{sub_key}"] = "y"
elif type(value[0]) is str or type(value[0]) is int:
for idx, item in enumerate(value):
new_dict[f"{key}-{idx}"] = item
if type(item) is not bool:
new_dict[f"{key}-{idx}"] = item
elif item:
new_dict[f"{key}-{idx}"] = "y"
else:
raise ValueError(
"Lists must contain dicts, strings or ints. {} submitted".format(
type(value[0])
)
)
elif type(value) == dict:
elif type(value) is dict:
for sub_key, sub_value in value.items():
new_dict[f"{key}-{sub_key}"] = sub_value
else:
elif type(value) is not bool:
new_dict[key] = value
elif value:
new_dict[key] = "y"

return new_dict
69 changes: 68 additions & 1 deletion OpenOversight/tests/routes/test_incidents.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,68 @@ def test_admins_can_create_basic_incidents(report_number, mockdata, client, sess
assert inc is not None


@pytest.mark.parametrize(
"link_type, expected_text",
[
("link", "The linked page may be disturbing for some viewers"),
("video", 'data-has-content-warning="true"'),
("other_video", "The linked video may be disturbing for some viewers"),
],
)
def test_admins_can_create_incidents_with_links(
mockdata, client, session, link_type, expected_text
):
with current_app.test_request_context():
login_admin(client)
test_date = datetime(2000, 5, 25, 1, 45)

# No content warning
address_form = LocationForm(city="FFFFF", state="IA")
link_form = LinkForm(
url="https://website.example",
link_type=link_type,
has_content_warning=False,
)
license_plates_form = LicensePlateForm(state="AZ")
form = IncidentForm(
date_field=str(test_date.date()),
time_field=str(test_date.time()),
report_number="report1",
description="Something happened",
department="1",
address=address_form.data,
links=[link_form.data],
license_plates=[license_plates_form.data],
officers=[],
)

rv = client.post(
url_for("main.incident_api_new"),
data=process_form_data(form.data),
follow_redirects=True,
)
assert rv.status_code == HTTPStatus.OK
assert "created" in rv.data.decode(ENCODING_UTF_8)
assert expected_text not in rv.data.decode(ENCODING_UTF_8)

# Has content warning
link_form = LinkForm(
url="https://website2.example",
link_type=link_type,
has_content_warning=True,
)
form.links.append_entry(link_form.data)

rv = client.post(
url_for("main.incident_api_new"),
data=process_form_data(form.data),
follow_redirects=True,
)
assert rv.status_code == HTTPStatus.OK
assert "created" in rv.data.decode(ENCODING_UTF_8)
assert expected_text in rv.data.decode(ENCODING_UTF_8)


def test_admins_cannot_create_incident_with_invalid_report_number(
mockdata, client, session
):
Expand Down Expand Up @@ -213,7 +275,12 @@ def test_admins_can_edit_incident_links_and_licenses(mockdata, client, session,
)
old_links = inc.links
old_links_forms = [
LinkForm(url=link.url, link_type=link.link_type).data for link in inc.links
LinkForm(
url=link.url,
link_type=link.link_type,
has_content_warning=link.has_content_warning,
).data
for link in inc.links
]
new_url = faker.url()
link_form = LinkForm(url=new_url, link_type="video")
Expand Down
47 changes: 47 additions & 0 deletions OpenOversight/tests/routes/test_officer_and_department.py
Original file line number Diff line number Diff line change
Expand Up @@ -2526,6 +2526,53 @@ def test_ac_cannot_add_link_to_officer_profile_not_in_their_dept(
assert rv.status_code == HTTPStatus.FORBIDDEN


@pytest.mark.parametrize(
"link_type, expected_text",
[
("link", "The linked page may be disturbing for some viewers"),
("video", 'data-has-content-warning="true"'),
("other_video", "The linked video may be disturbing for some viewers"),
],
)
def test_ac_can_add_link_with_content_warning(
mockdata, client, session, link_type, expected_text
):
with current_app.test_request_context():
login_ac(client)
officer = Officer.query.filter_by(department_id=AC_DEPT).first()

# No content warning
form = OfficerLinkForm(
title="BPD Watch",
description="Baltimore instance of OpenOversight",
author="OJB",
url="https://bpdwatch.com",
link_type=link_type,
officer_id=officer.id,
has_content_warning=False,
)

rv = client.post(
url_for("main.link_api_new", officer_id=officer.id),
data=process_form_data(form.data),
follow_redirects=True,
)

assert "link created!" in rv.data.decode(ENCODING_UTF_8)
assert expected_text not in rv.data.decode(ENCODING_UTF_8)

# Has content warning
form.has_content_warning.data = True

rv = client.post(
url_for("main.link_api_new", officer_id=officer.id),
data=process_form_data(form.data),
follow_redirects=True,
)
assert "link created!" in rv.data.decode(ENCODING_UTF_8)
assert expected_text in rv.data.decode(ENCODING_UTF_8)


def test_admin_can_edit_link_on_officer_profile(mockdata, client, session):
with current_app.test_request_context():
login_admin(client)
Expand Down

0 comments on commit 3c90393

Please sign in to comment.