@@ -67,8 +67,8 @@

Select Department

Do you remember any part of the Officer's last name?

- {{ form.name(class="form-control") }} - {% for error in form.name.errors %} + {{ form.last_name(class="form-control") }} + {% for error in form.last_name.errors %}

[{{ error }}]

{% endfor %}
diff --git a/OpenOversight/app/templates/list_officer.html b/OpenOversight/app/templates/list_officer.html index f362e62aa..d057baf1f 100644 --- a/OpenOversight/app/templates/list_officer.html +++ b/OpenOversight/app/templates/list_officer.html @@ -10,13 +10,25 @@

{{ department.name|title }} Officers

-
+

Last name

-
+
- + +
+
+
+
+
+
+

First name

+
+
+
+
+
@@ -93,6 +105,51 @@

Rank

+
+
+

Photo

+
+
+
+
+ + +
+
+
+
+
+
+

Total pay

+
+
+
+
+
+
+
+
+
+
+
+ $ + +
+
+
+
+ $ + +
+
+
+
+
+

Unit

@@ -132,50 +189,81 @@

Age range

- {% with paginate=officers, location='top' %} - {% include "partials/paginate_nav.html" %} - {% endwith %} -
    - {% for officer in officers.items %} -
  • -
    -
    - - {{ officer.full_name() }} - -
    -
    -

    - {{ officer.full_name() }} - {{ officer.badge_number()|default('') }} -

    -
    -
    -
    -
    Job Title
    -
    {{ officer.job_title()|default('Unknown') }}
    -
    Race
    -
    {{ officer.race_label()|default('Unknown')|lower|title }}
    -
    -
    -
    -
    -
    Gender
    -
    {{ officer.gender_label()|default('Unknown') }}
    -
    Number of Photos
    -
    {{ officer.face.count() }}
    -
    +
    +
    +
    +
    + + + +
    +
    +
    +
    + {% with paginate=officers, location='top' %} + {% include "partials/paginate_nav.html" %} + {% endwith %} +
    +
    +
      + {% for officer in officers.items %} +
    • +
      +
      + + {{ officer.full_name() }} + +
      +
      +

      + {{ officer.full_name() }} + {{ officer.badge_number()|default('') }} +

      +
      +
      +
      +
      Job Title
      +
      {{ officer.job_title()|default('Unknown') }}
      +
      Race
      +
      {{ officer.race_label()|default('Unknown')|lower|title }}
      +
      +
      +
      +
      +
      Gender
      +
      {{ officer.gender_label()|default('Unknown') }}
      +
      Number of Photos
      +
      {{ officer.face.count() }}
      +
      +
      -
    -
  • - {% endfor %} -
- {% with paginate=officers, location='bottom' %} - {% include "partials/paginate_nav.html" %} - {% endwith %} -
+ + {% endfor %} + + {% with paginate=officers, location='bottom' %} + {% include "partials/paginate_nav.html" %} + {% endwith %} +
{% endblock content %} diff --git a/OpenOversight/app/templates/partials/paginate.html b/OpenOversight/app/templates/partials/paginate.html deleted file mode 100644 index 82ba0502a..000000000 --- a/OpenOversight/app/templates/partials/paginate.html +++ /dev/null @@ -1,20 +0,0 @@ -
- - {% if officers.has_prev %} -
- {% include 'partials/officer_form_fields_hidden.html' %} - -
- {% endif %} - {% if officers.total > 0 %} -

Gallery {{ officers.page }} of {{ officers.pages }}

