Skip to content

Commit 321ae47

Browse files
committed
fix: Multi Select course/course run issue
1 parent 127ea98 commit 321ae47

4 files changed

Lines changed: 449 additions & 5 deletions

File tree

course_discovery/apps/course_metadata/admin.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,26 @@ def __str__(self):
6161
return f'<script src="{abs_path}" defer></script>'
6262

6363

64+
class DALAdminMixin(admin.ModelAdmin):
65+
"""
66+
Mixin for Django admin classes using django-autocomplete-light.
67+
Ensures Select2 library is properly loaded before autocomplete.js.
68+
Required for Django 5.2 compatibility.
69+
"""
70+
class Media:
71+
css = {
72+
'all': (
73+
'admin/css/autocomplete.css',
74+
'select2/dist/css/select2.css',
75+
'dal_select2/dist/css/choices.css',
76+
)
77+
}
78+
js = (
79+
'select2/dist/js/select2.js',
80+
'admin/js/autocomplete.js',
81+
)
82+
83+
6484
class ProgramEligibilityFilter(admin.SimpleListFilter):
6585
title = _('eligible for one-click purchase')
6686
parameter_name = 'eligible_for_one_click_purchase'
@@ -138,7 +158,7 @@ class ProductValueAdmin(admin.ModelAdmin):
138158

139159

140160
@admin.register(Course)
141-
class CourseAdmin(DjangoObjectActions, SimpleHistoryAdmin):
161+
class CourseAdmin(DALAdminMixin, DjangoObjectActions, SimpleHistoryAdmin):
142162
form = CourseAdminForm
143163
list_display = ('uuid', 'key', 'key_for_reruns', 'title', 'draft',)
144164
list_filter = ('partner', 'product_source')
@@ -234,7 +254,16 @@ def get_urls(self):
234254
course_skills.label = "view course skills"
235255

