diff --git a/server/config/settings/base.py b/server/config/settings/base.py index de7744aa..48c68bf1 100644 --- a/server/config/settings/base.py +++ b/server/config/settings/base.py @@ -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", ] diff --git a/server/vbv_lernwelt/course/middleware.py b/server/vbv_lernwelt/course/middleware.py new file mode 100644 index 00000000..d9814760 --- /dev/null +++ b/server/vbv_lernwelt/course/middleware.py @@ -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(f"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) diff --git a/server/vbv_lernwelt/debugtools/decorators.py b/server/vbv_lernwelt/debugtools/decorators.py new file mode 100644 index 00000000..102707e5 --- /dev/null +++ b/server/vbv_lernwelt/debugtools/decorators.py @@ -0,0 +1,32 @@ +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