- {% endif %} - {% if officers.has_next %} -
- {% include 'partials/officer_form_fields_hidden.html' %} - -
- {% endif %} - -
- diff --git a/OpenOversight/app/templates/partials/paginate_nav.html b/OpenOversight/app/templates/partials/paginate_nav.html index fcce55215..9a800f2a6 100644 --- a/OpenOversight/app/templates/partials/paginate_nav.html +++ b/OpenOversight/app/templates/partials/paginate_nav.html @@ -1,6 +1,12 @@ diff --git a/OpenOversight/app/utils.py b/OpenOversight/app/utils.py index 1ef660408..318e729a3 100644 --- a/OpenOversight/app/utils.py +++ b/OpenOversight/app/utils.py @@ -16,8 +16,8 @@ from traceback import format_exc from distutils.util import strtobool -from sqlalchemy import func -from sqlalchemy.sql.expression import cast +from sqlalchemy import func, or_ +from sqlalchemy.sql.expression import cast, nullslast, desc import imghdr as imghdr from flask import current_app, url_for from flask_login import current_user @@ -132,7 +132,8 @@ def add_officer_profile(form, current_user): gender=form.gender.data, birth_year=form.birth_year.data, employment_date=form.employment_date.data, - department_id=form.department.data.id) + department_id=form.department.data.id, + unique_internal_identifier=form.unique_internal_identifier.data) db.session.add(officer) db.session.commit() @@ -141,12 +142,13 @@ def add_officer_profile(form, current_user): else: officer_unit = None - assignment = Assignment(baseofficer=officer, - star_no=form.star_no.data, - job_id=form.job_id.data, - unit=officer_unit, - star_date=form.employment_date.data) - db.session.add(assignment) + if form.job_id.data: + assignment = Assignment(baseofficer=officer, + star_no=form.star_no.data, + job_id=form.job_id.data, + unit=officer_unit, + star_date=form.employment_date.data) + db.session.add(assignment) if form.links.data: for link in form.data['links']: # don't try to create with a blank string @@ -251,23 +253,36 @@ def upload_obj_to_s3(file_obj, dest_filename): return url -def filter_by_form(form, officer_query, department_id=None): - # Some SQL acrobatics to left join only the most recent assignment per officer - row_num_col = func.row_number().over( +def filter_by_form(form, officer_query, department_id=None, order=0): + # Some SQL acrobatics to left join only the most recent assignment and salary per officer + assignment_row_num_col = func.row_number().over( partition_by=Assignment.officer_id, order_by=Assignment.star_date.desc() - ).label('row_num') - subq = db.session.query( + ).label('assignment_row_num') + assignment_subq = db.session.query( Assignment.officer_id, Assignment.job_id, Assignment.star_date, Assignment.star_no, Assignment.unit_id - ).add_columns(row_num_col).from_self().filter(row_num_col == 1).subquery() - officer_query = officer_query.outerjoin(subq) - - if form.get('name'): + ).add_columns(assignment_row_num_col).from_self().filter(assignment_row_num_col == 1).subquery() + salary_row_num_col = func.row_number().over( + partition_by=Salary.officer_id, order_by=Salary.year.desc() + ).label('salary_row_num') + salary_subq = db.session.query( + Salary.officer_id, + Salary.salary, + Salary.overtime_pay, + Salary.year, + ).add_columns(salary_row_num_col).from_self().filter(salary_row_num_col == 1).subquery() + officer_query = officer_query.outerjoin(assignment_subq).outerjoin(salary_subq) + + if form.get('last_name'): officer_query = officer_query.filter( - Officer.last_name.ilike('%%{}%%'.format(form['name'])) + Officer.last_name.ilike('%%{}%%'.format(form['last_name'])) + ) + if form.get('first_name'): + officer_query = officer_query.filter( + Officer.first_name.ilike('%%{}%%'.format(form['first_name'])) ) if not department_id and form.get('dept'): department_id = form['dept'].id @@ -275,23 +290,28 @@ def filter_by_form(form, officer_query, department_id=None): Officer.department_id == department_id ) if form.get('badge'): - officer_query = officer_query.filter( - subq.c.assignments_star_no.like('%%{}%%'.format(form['badge'])) - ) + or_clauses = [ + assignment_subq.c.assignments_star_no.ilike('%%{}%%'.format(star_no.strip())) + for star_no in form['badge'].split(',') + ] + officer_query = officer_query.filter(or_(*or_clauses)) if form.get('unit'): officer_query = officer_query.filter( - subq.c.assignments_unit_id == form['unit'] + assignment_subq.c.assignments_unit_id == form['unit'] ) - if form.get('unique_internal_identifier'): - officer_query = officer_query.filter( - Officer.unique_internal_identifier.ilike('%%{}%%'.format(form['unique_internal_identifier'])) - ) + or_clauses = [ + Officer.unique_internal_identifier.ilike('%%{}%%'.format(uii.strip())) + for uii in form['unique_internal_identifier'].split(',') + ] + officer_query = officer_query.filter(or_(*or_clauses)) + race_values = [x for x, _ in RACE_CHOICES] if form.get('race') and all(race in race_values for race in form['race']): if 'Not Sure' in form['race']: form['race'].append(None) officer_query = officer_query.filter(Officer.race.in_(form['race'])) + gender_values = [x for x, _ in GENDER_CHOICES] if form.get('gender') and all(gender in gender_values for gender in form['gender']): if 'Not Sure' in form['gender']: @@ -313,6 +333,44 @@ def filter_by_form(form, officer_query, department_id=None): form['rank'].append(None) officer_query = officer_query.filter(Job.job_title.in_(form['rank'])) + if form.get('photo') and all(photo in ['0', '1'] for photo in form['photo']): + face_officer_ids = set([face.officer_id for face in Face.query.all()]) + if '0' in form['photo'] and '1' not in form['photo']: + officer_query = officer_query.filter( + Officer.id.notin_(face_officer_ids) + ) + elif '1' in form['photo'] and '0' not in form['photo']: + officer_query = officer_query.filter( + Officer.id.in_(face_officer_ids) + ) + + if form.get('max_pay') and form.get('min_pay') and float(form['max_pay']) > float(form['min_pay']): + officer_query = officer_query.filter( + db.and_( + salary_subq.c.salaries_salary + salary_subq.c.salaries_overtime_pay >= float(form['min_pay']), + salary_subq.c.salaries_salary + salary_subq.c.salaries_overtime_pay <= float(form['max_pay']) + ) + ) + elif form.get('min_pay') and float(form['min_pay']) > 0 and not form.get('max_pay'): + officer_query = officer_query.filter( + salary_subq.c.salaries_salary + salary_subq.c.salaries_overtime_pay >= float(form['min_pay']) + ) + elif form.get('max_pay') and float(form['max_pay']) > 0 and not form.get('min_pay'): + officer_query = officer_query.filter( + salary_subq.c.salaries_salary + salary_subq.c.salaries_overtime_pay <= float(form['max_pay']) + ) + + if order == 0: # Last name alphabetical + officer_query = officer_query.order_by(Officer.last_name, Officer.first_name, Officer.id) + elif order == 1: # Rank + officer_query = officer_query.order_by(nullslast(Job.order.desc())) + elif order == 2: # Total pay + officer_query = officer_query.order_by(nullslast(desc(salary_subq.c.salaries_salary + salary_subq.c.salaries_overtime_pay))) + elif order == 3: # Salary + officer_query = officer_query.order_by(nullslast(salary_subq.c.salaries_salary.desc())) + elif order == 4: # Overtime pay + officer_query = officer_query.order_by(nullslast(salary_subq.c.salaries_overtime_pay.desc())) + return officer_query diff --git a/OpenOversight/migrations/versions/b4145ba7d4c6_overtime_pay_default_0.py b/OpenOversight/migrations/versions/b4145ba7d4c6_overtime_pay_default_0.py new file mode 100644 index 000000000..1d0427c1c --- /dev/null +++ b/OpenOversight/migrations/versions/b4145ba7d4c6_overtime_pay_default_0.py @@ -0,0 +1,34 @@ +"""overtime_pay default 0 + +Revision ID: b4145ba7d4c6 +Revises: 86eb228e4bc0 +Create Date: 2020-09-27 14:43:58.704560 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'b4145ba7d4c6' +down_revision = '86eb228e4bc0' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.execute('update salaries set overtime_pay = 0 where overtime_pay is null') + op.alter_column('salaries', 'overtime_pay', + existing_type=sa.NUMERIC(), + server_default='0', + nullable=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('salaries', 'overtime_pay', + existing_type=sa.NUMERIC(), + nullable=True) + # ### end Alembic commands ### diff --git a/OpenOversight/tests/routes/test_officer_and_department.py b/OpenOversight/tests/routes/test_officer_and_department.py index 9e7e26166..354bfb2dc 100644 --- a/OpenOversight/tests/routes/test_officer_and_department.py +++ b/OpenOversight/tests/routes/test_officer_and_department.py @@ -1,4 +1,5 @@ # Routing and view tests +import re import csv import copy import json @@ -1511,6 +1512,162 @@ def test_browse_filtering_allows_good(client, mockdata, session): assert any("
Male
" in token for token in filter_list) +def test_browse_filtering_multiple_uiis(client, mockdata, session): + with current_app.test_request_context(): + department_id = Department.query.first().id + + # Add two officers + login_admin(client) + form = AddOfficerForm(first_name='Porkchops', + last_name='McBacon', + unique_internal_identifier='foo', + department=department_id) + data = process_form_data(form.data) + rv = client.post( + url_for('main.add_officer'), + data=data, + follow_redirects=True + ) + assert 'New Officer McBacon added' in rv.data.decode('utf-8') + + form = AddOfficerForm(first_name='Bacon', + last_name='McPorkchops', + unique_internal_identifier='bar', + department=department_id) + data = process_form_data(form.data) + rv = client.post( + url_for('main.add_officer'), + data=data, + follow_redirects=True + ) + assert 'New Officer McPorkchops added' in rv.data.decode('utf-8') + + # Check the officers were added to the database + assert Officer.query.filter_by(department_id=department_id).filter_by(unique_internal_identifier='foo').count() == 1 + assert Officer.query.filter_by(department_id=department_id).filter_by(unique_internal_identifier='bar').count() == 1 + + # Check that added officers appear when filtering for both UIIs + form = BrowseForm(unique_internal_identifier='foo,bar', race=None, gender=None) + data = process_form_data(form.data) + rv = client.get( + url_for('main.list_officer', department_id=department_id), + query_string=data, + follow_redirects=True + ) + assert 'McBacon' in rv.data.decode('utf-8') + assert 'McPorkchops' in rv.data.decode('utf-8') + assert 'foo' in rv.data.decode('utf-8') + assert 'bar' in rv.data.decode('utf-8') + + +def test_browse_sort_by_last_name(client, mockdata): + with current_app.test_request_context(): + department_id = Department.query.first().id + rv = client.get( + url_for('main.list_officer', department_id=department_id, order=0), + follow_redirects=True + ) + response = rv.data.decode('utf-8') + officer_ids = re.findall(r'', response) + assert officer_ids is not None and len(officer_ids) == int(current_app.config['OFFICERS_PER_PAGE']) + for idx, officer_id in enumerate(officer_ids): + if idx + 1 < len(officer_ids): + officer = Officer.query.filter_by(id=officer_id).one() + next_officer_id = officer_ids[idx + 1] + next_officer = Officer.query.filter_by(id=next_officer_id).one() + assert officer.last_name <= next_officer.last_name + + +def test_browse_sort_by_rank(client, mockdata): + with current_app.test_request_context(): + department_id = Department.query.first().id + rv = client.get( + url_for('main.list_officer', department_id=department_id, order=1), + follow_redirects=True + ) + response = rv.data.decode('utf-8') + officer_ids = re.findall(r'', response) + assert officer_ids is not None and len(officer_ids) == int(current_app.config['OFFICERS_PER_PAGE']) + for idx, officer_id in enumerate(officer_ids): + if idx + 1 < len(officer_ids): + officer = Officer.query.filter_by(id=officer_id).one() + next_officer_id = officer_ids[idx + 1] + next_officer = Officer.query.filter_by(id=next_officer_id).one() + officer_job = officer\ + .assignments\ + .order_by(Assignment.star_date.desc())\ + .first()\ + .job + next_officer_job = next_officer\ + .assignments\ + .order_by(Assignment.star_date.desc())\ + .first()\ + .job + assert officer_job.order >= next_officer_job.order + + +def test_browse_sort_by_total_pay(client, mockdata): + with current_app.test_request_context(): + department_id = Department.query.first().id + rv = client.get( + url_for('main.list_officer', department_id=department_id, order=2), + follow_redirects=True + ) + response = rv.data.decode('utf-8') + officer_ids = re.findall(r'', response) + assert officer_ids is not None and len(officer_ids) == int(current_app.config['OFFICERS_PER_PAGE']) + for idx, officer_id in enumerate(officer_ids): + if idx + 1 < len(officer_ids): + officer = Officer.query.filter_by(id=officer_id).one() + next_officer_id = officer_ids[idx + 1] + next_officer = Officer.query.filter_by(id=next_officer_id).one() + officer_salary = officer.salaries[0] + next_officer_salary = next_officer.salaries[0] + officer_total_pay = officer_salary.salary + officer_salary.overtime_pay + next_officer_total_pay = next_officer_salary.salary + next_officer_salary.overtime_pay + assert officer_total_pay >= next_officer_total_pay + + +def test_browse_sort_by_salary(client, mockdata): + with current_app.test_request_context(): + department_id = Department.query.first().id + rv = client.get( + url_for('main.list_officer', department_id=department_id, order=3), + follow_redirects=True + ) + response = rv.data.decode('utf-8') + officer_ids = re.findall(r'', response) + assert officer_ids is not None and len(officer_ids) == int(current_app.config['OFFICERS_PER_PAGE']) + for idx, officer_id in enumerate(officer_ids): + if idx + 1 < len(officer_ids): + officer = Officer.query.filter_by(id=officer_id).one() + next_officer_id = officer_ids[idx + 1] + next_officer = Officer.query.filter_by(id=next_officer_id).one() + officer_salary = officer.salaries[0] + next_officer_salary = next_officer.salaries[0] + assert officer_salary.salary >= next_officer_salary.salary + + +def test_browse_sort_by_overtime_pay(client, mockdata): + with current_app.test_request_context(): + department_id = Department.query.first().id + rv = client.get( + url_for('main.list_officer', department_id=department_id, order=4), + follow_redirects=True + ) + response = rv.data.decode('utf-8') + officer_ids = re.findall(r'', response) + assert officer_ids is not None and len(officer_ids) == int(current_app.config['OFFICERS_PER_PAGE']) + for idx, officer_id in enumerate(officer_ids): + if idx + 1 < len(officer_ids): + officer = Officer.query.filter_by(id=officer_id).one() + next_officer_id = officer_ids[idx + 1] + next_officer = Officer.query.filter_by(id=next_officer_id).one() + officer_salary = officer.salaries[0] + next_officer_salary = next_officer.salaries[0] + assert officer_salary.overtime_pay >= next_officer_salary.overtime_pay + + def test_admin_can_upload_photos_of_dept_officers(mockdata, client, session, test_jpg_BytesIO): with current_app.test_request_context(): login_admin(client) diff --git a/OpenOversight/tests/test_commands.py b/OpenOversight/tests/test_commands.py index 6e6b5bdb3..e3838cfbb 100644 --- a/OpenOversight/tests/test_commands.py +++ b/OpenOversight/tests/test_commands.py @@ -839,7 +839,7 @@ def test_advanced_csv_import__success(session, department_with_ranks, test_csv_d assert salary_2018.year == 2018 assert salary_2018.salary == 10000 assert salary_2018.is_fiscal_year is True - assert salary_2018.overtime_pay is None + assert salary_2018.overtime_pay == 0 assert salary_2019.salary == 10001 assignment_po, assignment_cap = sorted( diff --git a/OpenOversight/tests/test_models.py b/OpenOversight/tests/test_models.py index 3681daa58..924061de3 100644 --- a/OpenOversight/tests/test_models.py +++ b/OpenOversight/tests/test_models.py @@ -36,7 +36,7 @@ def test_face_repr(mockdata): def test_unit_repr(mockdata): unit = Unit.query.first() - assert unit.__repr__() == 'Unit: {}'.format(unit.descrip) + assert unit.__repr__() == ''.format(unit.descrip) def test_user_repr(mockdata): @@ -46,7 +46,7 @@ def test_user_repr(mockdata): def test_salary_repr(mockdata): salary = Salary.query.first() - assert salary.__repr__() == ''.format(salary.officer_id, salary.salary, salary.overtime_pay, salary.year) def test_password_not_printed(mockdata): diff --git a/OpenOversight/tests/test_utils.py b/OpenOversight/tests/test_utils.py index 64dc3e7fe..ff9454e46 100644 --- a/OpenOversight/tests/test_utils.py +++ b/OpenOversight/tests/test_utils.py @@ -13,91 +13,110 @@ def test_department_filter(mockdata): department = OpenOversight.app.models.Department.query.first() - results = OpenOversight.app.utils.grab_officers( + officers = OpenOversight.app.utils.grab_officers( {'race': ['Not Sure'], 'gender': ['Not Sure'], 'rank': ['Not Sure'], - 'min_age': 16, 'max_age': 85, 'name': '', 'badge': '', + 'min_age': 16, 'max_age': 85, 'last_name': '', 'badge': '', 'dept': department, 'unique_internal_identifier': ''} ) - for element in results.all(): - assert element.department == department + for officer in officers.all(): + assert officer.department == department def test_race_filter_select_all_black_officers(mockdata): department = OpenOversight.app.models.Department.query.first() - results = OpenOversight.app.utils.grab_officers( + officers = OpenOversight.app.utils.grab_officers( {'race': ['BLACK'], 'dept': department} ) - for element in results.all(): - assert element.race in ('BLACK', 'Not Sure') + for officer in officers.all(): + assert officer.race in ('BLACK', 'Not Sure') def test_gender_filter_select_all_male_officers(mockdata): department = OpenOversight.app.models.Department.query.first() - results = OpenOversight.app.utils.grab_officers( + officers = OpenOversight.app.utils.grab_officers( {'gender': ['M'], 'dept': department} ) - for element in results.all(): - assert element.gender in ('M', 'Not Sure') + for officer in officers.all(): + assert officer.gender in ('M', 'Not Sure') def test_rank_filter_select_all_commanders(mockdata): department = OpenOversight.app.models.Department.query.first() - results = OpenOversight.app.utils.grab_officers( + officers = OpenOversight.app.utils.grab_officers( {'rank': ['Commander'], 'dept': department} ) - for element in results.all(): - assignment = element.assignments.first() + for officer in officers.all(): + assignment = officer.assignments.first() assert assignment.job.job_title in ('Commander', 'Not Sure') def test_rank_filter_select_all_police_officers(mockdata): department = OpenOversight.app.models.Department.query.first() - results = OpenOversight.app.utils.grab_officers( + officers = OpenOversight.app.utils.grab_officers( {'rank': ['Police Officer'], 'dept': department} ) - for element in results.all(): - assignment = element.assignments.first() + for officer in officers.all(): + assignment = officer.assignments.first() assert assignment.job.job_title in ('Police Officer', 'Not Sure') -def test_filter_by_name(mockdata): +def test_filter_by_last_name(mockdata): department = OpenOversight.app.models.Department.query.first() - results = OpenOversight.app.utils.grab_officers( - {'name': 'J', 'dept': department} + officers = OpenOversight.app.utils.grab_officers( + {'last_name': 'J', 'dept': department} ) - for element in results.all(): - assert 'J' in element.last_name + for officer in officers.all(): + assert 'J' in officer.last_name + + +def test_filter_by_first_name(mockdata): + department = OpenOversight.app.models.Department.query.first() + officers = OpenOversight.app.utils.grab_officers( + {'first_name': 'J', 'dept': department} + ) + for officer in officers.all(): + assert 'J' in officer.first_name def test_filters_do_not_exclude_officers_without_assignments(mockdata): department = OpenOversight.app.models.Department.query.first() officer = OpenOversight.app.models.Officer(first_name='Rachel', last_name='S', department=department, birth_year=1992) - results = OpenOversight.app.utils.grab_officers( - {'name': 'S', 'dept': department} + officers = OpenOversight.app.utils.grab_officers( + {'last_name': 'S', 'dept': department} ) - assert officer in results.all() + assert officer in officers.all() def test_filter_by_badge_no(mockdata): department = OpenOversight.app.models.Department.query.first() - results = OpenOversight.app.utils.grab_officers( + officers = OpenOversight.app.utils.grab_officers( {'badge': '12', 'dept': department} ) - for element in results.all(): - assignment = element.assignments.first() - assert '12' in str(assignment.star_no) + for officer in officers.all(): + assert '12' in officer.badge_number() + + +def test_filter_by_multiple_badge_no(mockdata): + department = OpenOversight.app.models.Department.query.first() + target_officers = OpenOversight.app.models.Officer.query.filter_by(department_id=department.id).limit(2).all() + target_badge_nos = [officer.badge_number() for officer in target_officers] + officers = OpenOversight.app.utils.grab_officers( + {'badge': ','.join(target_badge_nos), 'dept': department} + ) + for officer in officers.all(): + assert officer.badge_number() in target_badge_nos def test_filter_by_full_unique_internal_identifier_returns_officers(mockdata): department = OpenOversight.app.models.Department.query.first() - target_unique_internal_id = OpenOversight.app.models.Officer.query.first().unique_internal_identifier - results = OpenOversight.app.utils.grab_officers( + target_unique_internal_id = OpenOversight.app.models.Officer.query.filter_by(department_id=department.id).first().unique_internal_identifier + officers = OpenOversight.app.utils.grab_officers( {'race': 'Not Sure', 'gender': 'Not Sure', 'rank': 'Not Sure', - 'min_age': 16, 'max_age': 85, 'name': '', 'badge': '', + 'min_age': 16, 'max_age': 85, 'last_name': '', 'badge': '', 'dept': department, 'unique_internal_identifier': target_unique_internal_id} ) - for element in results: - returned_unique_internal_id = element.unique_internal_identifier + for officer in officers: + returned_unique_internal_id = officer.unique_internal_identifier assert returned_unique_internal_id == target_unique_internal_id @@ -105,16 +124,93 @@ def test_filter_by_partial_unique_internal_identifier_returns_officers(mockdata) department = OpenOversight.app.models.Department.query.first() identifier = OpenOversight.app.models.Officer.query.first().unique_internal_identifier partial_identifier = identifier[:len(identifier) // 2] - results = OpenOversight.app.utils.grab_officers( + officers = OpenOversight.app.utils.grab_officers( {'race': 'Not Sure', 'gender': 'Not Sure', 'rank': 'Not Sure', - 'min_age': 16, 'max_age': 85, 'name': '', 'badge': '', + 'min_age': 16, 'max_age': 85, 'last_name': '', 'badge': '', 'dept': department, 'unique_internal_identifier': partial_identifier} ) - for element in results: - returned_identifier = element.unique_internal_identifier + for officer in officers: + returned_identifier = officer.unique_internal_identifier assert returned_identifier == identifier +def test_filter_by_multiple_unique_internal_identifiers_returns_officers(mockdata): + department = OpenOversight.app.models.Department.query.first() + target_officers = OpenOversight.app.models.Officer.query.filter_by(department_id=department.id).limit(2).all() + target_unique_internal_ids = [officer.unique_internal_identifier for officer in target_officers] + officers = OpenOversight.app.utils.grab_officers( + {'race': 'Not Sure', 'gender': 'Not Sure', 'rank': 'Not Sure', + 'min_age': 16, 'max_age': 85, 'last_name': '', 'badge': '', + 'dept': department, 'unique_internal_identifier': ','.join(target_unique_internal_ids)} + ) + for officer in officers: + returned_unique_internal_id = officer.unique_internal_identifier + assert returned_unique_internal_id in target_unique_internal_ids + + +def test_filter_by_photo_available(mockdata): + department = OpenOversight.app.models.Department.query.first() + officers = OpenOversight.app.utils.grab_officers( + {'race': 'Not Sure', 'gender': 'Not Sure', 'rank': 'Not Sure', + 'min_age': 16, 'max_age': 85, 'last_name': '', 'badge': '', + 'dept': department, 'unique_internal_identifier': '', 'photo': ['1']} + ) + for officer in officers: + assert officer.face.count() > 0 + + +def test_filter_by_photo_not_available(mockdata): + department = OpenOversight.app.models.Department.query.first() + officers = OpenOversight.app.utils.grab_officers( + {'race': 'Not Sure', 'gender': 'Not Sure', 'rank': 'Not Sure', + 'min_age': 16, 'max_age': 85, 'last_name': '', 'badge': '', + 'dept': department, 'unique_internal_identifier': '', 'photo': ['0']} + ) + for officer in officers: + assert officer.face.count() == 0 + + +def test_filter_min_pay(mockdata): + department = OpenOversight.app.models.Department.query.first() + officers = OpenOversight.app.utils.grab_officers( + {'race': 'Not Sure', 'gender': 'Not Sure', 'rank': 'Not Sure', + 'min_age': 16, 'max_age': 85, 'last_name': '', 'badge': '', + 'dept': department, 'unique_internal_identifier': '', 'min_pay': '500000'} + ) + for officer in officers: + most_recent_salary = max(officer.salaries, key=lambda s: s.year) + total_pay = most_recent_salary.salary + most_recent_salary.overtime_pay + assert total_pay >= 500000 + + +def test_filter_max_pay(mockdata): + department = OpenOversight.app.models.Department.query.first() + officers = OpenOversight.app.utils.grab_officers( + {'race': 'Not Sure', 'gender': 'Not Sure', 'rank': 'Not Sure', + 'min_age': 16, 'max_age': 85, 'last_name': '', 'badge': '', + 'dept': department, 'unique_internal_identifier': '', 'max_pay': '500000'} + ) + for officer in officers: + most_recent_salary = max(officer.salaries, key=lambda s: s.year) + total_pay = most_recent_salary.salary + most_recent_salary.overtime_pay + assert total_pay <= 500000 + + +def test_filter_min_and_max_pay(mockdata): + department = OpenOversight.app.models.Department.query.first() + officers = OpenOversight.app.utils.grab_officers( + {'race': 'Not Sure', 'gender': 'Not Sure', 'rank': 'Not Sure', + 'min_age': 16, 'max_age': 85, 'last_name': '', 'badge': '', + 'dept': department, 'unique_internal_identifier': '', + 'min_pay': '400000', 'max_pay': '500000'} + ) + for officer in officers: + most_recent_salary = max(officer.salaries, key=lambda s: s.year) + total_pay = most_recent_salary.salary + most_recent_salary.overtime_pay + assert total_pay <= 500000 + assert total_pay >= 400000 + + def test_compute_hash(mockdata): hash_result = OpenOversight.app.utils.compute_hash(b'bacon') expected_hash = '9cca0703342e24806a9f64e08c053dca7f2cd90f10529af8ea872afb0a0c77d4' @@ -158,7 +254,7 @@ def test_user_cannot_submit_invalid_file_extension(mockdata): def test_unit_choices(mockdata): unit_choices = [str(x) for x in OpenOversight.app.utils.unit_choices()] - assert 'Unit: Bureau of Organized Crime' in unit_choices + assert '' in unit_choices @patch('OpenOversight.app.utils.upload_obj_to_s3', MagicMock(return_value='https://s3-some-bucket/someaddress.jpg')) diff --git a/dockerfiles/web/Dockerfile b/dockerfiles/web/Dockerfile index 20eee30db..8d4844e88 100644 --- a/dockerfiles/web/Dockerfile +++ b/dockerfiles/web/Dockerfile @@ -7,10 +7,14 @@ WORKDIR /usr/src/app ENV DEBIAN-FRONTEND noninteractive ENV DISPLAY=:1 ENV GECKODRIVER_VERSION="v0.26.0" -RUN echo "deb http://deb.debian.org/debian stretch-backports main" > /etc/apt/sources.list.d/backports.list +ENV SQLITE3_VERSION="3330000" RUN wget -O - https://deb.nodesource.com/setup_12.x | bash - -RUN apt-get update && apt-get install -y xvfb firefox-esr libpq-dev python3-dev nodejs && \ - apt-get install -y -t stretch-backports libsqlite3-0 && apt-get clean +RUN apt-get update && apt-get install -y xvfb firefox-esr libpq-dev python3-dev nodejs && apt-get clean +RUN wget https://www.sqlite.org/2020/sqlite-autoconf-${SQLITE3_VERSION}.tar.gz && \ + tar -xzf sqlite-autoconf-${SQLITE3_VERSION}.tar.gz && \ + cd sqlite-autoconf-${SQLITE3_VERSION} && \ + ./configure && make && make install && ldconfig && \ + cd .. && rm -r sqlite-autoconf-${SQLITE3_VERSION} RUN wget https://github.com/mozilla/geckodriver/releases/download/${GECKODRIVER_VERSION}/geckodriver-${GECKODRIVER_VERSION}-linux64.tar.gz RUN mkdir geckodriver