Merged in feature/graphql-course-cache (pull request #381)
Feature/graphql course cache
This commit is contained in:
commit
02b08cf3a8
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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()])
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue