Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
C
course-discovery
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
course-discovery
Commits
f8a73cdb
Commit
f8a73cdb
authored
Sep 27, 2016
by
Matthew Piatetsky
Committed by
GitHub
Sep 27, 2016
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #349 from edx/ECOM-5436
ECOM-5436 Order courses in a program
parents
e0cd1654
b8100992
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
138 additions
and
7 deletions
+138
-7
bower.json
+2
-1
course_discovery/apps/api/serializers.py
+4
-1
course_discovery/apps/api/tests/test_serializers.py
+9
-0
course_discovery/apps/api/v1/tests/test_views/test_programs.py
+15
-0
course_discovery/apps/course_metadata/admin.py
+8
-2
course_discovery/apps/course_metadata/forms.py
+24
-2
course_discovery/apps/course_metadata/migrations/0029_auto_20160923_1306.py
+26
-0
course_discovery/apps/course_metadata/models.py
+5
-1
course_discovery/apps/course_metadata/tests/factories.py
+1
-0
course_discovery/static/js/sortable_select.js
+44
-0
No files found.
bower.json
View file @
f8a73cdb
...
...
@@ -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"
}
}
course_discovery/apps/api/serializers.py
View file @
f8a73cdb
...
...
@@ -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
,
...
...
course_discovery/apps/api/tests/test_serializers.py
View file @
f8a73cdb
...
...
@@ -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
...
...
course_discovery/apps/api/v1/tests/test_views/test_programs.py
View file @
f8a73cdb
...
...
@@ -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
()
...
...
course_discovery/apps/course_metadata/admin.py
View file @
f8a73cdb
...
...
@@ -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_organizatio
ns'
,
'credit_backing_organizations'
'courses'
,
'
order_courses_by_start_date'
,
'custom_course_runs_display'
,
'excluded_course_ru
ns'
,
'
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
):
...
...
course_discovery/apps/course_metadata/forms.py
View file @
f8a73cdb
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'
,
...
...
course_discovery/apps/course_metadata/migrations/0029_auto_20160923_1306.py
0 → 100644
View file @
f8a73cdb
# -*- 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'
),
),
]
course_discovery/apps/course_metadata/models.py
View file @
f8a73cdb
...
...
@@ -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
)
...
...
course_discovery/apps/course_metadata/tests/factories.py
View file @
f8a73cdb
...
...
@@ -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
):
...
...
course_discovery/static/js/sortable_select.js
0 → 100644
View file @
f8a73cdb
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
})
})
});
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment