Skip to content

Commit

Permalink
Cache the candidate listing (#367)
Browse files Browse the repository at this point in the history
* Cache the candidate listing

* Update the cache key name

* Reduce the number of queries made when returning ngos/candidates

* Reduce the queries for a user's logo

* Cache context processors

* Add a general cache for the Feature Flags

* Cache the user's permission flags

---------

Co-authored-by: Tudor Amariei <[email protected]>
  • Loading branch information
danniel and tudoramariei authored Nov 27, 2024
1 parent ab6978e commit 06de9fa
Show file tree
Hide file tree
Showing 10 changed files with 49 additions and 17 deletions.
6 changes: 6 additions & 0 deletions backend/accounts/models.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from auditlog.registry import auditlog
from django.conf import settings
from django.contrib.auth.models import AbstractUser, Group
from django.db import models
from django.db.models.functions import Lower
from django.utils.translation import gettext as _
from model_utils.models import TimeStampedModel

from civil_society_vote.common.cache import cache_decorator

# NOTE: If you change the group names here, make sure you also update the names in the live database before deployment
STAFF_GROUP = "Code4Romania Staff"
Expand Down Expand Up @@ -69,6 +71,7 @@ def make_staff(self):
self.is_staff = True
self.save()

@cache_decorator(cache_key_prefix="in_committee_or_staff_groups", timeout=settings.TIMEOUT_CACHE_SHORT)
def in_committee_or_staff_groups(self):
return self.groups.filter(
name__in=[
Expand All @@ -79,18 +82,21 @@ def in_committee_or_staff_groups(self):
]
).exists()

@cache_decorator(cache_key_prefix="in_commission_groups", timeout=settings.TIMEOUT_CACHE_SHORT)
def in_commission_groups(self):
return (
self.groups.filter(name__in=[COMMITTEE_GROUP, COMMITTEE_GROUP_READ_ONLY]).exists()
and not self.groups.filter(name__in=[STAFF_GROUP, SUPPORT_GROUP]).exists()
)

@cache_decorator(cache_key_prefix="in_voting_commission_groups", timeout=settings.TIMEOUT_CACHE_SHORT)
def in_voting_commission_groups(self):
return (
self.groups.filter(name=COMMITTEE_GROUP).exists()
and not self.groups.filter(name__in=[STAFF_GROUP, SUPPORT_GROUP]).exists()
)

@cache_decorator(cache_key_prefix="in_staff_groups", timeout=settings.TIMEOUT_CACHE_SHORT)
def in_staff_groups(self):
return self.groups.filter(name__in=[STAFF_GROUP, SUPPORT_GROUP]).exists()

Expand Down
4 changes: 4 additions & 0 deletions backend/civil_society_vote/common/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,7 @@ def wrapper(*args, **kwargs):
return wrapper

return decorator


def delete_cache_key(cache_key: str):
cache.delete(cache_key)
1 change: 1 addition & 0 deletions backend/hub/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,7 @@ def _flags_switch_phase(self, request, phase_name: str, enabled: List[str], disa

FeatureFlag.objects.filter(flag__in=enabled).update(is_enabled=True)
FeatureFlag.objects.filter(flag__in=disabled).update(is_enabled=False)
FeatureFlag.delete_cache()

if "enable_candidate_supporting" in enabled:
FeatureFlag.objects.filter(flag=PHASE_CHOICES.enable_candidate_supporting).update(
Expand Down
2 changes: 2 additions & 0 deletions backend/hub/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
from django.http import HttpRequest
from django.urls import reverse

from civil_society_vote.common.cache import cache_decorator
from hub.models import FLAG_CHOICES, FeatureFlag, SETTINGS_CHOICES


@cache_decorator(cache_key="hub_settings", timeout=settings.TIMEOUT_CACHE_SHORT)
def hub_settings(_: HttpRequest) -> Dict[str, Any]:
flags = {k: v for k, v in FeatureFlag.objects.all().values_list("flag", "is_enabled")}

Expand Down
12 changes: 11 additions & 1 deletion backend/hub/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from tinymce.models import HTMLField

from accounts.models import COMMITTEE_GROUP, COMMITTEE_GROUP_READ_ONLY, NGO_GROUP, STAFF_GROUP, SUPPORT_GROUP, User
from civil_society_vote.common.cache import cache_decorator, delete_cache_key
from civil_society_vote.common.formatting import get_human_readable_size

REPORTS_HELP_TEXT = (
Expand Down Expand Up @@ -144,6 +145,15 @@ class Meta:
def __str__(self):
return f"{FLAG_CHOICES[self.flag]}"

@staticmethod
def delete_cache():
delete_cache_key("feature_flags")

@staticmethod
@cache_decorator(cache_key="feature_flags", timeout=settings.TIMEOUT_CACHE_SHORT)
def get_feature_flags():
return {flag.flag: flag.is_enabled for flag in FeatureFlag.objects.all()}

@staticmethod
def flag_enabled(flag: str) -> bool:
"""
Expand All @@ -152,7 +162,7 @@ def flag_enabled(flag: str) -> bool:
if not flag:
return False

return FeatureFlag.objects.filter(flag=flag, is_enabled=True).exists()
return FeatureFlag.get_feature_flags().get(flag, False)


class BlogPost(TimeStampedModel):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@
{% for section_details in page_obj %}

<h3 id="{{ section_details.domain_key }}" class="section-title">
{{ section_details.domain.name }} ({{ section_details.items|length }})
{{ section_details.domain_name }} ({{ section_details.items|length }})
</h3>


{% include "hub/candidate/components/listing_detail.html" with candidates=section_details.items %}


{% endfor %}

7 changes: 5 additions & 2 deletions backend/hub/templates/hub/candidate/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{% load static %}
{% load spurl %}
{% load i18n %}
{% load cache %}

{% block extra-header %}
{% with header_template=CURRENT_EDITION_TYPE|add:"/edition_header.html" %}
Expand Down Expand Up @@ -59,14 +60,16 @@ <h2 class="section-title uppercase">
<div class="container">
{% if page_obj %}

{% cache listing_cache_duration "candidate_listing" %}
<div class="is-multiline infinite-container">

{% if not SINGLE_DOMAIN_ROUND %}
{% include "hub/candidate/components/listing_by_voting_domain.html" %}
{% include "hub/candidate/components/listing_by_voting_domain.html" %}
{% else %}
{% include "hub/candidate/components/listing_detail.html" %}
{% endif %}
</div>
{% endcache %}

{% endif %}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{% for section_details in page_obj %}

<h3 id="{{ section_details.domain_key }}" class="section-title">
{{ section_details.domain.name }} ({{ section_details.items|length }})
{{ section_details.domain_name }} ({{ section_details.items|length }})
</h3>

{% for ngo in section_details.items %}
Expand All @@ -14,4 +14,3 @@ <h3 id="{{ section_details.domain_key }}" class="section-title">
{% endfor %}

{% endfor %}

4 changes: 2 additions & 2 deletions backend/hub/templatetags/hub_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from django.utils.translation import gettext as _

from accounts.models import User
from hub.models import CandidateConfirmation
from hub.models import CandidateConfirmation, Organization

register = template.Library()

Expand Down Expand Up @@ -52,7 +52,7 @@ def org_logo(user, width=settings.AVATAR_DEFAULT_SIZE, height=None, **kwargs):
if not user:
return ""

org = user.organization
org = Organization.objects.filter(users__pk=user.pk).only("logo").first()
logo_url = static(settings.AVATAR_DEFAULT_URL)
if org and org.logo:
logo_url = org.logo.url
Expand Down
24 changes: 16 additions & 8 deletions backend/hub/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,29 +64,36 @@
def group_queryset_by_domain(
queryset: QuerySet, *, domain_variable_name: str, sort_variable: str = "name"
) -> List[Dict[str, Union[Domain, List[Union[Organization, Candidate]]]]]:

queryset_by_domain_dict: Dict[Domain, List[Union[Organization, Candidate]]] = {}

all_domains = dict(Domain.objects.values_list("pk", "name"))

domain_variable_name = f"{domain_variable_name}_id"

for element in queryset:
domain: Domain = getattr(element, domain_variable_name)
if domain not in queryset_by_domain_dict:
queryset_by_domain_dict[domain] = []
element_domain_pk: Domain = getattr(element, domain_variable_name)
if element_domain_pk not in queryset_by_domain_dict:
queryset_by_domain_dict[element_domain_pk] = []

queryset_by_domain_dict[domain].append(element)
queryset_by_domain_dict[element_domain_pk].append(element)

queryset_by_domain_list = []
for domain, query_item in queryset_by_domain_dict.items():
snake_case_domain_key = "".join(filter(_filter_letter, domain.name)).lower().replace(" ", "_")
for domain_pk, query_item in queryset_by_domain_dict.items():
domain_name = all_domains.get(domain_pk)
snake_case_domain_key = "".join(filter(_filter_letter, domain_name)).lower().replace(" ", "_")
normalized_domain_key = unicodedata.normalize("NFKD", snake_case_domain_key).encode("ascii", "ignore")

queryset_by_domain_list.append(
{
"domain": domain,
"domain_pk": domain_pk,
"domain_name": domain_name,
"domain_key": normalized_domain_key.decode("utf-8"),
"items": sorted(query_item, key=lambda x: getattr(x, sort_variable)),
}
)

queryset_by_domain_list = sorted(queryset_by_domain_list, key=lambda x: x["domain"].pk)
queryset_by_domain_list = sorted(queryset_by_domain_list, key=lambda x: x["domain_pk"])

return queryset_by_domain_list

Expand Down Expand Up @@ -569,6 +576,7 @@ def _get_candidate_counters(self):

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["listing_cache_duration"] = settings.TIMEOUT_CACHE_SHORT
context["current_search"] = self.request.GET.get("q", "")

context["should_display_candidates"] = False
Expand Down

0 comments on commit 06de9fa

Please sign in to comment.