Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create generic DB_CACHE functions for db.Model classes #1011

Merged
merged 35 commits into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
8afeb80
Simpsons
michplunkett Aug 4, 2023
c1f95e9
Update test_functional.py
michplunkett Aug 4, 2023
afb805a
Update sort.html
michplunkett Aug 4, 2023
383525d
Create cache_client.py
michplunkett Aug 4, 2023
4660f78
Update cache_client.py
michplunkett Aug 4, 2023
70b9bf9
Update database.py
michplunkett Aug 4, 2023
3227fc7
Update db_cache.py
michplunkett Aug 4, 2023
364b5d2
Update database.py
michplunkett Aug 4, 2023
1892b3e
Update db_cache.py
michplunkett Aug 4, 2023
6dfe278
Update database.py
michplunkett Aug 4, 2023
70fae44
Update constants.py
michplunkett Aug 4, 2023
7b98372
Update database.py
michplunkett Aug 4, 2023
c662150
Update db_cache.py
michplunkett Aug 4, 2023
dd85fdb
Update database.py
michplunkett Aug 4, 2023
3dc0747
Update views.py
michplunkett Aug 4, 2023
dfd7a75
Update model_view.py
michplunkett Aug 4, 2023
97d9431
Delete db_cache.py
michplunkett Aug 4, 2023
f93fd98
Create database_cache.py
michplunkett Aug 4, 2023
f218ea3
Update database.py
michplunkett Aug 4, 2023
c8e31b0
Update database_cache.py
michplunkett Aug 4, 2023
d5a938c
Create test_cache.py
michplunkett Aug 4, 2023
d7571c7
Update test_cache.py
michplunkett Aug 4, 2023
64bb6c4
Update test_cache.py
michplunkett Aug 4, 2023
b9cac0e
Update test_cache.py
michplunkett Aug 5, 2023
7ee9038
Update test_cache.py
michplunkett Aug 5, 2023
fa18b16
Update database_cache.py
michplunkett Aug 7, 2023
5854925
Update database.py
michplunkett Aug 7, 2023
194e0c3
Update test_cache.py
michplunkett Aug 7, 2023
11119c3
Update database_cache.py
michplunkett Aug 7, 2023
7854415
Update views.py
michplunkett Aug 7, 2023
db9dd2a
Update model_view.py
michplunkett Aug 7, 2023
16e62c8
Update database_cache.py
michplunkett Aug 7, 2023
26ae057
Update model_view.py
michplunkett Aug 7, 2023
b76bd11
Update views.py
michplunkett Aug 7, 2023
037ec06
Update test_cache.py
michplunkett Aug 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion OpenOversight/app/main/model_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from flask_login import current_user, login_required

from OpenOversight.app.models.database import db
from OpenOversight.app.models.database_cache import remove_department_cache_entry
from OpenOversight.app.utils.auth import ac_or_admin_required
from OpenOversight.app.utils.constants import KEY_DEPT_TOTAL_INCIDENTS
from OpenOversight.app.utils.db import add_department_query
from OpenOversight.app.utils.forms import set_dynamic_default

Expand Down Expand Up @@ -65,7 +67,6 @@ def new(self, form=None):
set_dynamic_default(form.department, current_user.dept_pref_rel)
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()
Expand All @@ -74,6 +75,10 @@ def new(self, form=None):
new_obj = self.create_function(form)
db.session.add(new_obj)
db.session.commit()
if self.create_function.__name__ == "create_incident":
remove_department_cache_entry(
new_obj.department_id, KEY_DEPT_TOTAL_INCIDENTS
)
flash(f"{self.model_name} created!")
return self.get_redirect_url(obj_id=new_obj.id)
else:
Expand Down
7 changes: 7 additions & 0 deletions OpenOversight/app/main/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,14 @@
User,
db,
)
from OpenOversight.app.models.database_cache import remove_department_cache_entry
from OpenOversight.app.utils.auth import ac_or_admin_required, admin_required
from OpenOversight.app.utils.choices import AGE_CHOICES, GENDER_CHOICES, RACE_CHOICES
from OpenOversight.app.utils.cloud import crop_image, upload_image_to_s3_and_store_in_db
from OpenOversight.app.utils.constants import (
ENCODING_UTF_8,
KEY_DEPT_TOTAL_ASSIGNMENTS,
KEY_DEPT_TOTAL_OFFICERS,
KEY_OFFICERS_PER_PAGE,
KEY_TIMEZONE,
)
Expand Down Expand Up @@ -340,6 +343,9 @@ def add_assignment(officer_id):
current_user.is_area_coordinator
and officer.department_id == current_user.ac_department_id
):
remove_department_cache_entry(
officer.department_id, KEY_DEPT_TOTAL_ASSIGNMENTS
)
try:
add_new_assignment(officer_id, form)
flash("Added new assignment!")
Expand Down Expand Up @@ -925,6 +931,7 @@ def add_officer():
new_form_data[key] = "y"
form = AddOfficerForm(new_form_data)
officer = add_officer_profile(form, current_user)
remove_department_cache_entry(officer.department_id, KEY_DEPT_TOTAL_OFFICERS)
flash(f"New Officer {officer.last_name} added to OpenOversight")
return redirect(url_for("main.submit_officer_images", officer_id=officer.id))
else:
Expand Down
52 changes: 19 additions & 33 deletions OpenOversight/app/models/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
from datetime import date

from authlib.jose import JoseError, JsonWebToken
from cachetools import TTLCache, cached
from cachetools.keys import hashkey
from cachetools import cached
from flask import current_app
from flask_login import UserMixin
from flask_sqlalchemy import SQLAlchemy
Expand All @@ -13,13 +12,16 @@
from sqlalchemy.sql import func as sql_func
from werkzeug.security import check_password_hash, generate_password_hash

