Skip to content

Commit

Permalink
add button to export department officer data as CSV
Browse files Browse the repository at this point in the history
  • Loading branch information
tomx4096 committed Aug 19, 2018
1 parent 830649a commit b0d244e
Show file tree
Hide file tree
Showing 16 changed files with 897 additions and 76 deletions.
23 changes: 15 additions & 8 deletions OpenOversight/app/main/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ class FindOfficerForm(Form):
Length(max=10)])
dept = QuerySelectField('dept', validators=[DataRequired()],
query_factory=dept_choices, get_label='name')
rank = SelectField('rank', default='COMMANDER', choices=RANK_CHOICES,
rank = SelectField('rank', default='Not Sure', choices=RANK_CHOICES,
validators=[AnyOf(allowed_values(RANK_CHOICES))])
race = SelectField('race', default='WHITE', choices=RACE_CHOICES,
race = SelectField('race', default='Not Sure', choices=RACE_CHOICES,
validators=[AnyOf(allowed_values(RACE_CHOICES))])
gender = SelectField('gender', default='M', choices=GENDER_CHOICES,
gender = SelectField('gender', default='Not Sure', choices=GENDER_CHOICES,
validators=[AnyOf(allowed_values(GENDER_CHOICES))])
min_age = IntegerField('min_age', default=16, validators=[
NumberRange(min=16, max=100)
Expand Down Expand Up @@ -134,15 +134,16 @@ def validate(self):
return success


class BaseNoteForm(Form):
note = TextAreaField()
class BaseTextForm(Form):
text_contents = TextAreaField()
description = "This information about the officer will be attributed to your username."


class EditNoteForm(BaseNoteForm):
class EditTextForm(BaseTextForm):
submit = SubmitField(label='Submit')


class NoteForm(EditNoteForm):
class TextForm(EditTextForm):
officer_id = HiddenField(validators=[Required(message='Not a valid officer ID')])
creator_id = HiddenField(validators=[Required(message='Not a valid user ID')])

Expand Down Expand Up @@ -179,11 +180,17 @@ class AddOfficerForm(Form):
min_entries=1,
widget=BootstrapListWidget())
notes = FieldList(FormField(
BaseNoteForm,
BaseTextForm,
widget=FormFieldWidget()),
description='This note about the officer will be attributed to your username.',
min_entries=1,
widget=BootstrapListWidget())
descriptions = FieldList(FormField(
BaseTextForm,
widget=FormFieldWidget()),
description='This description of the officer will be attributed to your username.',
min_entries=1,
widget=BootstrapListWidget())

submit = SubmitField(label='Add')

Expand Down
132 changes: 119 additions & 13 deletions OpenOversight/app/main/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from werkzeug import secure_filename

from flask import (abort, render_template, request, redirect, url_for,
flash, current_app, jsonify)
flash, current_app, jsonify, Response)
from flask_login import current_user, login_required, login_user

from . import main
Expand All @@ -21,15 +21,16 @@
ac_can_edit_officer, add_department_query, add_unit_query,
create_incident, get_or_create, replace_list,
set_dynamic_default, create_note,
get_uploaded_cropped_image)
get_uploaded_cropped_image, create_description)

from .forms import (FindOfficerForm, FindOfficerIDForm, AddUnitForm,
FaceTag, AssignmentForm, DepartmentForm, AddOfficerForm,
EditOfficerForm, IncidentForm, NoteForm, EditNoteForm,
EditOfficerForm, IncidentForm, TextForm, EditTextForm,
AddImageForm, EditDepartmentForm)
from .model_view import ModelView
from ..models import (db, Image, User, Face, Officer, Assignment, Department,
Unit, Incident, Location, LicensePlate, Link, Note)
Unit, Incident, Location, LicensePlate, Link, Note,
Description)

from ..auth.forms import LoginForm
from ..auth.utils import admin_required, ac_or_admin_required
Expand Down Expand Up @@ -583,6 +584,77 @@ def submit_data():
return render_template('submit_image.html', form=form, preferred_dept_id=preferred_dept_id)


def check_input(str_input):
if str_input is None or str_input == "Not Sure":
return ""
else:
return str(str_input).replace(",", " ") # no commas allowed


@main.route('/download/department/<int:department_id>', methods=['GET'])
@limiter.limit('5/minute')
def download_dept_csv(department_id):
department = Department.query.filter_by(id=department_id).first()
records = Officer.query.filter_by(department_id=department_id).all()
if not department or not records:
abort(404)
dept_name = records[0].department.name.replace(" ", "_")
first_row = "id, last, first, middle, suffix, gender, "\
"race, born, employment_date, assignments\n"

assign_dict = {}
assign_records = Assignment.query.all()
for r in assign_records:
if r.officer_id not in assign_dict:
assign_dict[r.officer_id] = []
assign_dict[r.officer_id].append("(#%s %s %s %s %s)" % (check_input(r.star_no), check_input(r.rank), check_input(r.unit), check_input(r.star_date), check_input(r.resign_date)))

