From 820c9337cd9c85909ba9f85c6a5d4619d8e9c15c Mon Sep 17 00:00:00 2001 From: hlbmtc Date: Mon, 29 Dec 2025 18:39:13 +0100 Subject: [PATCH 1/3] Include coherence links in the feed payload --- coherence/serializers.py | 53 ++++++++++++++++++++++++++++++--- coherence/services.py | 1 + posts/serializers.py | 21 +++++++++++++ questions/serializers/common.py | 9 ++++++ 4 files changed, 80 insertions(+), 4 deletions(-) diff --git a/coherence/serializers.py b/coherence/serializers.py index 6bb6c25b13..8a7b79b9e1 100644 --- a/coherence/serializers.py +++ b/coherence/serializers.py @@ -1,7 +1,8 @@ -from collections import Counter +from collections import Counter, defaultdict from typing import Iterable import numpy as np +from django.db.models import Q from multidict import MultiDict from rest_framework import serializers @@ -72,8 +73,8 @@ def serialize_coherence_link_many( links: Iterable[CoherenceLink], serialize_questions: bool = True ): ids = [link.pk for link in links] - qs = CoherenceLink.objects.filter(pk__in=[c.pk for c in links]).select_related( - "question1", "question2" + qs = CoherenceLink.objects.filter(pk__in=[c.pk for c in links]).prefetch_related( + "question1__related_posts", "question2__related_posts" ) objects = list(qs.all()) @@ -136,7 +137,7 @@ def serialize_aggregate_coherence_link_many( ids = [link.pk for link in links] qs = AggregateCoherenceLink.objects.filter( pk__in=[c.pk for c in links] - ).select_related("question1", "question2") + ).prefetch_related("question1__related_posts", "question2__related_posts") if current_user: qs = qs.annotate_user_vote(current_user) @@ -172,6 +173,50 @@ def serialize_aggregate_coherence_link_many( ] +def serialize_aggregate_coherence_links_questions_map( + questions: Iterable[Question], current_user: User = None +) -> dict[int, list[dict]]: + qs = AggregateCoherenceLink.objects.filter( + Q(question1__in=questions) | Q(question2__in=questions) + ) + questions_map = {q.id: q for q in questions} + + serialized_data = serialize_aggregate_coherence_link_many( + qs, current_user=current_user + ) + links_map = defaultdict(list) + + for link in serialized_data: + for alias in ("question1_id", "question2_id"): + question = questions_map.get(link[alias]) + + if question: + links_map[question].append(link) + + return links_map + + +def serialize_coherence_links_questions_map( + questions: Iterable[Question], current_user: User +) -> dict[int, list[dict]]: + qs = CoherenceLink.objects.filter( + Q(question1__in=questions) | Q(question2__in=questions), user=current_user + ) + questions_map = {q.id: q for q in questions} + + serialized_data = serialize_coherence_link_many(qs) + links_map = defaultdict(list) + + for link in serialized_data: + for alias in ("question1_id", "question2_id"): + question = questions_map.get(link[alias]) + + if question: + links_map[question].append(link) + + return links_map + + def serialize_aggregate_coherence_link_vote( vote_scores: list[AggregateCoherenceLinkVote], user_vote: int = None, diff --git a/coherence/services.py b/coherence/services.py index 76b56052a6..c2783f9c2f 100644 --- a/coherence/services.py +++ b/coherence/services.py @@ -12,6 +12,7 @@ LinkType, AggregateCoherenceLinkVote, ) +from posts.models import Post from questions.models import Question from questions.services.forecasts import get_user_last_forecasts_map from users.models import User diff --git a/posts/serializers.py b/posts/serializers.py index a09865e019..553b4c42ea 100644 --- a/posts/serializers.py +++ b/posts/serializers.py @@ -8,6 +8,10 @@ from rest_framework import serializers from rest_framework.exceptions import ValidationError +from coherence.serializers import ( + serialize_coherence_links_questions_map, + serialize_aggregate_coherence_links_questions_map, +) from comments.models import KeyFactor from comments.serializers.key_factors import serialize_key_factors_many from misc.models import ITNArticle @@ -341,6 +345,8 @@ def serialize_post( include_descriptions: bool = False, question_movements: dict[Question, QuestionMovement | None] = None, question_average_coverages: dict[Question, float] = None, + coherence_links: dict[Question, list[dict]] = None, + coherence_link_aggregations: dict[Question, list[dict]] = None, ) -> dict: current_user = ( current_user if current_user and not current_user.is_anonymous else None @@ -367,6 +373,8 @@ def serialize_post( include_descriptions=include_descriptions, question_movement=question_movements.get(post.question), question_average_coverage=question_average_coverages.get(post.question), + coherence_links=coherence_links.get(post.question), + coherence_link_aggregations=coherence_link_aggregations.get(post.question), ) if post.conditional: @@ -516,6 +524,8 @@ def serialize_post_many( ) comment_key_factors_map = {} + coherence_links_map = {} + coherence_link_aggs_map = {} if with_key_factors: comment_key_factors_map = generate_map_from_list( @@ -528,6 +538,15 @@ def serialize_post_many( key=lambda x: x["post"]["id"], ) + if current_user: + coherence_links_map = serialize_coherence_links_questions_map( + questions, current_user + ) + + coherence_link_aggs_map = serialize_aggregate_coherence_links_questions_map( + questions + ) + question_movements = {} if include_movements: question_movements = calculate_movement_for_questions(questions) @@ -559,6 +578,8 @@ def serialize_post_many( include_descriptions=include_descriptions, question_movements=question_movements, question_average_coverages=question_average_coverages, + coherence_links=coherence_links_map, + coherence_link_aggregations=coherence_link_aggs_map, ) for post in posts ] diff --git a/questions/serializers/common.py b/questions/serializers/common.py index d514b59516..91bfe4cfe6 100644 --- a/questions/serializers/common.py +++ b/questions/serializers/common.py @@ -585,6 +585,8 @@ def serialize_question( include_descriptions: bool = False, question_movement: QuestionMovement | None = None, question_average_coverage: float = None, + coherence_links: list[dict] = None, + coherence_link_aggregations: list[dict] = None, ): """ Serializes question object @@ -592,6 +594,13 @@ def serialize_question( serialized_data = QuestionSerializer(question).data + serialized_data.update( + { + "coherence_links": coherence_links, + "coherence_link_aggregations": coherence_link_aggregations, + } + ) + if include_descriptions: serialized_data.update( { From e3298c9850e91ca74f682c9a3df73fb959534d40 Mon Sep 17 00:00:00 2001 From: hlbmtc Date: Mon, 29 Dec 2025 18:43:29 +0100 Subject: [PATCH 2/3] Small fix --- coherence/services.py | 1 - posts/serializers.py | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/coherence/services.py b/coherence/services.py index c2783f9c2f..76b56052a6 100644 --- a/coherence/services.py +++ b/coherence/services.py @@ -12,7 +12,6 @@ LinkType, AggregateCoherenceLinkVote, ) -from posts.models import Post from questions.models import Question from questions.services.forecasts import get_user_last_forecasts_map from users.models import User diff --git a/posts/serializers.py b/posts/serializers.py index 553b4c42ea..ccccef2af1 100644 --- a/posts/serializers.py +++ b/posts/serializers.py @@ -354,6 +354,8 @@ def serialize_post( serialized_data = PostReadSerializer(post).data question_movements = question_movements or {} question_average_coverages = question_average_coverages or {} + coherence_links = coherence_links or {} + coherence_link_aggregations = coherence_link_aggregations or {} # Appending projects projects = projects or [] From a4b65f143c82ac4b9ef18efb6ff0ef72864de035 Mon Sep 17 00:00:00 2001 From: hlbmtc Date: Tue, 30 Dec 2025 19:06:42 +0100 Subject: [PATCH 3/3] Fixed N+1 calls --- .../components/coherence_links_provider.tsx | 34 +++++++++++++------ front_end/src/types/question.ts | 6 ++++ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/front_end/src/app/(main)/components/coherence_links_provider.tsx b/front_end/src/app/(main)/components/coherence_links_provider.tsx index 6f960ca494..92155b9662 100644 --- a/front_end/src/app/(main)/components/coherence_links_provider.tsx +++ b/front_end/src/app/(main)/components/coherence_links_provider.tsx @@ -50,13 +50,34 @@ export const CoherenceLinksProvider: FC< const { user } = useAuth(); const isLoggedIn = !isNil(user); - const fetchLinks = async () => { - if (!isLoggedIn || !post.question) { + useEffect(() => { + if ( + !isLoggedIn || + !post.question || + !post.question.coherence_links?.length || + !post.question.coherence_link_aggregations?.length + ) { setCoherenceLinks({ data: [] }); setAggregateCoherenceLinks({ data: [] }); return; } + setCoherenceLinks({ data: post.question.coherence_links }); + setAggregateCoherenceLinks({ + data: post.question.coherence_link_aggregations, + }); + }, [ + isLoggedIn, + post.question, + post.question?.coherence_links, + post.question?.coherence_link_aggregations, + ]); + + const updateCoherenceLinks = async () => { + if (!isLoggedIn || !post.question) { + return; + } + try { const [links, aggregate] = await Promise.all([ ClientCoherenceLinksApi.getCoherenceLinksForPost(post.question), @@ -72,15 +93,6 @@ export const CoherenceLinksProvider: FC< } }; - useEffect(() => { - void fetchLinks(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isLoggedIn, post.question?.id]); - - const updateCoherenceLinks = async () => { - await fetchLinks(); - }; - const getOtherQuestions = () => { const questionData = new Map(); const questionID = post.question?.id; diff --git a/front_end/src/types/question.ts b/front_end/src/types/question.ts index 7efc0b9807..0dc451d426 100644 --- a/front_end/src/types/question.ts +++ b/front_end/src/types/question.ts @@ -1,4 +1,8 @@ import { ContinuousQuestionTypes } from "@/constants/questions"; +import { + FetchedAggregateCoherenceLinks, + FetchedCoherenceLinks, +} from "@/types/coherence"; import { QuestionStatus, Resolution } from "@/types/post"; import { Category } from "@/types/projects"; @@ -268,6 +272,8 @@ export type Question = { movement: null | CPMovement; }; average_coverage?: number | null; + coherence_links?: FetchedCoherenceLinks["data"]; + coherence_link_aggregations?: FetchedAggregateCoherenceLinks["data"]; }; export enum MovementDirection {