11import hashlib
22from datetime import timedelta
33from types import SimpleNamespace
4- from typing import Any
4+ from typing import Any , cast
55
66from django import template
77from django .conf import settings
88from django .http import HttpRequest
99from django .template .loader import render_to_string
1010from django .urls import reverse
11+ from django .utils .functional import SimpleLazyObject , empty
1112from django .utils .html import escape , format_html , format_html_join
1213from django .utils .safestring import SafeString , mark_safe
1314from markdownify .templatetags .markdownify import markdownify as render_markdown
1617from core .freeipa .user import FreeIPAUser
1718from core .membership_notes import CUSTOS , last_votes , note_action_icon , note_action_label
1819from core .models import MembershipRequest , Note
19- from core .views_utils import get_username
20+ from core .tokens import make_membership_notes_aggregate_target_token
21+ from core .views_utils import get_username , try_get_username_from_user
2022
2123register = template .Library ()
2224
@@ -219,6 +221,110 @@ def _current_username_from_request(http_request: HttpRequest | None) -> str:
219221 return get_username (http_request , allow_user_fallback = False )
220222
221223
224+ def _normalize_username (value : object ) -> str :
225+ return str (value or "" ).strip ().lower ()
226+
227+
228+ def _aggregate_author_usernames (notes : list [Note ]) -> set [str ]:
229+ return {
230+ _normalize_username (note .username )
231+ for note in notes
232+ if note .username and note .username != CUSTOS and _normalize_username (note .username )
233+ }
234+
235+
236+ def _avatar_safe_aggregate_user (user_obj : object | None ) -> object | None :
237+ if user_obj is None :
238+ return None
239+
240+ if isinstance (user_obj , SimpleLazyObject ):
241+ # Only reuse an already-evaluated lazy user so this optimization never forces a new fetch.
242+ wrapped_user = user_obj ._wrapped
243+ if wrapped_user is empty or wrapped_user is user_obj :
244+ return None
245+ if isinstance (wrapped_user , FreeIPAUser ):
246+ return wrapped_user
247+ return None
248+
249+ if isinstance (user_obj , FreeIPAUser ):
250+ return user_obj
251+
252+ candidate_user = cast (Any , user_obj )
253+ if not hasattr (candidate_user , "is_authenticated" ) or not bool (candidate_user .is_authenticated ):
254+ return None
255+ if not hasattr (candidate_user , "username" ) or not hasattr (candidate_user , "email" ):
256+ return None
257+ if not hasattr (candidate_user , "get_username" ) or not callable (candidate_user .get_username ):
258+ return None
259+
260+ username = _normalize_username (try_get_username_from_user (candidate_user ))
261+ email = str (candidate_user .email or "" ).strip ()
262+ request_get_username = _normalize_username (candidate_user .get_username ())
263+ if not username or not email or request_get_username != username :
264+ return None
265+
266+ return candidate_user
267+
268+
269+ def _avatar_safe_request_user_for_aggregate_notes (http_request : HttpRequest | None ) -> object | None :
270+ if http_request is None or not hasattr (http_request , "user" ):
271+ return None
272+
273+ return _avatar_safe_aggregate_user (http_request .user )
274+
275+
276+ def _aggregate_preloaded_target_user (
277+ * ,
278+ context : dict [str , Any ],
279+ aggregate_target_type : str ,
280+ aggregate_target : str ,
281+ ) -> object | None :
282+ if aggregate_target_type != "user" :
283+ return None
284+
285+ normalized_target = _normalize_username (aggregate_target )
286+ if not normalized_target :
287+ return None
288+
289+ for candidate in (context .get ("fu" ), context .get ("aggregate_preloaded_target_user" )):
290+ safe_candidate = _avatar_safe_aggregate_user (candidate )
291+ if safe_candidate is None :
292+ continue
293+ candidate_username = _normalize_username (try_get_username_from_user (safe_candidate ))
294+ if candidate_username == normalized_target :
295+ return safe_candidate
296+
297+ return None
298+
299+
300+ def _preloaded_aggregate_avatar_users_by_username (
301+ * ,
302+ context : dict [str , Any ],
303+ notes : list [Note ],
304+ http_request : HttpRequest | None ,
305+ aggregate_target_user : object | None ,
306+ ) -> dict [str , object ]:
307+ aggregate_author_usernames = _aggregate_author_usernames (notes )
308+ if not aggregate_author_usernames :
309+ return {}
310+
311+ preloaded_users_by_username : dict [str , object ] = {}
312+
313+ target_user = _avatar_safe_aggregate_user (aggregate_target_user )
314+ if target_user is not None :
315+ target_username = _normalize_username (try_get_username_from_user (target_user ))
316+ if target_username in aggregate_author_usernames :
317+ preloaded_users_by_username [target_username ] = target_user
318+
319+ request_user = _avatar_safe_request_user_for_aggregate_notes (http_request )
320+ if request_user is not None :
321+ request_username = _normalize_username (try_get_username_from_user (request_user ))
322+ if request_username in aggregate_author_usernames and request_username not in preloaded_users_by_username :
323+ preloaded_users_by_username [request_username ] = request_user
324+
325+ return preloaded_users_by_username
326+
327+
222328def _avatar_users_by_username (notes : list [Note ]) -> dict [str , object ]:
223329 avatar_users_by_username : dict [str , object ] = {}
224330 for username in {str (n .username or "" ).strip () for n in notes if n .username and n .username != CUSTOS }:
@@ -228,15 +334,20 @@ def _avatar_users_by_username(notes: list[Note]) -> dict[str, object]:
228334 return avatar_users_by_username
229335
230336
231- def _aggregate_avatar_users_by_username (notes : list [ Note ]) -> dict [ str , object ]:
232- lookup_usernames = {
233- str ( note . username or "" ). strip (). lower ()
234- for note in notes
235- if note . username and note . username != CUSTOS and str ( note . username or "" ). strip ()
236- }
337+ def _aggregate_avatar_users_by_username (
338+ notes : list [ Note ],
339+ * ,
340+ preloaded_users_by_username : dict [ str , object ] | None = None ,
341+ ) -> dict [ str , object ]:
342+ lookup_usernames = _aggregate_author_usernames ( notes )
237343 if not lookup_usernames :
238344 return {}
239- return FreeIPAUser .find_lightweight_by_usernames (lookup_usernames )
345+
346+ resolved_users_by_username = dict (preloaded_users_by_username or {})
347+ unresolved_usernames = lookup_usernames - set (resolved_users_by_username )
348+ if unresolved_usernames :
349+ resolved_users_by_username .update (FreeIPAUser .find_lightweight_by_usernames (unresolved_usernames ))
350+ return resolved_users_by_username
240351
241352
242353def _note_display_username (note : Note ) -> str :
@@ -577,6 +688,29 @@ def _render_membership_notes_aggregate(
577688 notes ,
578689 current_responses_by_request_id = current_responses_by_request_id ,
579690 )
691+ aggregate_target_user = _aggregate_preloaded_target_user (
692+ context = context ,
693+ aggregate_target_type = aggregate_target_type ,
694+ aggregate_target = aggregate_target ,
695+ )
696+ preloaded_users_by_username = _preloaded_aggregate_avatar_users_by_username (
697+ context = context ,
698+ notes = notes ,
699+ http_request = http_request ,
700+ aggregate_target_user = aggregate_target_user ,
701+ )
702+ aggregate_preloaded_target_token = ""
703+ if aggregate_target_user is not None :
704+ safe_target_user = cast (Any , aggregate_target_user )
705+ aggregate_preloaded_target_username = str (try_get_username_from_user (aggregate_target_user ) or "" ).strip ()
706+ if aggregate_preloaded_target_username :
707+ aggregate_preloaded_target_token = make_membership_notes_aggregate_target_token (
708+ {
709+ "target_type" : aggregate_target_type ,
710+ "target" : aggregate_preloaded_target_username ,
711+ "email" : str (safe_target_user .email or "" ).strip (),
712+ }
713+ )
580714
581715 html = render_to_string (
582716 "core/_membership_notes.html" ,
@@ -589,7 +723,10 @@ def _render_membership_notes_aggregate(
589723 current_username = _current_username_from_request (http_request ),
590724 email_modal_ids = email_modal_ids ,
591725 request_resubmitted_new_snapshots_by_note_id = request_resubmitted_new_snapshots_by_note_id ,
592- avatar_users_by_username = _aggregate_avatar_users_by_username (notes ),
726+ avatar_users_by_username = _aggregate_avatar_users_by_username (
727+ notes ,
728+ preloaded_users_by_username = preloaded_users_by_username ,
729+ ),
593730 )
594731 ),
595732 "note_count" : len (notes ),
@@ -601,6 +738,7 @@ def _render_membership_notes_aggregate(
601738 "post_url" : post_url ,
602739 "aggregate_target_type" : aggregate_target_type ,
603740 "aggregate_target" : aggregate_target ,
741+ "aggregate_preloaded_target_token" : aggregate_preloaded_target_token ,
604742 "next_url" : resolved_next_url ,
605743 "email_modals" : email_modals ,
606744 },
0 commit comments