from OpenOversight.app.models.database_cache import (
DB_CACHE,
department_statistics_cache_key,
)
from OpenOversight.app.utils.choices import GENDER_CHOICES, RACE_CHOICES
from OpenOversight.app.utils.constants import (
ENCODING_UTF_8,
HOUR,
KEY_TOTAL_ASSIGNMENTS,
KEY_TOTAL_INCIDENTS,
KEY_TOTAL_OFFICERS,
KEY_DEPT_TOTAL_ASSIGNMENTS,
KEY_DEPT_TOTAL_INCIDENTS,
KEY_DEPT_TOTAL_OFFICERS,
)
from OpenOversight.app.validators import state_validator, url_validator

Expand Down Expand Up @@ -57,30 +59,6 @@
),
)

# 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
DATABASE_CACHE = TTLCache(maxsize=1024, ttl=12 * HOUR)


# TODO: In the singleton create functions for other model types.
def _date_updated_cache_key(update_type: str):
"""Return a key function to calculate the cache key for Department
methods using the department id and a given update type.

Department.id is used instead of a Department obj because the default Python
__hash__ is unique per obj instance, meaning multiple instances of the same
department will have different hashes.

Update type is used in the hash to differentiate between the update types we compute
per department.
"""

def _cache_key(dept: "Department"):
return hashkey(dept.id, update_type)

return _cache_key


class Department(BaseModel):
__tablename__ = "departments"
Expand Down Expand Up @@ -117,7 +95,10 @@ def to_custom_dict(self):
"unique_internal_identifier_label": self.unique_internal_identifier_label,
}

@cached(cache=DATABASE_CACHE, key=_date_updated_cache_key(KEY_TOTAL_ASSIGNMENTS))
@cached(
cache=DB_CACHE,
key=department_statistics_cache_key(KEY_DEPT_TOTAL_ASSIGNMENTS),
)
def total_documented_assignments(self):
return (
db.session.query(Assignment.id)
Expand All @@ -126,13 +107,18 @@ def total_documented_assignments(self):
.count()
)

@cached(cache=DATABASE_CACHE, key=_date_updated_cache_key(KEY_TOTAL_INCIDENTS))
@cached(
cache=DB_CACHE,
key=department_statistics_cache_key(KEY_DEPT_TOTAL_INCIDENTS),
)
def total_documented_incidents(self):
return (
db.session.query(Incident).filter(Incident.department_id == self.id).count()
)

@cached(cache=DATABASE_CACHE, key=_date_updated_cache_key(KEY_TOTAL_OFFICERS))
@cached(
cache=DB_CACHE, key=department_statistics_cache_key(KEY_DEPT_TOTAL_OFFICERS)
)
def total_documented_officers(self):
return (
db.session.query(Officer).filter(Officer.department_id == self.id).count()
Expand Down
44 changes: 44 additions & 0 deletions OpenOversight/app/models/database_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from cachetools import TTLCache
from cachetools.keys import hashkey

from OpenOversight.app.utils.constants import HOUR


DB_CACHE = TTLCache(maxsize=1024, ttl=12 * HOUR)


def _department_key(department_id: str, update_type: str):
"""Create unique department key."""
return hashkey(department_id, update_type, "Department")


def department_statistics_cache_key(update_type: str):
"""Return a key function to calculate the cache key for Department
methods using the department id and a given update type.

Department.id is used instead of a Department obj because the default Python
__hash__ is unique per obj instance, meaning multiple instances of the same
department will have different hashes.

Update type is used in the hash to differentiate between the update types we compute
per department.
"""

def _cache_key(department):
return _department_key(department.id, update_type)

return _cache_key


def has_department_cache_entry(department_id: str, update_type: str) -> bool:
"""Department key exists in cache."""
key = _department_key(department_id, update_type)
return key in DB_CACHE.keys()


def remove_department_cache_entry(department_id: str, update_type: str) -> None:
"""Remove department key from cache if it exists."""

key = _department_key(department_id, update_type)
if key in DB_CACHE.keys():
del DB_CACHE[key]
2 changes: 1 addition & 1 deletion OpenOversight/app/templates/sort.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ <h3>Your account has been disabled due to too many incorrect classifications/tag
role="button">Email us to get it enabled again</a>
</p>
{% else %}
<h3>All images have been classfied!</h3>
<h3>All images have been classified!</h3>
<p>
<a href="{{ url_for("main.submit_data") }}"
class="btn btn-lg btn-primary"
Expand Down
6 changes: 3 additions & 3 deletions OpenOversight/app/utils/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@


# Cache Key Constants
KEY_TOTAL_ASSIGNMENTS = "total_assignments"
KEY_TOTAL_INCIDENTS = "total_incidents"
KEY_TOTAL_OFFICERS = "total_officers"
KEY_DEPT_TOTAL_ASSIGNMENTS = "total_assignments"
KEY_DEPT_TOTAL_INCIDENTS = "total_incidents"
KEY_DEPT_TOTAL_OFFICERS = "total_officers"

# Config Key Constants
KEY_DATABASE_URI = "SQLALCHEMY_DATABASE_URI"
Expand Down
2 changes: 1 addition & 1 deletion OpenOversight/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def __init__(
("IVANA", "", "TINKLE"),
("SEYMOUR", "", "BUTZ"),
("HAYWOOD", "U", "CUDDLEME"),
("BEA", "", "PROBLEM"),
("BEA", "", "O'PROBLEM"),
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

("URA", "", "SNOTBALL"),
("HUGH", "", "JASS"),
]
Expand Down
Loading