Commit f8a73cdb by Matthew Piatetsky Committed by GitHub

Merge pull request #349 from edx/ECOM-5436

ECOM-5436 Order courses in a program
parents e0cd1654 b8100992
......@@ -14,6 +14,7 @@
"underscore": "~1.8.3",
"moment": "~2.13.0",
"pikaday": "https://github.com/owenmead/Pikaday.git#1.4.0",
"clipboard": "1.5.12"
"clipboard": "1.5.12",
"jquery-ui": "1.10.3"
}
}
......@@ -493,7 +493,10 @@ class ProgramSerializer(serializers.ModelSerializer):
)
def get_courses(self, program):
courses, course_runs = self.sort_courses(program)
if program.order_courses_by_start_date:
courses, course_runs = self.sort_courses(program)
else:
courses, course_runs = program.courses.all(), program.course_runs
course_serializer = ProgramCourseSerializer(
courses,
......
......@@ -434,6 +434,15 @@ class ProgramSerializerTests(TestCase):
expected = self.get_expected_data(program, request)
self.assertDictEqual(dict(serializer.data), expected)
def test_data_without_course_sorting(self):
request = make_request()
program = self.create_program()
program.order_courses_by_start_date = False
program.save()
serializer = ProgramSerializer(program, context={'request': request})
expected = self.get_expected_data(program, request)
self.assertDictEqual(dict(serializer.data), expected)
def test_data_with_exclusions(self):
"""
Verify we can specify program excluded_course_runs and the serializers will
......
......@@ -67,6 +67,21 @@ class ProgramViewSetTests(APITestCase):
with self.assertNumQueries(89):
self.assert_retrieve_success(program)
@ddt.data(
(True),
(False),
)
def test_retrieve_with_sorting_flag(self, order_courses_by_start_date=True):
""" Verify the number of queries is the same with sorting flag set to true. """
course_list = CourseFactory.create_batch(3)
for course in course_list:
CourseRunFactory(course=course)
program = ProgramFactory(courses=course_list, order_courses_by_start_date=order_courses_by_start_date)
num_queries = 132 if order_courses_by_start_date else 114
with self.assertNumQueries(num_queries):
self.assert_retrieve_success(program)
self.assertEqual(course_list, list(program.courses.all())) # pylint: disable=no-member
def test_retrieve_without_course_runs(self):
""" Verify the endpoint returns data for a program even if the program's courses have no course runs. """
course = CourseFactory()
......
......@@ -81,8 +81,8 @@ class ProgramAdmin(admin.ModelAdmin):
'min_hours_effort_per_week', 'max_hours_effort_per_week',
)
fields += (
'courses', 'custom_course_runs_display', 'excluded_course_runs', 'authoring_organizations',
'credit_backing_organizations'
'courses', 'order_courses_by_start_date', 'custom_course_runs_display', 'excluded_course_runs',
'authoring_organizations', 'credit_backing_organizations'
)
fields += filter_horizontal
save_error = None
......@@ -109,12 +109,18 @@ class ProgramAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
try:
# courses are ordered by django id, but form.cleaned_data is ordered correctly
obj.courses = form.cleaned_data.get('courses')
obj.save()
self.save_error = False
except ProgramPublisherException as ex:
messages.add_message(request, messages.ERROR, ex.message)
self.save_error = True
class Media:
js = ('bower_components/jquery-ui/ui/minified/jquery-ui.min.js',
'js/sortable_select.js')
@admin.register(ProgramType)
class ProgramTypeAdmin(admin.ModelAdmin):
......
from dal import autocomplete
from dal import widgets
from django import forms
from django.core.exceptions import ValidationError
from django.forms.utils import ErrorList
......@@ -8,10 +8,32 @@ from course_discovery.apps.course_metadata.choices import ProgramStatus
from course_discovery.apps.course_metadata.models import Program, CourseRun
class ProgramAdminForm(forms.ModelForm):
class HackDjangoAutocompleteMixin(object):
# It seems to me there is an issue with the select 2 widget in django autocomplete.
# When the widget loads selected choices it loads them in order of django id, not the order
# they are stored in in the database. This workaround works, but not sure what approach
# would be less hacky. Perhaps opening a PR to the django autocomplete repo if this is
# fact an issue?
class QuerySetSelectMixin2(widgets.WidgetMixin):
def filter_choices_to_render(self, selected_choices):
# preserve ordering of selected_choices in queryset
# https://codybonney.com/creating-a-queryset-from-a-list-while-preserving-order-using-django/
clauses = ' '.join(['WHEN id={} THEN {}'.format(pk, i) for i, pk in enumerate(selected_choices)])
ordering = 'CASE {} END'.format(clauses)
self.choices.queryset = self.choices.queryset.filter(
pk__in=[c for c in selected_choices if c]
).extra(select={'ordering': ordering}, order_by=('ordering',))
widgets.QuerySetSelectMixin = QuerySetSelectMixin2
class ProgramAdminForm(HackDjangoAutocompleteMixin, forms.ModelForm):
class Meta:
model = Program
fields = '__all__'
from dal import autocomplete
widgets = {
'courses': autocomplete.ModelSelect2Multiple(
url='admin_metadata:course-autocomplete',
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
from sortedm2m import fields, operations
class Migration(migrations.Migration):
dependencies = [
('course_metadata', '0028_courserun_hidden'),
]
operations = [
migrations.AddField(
model_name='program',
name='order_courses_by_start_date',
field=models.BooleanField(default=True, help_text='If this box is not checked, courses will be ordered as in the courses select box above.', verbose_name='Order Courses By Start Date'),
),
operations.AlterSortedManyToManyField(
model_name='program',
name='courses',
field=fields.SortedManyToManyField(help_text=None, related_name='programs', to='course_metadata.Course'),
),
]
......@@ -583,7 +583,11 @@ class Program(TimeStampedModel):
)
marketing_slug = models.CharField(
help_text=_('Slug used to generate links to the marketing site'), blank=True, max_length=255, db_index=True)
courses = models.ManyToManyField(Course, related_name='programs')
courses = SortedManyToManyField(Course, related_name='programs')
order_courses_by_start_date = models.BooleanField(
default=True, verbose_name='Order Courses By Start Date',
help_text=_('If this box is not checked, courses will be ordered as in the courses select box above.')
)
# NOTE (CCB): Editors of this field should validate the values to ensure only CourseRuns associated
# with related Courses are stored.
excluded_course_runs = models.ManyToManyField(CourseRun, blank=True)
......
......@@ -251,6 +251,7 @@ class ProgramFactory(factory.django.DjangoModelFactory):
min_hours_effort_per_week = FuzzyInteger(2)
max_hours_effort_per_week = FuzzyInteger(4)
credit_redemption_overview = FuzzyText()
order_courses_by_start_date = True
@factory.post_generation
def courses(self, create, extracted, **kwargs):
......
function updateSelect2Data(visibleCourseTitles){
var i, j,
visibleCourseTitlesLength,
selectOptionsLength,
visibleCourseTitles = [],
selectOptions = [],
items = [],
selectOptionsSelector = '.field-courses .select2-hidden-accessible';
$('.field-courses .select2-selection__choice').each(function(index, value){
if (value.title){
visibleCourseTitles.push(value.title);
}
});
$('.field-courses .select2-hidden-accessible option').each(function(index, value){
selectOptions.push({id: value.value, text: value.text});
});
// Update select2 options with new data
visibleCourseTitlesLength = visibleCourseTitles.length;
selectOptionsLength = selectOptions.length;
for (i = 0; i < visibleCourseTitlesLength; i++) {
for (j = 0; j < selectOptionsLength; j++) {
if (selectOptions[j].text === visibleCourseTitles[i]){
items.push('<option selected="selected" value="' + selectOptions[j].id + '">' +
selectOptions[j].text + '</option>'
);
}
}
}
if (items){
$(selectOptionsSelector).html(items.join('\n'));
}
}
$(window).load(function(){
$(function() {
var domSelector = '.field-courses .select2-selection--multiple';
$('.field-courses ul.select2-selection__rendered').sortable({
containment: 'parent',
update: updateSelect2Data
})
})
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment