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

Caching Project list & ordering them by view count #208

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions core/constants.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ONE_DAY_IN_SECONDS = 60 * 60 * 24
ONE_WEEK_IN_SECONDS = ONE_DAY_IN_SECONDS * 7
VIEWS_CACHING_TIMEOUT = 60 * 1
LIKES_CACHING_TIMEOUT = 60 * 1
VIEWS_CACHING_TIMEOUT = ONE_DAY_IN_SECONDS
LIKES_CACHING_TIMEOUT = 60 * 60 * 8
39 changes: 39 additions & 0 deletions core/services.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.core.cache import cache
from django.db.models import Count

from core.constants import VIEWS_CACHING_TIMEOUT
from core.models import Like, View, Link
Expand Down Expand Up @@ -64,12 +65,23 @@ def add_view(obj, user):
view, is_created = View.objects.get_or_create(
content_type=obj_type, object_id=obj.id, user=user
)
views_count = cache.get(f"views_count_{obj_type}_{obj.id}", None)
if views_count is not None:
cache.set(
f"views_count_{obj_type}_{obj.id}", views_count + 1, VIEWS_CACHING_TIMEOUT
)

return view


def remove_view(obj, user):
obj_type = ContentType.objects.get_for_model(obj)
View.objects.filter(content_type=obj_type, object_id=obj.id, user=user).delete()
views_count = cache.get(f"views_count_{obj_type}_{obj.id}", None)
if views_count is not None:
cache.set(
f"views_count_{obj_type}_{obj.id}", views_count - 1, VIEWS_CACHING_TIMEOUT
)


def is_viewer(obj, user) -> bool:
Expand All @@ -85,6 +97,33 @@ def get_viewers(obj):
return User.objects.filter(views__content_type=obj_type, views__object_id=obj.id)


def cache_views_for_many_objects(objs):
"""
Set cached views count for many objects in 1 SQL query

Args:
objs:
List of objects that need their views retrieved

Returns: QuerySet[dict[object_id, count]]
"""
obj_type = ContentType.objects.get_for_model(objs[0])
ids = [obj.id for obj in objs]
data = (
View.objects.filter(content_type=obj_type, object_id__in=ids)
.values("object_id")
.annotate(count=Count("object_id"))
)
for i in data:
cache.set(f"views_count_{obj_type}_{i.object_id}", i.count, VIEWS_CACHING_TIMEOUT)
return data


def get_views_count_cached(obj):
obj_type = ContentType.objects.get_for_model(obj)
return cache.get(f"views_count_{obj_type}_{obj.id}", None)


def get_views_count(obj):
obj_type = ContentType.objects.get_for_model(obj)
# cache this
Expand Down
12 changes: 1 addition & 11 deletions projects/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ class ProjectDetailSerializer(serializers.ModelSerializer):
vacancies = ProjectVacancyListSerializer(many=True, read_only=True)
short_description = serializers.SerializerMethodField()
industry_id = serializers.IntegerField(required=False)
likes_count = serializers.SerializerMethodField(method_name="count_likes")
views_count = serializers.SerializerMethodField(method_name="count_views")
links = serializers.SerializerMethodField()
partner_programs_tags = serializers.SerializerMethodField()
Expand All @@ -95,9 +94,6 @@ def validate(self, data):
def get_short_description(cls, project):
return project.get_short_description()

def count_likes(self, project):
return get_likes_count(project)

def count_views(self, project):
return get_views_count(project)

Expand Down Expand Up @@ -128,7 +124,6 @@ class Meta:
"datetime_created",
"datetime_updated",
"views_count",
"likes_count",
"cover",
"partner_programs_tags",
]
Expand All @@ -141,7 +136,6 @@ class Meta:


class ProjectListSerializer(serializers.ModelSerializer):
likes_count = serializers.SerializerMethodField(method_name="count_likes")
views_count = serializers.SerializerMethodField(method_name="count_views")
collaborator_count = serializers.SerializerMethodField(
method_name="get_collaborator_count"
Expand All @@ -168,9 +162,6 @@ def get_short_description(cls, project):
def get_collaborator_count(cls, obj):
return len(obj.collaborator_set.all())

def count_likes(self, obj):
return get_likes_count(obj)

class Meta:
model = Project
fields = [
Expand All @@ -186,12 +177,11 @@ class Meta:
"collaborator_count",
"vacancies",
"datetime_created",
"likes_count",
"views_count",
"partner_programs_tags",
]

read_only_fields = ["leader", "views_count", "likes_count"]
read_only_fields = ["leader", "views_count"]

def is_valid(self, *, raise_exception=False):
return super().is_valid(raise_exception=raise_exception)
Expand Down
41 changes: 40 additions & 1 deletion projects/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.contrib.auth import get_user_model
from django.core.cache import cache
from django.db.models import Q
from django_filters import rest_framework as filters
from rest_framework import generics, permissions, status
Expand All @@ -8,7 +9,13 @@

from core.permissions import IsStaffOrReadOnly
from core.serializers import SetLikedSerializer
from core.services import add_view, set_like
from core.services import (
add_view,
set_like,
get_views_count,
get_views_count_cached,
cache_views_for_many_objects,
)
from partner_programs.models import PartnerProgram, PartnerProgramUserProfile
from projects.filters import ProjectFilter
from projects.constants import VERBOSE_STEPS
Expand Down Expand Up @@ -49,6 +56,35 @@ class ProjectList(generics.ListCreateAPIView):
filter_backends = (filters.DjangoFilterBackend,)
filterset_class = ProjectFilter

def list(self, request, *args, **kwargs):
# check that we are not filtering
if not request.query_params:
cached_view = cache.get("project_list_view", None)
if cached_view is not None:
return Response(cached_view)

queryset = self.filter_queryset(self.get_queryset())
# order by view count. View Count is a cached value for each project

# check random project for whether it's views are in the cache
# if not, then cache all projects' views
if get_views_count_cached(queryset.first()) is None:
project_views = cache_views_for_many_objects(queryset)
project_views_dict = {
view.object_id: project_views[view.object_id] for view in queryset
}
views = {project.id: project_views_dict[project.id] for project in queryset}
else:
views = {project.id: get_views_count(project) for project in queryset}

# TODO: add paging ASAP
queryset = sorted(queryset, key=lambda project: views[project.id], reverse=True)
serializer = self.get_serializer(queryset, many=True)
if not request.query_params:
data = serializer.data
cache.set("project_list_view", data, 60 * 15)
return Response(serializer.data)

def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
Expand All @@ -72,6 +108,7 @@ def create(self, request, *args, **kwargs):
)

headers = self.get_success_headers(serializer.data)
cache.delete("project_list_view")
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

def post(self, request, *args, **kwargs):
Expand Down Expand Up @@ -136,6 +173,7 @@ def put(self, request, pk, **kwargs):
status=status.HTTP_400_BAD_REQUEST,
)
check_related_fields_update(request.data, pk)
cache.delete("project_list_view")
return super(ProjectDetail, self).put(request, pk)

def patch(self, request, pk, **kwargs):
Expand All @@ -154,6 +192,7 @@ def patch(self, request, pk, **kwargs):
status=status.HTTP_400_BAD_REQUEST,
)
check_related_fields_update(request.data, pk)
cache.delete("project_list_view")
return super(ProjectDetail, self).put(request, pk)


Expand Down