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
c5d0f4da
Commit
c5d0f4da
authored
Feb 06, 2018
by
Anthony Mangano
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Create entitlement-aware CourseRuns in Publisher
parent
8d83ddfd
Hide whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
487 additions
and
41 deletions
+487
-41
course_discovery/apps/publisher/api/tests/test_views.py
+20
-0
course_discovery/apps/publisher/api/views.py
+15
-0
course_discovery/apps/publisher/forms.py
+7
-0
course_discovery/apps/publisher/models.py
+12
-0
course_discovery/apps/publisher/templates/publisher/add_courserun_form.html
+4
-3
course_discovery/apps/publisher/tests/factories.py
+1
-1
course_discovery/apps/publisher/tests/test_models.py
+10
-1
course_discovery/apps/publisher/tests/test_views.py
+301
-1
course_discovery/apps/publisher/views.py
+65
-31
course_discovery/conf/locale/en/LC_MESSAGES/django.mo
+0
-0
course_discovery/conf/locale/en/LC_MESSAGES/django.po
+7
-1
course_discovery/conf/locale/en/LC_MESSAGES/djangojs.mo
+0
-0
course_discovery/conf/locale/en/LC_MESSAGES/djangojs.po
+1
-1
course_discovery/conf/locale/eo/LC_MESSAGES/django.mo
+0
-0
course_discovery/conf/locale/eo/LC_MESSAGES/django.po
+9
-1
course_discovery/conf/locale/eo/LC_MESSAGES/djangojs.mo
+0
-0
course_discovery/conf/locale/eo/LC_MESSAGES/djangojs.po
+1
-1
course_discovery/static/js/publisher/toggle-seat-form.js
+34
-0
No files found.
course_discovery/apps/publisher/api/tests/test_views.py
View file @
c5d0f4da
...
...
@@ -920,6 +920,26 @@ class CoursesAutoCompleteTests(SiteMixin, TestCase):
response
=
self
.
client
.
get
(
self
.
course_autocomplete_url
.
format
(
title
=
'test'
))
self
.
_assert_response
(
response
,
1
)
def
test_course_autocomplete_entitlement_info
(
self
):
""" Verify that the response from CourseAutoComplete includes info about whether or not the courses
use entitlements. """
self
.
user
.
groups
.
add
(
Group
.
objects
.
get
(
name
=
ADMIN_GROUP_NAME
))
self
.
course
.
version
=
Course
.
SEAT_VERSION
self
.
course
.
save
()
self
.
course2
.
version
=
Course
.
ENTITLEMENT_VERSION
self
.
course2
.
save
()
response
=
self
.
client
.
get
(
self
.
course_autocomplete_url
.
format
(
title
=
'test'
))
self
.
assertEqual
(
response
.
status_code
,
200
)
data
=
json
.
loads
(
response
.
content
.
decode
(
'utf-8'
))
self
.
assertEqual
(
len
(
data
[
'results'
]),
2
)
results_by_id
=
{
record
[
'id'
]:
record
for
record
in
data
[
'results'
]}
self
.
assertFalse
(
results_by_id
[
self
.
course
.
id
][
'uses_entitlements'
])
self
.
assertTrue
(
results_by_id
[
self
.
course2
.
id
][
'uses_entitlements'
])
def
_assert_response
(
self
,
response
,
expected_length
):
""" Assert autocomplete response. """
self
.
assertEqual
(
response
.
status_code
,
200
)
...
...
course_discovery/apps/publisher/api/views.py
View file @
c5d0f4da
...
...
@@ -100,6 +100,21 @@ class RevertCourseRevisionView(APIView):
class
CoursesAutoComplete
(
LoginRequiredMixin
,
autocomplete
.
Select2QuerySetView
):
""" Course Autocomplete. """
def
get_results
(
self
,
context
):
"""
Format the result set so that it can be returned as a JSON object.
Overridden from https://github.com/yourlabs/django-autocomplete-light/blob/3.1.8/src/dal_select2/views.py#L14
to include information about whether or not the suggested Course(s) use entitlements.
"""
return
[
{
'id'
:
self
.
get_result_value
(
course
),
'text'
:
self
.
get_result_label
(
course
),
'uses_entitlements'
:
course
.
uses_entitlements
}
for
course
in
context
[
'object_list'
]
]
def
get_queryset
(
self
):
if
self
.
q
:
user
=
self
.
request
.
user
...
...
course_discovery/apps/publisher/forms.py
View file @
c5d0f4da
...
...
@@ -220,6 +220,13 @@ class CourseSearchForm(forms.Form):
required
=
True
,
)
def
__init__
(
self
,
*
args
,
**
kwargs
):
qs
=
kwargs
.
pop
(
'queryset'
,
None
)
super
(
CourseSearchForm
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
if
qs
is
not
None
:
self
.
fields
[
'course'
]
.
queryset
=
qs
class
CourseRunForm
(
BaseForm
):
start
=
forms
.
DateTimeField
(
label
=
_
(
'Course Start Date'
),
required
=
True
)
...
...
course_discovery/apps/publisher/models.py
View file @
c5d0f4da
...
...
@@ -101,6 +101,13 @@ class Course(TimeStampedModel, ChangedByMixin):
return
self
.
title
@property
def
uses_entitlements
(
self
):
"""
Returns a bool indicating whether or not this Course has been configured to use entitlement products.
"""
return
self
.
version
==
self
.
ENTITLEMENT_VERSION
@property
def
post_back_url
(
self
):
return
reverse
(
'publisher:publisher_courses_edit'
,
kwargs
=
{
'pk'
:
self
.
id
})
...
...
@@ -508,6 +515,11 @@ class CourseEntitlement(TimeStampedModel):
'default'
:
0.00
,
}
MODE_TO_SEAT_TYPE_MAPPING
=
{
VERIFIED
:
Seat
.
VERIFIED
,
PROFESSIONAL
:
Seat
.
PROFESSIONAL
}
course
=
models
.
ForeignKey
(
Course
,
related_name
=
'entitlements'
)
mode
=
models
.
CharField
(
max_length
=
63
,
choices
=
COURSE_MODE_CHOICES
,
verbose_name
=
'Course mode'
)
price
=
models
.
DecimalField
(
**
PRICE_FIELD_CONFIG
)
...
...
course_discovery/apps/publisher/templates/publisher/add_courserun_form.html
View file @
c5d0f4da
...
...
@@ -56,7 +56,7 @@
</div>
{% endif %}
<div
class=
"layout-full layout"
>
<div
class=
"layout-full layout
js-courserun-form
"
>
<div
class=
"course-form"
>
<div
class=
"course-information"
>
<fieldset
class=
"form-group grid-container grid-manual"
>
...
...
@@ -110,7 +110,7 @@
</div>
</div>
<div
class=
"layout-full layout"
>
<div
class=
"layout-full layout
js-seat-form{% if hide_seat_form %} hidden{% endif %}
"
>
<div
class=
"course-form"
>
<div
class=
"course-information"
>
<fieldset
class=
"form-group grid-container grid-manual"
>
...
...
@@ -136,7 +136,7 @@
</div>
</div>
{% if seat_form.price.errors %}
<div
class=
"field-message has-error"
>
<div
class=
"field-message has-error
js-seat-form-errors
"
>
<span
class=
"field-message-content"
>
{{ seat_form.price.errors|escape }}
</span>
...
...
@@ -165,6 +165,7 @@
{% block extra_js %}
<script
src=
"{% static 'js/publisher/course-tabs.js' %}"
></script>
<script
src=
"{% static 'js/publisher/seat-type-change.js' %}"
></script>
<script
src=
"{% static 'js/publisher/toggle-seat-form.js' %}"
></script>
{% endblock %}
{% block js_without_compress %}
...
...
course_discovery/apps/publisher/tests/factories.py
View file @
c5d0f4da
...
...
@@ -27,7 +27,7 @@ class CourseFactory(factory.DjangoModelFactory):
learner_testimonial
=
FuzzyText
()
level_type
=
factory
.
SubFactory
(
factories
.
LevelTypeFactory
)
image
=
factory
.
django
.
ImageField
()
version
=
FuzzyInteger
(
0
,
1
)
version
=
Course
.
SEAT_VERSION
primary_subject
=
factory
.
SubFactory
(
factories
.
SubjectFactory
)
secondary_subject
=
factory
.
SubFactory
(
factories
.
SubjectFactory
)
...
...
course_discovery/apps/publisher/tests/test_models.py
View file @
c5d0f4da
...
...
@@ -16,7 +16,8 @@ from course_discovery.apps.course_metadata.tests.factories import OrganizationFa
from
course_discovery.apps.ietf_language_tags.models
import
LanguageTag
from
course_discovery.apps.publisher.choices
import
CourseRunStateChoices
,
CourseStateChoices
,
PublisherUserRole
from
course_discovery.apps.publisher.mixins
import
check_course_organization_permission
from
course_discovery.apps.publisher.models
import
CourseUserRole
,
OrganizationExtension
,
OrganizationUserRole
,
Seat
from
course_discovery.apps.publisher.models
import
(
Course
,
CourseUserRole
,
OrganizationExtension
,
OrganizationUserRole
,
Seat
)
from
course_discovery.apps.publisher.tests
import
factories
...
...
@@ -179,6 +180,14 @@ class CourseTests(TestCase):
course
=
self
.
course
,
role
=
PublisherUserRole
.
Publisher
,
user
=
self
.
user3
)
def
test_uses_entitlements
(
self
):
""" Verify that uses_entitlements is True when version is set to ENTITLEMENT_VERSION, and False otherwise. """
self
.
course
.
version
=
Course
.
SEAT_VERSION
assert
not
self
.
course
.
uses_entitlements
self
.
course
.
version
=
Course
.
ENTITLEMENT_VERSION
assert
self
.
course
.
uses_entitlements
def
test_str
(
self
):
""" Verify casting an instance to a string returns a string containing the course title. """
self
.
assertEqual
(
str
(
self
.
course
),
self
.
course
.
title
)
...
...
course_discovery/apps/publisher/tests/test_views.py
View file @
c5d0f4da
...
...
@@ -22,7 +22,7 @@ from pytz import timezone
from
testfixtures
import
LogCapture
from
course_discovery.apps.api.tests.mixins
import
SiteMixin
from
course_discovery.apps.core.models
import
User
from
course_discovery.apps.core.models
import
Currency
,
User
from
course_discovery.apps.core.tests.factories
import
USER_PASSWORD
,
UserFactory
from
course_discovery.apps.core.tests.helpers
import
make_image_file
from
course_discovery.apps.course_metadata.tests
import
toggle_switch
...
...
@@ -349,6 +349,7 @@ class CreateCourseViewTests(SiteMixin, TestCase):
self
.
assertEqual
(
response
.
status_code
,
400
)
@ddt.ddt
class
CreateCourseRunViewTests
(
SiteMixin
,
TestCase
):
""" Tests for the publisher `CreateCourseRunView`. """
...
...
@@ -356,7 +357,11 @@ class CreateCourseRunViewTests(SiteMixin, TestCase):
super
(
CreateCourseRunViewTests
,
self
)
.
setUp
()
self
.
user
=
UserFactory
()
self
.
course_run
=
factories
.
CourseRunFactory
()
self
.
course
=
self
.
course_run
.
course
self
.
course
.
version
=
Course
.
SEAT_VERSION
self
.
course
.
save
()
factories
.
CourseStateFactory
(
course
=
self
.
course
)
factories
.
CourseUserRoleFactory
.
create
(
course
=
self
.
course
,
role
=
PublisherUserRole
.
CourseTeam
,
user
=
self
.
user
)
factories
.
CourseUserRoleFactory
.
create
(
course
=
self
.
course
,
role
=
PublisherUserRole
.
Publisher
,
user
=
UserFactory
())
...
...
@@ -415,6 +420,22 @@ class CreateCourseRunViewTests(SiteMixin, TestCase):
response
=
self
.
client
.
get
(
self
.
create_course_run_url_new
)
self
.
assertEqual
(
response
.
status_code
,
200
)
def
test_courserun_form_for_course_with_entitlements
(
self
):
""" Verify that the Seat fields are hidden for Courses that use entitlements. """
self
.
course
.
version
=
Course
.
ENTITLEMENT_VERSION
self
.
course
.
save
()
response
=
self
.
client
.
get
(
self
.
create_course_run_url_new
)
self
.
assertContains
(
response
,
'<div class="layout-full layout js-seat-form hidden">'
,
status_code
=
200
)
def
test_courserun_form_for_course_without_entitlements
(
self
):
""" Verify that the Seat fields are visible for Courses that do not use entitlements. """
self
.
course
.
version
=
Course
.
SEAT_VERSION
self
.
course
.
save
()
response
=
self
.
client
.
get
(
self
.
create_course_run_url_new
)
self
.
assertContains
(
response
,
'<div class="layout-full layout js-seat-form">'
,
status_code
=
200
)
def
test_create_course_run_without_permission
(
self
):
"""
Verify that a course run create page shows the proper error when non-publisher user tries to
...
...
@@ -628,6 +649,126 @@ class CreateCourseRunViewTests(SiteMixin, TestCase):
)
)
@ddt.data
(
(
CourseEntitlement
.
PROFESSIONAL
,
1
,
[{
'type'
:
Seat
.
PROFESSIONAL
,
'price'
:
1
}]),
(
CourseEntitlement
.
VERIFIED
,
1
,
[{
'type'
:
Seat
.
VERIFIED
,
'price'
:
1
},
{
'type'
:
Seat
.
AUDIT
,
'price'
:
0
}]),
)
@ddt.unpack
def
test_create_run_for_entitlement_course
(
self
,
entitlement_mode
,
entitlement_price
,
expected_seats
):
"""
Verify that when creating a run for a Course that uses entitlements, Seats are created from the
entitlement data associated with the parent course.
"""
self
.
course
.
version
=
Course
.
ENTITLEMENT_VERSION
self
.
course
.
save
()
assign_perm
(
OrganizationExtension
.
VIEW_COURSE_RUN
,
self
.
organization_extension
.
group
,
self
.
organization_extension
)
toggle_switch
(
'publisher_create_audit_seats_for_verified_course_runs'
,
True
)
self
.
course
.
entitlements
.
create
(
mode
=
entitlement_mode
,
price
=
entitlement_price
)
post_data
=
{
'start'
:
'2018-02-01 00:00:00'
,
'end'
:
'2018-02-28 00:00:00'
,
'pacing_type'
:
'instructor_paced'
}
num_courseruns_before
=
self
.
course
.
course_runs
.
count
()
response
=
self
.
client
.
post
(
self
.
create_course_run_url_new
,
post_data
)
num_courseruns_after
=
self
.
course
.
course_runs
.
count
()
self
.
assertTrue
(
num_courseruns_after
>
num_courseruns_before
)
new_courserun
=
self
.
course
.
course_runs
.
latest
(
'created'
)
self
.
assertEqual
(
new_courserun
.
start
.
strftime
(
'
%
Y-
%
m-
%
d
%
H:
%
M:
%
S'
),
post_data
[
'start'
])
self
.
assertEqual
(
new_courserun
.
end
.
strftime
(
'
%
Y-
%
m-
%
d
%
H:
%
M:
%
S'
),
post_data
[
'end'
])
self
.
assertEqual
(
new_courserun
.
pacing_type
,
post_data
[
'pacing_type'
])
self
.
assertRedirects
(
response
,
expected_url
=
reverse
(
'publisher:publisher_course_run_detail'
,
kwargs
=
{
'pk'
:
new_courserun
.
id
}),
status_code
=
302
,
target_status_code
=
200
)
self
.
assertEqual
(
new_courserun
.
seats
.
count
(),
len
(
expected_seats
))
for
expected_seat
in
expected_seats
:
actual_seat
=
new_courserun
.
seats
.
get
(
type
=
expected_seat
[
'type'
])
self
.
assertEqual
(
expected_seat
[
'type'
],
actual_seat
.
type
)
self
.
assertEqual
(
expected_seat
[
'price'
],
actual_seat
.
price
)
def
test_create_run_for_misconfigured_entitlement_course
(
self
):
"""
Verify that a user cannot create a new course run for a Course that has been configured to use entitlements
but does not have exactly one entitlement.
"""
self
.
course
.
version
=
Course
.
ENTITLEMENT_VERSION
self
.
course
.
save
()
assign_perm
(
OrganizationExtension
.
VIEW_COURSE_RUN
,
self
.
organization_extension
.
group
,
self
.
organization_extension
)
post_data
=
{
'start'
:
'2018-02-01 00:00:00'
,
'end'
:
'2018-02-28 00:00:00'
,
'pacing_type'
:
'instructor_paced'
}
self
.
assertEqual
(
self
.
course
.
entitlements
.
count
(),
0
)
num_courseruns_before
=
self
.
course
.
course_runs
.
count
()
response
=
self
.
client
.
post
(
self
.
create_course_run_url_new
,
post_data
)
num_courseruns_after
=
self
.
course
.
course_runs
.
count
()
self
.
assertEqual
(
num_courseruns_before
,
num_courseruns_after
)
self
.
assertContains
(
response
,
'The certificate configuration for this course is incorrect'
,
status_code
=
400
)
self
.
course
.
entitlements
.
create
(
mode
=
CourseEntitlement
.
VERIFIED
,
price
=
1
)
self
.
course
.
entitlements
.
create
(
mode
=
CourseEntitlement
.
PROFESSIONAL
,
price
=
1
)
self
.
assertEqual
(
self
.
course
.
entitlements
.
count
(),
2
)
response
=
self
.
client
.
post
(
self
.
create_course_run_url_new
,
post_data
)
num_courseruns_after
=
self
.
course
.
course_runs
.
count
()
self
.
assertEqual
(
num_courseruns_before
,
num_courseruns_after
)
self
.
assertContains
(
response
,
'The certificate configuration for this course is incorrect'
,
status_code
=
400
)
def
test_create_run_for_non_usd_entitlement_course
(
self
):
"""
Verify that a user cannot create a new course run for a Course that has been configured to use entitlements
with a currency other than USD.
"""
self
.
course
.
version
=
Course
.
ENTITLEMENT_VERSION
self
.
course
.
save
()
assign_perm
(
OrganizationExtension
.
VIEW_COURSE_RUN
,
self
.
organization_extension
.
group
,
self
.
organization_extension
)
post_data
=
{
'start'
:
'2018-02-01 00:00:00'
,
'end'
:
'2018-02-28 00:00:00'
,
'pacing_type'
:
'instructor_paced'
}
self
.
course
.
entitlements
.
create
(
mode
=
CourseEntitlement
.
VERIFIED
,
price
=
100
,
currency
=
Currency
.
objects
.
get
(
code
=
'JPY'
)
)
num_courseruns_before
=
self
.
course
.
course_runs
.
count
()
response
=
self
.
client
.
post
(
self
.
create_course_run_url_new
,
post_data
)
num_courseruns_after
=
self
.
course
.
course_runs
.
count
()
self
.
assertEqual
(
num_courseruns_before
,
num_courseruns_after
)
self
.
assertContains
(
response
,
'The certificate configuration for this course is incorrect'
,
status_code
=
400
)
def
test_create_run_for_entitlement_course_with_seat_data_in_form
(
self
):
"""
Verify that a user cannot submit Seat data with the form when creating a new course run for a Course that has
been configured to use entitlements.
"""
self
.
course
.
version
=
Course
.
ENTITLEMENT_VERSION
self
.
course
.
save
()
assign_perm
(
OrganizationExtension
.
VIEW_COURSE_RUN
,
self
.
organization_extension
.
group
,
self
.
organization_extension
)
post_data
=
{
'start'
:
'2018-02-01 00:00:00'
,
'end'
:
'2018-02-28 00:00:00'
,
'pacing_type'
:
'instructor_paced'
,
'type'
:
Seat
.
VERIFIED
,
'price'
:
2
}
self
.
course
.
entitlements
.
create
(
mode
=
CourseEntitlement
.
PROFESSIONAL
,
price
=
1
)
num_courseruns_before
=
self
.
course
.
course_runs
.
count
()
response
=
self
.
client
.
post
(
self
.
create_course_run_url_new
,
post_data
)
num_courseruns_after
=
self
.
course
.
course_runs
.
count
()
self
.
assertEqual
(
num_courseruns_before
,
num_courseruns_after
)
self
.
assertContains
(
response
,
'The page could not be updated.'
,
status_code
=
400
)
@ddt.ddt
class
CourseRunDetailTests
(
SiteMixin
,
TestCase
):
...
...
@@ -3496,6 +3637,7 @@ class CourseRevisionViewTests(SiteMixin, TestCase):
return
self
.
client
.
get
(
path
=
revision_path
)
@ddt.ddt
class
CreateRunFromDashboardViewTests
(
SiteMixin
,
TestCase
):
""" Tests for the publisher `CreateRunFromDashboardView`. """
...
...
@@ -3503,7 +3645,11 @@ class CreateRunFromDashboardViewTests(SiteMixin, TestCase):
super
(
CreateRunFromDashboardViewTests
,
self
)
.
setUp
()
self
.
user
=
UserFactory
()
self
.
organization_extension
=
factories
.
OrganizationExtensionFactory
()
self
.
course
=
factories
.
CourseFactory
(
organizations
=
[
self
.
organization_extension
.
organization
])
self
.
course
.
version
=
Course
.
SEAT_VERSION
self
.
course
.
save
()
factories
.
CourseStateFactory
(
course
=
self
.
course
)
factories
.
CourseUserRoleFactory
.
create
(
course
=
self
.
course
,
role
=
PublisherUserRole
.
CourseTeam
,
user
=
self
.
user
)
factories
.
CourseUserRoleFactory
.
create
(
course
=
self
.
course
,
role
=
PublisherUserRole
.
Publisher
,
user
=
UserFactory
())
...
...
@@ -3594,6 +3740,160 @@ class CreateRunFromDashboardViewTests(SiteMixin, TestCase):
expected_subject
=
'Studio URL requested: {title}'
.
format
(
title
=
self
.
course
.
title
)
self
.
assertEqual
(
str
(
mail
.
outbox
[
0
]
.
subject
),
expected_subject
)
def
test_courserun_form_includes_seat_fields_on_error_for_non_entitlement_course
(
self
):
""" Verify that the Seat fields are visible when error occurs for Courses that do not use entitlements. """
self
.
course
.
version
=
Course
.
SEAT_VERSION
self
.
course
.
save
()
post_data
=
{
'course'
:
self
.
course
.
id
}
response
=
self
.
client
.
post
(
self
.
create_course_run_url
,
post_data
)
self
.
assertContains
(
response
,
'<div class="layout-full layout js-seat-form">'
,
status_code
=
400
)
def
test_courserun_form_does_not_include_seat_fields_on_error_for_entitlement_course
(
self
):
""" Verify that the Seat fields are hidden when error occurs for Courses that use entitlements. """
self
.
course
.
version
=
Course
.
ENTITLEMENT_VERSION
self
.
course
.
save
()
post_data
=
{
'course'
:
self
.
course
.
id
}
response
=
self
.
client
.
post
(
self
.
create_course_run_url
,
post_data
)
self
.
assertContains
(
response
,
'<div class="layout-full layout js-seat-form hidden">'
,
status_code
=
400
)
@ddt.data
(
(
CourseEntitlement
.
PROFESSIONAL
,
1
,
[{
'type'
:
Seat
.
PROFESSIONAL
,
'price'
:
1
}]),
(
CourseEntitlement
.
VERIFIED
,
1
,
[{
'type'
:
Seat
.
VERIFIED
,
'price'
:
1
},
{
'type'
:
Seat
.
AUDIT
,
'price'
:
0
}]),
)
@ddt.unpack
def
test_create_run_for_entitlement_course
(
self
,
entitlement_mode
,
entitlement_price
,
expected_seats
):
"""
Verify that when creating a run for a Course that uses entitlements, Seats are created from the
entitlement data associated with the parent course.
"""
self
.
course
.
version
=
Course
.
ENTITLEMENT_VERSION
self
.
course
.
save
()
assign_perm
(
OrganizationExtension
.
VIEW_COURSE_RUN
,
self
.
organization_extension
.
group
,
self
.
organization_extension
)
toggle_switch
(
'publisher_create_audit_seats_for_verified_course_runs'
,
True
)
self
.
course
.
entitlements
.
create
(
mode
=
entitlement_mode
,
price
=
entitlement_price
)
post_data
=
{
'start'
:
'2018-02-01 00:00:00'
,
'end'
:
'2018-02-28 00:00:00'
,
'pacing_type'
:
'instructor_paced'
,
'course'
:
self
.
course
.
id
}
num_courseruns_before
=
self
.
course
.
course_runs
.
count
()
response
=
self
.
client
.
post
(
self
.
create_course_run_url
,
post_data
)
num_courseruns_after
=
self
.
course
.
course_runs
.
count
()
self
.
assertTrue
(
num_courseruns_after
>
num_courseruns_before
)
new_courserun
=
self
.
course
.
course_runs
.
latest
(
'created'
)
self
.
assertEqual
(
new_courserun
.
start
.
strftime
(
'
%
Y-
%
m-
%
d
%
H:
%
M:
%
S'
),
post_data
[
'start'
])
self
.
assertEqual
(
new_courserun
.
end
.
strftime
(
'
%
Y-
%
m-
%
d
%
H:
%
M:
%
S'
),
post_data
[
'end'
])
self
.
assertEqual
(
new_courserun
.
pacing_type
,
post_data
[
'pacing_type'
])
self
.
assertRedirects
(
response
,
expected_url
=
reverse
(
'publisher:publisher_course_run_detail'
,
kwargs
=
{
'pk'
:
new_courserun
.
id
}),
status_code
=
302
,
target_status_code
=
200
)
self
.
assertEqual
(
new_courserun
.
seats
.
count
(),
len
(
expected_seats
))
for
expected_seat
in
expected_seats
:
actual_seat
=
new_courserun
.
seats
.
get
(
type
=
expected_seat
[
'type'
])
self
.
assertEqual
(
expected_seat
[
'type'
],
actual_seat
.
type
)
self
.
assertEqual
(
expected_seat
[
'price'
],
actual_seat
.
price
)
def
test_create_run_for_misconfigured_entitlement_course
(
self
):
"""
Verify that a user cannot create a new course run for a Course that has been configured to use entitlements
but does not have exactly one entitlement.
"""
self
.
course
.
version
=
Course
.
ENTITLEMENT_VERSION
self
.
course
.
save
()
assign_perm
(
OrganizationExtension
.
VIEW_COURSE_RUN
,
self
.
organization_extension
.
group
,
self
.
organization_extension
)
post_data
=
{
'start'
:
'2018-02-01 00:00:00'
,
'end'
:
'2018-02-28 00:00:00'
,
'pacing_type'
:
'instructor_paced'
,
'course'
:
self
.
course
.
id
}
self
.
assertEqual
(
self
.
course
.
entitlements
.
count
(),
0
)
num_courseruns_before
=
self
.
course
.
course_runs
.
count
()
response
=
self
.
client
.
post
(
self
.
create_course_run_url
,
post_data
)
num_courseruns_after
=
self
.
course
.
course_runs
.
count
()
self
.
assertEqual
(
num_courseruns_before
,
num_courseruns_after
)
self
.
assertContains
(
response
,
'The certificate configuration for this course is incorrect'
,
status_code
=
400
)
self
.
course
.
entitlements
.
create
(
mode
=
CourseEntitlement
.
VERIFIED
,
price
=
1
)
self
.
course
.
entitlements
.
create
(
mode
=
CourseEntitlement
.
PROFESSIONAL
,
price
=
1
)
self
.
assertEqual
(
self
.
course
.
entitlements
.
count
(),
2
)
response
=
self
.
client
.
post
(
self
.
create_course_run_url
,
post_data
)
num_courseruns_after
=
self
.
course
.
course_runs
.
count
()
self
.
assertEqual
(
num_courseruns_before
,
num_courseruns_after
)
self
.
assertContains
(
response
,
'The certificate configuration for this course is incorrect'
,
status_code
=
400
)
def
test_create_run_for_non_usd_entitlement_course
(
self
):
"""
Verify that a user cannot create a new course run for a Course that has been configured to use entitlements
with a currency other than USD.
"""
self
.
course
.
version
=
Course
.
ENTITLEMENT_VERSION
self
.
course
.
save
()
assign_perm
(
OrganizationExtension
.
VIEW_COURSE_RUN
,
self
.
organization_extension
.
group
,
self
.
organization_extension
)
post_data
=
{
'start'
:
'2018-02-01 00:00:00'
,
'end'
:
'2018-02-28 00:00:00'
,
'pacing_type'
:
'instructor_paced'
,
'course'
:
self
.
course
.
id
}
self
.
course
.
entitlements
.
create
(
mode
=
CourseEntitlement
.
VERIFIED
,
price
=
100
,
currency
=
Currency
.
objects
.
get
(
code
=
'JPY'
)
)
num_courseruns_before
=
self
.
course
.
course_runs
.
count
()
response
=
self
.
client
.
post
(
self
.
create_course_run_url
,
post_data
)
num_courseruns_after
=
self
.
course
.
course_runs
.
count
()
self
.
assertEqual
(
num_courseruns_before
,
num_courseruns_after
)
self
.
assertContains
(
response
,
'The certificate configuration for this course is incorrect'
,
status_code
=
400
)
def
test_create_run_for_entitlement_course_with_seat_data_in_form
(
self
):
"""
Verify that a user cannot submit Seat data with the form when creating a new course run for a Course that has
been configured to use entitlements.
"""
self
.
course
.
version
=
Course
.
ENTITLEMENT_VERSION
self
.
course
.
save
()
assign_perm
(
OrganizationExtension
.
VIEW_COURSE_RUN
,
self
.
organization_extension
.
group
,
self
.
organization_extension
)
post_data
=
{
'start'
:
'2018-02-01 00:00:00'
,
'end'
:
'2018-02-28 00:00:00'
,
'pacing_type'
:
'instructor_paced'
,
'course'
:
self
.
course
.
id
,
'type'
:
Seat
.
VERIFIED
,
'price'
:
2
}
self
.
course
.
entitlements
.
create
(
mode
=
CourseEntitlement
.
PROFESSIONAL
,
price
=
1
)
num_courseruns_before
=
self
.
course
.
course_runs
.
count
()
response
=
self
.
client
.
post
(
self
.
create_course_run_url
,
post_data
)
num_courseruns_after
=
self
.
course
.
course_runs
.
count
()
self
.
assertEqual
(
num_courseruns_before
,
num_courseruns_after
)
self
.
assertContains
(
response
,
'The page could not be updated.'
,
status_code
=
400
)
class
CreateAdminImportCourseTest
(
SiteMixin
,
TestCase
):
""" Tests for the publisher `CreateAdminImportCourse`. """
...
...
course_discovery/apps/publisher/views.py
View file @
c5d0f4da
...
...
@@ -32,8 +32,9 @@ from course_discovery.apps.publisher.dataloader.create_courses import process_co
from
course_discovery.apps.publisher.emails
import
send_email_for_published_course_run_editing
from
course_discovery.apps.publisher.forms
import
(
AdminImportCourseForm
,
CourseEntitlementForm
,
CourseForm
,
CourseRunForm
,
CourseSearchForm
,
SeatForm
)
from
course_discovery.apps.publisher.models
import
(
PAID_SEATS
,
Course
,
CourseRun
,
CourseRunState
,
CourseState
,
CourseUserRole
,
OrganizationExtension
,
Seat
,
UserAttributes
)
from
course_discovery.apps.publisher.models
import
(
PAID_SEATS
,
Course
,
CourseEntitlement
,
CourseRun
,
CourseRunState
,
CourseState
,
CourseUserRole
,
OrganizationExtension
,
Seat
,
UserAttributes
)
from
course_discovery.apps.publisher.utils
import
(
get_internal_users
,
has_role_for_course
,
is_internal_user
,
is_project_coordinator_user
,
is_publisher_admin
,
make_bread_crumbs
)
from
course_discovery.apps.publisher.wrappers
import
CourseRunWrapper
...
...
@@ -609,18 +610,65 @@ class CreateCourseRunView(mixins.LoginRequiredMixin, mixins.PublisherUserRequire
run_initial_data
=
{
'pacing_type'
:
last_run
.
pacing_type
}
return
self
.
run_form
(
initial
=
run_initial_data
)
def
_entitlement_is_valid_for_seat_creation
(
self
,
entitlement
):
if
entitlement
is
None
:
return
False
# The SeatForm does not support custom currency values, and assumes everything is USD.
if
entitlement
.
currency
is
None
or
entitlement
.
currency
.
code
!=
'USD'
:
return
False
if
entitlement
.
mode
not
in
CourseEntitlement
.
MODE_TO_SEAT_TYPE_MAPPING
:
return
False
return
True
def
_render_post_error
(
self
,
request
,
ctx_overrides
=
None
,
status
=
400
):
context
=
self
.
get_context_data
()
if
ctx_overrides
:
context
.
update
(
ctx_overrides
)
return
render
(
request
,
self
.
template_name
,
context
,
status
=
status
)
def
_process_post_request
(
self
,
request
,
parent_course
,
run_form
,
seat_form
,
ctx_overrides
=
None
):
user
=
request
.
user
def
_process_post_request
(
self
,
request
,
parent_course
,
context
=
None
):
context
=
context
or
{}
run_form
=
self
.
run_form
(
request
.
POST
)
context
[
'run_form'
]
=
run_form
if
parent_course
.
uses_entitlements
:
context
[
'hide_seat_form'
]
=
True
# Fail if Seat fields are present in the POST data.
seat_data_in_form
=
any
([
key
for
key
in
self
.
seat_form
.
declared_fields
.
keys
()
if
key
in
request
.
POST
])
if
seat_data_in_form
:
messages
.
error
(
request
,
_
(
'The page could not be updated. Make sure that all values are correct, then try again.'
)
)
return
self
.
_render_post_error
(
request
,
ctx_overrides
=
context
)
try
:
entitlement
=
parent_course
.
entitlements
.
get
()
except
(
CourseEntitlement
.
DoesNotExist
,
CourseEntitlement
.
MultipleObjectsReturned
):
entitlement
=
None
if
not
self
.
_entitlement_is_valid_for_seat_creation
(
entitlement
):
messages
.
error
(
request
,
_
(
'The certificate configuration for this course is incorrect. Please fix it, then try again.'
)
)
return
self
.
_render_post_error
(
request
,
ctx_overrides
=
context
)
seat_form
=
self
.
seat_form
({
'type'
:
CourseEntitlement
.
MODE_TO_SEAT_TYPE_MAPPING
[
entitlement
.
mode
],
'price'
:
entitlement
.
price
})
else
:
seat_form
=
self
.
seat_form
(
request
.
POST
)
context
[
'seat_form'
]
=
seat_form
context
[
'hide_seat_form'
]
=
False
course_user_roles
=
parent_course
.
course_user_roles
.
filter
(
role__in
=
COURSE_ROLES
)
has_default_course_user_roles
=
course_user_roles
.
count
()
==
len
(
COURSE_ROLES
)
if
not
(
has_default_course_user_roles
or
waffle
.
switch_is_active
(
'disable_publisher_permissions'
)):
logger
.
error
(
'Course [
%
s] is missing default course roles. Current roles [
%
s], required roles [
%
s]'
,
...
...
@@ -635,16 +683,17 @@ class CreateCourseRunView(mixins.LoginRequiredMixin, mixins.PublisherUserRequire
'Please contact your partner manager to create default roles.'
)
)
return
self
.
_render_post_error
(
request
,
ctx_overrides
=
c
tx_overrides
)
return
self
.
_render_post_error
(
request
,
ctx_overrides
=
c
ontext
)
if
not
(
run_form
.
is_valid
()
and
seat_form
.
is_valid
()):
messages
.
error
(
request
,
_
(
'The page could not be updated. Make sure that all values are correct, then try again.'
)
)
return
self
.
_render_post_error
(
request
,
ctx_overrides
=
c
tx_overrides
)
return
self
.
_render_post_error
(
request
,
ctx_overrides
=
c
ontext
)
try
:
with
transaction
.
atomic
():
user
=
request
.
user
course_run
=
run_form
.
save
(
commit
=
False
,
course
=
parent_course
,
changed_by
=
user
)
self
.
_set_last_run_data
(
course_run
)
seat_form
.
save
(
course_run
=
course_run
,
changed_by
=
user
)
...
...
@@ -665,7 +714,7 @@ class CreateCourseRunView(mixins.LoginRequiredMixin, mixins.PublisherUserRequire
error_msg
=
self
.
_format_post_exception_message
(
ex
)
messages
.
error
(
request
,
error_msg
)
logger
.
exception
(
'Unable to create course run and seat for course [
%
s].'
,
parent_course
.
id
)
return
self
.
_render_post_error
(
request
,
ctx_overrides
=
c
tx_overrides
)
return
self
.
_render_post_error
(
request
,
ctx_overrides
=
c
ontext
)
def
get_context_data
(
self
,
**
kwargs
):
parent_course
=
self
.
_get_parent_course
()
...
...
@@ -676,18 +725,13 @@ class CreateCourseRunView(mixins.LoginRequiredMixin, mixins.PublisherUserRequire
context
=
{
'cancel_url'
:
reverse
(
'publisher:publisher_course_detail'
,
kwargs
=
{
'pk'
:
parent_course
.
pk
}),
'run_form'
:
run_form
,
'seat_form'
:
seat_form
'seat_form'
:
seat_form
,
'hide_seat_form'
:
parent_course
.
uses_entitlements
}
return
context
def
post
(
self
,
request
,
*
args
,
**
kwargs
):
parent_course
=
self
.
_get_parent_course
()
run_form
=
self
.
run_form
(
request
.
POST
)
seat_form
=
self
.
seat_form
(
request
.
POST
)
return
self
.
_process_post_request
(
request
,
parent_course
,
run_form
,
seat_form
,
ctx_overrides
=
{
'run_form'
:
run_form
,
'seat_form'
:
seat_form
})
return
self
.
_process_post_request
(
request
,
self
.
_get_parent_course
())
class
CreateRunFromDashboardView
(
CreateCourseRunView
):
...
...
@@ -700,33 +744,23 @@ class CreateRunFromDashboardView(CreateCourseRunView):
def
get_context_data
(
self
,
**
kwargs
):
context
=
{
'cancel_url'
:
reverse
(
'publisher:publisher_dashboard'
),
'course_form'
:
self
.
course_form
(),
'course_form'
:
self
.
course_form
(
queryset
=
Course
.
objects
.
none
()
),
'run_form'
:
self
.
run_form
(),
'seat_form'
:
self
.
seat_form
()
'seat_form'
:
self
.
seat_form
(),
'hide_seat_form'
:
False
}
return
context
def
post
(
self
,
request
,
*
args
,
**
kwargs
):
course_form
=
self
.
course_form
(
request
.
POST
)
run_form
=
self
.
run_form
(
request
.
POST
)
seat_form
=
self
.
seat_form
(
request
.
POST
)
ctx_overrides
=
{
'course_form'
:
course_form
,
'run_form'
:
run_form
,
'seat_form'
:
seat_form
,
}
if
not
course_form
.
is_valid
():
messages
.
error
(
request
,
_
(
'The page could not be updated. Make sure that all values are correct, then try again.'
)
)
return
self
.
_render_post_error
(
request
,
ctx_overrides
=
ctx_overrides
)
return
self
.
_render_post_error
(
request
,
ctx_overrides
=
{
'run_form'
:
self
.
run_form
(
request
.
POST
)}
)
self
.
parent_course
=
course_form
.
cleaned_data
.
get
(
'course'
)
return
self
.
_process_post_request
(
request
,
self
.
parent_course
,
run_form
,
seat_form
,
ctx_overrides
=
ctx_overrides
)
return
self
.
_process_post_request
(
request
,
self
.
parent_course
,
context
=
{
'course_form'
:
course_form
})
class
CourseRunEditView
(
mixins
.
LoginRequiredMixin
,
mixins
.
PublisherPermissionMixin
,
UpdateView
):
...
...
course_discovery/conf/locale/en/LC_MESSAGES/django.mo
View file @
c5d0f4da
No preview for this file type
course_discovery/conf/locale/en/LC_MESSAGES/django.po
View file @
c5d0f4da
...
...
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-02-
06 14:34
+0000\n"
"POT-Creation-Date: 2018-02-
12 17:50
+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
...
...
@@ -3326,6 +3326,12 @@ msgstr ""
#: apps/publisher/views.py
msgid ""
"The certificate configuration for this Course is incorrect. Please fix it, "
"then try again."
msgstr ""
#: apps/publisher/views.py
msgid ""
"Your organization does not have default roles to review/approve this course-"
"run. Please contact your partner manager to create default roles."
msgstr ""
...
...
course_discovery/conf/locale/en/LC_MESSAGES/djangojs.mo
View file @
c5d0f4da
No preview for this file type
course_discovery/conf/locale/en/LC_MESSAGES/djangojs.po
View file @
c5d0f4da
...
...
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-02-
06 14:34
+0000\n"
"POT-Creation-Date: 2018-02-
12 17:50
+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
...
...
course_discovery/conf/locale/eo/LC_MESSAGES/django.mo
View file @
c5d0f4da
No preview for this file type
course_discovery/conf/locale/eo/LC_MESSAGES/django.po
View file @
c5d0f4da
...
...
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-02-
06 14:34
+0000\n"
"POT-Creation-Date: 2018-02-
12 17:50
+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
...
...
@@ -4037,6 +4037,14 @@ msgstr ""
#: apps/publisher/views.py
msgid ""
"The certificate configuration for this Course is incorrect. Please fix it, "
"then try again."
msgstr ""
"Thé çértïfïçäté çönfïgürätïön för thïs Çöürsé ïs ïnçörréçt. Pléäsé fïx ït, "
"thén trý ägäïn. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#"
#: apps/publisher/views.py
msgid ""
"Your organization does not have default roles to review/approve this course-"
"run. Please contact your partner manager to create default roles."
msgstr ""
...
...
course_discovery/conf/locale/eo/LC_MESSAGES/djangojs.mo
View file @
c5d0f4da
No preview for this file type
course_discovery/conf/locale/eo/LC_MESSAGES/djangojs.po
View file @
c5d0f4da
...
...
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-02-
06 14:34
+0000\n"
"POT-Creation-Date: 2018-02-
12 17:50
+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
...
...
course_discovery/static/js/publisher/toggle-seat-form.js
0 → 100644
View file @
c5d0f4da
$
(
document
).
ready
(
function
()
{
var
$courseRunForm
=
$
(
'.js-courserun-form'
),
$seatForm
=
$
(
'.js-seat-form'
),
$courseSelect
=
$
(
'#id_course'
);
// If the rendered SeatForm is hidden, remove it from the DOM.
if
(
$seatForm
.
hasClass
(
'hidden'
))
{
$seatForm
.
detach
();
$seatForm
.
removeClass
(
'hidden'
);
}
if
(
$courseSelect
.
length
)
{
// See https://select2.org/programmatic-control/events for information about the select2:select event.
$courseSelect
.
on
(
'select2:select'
,
function
(
e
)
{
var
usesEntitlements
=
e
.
params
.
data
.
uses_entitlements
;
$seatForm
.
detach
();
if
(
!
usesEntitlements
)
{
// Remove any errors that may have been initially loaded with the form.
$seatForm
.
find
(
'.js-seat-form-errors'
).
remove
();
// Reset inputs before re-attaching the form.
$seatForm
.
find
(
'#id_type'
).
val
(
''
);
$seatForm
.
find
(
'#seatPriceBlock'
).
hide
();
$seatForm
.
find
(
'#id_price'
).
val
(
'0.0'
);
$seatForm
.
find
(
'#creditPrice'
).
hide
();
$seatForm
.
find
(
'#id_credit_price'
).
val
(
'0.0'
);
// Re-attach the form
$seatForm
.
insertAfter
(
$courseRunForm
);
}
});
}
});
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