236256
class Media:
257+
css = {
258+
'all': (
259+
'admin/css/autocomplete.css',
260+
'select2/dist/css/select2.css',
261+
'dal_select2/dist/css/choices.css',
262+
)
263+
}
237264
js = (
265+
'select2/dist/js/select2.js',
266+
'admin/js/autocomplete.js',
238267
'bower_components/jquery-ui/ui/minified/jquery-ui.min.js',
239268
'bower_components/jquery/dist/jquery.min.js',
240269
SortableSelectJSPath()
@@ -444,7 +473,7 @@ class ProgramLocationRestrictionAdmin(admin.ModelAdmin):
444473

445474

446475
@admin.register(Program)
447-
class ProgramAdmin(DjangoObjectActions, SimpleHistoryAdmin):
476+
class ProgramAdmin(DALAdminMixin, DjangoObjectActions, SimpleHistoryAdmin):
448477
form = ProgramAdminForm
449478
list_display = ('id', 'uuid', 'title', 'type', 'partner', 'status', 'hidden')
450479
list_filter = ('partner', 'type', 'product_source', 'status', ProgramEligibilityFilter, 'hidden')
@@ -585,7 +614,16 @@ def save_model(self, request, obj, form, change):
585614
messages.add_message(request, messages.ERROR, msg)
586615

587616
class Media:
617+
css = {
618+
'all': (
619+
'admin/css/autocomplete.css',
620+
'select2/dist/css/select2.css',
621+
'dal_select2/dist/css/choices.css',
622+
)
623+
}
588624
js = (
625+
'select2/dist/js/select2.js',
626+
'admin/js/autocomplete.js',
589627
'bower_components/jquery-ui/ui/minified/jquery-ui.min.js',
590628
'bower_components/jquery/dist/jquery.min.js',
591629
SortableSelectJSPath()

course_discovery/apps/course_metadata/tests/test_admin.py

Lines changed: 183 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,16 @@
1818
from selenium.webdriver.support.ui import Select
1919
from selenium.webdriver.support.wait import WebDriverWait
2020

21+
from django.contrib.admin import AdminSite, ModelAdmin
22+
2123
from course_discovery.apps.api.tests.mixins import SiteMixin
2224
from course_discovery.apps.api.v1.tests.test_views.mixins import FuzzyInt
2325
from course_discovery.apps.core.models import Partner
2426
from course_discovery.apps.core.tests.factories import USER_PASSWORD, PartnerFactory, UserFactory
2527
from course_discovery.apps.core.tests.helpers import make_image_file
26-
from course_discovery.apps.course_metadata.admin import DegreeAdmin, PositionAdmin, ProgramEligibilityFilter
28+
from course_discovery.apps.course_metadata.admin import (
29+
DALAdminMixin, DegreeAdmin, PositionAdmin, ProgramEligibilityFilter
30+
)
2731
from course_discovery.apps.course_metadata.choices import PathwayStatus, ProgramStatus
2832
from course_discovery.apps.course_metadata.constants import PathwayType
2933
from course_discovery.apps.course_metadata.forms import PathwayAdminForm, ProgramAdminForm
@@ -662,3 +666,181 @@ def test_program_with_different_partner(self):
662666
'__all__': ['These programs are for a different partner than the pathway itself: '
663667
'partner2 program - partner2-program']
664668
})
669+
670+
671+
class DALAdminMixinTests(TestCase):
672+
"""
673+
Test suite for DALAdminMixin class.
674+
Tests the proper configuration of django-autocomplete-light with Select2 for Django 5.2 compatibility.
675+
"""
676+
677+
def test_dal_admin_mixin_has_media_class(self):
678+
"""Test that DALAdminMixin has a Media class attribute."""
679+
self.assertTrue(hasattr(DALAdminMixin, 'Media'))
680+
self.assertIsNotNone(DALAdminMixin.Media)
681+
682+
def test_dal_admin_mixin_media_css(self):
683+
"""Test that DALAdminMixin includes required CSS files."""
684+
media = DALAdminMixin.Media
685+
self.assertTrue(hasattr(media, 'css'))
686+
self.assertIn('all', media.css)
687+
688+
css_files = media.css['all']
689+
expected_css = (
690+
'admin/css/autocomplete.css',
691+
'select2/dist/css/select2.css',
692+
'dal_select2/dist/css/choices.css',
693+
)
694+
695+
self.assertEqual(css_files, expected_css)
696+
self.assertEqual(len(css_files), 3)
697+
698+
def test_dal_admin_mixin_media_js(self):
699+
"""Test that DALAdminMixin includes required JavaScript files."""
700+
media = DALAdminMixin.Media
701+
self.assertTrue(hasattr(media, 'js'))
702+
703+
js_files = media.js
704+
expected_js = (
705+
'select2/dist/js/select2.js',
706+
'admin/js/autocomplete.js',
707+
)
708+
709+
self.assertEqual(js_files, expected_js)
710+
self.assertEqual(len(js_files), 2)
711+
712+
def test_dal_admin_mixin_css_files_have_correct_paths(self):
713+
"""Test specific CSS file paths are correct."""
714+
media = DALAdminMixin.Media
715+
css_files = media.css['all']
716+
717+
# Test each CSS file path
718+
self.assertEqual(css_files[0], 'admin/css/autocomplete.css')
719+
self.assertEqual(css_files[1], 'select2/dist/css/select2.css')
720+
self.assertEqual(css_files[2], 'dal_select2/dist/css/choices.css')
721+
722+
def test_dal_admin_mixin_js_files_have_correct_order(self):
723+
"""
724+
Test that JavaScript files are loaded in the correct order.
725+
Select2 MUST be loaded before autocomplete.js for proper functionality.
726+
"""
727+
media = DALAdminMixin.Media
728+
js_files = media.js
729+
730+
# Select2 should come before autocomplete.js
731+
select2_index = js_files.index('select2/dist/js/select2.js')
732+
autocomplete_index = js_files.index('admin/js/autocomplete.js')
733+
734+
self.assertLess(select2_index, autocomplete_index,
735+
msg="Select2 must be loaded before autocomplete.js")
736+
737+
def test_dal_admin_mixin_inheritable(self):
738+
"""
739+
Test that DALAdminMixin can be properly inherited by admin classes.
740+
"""
741+
# Create a test admin class that inherits from DALAdminMixin
742+
class TestModelAdmin(DALAdminMixin):
743+
list_display = ('id', 'name')
744+
745+
self.assertTrue(issubclass(TestModelAdmin, DALAdminMixin))
746+
self.assertTrue(hasattr(TestModelAdmin, 'Media'))
747+
748+
def test_dal_admin_mixin_media_inheritance(self):
749+
"""
750+
Test that Media attributes are properly inherited when using DALAdminMixin.
751+
"""
752+
class TestModelAdmin(DALAdminMixin):
753+
list_display = ('id', 'name')
754+
755+
# Check that inherited admin has Media attribute
756+
admin_instance = TestModelAdmin(Person, AdminSite())
757+
media = admin_instance.media
758+
759+
# Verify CSS files are present
760+
self.assertIn('select2/dist/css/select2.css', str(media))
761+
self.assertIn('admin/css/autocomplete.css', str(media))
762+
763+
# Verify JS files are present
764+
self.assertIn('select2/dist/js/select2.js', str(media))
765+
self.assertIn('admin/js/autocomplete.js', str(media))
766+
767+
def test_dal_admin_mixin_with_additional_media(self):
768+
"""
769+
Test that DALAdminMixin works with admin classes that define additional media.
770+
"""
771+
class TestModelAdminWithMedia(DALAdminMixin):
772+
list_display = ('id', 'name')
773+
774+
class Media:
775+
# This would extend the parent media or override it
776+
css = {
777+
'all': (
778+
'admin/css/autocomplete.css',
779+
'select2/dist/css/select2.css',
780+
'dal_select2/dist/css/choices.css',
781+
'custom/style.css', # Custom CSS in addition to DAL
782+
)
783+
}
784+
js = (
785+
'select2/dist/js/select2.js',
786+
'admin/js/autocomplete.js',
787+
'custom/script.js', # Custom JS
788+
)
789+
790+
admin = TestModelAdminWithMedia(Person, AdminSite())
791+
media_str = str(admin.media)
792+
793+
# Check that both DAL and custom files are included
794+
self.assertIn('select2', media_str)
795+
self.assertIn('autocomplete', media_str)
796+
797+
def test_dal_admin_mixin_docstring(self):
798+
"""Test that DALAdminMixin has proper documentation."""
799+
self.assertIsNotNone(DALAdminMixin.__doc__)
800+
self.assertIn('django-autocomplete-light', DALAdminMixin.__doc__)
801+
self.assertIn('Select2', DALAdminMixin.__doc__)
802+
self.assertIn('Django 5.2', DALAdminMixin.__doc__)
803+
804+
def test_dal_admin_mixin_is_admin_model_admin(self):
805+
"""Test that DALAdminMixin inherits from admin.ModelAdmin."""
806+
self.assertTrue(issubclass(DALAdminMixin, ModelAdmin))
807+
808+
def test_multiple_css_entries_for_specific_media(self):
809+
"""Test that CSS entries are specifically for 'all' media query."""
810+
media = DALAdminMixin.Media
811+
812+
# Should only have 'all' key for CSS
813+
self.assertEqual(len(media.css), 1)
814+
self.assertIn('all', media.css)
815+
816+
def test_css_and_js_are_tuples_not_lists(self):
817+
"""Test that CSS and JS are defined as tuples (immutable)."""
818+
media = DALAdminMixin.Media
819+
820+
self.assertIsInstance(media.css['all'], tuple)
821+
self.assertIsInstance(media.js, tuple)
822+
823+
def test_dal_admin_mixin_with_person_model(self):
824+
"""
825+
Test that DALAdminMixin integrates properly with Person admin.
826+
This is a real-world integration test.
827+
"""
828+
# Verify PersonAdmin inherits from DALAdminMixin
829+
# Note: This test assumes PersonAdmin uses DALAdminMixin in the actual implementation
830+
admin_site = AdminSite()
831+
832+
# Create a test admin using DALAdminMixin
833+
class PersonTestAdmin(DALAdminMixin):
834+
list_display = ('uuid', 'given_name', 'family_name')
835+
836+
person_admin = PersonTestAdmin(Person, admin_site)
837+
838+
# Verify media is properly configured
839+
self.assertTrue(hasattr(person_admin, 'media'))
840+
media_str = str(person_admin.media)
841+
842+
# Check for expected Select2 and autocomplete references
843+
self.assertIn('select2', media_str)
844+
self.assertIn('autocomplete', media_str)
845+
846+

0 commit comments

Comments
 (0)