From 8c3e208d17666d9718102b4731946a650e9f56ee Mon Sep 17 00:00:00 2001 From: Michael Plunkett <5885605+michplunkett@users.noreply.github.com> Date: Thu, 3 Aug 2023 14:20:06 -0500 Subject: [PATCH] Add `created_at` and `created_by` columns (#1002) https://github.com/lucyparsons/OpenOversight/issues/928 Add `created_by` and `created_at` columns to tables so that we can better audit things happening in the application. - [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.
DB migration output ```console $ flask db stamp head [2023-08-01 18:18:34,782] INFO in __init__: OpenOversight startup INFO [alembic.runtime.migration] Context impl PostgresqlImpl. INFO [alembic.runtime.migration] Will assume transactional DDL. INFO [alembic.runtime.migration] Running stamp_revision -> 18f43ac4622f $ flask db upgrade [2023-08-01 18:18:49,052] INFO in __init__: OpenOversight startup INFO [alembic.runtime.migration] Context impl PostgresqlImpl. INFO [alembic.runtime.migration] Will assume transactional DDL. INFO [alembic.runtime.migration] Running upgrade 18f43ac4622f -> b429700e2dd2, add created_by and created_at columns $ flask db downgrade [2023-08-01 18:18:54,540] INFO in __init__: OpenOversight startup INFO [alembic.runtime.migration] Context impl PostgresqlImpl. INFO [alembic.runtime.migration] Will assume transactional DDL. INFO [alembic.runtime.migration] Running downgrade b429700e2dd2 -> 18f43ac4622f, add created_by and created_at columns $ flask db upgrade [2023-08-01 18:19:04,407] INFO in __init__: OpenOversight startup INFO [alembic.runtime.migration] Context impl PostgresqlImpl. INFO [alembic.runtime.migration] Will assume transactional DDL. INFO [alembic.runtime.migration] Running upgrade 18f43ac4622f -> b429700e2dd2, add created_by and created_at columns $ ```
--- OpenOversight/app/csv_imports.py | 8 +- OpenOversight/app/main/downloads.py | 4 +- OpenOversight/app/main/forms.py | 67 +- OpenOversight/app/main/model_view.py | 29 +- OpenOversight/app/main/views.py | 44 +- OpenOversight/app/models/config.py | 3 +- OpenOversight/app/models/database.py | 191 +++++- OpenOversight/app/models/database_imports.py | 31 +- OpenOversight/app/models/emails.py | 12 +- OpenOversight/app/utils/cloud.py | 2 +- OpenOversight/app/utils/constants.py | 6 + OpenOversight/app/utils/db.py | 12 +- OpenOversight/app/utils/forms.py | 15 +- ...c_add_created_by_and_created_at_columns.py | 629 ++++++++++++++++++ OpenOversight/tests/conftest.py | 220 +++--- OpenOversight/tests/routes/route_helpers.py | 23 +- OpenOversight/tests/routes/test_auth.py | 85 +-- .../tests/routes/test_descriptions.py | 61 +- .../tests/routes/test_image_tagging.py | 15 + OpenOversight/tests/routes/test_incidents.py | 123 ++-- OpenOversight/tests/routes/test_notes.py | 46 +- .../routes/test_officer_and_department.py | 166 +++-- OpenOversight/tests/routes/test_user_api.py | 76 +-- OpenOversight/tests/test_commands.py | 69 +- OpenOversight/tests/test_csvs/incidents.csv | 2 +- OpenOversight/tests/test_csvs/links.csv | 2 +- OpenOversight/tests/test_models.py | 4 +- docs/advanced_csv_import.rst | 8 +- 28 files changed, 1512 insertions(+), 441 deletions(-) create mode 100644 OpenOversight/migrations/versions/2023-08-01-1905_b38c133bed3c_add_created_by_and_created_at_columns.py diff --git a/OpenOversight/app/csv_imports.py b/OpenOversight/app/csv_imports.py index 7fc86a828..8b365dcc1 100644 --- a/OpenOversight/app/csv_imports.py +++ b/OpenOversight/app/csv_imports.py @@ -234,7 +234,7 @@ def _handle_assignments_csv( csv_reader = rows else: existing_assignments = ( - Assignment.query.join(Assignment.baseofficer) + Assignment.query.join(Assignment.base_officer) .filter(Officer.department_id == department_id) .all() ) @@ -376,8 +376,8 @@ def _handle_incidents_csv( "city", "state", "zip_code", - "creator_id", - "last_updated_id", + "created_by", + "last_updated_by", "officer_ids", "license_plates", ], @@ -442,7 +442,7 @@ def _handle_links_csv( "link_type", "description", "author", - "creator_id", + "created_by", "officer_ids", "incident_ids", ], diff --git a/OpenOversight/app/main/downloads.py b/OpenOversight/app/main/downloads.py index 90f0a3a70..bc94eb084 100644 --- a/OpenOversight/app/main/downloads.py +++ b/OpenOversight/app/main/downloads.py @@ -114,7 +114,7 @@ def officer_record_maker(officer: Officer) -> _Record: def assignment_record_maker(assignment: Assignment) -> _Record: - officer = assignment.baseofficer + officer = assignment.base_officer return { "id": assignment.id, "officer id": assignment.officer_id, @@ -159,7 +159,7 @@ def descriptions_record_maker(description: Description) -> _Record: return { "id": description.id, "text_contents": description.text_contents, - "creator_id": description.creator_id, + "created_by": description.created_by, "officer_id": description.officer_id, "created_at": description.created_at, "updated_at": description.updated_at, diff --git a/OpenOversight/app/main/forms.py b/OpenOversight/app/main/forms.py index 91b6ae759..18103156a 100644 --- a/OpenOversight/app/main/forms.py +++ b/OpenOversight/app/main/forms.py @@ -140,6 +140,11 @@ class FaceTag(Form): dataWidth = IntegerField("dataWidth", validators=[InputRequired()]) dataHeight = IntegerField("dataHeight", validators=[InputRequired()]) officer_id = IntegerField("officer_id") + created_by = HiddenField( + validators=[ + DataRequired(message="Face Tags must have a valid user ID for creating.") + ] + ) class AssignmentForm(Form): @@ -164,6 +169,11 @@ class AssignmentForm(Form): resign_date = DateField( "Assignment end date", validators=[Optional(), validate_end_date] ) + created_by = HiddenField( + validators=[ + DataRequired(message="Assignments must have a valid user ID for creating.") + ] + ) class SalaryForm(Form): @@ -179,6 +189,11 @@ class SalaryForm(Form): validators=[NumberRange(min=1900, max=2100)], ) is_fiscal_year = BooleanField("Is fiscal year?", default=False) + created_by = HiddenField( + validators=[ + DataRequired(message="Salaries must have a valid user ID for creating.") + ] + ) def validate(form, extra_validators=None): if not form.data.get("salary") and not form.data.get("overtime_pay"): @@ -209,6 +224,11 @@ class DepartmentForm(Form): jobs = FieldList( StringField("Job", default="", validators=[Regexp(r"\w*")]), label="Ranks" ) + created_by = HiddenField( + validators=[ + DataRequired(message="Departments must have a valid user ID for creating.") + ] + ) submit = SubmitField(label="Add") @@ -238,7 +258,11 @@ class LinkForm(Form): default="", validators=[AnyOf(allowed_values(LINK_CHOICES))], ) - creator_id = HiddenField(validators=[DataRequired(message="Not a valid user ID")]) + created_by = HiddenField( + validators=[ + DataRequired(message="Links must have a valid user ID for creating.") + ] + ) def validate(self, extra_validators=None): success = super(LinkForm, self).validate(extra_validators=extra_validators) @@ -272,7 +296,11 @@ class TextForm(EditTextForm): officer_id = HiddenField( validators=[DataRequired(message="Not a valid officer ID")] ) - creator_id = HiddenField(validators=[DataRequired(message="Not a valid user ID")]) + created_by = HiddenField( + validators=[ + DataRequired(message="Text fields must have a valid user ID for creating.") + ] + ) class AddOfficerForm(Form): @@ -358,6 +386,11 @@ class AddOfficerForm(Form): min_entries=1, widget=BootstrapListWidget(), ) + created_by = HiddenField( + validators=[ + DataRequired(message="Officers must have a valid user ID for creating.") + ] + ) submit = SubmitField(label="Add") @@ -419,6 +452,11 @@ class AddUnitForm(Form): query_factory=dept_choices, get_label="name", ) + created_by = HiddenField( + validators=[ + DataRequired(message="Units must have a valid user ID for creating.") + ] + ) submit = SubmitField(label="Add") @@ -469,6 +507,11 @@ class LocationForm(Form): Regexp(r"^\d{5}$", message="Zip codes must have 5 digits."), ], ) + created_by = HiddenField( + validators=[ + DataRequired(message="Locations must have a valid user ID for creating.") + ] + ) class LicensePlateForm(Form): @@ -478,6 +521,13 @@ class LicensePlateForm(Form): choices=STATE_CHOICES, validators=[AnyOf(allowed_values(STATE_CHOICES))], ) + created_by = HiddenField( + validators=[ + DataRequired( + message="License Plates must have a valid user ID for creating." + ) + ] + ) def validate_state(self, field): if self.number.data != "" and field.data == "": @@ -547,11 +597,16 @@ class IncidentForm(DateFieldForm): min_entries=1, widget=BootstrapListWidget(), ) - creator_id = HiddenField( - validators=[DataRequired(message="Incidents must have a creator id.")] + created_by = HiddenField( + validators=[DataRequired(message="Incidents must have a user id for creating.")] ) - last_updated_id = HiddenField( - validators=[DataRequired(message="Incidents must have a user id for editing.")] + last_updated_by = HiddenField( + validators=[DataRequired(message="Incidents must have a user ID for editing.")] + ) + last_updated_at = HiddenField( + validators=[ + DataRequired(message="Incidents must have a timestamp for editing.") + ] ) submit = SubmitField(label="Submit") diff --git a/OpenOversight/app/main/model_view.py b/OpenOversight/app/main/model_view.py index 15b351d4f..02a64842f 100644 --- a/OpenOversight/app/main/model_view.py +++ b/OpenOversight/app/main/model_view.py @@ -63,10 +63,12 @@ def new(self, form=None): add_department_query(form, current_user) if getattr(current_user, "dept_pref_rel", None): set_dynamic_default(form.department, current_user.dept_pref_rel) - if hasattr(form, "creator_id") and not form.creator_id.data: - form.creator_id.data = current_user.get_id() - if hasattr(form, "last_updated_id"): - form.last_updated_id.data = current_user.get_id() + if hasattr(form, "created_by") and not form.created_by.data: + form.created_by.data = current_user.get_id() + # TODO: Determine whether creating counts as updating, seems redundant + if hasattr(form, "last_updated_by"): + form.last_updated_by.data = current_user.get_id() + form.last_updated_at.data = datetime.datetime.now() if form.validate_on_submit(): new_obj = self.create_function(form) @@ -92,19 +94,20 @@ def edit(self, obj_id, form=None): if not form: form = self.get_edit_form(obj) - # if the object doesn't have a creator id set, st it to current user + # if the object doesn't have a creator id set it to current user if ( - hasattr(obj, "creator_id") - and hasattr(form, "creator_id") - and getattr(obj, "creator_id") + hasattr(obj, "created_by") + and hasattr(form, "created_by") + and getattr(obj, "created_by") ): - form.creator_id.data = obj.creator_id - elif hasattr(form, "creator_id"): - form.creator_id.data = current_user.get_id() + form.created_by.data = obj.created_by + elif hasattr(form, "created_by"): + form.created_by.data = current_user.get_id() # if the object keeps track of who updated it last, set to current user - if hasattr(form, "last_updated_id"): - form.last_updated_id.data = current_user.get_id() + if hasattr(obj, "last_updated_by") and hasattr(form, "last_updated_by"): + form.last_updated_by.data = current_user.get_id() + form.last_updated_at.data = datetime.datetime.now() if hasattr(form, "department"): add_department_query(form, current_user) diff --git a/OpenOversight/app/main/views.py b/OpenOversight/app/main/views.py index 7bf627d6d..d4151f1da 100644 --- a/OpenOversight/app/main/views.py +++ b/OpenOversight/app/main/views.py @@ -321,6 +321,7 @@ def sitemap_officers(): @ac_or_admin_required def add_assignment(officer_id): form = AssignmentForm() + form.created_by.data = current_user.get_id() officer = Officer.query.filter_by(id=officer_id).first() form.job_title.query = ( Job.query.filter_by(department_id=officer.department_id) @@ -399,6 +400,7 @@ def edit_assignment(officer_id, assignment_id): @ac_or_admin_required def add_salary(officer_id): form = SalaryForm() + form.created_by.data = current_user.get_id() officer = Officer.query.filter_by(id=officer_id).first() if not officer: flash("Officer not found") @@ -495,7 +497,7 @@ def classify_submission(image_id, contains_cops): if image.contains_cops is not None and not current_user.is_administrator: flash("Only administrator can re-classify image") return redirect(redirect_url()) - image.user_id = current_user.get_id() + image.created_by = current_user.get_id() if contains_cops == 1: image.contains_cops = True elif contains_cops == 0: @@ -516,6 +518,7 @@ def classify_submission(image_id, contains_cops): @admin_required def add_department(): form = DepartmentForm() + form.created_by = current_user.get_id() if form.validate_on_submit(): department_does_not_exist = ( Department.query.filter_by( @@ -529,6 +532,7 @@ def add_department(): name=form.name.data, short_name=form.short_name.data, state=form.state.data, + created_by=current_user.get_id(), ) db.session.add(department) db.session.flush() @@ -575,6 +579,7 @@ def edit_department(department_id): previous_name = department.name form = EditDepartmentForm(obj=department) original_ranks = department.jobs + form.created_by.data = department.created_by if form.validate_on_submit(): if form.name.data != previous_name: does_already_department_exist = ( @@ -895,10 +900,10 @@ def get_dept_units(department_id=None): @login_required @ac_or_admin_required def add_officer(): - jsloads = ["js/dynamic_lists.js", "js/add_officer.js"] form = AddOfficerForm() + form.created_by.data = current_user.get_id() for link in form.links: - link.creator_id.data = current_user.id + link.created_by.data = current_user.id add_unit_query(form, current_user) add_department_query(form, current_user) set_dynamic_default(form.department, current_user.dept_pref_rel) @@ -921,14 +926,17 @@ def add_officer(): return redirect(url_for("main.submit_officer_images", officer_id=officer.id)) else: current_app.logger.info(form.errors) - return render_template("add_officer.html", form=form, jsloads=jsloads) + return render_template( + "add_officer.html", + form=form, + jsloads=["js/dynamic_lists.js", "js/add_officer.js"], + ) @main.route("/officer//edit", methods=[HTTPMethod.GET, HTTPMethod.POST]) @login_required @ac_or_admin_required def edit_officer(officer_id): - jsloads = ["js/dynamic_lists.js"] officer = Officer.query.filter_by(id=officer_id).one() form = EditOfficerForm(obj=officer) @@ -950,7 +958,9 @@ def edit_officer(officer_id): return redirect(url_for("main.officer_profile", officer_id=officer.id)) else: current_app.logger.info(form.errors) - return render_template("edit_officer.html", form=form, jsloads=jsloads) + return render_template( + "edit_officer.html", form=form, jsloads=["js/dynamic_lists.js"] + ) @main.route("/unit/new", methods=[HTTPMethod.GET, HTTPMethod.POST]) @@ -1148,7 +1158,7 @@ def label_data(department_id=None, image_id=None): face_position_y=upper, face_width=form.dataWidth.data, face_height=form.dataHeight.data, - user_id=current_user.get_id(), + created_by=current_user.get_id(), ) db.session.add(new_tag) db.session.commit() @@ -1264,9 +1274,9 @@ def download_dept_officers_csv(department_id): def download_dept_assignments_csv(department_id): assignments = ( db.session.query(Assignment) - .join(Assignment.baseofficer) + .join(Assignment.base_officer) .filter(Officer.department_id == department_id) - .options(contains_eager(Assignment.baseofficer)) + .options(contains_eager(Assignment.base_officer)) .options(joinedload(Assignment.unit)) .options(joinedload(Assignment.job)) ) @@ -1384,7 +1394,7 @@ def download_dept_descriptions_csv(department_id): field_names = [ "id", "text_contents", - "creator_id", + "created_by", "officer_id", "created_at", "updated_at", @@ -1458,7 +1468,7 @@ def upload(department_id, officer_id=None): # we set both images to the uploaded one img_id=image.id, original_image_id=image.id, - user_id=current_user.get_id(), + created_by=current_user.get_id(), ) db.session.add(face) db.session.commit() @@ -1565,7 +1575,7 @@ def get_new_form(self): form.officers[0].oo_id.data = request.args.get("officer_id") for link in form.links: - link.creator_id.data = current_user.id + link.created_by.data = current_user.id return form def get_edit_form(self, obj): @@ -1575,10 +1585,10 @@ def get_edit_form(self, obj): no_links = len(obj.links) no_officers = len(obj.officers) for link in form.links: - if link.creator_id.data: + if link.created_by.data: continue else: - link.creator_id.data = current_user.id + link.created_by.data = current_user.id for officer_idx, officer in enumerate(obj.officers): form.officers[officer_idx].oo_id.data = officer.id @@ -1795,8 +1805,8 @@ def new(self, form=None): abort(HTTPStatus.FORBIDDEN) if not form: form = self.get_new_form() - if hasattr(form, "creator_id") and not form.creator_id.data: - form.creator_id.data = current_user.get_id() + if hasattr(form, "created_by") and not form.created_by.data: + form.created_by.data = current_user.get_id() if form.validate_on_submit(): link = Link( @@ -1805,7 +1815,7 @@ def new(self, form=None): link_type=form.link_type.data, description=form.description.data, author=form.author.data, - creator_id=form.creator_id.data, + created_by=form.created_by.data, ) self.officer.links.append(link) db.session.add(link) diff --git a/OpenOversight/app/models/config.py b/OpenOversight/app/models/config.py index 04c81e7f5..9b1bcd316 100644 --- a/OpenOversight/app/models/config.py +++ b/OpenOversight/app/models/config.py @@ -8,6 +8,7 @@ KEY_MAIL_USERNAME, KEY_OFFICERS_PER_PAGE, KEY_OO_HELP_EMAIL, + KEY_OO_MAIL_SUBJECT_PREFIX, KEY_TIMEZONE, MEGABYTE, ) @@ -46,7 +47,7 @@ def __init__(self): # Mail Settings self.OO_MAIL_SUBJECT_PREFIX = os.environ.get( - "OO_MAIL_SUBJECT_PREFIX", "[OpenOversight]" + KEY_OO_MAIL_SUBJECT_PREFIX, "[OpenOversight]" ) self.OO_SERVICE_EMAIL = os.environ.get("OO_SERVICE_EMAIL") # TODO: Remove the default once we are able to update the production .env file diff --git a/OpenOversight/app/models/database.py b/OpenOversight/app/models/database.py index 6f36108d8..972f266b3 100644 --- a/OpenOversight/app/models/database.py +++ b/OpenOversight/app/models/database.py @@ -33,6 +33,13 @@ "officer_links", db.Column("officer_id", db.Integer, db.ForeignKey("officers.id"), primary_key=True), db.Column("link_id", db.Integer, db.ForeignKey("links.id"), primary_key=True), + db.Column( + "created_at", + db.DateTime(timezone=True), + nullable=False, + server_default=sql_func.now(), + unique=False, + ), ) officer_incidents = db.Table( @@ -41,9 +48,19 @@ db.Column( "incident_id", db.Integer, db.ForeignKey("incidents.id"), primary_key=True ), + db.Column( + "created_at", + db.DateTime(timezone=True), + nullable=False, + server_default=sql_func.now(), + unique=False, + ), ) +# This is a last recently used cache that also utilizes a time-to-live function for each +# value saved in it (12 hours). +# TODO: Change this into a singleton so that we can clear values when updates happen date_updated_cache = TTLCache(maxsize=1024, ttl=12 * 60 * 60) @@ -71,6 +88,15 @@ class Department(BaseModel): name = db.Column(db.String(255), index=False, unique=False, nullable=False) short_name = db.Column(db.String(100), unique=False, nullable=False) state = db.Column(db.String(2), server_default="", nullable=False) + created_at = db.Column( + db.DateTime(timezone=True), + nullable=False, + server_default=sql_func.now(), + unique=False, + ) + created_by = db.Column( + db.Integer, db.ForeignKey("users.id", ondelete="SET NULL"), unique=False + ) # See https://github.com/lucyparsons/OpenOversight/issues/462 unique_internal_identifier_label = db.Column( @@ -130,6 +156,15 @@ class Job(BaseModel): order = db.Column(db.Integer, index=True, unique=False, nullable=False) department_id = db.Column(db.Integer, db.ForeignKey("departments.id")) department = db.relationship("Department", backref="jobs") + created_at = db.Column( + db.DateTime(timezone=True), + nullable=False, + server_default=sql_func.now(), + unique=False, + ) + created_by = db.Column( + db.Integer, db.ForeignKey("users.id", ondelete="SET NULL"), unique=False + ) __table_args__ = ( UniqueConstraint( @@ -149,7 +184,9 @@ class Note(BaseModel): 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")) + created_by = db.Column( + db.Integer, db.ForeignKey("users.id", ondelete="SET NULL"), unique=False + ) creator = db.relationship("User", backref="notes") officer_id = db.Column(db.Integer, db.ForeignKey("officers.id", ondelete="CASCADE")) officer = db.relationship("Officer", back_populates="notes") @@ -169,7 +206,9 @@ class Description(BaseModel): 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")) + created_by = db.Column( + db.Integer, db.ForeignKey("users.id", ondelete="SET NULL"), unique=False + ) officer_id = db.Column(db.Integer, db.ForeignKey("officers.id", ondelete="CASCADE")) created_at = db.Column( db.DateTime(timezone=True), @@ -200,10 +239,18 @@ class Officer(BaseModel): unique_internal_identifier = db.Column( db.String(50), index=True, unique=True, nullable=True ) - date_created = db.Column(db.DateTime, default=func.now()) date_updated = db.Column( db.DateTime, default=func.now(), onupdate=func.now(), index=True ) + created_at = db.Column( + db.DateTime(timezone=True), + nullable=False, + server_default=sql_func.now(), + unique=False, + ) + created_by = db.Column( + db.Integer, db.ForeignKey("users.id", ondelete="SET NULL"), unique=False + ) links = db.relationship( "Link", secondary=officer_links, backref=db.backref("officers", lazy=True) @@ -330,6 +377,15 @@ class Salary(BaseModel): overtime_pay = db.Column(Currency(), index=True, unique=False, nullable=True) year = db.Column(db.Integer, index=True, unique=False, nullable=False) is_fiscal_year = db.Column(db.Boolean, index=False, unique=False, nullable=False) + created_at = db.Column( + db.DateTime(timezone=True), + nullable=False, + server_default=sql_func.now(), + unique=False, + ) + created_by = db.Column( + db.Integer, db.ForeignKey("users.id", ondelete="SET NULL"), unique=False + ) def __repr__(self): return f"" @@ -366,6 +430,15 @@ class Unit(BaseModel): department = db.relationship( "Department", backref="unit_types", order_by="Unit.description.asc()" ) + created_at = db.Column( + db.DateTime(timezone=True), + nullable=False, + server_default=sql_func.now(), + unique=False, + ) + created_by = db.Column( + db.Integer, db.ForeignKey("users.id", ondelete="SET NULL"), unique=False + ) def __repr__(self): return f"Unit: {self.description}" @@ -404,11 +477,17 @@ class Face(BaseModel): original_image = db.relationship( "Image", backref="tags", foreign_keys=[original_image_id], lazy=True ) - user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True) + created_by = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True) user = db.relationship("User", backref="faces") featured = db.Column( db.Boolean, nullable=False, default=False, server_default="false" ) + created_at = db.Column( + db.DateTime(timezone=True), + nullable=False, + server_default=sql_func.now(), + unique=False, + ) __table_args__ = (UniqueConstraint("officer_id", "img_id", name="unique_faces"),) @@ -423,7 +502,6 @@ class Image(BaseModel): filepath = db.Column(db.String(255), unique=False) hash_img = db.Column(db.String(120), unique=False, nullable=True) - # Track when the image was put into our database created_at = db.Column( db.DateTime(timezone=True), index=True, @@ -436,7 +514,12 @@ class Image(BaseModel): db.DateTime(timezone=True), index=True, unique=False, nullable=True ) contains_cops = db.Column(db.Boolean, nullable=True) - user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True) + created_by = db.Column( + db.Integer, + db.ForeignKey("users.id", ondelete="SET NULL"), + nullable=True, + unique=False, + ) user = db.relationship("User", backref="raw_images") is_tagged = db.Column(db.Boolean, default=False, unique=False, nullable=True) @@ -454,6 +537,13 @@ def __repr__(self): "incident_id", db.Integer, db.ForeignKey("incidents.id"), primary_key=True ), db.Column("link_id", db.Integer, db.ForeignKey("links.id"), primary_key=True), + db.Column( + "created_at", + db.DateTime(timezone=True), + nullable=False, + server_default=sql_func.now(), + unique=False, + ), ) incident_license_plates = db.Table( @@ -467,6 +557,13 @@ def __repr__(self): db.ForeignKey("license_plates.id"), primary_key=True, ), + db.Column( + "created_at", + db.DateTime(timezone=True), + nullable=False, + server_default=sql_func.now(), + unique=False, + ), ) incident_officers = db.Table( @@ -477,6 +574,13 @@ def __repr__(self): db.Column( "officers_id", db.Integer, db.ForeignKey("officers.id"), primary_key=True ), + db.Column( + "created_at", + db.DateTime(timezone=True), + nullable=False, + server_default=sql_func.now(), + unique=False, + ), ) @@ -490,6 +594,18 @@ class Location(BaseModel): city = db.Column(db.String(100), unique=False, index=True) state = db.Column(db.String(2), unique=False, index=True) zip_code = db.Column(db.String(5), unique=False, index=True) + created_at = db.Column( + db.DateTime(timezone=True), + nullable=False, + server_default=sql_func.now(), + unique=False, + ) + created_by = db.Column( + db.Integer, + db.ForeignKey("users.id", ondelete="SET NULL"), + nullable=True, + unique=False, + ) @validates("zip_code") def validate_zip_code(self, key, zip_code): @@ -529,6 +645,18 @@ class LicensePlate(BaseModel): id = db.Column(db.Integer, primary_key=True) number = db.Column(db.String(8), nullable=False, index=True) state = db.Column(db.String(2), index=True) + created_at = db.Column( + db.DateTime(timezone=True), + nullable=False, + server_default=sql_func.now(), + unique=False, + ) + created_by = db.Column( + db.Integer, + db.ForeignKey("users.id", ondelete="SET NULL"), + nullable=True, + unique=False, + ) # for use if car is federal, diplomat, or other non-state # non_state_identifier = db.Column(db.String(20), index=True) @@ -547,8 +675,19 @@ class Link(BaseModel): link_type = db.Column(db.String(100), index=True) description = db.Column(db.Text(), nullable=True) author = db.Column(db.String(255), nullable=True) - creator_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="SET NULL")) + created_by = db.Column( + db.Integer, + db.ForeignKey("users.id", ondelete="SET NULL"), + nullable=True, + unique=False, + ) creator = db.relationship("User", backref="links", lazy=True) + created_at = db.Column( + db.DateTime(timezone=True), + nullable=False, + server_default=sql_func.now(), + unique=False, + ) @validates("url") def validate_url(self, key, url): @@ -585,13 +724,31 @@ class Incident(BaseModel): ) department_id = db.Column(db.Integer, db.ForeignKey("departments.id")) department = db.relationship("Department", backref="incidents", lazy=True) - creator_id = db.Column(db.Integer, db.ForeignKey("users.id")) + created_at = db.Column( + db.DateTime(timezone=True), + nullable=False, + server_default=sql_func.now(), + unique=False, + ) + created_by = db.Column( + db.Integer, + db.ForeignKey("users.id", ondelete="SET NULL"), + nullable=True, + unique=False, + ) creator = db.relationship( - "User", backref="incidents_created", lazy=True, foreign_keys=[creator_id] + "User", backref="incidents_created", lazy=True, foreign_keys=[created_by] + ) + last_updated_by = db.Column( + db.Integer, + db.ForeignKey("users.id", ondelete="SET NULL"), + nullable=True, + unique=False, ) - last_updated_id = db.Column(db.Integer, db.ForeignKey("users.id")) - last_updated_by = db.relationship( - "User", backref="incidents_updated", lazy=True, foreign_keys=[last_updated_id] + last_updated_at = db.Column( + db.DateTime(timezone=True), + nullable=True, + unique=False, ) date_created = db.Column(db.DateTime, default=func.now()) date_updated = db.Column( @@ -618,6 +775,12 @@ class User(UserMixin, BaseModel): dept_pref_rel = db.relationship("Department", foreign_keys=[dept_pref]) classifications = db.relationship("Image", backref="users") tags = db.relationship("Face", backref="users") + created_at = db.Column( + db.DateTime(timezone=True), + nullable=False, + server_default=sql_func.now(), + unique=False, + ) def _jwt_encode(self, payload, expiration): secret = current_app.config["SECRET_KEY"] diff --git a/OpenOversight/app/models/database_imports.py b/OpenOversight/app/models/database_imports.py index ee576591a..832ae52a9 100644 --- a/OpenOversight/app/models/database_imports.py +++ b/OpenOversight/app/models/database_imports.py @@ -1,5 +1,6 @@ +import datetime from decimal import Decimal -from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Tuple, Union +from typing import Any, Dict, Optional, Sequence, Tuple, Union import dateutil.parser @@ -25,10 +26,6 @@ from OpenOversight.app.validators import state_validator, url_validator -if TYPE_CHECKING: - import datetime - - def validate_choice( value: Optional[str], given_choices: Sequence[Tuple[str, str]] ) -> Optional[str]: @@ -40,13 +37,13 @@ def validate_choice( return None -def parse_date(date_str: Optional[str]) -> Optional["datetime.date"]: +def parse_date(date_str: Optional[str]) -> Optional[datetime.date]: if date_str: return dateutil.parser.parse(date_str).date() return None -def parse_time(time_str: Optional[str]) -> Optional["datetime.time"]: +def parse_time(time_str: Optional[str]) -> Optional[datetime.time]: if time_str: return dateutil.parser.parse(time_str).time() return None @@ -202,7 +199,7 @@ def create_link_from_dict(data: Dict[str, Any], force_id: bool = False) -> Link: link_type=validate_choice(data.get("link_type"), LINK_CHOICES), description=parse_str(data.get("description"), None), author=parse_str(data.get("author"), None), - creator_id=parse_int(data.get("creator_id")), + created_by=parse_int(data.get("created_by")), ) if force_id and data.get("id"): @@ -227,8 +224,8 @@ def update_link_from_dict(data: Dict[str, Any], link: Link) -> Link: link.description = parse_str(data.get("description"), None) if "author" in data: link.author = parse_str(data.get("author"), None) - if "creator_id" in data: - link.creator_id = parse_int(data.get("creator_id")) + if "created_by" in data: + link.created_by = parse_int(data.get("created_by")) if "officers" in data: link.officers = data.get("officers") or [] if "incidents" in data: @@ -286,8 +283,9 @@ def create_incident_from_dict(data: Dict[str, Any], force_id: bool = False) -> I description=parse_str(data.get("description"), None), address_id=data.get("address_id"), department_id=parse_int(data.get("department_id")), - creator_id=parse_int(data.get("creator_id")), - last_updated_id=parse_int(data.get("last_updated_id")), + created_by=parse_int(data.get("created_by")), + last_updated_by=parse_int(data.get("last_updated_by")), + last_updated_at=datetime.datetime.now(), ) incident.officers = data.get("officers", []) @@ -314,10 +312,11 @@ def update_incident_from_dict(data: Dict[str, Any], incident: Incident) -> Incid incident.address_id = data.get("address_id") if "department_id" in data: incident.department_id = parse_int(data.get("department_id")) - if "creator_id" in data: - incident.creator_id = parse_int(data.get("creator_id")) - if "last_updated_id" in data: - incident.last_updated_id = parse_int(data.get("last_updated_id")) + if "created_by" in data: + incident.created_by = parse_int(data.get("created_by")) + if "last_updated_by" in data: + incident.last_updated_by = parse_int(data.get("last_updated_by")) + incident.last_updated_at = datetime.datetime.now() if "officers" in data: incident.officers = data["officers"] or [] if "license_plate_objects" in data: diff --git a/OpenOversight/app/models/emails.py b/OpenOversight/app/models/emails.py index 552d1b7b0..4d5dd4149 100644 --- a/OpenOversight/app/models/emails.py +++ b/OpenOversight/app/models/emails.py @@ -38,7 +38,9 @@ def create_message(self): class AdministratorApprovalEmail(Email): def __init__(self, receiver: str, user, admin): - subject = f"{current_app.config['OO_MAIL_SUBJECT_PREFIX']} New User Registered" + subject = ( + f"{current_app.config[KEY_OO_MAIL_SUBJECT_PREFIX]} New User Registered" + ) body = render_template( f"{self.EMAIL_PATH}new_registration.txt", user=user, admin=admin ) @@ -51,7 +53,7 @@ def __init__(self, receiver: str, user, admin): class ChangeEmailAddressEmail(Email): def __init__(self, receiver: str, user, token: str): subject = ( - f"{current_app.config['OO_MAIL_SUBJECT_PREFIX']} Confirm Your Email " + f"{current_app.config[KEY_OO_MAIL_SUBJECT_PREFIX]} Confirm Your Email " f"Address" ) body = render_template( @@ -65,9 +67,7 @@ def __init__(self, receiver: str, user, token: str): class ChangePasswordEmail(Email): def __init__(self, receiver: str, user): - subject = ( - f"{current_app.config['OO_MAIL_SUBJECT_PREFIX']} Your Password Has Changed" - ) + subject = f"{current_app.config[KEY_OO_MAIL_SUBJECT_PREFIX]} Your Password Has Changed" body = render_template( f"{self.EMAIL_PATH}change_password.txt", user=user, @@ -93,7 +93,7 @@ def __init__(self, receiver: str, user, token: str): class ConfirmedUserEmail(Email): def __init__(self, receiver: str, user, admin): - subject = f"{current_app.config['OO_MAIL_SUBJECT_PREFIX']} New User Confirmed" + subject = f"{current_app.config[KEY_OO_MAIL_SUBJECT_PREFIX]} New User Confirmed" body = render_template( f"{self.EMAIL_PATH}new_confirmation.txt", user=user, admin=admin ) diff --git a/OpenOversight/app/utils/cloud.py b/OpenOversight/app/utils/cloud.py index 721c65572..a012f59ad 100644 --- a/OpenOversight/app/utils/cloud.py +++ b/OpenOversight/app/utils/cloud.py @@ -155,7 +155,7 @@ def upload_image_to_s3_and_store_in_db(image_buf, user_id, department_id=None): created_at=datetime.datetime.now(), department_id=department_id, taken_at=date_taken, - user_id=user_id, + created_by=user_id, ) db.session.add(new_image) db.session.commit() diff --git a/OpenOversight/app/utils/constants.py b/OpenOversight/app/utils/constants.py index 2c481f135..440618f7b 100644 --- a/OpenOversight/app/utils/constants.py +++ b/OpenOversight/app/utils/constants.py @@ -52,3 +52,9 @@ BYTE = 1 KILOBYTE = 1024 * BYTE MEGABYTE = 1024 * KILOBYTE +MINUTE = 60 +HOUR = 60 * MINUTE + +# Test Constants +ADMIN_EMAIL = "test@example.org" +ADMIN_PASSWORD = "testtest" diff --git a/OpenOversight/app/utils/db.py b/OpenOversight/app/utils/db.py index 87c2402bb..febc96590 100644 --- a/OpenOversight/app/utils/db.py +++ b/OpenOversight/app/utils/db.py @@ -33,20 +33,20 @@ def add_unit_query(form, current_user): def compute_leaderboard_stats(select_top=25): top_sorters = ( - db.session.query(User, func.count(Image.user_id)) + db.session.query(User, func.count(Image.created_by)) .select_from(Image) .join(User) .group_by(User) - .order_by(func.count(Image.user_id).desc()) + .order_by(func.count(Image.created_by).desc()) .limit(select_top) .all() ) top_taggers = ( - db.session.query(User, func.count(Face.user_id)) + db.session.query(User, func.count(Face.created_by)) .select_from(Face) .join(User) .group_by(User) - .order_by(func.count(Face.user_id).desc()) + .order_by(func.count(Face.created_by).desc()) .limit(select_top) .all() ) @@ -72,8 +72,8 @@ def get_officer(department_id, star_no, first_name, last_name): else: star_no = str(star_no) for assignment in Assignment.query.filter_by(star_no=star_no).all(): - if assignment.baseofficer in officers: - return assignment.baseofficer + if assignment.base_officer in officers: + return assignment.base_officer return None diff --git a/OpenOversight/app/utils/forms.py b/OpenOversight/app/utils/forms.py index b99e43577..821c30bc3 100644 --- a/OpenOversight/app/utils/forms.py +++ b/OpenOversight/app/utils/forms.py @@ -67,7 +67,7 @@ def add_officer_profile(form, current_user): officer_unit = None assignment = Assignment( - baseofficer=officer, + base_officer=officer, star_no=form.star_no.data, job_id=form.job_id.data, unit=officer_unit, @@ -125,7 +125,7 @@ def add_officer_profile(form, current_user): def create_description(self, form): return Description( text_contents=form.text_contents.data, - creator_id=form.creator_id.data, + created_by=form.created_by.data, officer_id=form.officer_id.data, created_at=datetime.datetime.now(), updated_at=datetime.datetime.now(), @@ -140,8 +140,8 @@ def create_incident(self, form): "license_plates": [], "links": [], "address": "", - "creator_id": form.creator_id.data, - "last_updated_id": form.last_updated_id.data, + "created_by": form.created_by.data, + "last_updated_by": form.last_updated_by.data, } if "address" in form.data: @@ -180,15 +180,16 @@ def create_incident(self, form): report_number=form.data["report_number"], license_plates=fields["license_plates"], links=fields["links"], - creator_id=fields["creator_id"], - last_updated_id=fields["last_updated_id"], + created_by=fields["created_by"], + last_updated_by=fields["last_updated_by"], + last_updated_at=datetime.datetime.now(), ) def create_note(self, form): return Note( text_contents=form.text_contents.data, - creator_id=form.creator_id.data, + created_by=form.created_by.data, officer_id=form.officer_id.data, created_at=datetime.datetime.now(), updated_at=datetime.datetime.now(), diff --git a/OpenOversight/migrations/versions/2023-08-01-1905_b38c133bed3c_add_created_by_and_created_at_columns.py b/OpenOversight/migrations/versions/2023-08-01-1905_b38c133bed3c_add_created_by_and_created_at_columns.py new file mode 100644 index 000000000..ac38861d5 --- /dev/null +++ b/OpenOversight/migrations/versions/2023-08-01-1905_b38c133bed3c_add_created_by_and_created_at_columns.py @@ -0,0 +1,629 @@ +"""add created_by and created_at columns + +Revision ID: b38c133bed3c +Revises: 18f43ac4622f +Create Date: 2023-08-01 19:05:34.745077 + +""" +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision = "b38c133bed3c" +down_revision = "18f43ac4622f" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("assignments", schema=None) as batch_op: + batch_op.add_column( + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ) + ) + batch_op.add_column(sa.Column("created_by", sa.Integer(), nullable=True)) + batch_op.create_foreign_key( + "assignments_created_by_fkey", + "users", + ["created_by"], + ["id"], + ondelete="SET NULL", + ) + + op.execute( + """ + UPDATE assignments + SET created_at=date_created + WHERE created_at IS NULL + """ + ) + + with op.batch_alter_table("assignments", schema=None) as batch_op: + batch_op.drop_column("date_created") + + with op.batch_alter_table("departments", schema=None) as batch_op: + batch_op.add_column( + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ) + ) + batch_op.add_column(sa.Column("created_by", sa.Integer(), nullable=True)) + batch_op.create_foreign_key( + "departments_created_by_fkey", + "users", + ["created_by"], + ["id"], + ondelete="SET NULL", + ) + + with op.batch_alter_table("descriptions", schema=None) as batch_op: + batch_op.add_column(sa.Column("created_by", sa.Integer(), nullable=True)) + batch_op.drop_constraint("descriptions_creator_id_fkey", type_="foreignkey") + batch_op.create_foreign_key( + "descriptions_created_by_fkey", + "users", + ["created_by"], + ["id"], + ondelete="SET NULL", + ) + + op.execute( + """ + UPDATE descriptions + SET created_by = creator_id + WHERE created_by IS NULL + """ + ) + + with op.batch_alter_table("descriptions", schema=None) as batch_op: + batch_op.drop_column("creator_id") + + with op.batch_alter_table("faces", schema=None) as batch_op: + batch_op.add_column(sa.Column("created_by", sa.Integer(), nullable=True)) + batch_op.add_column( + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ) + ) + batch_op.drop_constraint("faces_user_id_fkey", type_="foreignkey") + batch_op.create_foreign_key( + "faces_created_by_fkey", "users", ["created_by"], ["id"] + ) + + op.execute( + """ + UPDATE faces + SET created_by = user_id + WHERE created_by IS NULL + """ + ) + + with op.batch_alter_table("faces", schema=None) as batch_op: + batch_op.drop_column("user_id") + + with op.batch_alter_table("incident_license_plates", schema=None) as batch_op: + batch_op.add_column( + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ) + ) + + with op.batch_alter_table("incident_links", schema=None) as batch_op: + batch_op.add_column( + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ) + ) + + with op.batch_alter_table("incident_officers", schema=None) as batch_op: + batch_op.add_column( + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ) + ) + + with op.batch_alter_table("incidents", schema=None) as batch_op: + batch_op.add_column(sa.Column("created_by", sa.Integer(), nullable=True)) + batch_op.add_column( + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ) + ) + batch_op.create_foreign_key( + "incidents_created_by_fkey", + "users", + ["created_by"], + ["id"], + ondelete="SET NULL", + ) + + op.execute( + """ + UPDATE incidents + SET created_at=date_created + WHERE created_at IS NULL + """ + ) + op.execute( + """ + UPDATE incidents + SET created_by = creator_id + WHERE created_by IS NULL + """ + ) + + with op.batch_alter_table("incidents", schema=None) as batch_op: + batch_op.drop_constraint("incidents_creator_id_fkey", type_="foreignkey") + batch_op.drop_column("creator_id") + batch_op.drop_column("date_created") + + with op.batch_alter_table("jobs", schema=None) as batch_op: + batch_op.add_column( + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ) + ) + batch_op.add_column(sa.Column("created_by", sa.Integer(), nullable=True)) + batch_op.create_foreign_key( + "jobs_created_by_fkey", "users", ["created_by"], ["id"], ondelete="SET NULL" + ) + + with op.batch_alter_table("license_plates", schema=None) as batch_op: + batch_op.add_column( + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ) + ) + batch_op.add_column(sa.Column("created_by", sa.Integer(), nullable=True)) + batch_op.create_foreign_key( + "license_plates_created_by_fkey", + "users", + ["created_by"], + ["id"], + ondelete="SET NULL", + ) + + with op.batch_alter_table("links", schema=None) as batch_op: + batch_op.add_column(sa.Column("created_by", sa.Integer(), nullable=True)) + batch_op.add_column( + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ) + ) + batch_op.drop_constraint("links_creator_id_fkey", type_="foreignkey") + batch_op.create_foreign_key( + "links_created_by_fkey", + "users", + ["created_by"], + ["id"], + ondelete="SET NULL", + ) + + op.execute( + """ + UPDATE links + SET created_by = creator_id + WHERE created_by IS NULL + """ + ) + + with op.batch_alter_table("links", schema=None) as batch_op: + batch_op.drop_column("creator_id") + + with op.batch_alter_table("locations", schema=None) as batch_op: + batch_op.add_column( + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ) + ) + batch_op.add_column(sa.Column("created_by", sa.Integer(), nullable=True)) + batch_op.create_foreign_key( + "locations_created_by_fkey", + "users", + ["created_by"], + ["id"], + ondelete="SET NULL", + ) + + with op.batch_alter_table("notes", schema=None) as batch_op: + batch_op.add_column(sa.Column("created_by", sa.Integer(), nullable=True)) + batch_op.drop_constraint("notes_creator_id_fkey", type_="foreignkey") + batch_op.create_foreign_key( + "notes_created_by_fkey", + "users", + ["created_by"], + ["id"], + ondelete="SET NULL", + ) + + op.execute( + """ + UPDATE notes + SET created_by = creator_id + WHERE created_by IS NULL + """ + ) + + with op.batch_alter_table("notes", schema=None) as batch_op: + batch_op.drop_column("creator_id") + + with op.batch_alter_table("officer_incidents", schema=None) as batch_op: + batch_op.add_column( + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ) + ) + + with op.batch_alter_table("officer_links", schema=None) as batch_op: + batch_op.add_column( + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ) + ) + + with op.batch_alter_table("officers", schema=None) as batch_op: + batch_op.add_column( + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ) + ) + batch_op.add_column(sa.Column("created_by", sa.Integer(), nullable=True)) + batch_op.create_foreign_key( + "officers_created_by_fkey", + "users", + ["created_by"], + ["id"], + ondelete="SET NULL", + ) + + op.execute( + """ + UPDATE officers + SET created_at=date_created + WHERE created_at IS NULL + """ + ) + + with op.batch_alter_table("officers", schema=None) as batch_op: + op.drop_column("officers", "date_created") + + with op.batch_alter_table("raw_images", schema=None) as batch_op: + batch_op.add_column(sa.Column("created_by", sa.Integer(), nullable=True)) + batch_op.drop_constraint("raw_images_user_id_fkey", type_="foreignkey") + batch_op.create_foreign_key( + "raw_images_created_by_fkey", + "users", + ["created_by"], + ["id"], + ondelete="SET NULL", + ) + + op.execute( + """ + UPDATE raw_images + SET created_by = user_id + WHERE created_by IS NULL + """ + ) + + with op.batch_alter_table("raw_images", schema=None) as batch_op: + batch_op.drop_column("user_id") + + with op.batch_alter_table("salaries", schema=None) as batch_op: + batch_op.add_column( + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ) + ) + batch_op.add_column(sa.Column("created_by", sa.Integer(), nullable=True)) + batch_op.create_foreign_key( + "salaries_created_by_fkey", + "users", + ["created_by"], + ["id"], + ondelete="SET NULL", + ) + + with op.batch_alter_table("unit_types", schema=None) as batch_op: + batch_op.add_column( + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ) + ) + batch_op.add_column(sa.Column("created_by", sa.Integer(), nullable=True)) + batch_op.create_foreign_key( + "unit_types_created_by_fkey", + "users", + ["created_by"], + ["id"], + ondelete="SET NULL", + ) + + with op.batch_alter_table("users", schema=None) as batch_op: + batch_op.add_column( + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ) + ) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("users", schema=None) as batch_op: + batch_op.drop_column("created_at") + + with op.batch_alter_table("unit_types", schema=None) as batch_op: + batch_op.drop_constraint("unit_types_created_by_fkey", type_="foreignkey") + batch_op.drop_column("created_by") + batch_op.drop_column("created_at") + + with op.batch_alter_table("salaries", schema=None) as batch_op: + batch_op.drop_constraint("salaries_created_by_fkey", type_="foreignkey") + batch_op.drop_column("created_by") + batch_op.drop_column("created_at") + + with op.batch_alter_table("raw_images", schema=None) as batch_op: + batch_op.add_column( + sa.Column("user_id", sa.INTEGER(), autoincrement=False, nullable=True) + ) + batch_op.drop_constraint("raw_images_created_by_fkey", type_="foreignkey") + batch_op.create_foreign_key( + "raw_images_user_id_fkey", "users", ["user_id"], ["id"] + ) + + op.execute( + """ + UPDATE raw_images + SET user_id = created_by + WHERE user_id IS NULL + """ + ) + + with op.batch_alter_table("raw_images", schema=None) as batch_op: + batch_op.drop_column("created_by") + + with op.batch_alter_table("officers", schema=None) as batch_op: + batch_op.add_column(sa.Column("date_created", sa.DateTime(), nullable=True)) + + op.execute( + """ + UPDATE officers + SET date_created=created_at + WHERE date_created IS NULL + """ + ) + + with op.batch_alter_table("officers", schema=None) as batch_op: + batch_op.drop_constraint("officers_created_by_fkey", type_="foreignkey") + batch_op.drop_column("created_by") + batch_op.drop_column("created_at") + + with op.batch_alter_table("officer_links", schema=None) as batch_op: + batch_op.drop_column("created_at") + + with op.batch_alter_table("officer_incidents", schema=None) as batch_op: + batch_op.drop_column("created_at") + + with op.batch_alter_table("notes", schema=None) as batch_op: + batch_op.add_column( + sa.Column("creator_id", sa.INTEGER(), autoincrement=False, nullable=True) + ) + batch_op.drop_constraint("notes_created_by_fkey", type_="foreignkey") + batch_op.create_foreign_key( + "notes_creator_id_fkey", + "users", + ["creator_id"], + ["id"], + ondelete="SET NULL", + ) + + op.execute( + """ + UPDATE notes + SET creator_id = created_by + WHERE creator_id IS NULL + """ + ) + + with op.batch_alter_table("notes", schema=None) as batch_op: + batch_op.drop_column("created_by") + + with op.batch_alter_table("locations", schema=None) as batch_op: + batch_op.drop_constraint("locations_created_by_fkey", type_="foreignkey") + batch_op.drop_column("created_by") + batch_op.drop_column("created_at") + + with op.batch_alter_table("links", schema=None) as batch_op: + batch_op.add_column( + sa.Column("creator_id", sa.INTEGER(), autoincrement=False, nullable=True) + ) + batch_op.drop_constraint("links_created_by_fkey", type_="foreignkey") + batch_op.create_foreign_key( + "links_creator_id_fkey", + "users", + ["creator_id"], + ["id"], + ondelete="SET NULL", + ) + + op.execute( + """ + UPDATE links + SET creator_id = created_by + WHERE creator_id IS NULL + """ + ) + + with op.batch_alter_table("links", schema=None) as batch_op: + batch_op.drop_column("created_at") + batch_op.drop_column("created_by") + + with op.batch_alter_table("license_plates", schema=None) as batch_op: + batch_op.drop_constraint("license_plates_created_by_fkey", type_="foreignkey") + batch_op.drop_column("created_by") + batch_op.drop_column("created_at") + + with op.batch_alter_table("jobs", schema=None) as batch_op: + batch_op.drop_constraint("jobs_created_by_fkey", type_="foreignkey") + batch_op.drop_column("created_by") + batch_op.drop_column("created_at") + + with op.batch_alter_table("incidents", schema=None) as batch_op: + batch_op.add_column( + sa.Column("creator_id", sa.INTEGER(), autoincrement=False, nullable=True) + ) + batch_op.create_foreign_key( + "incidents_creator_id_fkey", "users", ["creator_id"], ["id"] + ) + batch_op.add_column(sa.Column("date_created", sa.DateTime(), nullable=True)) + + op.execute( + """ + UPDATE incidents + SET creator_id = created_by + WHERE creator_id IS NULL + """ + ) + op.execute( + """ + UPDATE incidents + SET date_created=created_at + WHERE date_created IS NULL + """ + ) + + with op.batch_alter_table("incidents", schema=None) as batch_op: + batch_op.drop_constraint("incidents_created_by_fkey", type_="foreignkey") + batch_op.drop_column("created_at") + batch_op.drop_column("created_by") + + with op.batch_alter_table("incident_officers", schema=None) as batch_op: + batch_op.drop_column("created_at") + + with op.batch_alter_table("incident_links", schema=None) as batch_op: + batch_op.drop_column("created_at") + + with op.batch_alter_table("incident_license_plates", schema=None) as batch_op: + batch_op.drop_column("created_at") + + with op.batch_alter_table("faces", schema=None) as batch_op: + batch_op.add_column( + sa.Column("user_id", sa.INTEGER(), autoincrement=False, nullable=True) + ) + batch_op.drop_constraint("faces_created_by_fkey", type_="foreignkey") + batch_op.create_foreign_key("faces_user_id_fkey", "users", ["user_id"], ["id"]) + + op.execute( + """ + UPDATE faces + SET user_id = created_by + WHERE user_id IS NULL + """ + ) + + with op.batch_alter_table("faces", schema=None) as batch_op: + batch_op.drop_column("created_at") + batch_op.drop_column("created_by") + + with op.batch_alter_table("descriptions", schema=None) as batch_op: + batch_op.add_column( + sa.Column("creator_id", sa.INTEGER(), autoincrement=False, nullable=True) + ) + batch_op.drop_constraint("descriptions_created_by_fkey", type_="foreignkey") + batch_op.create_foreign_key( + "descriptions_creator_id_fkey", + "users", + ["creator_id"], + ["id"], + ondelete="SET NULL", + ) + + op.execute( + """ + UPDATE descriptions + SET creator_id = created_by + WHERE creator_id IS NULL + """ + ) + + with op.batch_alter_table("descriptions", schema=None) as batch_op: + batch_op.drop_column("created_by") + + with op.batch_alter_table("departments", schema=None) as batch_op: + batch_op.drop_constraint("departments_created_by_fkey", type_="foreignkey") + batch_op.drop_column("created_by") + batch_op.drop_column("created_at") + + with op.batch_alter_table("assignments", schema=None) as batch_op: + batch_op.add_column(sa.Column("date_created", sa.DateTime(), nullable=True)) + + op.execute( + """ + UPDATE assignments + SET date_created=created_at + WHERE date_created IS NULL + """ + ) + + with op.batch_alter_table("assignments", schema=None) as batch_op: + batch_op.drop_constraint("assignments_created_by_fkey", type_="foreignkey") + batch_op.drop_column("created_by") + batch_op.drop_column("created_at") + + # ### end Alembic commands ### diff --git a/OpenOversight/tests/conftest.py b/OpenOversight/tests/conftest.py index 4b28ccec1..7692a91a5 100644 --- a/OpenOversight/tests/conftest.py +++ b/OpenOversight/tests/conftest.py @@ -3,7 +3,6 @@ import math import os import random -import string import sys import threading import uuid @@ -43,14 +42,22 @@ ) from OpenOversight.app.models.database import db as _db from OpenOversight.app.utils.choices import DEPARTMENT_STATE_CHOICES -from OpenOversight.app.utils.constants import ENCODING_UTF_8 +from OpenOversight.app.utils.constants import ( + ADMIN_EMAIL, + ADMIN_PASSWORD, + ENCODING_UTF_8, + KEY_NUM_OFFICERS, +) from OpenOversight.app.utils.general import merge_dicts -from OpenOversight.tests.routes.route_helpers import ADMIN_EMAIL, ADMIN_PASSWORD factory = Faker() +def pick_uid(): + return str(uuid.uuid4()) + + class PoliceDepartment: """Base Police Department class.""" @@ -61,7 +68,7 @@ def __init__(self, name, short_name, state="", unique_internal_identifier_label= self.unique_internal_identifier_label = ( unique_internal_identifier_label if unique_internal_identifier_label - else "".join(random.choices(string.ascii_uppercase + string.digits, k=20)) + else pick_uid() ) @@ -148,15 +155,11 @@ def pick_department(): return random.choice(departments) -def pick_uid(): - return str(uuid.uuid4()) - - def pick_salary(): return Decimal(random.randint(100, 100000000)) / 100 -def generate_officer(department): +def generate_officer(department: Department, user: User) -> Officer: year_born = pick_birth_date() f_name, m_initial, l_name = pick_name() return Officer( @@ -169,10 +172,13 @@ def generate_officer(department): employment_date=datetime.datetime(year_born + 20, 4, 4, 1, 1, 1), department_id=department.id, unique_internal_identifier=pick_uid(), + created_by=user.id, ) -def build_assignment(officer: Officer, units: List[Optional[Unit]], jobs: Job): +def build_assignment( + officer: Officer, units: List[Optional[Unit]], jobs: Job, user: User +) -> Assignment: unit = random.choice(units) unit_id = unit.id if unit else None return Assignment( @@ -182,46 +188,48 @@ def build_assignment(officer: Officer, units: List[Optional[Unit]], jobs: Job): unit_id=unit_id, start_date=pick_date(officer.full_name().encode(ENCODING_UTF_8)), resign_date=pick_date(officer.full_name().encode(ENCODING_UTF_8)), + created_by=user.id, ) -def build_note(officer, user, content=None): +def build_note(officer: Officer, user: User, content=None) -> Note: date = factory.date_time_this_year() if content is None: content = factory.text() return Note( text_contents=content, officer_id=officer.id, - creator_id=user.id, + created_by=user.id, created_at=date, updated_at=date, ) -def build_description(officer, user, content=None): +def build_description(officer: Officer, user: User, content=None) -> Description: date = factory.date_time_this_year() if content is None: content = factory.text() return Description( text_contents=content, officer_id=officer.id, - creator_id=user.id, + created_by=user.id, created_at=date, updated_at=date, ) -def build_salary(officer): +def build_salary(officer: Officer, user: User) -> Salary: return Salary( officer_id=officer.id, salary=pick_salary(), overtime_pay=pick_salary(), year=random.randint(2000, 2019), is_fiscal_year=True if random.randint(0, 1) else False, + created_by=user.id, ) -def assign_faces(officer, images): +def assign_faces(officer: Officer, images: Image, user: User): if random.uniform(0, 1) >= 0.5: img_id = random.choice(images).id return Face( @@ -229,6 +237,7 @@ def assign_faces(officer, images): img_id=img_id, original_image_id=img_id, featured=False, + created_by=user.id, ) else: return False @@ -314,28 +323,82 @@ def test_csv_dir(): def add_mockdata(session): - assert current_app.config["NUM_OFFICERS"] >= 5 + assert current_app.config[KEY_NUM_OFFICERS] >= 5 + + test_user = User( + email="jen@example.org", username="test_user", password="dog", confirmed=True + ) + session.add(test_user) + + test_admin = User( + email=ADMIN_EMAIL, + username="test_admin", + password=ADMIN_PASSWORD, + confirmed=True, + is_administrator=True, + ) + session.add(test_admin) + + test_unconfirmed_user = User( + email="freddy@example.org", username="b_meson", password="dog", confirmed=False + ) + session.add(test_unconfirmed_user) + session.commit() + + test_disabled_user = User( + email="may@example.org", + username="may", + password="yam", + confirmed=True, + is_disabled=True, + ) + session.add(test_disabled_user) + session.commit() + + test_modified_disabled_user = User( + email="sam@example.org", + username="sam", + password="the yam", + confirmed=True, + is_disabled=True, + ) + session.add(test_modified_disabled_user) + session.commit() + department = Department( name=SPRINGFIELD_PD.name, short_name=SPRINGFIELD_PD.short_name, state=SPRINGFIELD_PD.state, unique_internal_identifier_label=SPRINGFIELD_PD.unique_internal_identifier_label, + created_by=test_admin.id, ) session.add(department) department2 = Department( name=OTHER_PD.name, short_name=OTHER_PD.short_name, state=OTHER_PD.state, + created_by=test_admin.id, ) session.add(department2) empty_department = Department( name=NO_OFFICER_PD.name, short_name=NO_OFFICER_PD.short_name, state=NO_OFFICER_PD.state, + created_by=test_admin.id, ) session.add(empty_department) session.commit() + test_area_coordinator = User( + email="raq929@example.org", + username="test_ac", + password="horse", + confirmed=True, + is_area_coordinator=True, + ac_department_id=AC_DEPT, + ) + session.add(test_area_coordinator) + for i, rank in enumerate(RANK_CHOICES_1): session.add( Job( @@ -343,6 +406,7 @@ def add_mockdata(session): order=i, is_sworn_officer=True, department_id=department.id, + created_by=test_admin.id, ) ) session.add( @@ -351,6 +415,7 @@ def add_mockdata(session): order=i, is_sworn_officer=True, department_id=empty_department.id, + created_by=test_admin.id, ) ) @@ -361,6 +426,7 @@ def add_mockdata(session): order=i, is_sworn_officer=True, department_id=department2.id, + created_by=test_admin.id, ) ) session.commit() @@ -369,11 +435,19 @@ def add_mockdata(session): random.seed(current_app.config["SEED"]) test_units = [ - Unit(description="test", department_id=1), - Unit(description="District 13", department_id=1), - Unit(description="Donut Devourers", department_id=1), - Unit(description="Bureau of Organized Crime", department_id=2), - Unit(description="Porky's BBQ: Rub Division", department_id=2), + Unit(description="test", department_id=1, created_by=test_admin.id), + Unit(description="District 13", department_id=1, created_by=test_admin.id), + Unit(description="Donut Devourers", department_id=1, created_by=test_admin.id), + Unit( + description="Bureau of Organized Crime", + department_id=2, + created_by=test_admin.id, + ), + Unit( + description="Porky's BBQ: Rub Division", + department_id=2, + created_by=test_admin.id, + ), ] session.add_all(test_units) session.commit() @@ -383,12 +457,14 @@ def add_mockdata(session): Image( filepath=f"/static/images/test_cop{x + 1}.png", department_id=department.id, + created_by=test_admin.id, ) for x in range(5) ] + [ Image( filepath=f"/static/images/test_cop{x + 1}.png", department_id=department2.id, + created_by=test_admin.id, ) for x in range(5) ] @@ -400,19 +476,22 @@ def add_mockdata(session): link_type="link", title="OpenOversight", description="A public, searchable database of law enforcement officers.", + created_by=test_admin.id, ), Link( url="http://www.youtube.com/?v=help", link_type="video", title="Youtube", author="the internet", + created_by=test_admin.id, ), ] officers = [] for d in [department, department2]: officers += [ - generate_officer(d) for _ in range(current_app.config["NUM_OFFICERS"]) + generate_officer(d, test_admin) + for _ in range(current_app.config[KEY_NUM_OFFICERS]) ] officers[0].links = test_officer_links session.add_all(officers) @@ -434,21 +513,23 @@ def add_mockdata(session): assignment_ratio = 0.9 # 90% num_officers_with_assignments_1 = math.ceil(len(officers_dept1) * assignment_ratio) assignments_dept1 = [ - build_assignment(officer, test_units, jobs_dept1) + build_assignment(officer, test_units, jobs_dept1, test_admin) for officer in officers_dept1[:num_officers_with_assignments_1] ] num_officers_with_assignments_2 = math.ceil(len(officers_dept2) * assignment_ratio) assignments_dept2 = [ - build_assignment(officer, test_units, jobs_dept2) + build_assignment(officer, test_units, jobs_dept2, test_admin) for officer in officers_dept2[:num_officers_with_assignments_2] ] - salaries = [build_salary(officer) for officer in all_officers] + salaries = [build_salary(officer, test_admin) for officer in all_officers] faces_dept1 = [ - assign_faces(officer, assigned_images_dept1) for officer in officers_dept1 + assign_faces(officer, assigned_images_dept1, test_admin) + for officer in officers_dept1 ] faces_dept2 = [ - assign_faces(officer, assigned_images_dept2) for officer in officers_dept2 + assign_faces(officer, assigned_images_dept2, test_admin) + for officer in officers_dept2 ] faces1 = [f for f in faces_dept1 if f] faces2 = [f for f in faces_dept2 if f] @@ -459,56 +540,6 @@ def add_mockdata(session): session.add_all(faces1) session.add_all(faces2) - test_user = User( - email="jen@example.org", username="test_user", password="dog", confirmed=True - ) - session.add(test_user) - - test_admin = User( - email=ADMIN_EMAIL, - username="test_admin", - password=ADMIN_PASSWORD, - confirmed=True, - is_administrator=True, - ) - session.add(test_admin) - - test_area_coordinator = User( - email="raq929@example.org", - username="test_ac", - password="horse", - confirmed=True, - is_area_coordinator=True, - ac_department_id=AC_DEPT, - ) - session.add(test_area_coordinator) - - test_unconfirmed_user = User( - email="freddy@example.org", username="b_meson", password="dog", confirmed=False - ) - session.add(test_unconfirmed_user) - session.commit() - - test_disabled_user = User( - email="may@example.org", - username="may", - password="yam", - confirmed=True, - is_disabled=True, - ) - session.add(test_disabled_user) - session.commit() - - test_modified_disabled_user = User( - email="sam@example.org", - username="sam", - password="the yam", - confirmed=True, - is_disabled=True, - ) - session.add(test_modified_disabled_user) - session.commit() - test_addresses = [ Location( street_name="Test St", @@ -517,6 +548,7 @@ def add_mockdata(session): city="My City", state="AZ", zip_code="23456", + created_by=test_admin.id, ), Location( street_name="Testing St", @@ -525,6 +557,7 @@ def add_mockdata(session): city="Another City", state="ME", zip_code="23456", + created_by=test_admin.id, ), ] @@ -532,8 +565,8 @@ def add_mockdata(session): session.commit() test_license_plates = [ - LicensePlate(number="603EEE", state="MA"), - LicensePlate(number="404301", state="WA"), + LicensePlate(number="603EEE", state="MA", created_by=test_admin.id), + LicensePlate(number="404301", state="WA", created_by=test_admin.id), ] session.add_all(test_license_plates) @@ -544,13 +577,13 @@ def add_mockdata(session): url="https://stackoverflow.com/", link_type="link", creator=test_admin, - creator_id=test_admin.id, + created_by=test_admin.id, ), Link( url="http://www.youtube.com/?v=help", link_type="video", creator=test_admin, - creator_id=test_admin.id, + created_by=test_admin.id, ), ] @@ -568,8 +601,9 @@ def add_mockdata(session): license_plates=test_license_plates, links=test_incident_links, officers=[all_officers[o] for o in range(4)], - creator_id=1, - last_updated_id=1, + created_by=test_admin.id, + last_updated_by=test_admin.id, + last_updated_at=datetime.datetime.now(), ), Incident( date=datetime.date(2017, 12, 11), @@ -581,8 +615,9 @@ def add_mockdata(session): license_plates=[test_license_plates[0]], links=test_incident_links, officers=[all_officers[o] for o in range(3)], - creator_id=2, - last_updated_id=1, + created_by=test_admin.id, + last_updated_by=test_admin.id, + last_updated_at=datetime.datetime.now(), ), Incident( date=datetime.datetime(2019, 1, 15), @@ -595,8 +630,9 @@ def add_mockdata(session): license_plates=[test_license_plates[0]], links=test_incident_links, officers=[all_officers[o] for o in range(1)], - creator_id=2, - last_updated_id=1, + created_by=test_admin.id, + last_updated_by=test_admin.id, + last_updated_at=datetime.datetime.now(), ), ] session.add_all(test_incidents) @@ -707,11 +743,11 @@ def teardown(): officers_dept1 = Officer.query.filter_by(department_id=1).all() if sys.version_info.major == 2: - csvf = open(str(csv_path), "w") + csv_file = open(str(csv_path), "w") else: - csvf = open(str(csv_path), "w", newline="") + csv_file = open(str(csv_path), "w", newline="") try: - writer = csv.DictWriter(csvf, fieldnames=fieldnames, extrasaction="ignore") + writer = csv.DictWriter(csv_file, fieldnames=fieldnames, extrasaction="ignore") writer.writeheader() for officer in officers_dept1: if not officer.unique_internal_identifier: @@ -746,7 +782,7 @@ def teardown(): except: # noqa: E722 raise finally: - csvf.close() + csv_file.close() request.addfinalizer(teardown) return str(csv_path) diff --git a/OpenOversight/tests/routes/route_helpers.py b/OpenOversight/tests/routes/route_helpers.py index b6f6f08be..5069dea27 100644 --- a/OpenOversight/tests/routes/route_helpers.py +++ b/OpenOversight/tests/routes/route_helpers.py @@ -1,20 +1,21 @@ from flask import url_for from OpenOversight.app.auth.forms import LoginForm - - -ADMIN_EMAIL = "test@example.org" -ADMIN_PASSWORD = "testtest" +from OpenOversight.app.models.database import User +from OpenOversight.app.utils.constants import ADMIN_PASSWORD +from OpenOversight.tests.conftest import AC_DEPT def login_user(client): - form = LoginForm(email="jen@example.org", password="dog", remember_me=True) + user = User.query.filter_by(id=1).first() + form = LoginForm(email=user.email, password="dog", remember_me=True) rv = client.post(url_for("auth.login"), data=form.data, follow_redirects=False) return rv def login_unconfirmed_user(client): - form = LoginForm(email="freddy@example.org", password="dog", remember_me=True) + user = User.query.filter_by(confirmed=False).first() + form = LoginForm(email=user.email, password="dog", remember_me=True) rv = client.post(url_for("auth.login"), data=form.data, follow_redirects=False) assert b"Invalid username or password" not in rv.data return rv @@ -33,21 +34,21 @@ def login_modified_disabled_user(client): def login_admin(client): - form = LoginForm(email=ADMIN_EMAIL, password=ADMIN_PASSWORD, remember_me=True) + user = User.query.filter_by(is_administrator=True).first() + form = LoginForm(email=user.email, password=ADMIN_PASSWORD, remember_me=True) rv = client.post(url_for("auth.login"), data=form.data, follow_redirects=False) return rv def login_ac(client): - form = LoginForm(email="raq929@example.org", password="horse", remember_me=True) + user = User.query.filter_by(ac_department_id=AC_DEPT).first() + form = LoginForm(email=user.email, password="horse", remember_me=True) rv = client.post(url_for("auth.login"), data=form.data, follow_redirects=False) return rv def process_form_data(form_dict): - """Takes the dict from a form with embedded formd and flattens it - - in the way that it is flattened in the browser""" + """Mock the browser-flattening of a form containing embedded data.""" new_dict = {} for key, value in form_dict.items(): if type(value) == list: diff --git a/OpenOversight/tests/routes/test_auth.py b/OpenOversight/tests/routes/test_auth.py index a6f5e0d82..9950ac40a 100644 --- a/OpenOversight/tests/routes/test_auth.py +++ b/OpenOversight/tests/routes/test_auth.py @@ -1,4 +1,3 @@ -# Routing and view tests from http import HTTPStatus from unittest import TestCase from urllib.parse import urlparse @@ -16,8 +15,9 @@ RegistrationForm, ) from OpenOversight.app.models.database import User - -from .route_helpers import ( +from OpenOversight.app.utils.constants import KEY_OO_MAIL_SUBJECT_PREFIX +from OpenOversight.tests.conftest import AC_DEPT +from OpenOversight.tests.routes.route_helpers import ( login_disabled_user, login_modified_disabled_user, login_unconfirmed_user, @@ -90,9 +90,10 @@ def test_user_can_logout(mockdata, client, session): def test_user_cannot_register_with_existing_email(mockdata, client, session): with current_app.test_request_context(): + user = User.query.filter_by(is_administrator=True).first() form = RegistrationForm( - email="jen@example.org", - username="redshiftzero", + email=user.email, + username=user.username, password="dog", password2="dog", ) @@ -109,9 +110,10 @@ def test_user_cannot_register_with_existing_email_differently_cased( mockdata, client, session ): with current_app.test_request_context(): + user = User.query.filter_by(is_administrator=True).first() form = RegistrationForm( - email="JEN@EXAMPLE.ORG", - username="redshiftzero", + email=user.email.upper(), + username=user.username, password="dog", password2="dog", ) @@ -126,9 +128,10 @@ def test_user_cannot_register_with_existing_email_differently_cased( def test_user_cannot_register_if_passwords_dont_match(mockdata, client, session): with current_app.test_request_context(): + user = User.query.filter_by(is_administrator=True).first() form = RegistrationForm( - email="freddy@example.org", - username="b_meson", + email=user.email, + username=user.username, password="dog", password2="cat", ) @@ -141,14 +144,14 @@ def test_user_cannot_register_if_passwords_dont_match(mockdata, client, session) assert b"Passwords must match" in rv.data -def test_user_can_register_with_legit_credentials(mockdata, client, session): +def test_user_can_register_with_legit_credentials(mockdata, client, session, faker): with current_app.test_request_context(), TestCase.assertLogs( current_app.logger ) as log: diceware_password = "operative hamster persevere verbalize curling" form = RegistrationForm( - email="jen@example.com", - username="redshiftzero", + email=faker.ascii_email(), + username="generic_username", password=diceware_password, password2=diceware_password, ) @@ -158,16 +161,17 @@ def test_user_can_register_with_legit_credentials(mockdata, client, session): assert b"A confirmation email has been sent to you." in rv.data assert ( - f"{current_app.config['OO_MAIL_SUBJECT_PREFIX']} Confirm Your Account" + f"{current_app.config[KEY_OO_MAIL_SUBJECT_PREFIX]} Confirm Your Account" in str(log.output) ) def test_user_cannot_register_with_weak_password(mockdata, client, session): with current_app.test_request_context(): + user = User.query.filter_by(is_administrator=True).first() form = RegistrationForm( - email="jen@example.com", - username="redshiftzero", + email=user.email, + username=user.username, password="weak", password2="weak", ) @@ -188,7 +192,7 @@ def test_user_can_get_a_confirmation_token_resent(mockdata, client, session): assert b"A new confirmation email has been sent to you." in rv.data assert ( - f"{current_app.config['OO_MAIL_SUBJECT_PREFIX']} Confirm Your Account" + f"{current_app.config[KEY_OO_MAIL_SUBJECT_PREFIX]} Confirm Your Account" in str(log.output) ) @@ -197,7 +201,8 @@ def test_user_can_get_password_reset_token_sent(mockdata, client, session): with current_app.test_request_context(), TestCase.assertLogs( current_app.logger ) as log: - form = PasswordResetRequestForm(email="jen@example.org") + user = User.query.filter_by(is_administrator=True).first() + form = PasswordResetRequestForm(email=user.email) rv = client.post( url_for("auth.password_reset_request"), @@ -207,7 +212,7 @@ def test_user_can_get_password_reset_token_sent(mockdata, client, session): assert b"An email with instructions to reset your password" in rv.data assert ( - f"{current_app.config['OO_MAIL_SUBJECT_PREFIX']} Reset Your Password" + f"{current_app.config[KEY_OO_MAIL_SUBJECT_PREFIX]} Reset Your Password" in str(log.output) ) @@ -218,7 +223,8 @@ def test_user_can_get_password_reset_token_sent_with_differently_cased_email( with current_app.test_request_context(), TestCase.assertLogs( current_app.logger ) as log: - form = PasswordResetRequestForm(email="JEN@EXAMPLE.ORG") + user = User.query.filter_by(is_administrator=True).first() + form = PasswordResetRequestForm(email=user.email.upper()) rv = client.post( url_for("auth.password_reset_request"), @@ -228,17 +234,17 @@ def test_user_can_get_password_reset_token_sent_with_differently_cased_email( assert b"An email with instructions to reset your password" in rv.data assert ( - f"{current_app.config['OO_MAIL_SUBJECT_PREFIX']} Reset Your Password" + f"{current_app.config[KEY_OO_MAIL_SUBJECT_PREFIX]} Reset Your Password" in str(log.output) ) def test_user_can_get_reset_password_with_valid_token(mockdata, client, session): with current_app.test_request_context(): + user = User.query.filter_by(is_administrator=True).first() form = PasswordResetForm( - email="jen@example.org", password="catdog", password2="catdog" + email=user.email, password="catdog", password2="catdog" ) - user = User.query.filter_by(email="jen@example.org").one() token = user.generate_reset_token() rv = client.post( @@ -254,10 +260,10 @@ def test_user_can_get_reset_password_with_valid_token_differently_cased( mockdata, client, session ): with current_app.test_request_context(): + user = User.query.filter_by(is_administrator=True).first() form = PasswordResetForm( - email="JEN@EXAMPLE.ORG", password="catdog", password2="catdog" + email=user.email.upper(), password="catdog", password2="catdog" ) - user = User.query.filter_by(email="jen@example.org").one() token = user.generate_reset_token() rv = client.post( @@ -271,8 +277,9 @@ def test_user_can_get_reset_password_with_valid_token_differently_cased( def test_user_cannot_reset_password_with_invalid_token(mockdata, client, session): with current_app.test_request_context(): + user = User.query.filter_by(is_administrator=True).first() form = PasswordResetForm( - email="jen@example.org", password="catdog", password2="catdog" + email=user.email, password="catdog", password2="catdog" ) token = "beepboopbeep" @@ -290,7 +297,8 @@ def test_user_cannot_get_email_reset_token_sent_without_valid_password( ): with current_app.test_request_context(): login_user(client) - form = ChangeEmailForm(email="jen@example.org", password="dogdogdogdog") + user = User.query.filter_by(is_administrator=True).first() + form = ChangeEmailForm(email=user.email, password="dogdogdogdog") rv = client.post( url_for("auth.change_email_request"), data=form.data, follow_redirects=True @@ -304,7 +312,8 @@ def test_user_cannot_get_email_reset_token_sent_to_existing_email( ): with current_app.test_request_context(): login_user(client) - form = ChangeEmailForm(email="freddy@example.org", password="dogdogdogdog") + user = User.query.filter_by(is_administrator=True).first() + form = ChangeEmailForm(email=user.email, password="dogdogdogdog") rv = client.post( url_for("auth.change_email_request"), data=form.data, follow_redirects=True @@ -318,7 +327,8 @@ def test_user_cannot_get_email_reset_token_sent_to_existing_email_differently_ca ): with current_app.test_request_context(): login_user(client) - form = ChangeEmailForm(email="FREDDY@EXAMPLE.ORG", password="dogdogdogdog") + user = User.query.filter_by(is_administrator=True).first() + form = ChangeEmailForm(email=user.email.upper(), password="dogdogdogdog") rv = client.post( url_for("auth.change_email_request"), data=form.data, follow_redirects=True @@ -327,10 +337,12 @@ def test_user_cannot_get_email_reset_token_sent_to_existing_email_differently_ca assert b"An email with instructions to confirm your new email" not in rv.data -def test_user_can_get_email_reset_token_sent_with_password(mockdata, client, session): +def test_user_can_get_email_reset_token_sent_with_password( + mockdata, client, session, faker +): with current_app.test_request_context(): login_user(client) - form = ChangeEmailForm(email="alice@example.org", password="dog") + form = ChangeEmailForm(email=faker.ascii_email(), password="dog") rv = client.post( url_for("auth.change_email_request"), data=form.data, follow_redirects=True @@ -342,7 +354,7 @@ def test_user_can_get_email_reset_token_sent_with_password(mockdata, client, ses def test_user_can_change_email_with_valid_reset_token(mockdata, client, session): with current_app.test_request_context(): login_user(client) - user = User.query.filter_by(email="jen@example.org").one() + user = User.query.filter_by(is_administrator=False, is_disabled=False).first() token = user.generate_email_change_token("alice@example.org") rv = client.get( @@ -367,7 +379,7 @@ def test_user_cannot_change_email_with_invalid_reset_token(mockdata, client, ses def test_user_can_confirm_account_with_valid_token(mockdata, client, session): with current_app.test_request_context(): login_unconfirmed_user(client) - user = User.query.filter_by(email="freddy@example.org").one() + user = User.query.filter_by(confirmed=False).first() token = user.generate_confirmation_token() rv = client.get(url_for("auth.confirm", token=token), follow_redirects=True) @@ -400,7 +412,7 @@ def test_user_can_change_password_if_they_match(mockdata, client, session): assert b"Your password has been updated." in rv.data assert ( - f"{current_app.config['OO_MAIL_SUBJECT_PREFIX']} Your Password Has Changed" + f"{current_app.config[KEY_OO_MAIL_SUBJECT_PREFIX]} Your Password Has Changed" in str(log.output) ) @@ -457,10 +469,7 @@ def test_user_cannot_change_password_if_they_dont_match(mockdata, client, sessio def test_user_can_change_dept_pref(mockdata, client, session): with current_app.test_request_context(): login_user(client) - - test_department_id = 1 - - form = ChangeDefaultDepartmentForm(dept_pref=test_department_id) + form = ChangeDefaultDepartmentForm(dept_pref=AC_DEPT) rv = client.post( url_for("auth.change_dept"), data=form.data, follow_redirects=True @@ -469,4 +478,4 @@ def test_user_can_change_dept_pref(mockdata, client, session): assert b"Updated!" in rv.data user = User.query.filter_by(email="jen@example.org").one() - assert user.dept_pref == test_department_id + assert user.dept_pref == AC_DEPT diff --git a/OpenOversight/tests/routes/test_descriptions.py b/OpenOversight/tests/routes/test_descriptions.py index 6bab3d94f..61b6bf6ee 100644 --- a/OpenOversight/tests/routes/test_descriptions.py +++ b/OpenOversight/tests/routes/test_descriptions.py @@ -44,9 +44,9 @@ def test_admins_can_create_descriptions(mockdata, client, session): login_admin(client) officer = Officer.query.first() text_contents = "I can haz descriptionz" - admin = User.query.filter_by(email="jen@example.org").first() + admin = User.query.filter_by(is_administrator=True).first() form = TextForm( - text_contents=text_contents, officer_id=officer.id, creator_id=admin.id + text_contents=text_contents, officer_id=officer.id, created_by=admin.id ) rv = client.post( @@ -70,9 +70,9 @@ def test_acs_can_create_descriptions(mockdata, client, session): login_ac(client) officer = Officer.query.first() description = "A description" - ac = User.query.filter_by(email="raq929@example.org").first() + ac = User.query.filter_by(ac_department_id=AC_DEPT).first() form = TextForm( - text_contents=description, officer_id=officer.id, creator_id=ac.id + text_contents=description, officer_id=officer.id, created_by=ac.id ) rv = client.post( @@ -94,6 +94,7 @@ def test_acs_can_create_descriptions(mockdata, client, session): def test_admins_can_edit_descriptions(mockdata, client, session): with current_app.test_request_context(): login_admin(client) + user = User.query.filter_by(is_administrator=True).first() officer = Officer.query.first() old_description = "meow" new_description = "I can haz editing descriptionz" @@ -101,7 +102,7 @@ def test_admins_can_edit_descriptions(mockdata, client, session): description = Description( text_contents=old_description, officer_id=officer.id, - creator_id=1, + created_by=user.id, created_at=original_date, updated_at=original_date, ) @@ -130,7 +131,7 @@ def test_admins_can_edit_descriptions(mockdata, client, session): def test_ac_can_edit_their_descriptions_in_their_department(mockdata, client, session): with current_app.test_request_context(): login_ac(client) - ac = User.query.filter_by(email="raq929@example.org").first() + ac = User.query.filter_by(ac_department_id=AC_DEPT).first() officer = Officer.query.filter_by(department_id=AC_DEPT).first() old_description = "meow" new_description = "I can haz editing descriptionz" @@ -138,7 +139,7 @@ def test_ac_can_edit_their_descriptions_in_their_department(mockdata, client, se description = Description( text_contents=old_description, officer_id=officer.id, - creator_id=ac.id, + created_by=ac.id, created_at=original_date, updated_at=original_date, ) @@ -167,7 +168,7 @@ def test_ac_can_edit_their_descriptions_in_their_department(mockdata, client, se def test_ac_can_edit_others_descriptions(mockdata, client, session): with current_app.test_request_context(): login_ac(client) - ac = User.query.filter_by(email="raq929@example.org").first() + ac = User.query.filter_by(ac_department_id=AC_DEPT).first() officer = Officer.query.filter_by(department_id=AC_DEPT).first() old_description = "meow" new_description = "I can haz editing descriptionz" @@ -175,7 +176,7 @@ def test_ac_can_edit_others_descriptions(mockdata, client, session): description = Description( text_contents=old_description, officer_id=officer.id, - creator_id=ac.id - 1, + created_by=ac.id, created_at=original_date, updated_at=original_date, ) @@ -204,18 +205,19 @@ def test_ac_can_edit_others_descriptions(mockdata, client, session): def test_ac_cannot_edit_descriptions_not_in_their_department(mockdata, client, session): with current_app.test_request_context(): login_ac(client) + ac = User.query.filter_by(ac_department_id=AC_DEPT).first() officer = Officer.query.except_( Officer.query.filter_by(department_id=AC_DEPT) ).first() - ac = User.query.filter_by(email="raq929@example.org").first() + old_description = "meow" new_description = "I can haz editing descriptionz" original_date = datetime.now() description = Description( text_contents=old_description, officer_id=officer.id, - creator_id=ac.id, + created_by=ac.id, created_at=original_date, updated_at=original_date, ) @@ -261,12 +263,12 @@ def test_acs_can_delete_their_descriptions_in_their_department( ): with current_app.test_request_context(): login_ac(client) - ac = User.query.filter_by(email="raq929@example.org").first() + ac = User.query.filter_by(ac_department_id=AC_DEPT).first() officer = Officer.query.filter_by(department_id=AC_DEPT).first() description = Description( text_contents="Hello", officer_id=officer.id, - creator_id=ac.id, + created_by=ac.id, created_at=datetime.now(), updated_at=datetime.now(), ) @@ -290,13 +292,14 @@ def test_acs_cannot_delete_descriptions_not_in_their_department( ): with current_app.test_request_context(): login_ac(client) + ac = User.query.filter_by(ac_department_id=AC_DEPT).first() officer = Officer.query.except_( Officer.query.filter_by(department_id=AC_DEPT) ).first() description = Description( text_contents="Hello", officer_id=officer.id, - creator_id=2, + created_by=ac.id, created_at=datetime.now(), updated_at=datetime.now(), ) @@ -320,11 +323,11 @@ def test_acs_can_get_edit_form_for_their_dept(mockdata, client, session): with current_app.test_request_context(): login_ac(client) officer = Officer.query.filter_by(department_id=AC_DEPT).first() - ac = User.query.filter_by(email="raq929@example.org").first() + ac = User.query.filter_by(ac_department_id=AC_DEPT).first() description = Description( text_contents="Hello", officer_id=officer.id, - creator_id=ac.id, + created_by=ac.id, created_at=datetime.now(), updated_at=datetime.now(), ) @@ -345,11 +348,11 @@ def test_acs_can_get_others_edit_form(mockdata, client, session): with current_app.test_request_context(): login_ac(client) officer = Officer.query.filter_by(department_id=AC_DEPT).first() - ac = User.query.filter_by(email="raq929@example.org").first() + ac = User.query.filter_by(ac_department_id=AC_DEPT).first() description = Description( text_contents="Hello", officer_id=officer.id, - creator_id=ac.id - 1, + created_by=ac.id - 1, created_at=datetime.now(), updated_at=datetime.now(), ) @@ -369,13 +372,14 @@ def test_acs_can_get_others_edit_form(mockdata, client, session): def test_acs_cannot_get_edit_form_for_their_non_dept(mockdata, client, session): with current_app.test_request_context(): login_ac(client) + ac = User.query.filter_by(ac_department_id=AC_DEPT).first() officer = Officer.query.except_( Officer.query.filter_by(department_id=AC_DEPT) ).first() description = Description( text_contents="Hello", officer_id=officer.id, - creator_id=2, + created_by=ac.id, created_at=datetime.now(), updated_at=datetime.now(), ) @@ -393,12 +397,13 @@ def test_acs_cannot_get_edit_form_for_their_non_dept(mockdata, client, session): def test_users_can_see_descriptions(mockdata, client, session): with current_app.test_request_context(): + admin = User.query.filter_by(is_administrator=True).first() officer = Officer.query.first() text_contents = "You can see me" description = Description( text_contents=text_contents, officer_id=officer.id, - creator_id=1, + created_by=admin.id, created_at=datetime.now(), updated_at=datetime.now(), ) @@ -417,12 +422,13 @@ def test_users_can_see_descriptions(mockdata, client, session): def test_admins_can_see_descriptions(mockdata, client, session): with current_app.test_request_context(): login_admin(client) + admin = User.query.filter_by(is_administrator=True).first() officer = Officer.query.first() text_contents = "Kittens see everything" description = Description( text_contents=text_contents, officer_id=officer.id, - creator_id=1, + created_by=admin.id, created_at=datetime.now(), updated_at=datetime.now(), ) @@ -440,12 +446,13 @@ def test_admins_can_see_descriptions(mockdata, client, session): def test_acs_can_see_descriptions_in_their_department(mockdata, client, session): with current_app.test_request_context(): login_ac(client) + ac = User.query.filter_by(ac_department_id=AC_DEPT).first() officer = Officer.query.filter_by(department_id=AC_DEPT).first() text_contents = "I can haz descriptionz" description = Description( text_contents=text_contents, officer_id=officer.id, - creator_id=1, + created_by=ac.id, created_at=datetime.now(), updated_at=datetime.now(), ) @@ -467,12 +474,12 @@ def test_acs_can_see_descriptions_not_in_their_department(mockdata, client, sess Officer.query.filter_by(department_id=AC_DEPT) ).first() login_ac(client) - creator = User.query.get(1) + ac = User.query.filter_by(ac_department_id=AC_DEPT).first() text_contents = "Hello it me" description = Description( text_contents=text_contents, officer_id=officer.id, - creator_id=creator.id, + created_by=ac.id, created_at=datetime.now(), updated_at=datetime.now(), ) @@ -487,18 +494,18 @@ def test_acs_can_see_descriptions_not_in_their_department(mockdata, client, sess assert description in officer.descriptions assert rv.status_code == HTTPStatus.OK assert text_contents in response_text - assert creator.username in response_text + assert ac.username in response_text def test_anonymous_users_cannot_see_description_creators(mockdata, client, session): with current_app.test_request_context(): officer = Officer.query.first() - ac = User.query.filter_by(email="raq929@example.org").first() + ac = User.query.filter_by(ac_department_id=AC_DEPT).first() text_contents = "All we have is each other" description = Description( text_contents=text_contents, officer_id=officer.id, - creator_id=ac.id, + created_by=ac.id, created_at=datetime.now(), updated_at=datetime.now(), ) diff --git a/OpenOversight/tests/routes/test_image_tagging.py b/OpenOversight/tests/routes/test_image_tagging.py index 9bfc4ce57..83fe1f243 100644 --- a/OpenOversight/tests/routes/test_image_tagging.py +++ b/OpenOversight/tests/routes/test_image_tagging.py @@ -15,6 +15,7 @@ Image, Job, Officer, + User, ) from OpenOversight.app.utils.constants import ENCODING_UTF_8 from OpenOversight.tests.conftest import AC_DEPT @@ -159,6 +160,8 @@ def test_user_can_add_tag(mockdata, client, session): officer = Officer.query.filter_by(department_id=1).first() image = Image.query.filter_by(department_id=1).first() login_user(client) + user = User.query.filter_by(is_administrator=True).first() + form = FaceTag( department_id=officer.department_id, star_no=officer.assignments[0].star_no, @@ -167,6 +170,7 @@ def test_user_can_add_tag(mockdata, client, session): dataY=32, dataWidth=3, dataHeight=33, + created_by=user.id, ) rv = client.post( url_for("main.label_data", image_id=image.id), @@ -250,6 +254,8 @@ def test_user_cannot_open_image_in_tagger_if_has_already_been_tagged( def test_user_cannot_add_tag_if_it_exists(mockdata, client, session): with current_app.test_request_context(): login_user(client) + user = User.query.filter_by(is_administrator=True).first() + tag = Face.query.first() form = FaceTag( department_id=tag.officer.department_id, @@ -259,6 +265,7 @@ def test_user_cannot_add_tag_if_it_exists(mockdata, client, session): dataY=32, dataWidth=3, dataHeight=33, + created_by=user.id, ) rv = client.post( @@ -275,6 +282,8 @@ def test_user_cannot_add_tag_if_it_exists(mockdata, client, session): def test_user_cannot_tag_nonexistent_officer(mockdata, client, session): with current_app.test_request_context(): login_user(client) + user = User.query.filter_by(is_administrator=True).first() + tag = Face.query.first() form = FaceTag( department_id=Department.query.first().id, @@ -284,6 +293,7 @@ def test_user_cannot_tag_nonexistent_officer(mockdata, client, session): dataY=32, dataWidth=3, dataHeight=33, + created_by=user.id, ) rv = client.post( @@ -298,6 +308,8 @@ def test_user_cannot_tag_officer_mismatched_with_department(mockdata, client, se with current_app.test_request_context(): login_user(client) tag = Face.query.first() + user = User.query.filter_by(is_administrator=True).first() + form = FaceTag( department_id=tag.officer.department_id, star_no=tag.officer.assignments[0].star_no, @@ -307,6 +319,7 @@ def test_user_cannot_tag_officer_mismatched_with_department(mockdata, client, se dataY=32, dataWidth=3, dataHeight=33, + created_by=user.id, ) rv = client.post( @@ -381,6 +394,7 @@ def test_featured_tag_replaces_others(mockdata, client, session): with current_app.test_request_context(): login_user(client) + user = User.query.filter_by(is_administrator=True).first() tag1 = Face.query.first() officer = Officer.query.filter_by(id=tag1.officer_id).one() @@ -401,6 +415,7 @@ def test_featured_tag_replaces_others(mockdata, client, session): dataY=32, dataWidth=3, dataHeight=33, + created_by=user.id, ) rv = client.post( url_for("main.label_data", image_id=second_image.id), diff --git a/OpenOversight/tests/routes/test_incidents.py b/OpenOversight/tests/routes/test_incidents.py index 4838ae874..61aaebd1a 100644 --- a/OpenOversight/tests/routes/test_incidents.py +++ b/OpenOversight/tests/routes/test_incidents.py @@ -13,7 +13,7 @@ LocationForm, OOIdForm, ) -from OpenOversight.app.models.database import Department, Incident, Officer +from OpenOversight.app.models.database import Department, Incident, Officer, User from OpenOversight.app.utils.constants import ENCODING_UTF_8 from OpenOversight.tests.conftest import AC_DEPT from OpenOversight.tests.routes.route_helpers import ( @@ -62,6 +62,7 @@ def test_route_admin_or_required(route, client, mockdata): def test_admins_can_create_basic_incidents(report_number, mockdata, client, session): with current_app.test_request_context(): login_admin(client) + user = User.query.filter_by(is_administrator=True).first() test_date = datetime(2000, 5, 25, 1, 45) address_form = LocationForm( @@ -70,11 +71,12 @@ def test_admins_can_create_basic_incidents(report_number, mockdata, client, sess city="FFFFF", state="IA", zip_code="03435", + created_by=user.id, ) # These have to have a dropdown selected because if not, an empty Unicode # string is sent, which does not mach the '' selector. - link_form = LinkForm(link_type="video") - license_plates_form = LicensePlateForm(state="AZ") + link_form = LinkForm(link_type="video", created_by=user.id) + license_plates_form = LicensePlateForm(state="AZ", created_by=user.id) form = IncidentForm( date_field=str(test_date.date()), time_field=str(test_date.time()), @@ -85,6 +87,9 @@ def test_admins_can_create_basic_incidents(report_number, mockdata, client, sess links=[link_form.data], license_plates=[license_plates_form.data], officers=[], + created_by=user.id, + last_updated_by=user.id, + last_updated_at=datetime.now(), ) data = process_form_data(form.data) @@ -103,7 +108,8 @@ def test_admins_cannot_create_incident_with_invalid_report_number( ): with current_app.test_request_context(): login_admin(client) - date = datetime(2000, 5, 25, 1, 45) + user = User.query.filter_by(is_administrator=True).first() + test_date = datetime(2000, 5, 25, 1, 45) report_number = "Will Not Work! #45" address_form = LocationForm( @@ -112,14 +118,15 @@ def test_admins_cannot_create_incident_with_invalid_report_number( city="FFFFF", state="IA", zip_code="03435", + created_by=user.id, ) # These have to have a dropdown selected because if not, an empty Unicode # string is sent, which does not mach the '' selector. - link_form = LinkForm(link_type="video") - license_plates_form = LicensePlateForm(state="AZ") + link_form = LinkForm(link_type="video", created_by=user.id) + license_plates_form = LicensePlateForm(state="AZ", created_by=user.id) form = IncidentForm( - date_field=str(date.date()), - time_field=str(date.time()), + date_field=str(test_date.date()), + time_field=str(test_date.time()), report_number=report_number, description="Something happened", department="1", @@ -143,14 +150,15 @@ def test_admins_cannot_create_incident_with_invalid_report_number( def test_admins_can_edit_incident_date_and_address(mockdata, client, session): with current_app.test_request_context(): login_admin(client) + user = User.query.filter_by(is_administrator=True).first() inc = Incident.query.options( joinedload(Incident.links), joinedload(Incident.license_plates), joinedload(Incident.officers), ).first() inc_id = inc.id - new_date = date(2017, 6, 25) - new_time = time(1, 45) + test_date = date(2017, 6, 25) + test_time = time(1, 45) street_name = "Newest St" address_form = LocationForm( street_name=street_name, @@ -158,19 +166,21 @@ def test_admins_can_edit_incident_date_and_address(mockdata, client, session): city="Boston", state="NH", zip_code="03435", + created_by=user.id, ) 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, created_by=user.id).data + for link in inc.links ] license_plates_forms = [ - LicensePlateForm(number=lp.number, state=lp.state).data + LicensePlateForm(number=lp.number, state=lp.state, created_by=user.id).data for lp in inc.license_plates ] ooid_forms = [OOIdForm(ooid=officer.id) for officer in inc.officers] form = IncidentForm( - date_field=str(new_date), - time_field=str(new_time), + date_field=str(test_date), + time_field=str(test_time), report_number=inc.report_number, description=inc.description, department="1", @@ -178,6 +188,7 @@ def test_admins_can_edit_incident_date_and_address(mockdata, client, session): links=links_forms, license_plates=license_plates_forms, officers=ooid_forms, + created_by=user.id, ) data = process_form_data(form.data) @@ -189,14 +200,15 @@ def test_admins_can_edit_incident_date_and_address(mockdata, client, session): assert rv.status_code == HTTPStatus.OK assert "successfully updated" in rv.data.decode(ENCODING_UTF_8) updated = Incident.query.get(inc_id) - assert updated.date == new_date - assert updated.time == new_time + assert updated.date == test_date + assert updated.time == test_time assert updated.address.street_name == street_name def test_admins_can_edit_incident_links_and_licenses(mockdata, client, session, faker): with current_app.test_request_context(): login_admin(client) + user = User.query.filter_by(is_administrator=True).first() inc = Incident.query.options( joinedload(Incident.links), joinedload(Incident.license_plates), @@ -210,16 +222,20 @@ def test_admins_can_edit_incident_links_and_licenses(mockdata, client, session, city=inc.address.city, state=inc.address.state, zip_code=inc.address.zip_code, + created_by=inc.created_by, ) 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, created_by=user.id).data + for link in inc.links ] new_url = faker.url() - link_form = LinkForm(url=new_url, link_type="video") + link_form = LinkForm(url=new_url, link_type="video", created_by=user.id) old_license_plates = inc.license_plates new_number = "453893" - license_plates_form = LicensePlateForm(number=new_number, state="IA") + license_plates_form = LicensePlateForm( + number=new_number, state="IA", created_by=user.id + ) ooid_forms = [OOIdForm(ooid=officer.id) for officer in inc.officers] form = IncidentForm( @@ -232,6 +248,7 @@ def test_admins_can_edit_incident_links_and_licenses(mockdata, client, session, links=old_links_forms + [link_form.data], license_plates=[license_plates_form.data], officers=ooid_forms, + created_by=user.id, ) data = process_form_data(form.data) @@ -255,6 +272,7 @@ def test_admins_can_edit_incident_links_and_licenses(mockdata, client, session, def test_admins_cannot_make_ancient_incidents(mockdata, client, session): with current_app.test_request_context(): login_admin(client) + user = User.query.filter_by(is_administrator=True).first() inc = Incident.query.options( joinedload(Incident.links), joinedload(Incident.license_plates), @@ -268,6 +286,7 @@ def test_admins_cannot_make_ancient_incidents(mockdata, client, session): city=inc.address.city, state=inc.address.state, zip_code=inc.address.zip_code, + created_by=inc.created_by, ) ooid_forms = [OOIdForm(ooid=officer.id) for officer in inc.officers] @@ -279,6 +298,7 @@ def test_admins_cannot_make_ancient_incidents(mockdata, client, session): department="1", address=address_form.data, officers=ooid_forms, + created_by=user.id, ) data = process_form_data(form.data) @@ -294,7 +314,8 @@ def test_admins_cannot_make_ancient_incidents(mockdata, client, session): def test_admins_cannot_make_incidents_without_state(mockdata, client, session): with current_app.test_request_context(): login_admin(client) - date = datetime(2000, 5, 25, 1, 45) + user = User.query.filter_by(is_administrator=True).first() + test_date = datetime(2000, 5, 25, 1, 45) report_number = "42" address_form = LocationForm( @@ -303,17 +324,19 @@ def test_admins_cannot_make_incidents_without_state(mockdata, client, session): city="FFFFF", state="", zip_code="03435", + created_by=user.id, ) ooid_forms = [OOIdForm(ooid=officer.id) for officer in Officer.query.all()[:5]] form = IncidentForm( - date_field=str(date.date()), - time_field=str(date.time()), + date_field=str(test_date.date()), + time_field=str(test_date.time()), report_number=report_number, description="Something happened", department="1", address=address_form.data, officers=ooid_forms, + created_by=user.id, ) data = process_form_data(form.data) @@ -331,7 +354,8 @@ def test_admins_cannot_make_incidents_with_multiple_validation_errors( ): with current_app.test_request_context(): login_admin(client) - date = datetime(2000, 5, 25, 1, 45) + user = User.query.filter_by(is_administrator=True).first() + test_date = datetime(2000, 5, 25, 1, 45) report_number = "42" address_form = LocationForm( @@ -342,16 +366,18 @@ def test_admins_cannot_make_incidents_with_multiple_validation_errors( state="NY", # invalid ZIP code => 'Zip codes must have 5 digits.' zip_code="0343", + created_by=user.id, ) - # license plate number given, but no state selected => 'Must also select a state.' + # license plate number given, but no state selected => + # 'Must also select a state.' license_plate_form = LicensePlateForm(number="ABCDE", state="") ooid_forms = [OOIdForm(ooid=officer.id) for officer in Officer.query.all()[:5]] form = IncidentForm( # no date given => 'This field is required.' date_field="", - time_field=str(date.time()), + time_field=str(test_date.time()), report_number=report_number, description="Something happened", # invalid department id => 'This field is required.' @@ -376,6 +402,8 @@ def test_admins_cannot_make_incidents_with_multiple_validation_errors( def test_admins_can_edit_incident_officers(mockdata, client, session): with current_app.test_request_context(): login_admin(client) + user = User.query.filter_by(is_administrator=True).first() + inc = Incident.query.options( joinedload(Incident.links), joinedload(Incident.license_plates), @@ -389,12 +417,14 @@ def test_admins_can_edit_incident_officers(mockdata, client, session): city=inc.address.city, state=inc.address.state, zip_code=inc.address.zip_code, + created_by=inc.created_by, ) 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, created_by=user.id).data + for link in inc.links ] license_plates_forms = [ - LicensePlateForm(number=lp.number, state=lp.state).data + LicensePlateForm(number=lp.number, state=lp.state, created_by=user.id).data for lp in inc.license_plates ] @@ -435,6 +465,8 @@ def test_admins_can_edit_incident_officers(mockdata, client, session): def test_admins_cannot_edit_nonexisting_officers(mockdata, client, session): with current_app.test_request_context(): login_admin(client) + user = User.query.filter_by(is_administrator=True).first() + inc = Incident.query.options( joinedload(Incident.links), joinedload(Incident.license_plates), @@ -448,9 +480,11 @@ def test_admins_cannot_edit_nonexisting_officers(mockdata, client, session): city=inc.address.city, state=inc.address.state, zip_code=inc.address.zip_code, + created_by=inc.created_by, ) 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, created_by=user.id).data + for link in inc.links ] license_plates_forms = [ LicensePlateForm(number=lp.number, state=lp.state).data @@ -490,8 +524,10 @@ def test_admins_cannot_edit_nonexisting_officers(mockdata, client, session): def test_ac_can_edit_incidents_in_their_department(mockdata, client, session): with current_app.test_request_context(): login_ac(client) + user = User.query.filter_by(ac_department_id=AC_DEPT).first() + inc = Incident.query.filter_by(department_id=AC_DEPT).first() - new_date = datetime(2017, 6, 25, 1, 45) + test_date = datetime(2017, 6, 25, 1, 45) street_name = "Newest St" address_form = LocationForm( street_name=street_name, @@ -499,19 +535,21 @@ def test_ac_can_edit_incidents_in_their_department(mockdata, client, session): city="Boston", state="NH", zip_code="03435", + created_by=user.id, ) 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, created_by=user.id).data + for link in inc.links ] license_plates_forms = [ - LicensePlateForm(number=lp.number, state=lp.state).data + LicensePlateForm(number=lp.number, state=lp.state, created_by=user.id).data for lp in inc.license_plates ] ooid_forms = [OOIdForm(ooid=officer.id) for officer in inc.officers] form = IncidentForm( - date_field=str(new_date.date()), - time_field=str(new_date.time()), + date_field=str(test_date.date()), + time_field=str(test_date.time()), report_number=inc.report_number, description=inc.description, department=AC_DEPT, @@ -529,19 +567,22 @@ def test_ac_can_edit_incidents_in_their_department(mockdata, client, session): ) assert rv.status_code == HTTPStatus.OK assert "successfully updated" in rv.data.decode(ENCODING_UTF_8) - assert inc.date == new_date.date() - assert inc.time == new_date.time() + assert inc.date == test_date.date() + assert inc.time == test_date.time() assert inc.address.street_name == street_name def test_ac_cannot_edit_incidents_not_in_their_department(mockdata, client, session): with current_app.test_request_context(): login_ac(client) + user = User.query.filter_by( + ac_department_id=None, is_administrator=False + ).first() inc = Incident.query.except_( Incident.query.filter_by(department_id=AC_DEPT) ).first() - new_date = datetime(2017, 6, 25, 1, 45) + test_date = datetime(2017, 6, 25, 1, 45) street_name = "Not Allowed St" address_form = LocationForm( street_name=street_name, @@ -549,19 +590,21 @@ def test_ac_cannot_edit_incidents_not_in_their_department(mockdata, client, sess city="Boston", state="NH", zip_code="03435", + created_by=user.id, ) 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, created_by=user.id).data + for link in inc.links ] license_plates_forms = [ - LicensePlateForm(number=lp.number, state=lp.state).data + LicensePlateForm(number=lp.number, state=lp.state, created_by=user.id).data for lp in inc.license_plates ] ooid_forms = [OOIdForm(ooid=officer.id) for officer in inc.officers] form = IncidentForm( - date_field=str(new_date.date()), - time_field=str(new_date.time()), + date_field=str(test_date.date()), + time_field=str(test_date.time()), report_number=inc.report_number, description=inc.description, department=AC_DEPT, diff --git a/OpenOversight/tests/routes/test_notes.py b/OpenOversight/tests/routes/test_notes.py index 3995094bd..821f22567 100644 --- a/OpenOversight/tests/routes/test_notes.py +++ b/OpenOversight/tests/routes/test_notes.py @@ -37,9 +37,9 @@ def test_admins_can_create_notes(mockdata, client, session): login_admin(client) officer = Officer.query.first() text_contents = "I can haz notez" - admin = User.query.filter_by(email="jen@example.org").first() + admin = User.query.filter_by(is_administrator=True).first() form = TextForm( - text_contents=text_contents, officer_id=officer.id, creator_id=admin.id + text_contents=text_contents, officer_id=officer.id, created_by=admin.id ) rv = client.post( @@ -61,8 +61,8 @@ def test_acs_can_create_notes(mockdata, client, session): login_ac(client) officer = Officer.query.first() note = "I can haz notez" - ac = User.query.filter_by(email="raq929@example.org").first() - form = TextForm(text_contents=note, officer_id=officer.id, creator_id=ac.id) + ac = User.query.filter_by(ac_department_id=AC_DEPT).first() + form = TextForm(text_contents=note, officer_id=officer.id, created_by=ac.id) rv = client.post( url_for("main.note_api", officer_id=officer.id), @@ -88,7 +88,7 @@ def test_admins_can_edit_notes(mockdata, client, session): note = Note( text_contents=old_note, officer_id=officer.id, - creator_id=1, + created_by=1, created_at=original_date, updated_at=original_date, ) @@ -114,7 +114,7 @@ def test_admins_can_edit_notes(mockdata, client, session): def test_ac_can_edit_their_notes_in_their_department(mockdata, client, session): with current_app.test_request_context(): login_ac(client) - ac = User.query.filter_by(email="raq929@example.org").first() + ac = User.query.filter_by(ac_department_id=AC_DEPT).first() officer = Officer.query.filter_by(department_id=AC_DEPT).first() old_note = "meow" new_note = "I can haz editing notez" @@ -122,7 +122,7 @@ def test_ac_can_edit_their_notes_in_their_department(mockdata, client, session): note = Note( text_contents=old_note, officer_id=officer.id, - creator_id=ac.id, + created_by=ac.id, created_at=original_date, updated_at=original_date, ) @@ -148,7 +148,7 @@ def test_ac_can_edit_their_notes_in_their_department(mockdata, client, session): def test_ac_can_edit_others_notes(mockdata, client, session): with current_app.test_request_context(): login_ac(client) - ac = User.query.filter_by(email="raq929@example.org").first() + ac = User.query.filter_by(ac_department_id=AC_DEPT).first() officer = Officer.query.filter_by(department_id=AC_DEPT).first() old_note = "meow" new_note = "I can haz editing notez" @@ -156,7 +156,7 @@ def test_ac_can_edit_others_notes(mockdata, client, session): note = Note( text_contents=old_note, officer_id=officer.id, - creator_id=ac.id - 1, + created_by=ac.id - 1, created_at=original_date, updated_at=original_date, ) @@ -186,14 +186,14 @@ def test_ac_cannot_edit_notes_not_in_their_department(mockdata, client, session) officer = Officer.query.except_( Officer.query.filter_by(department_id=AC_DEPT) ).first() - ac = User.query.filter_by(email="raq929@example.org").first() + ac = User.query.filter_by(ac_department_id=AC_DEPT).first() old_note = "meow" new_note = "I can haz editing notez" original_date = datetime.now() note = Note( text_contents=old_note, officer_id=officer.id, - creator_id=ac.id, + created_by=ac.id, created_at=original_date, updated_at=original_date, ) @@ -230,12 +230,12 @@ def test_admins_can_delete_notes(mockdata, client, session): def test_acs_can_delete_their_notes_in_their_department(mockdata, client, session): with current_app.test_request_context(): login_ac(client) - ac = User.query.filter_by(email="raq929@example.org").first() + ac = User.query.filter_by(ac_department_id=AC_DEPT).first() officer = Officer.query.filter_by(department_id=AC_DEPT).first() note = Note( text_contents="Hello", officer_id=officer.id, - creator_id=ac.id, + created_by=ac.id, created_at=datetime.now(), updated_at=datetime.now(), ) @@ -260,7 +260,7 @@ def test_acs_cannot_delete_notes_not_in_their_department(mockdata, client, sessi note = Note( text_contents="Hello", officer_id=officer.id, - creator_id=2, + created_by=2, created_at=datetime.now(), updated_at=datetime.now(), ) @@ -281,11 +281,11 @@ def test_acs_can_get_edit_form_for_their_dept(mockdata, client, session): with current_app.test_request_context(): login_ac(client) officer = Officer.query.filter_by(department_id=AC_DEPT).first() - ac = User.query.filter_by(email="raq929@example.org").first() + ac = User.query.filter_by(ac_department_id=AC_DEPT).first() note = Note( text_contents="Hello", officer_id=officer.id, - creator_id=ac.id, + created_by=ac.id, created_at=datetime.now(), updated_at=datetime.now(), ) @@ -303,11 +303,11 @@ def test_acs_can_get_others_edit_form(mockdata, client, session): with current_app.test_request_context(): login_ac(client) officer = Officer.query.filter_by(department_id=AC_DEPT).first() - ac = User.query.filter_by(email="raq929@example.org").first() + ac = User.query.filter_by(ac_department_id=AC_DEPT).first() note = Note( text_contents="Hello", officer_id=officer.id, - creator_id=ac.id - 1, + created_by=ac.id - 1, created_at=datetime.now(), updated_at=datetime.now(), ) @@ -330,7 +330,7 @@ def test_acs_cannot_get_edit_form_for_their_non_dept(mockdata, client, session): note = Note( text_contents="Hello", officer_id=officer.id, - creator_id=2, + created_by=2, created_at=datetime.now(), updated_at=datetime.now(), ) @@ -350,7 +350,7 @@ def test_users_cannot_see_notes(mockdata, client, session): note = Note( text_contents=text_contents, officer_id=officer.id, - creator_id=1, + created_by=1, created_at=datetime.now(), updated_at=datetime.now(), ) @@ -374,7 +374,7 @@ def test_admins_can_see_notes(mockdata, client, session): note = Note( text_contents=text_contents, officer_id=officer.id, - creator_id=1, + created_by=1, created_at=datetime.now(), updated_at=datetime.now(), ) @@ -397,7 +397,7 @@ def test_acs_can_see_notes_in_their_department(mockdata, client, session): note = Note( text_contents=text_contents, officer_id=officer.id, - creator_id=1, + created_by=1, created_at=datetime.now(), updated_at=datetime.now(), ) @@ -422,7 +422,7 @@ def test_acs_cannot_see_notes_not_in_their_department(mockdata, client, session) note = Note( text_contents=text_contents, officer_id=officer.id, - creator_id=1, + created_by=1, created_at=datetime.now(), updated_at=datetime.now(), ) diff --git a/OpenOversight/tests/routes/test_officer_and_department.py b/OpenOversight/tests/routes/test_officer_and_department.py index 4786274e5..71938cf5c 100644 --- a/OpenOversight/tests/routes/test_officer_and_department.py +++ b/OpenOversight/tests/routes/test_officer_and_department.py @@ -391,6 +391,7 @@ def test_admin_edit_assignment_validation_error( def test_ac_can_edit_officer_in_their_dept_assignment(mockdata, client, session): with current_app.test_request_context(): login_ac(client) + user = User.query.filter_by(ac_department_id=AC_DEPT).first() star_no = "1234" new_star_no = "12345" @@ -403,6 +404,7 @@ def test_ac_can_edit_officer_in_their_dept_assignment(mockdata, client, session) job_title=job.id, start_date=date(2019, 1, 1), resign_date=date(2019, 12, 31), + created_by=user.id, ) # Remove existing assignments @@ -430,6 +432,7 @@ def test_ac_can_edit_officer_in_their_dept_assignment(mockdata, client, session) job_title=job.id, start_date=date(2019, 2, 1), resign_date=date(2019, 11, 30), + created_by=user.id, ) rv = client.post( @@ -504,9 +507,13 @@ def test_ac_cannot_edit_assignment_outside_their_dept(mockdata, client, session) def test_admin_can_add_police_department(mockdata, client, session): with current_app.test_request_context(): login_admin(client) + user = User.query.filter_by(is_administrator=True).first() form = DepartmentForm( - name=TestPD.name, short_name=TestPD.short_name, state=TestPD.state + name=TestPD.name, + short_name=TestPD.short_name, + state=TestPD.state, + created_by=user.id, ) rv = client.post( @@ -527,8 +534,11 @@ def test_admin_can_add_police_department(mockdata, client, session): def test_admin_cannot_add_police_department_without_state(mockdata, client, session): with current_app.test_request_context(): login_admin(client) + user = User.query.filter_by(is_administrator=True).first() - form = DepartmentForm(name=TestPD.name, short_name=TestPD.short_name, state="") + form = DepartmentForm( + name=TestPD.name, short_name=TestPD.short_name, state="", created_by=user.id + ) form.validate() errors = form.errors @@ -540,9 +550,13 @@ def test_admin_cannot_add_police_department_without_state(mockdata, client, sess def test_ac_cannot_add_police_department(mockdata, client, session): with current_app.test_request_context(): login_ac(client) + user = User.query.filter_by(is_administrator=False).first() form = DepartmentForm( - name=TestPD.name, short_name=TestPD.short_name, state=TestPD.state + name=TestPD.name, + short_name=TestPD.short_name, + state=TestPD.state, + created_by=user.id, ) rv = client.post( @@ -555,9 +569,13 @@ def test_ac_cannot_add_police_department(mockdata, client, session): def test_admin_cannot_add_duplicate_police_department(mockdata, client, session): with current_app.test_request_context(): login_admin(client) + user = User.query.filter_by(is_administrator=True).first() form = DepartmentForm( - name=TestPD.name, short_name=TestPD.short_name, state=TestPD.state + name=TestPD.name, + short_name=TestPD.short_name, + state=TestPD.state, + created_by=user.id, ) rv = client.post( @@ -598,11 +616,13 @@ def test_admin_can_edit_police_department(mockdata, client, session): ) login_admin(client) + user = User.query.filter_by(is_administrator=True).first() misspelled_form = DepartmentForm( name=MisspelledPD.name, short_name=MisspelledPD.short_name, state=MisspelledPD.state, + created_by=user.id, ) misspelled_rv = client.post( @@ -700,11 +720,13 @@ def test_admin_can_edit_police_department(mockdata, client, session): def test_admin_cannot_edit_police_department_without_state(mockdata, client, session): with current_app.test_request_context(): login_admin(client) + user = User.query.filter_by(is_administrator=True).first() add_department_form = DepartmentForm( name=TestPD.name, short_name=TestPD.short_name, state=TestPD.state, + created_by=user.id, ) add_department_rv = client.post( @@ -719,7 +741,7 @@ def test_admin_cannot_edit_police_department_without_state(mockdata, client, ses ) without_state_form = EditDepartmentForm( - name=TestPD.name, short_name=TestPD.short_name, state="" + name=TestPD.name, short_name=TestPD.short_name, state="", created_by=user.id ) without_state_form.validate() @@ -1009,11 +1031,13 @@ def test_admin_can_create_department_with_same_name_in_different_state( ): with current_app.test_request_context(): login_admin(client) + user = User.query.filter_by(ac_department_id=AC_DEPT).first() existing_form = DepartmentForm( name=ExistingPD.name, short_name=ExistingPD.short_name, state=ExistingPD.state, + created_by=user.id, ) existing_rv = client.post( @@ -1046,6 +1070,7 @@ def test_admin_can_create_department_with_same_name_in_different_state( name=ExistingDiffStatePD.name, short_name=ExistingDiffStatePD.short_name, state=ExistingDiffStatePD.state, + created_by=user.id, ) existing_diff_state_rv = client.post( @@ -1070,6 +1095,7 @@ def test_admin_can_create_department_with_same_name_in_different_state( name=ExistingPD.name, short_name=ExistingPD.short_name, state=ExistingPD.state, + created_by=user.id, ) existing_duplicate_rv = client.post( @@ -1088,11 +1114,13 @@ def test_admin_cannot_duplicate_police_department_during_edit( ): with current_app.test_request_context(): login_admin(client) + user = User.query.filter_by(is_administrator=True).first() existing_dep_form = DepartmentForm( name=ExistingPD.name, short_name=ExistingPD.short_name, state=ExistingPD.state, + created_by=user.id, ) existing_dep_rv = client.post( @@ -1109,7 +1137,10 @@ def test_admin_cannot_duplicate_police_department_during_edit( NewPD = PoliceDepartment("New Police Department", "NPD", ExistingPD.state) new_dep_form = DepartmentForm( - name=NewPD.name, short_name=NewPD.short_name, state=NewPD.state + name=NewPD.name, + short_name=NewPD.short_name, + state=NewPD.state, + created_by=user.id, ) new_dep_rv = client.post( @@ -1159,6 +1190,8 @@ def test_expected_dept_appears_in_submission_dept_selection(mockdata, client, se def test_admin_can_add_new_officer(mockdata, client, session, department, faker): with current_app.test_request_context(): login_admin(client) + user = User.query.filter_by(is_administrator=True).first() + links = [ LinkForm(url=faker.url(), link_type="link").data, LinkForm(url=faker.url(), link_type="video").data, @@ -1175,6 +1208,7 @@ def test_admin_can_add_new_officer(mockdata, client, session, department, faker) department=department.id, birth_year=1990, links=links, + created_by=user.id, ) data = process_form_data(form.data) @@ -1195,6 +1229,8 @@ def test_admin_can_add_new_officer_with_unit( ): with current_app.test_request_context(): login_admin(client) + user = User.query.filter_by(is_administrator=True).first() + unit = random.choice(unit_choices()) links = [ LinkForm(url=faker.url(), link_type="link").data, @@ -1213,6 +1249,7 @@ def test_admin_can_add_new_officer_with_unit( department=department.id, birth_year=1990, links=links, + created_by=user.id, ) data = process_form_data(form.data) @@ -1226,7 +1263,7 @@ def test_admin_can_add_new_officer_with_unit( assert officer.first_name == "Test" assert officer.race == "WHITE" assert officer.gender == "M" - assert Assignment.query.filter_by(baseofficer=officer, unit=unit).one() + assert Assignment.query.filter_by(base_officer=officer, unit=unit).one() def test_ac_can_add_new_officer_in_their_dept(mockdata, client, session): @@ -1313,12 +1350,14 @@ def test_ac_can_add_new_officer_with_unit_in_their_dept(mockdata, client, sessio ) else: assert officer.gender == gender - assert Assignment.query.filter_by(baseofficer=officer, unit=unit).one() + assert Assignment.query.filter_by(base_officer=officer, unit=unit).one() def test_ac_cannot_add_new_officer_not_in_their_dept(mockdata, client, session): with current_app.test_request_context(): login_ac(client) + user = User.query.filter_by(is_administrator=True).first() + department = Department.query.except_( Department.query.filter_by(id=AC_DEPT) ).first() @@ -1338,6 +1377,7 @@ def test_ac_cannot_add_new_officer_not_in_their_dept(mockdata, client, session): job_id=job.id, department=department.id, birth_year=1990, + created_by=user.id, ) data = process_form_data(form.data) @@ -1351,12 +1391,14 @@ def test_ac_cannot_add_new_officer_not_in_their_dept(mockdata, client, session): def test_admin_can_edit_existing_officer(mockdata, client, session, department, faker): with current_app.test_request_context(): login_admin(client) + user = User.query.filter_by(is_administrator=True).first() + unit = random.choice(unit_choices()) link_url0 = faker.url() link_url1 = faker.url() links = [ - LinkForm(url=link_url0, link_type="link").data, - LinkForm(url=link_url0, link_type="video").data, + LinkForm(url=link_url0, link_type="link", created_by=user.id).data, + LinkForm(url=link_url0, link_type="video", created_by=user.id).data, ] job = Job.query.filter_by(department_id=department.id).first() form = AddOfficerForm( @@ -1371,10 +1413,11 @@ def test_admin_can_edit_existing_officer(mockdata, client, session, department, unit=unit.id, birth_year=1990, links=links, + created_by=user.id, ) data = process_form_data(form.data) - rv = client.post(url_for("main.add_officer"), data=data, follow_redirects=True) + client.post(url_for("main.add_officer"), data=data, follow_redirects=True) officer = Officer.query.filter_by(last_name="Testerinski").one() @@ -1397,7 +1440,7 @@ def test_ac_cannot_edit_officer_not_in_their_dept(mockdata, client, session): with current_app.test_request_context(): login_ac(client) - officer = officer = Officer.query.except_( + officer = Officer.query.except_( Officer.query.filter_by(department_id=AC_DEPT) ).first() old_last_name = officer.last_name @@ -1466,7 +1509,7 @@ def test_ac_can_edit_officer_in_their_dept(mockdata, client, session): data = process_form_data(form.data) - rv = client.post(url_for("main.add_officer"), data=data, follow_redirects=True) + client.post(url_for("main.add_officer"), data=data, follow_redirects=True) officer = Officer.query.filter_by(last_name=last_name).one() @@ -1500,6 +1543,7 @@ def test_admin_adds_officer_without_middle_initial( ): with current_app.test_request_context(): login_admin(client) + user = User.query.filter_by(is_administrator=True).first() job = Job.query.filter_by(department_id=department.id).first() form = AddOfficerForm( @@ -1511,6 +1555,7 @@ def test_admin_adds_officer_without_middle_initial( job_id=job.id, department=department.id, birth_year=1990, + created_by=user.id, ) data = process_form_data(form.data) @@ -1531,6 +1576,7 @@ def test_admin_adds_officer_with_letter_in_badge_no( ): with current_app.test_request_context(): login_admin(client) + user = User.query.filter_by(is_administrator=True).first() job = Job.query.filter_by(department_id=department.id).first() form = AddOfficerForm( @@ -1543,6 +1589,7 @@ def test_admin_adds_officer_with_letter_in_badge_no( job_id=job.id, department=department.id, birth_year=1990, + created_by=user.id, ) data = process_form_data(form.data) @@ -1561,8 +1608,11 @@ def test_admin_adds_officer_with_letter_in_badge_no( def test_admin_can_add_new_unit(mockdata, client, session, department): with current_app.test_request_context(): login_admin(client) + user = User.query.filter_by(is_administrator=True).first() - form = AddUnitForm(description="Test", department=department.id) + form = AddUnitForm( + description="Test", department=department.id, created_by=user.id + ) rv = client.post( url_for("main.add_unit"), data=form.data, follow_redirects=True @@ -1578,9 +1628,12 @@ def test_admin_can_add_new_unit(mockdata, client, session, department): def test_ac_can_add_new_unit_in_their_dept(mockdata, client, session): with current_app.test_request_context(): login_ac(client) + user = User.query.filter_by(ac_department_id=AC_DEPT).first() department = Department.query.filter_by(id=AC_DEPT).first() - form = AddUnitForm(description="Test", department=department.id) + form = AddUnitForm( + description="Test", department=department.id, created_by=user.id + ) rv = client.post( url_for("main.add_unit"), data=form.data, follow_redirects=True @@ -1614,9 +1667,11 @@ def test_admin_can_add_new_officer_with_suffix( ): with current_app.test_request_context(): login_admin(client) + user = User.query.filter_by(is_administrator=True).first() + links = [ - LinkForm(url=faker.url(), link_type="link").data, - LinkForm(url=faker.url(), link_type="video").data, + LinkForm(url=faker.url(), link_type="link", created_by=user.id).data, + LinkForm(url=faker.url(), link_type="video", created_by=user.id).data, ] job = Job.query.filter_by(department_id=department.id).first() form = AddOfficerForm( @@ -1631,6 +1686,7 @@ def test_admin_can_add_new_officer_with_suffix( department=department.id, birth_year=1990, links=links, + created_by=user.id, ) data = process_form_data(form.data) @@ -1756,6 +1812,7 @@ def test_assignments_csv(mockdata, client, session, department): def test_incidents_csv(mockdata, client, session, department, faker): with current_app.test_request_context(): login_admin(client) + user = User.query.filter_by(is_administrator=True).first() # Delete existing incidents for chosen department Incident.query.filter_by(department_id=department.id).delete() @@ -1763,9 +1820,11 @@ def test_incidents_csv(mockdata, client, session, department, faker): incident_date = datetime(2000, 5, 25, 1, 45) report_number = "42" - address_form = LocationForm(street_name="ABCDE", city="FGHI", state="IA") - link_form = LinkForm(url=faker.url(), link_type="video") - license_plates_form = LicensePlateForm(state="AZ") + address_form = LocationForm( + street_name="ABCDE", city="FGHI", state="IA", created_by=user.id + ) + link_form = LinkForm(url=faker.url(), link_type="video", created_by=user.id) + license_plates_form = LicensePlateForm(state="AZ", created_by=user.id) form = IncidentForm( date_field=str(incident_date.date()), time_field=str(incident_date.time()), @@ -1777,6 +1836,7 @@ def test_incidents_csv(mockdata, client, session, department, faker): links=[link_form.data], license_plates=[license_plates_form.data], officers=[], + created_by=user.id, ) # add the incident rv = client.post( @@ -2176,9 +2236,14 @@ def test_edit_officers_with_blank_uids(mockdata, client, session): def test_admin_can_add_salary(mockdata, client, session): with current_app.test_request_context(): login_admin(client) + user = User.query.filter_by(is_administrator=True).first() form = SalaryForm( - salary="123456.78", overtime_pay="666.66", year=2019, is_fiscal_year=False + salary="123456.78", + overtime_pay="666.66", + year=2019, + is_fiscal_year=False, + created_by=user.id, ) rv = client.post( @@ -2199,9 +2264,14 @@ def test_admin_can_add_salary(mockdata, client, session): def test_ac_can_add_salary_in_their_dept(mockdata, client, session): with current_app.test_request_context(): login_ac(client) + user = User.query.filter_by(ac_department_id=AC_DEPT).first() form = SalaryForm( - salary="123456.78", overtime_pay="666.66", year=2019, is_fiscal_year=False + salary="123456.78", + overtime_pay="666.66", + year=2019, + is_fiscal_year=False, + created_by=user.id, ) officer = Officer.query.filter_by(department_id=AC_DEPT).first() @@ -2243,12 +2313,17 @@ def test_ac_cannot_add_non_dept_salary(mockdata, client, session): def test_admin_can_edit_salary(mockdata, client, session): with current_app.test_request_context(): login_admin(client) + user = User.query.filter_by(is_administrator=True).first() # Remove existing salaries Salary.query.filter_by(officer_id=1).delete() form = SalaryForm( - salary="123456.78", overtime_pay="666.66", year=2019, is_fiscal_year=False + salary="123456.78", + overtime_pay="666.66", + year=2019, + is_fiscal_year=False, + created_by=user.id, ) rv = client.post( @@ -2260,7 +2335,7 @@ def test_admin_can_edit_salary(mockdata, client, session): assert "Added new salary" in rv.data.decode(ENCODING_UTF_8) assert "$123,456.78" in rv.data.decode(ENCODING_UTF_8) - form = SalaryForm(salary="150000") + form = SalaryForm(salary="150000", created_by=user.id) officer = Officer.query.filter_by(id=1).one() rv = client.post( @@ -2284,6 +2359,7 @@ def test_admin_can_edit_salary(mockdata, client, session): def test_ac_can_edit_salary_in_their_dept(mockdata, client, session): with current_app.test_request_context(): login_ac(client) + user = User.query.filter_by(ac_department_id=AC_DEPT).first() officer = Officer.query.filter_by(department_id=AC_DEPT).first() officer_id = officer.id @@ -2291,7 +2367,11 @@ def test_ac_can_edit_salary_in_their_dept(mockdata, client, session): Salary.query.filter_by(officer_id=officer_id).delete() form = SalaryForm( - salary="123456.78", overtime_pay="666.66", year=2019, is_fiscal_year=False + salary="123456.78", + overtime_pay="666.66", + year=2019, + is_fiscal_year=False, + created_by=user.id, ) rv = client.post( @@ -2303,7 +2383,7 @@ def test_ac_can_edit_salary_in_their_dept(mockdata, client, session): assert "Added new salary" in rv.data.decode(ENCODING_UTF_8) assert "$123,456.78" in rv.data.decode(ENCODING_UTF_8) - form = SalaryForm(salary="150000") + form = SalaryForm(salary="150000", created_by=user.id) officer = Officer.query.filter_by(id=officer_id).one() rv = client.post( @@ -2397,7 +2477,7 @@ def test_get_department_ranks_with_no_department(mockdata, client, session): def test_admin_can_add_link_to_officer_profile(mockdata, client, session): with current_app.test_request_context(): login_admin(client) - admin = User.query.filter_by(email="jen@example.org").first() + admin = User.query.filter_by(is_administrator=True).first() officer = Officer.query.first() form = OfficerLinkForm( @@ -2406,7 +2486,7 @@ def test_admin_can_add_link_to_officer_profile(mockdata, client, session): author="OJB", url="https://bpdwatch.com", link_type="link", - creator_id=admin.id, + created_by=admin.id, officer_id=officer.id, ) @@ -2424,7 +2504,7 @@ def test_admin_can_add_link_to_officer_profile(mockdata, client, session): def test_ac_can_add_link_to_officer_profile_in_their_dept(mockdata, client, session): with current_app.test_request_context(): login_ac(client) - ac = User.query.filter_by(email="raq929@example.org").first() + ac = User.query.filter_by(ac_department_id=AC_DEPT).first() officer = Officer.query.filter_by(department_id=AC_DEPT).first() form = OfficerLinkForm( @@ -2433,7 +2513,7 @@ def test_ac_can_add_link_to_officer_profile_in_their_dept(mockdata, client, sess author="OJB", url="https://bpdwatch.com", link_type="link", - creator_id=ac.id, + created_by=ac.id, officer_id=officer.id, ) @@ -2453,7 +2533,7 @@ def test_ac_cannot_add_link_to_officer_profile_not_in_their_dept( ): with current_app.test_request_context(): login_ac(client) - ac = User.query.filter_by(email="raq929@example.org").first() + ac = User.query.filter_by(ac_department_id=AC_DEPT).first() officer = Officer.query.except_( Officer.query.filter_by(department_id=AC_DEPT) ).first() @@ -2464,7 +2544,7 @@ def test_ac_cannot_add_link_to_officer_profile_not_in_their_dept( author="OJB", url="https://bpdwatch.com", link_type="link", - creator_id=ac.id, + created_by=ac.id, officer_id=officer.id, ) @@ -2491,7 +2571,7 @@ def test_admin_can_edit_link_on_officer_profile(mockdata, client, session): author=link.author, url=link.url, link_type=link.link_type, - creator_id=link.creator_id, + created_by=link.created_by, officer_id=officer.id, ) @@ -2509,7 +2589,7 @@ def test_admin_can_edit_link_on_officer_profile(mockdata, client, session): def test_ac_can_edit_link_on_officer_profile_in_their_dept(mockdata, client, session): with current_app.test_request_context(): login_ac(client) - ac = User.query.filter_by(email="raq929@example.org").first() + ac = User.query.filter_by(ac_department_id=AC_DEPT).first() # Officer from department with id AC_DEPT and no links officer = ( Officer.query.filter_by(department_id=AC_DEPT) @@ -2526,7 +2606,7 @@ def test_ac_can_edit_link_on_officer_profile_in_their_dept(mockdata, client, ses author="OJB", url="https://bpdwatch.com", link_type="link", - creator_id=ac.id, + created_by=ac.id, officer_id=officer.id, ) @@ -2547,7 +2627,7 @@ def test_ac_can_edit_link_on_officer_profile_in_their_dept(mockdata, client, ses author=link.author, url=link.url, link_type=link.link_type, - creator_id=link.creator_id, + created_by=link.created_by, officer_id=officer.id, ) @@ -2567,7 +2647,7 @@ def test_ac_cannot_edit_link_on_officer_profile_not_in_their_dept( ): with current_app.test_request_context(): login_admin(client) - admin = User.query.filter_by(email="jen@example.org").first() + admin = User.query.filter_by(is_administrator=True).first() # Officer from another department (not id AC_DEPT) and no links officer = ( Officer.query.filter(Officer.department_id != AC_DEPT) @@ -2584,7 +2664,7 @@ def test_ac_cannot_edit_link_on_officer_profile_not_in_their_dept( author="OJB", url="https://bpdwatch.com", link_type="link", - creator_id=admin.id, + created_by=admin.id, officer_id=officer.id, ) @@ -2607,7 +2687,7 @@ def test_ac_cannot_edit_link_on_officer_profile_not_in_their_dept( author=link.author, url=link.url, link_type=link.link_type, - creator_id=link.creator_id, + created_by=link.created_by, officer_id=officer.id, ) @@ -2648,7 +2728,7 @@ def test_ac_can_delete_link_from_officer_profile_in_their_dept( ): with current_app.test_request_context(): login_ac(client) - ac = User.query.filter_by(email="raq929@example.org").first() + ac = User.query.filter_by(ac_department_id=AC_DEPT).first() # Officer from department with id AC_DEPT and no links officer = ( Officer.query.filter_by(department_id=AC_DEPT) @@ -2665,7 +2745,7 @@ def test_ac_can_delete_link_from_officer_profile_in_their_dept( author="OJB", url="https://bpdwatch.com", link_type="link", - creator_id=ac.id, + created_by=ac.id, officer_id=officer.id, ) @@ -2694,7 +2774,7 @@ def test_ac_cannot_delete_link_from_officer_profile_not_in_their_dept( ): with current_app.test_request_context(): login_admin(client) - admin = User.query.filter_by(email="jen@example.org").first() + admin = User.query.filter_by(is_administrator=True).first() # Officer from another department (not id AC_DEPT) and no links officer = ( Officer.query.filter(Officer.department_id != AC_DEPT) @@ -2711,7 +2791,7 @@ def test_ac_cannot_delete_link_from_officer_profile_not_in_their_dept( author="OJB", url="https://bpdwatch.com", link_type="link", - creator_id=admin.id, + created_by=admin.id, officer_id=officer.id, ) diff --git a/OpenOversight/tests/routes/test_user_api.py b/OpenOversight/tests/routes/test_user_api.py index 3e3df0d47..d7c0e0b88 100644 --- a/OpenOversight/tests/routes/test_user_api.py +++ b/OpenOversight/tests/routes/test_user_api.py @@ -1,4 +1,3 @@ -# Routing and view tests from http import HTTPMethod, HTTPStatus import pytest @@ -8,12 +7,7 @@ from OpenOversight.app.models.database import User, db from OpenOversight.app.utils.constants import ENCODING_UTF_8 from OpenOversight.tests.conftest import AC_DEPT -from OpenOversight.tests.routes.route_helpers import ( - ADMIN_EMAIL, - login_ac, - login_admin, - login_user, -) +from OpenOversight.tests.routes.route_helpers import login_ac, login_admin, login_user routes_methods = [ @@ -63,14 +57,13 @@ def test_admin_can_update_users_to_ac(mockdata, client, session): login_admin(client) user = User.query.except_(User.query.filter_by(is_administrator=True)).first() - user_id = user.id form = EditUserForm( is_area_coordinator=True, ac_department=AC_DEPT, submit=True ) rv = client.post( - url_for("auth.edit_user", user_id=user_id), + url_for("auth.edit_user", user_id=user.id), data=form.data, follow_redirects=True, ) @@ -84,12 +77,11 @@ def test_admin_cannot_update_to_ac_without_department(mockdata, client, session) login_admin(client) user = User.query.except_(User.query.filter_by(is_administrator=True)).first() - user_id = user.id form = EditUserForm(is_area_coordinator=True, submit=True) rv = client.post( - url_for("auth.edit_user", user_id=user_id), + url_for("auth.edit_user", user_id=user.id), data=form.data, follow_redirects=True, ) @@ -103,14 +95,13 @@ def test_admin_can_update_users_to_admin(mockdata, client, session): login_admin(client) user = User.query.except_(User.query.filter_by(is_administrator=True)).first() - user_id = user.id form = EditUserForm( is_area_coordinator=False, is_administrator=True, submit=True ) rv = client.post( - url_for("auth.edit_user", user_id=user_id), + url_for("auth.edit_user", user_id=user.id), data=form.data, follow_redirects=True, ) @@ -124,21 +115,21 @@ def test_admin_can_delete_user(mockdata, client, session): login_admin(client) user = User.query.first() - user_id = user.id - username = user.username rv = client.get( - url_for("auth.delete_user", user_id=user_id), + url_for("auth.delete_user", user_id=user.id), ) assert b"Are you sure you want to delete this user?" in rv.data rv = client.post( - url_for("auth.delete_user", user_id=user_id), follow_redirects=True + url_for("auth.delete_user", user_id=user.id), follow_redirects=True ) - assert f"User {username} has been deleted!" in rv.data.decode(ENCODING_UTF_8) - assert not User.query.get(user_id) + assert f"User {user.username} has been deleted!" in rv.data.decode( + ENCODING_UTF_8 + ) + assert not User.query.get(user.id) def test_admin_cannot_delete_other_admin(mockdata, client, session): @@ -148,14 +139,13 @@ def test_admin_cannot_delete_other_admin(mockdata, client, session): user = User(is_administrator=True, email="another_user@example.org") session.add(user) session.commit() - user_id = user.id rv = client.post( - url_for("auth.delete_user", user_id=user_id), follow_redirects=True + url_for("auth.delete_user", user_id=user.id), follow_redirects=True ) assert rv.status_code == HTTPStatus.FORBIDDEN - assert User.query.get(user_id) is not None + assert User.query.get(user.id) is not None def test_admin_can_disable_user(mockdata, client, session): @@ -164,7 +154,6 @@ def test_admin_can_disable_user(mockdata, client, session): # just need to make sure to not select the admin user user = User.query.filter_by(is_administrator=False).first() - user_id = user.id assert not user.is_disabled @@ -174,14 +163,14 @@ def test_admin_can_disable_user(mockdata, client, session): ) rv = client.post( - url_for("auth.edit_user", user_id=user_id), + url_for("auth.edit_user", user_id=user.id), data=form.data, follow_redirects=True, ) assert "updated!" in rv.data.decode(ENCODING_UTF_8) - user = User.query.get(user_id) + user = User.query.get(user.id) assert user.is_disabled @@ -189,8 +178,7 @@ def test_admin_cannot_disable_self(mockdata, client, session): with current_app.test_request_context(): login_admin(client) - user = User.query.filter_by(email=ADMIN_EMAIL).first() - user_id = user.id + user = User.query.filter_by(is_administrator=True).first() assert not user.is_disabled @@ -200,14 +188,14 @@ def test_admin_cannot_disable_self(mockdata, client, session): ) rv = client.post( - url_for("auth.edit_user", user_id=user_id), + url_for("auth.edit_user", user_id=user.id), data=form.data, follow_redirects=True, ) assert "You cannot edit your own account!" in rv.data.decode(ENCODING_UTF_8) - user = User.query.get(user_id) + user = User.query.get(user.id) assert not user.is_disabled @@ -216,11 +204,10 @@ def test_admin_can_enable_user(mockdata, client, session): login_admin(client) user = User.query.filter_by(is_administrator=False).first() - user_id = user.id user.is_disabled = True db.session.commit() - user = User.query.get(user_id) + user = User.query.get(user.id) assert user.is_disabled form = EditUserForm( @@ -229,14 +216,14 @@ def test_admin_can_enable_user(mockdata, client, session): ) rv = client.post( - url_for("auth.edit_user", user_id=user_id), + url_for("auth.edit_user", user_id=user.id), data=form.data, follow_redirects=True, ) assert "updated!" in rv.data.decode(ENCODING_UTF_8) - user = User.query.get(user_id) + user = User.query.get(user.id) assert not user.is_disabled @@ -245,21 +232,20 @@ def test_admin_can_resend_user_confirmation_email(mockdata, client, session): login_admin(client) user = User.query.filter_by(confirmed=False).first() - user_id = user.id - email = user.email form = EditUserForm( resend=True, ) rv = client.post( - url_for("auth.edit_user", user_id=user_id), + url_for("auth.edit_user", user_id=user.id), data=form.data, follow_redirects=True, ) - assert f"A new confirmation email has been sent to {email}." in rv.data.decode( - ENCODING_UTF_8 + assert ( + f"A new confirmation email has been sent to {user.email}." + in rv.data.decode(ENCODING_UTF_8) ) @@ -300,11 +286,10 @@ def test_admin_can_approve_user(mockdata, client, session): login_admin(client) user = User.query.filter_by(is_administrator=False).first() - user_id = user.id user.approved = False db.session.commit() - user = User.query.get(user_id) + user = User.query.get(user.id) assert not user.approved form = EditUserForm( @@ -313,14 +298,14 @@ def test_admin_can_approve_user(mockdata, client, session): ) rv = client.post( - url_for("auth.edit_user", user_id=user_id), + url_for("auth.edit_user", user_id=user.id), data=form.data, follow_redirects=True, ) assert "updated!" in rv.data.decode(ENCODING_UTF_8) - user = User.query.get(user_id) + user = User.query.get(user.id) assert user.approved @@ -353,12 +338,11 @@ def test_admin_approval_sends_confirmation_email( login_admin(client) user = User.query.filter_by(is_administrator=False).first() - user_id = user.id user.approved = currently_approved user.confirmed = currently_confirmed db.session.commit() - user = User.query.get(user_id) + user = User.query.get(user.id) assert user.approved == currently_approved assert user.confirmed == currently_confirmed @@ -369,7 +353,7 @@ def test_admin_approval_sends_confirmation_email( ) rv = client.post( - url_for("auth.edit_user", user_id=user_id), + url_for("auth.edit_user", user_id=user.id), data=form.data, follow_redirects=True, ) @@ -379,5 +363,5 @@ def test_admin_approval_sends_confirmation_email( ) == should_send_email assert "updated!" in rv.data.decode(ENCODING_UTF_8) - user = User.query.get(user_id) + user = User.query.get(user.id) assert user.approved diff --git a/OpenOversight/tests/test_commands.py b/OpenOversight/tests/test_commands.py index 5272c2966..b8750d0f6 100644 --- a/OpenOversight/tests/test_commands.py +++ b/OpenOversight/tests/test_commands.py @@ -28,6 +28,7 @@ Officer, Salary, Unit, + User, ) from OpenOversight.app.utils.choices import DEPARTMENT_STATE_CHOICES from OpenOversight.app.utils.db import get_officer @@ -58,8 +59,6 @@ def run_command_print_output(cli, args=None, **kwargs): """ runner = CliRunner() result = runner.invoke(cli, args=args, **kwargs) - print(result.output) - print(result.stderr_bytes) if result.exception is not None: print(result.exception) print(traceback.print_exception(*result.exc_info)) @@ -497,10 +496,9 @@ def test_csv_new_salary(csvfile): def test_bulk_add_officers__success(session, department_without_officers, csv_path): + user = User.query.filter_by(is_administrator=False).first() # generate two officers with different names - first_officer = generate_officer(department_without_officers) - print(Job.query.all()) - print(Job.query.filter_by(department=department_without_officers).all()) + first_officer = generate_officer(department_without_officers, user) job = ( Job.query.filter_by(department=department_without_officers).filter_by(order=1) ).first() @@ -509,15 +507,15 @@ def test_bulk_add_officers__success(session, department_without_officers, csv_pa fo_ln = first_officer.last_name session.add(first_officer) session.commit() - assignment = Assignment(baseofficer=first_officer, job_id=job.id) + assignment = Assignment(base_officer=first_officer, job_id=job.id) session.add(assignment) session.commit() - different_officer = generate_officer(department_without_officers) + different_officer = generate_officer(department_without_officers, user) different_officer.job = job do_fn = different_officer.first_name do_ln = different_officer.last_name session.add(different_officer) - assignment = Assignment(baseofficer=different_officer, job=job) + assignment = Assignment(base_officer=different_officer, job=job, created_by=user.id) session.add(assignment) session.commit() @@ -590,15 +588,16 @@ def test_bulk_add_officers__success(session, department_without_officers, csv_pa def test_bulk_add_officers__duplicate_name(session, department, csv_path): # two officers with the same name + user = User.query.filter_by(is_administrator=False).first() first_name = "James" last_name = "Smith" - first_officer = generate_officer(department) + first_officer = generate_officer(department, user) first_officer.first_name = first_name first_officer.last_name = last_name session.add(first_officer) session.commit() - different_officer = generate_officer(department) + different_officer = generate_officer(department, user) different_officer.first_name = first_name different_officer.last_name = last_name session.add(different_officer) @@ -634,8 +633,9 @@ def test_bulk_add_officers__duplicate_name(session, department, csv_path): def test_bulk_add_officers__write_static_null_field(session, department, csv_path): + user = User.query.filter_by(is_administrator=False).first() # start with an officer whose birth_year is missing - officer = generate_officer(department) + officer = generate_officer(department, user) officer.birth_year = None session.add(officer) session.commit() @@ -677,8 +677,9 @@ def test_bulk_add_officers__write_static_null_field(session, department, csv_pat def test_bulk_add_officers__write_static_field_no_flag(session, department, csv_path): + user = User.query.filter_by(is_administrator=False).first() # officer with birth year set - officer = generate_officer(department) + officer = generate_officer(department, user) old_birth_year = 1979 officer.birth_year = old_birth_year session.add(officer) @@ -723,8 +724,9 @@ def test_bulk_add_officers__write_static_field_no_flag(session, department, csv_ def test_bulk_add_officers__write_static_field__flag_set(session, department, csv_path): + user = User.query.filter_by(is_administrator=False).first() # officer with birth year set - officer = generate_officer(department) + officer = generate_officer(department, user) officer.birth_year = 1979 session.add(officer) session.commit() @@ -771,9 +773,10 @@ def test_bulk_add_officers__write_static_field__flag_set(session, department, cs def test_bulk_add_officers__no_create_flag( session, department_without_officers, csv_path ): + user = User.query.filter_by(is_administrator=False).first() # department with one officer department_id = department_without_officers.id - officer = generate_officer(department_without_officers) + officer = generate_officer(department_without_officers, user) officer.gender = None session.add(officer) session.commit() @@ -821,13 +824,13 @@ def test_bulk_add_officers__no_create_flag( assert result.exception is None # confirm that only one officer is in database and information was updated - print(Officer.query.filter_by(department_id=department_id).all()) officer = Officer.query.filter_by(department_id=department_id).one() assert officer.unique_internal_identifier == officer_uuid assert officer.gender == officer_gender_updated def test_advanced_csv_import__success(session, department, test_csv_dir): + user = User.query.filter_by(is_administrator=False).first() # make sure department name aligns with the csv files assert department.name == SPRINGFIELD_PD.name assert department.state == SPRINGFIELD_PD.state @@ -839,6 +842,7 @@ def test_advanced_csv_import__success(session, department, test_csv_dir): first_name="Already", last_name="InDatabase", birth_year=1951, + created_by=user.id, ) session.add(officer) @@ -848,6 +852,7 @@ def test_advanced_csv_import__success(session, department, test_csv_dir): star_no="4567", start_date=datetime.date(2020, 1, 1), job_id=department.jobs[0].id, + created_by=user.id, ) session.add(assignment) @@ -857,6 +862,7 @@ def test_advanced_csv_import__success(session, department, test_csv_dir): officer_id=officer.id, year=2018, is_fiscal_year=False, + created_by=user.id, ) session.add(salary) @@ -866,11 +872,17 @@ def test_advanced_csv_import__success(session, department, test_csv_dir): department_id=1, description="description", time=datetime.time(23, 45, 16), + created_by=user.id, ) incident.officers = [officer] session.add(incident) - link = Link(id=55051, title="Existing Link", url="https://www.example.org") + link = Link( + id=55051, + title="Existing Link", + url="https://www.example.org", + created_by=user.id, + ) session.add(link) officer.links = [link] @@ -897,7 +909,6 @@ def test_advanced_csv_import__success(session, department, test_csv_dir): assert result.exception is None assert result.exit_code == 0 - print(list(Officer.query.all())) all_officers = { officer.unique_internal_identifier: officer for officer in Officer.query.filter_by(department_id=1).all() @@ -1022,12 +1033,14 @@ def _create_csv(data, path, csv_file_name): def test_advanced_csv_import__force_create(session, department, tmp_path): + user = User.query.filter_by(is_administrator=False).first() tmp_path = str(tmp_path) other_department = Department( name="Other department", short_name="OPD", state=random.choice(DEPARTMENT_STATE_CHOICES)[0], + created_by=user.id, ) session.add(other_department) @@ -1036,6 +1049,7 @@ def test_advanced_csv_import__force_create(session, department, tmp_path): department_id=other_department.id, first_name="Already", last_name="InDatabase", + created_by=user.id, ) session.add(officer) session.flush() @@ -1150,11 +1164,13 @@ def test_advanced_csv_import__force_create(session, department, tmp_path): def test_advanced_csv_import__overwrite_assignments(session, department, tmp_path): tmp_path = str(tmp_path) + user = User.query.filter_by(is_administrator=False).first() other_department = Department( name="Other department", short_name="OPD", state=random.choice(DEPARTMENT_STATE_CHOICES)[0], + created_by=user.id, ) session.add(other_department) @@ -1165,12 +1181,14 @@ def test_advanced_csv_import__overwrite_assignments(session, department, tmp_pat department_id=department.id, first_name="Already", last_name="InDatabase", + created_by=user.id, ) officer2 = Officer( id=cop2_id, department_id=department.id, first_name="Also", last_name="InDatabase", + created_by=user.id, ) a1_id = 999101 a2_id = 999102 @@ -1178,11 +1196,13 @@ def test_advanced_csv_import__overwrite_assignments(session, department, tmp_pat id=a1_id, officer_id=officer.id, job_id=Job.query.filter_by(job_title="Police Officer").first().id, + created_by=user.id, ) assignment2 = Assignment( id=a2_id, officer_id=officer2.id, job_id=Job.query.filter_by(job_title="Police Officer").first().id, + created_by=user.id, ) session.add(officer) session.add(assignment) @@ -1305,10 +1325,12 @@ def test_advanced_csv_import__missing_required_field_officers( def test_advanced_csv_import__wrong_department(session, department, tmp_path): + user = User.query.filter_by(is_administrator=False).first() other_department = Department( name="Other department", short_name="OPD", state=random.choice(DEPARTMENT_STATE_CHOICES)[0], + created_by=user.id, ) session.add(other_department) @@ -1339,15 +1361,21 @@ def test_advanced_csv_import__wrong_department(session, department, tmp_path): def test_advanced_csv_import__update_officer_different_department( session, department, tmp_path ): + user = User.query.filter_by(is_administrator=False).first() # set up data other_department = Department( name="Other department", short_name="OPD", state=random.choice(DEPARTMENT_STATE_CHOICES)[0], + created_by=user.id, ) session.add(other_department) officer = Officer( - id=99021, department_id=other_department.id, first_name="Chris", last_name="Doe" + id=99021, + department_id=other_department.id, + first_name="Chris", + last_name="Doe", + created_by=user.id, ) session.add(officer) @@ -1378,13 +1406,14 @@ def test_advanced_csv_import__update_officer_different_department( def test_advanced_csv_import__unit_other_department( session, department, department_without_officers, tmp_path ): + user = User.query.filter_by(is_administrator=False).first() # set up data - officer = generate_officer(department) + officer = generate_officer(department, user) session.add(officer) session.flush() session.add(department_without_officers) session.flush() - unit = Unit(department_id=department_without_officers.id) + unit = Unit(department_id=department_without_officers.id, created_by=user.id) session.add(unit) session.flush() diff --git a/OpenOversight/tests/test_csvs/incidents.csv b/OpenOversight/tests/test_csvs/incidents.csv index 45c9e803a..87b2eef78 100644 --- a/OpenOversight/tests/test_csvs/incidents.csv +++ b/OpenOversight/tests/test_csvs/incidents.csv @@ -1,4 +1,4 @@ -id,department name,department state,date,time,report number,description,street name,cross street1,cross street2,city,state,zip code,license plates,officer_ids,creator id,last updated id +id,department name,department state,date,time,report number,description,street name,cross street1,cross street2,city,state,zip code,license plates,officer_ids,created by,last updated by ,Springfield Police Department,IL,2020-07-20,06:30,CR-1234,Something happened,,East Ave,Main St,Chicago,IL,60603,ABC123_NY|98UMC_IL,#1|49483,, #I1,Springfield Police Department,IL,,,CR-9912,Something happened,Fake Street,Main Street,,Chicago,IL,60603,,#1,, 123456,Springfield Police Department,IL,2020-07-26,,CR-39283,Don't know where it happened,,,,,,,XYZ11,#1,, diff --git a/OpenOversight/tests/test_csvs/links.csv b/OpenOversight/tests/test_csvs/links.csv index b5ed44392..8b1c6fac8 100644 --- a/OpenOversight/tests/test_csvs/links.csv +++ b/OpenOversight/tests/test_csvs/links.csv @@ -1,4 +1,4 @@ -id,title,url,link type,description,author,creator_id,officer ids,incident ids +id,title,url,link type,description,author,created_by,officer ids,incident ids ,A Link,https://www.example.com,link,A link about officers,,,#1|49483, ,Another Link,https://www.example.com/incident,link,A link on an incident,Example Times,,,#I1 55051,Updated Link,https://www.example.org,,,,,,123456 diff --git a/OpenOversight/tests/test_models.py b/OpenOversight/tests/test_models.py index 9cd29e5fb..8508eda9a 100644 --- a/OpenOversight/tests/test_models.py +++ b/OpenOversight/tests/test_models.py @@ -370,11 +370,11 @@ def test_images_added_with_user_id(mockdata, faker): created_at=datetime.datetime.now(), department_id=1, taken_at=datetime.datetime.now(), - user_id=user_id, + created_by=user_id, ) db.session.add(new_image) db.session.commit() - saved = Image.query.filter_by(user_id=user_id).first() + saved = Image.query.filter_by(created_by=user_id).first() assert saved is not None diff --git a/docs/advanced_csv_import.rst b/docs/advanced_csv_import.rst index 665b941c5..52840ed9a 100644 --- a/docs/advanced_csv_import.rst +++ b/docs/advanced_csv_import.rst @@ -150,7 +150,7 @@ Incidents csv ^^^^^^^^^^^^^ - Required: ``id, department_name, department_state`` - Optional: ``date, time, report_number, description, street_name, cross_street1, cross_street2, city, state, zip_code, - creator_id, last_updated_id, officer_ids, license_plates`` + created_by, last_updated_by, officer_ids, license_plates`` Details: ~~~~~~~~ @@ -165,7 +165,7 @@ Details: - ``street_name`` Name of the street the incident occurred, but should not include the street number. - ``cross_street1``, ``cross_street2`` The two closest intersecting streets. - ``city``, ``state``, ``zip_code`` State needs to be in 2 letter abbreviated notation. -- ``creator_id``, ``last_updated_id`` Id of existing user shown as responsible for adding this entry. +- ``created_by``, ``last_updated_by`` Id of existing user shown as responsible for adding this entry. - ``officer_ids`` Ids of officers involved in the incident, separated by ``|``. - Each individual id can either be an integer referring to an existing officer or a string starting with ``#`` referring to a newly created officer. @@ -181,7 +181,7 @@ Details: Links csv ^^^^^^^^^ - Required: ``id, url`` -- Optional: ``title, link_type, description, author, creator_id, officer_ids, incident_ids`` +- Optional: ``title, link_type, description, author, created_by, officer_ids, incident_ids`` Details: ~~~~~~~~ @@ -190,7 +190,7 @@ Details: - ``description`` A short description of the link. - ``link_type`` Choice of ``Link``, ``YouTube Video`` and ``Other Video``. - ``author`` The source or author of the linked article, report, video. -- ``creator_id`` Id of existing user shown as responsible for adding this entry. +- ``created_by`` Id of existing user shown as responsible for adding this entry. - ``officer_ids`` Ids of officer profiles this link should be visible on, separated by ``|``. See same field in incidents above for more details. - ``incidents_ids`` Ids of incidents this link should be associated with, separated by ``|``. Just like ``officer_ids`` this can contain strings starting with ``#`` to refer to an incident created in the incident csv.