record_list = ["%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n" %
(str(record.id),
check_input(record.last_name),
check_input(record.first_name),
check_input(record.middle_initial),
check_input(record.suffix),
check_input(record.gender),
check_input(record.race),
check_input(record.birth_year),
check_input(record.employment_date),
" ".join(assign_dict.get(record.id, [])),
) for record in records]

csv_name = dept_name + "_Officers.csv"
csv = first_row + "".join(record_list)
csv_headers = {"Content-disposition": "attachment; filename=" + csv_name}
return Response(csv, mimetype="text/csv", headers=csv_headers)


@main.route('/download/department/<int:department_id>/incidents', methods=['GET'])
@limiter.limit('5/minute')
def download_incidents_csv(department_id):
department = Department.query.filter_by(id=department_id).first()
records = Incident.query.filter_by(department_id=department.id).all()
if not department or not records:
abort(404)
dept_name = records[0].department.name.replace(" ", "_")
first_row = "id,report_num,date,description,location,licences,links,officers\n"

record_list = ["%s,%s,%s,%s,%s,%s,%s,%s\n" %
(str(record.id),
check_input(record.report_number),
check_input(record.date),
check_input(record.description),
check_input(record.address),
" ".join(map(lambda x: str(x), record.license_plates)),
" ".join(map(lambda x: str(x), record.links)),
" ".join(map(lambda x: str(x), record.officers)),
) for record in records]

csv_name = dept_name + "_Incidents.csv"
csv = first_row + "".join(record_list)
csv_headers = {"Content-disposition": "attachment; filename=" + csv_name}
return Response(csv, mimetype="text/csv", headers=csv_headers)


@main.route('/upload/department/<int:department_id>', methods=['POST'])
@limiter.limit('250/minute')
def upload(department_id):
Expand Down Expand Up @@ -771,14 +843,11 @@ def populate_obj(self, form, obj):
methods=['GET', 'POST'])


class NoteApi(ModelView):
model = Note
model_name = 'note'
class TextApi(ModelView):
order_by = 'date_created'
descending = True
form = NoteForm
create_function = create_note
department_check = True
form = TextForm

def get_new_form(self):
form = self.form()
Expand All @@ -791,22 +860,41 @@ def get_redirect_url(self, *args, **kwargs):
def get_post_delete_url(self, *args, **kwargs):
return self.get_redirect_url()

def get_edit_form(self, obj):
form = EditNoteForm(obj=obj)
return form

def get_department_id(self, obj):
return self.department_id

def get_edit_form(self, obj):
form = EditTextForm(obj=obj)
return form

def dispatch_request(self, *args, **kwargs):
if 'officer_id' in kwargs:
officer = Officer.query.get_or_404(kwargs['officer_id'])
self.officer_id = kwargs.pop('officer_id')
self.department_id = officer.department_id
return super(TextApi, self).dispatch_request(*args, **kwargs)


class NoteApi(TextApi):
model = Note
model_name = 'note'
form = TextForm
create_function = create_note

def dispatch_request(self, *args, **kwargs):
return super(NoteApi, self).dispatch_request(*args, **kwargs)


class DescriptionApi(TextApi):
model = Description
model_name = 'description'
form = TextForm
create_function = create_description

def dispatch_request(self, *args, **kwargs):
return super(DescriptionApi, self).dispatch_request(*args, **kwargs)


note_view = NoteApi.as_view('note_api')
main.add_url_rule(
'/officer/<int:officer_id>/note/new',
Expand All @@ -824,3 +912,21 @@ def dispatch_request(self, *args, **kwargs):
'/officer/<int:officer_id>/note/<int:obj_id>/delete',
view_func=note_view,
methods=['GET', 'POST'])

description_view = DescriptionApi.as_view('description_api')
main.add_url_rule(
'/officer/<int:officer_id>/description/new',
view_func=description_view,
methods=['GET', 'POST'])
main.add_url_rule(
'/officer/<int:officer_id>/description/<int:obj_id>',
view_func=description_view,
methods=['GET'])
main.add_url_rule(
'/officer/<int:officer_id>/description/<int:obj_id>/edit',
view_func=description_view,
methods=['GET', 'POST'])
main.add_url_rule(
'/officer/<int:officer_id>/description/<int:obj_id>/delete',
view_func=description_view,
methods=['GET', 'POST'])
16 changes: 15 additions & 1 deletion OpenOversight/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class Note(db.Model):
__tablename__ = 'notes'

id = db.Column(db.Integer, primary_key=True)
note = db.Column(db.Text())
text_contents = db.Column(db.Text())
creator_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='SET NULL'))
creator = db.relationship('User', backref='notes')
officer_id = db.Column(db.Integer, db.ForeignKey('officers.id', ondelete='CASCADE'))
Expand All @@ -46,6 +46,19 @@ class Note(db.Model):
date_updated = db.Column(db.DateTime)


