Skip to content
Snippets Groups Projects
Unverified Commit b91f9953 authored by Saurabh Kumar's avatar Saurabh Kumar Committed by GitHub
Browse files

Load the activities tab of submission and project on demand (#3330)

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
parent 1ecd4dfc
No related branches found
No related tags found
2 merge requests!18Fix application form submit button remaining disabled (#3483),!13Merge in v4.1.1
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)
)
......@@ -9,25 +9,17 @@ from .filters import NotificationFilter
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)
......
......@@ -44,7 +44,16 @@
{% 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 %}
......@@ -58,7 +67,7 @@
</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 %}
......@@ -169,7 +178,8 @@
{# 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>
......
......@@ -27,6 +27,7 @@ from .views import (
SubmissionStaffFlaggedView,
SubmissionUserFlaggedView,
)
from .views_partials import partial_submission_activities
revision_urls = ([
path('', RevisionListView.as_view(), name='list'),
......@@ -59,6 +60,7 @@ submission_urls = ([
])),
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"),
......
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)
......@@ -86,7 +86,18 @@
{% 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>
......@@ -204,7 +215,8 @@
{# 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>
......
......@@ -26,6 +26,7 @@ from .views import (
ReportUpdateView,
VendorDetailView,
VendorPrivateMediaView,
partial_project_activities,
)
app_name = 'projects'
......@@ -37,6 +38,7 @@ urlpatterns = [
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"),
......
......@@ -30,6 +30,7 @@ from .project import (
UploadContractView,
UploadDocumentView,
)
from .project_partials import partial_project_activities
from .report import (
ReportDetailView,
ReportFrequencyUpdate,
......@@ -41,6 +42,7 @@ from .report import (
from .vendor import CreateVendorView, VendorDetailView, VendorPrivateMediaView
__all__ = [
'partial_project_activities',
'ChangeInvoiceStatusView',
'SendForApprovalView',
'UploadDocumentView',
......
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% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment