Skip to content

Commit

Permalink
perf: optimize django admin pages (#4543)
Browse files Browse the repository at this point in the history
  • Loading branch information
zawan-ila authored Jan 14, 2025
1 parent 9c571cb commit 727e828
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 10 deletions.
21 changes: 17 additions & 4 deletions course_discovery/apps/course_metadata/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ class AdditionalMetadataInline(admin.TabularInline):
@admin.register(GeoLocation)
class GeoLocationAdmin(admin.ModelAdmin):
"""Admin for GeoLocation model."""
search_fields = ('location_name', )


@admin.register(ProductValue)
Expand All @@ -145,7 +146,10 @@ class CourseAdmin(DjangoObjectActions, SimpleHistoryAdmin):
readonly_fields = ['enrollment_count', 'recent_enrollment_count', 'active_url_slug', 'key', 'number']
search_fields = ('uuid', 'key', 'key_for_reruns', 'title',)
raw_id_fields = ('canonical_course_run', 'draft_version', 'location_restriction')
autocomplete_fields = ['canonical_course_run']
autocomplete_fields = [
'canonical_course_run', 'geolocation', 'in_year_value', 'video', 'extra_description',
'additional_metadata'
]
change_actions = ('course_skills', 'refresh_course_skills')

def get_queryset(self, request):
Expand Down Expand Up @@ -229,6 +233,13 @@ def get_urls(self):

course_skills.label = "view course skills"

class Media:
js = (
'bower_components/jquery-ui/ui/minified/jquery-ui.min.js',
'bower_components/jquery/dist/jquery.min.js',
SortableSelectJSPath()
)


@admin.register(CourseEditor)
class CourseEditorAdmin(admin.ModelAdmin):
Expand Down Expand Up @@ -307,6 +318,9 @@ class CourseRunAdmin(SimpleHistoryAdmin):
search_fields = ('uuid', 'key', 'title_override', 'course__title', 'slug', 'external_key', 'variant_id')
save_error = False
form = CourseRunAdminForm
autocomplete_fields = (
'video',
)

def get_queryset(self, request):
qs = super().get_queryset(request)
Expand Down Expand Up @@ -398,8 +412,7 @@ class ProgramAdmin(DjangoObjectActions, SimpleHistoryAdmin):
)
raw_id_fields = ('video',)
autocomplete_fields = (
'corporate_endorsements', 'faq', 'individual_endorsements', 'job_outlook_items',
'expected_learning_items', 'in_year_value'
'in_year_value',
)
search_fields = ('uuid', 'title', 'marketing_slug')
exclude = ('card_image_url',)
Expand Down Expand Up @@ -603,7 +616,7 @@ class RankingAdmin(admin.ModelAdmin):
@admin.register(AdditionalPromoArea)
class AdditionalPromoAreaAdmin(admin.ModelAdmin):
list_display = ('title', 'description', 'courses')
search_fields = ('description',)
search_fields = ('description', 'title')

def get_queryset(self, request):
queryset = super().get_queryset(request)
Expand Down
65 changes: 65 additions & 0 deletions course_discovery/apps/course_metadata/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ class Meta:
},
forward=['product_source'],
),
'corporate_endorsements': SortedModelSelect2Multiple(
url='admin_metadata:corporate-endorsement-autocomplete',
attrs={
'data-minimum-input-length': 2,
'class': 'sortable-select',
}
),
'credit_backing_organizations': SortedModelSelect2Multiple(
url='admin_metadata:organisation-autocomplete',
attrs={
Expand All @@ -38,13 +45,41 @@ class Meta:
},
forward=['product_source'],
),
'expected_learning_items': SortedModelSelect2Multiple(
url='admin_metadata:expected-learning-item-autocomplete',
attrs={
'data-minimum-input-length': 2,
'class': 'sortable-select',
}
),
'faq': SortedModelSelect2Multiple(
url='admin_metadata:faq-autocomplete',
attrs={
'data-minimum-input-length': 2,
'class': 'sortable-select',
}
),
'individual_endorsements': SortedModelSelect2Multiple(
url='admin_metadata:endorsement-autocomplete',
attrs={
'data-minimum-input-length': 2,
'class': 'sortable-select',
}
),
'instructor_ordering': SortedModelSelect2Multiple(
url='admin_metadata:person-autocomplete',
attrs={
'data-minimum-input-length': 3,
'class': 'sortable-select',
}
),
'job_outlook_items': SortedModelSelect2Multiple(
url='admin_metadata:job-outlook-item-autocomplete',
attrs={
'data-minimum-input-length': 2,
'class': 'sortable-select',
}
),
}

def __init__(self, *args, **kwargs):
Expand Down Expand Up @@ -117,6 +152,36 @@ class Meta:
model = Course
fields = '__all__'
exclude = ('slug', 'url_slug', )
widgets = {
'authoring_organizations': SortedModelSelect2Multiple(
url='admin_metadata:organisation-autocomplete',
attrs={
'data-minimum-input-length': 2,
'class': 'sortable-select',
},
),
'collaborators': SortedModelSelect2Multiple(
url='admin_metadata:collaborator-autocomplete',
attrs={
'data-minimum-input-length': 2,
'class': 'sortable-select',
},
),
'expected_learning_items': SortedModelSelect2Multiple(
url='admin_metadata:expected-learning-item-autocomplete',
attrs={
'data-minimum-input-length': 2,
'class': 'sortable-select',
},
),
'sponsoring_organizations': SortedModelSelect2Multiple(
url='admin_metadata:organisation-autocomplete',
attrs={
'data-minimum-input-length': 2,
'class': 'sortable-select',
},
),
}

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down
77 changes: 76 additions & 1 deletion course_discovery/apps/course_metadata/lookups.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q

from .models import Course, CourseRun, Organization, Person, Program
from course_discovery.apps.course_metadata.models import (
FAQ, Collaborator, CorporateEndorsement, Course, CourseRun, Endorsement, ExpectedLearningItem, JobOutlookItem,
Organization, Person, Program
)


class CourseAutocomplete(autocomplete.Select2QuerySetView):
Expand All @@ -19,6 +22,30 @@ def get_queryset(self):
return []


class CollaboratorAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
if self.request.user.is_authenticated and self.request.user.is_staff:
qs = Collaborator.objects.all()
if self.q:
qs = qs.filter(Q(name__icontains=self.q) | Q(uuid__icontains=self.q.strip()))

return qs

return []


class CorporateEndorsementAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
if self.request.user.is_authenticated and self.request.user.is_staff:
qs = CorporateEndorsement.objects.all()
if self.q:
qs = qs.filter(Q(corporation_name__icontains=self.q) | Q(statement__icontains=self.q))

return qs

return []


class CourseRunAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
if self.request.user.is_authenticated and self.request.user.is_staff:
Expand All @@ -36,6 +63,54 @@ def get_queryset(self):
return []


class EndorsementAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
if self.request.user.is_authenticated and self.request.user.is_staff:
qs = Endorsement.objects.all()
if self.q:
qs = qs.filter(quote__icontains=self.q)

return qs

return []


class ExpectedLearningItemAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
if self.request.user.is_authenticated and self.request.user.is_staff:
qs = ExpectedLearningItem.objects.all()
if self.q:
qs = qs.filter(value__icontains=self.q)

return qs

return []


class FAQAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
if self.request.user.is_authenticated and self.request.user.is_staff:
qs = FAQ.objects.all()
if self.q:
qs = qs.filter(Q(question__icontains=self.q) | Q(answer__icontains=self.q))

return qs

return []


class JobOutlookItemAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
if self.request.user.is_authenticated and self.request.user.is_staff:
qs = JobOutlookItem.objects.all()
if self.q:
qs = qs.filter(value__icontains=self.q)

return qs

return []


class OrganizationAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
if self.request.user.is_authenticated and self.request.user.is_staff:
Expand Down
3 changes: 1 addition & 2 deletions course_discovery/apps/course_metadata/tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,7 @@ def test_updating_order_of_authoring_orgs(self):

course = factories.CourseFactory(authoring_organizations=[org1, org2, org3])

new_ordering = (',').join(map(lambda org: str(org.id), [org2, org3, org1]))
params = {'authoring_organizations': new_ordering}
params = {'authoring_organizations': [org2.id, org3.id, org1.id]}

post_url = reverse('admin:course_metadata_course_change', args=(course.id,))
response = self.client.post(post_url, params)
Expand Down
38 changes: 36 additions & 2 deletions course_discovery/apps/course_metadata/tests/test_lookups.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
from itertools import cycle
from urllib.parse import quote, urlencode

import pytest
Expand All @@ -8,7 +9,9 @@
from course_discovery.apps.api.tests.mixins import SiteMixin
from course_discovery.apps.core.tests.factories import USER_PASSWORD, UserFactory
from course_discovery.apps.course_metadata.tests.factories import (
CourseFactory, CourseRunFactory, OrganizationFactory, PersonFactory, PositionFactory, ProgramFactory
CollaboratorFactory, CorporateEndorsementFactory, CourseFactory, CourseRunFactory, EndorsementFactory,
ExpectedLearningItemFactory, FAQFactory, JobOutlookItemFactory, OrganizationFactory, PersonFactory, PositionFactory,
ProgramFactory
)
from course_discovery.apps.publisher.tests.factories import OrganizationExtensionFactory

