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 content warnings for videos/links #457

Merged
merged 1 commit into from
Jun 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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>")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we need to add a </div> to this or does jquery handle it for us?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Edit: This is the case based on the Jquery documentation!

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
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",
Copy link
Collaborator Author

@sea-kelp sea-kelp Jun 6, 2024

Choose a reason for hiding this comment

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

This migration does not add content warnings to existing links and videos, so users will need to mark those manually

)
)


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