class Description(db.Model):
__tablename__ = 'descriptions'

creator = db.relationship('User', backref='descriptions')
officer = db.relationship('Officer', back_populates='descriptions')
id = db.Column(db.Integer, primary_key=True)
text_contents = db.Column(db.Text())
creator_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='SET NULL'))
officer_id = db.Column(db.Integer, db.ForeignKey('officers.id', ondelete='CASCADE'))
date_created = db.Column(db.DateTime)
date_updated = db.Column(db.DateTime)


class Officer(db.Model):
__tablename__ = 'officers'

Expand All @@ -70,6 +83,7 @@ class Officer(db.Model):
lazy='subquery',
backref=db.backref('officers', lazy=True))
notes = db.relationship('Note', back_populates='officer', order_by='Note.date_created')
descriptions = db.relationship('Description', back_populates='officer', order_by='Description.date_created')

def full_name(self):
if self.middle_initial:
Expand Down
7 changes: 7 additions & 0 deletions OpenOversight/app/templates/add_officer.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ <h4><small><a href="{{ url_for( 'main.add_unit' )}}">Don't see your unit? Add on
{% endfor %}
<button class="btn btn-success js-add-another-button" disabled>Add another link</button>
</div>
<div>
<legend>{{ form.descriptions.label }}</legend>
{% for subform in form.descriptions %}
{% include "partials/subform.html" %}
{% endfor %}
<button class="btn btn-success js-add-another-button" disabled>Add another link</button>
</div>
{{ wtf.form_field(form.submit, id="submit", button_map={'submit':'primary'}) }}
</form>
<br>
Expand Down
14 changes: 10 additions & 4 deletions OpenOversight/app/templates/browse.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,20 @@ <h2>{{ department.name }}
</h2>
<p>
<a class="btn btn-lg btn-primary" href="{{ url_for('main.list_officer', department_id=department.id) }}">
Officers
Browse Officers
</a>
{% if department.incidents %}
<a class="btn btn-lg btn-primary" href="{{ url_for('main.incident_api', department_id=department.id) }}">
Incidents
Browse Incidents
</a>
</p>
{% endif %}
<a class="btn btn-lg btn-primary" href="{{ url_for('main.download_incidents_csv', department_id=department.id) }}">
Download Incident Data
</a>
{% endif %}
<a class="btn btn-lg btn-primary" href="{{ url_for('main.download_dept_csv', department_id=department.id) }}">
Download Officer Data
</a>
</p>
</div>
</p>
{% endfor %}
Expand Down
22 changes: 22 additions & 0 deletions OpenOversight/app/templates/description_delete.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{% extends "base.html" %}

{% block content %}
<div class="container theme-showcase" role="main">

<div class="page-header">
<h1>
Delete Description of officer {{ obj.officer_id }}
</h1>
<p>
{{ obj.description }}
</p>
</div>
<p class="lead">
Are you sure you want to delete this description?
This cannot be undone.
<form action="{{ '{}/delete'.format(url_for('main.description_api', obj_id=obj.id, officer_id=obj.officer_id)) }}" method="post">
<button class='btn btn-danger' type="submit">Delete</button>
</form>
</p>
</div>
{% endblock content %}
18 changes: 18 additions & 0 deletions OpenOversight/app/templates/description_edit.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{% extends "form.html" %}
{% import "bootstrap/wtf.html" as wtf %}


{% block page_title %}
Update Description
{% endblock page_title %}


{% block form %}
{% if form.errors %}
{% set post_url=url_for('main.description_api', officer_id=obj.officer_id, obj_id=obj.id) %}
{% else %}
{% set post_url="{}/edit".format(url_for('main.description_api', officer_id=obj.officer_id, obj_id=obj.id)) %}
{% endif %}
{{ wtf.quick_form(form, action=post_url, method='post', button_map={'submit':'primary'}) }}
<br>
{% endblock form %}
14 changes: 14 additions & 0 deletions OpenOversight/app/templates/description_new.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{% extends 'form.html' %}

{% block page_title %}
New Description
{% endblock page_title %}

{% block form %}
<p>For officer with OOID {{ form.officer_id.data }}.<br>{{form.description}}</p>
{{ super() }}
{% endblock form %}

{% block js_footer %}
<script src="{{ url_for('static', filename='js/incidentAddButtons.js') }}"></script>
{% endblock %}
2 changes: 1 addition & 1 deletion OpenOversight/app/templates/note_new.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
{% endblock page_title %}

{% block form %}
<p>For officer with OOID {{ form.officer_id.data }}</p>
<p>For officer with OOID {{ form.officer_id.data }}.<br>{{form.description}}</p>
{{ super() }}
{% endblock form %}

Expand Down
Loading

0 comments on commit b0d244e

Please sign in to comment.