Expand Down Expand Up @@ -91,7 +94,13 @@ def test_organization_autocomplete(self, admin_client):
self.assert_valid_query_result(admin_client, path, organization.key[:3], organization)
self.assert_valid_query_result(admin_client, path, organization.name[:5], organization)

@pytest.mark.parametrize('view_prefix', ['organisation', 'course', 'course-run'])
@pytest.mark.parametrize(
'view_prefix',
[
'collaborator', 'corporate-endorsement', 'course', 'course-run', 'endorsement',
'expected-learning-item', 'faq', 'job-outlook-item', 'organisation'
]
)
def test_autocomplete_requires_staff_permission(self, view_prefix, client):
""" Verify autocomplete returns empty list for non-staff users. """

Expand All @@ -102,6 +111,31 @@ def test_autocomplete_requires_staff_permission(self, view_prefix, client):
assert response.status_code == 200
assert data['results'] == []

@pytest.mark.parametrize(
'model_factory, autocomplete_path, lookup_attrs',
[
(CollaboratorFactory, 'collaborator-autocomplete', ['name', 'uuid']),
(CorporateEndorsementFactory, 'corporate-endorsement-autocomplete', ['corporation_name', 'statement']),
(EndorsementFactory, 'endorsement-autocomplete', ['quote']),
(ExpectedLearningItemFactory, 'expected-learning-item-autocomplete', ['value']),
(FAQFactory, 'faq-autocomplete', ['question', 'answer']),
(JobOutlookItemFactory, 'job-outlook-item-autocomplete', ['value']),
]
)
def test_models_autocomplete(self, admin_client, model_factory, autocomplete_path, lookup_attrs):
objects = model_factory.create_batch(3)
path = reverse(f'admin_metadata:{autocomplete_path}')
response = admin_client.get(path)
data = json.loads(response.content.decode('utf-8'))
assert response.status_code == 200
assert len(data['results']) == 3

# Search based on attributes
cycle_objects = cycle(objects)
for attr in lookup_attrs:
obj = next(cycle_objects)
self.assert_valid_query_result(admin_client, path, str(getattr(obj, attr))[:4], obj)


class AutoCompletePersonTests(SiteMixin, TestCase):
"""
Expand Down
18 changes: 17 additions & 1 deletion course_discovery/apps/course_metadata/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,32 @@
from django.urls import path

from course_discovery.apps.course_metadata.lookups import (
CourseAutocomplete, CourseRunAutocomplete, OrganizationAutocomplete, PersonAutocomplete, ProgramAutocomplete
CollaboratorAutocomplete, CorporateEndorsementAutocomplete, CourseAutocomplete, CourseRunAutocomplete,
EndorsementAutocomplete, ExpectedLearningItemAutocomplete, FAQAutocomplete, JobOutlookItemAutocomplete,
OrganizationAutocomplete, PersonAutocomplete, ProgramAutocomplete
)
from course_discovery.apps.course_metadata.views import CourseRunSelectionAdmin

app_name = 'course_metadata'

urlpatterns = [
path('update_course_runs/<int:pk>/', CourseRunSelectionAdmin.as_view(), name='update_course_runs',),
path('collaborator-autocomplete/', CollaboratorAutocomplete.as_view(), name='collaborator-autocomplete',),
path(
'corporate-endorsement-autocomplete/',
CorporateEndorsementAutocomplete.as_view(),
name='corporate-endorsement-autocomplete',
),
path('course-autocomplete/', CourseAutocomplete.as_view(), name='course-autocomplete',),
path('course-run-autocomplete/', CourseRunAutocomplete.as_view(), name='course-run-autocomplete',),
path('endorsement-autocomplete/', EndorsementAutocomplete.as_view(), name='endorsement-autocomplete',),
path(
'expected-learning-item-autocomplete/',
ExpectedLearningItemAutocomplete.as_view(),
name='expected-learning-item-autocomplete',
),
path('faq-autocomplete/', FAQAutocomplete.as_view(), name='faq-autocomplete',),
path('job-outlook-item-autocomplete/', JobOutlookItemAutocomplete.as_view(), name='job-outlook-item-autocomplete',),
path('organisation-autocomplete/', OrganizationAutocomplete.as_view(), name='organisation-autocomplete',),
path('person-autocomplete/', PersonAutocomplete.as_view(), name='person-autocomplete',),
path('program-autocomplete/', ProgramAutocomplete.as_view(), name='program-autocomplete',),
Expand Down

0 comments on commit 727e828

Please sign in to comment.