Skip to content

Commit f8b4fe2

Browse files
committed
Optimize loading of membership committee notes
1 parent 6dd8e3e commit f8b4fe2

7 files changed

Lines changed: 1055 additions & 10 deletions

File tree

astra_app/core/membership_notes_render.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,11 @@ def render_membership_notes_aggregate_widget(
2929
target: str,
3030
compact: bool,
3131
next_url: str,
32+
aggregate_preloaded_target_user: object | None = None,
3233
) -> str:
3334
context = {"request": request, **review_permissions}
35+
if aggregate_preloaded_target_user is not None:
36+
context["aggregate_preloaded_target_user"] = aggregate_preloaded_target_user
3437
if target_type == "user":
3538
html = core_membership_notes.membership_notes_aggregate_for_user(
3639
context,

astra_app/core/templates/core/_membership_notes.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,9 @@
195195
{% if aggregate_target_type and aggregate_target %}
196196
<input type="hidden" name="aggregate_target_type" value="{{ aggregate_target_type }}">
197197
<input type="hidden" name="aggregate_target" value="{{ aggregate_target }}">
198+
{% if aggregate_preloaded_target_token %}
199+
<input type="hidden" name="aggregate_preloaded_target_token" value="{{ aggregate_preloaded_target_token }}">
200+
{% endif %}
198201
{% endif %}
199202
<input type="hidden" name="note_action" value="message">
200203
<div class="input-group">

astra_app/core/templatetags/core_membership_notes.py

Lines changed: 148 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import hashlib
22
from datetime import timedelta
33
from types import SimpleNamespace
4-
from typing import Any
4+
from typing import Any, cast
55

66
from django import template
77
from django.conf import settings
88
from django.http import HttpRequest
99
from django.template.loader import render_to_string
1010
from django.urls import reverse
11+
from django.utils.functional import SimpleLazyObject, empty
1112
from django.utils.html import escape, format_html, format_html_join
1213
from django.utils.safestring import SafeString, mark_safe
1314
from markdownify.templatetags.markdownify import markdownify as render_markdown
@@ -16,7 +17,8 @@
1617
from core.freeipa.user import FreeIPAUser
1718
from core.membership_notes import CUSTOS, last_votes, note_action_icon, note_action_label
1819
from 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

2123
register = 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+
222328
def _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

242353
def _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

Comments
 (0)