Skip to content

Commit

Permalink
Load the activities tab of submission and project on demand (#3330)
Browse files Browse the repository at this point in the history
The activities tab can have a lot of activities as it’s unpaginated 
and also has an N+1 query to fetch the related objects.

This PR loads the data only after the tab is clicked.

I makes use of the htmx and alpine to observe the window-hash change. 

Notes:
- The communication tab with it's markdown editor, requires some custom
initialization logic, which breaks with the htmx loaded content. So it'
not included in this PR.
- On a submission with 5 activities the number of SQL queries reduced
from 168 to 148.

Related #3328
  • Loading branch information
theskumar authored Apr 11, 2023
1 parent 1ecd4df commit b91f995
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 17 deletions.
47 changes: 47 additions & 0 deletions hypha/apply/activity/services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from .models import Activity


def get_related_actions_for_user(obj, user):
"""Return Activity objects related to an object, esp. useful with
ApplicationSubmission and Project.
Args:
obj: instance of a model class
user: user who these actions are visible to.
Returns:
`Activity` queryset
"""
related_query = type(obj).activities.rel.related_query_name

return (
Activity.actions.filter(**{related_query: obj})
.select_related('user')
.prefetch_related(
'related_object',
)
.visible_to(user)
)


def get_related_comments_for_user(obj, user):
"""Return comments/communications related to an object, esp. useful with
ApplicationSubmission and Project.
Args:
obj: instance of a model class
user: user who these actions are visible to.
Returns:
`Activity` queryset
"""
related_query = type(obj).activities.rel.related_query_name

return (
Activity.comments.filter(**{related_query: obj})
.select_related('user')
.prefetch_related(
'related_object',
)
.visible_to(user)
)
16 changes: 4 additions & 12 deletions hypha/apply/activity/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,17 @@
from .forms import CommentForm
from .messaging import MESSAGES, messenger
from .models import COMMENT, Activity
from .services import get_related_comments_for_user


class ActivityContextMixin:
"""Mixin to add related 'comments' of the current view's 'self.object'
"""
def get_context_data(self, **kwargs):
related_query = self.model.activities.rel.related_query_name
query = {related_query: self.object}
extra = {
# Do not prefetch on the related_object__author as the models
# are not homogeneous and this will fail
'actions': Activity.actions.filter(**query).select_related(
'user',
).prefetch_related(
'related_object',
).visible_to(self.request.user),
'comments': Activity.comments.filter(**query).select_related(
'user',
).prefetch_related(
'related_object',
).visible_to(self.request.user),
'comments': get_related_comments_for_user(self.object, self.request.user)
}
return super().get_context_data(**extra, **kwargs)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,16 @@ <h5 class="heading heading--meta">
{% trans "Communications" %}
</a>

<a class="tab__item" href="#activity-feed" data-tab="tab-3">
<a class="tab__item"
href="#activity-feed"
hx-get="{% url 'funds:submissions:partial-activities' object.id %}"
hx-target="#tab-3 .feed"
hx-trigger="open-tab-3 once"
data-tab="tab-3"
x-data
@hashchange.window="location.hash === '#activity-feed' ? $dispatch('open-tab-3') : ''"
x-init="location.hash === '#activity-feed' ? $dispatch('open-tab-3') : ''"
>
{% trans "Activity feed" %}
</a>
{% if request.user.is_apply_staff_admin %}
Expand All @@ -58,7 +67,7 @@ <h5 class="heading heading--meta">
</div>

<div class="wrapper wrapper--large wrapper--tabs js-tabs-content">
{# Tab 1 #}
{# Tab 1 #}
<div class="tabs__content" id="tab-1">
{% block mobile_actions %}
{% endblock %}
Expand Down Expand Up @@ -169,7 +178,8 @@ <h6 class="heading heading--light-grey heading--uppercase">{% trans "Past Submis
{# Tab 3 #}
<div class="tabs__content" id="tab-3">
<div class="feed">
{% include "activity/include/action_list.html" %}
{% comment %} Loaded using the htmx via alpine's custom event "open-tab-3"{% endcomment %}
<p>Loading...</p>
</div>
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions hypha/apply/funds/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
SubmissionStaffFlaggedView,
SubmissionUserFlaggedView,
)
from .views_partials import partial_submission_activities

revision_urls = ([
path('', RevisionListView.as_view(), name='list'),
Expand Down Expand Up @@ -59,6 +60,7 @@
])),
path('<int:pk>/', include([
path('', SubmissionDetailView.as_view(), name="detail"),
path('partial/activities/', partial_submission_activities, name="partial-activities"),
path('edit/', SubmissionEditView.as_view(), name="edit"),
path('sealed/', SubmissionSealedView.as_view(), name="sealed"),
path('simplified/', SubmissionDetailSimplifiedView.as_view(), name="simplified"),
Expand Down
21 changes: 21 additions & 0 deletions hypha/apply/funds/views_partials.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, render
from django.views.decorators.http import require_GET

from hypha.apply.activity.services import (
get_related_actions_for_user,
)
from hypha.apply.funds.permissions import has_permission

from .models import ApplicationSubmission


@login_required
@require_GET
def partial_submission_activities(request, pk):
submission = get_object_or_404(ApplicationSubmission, pk=pk)
has_permission(
'submission_view', request.user, object=submission, raise_exception=True
)
ctx = {'actions': get_related_actions_for_user(submission, request.user)}
return render(request, 'activity/include/action_list.html', ctx)
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,18 @@ <h6 class="status-bar__subheading">
{% trans "Communications" %}
</a>

<a class="tab__item" href="#activity-feed" data-tab="tab-3">
<a
class="tab__item"
href="#activity-feed"
data-tab="tab-3"
hx-get="{% url 'apply:projects:partial-activities' object.id %}"
hx-target="#tab-3 .feed"
hx-trigger="open-tab-3 once"
data-tab="tab-3"
x-data
@hashchange.window="location.hash === '#activity-feed' ? $dispatch('open-tab-3') : ''"
x-init="location.hash === '#activity-feed' ? $dispatch('open-tab-3') : ''"
>
{% trans "Activity Feed" %}
</a>
</div>
Expand Down Expand Up @@ -204,7 +215,8 @@ <h5>{% trans "PAF Approvals" %}</h5>
{# Tab 3 #}
<div class="tabs__content" id="tab-3">
<div class="feed">
{% include "activity/include/action_list.html" with editable=False %}
{% comment %} Loaded using the htmx via alpine's custom event "open-tab-3"{% endcomment %}
<p>Loading...</p>
</div>
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions hypha/apply/projects/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
ReportUpdateView,
VendorDetailView,
VendorPrivateMediaView,
partial_project_activities,
)

app_name = 'projects'
Expand All @@ -37,6 +38,7 @@

path('<int:pk>/', include([
path('', ProjectDetailView.as_view(), name='detail'),
path('partial/activities/', partial_project_activities, name="partial-activities"),
path('edit/', ProjectApprovalFormEditView.as_view(), name="edit"),
path('documents/<int:file_pk>/', ProjectPrivateMediaView.as_view(), name="document"),
path('contract/<int:file_pk>/', ContractPrivateMediaView.as_view(), name="contract"),
Expand Down
2 changes: 2 additions & 0 deletions hypha/apply/projects/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
UploadContractView,
UploadDocumentView,
)
from .project_partials import partial_project_activities
from .report import (
ReportDetailView,
ReportFrequencyUpdate,
Expand All @@ -41,6 +42,7 @@
from .vendor import CreateVendorView, VendorDetailView, VendorPrivateMediaView

__all__ = [
'partial_project_activities',
'ChangeInvoiceStatusView',
'SendForApprovalView',
'UploadDocumentView',
Expand Down
19 changes: 19 additions & 0 deletions hypha/apply/projects/views/project_partials.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, render
from django.views.decorators.http import require_GET

from hypha.apply.activity.services import (
get_related_actions_for_user,
)

from ..models.project import Project


@login_required
@require_GET
def partial_project_activities(request, pk):
project = get_object_or_404(Project, pk=pk)
ctx = {
'actions': get_related_actions_for_user(project, request.user)
}
return render(request, 'activity/include/action_list.html', ctx)

0 comments on commit b91f995

Please sign in to comment.