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
b8100992
Commit
b8100992
authored
Sep 20, 2016
by
Matthew Piatetsky
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Sortable courses dropdown
parent
e0cd1654
Show whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
137 additions
and
6 deletions
+137
-6
bower.json
+2
-1
course_discovery/apps/api/serializers.py
+3
-0
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 @
b8100992
...
@@ -14,6 +14,7 @@
...
@@ -14,6 +14,7 @@
"underscore"
:
"~1.8.3"
,
"underscore"
:
"~1.8.3"
,
"moment"
:
"~2.13.0"
,
"moment"
:
"~2.13.0"
,
"pikaday"
:
"https://github.com/owenmead/Pikaday.git#1.4.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 @
b8100992
...
@@ -493,7 +493,10 @@ class ProgramSerializer(serializers.ModelSerializer):
...
@@ -493,7 +493,10 @@ class ProgramSerializer(serializers.ModelSerializer):
)
)
def
get_courses
(
self
,
program
):
def
get_courses
(
self
,
program
):
if
program
.
order_courses_by_start_date
:
courses
,
course_runs
=
self
.
sort_courses
(
program
)
courses
,
course_runs
=
self
.
sort_courses
(
program
)
else
:
courses
,
course_runs
=
program
.
courses
.
all
(),
program
.
course_runs
course_serializer
=
ProgramCourseSerializer
(
course_serializer
=
ProgramCourseSerializer
(
courses
,
courses
,
...
...
course_discovery/apps/api/tests/test_serializers.py
View file @
b8100992
...
@@ -434,6 +434,15 @@ class ProgramSerializerTests(TestCase):
...
@@ -434,6 +434,15 @@ class ProgramSerializerTests(TestCase):
expected
=
self
.
get_expected_data
(
program
,
request
)
expected
=
self
.
get_expected_data
(
program
,
request
)
self
.
assertDictEqual
(
dict
(
serializer
.
data
),
expected
)
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
):
def
test_data_with_exclusions
(
self
):
"""
"""
Verify we can specify program excluded_course_runs and the serializers will
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 @
b8100992
...
@@ -67,6 +67,21 @@ class ProgramViewSetTests(APITestCase):
...
@@ -67,6 +67,21 @@ class ProgramViewSetTests(APITestCase):
with
self
.
assertNumQueries
(
89
):
with
self
.
assertNumQueries
(
89
):
self
.
assert_retrieve_success
(
program
)
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
):
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. """
""" Verify the endpoint returns data for a program even if the program's courses have no course runs. """
course
=
CourseFactory
()
course
=
CourseFactory
()
...
...
course_discovery/apps/course_metadata/admin.py
View file @
b8100992
...
@@ -81,8 +81,8 @@ class ProgramAdmin(admin.ModelAdmin):
...
@@ -81,8 +81,8 @@ class ProgramAdmin(admin.ModelAdmin):
'min_hours_effort_per_week'
,
'max_hours_effort_per_week'
,
'min_hours_effort_per_week'
,
'max_hours_effort_per_week'
,
)
)
fields
+=
(
fields
+=
(
'courses'
,
'
custom_course_runs_display'
,
'excluded_course_runs'
,
'authoring_organizatio
ns'
,
'courses'
,
'
order_courses_by_start_date'
,
'custom_course_runs_display'
,
'excluded_course_ru
ns'
,
'credit_backing_organizations'
'
authoring_organizations'
,
'
credit_backing_organizations'
)
)
fields
+=
filter_horizontal
fields
+=
filter_horizontal
save_error
=
None
save_error
=
None
...
@@ -109,12 +109,18 @@ class ProgramAdmin(admin.ModelAdmin):
...
@@ -109,12 +109,18 @@ class ProgramAdmin(admin.ModelAdmin):
def
save_model
(
self
,
request
,
obj
,
form
,
change
):
def
save_model
(
self
,
request
,
obj
,
form
,
change
):
try
:
try
:
# courses are ordered by django id, but form.cleaned_data is ordered correctly
obj
.
courses
=
form
.
cleaned_data
.
get
(
'courses'
)
obj
.
save
()
obj
.
save
()
self
.
save_error
=
False
self
.
save_error
=
False
except
ProgramPublisherException
as
ex
:
except
ProgramPublisherException
as
ex
:
messages
.
add_message
(
request
,
messages
.
ERROR
,
ex
.
message
)
messages
.
add_message
(
request
,
messages
.
ERROR
,
ex
.
message
)
self
.
save_error
=
True
self
.
save_error
=
True
class
Media
:
js
=
(
'bower_components/jquery-ui/ui/minified/jquery-ui.min.js'
,
'js/sortable_select.js'
)
@admin.register
(
ProgramType
)
@admin.register
(
ProgramType
)
class
ProgramTypeAdmin
(
admin
.
ModelAdmin
):
class
ProgramTypeAdmin
(
admin
.
ModelAdmin
):
...
...
course_discovery/apps/course_metadata/forms.py
View file @
b8100992
from
dal
import
autocomplete
from
dal
import
widgets
from
django
import
forms
from
django
import
forms
from
django.core.exceptions
import
ValidationError
from
django.core.exceptions
import
ValidationError
from
django.forms.utils
import
ErrorList
from
django.forms.utils
import
ErrorList
...
@@ -8,10 +8,32 @@ from course_discovery.apps.course_metadata.choices import ProgramStatus
...
@@ -8,10 +8,32 @@ from course_discovery.apps.course_metadata.choices import ProgramStatus
from
course_discovery.apps.course_metadata.models
import
Program
,
CourseRun
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
:
class
Meta
:
model
=
Program
model
=
Program
fields
=
'__all__'
fields
=
'__all__'
from
dal
import
autocomplete
widgets
=
{
widgets
=
{
'courses'
:
autocomplete
.
ModelSelect2Multiple
(
'courses'
:
autocomplete
.
ModelSelect2Multiple
(
url
=
'admin_metadata:course-autocomplete'
,
url
=
'admin_metadata:course-autocomplete'
,
...
...
course_discovery/apps/course_metadata/migrations/0029_auto_20160923_1306.py
0 → 100644
View file @
b8100992
# -*- 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 @
b8100992
...
@@ -583,7 +583,11 @@ class Program(TimeStampedModel):
...
@@ -583,7 +583,11 @@ class Program(TimeStampedModel):
)
)
marketing_slug
=
models
.
CharField
(
marketing_slug
=
models
.
CharField
(
help_text
=
_
(
'Slug used to generate links to the marketing site'
),
blank
=
True
,
max_length
=
255
,
db_index
=
True
)
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
# NOTE (CCB): Editors of this field should validate the values to ensure only CourseRuns associated
# with related Courses are stored.
# with related Courses are stored.
excluded_course_runs
=
models
.
ManyToManyField
(
CourseRun
,
blank
=
True
)
excluded_course_runs
=
models
.
ManyToManyField
(
CourseRun
,
blank
=
True
)
...
...
course_discovery/apps/course_metadata/tests/factories.py
View file @
b8100992
...
@@ -251,6 +251,7 @@ class ProgramFactory(factory.django.DjangoModelFactory):
...
@@ -251,6 +251,7 @@ class ProgramFactory(factory.django.DjangoModelFactory):
min_hours_effort_per_week
=
FuzzyInteger
(
2
)
min_hours_effort_per_week
=
FuzzyInteger
(
2
)
max_hours_effort_per_week
=
FuzzyInteger
(
4
)
max_hours_effort_per_week
=
FuzzyInteger
(
4
)
credit_redemption_overview
=
FuzzyText
()
credit_redemption_overview
=
FuzzyText
()
order_courses_by_start_date
=
True
@factory.post_generation
@factory.post_generation
def
courses
(
self
,
create
,
extracted
,
**
kwargs
):
def
courses
(
self
,
create
,
extracted
,
**
kwargs
):
...
...
course_discovery/static/js/sortable_select.js
0 → 100644
View file @
b8100992
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