Merged in feature/graphql-course-cache (pull request #381)

Feature/graphql course cache
This commit is contained in:
Daniel Egger 2024-08-28 20:31:35 +00:00
commit 02b08cf3a8
5 changed files with 248 additions and 1 deletions

View File

@ -39,7 +39,7 @@ export const useNotificationsStore = defineStore("notifications", () => {
updateUnreadCount();
timerHandle = setInterval(
async () => await updateUnreadCount(),
30000
150 * 1000
) as unknown as number;
} else if (!userStore.loggedIn) {
log.debug("Notification polling stopped");

View File

@ -201,6 +201,7 @@ MIDDLEWARE = [
"vbv_lernwelt.core.middleware.security.SecurityRequestResponseLoggingMiddleware",
"wagtail.contrib.redirects.middleware.RedirectMiddleware",
"vbv_lernwelt.core.middleware.auth.UserLoggedInCookieMiddleWare",
"vbv_lernwelt.course.middleware.GraphQLQueryFilterMiddleware",
# "vbv_lernwelt.debugtools.middleware.QueryCountDebugMiddleware",
]

View File

@ -65,6 +65,7 @@ class CourseSessionUserAdmin(admin.ModelAdmin):
"role",
"circles",
"optional_attendance",
"user_sso_id",
# "created_at",
# "updated_at",
]
@ -95,6 +96,12 @@ class CourseSessionUserAdmin(admin.ModelAdmin):
user_last_name.short_description = "Last Name"
user_last_name.admin_order_field = "user__last_name"
def user_sso_id(self, obj):
return obj.user.sso_id
user_sso_id.short_description = "SSO ID"
user_sso_id.admin_order_field = "user__sso_id"
def circles(self, obj):
return ", ".join([c.title for c in obj.expert.all()])

View File

@ -0,0 +1,205 @@
# myapp/middleware.py
import json
import re
import structlog
from django.core.cache import cache
logger = structlog.get_logger(__name__)
# TODO: refactor so that the query from the client is loaded
COURSE_QUERY = """
query courseQuery($slug: String!, $user: String) {
course(slug: $slug) {
id
title
slug
category_name
profiles
course_session_users(id: $user) {
id
__typename
chosen_profile
course_session {
id
__typename
}
}
configuration {
id
enable_circle_documents
enable_learning_mentor
enable_competence_certificates
is_uk
is_vv
__typename
}
action_competences {
competence_id
...CoursePageFields
performance_criteria {
competence_id
learning_unit {
id
slug
evaluate_url
__typename
}
...CoursePageFields
__typename
}
__typename
}
learning_path {
...CoursePageFields
topics {
is_visible
...CoursePageFields
circles {
description
goals
profiles
is_base_circle
...CoursePageFields
learning_sequences {
icon
...CoursePageFields
learning_units {
evaluate_url
...CoursePageFields
performance_criteria {
...CoursePageFields
__typename
}
learning_contents {
can_user_self_toggle_course_completion
content_url
minutes
description
...CoursePageFields
... on LearningContentAssignmentObjectType {
assignment_type
content_assignment {
id
assignment_type
__typename
}
competence_certificate {
...CoursePageFields
__typename
}
__typename
}
... on LearningContentEdoniqTestObjectType {
checkbox_text
has_extended_time_test
content_assignment {
id
assignment_type
__typename
}
competence_certificate {
...CoursePageFields
__typename
}
__typename
}
... on LearningContentRichTextObjectType {
text
__typename
}
__typename
}
__typename
}
__typename
}
__typename
}
__typename
}
__typename
}
__typename
}
}
fragment CoursePageFields on CoursePageInterface {
title
id
slug
content_type
frontend_url
__typename
}
"""
COURSE_SESSION_USER_QUERY = """
query courseQuery($slug: String!, $user: String) {
course(slug: $slug) {
course_session_users(id: $user) {
id
__typename
chosen_profile
course_session {
id
__typename
}
}
}
}
"""
class GraphQLQueryFilterMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Check if the request is a GraphQL request
if request.content_type == "application/json" and "/graphql" in request.path:
try:
# Parse the GraphQL query from the request body
body = json.loads(request.body)
query = body.get("query", "")
if self.normalize_string(query) == self.normalize_string(COURSE_QUERY):
logger.debug("CourseQuery detected")
slug = body.get("variables", {}).get("slug", "")
if slug:
key = f"course_query_{slug}"
cache_data = cache.get(key)
if cache_data:
# Cache hit: only make course session user query and add to cache data
logger.debug("cache hit", key=key)
body["query"] = COURSE_SESSION_USER_QUERY
request._body = json.dumps(body).encode("utf-8")
response = self.get_response(request)
content = json.loads(response.content)
cache_data["data"]["course"]["course_session_users"] = (
content["data"]["course"]["course_session_users"]
)
response.content = json.dumps(cache_data)
return response
else:
# Cache miss: make the original query and cache the result
logger.debug("cache miss", key=key)
response = self.get_response(request)
content = json.loads(response.content)
del content["data"]["course"]["course_session_users"]
cache.set(key, content, 60 * 10)
return response
except Exception as e:
# Handle any exceptions in parsing or filtering
logger.error("Error in GraphQLQueryFilterMiddleware", exc_info=e)
# Continue processing the request if not blocked
response = self.get_response(request)
return response
def normalize_string(self, s):
# Remove all whitespace characters (space, tabs, newlines)
return re.sub(r"\s+", "", s)

View File

@ -0,0 +1,34 @@
import functools
import time
from django.db import connection, reset_queries
def count_queries(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Reset the query log
reset_queries()
# Start the timer
start_time = time.time()
# Execute the function
result = func(*args, **kwargs)
# Stop the timer
end_time = time.time()
# Count the number of queries
query_count = len(connection.queries)
# Calculate the execution time
execution_time = end_time - start_time
print(
f"{func.__name__} executed {query_count} queries in {execution_time:.4f} seconds."
)
return result
return wrapper