From b91f995366e22c544ee24c56f14175f7c547c5b7 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar <theskumar@users.noreply.github.com> Date: Tue, 11 Apr 2023 15:11:27 +0530 Subject: [PATCH] Load the activities tab of submission and project on demand (#3330) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- hypha/apply/activity/services.py | 47 +++++++++++++++++++ hypha/apply/activity/views.py | 16 ++----- .../funds/applicationsubmission_detail.html | 16 +++++-- hypha/apply/funds/urls.py | 2 + hypha/apply/funds/views_partials.py | 21 +++++++++ .../application_projects/project_detail.html | 16 ++++++- hypha/apply/projects/urls.py | 2 + hypha/apply/projects/views/__init__.py | 2 + .../apply/projects/views/project_partials.py | 19 ++++++++ 9 files changed, 124 insertions(+), 17 deletions(-) create mode 100644 hypha/apply/activity/services.py create mode 100644 hypha/apply/funds/views_partials.py create mode 100644 hypha/apply/projects/views/project_partials.py diff --git a/hypha/apply/activity/services.py b/hypha/apply/activity/services.py new file mode 100644 index 000000000..e91c51145 --- /dev/null +++ b/hypha/apply/activity/services.py @@ -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) + ) diff --git a/hypha/apply/activity/views.py b/hypha/apply/activity/views.py index 1a1be200f..caadfa494 100644 --- a/hypha/apply/activity/views.py +++ b/hypha/apply/activity/views.py @@ -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) diff --git a/hypha/apply/funds/templates/funds/applicationsubmission_detail.html b/hypha/apply/funds/templates/funds/applicationsubmission_detail.html index cba7ed595..08aba5edc 100644 --- a/hypha/apply/funds/templates/funds/applicationsubmission_detail.html +++ b/hypha/apply/funds/templates/funds/applicationsubmission_detail.html @@ -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> diff --git a/hypha/apply/funds/urls.py b/hypha/apply/funds/urls.py index 0f4e7a03e..02962ba71 100644 --- a/hypha/apply/funds/urls.py +++ b/hypha/apply/funds/urls.py @@ -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"), diff --git a/hypha/apply/funds/views_partials.py b/hypha/apply/funds/views_partials.py new file mode 100644 index 000000000..a7442eaeb --- /dev/null +++ b/hypha/apply/funds/views_partials.py @@ -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) diff --git a/hypha/apply/projects/templates/application_projects/project_detail.html b/hypha/apply/projects/templates/application_projects/project_detail.html index e41078735..15d81d43c 100644 --- a/hypha/apply/projects/templates/application_projects/project_detail.html +++ b/hypha/apply/projects/templates/application_projects/project_detail.html @@ -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> diff --git a/hypha/apply/projects/urls.py b/hypha/apply/projects/urls.py index 6c3b8f7a0..909926715 100644 --- a/hypha/apply/projects/urls.py +++ b/hypha/apply/projects/urls.py @@ -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"), diff --git a/hypha/apply/projects/views/__init__.py b/hypha/apply/projects/views/__init__.py index fb7ca2a0a..7520c2342 100644 --- a/hypha/apply/projects/views/__init__.py +++ b/hypha/apply/projects/views/__init__.py @@ -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', diff --git a/hypha/apply/projects/views/project_partials.py b/hypha/apply/projects/views/project_partials.py new file mode 100644 index 000000000..72e24dc8c --- /dev/null +++ b/hypha/apply/projects/views/project_partials.py @@ -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) -- GitLab