Commit 0356845c by Feanil Patel Committed by GitHub

Merge pull request #15650 from edx/rc-2017-07-24

Rc 2017 07 24
parents 3b523bee 3fd17d3d
......@@ -93,6 +93,7 @@ source, template_path = Loader(engine).load_template_source(path)
</%doc>
<%
from django.template import Template, Context
from webpack_loader.exceptions import WebpackLoaderBadStatsError
try:
return Template("""
{% load render_bundle from webpack_loader %}
......@@ -105,9 +106,9 @@ source, template_path = Loader(engine).load_template_source(path)
'entry': entry,
'body': capture(caller.body)
}))
except IOError as e:
except (IOError, WebpackLoaderBadStatsError) as e:
# Don't break Mako template rendering if the bundle or webpack-stats can't be found, but log it
logger.error(e)
logger.error('[LEARNER-1938] {error}'.format(error=e))
%>
</%def>
......
"""
Tests for the Bulk Enrollment views.
"""
import ddt
import json
from django.conf import settings
from django.contrib.auth.models import User
......@@ -25,6 +26,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
@override_settings(ENABLE_BULK_ENROLLMENT_VIEW=True)
@ddt.ddt
class BulkEnrollmentTest(ModuleStoreTestCase, LoginEnrollmentTestCase, APITestCase):
"""
Test the bulk enrollment endpoint
......@@ -67,9 +69,13 @@ class BulkEnrollmentTest(ModuleStoreTestCase, LoginEnrollmentTestCase, APITestCa
self.about_path = '/courses/{}/about'.format(self.course.id)
self.course_path = '/courses/{}/'.format(self.course.id)
def request_bulk_enroll(self, data=None, **extra):
def request_bulk_enroll(self, data=None, use_json=False, **extra):
""" Make an authenticated request to the bulk enrollment API. """
request = self.request_factory.post(self.url, data=data, **extra)
content_type = None
if use_json:
content_type = 'application/json'
data = json.dumps(data)
request = self.request_factory.post(self.url, data=data, content_type=content_type, **extra)
force_authenticate(request, user=self.staff)
response = self.view(request)
response.render()
......@@ -221,14 +227,15 @@ class BulkEnrollmentTest(ModuleStoreTestCase, LoginEnrollmentTestCase, APITestCa
res_json = json.loads(response.content)
self.assertEqual(res_json, expected)
def test_enroll_with_email(self):
@ddt.data(False, True)
def test_enroll_with_email(self, use_json):
""" Test enrolling using a username as the identifier. """
response = self.request_bulk_enroll({
'identifiers': self.notenrolled_student.email,
'action': 'enroll',
'email_students': False,
'courses': self.course_key,
})
}, use_json=use_json)
self.assertEqual(response.status_code, 200)
# test that the user is now enrolled
......@@ -274,10 +281,15 @@ class BulkEnrollmentTest(ModuleStoreTestCase, LoginEnrollmentTestCase, APITestCa
# Check the outbox
self.assertEqual(len(mail.outbox), 0)
def test_unenroll(self):
@ddt.data(False, True)
def test_unenroll(self, use_json):
""" Test unenrolling a user. """
response = self.request_bulk_enroll({'identifiers': self.enrolled_student.email, 'action': 'unenroll',
'email_students': False, 'courses': self.course_key, })
response = self.request_bulk_enroll({
'identifiers': self.enrolled_student.email,
'action': 'unenroll',
'email_students': False,
'courses': self.course_key,
}, use_json=use_json)
self.assertEqual(response.status_code, 200)
# test that the user is now unenrolled
......
......@@ -60,6 +60,12 @@ class BulkEnrollView(APIView):
def post(self, request):
serializer = BulkEnrollmentSerializer(data=request.data)
if serializer.is_valid():
# Setting the content type to be form data makes Django Rest Framework v3.6.3 treat all passed JSON data as
# POST parameters. This is necessary because this request is forwarded on to the student_update_enrollment
# view, which requires all of the parameters to be passed in via POST parameters.
metadata = request._request.META # pylint: disable=protected-access
metadata['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
response_dict = {
'auto_enroll': serializer.data.get('auto_enroll'),
'email_students': serializer.data.get('email_students'),
......
......@@ -21,7 +21,7 @@ class CourseGradeBase(object):
"""
Base class for Course Grades.
"""
def __init__(self, user, course_data, percent=0, letter_grade=None, passed=False):
def __init__(self, user, course_data, percent=0, letter_grade=None, passed=False, force_update_subsections=False):
self.user = user
self.course_data = course_data
......@@ -30,6 +30,7 @@ class CourseGradeBase(object):
# Convert empty strings to None when reading from the table
self.letter_grade = letter_grade or None
self.force_update_subsections = force_update_subsections
def __unicode__(self):
return u'Course Grade: percent: {}, letter_grade: {}, passed: {}'.format(
......@@ -203,7 +204,9 @@ class CourseGrade(CourseGradeBase):
def update(self):
"""
Updates the grade for the course.
Updates the grade for the course. Also updates subsection grades
if self.force_update_subsections is true, via the lazy call
to self.grader_result.
"""
grade_cutoffs = self.course_data.course.grade_cutoffs
self.percent = self._compute_percent(self.grader_result)
......@@ -224,6 +227,9 @@ class CourseGrade(CourseGradeBase):
def _get_subsection_grade(self, subsection):
# Pass read_only here so the subsection grades can be persisted in bulk at the end.
if self.force_update_subsections:
return self._subsection_grade_factory.update(subsection)
else:
return self._subsection_grade_factory.create(subsection, read_only=True)
@staticmethod
......
......@@ -66,7 +66,15 @@ class CourseGradeFactory(object):
else:
return None
def update(self, user, course=None, collected_block_structure=None, course_structure=None, course_key=None):
def update(
self,
user,
course=None,
collected_block_structure=None,
course_structure=None,
course_key=None,
force_update_subsections=False,
):
"""
Computes, updates, and returns the CourseGrade for the given
user in the course.
......@@ -75,7 +83,7 @@ class CourseGradeFactory(object):
or course_key should be provided.
"""
course_data = CourseData(user, course, collected_block_structure, course_structure, course_key)
return self._update(user, course_data, read_only=False)
return self._update(user, course_data, read_only=False, force_update_subsections=force_update_subsections)
@contextmanager
def _course_transaction(self, course_key):
......@@ -118,10 +126,17 @@ class CourseGradeFactory(object):
def _iter_grade_result(self, user, course_data, force_update):
try:
kwargs = {
'user': user,
'course': course_data.course,
'collected_block_structure': course_data.collected_structure,
'course_key': course_data.course_key
}
if force_update:
kwargs['force_update_subsections'] = True
method = CourseGradeFactory().update if force_update else CourseGradeFactory().create
course_grade = method(
user, course_data.course, course_data.collected_structure, course_key=course_data.course_key,
)
course_grade = method(**kwargs)
return self.GradeResult(user, course_grade, None)
except Exception as exc: # pylint: disable=broad-except
# Keep marching on even if this student couldn't be graded for
......@@ -165,14 +180,14 @@ class CourseGradeFactory(object):
return course_grade, persistent_grade.grading_policy_hash
@staticmethod
def _update(user, course_data, read_only):
def _update(user, course_data, read_only, force_update_subsections=False):
"""
Computes, saves, and returns a CourseGrade object for the
given user and course.
Sends a COURSE_GRADE_CHANGED signal to listeners and a
COURSE_GRADE_NOW_PASSED if learner has passed course.
"""
course_grade = CourseGrade(user, course_data)
course_grade = CourseGrade(user, course_data, force_update_subsections=force_update_subsections)
course_grade.update()
should_persist = (
......
......@@ -106,7 +106,7 @@ def compute_grades_for_course_v2(self, **kwargs):
@task(base=_BaseTask)
def compute_grades_for_course(course_key, offset, batch_size, **kwargs): # pylint: disable=unused-argument
"""
Compute grades for a set of students in the specified course.
Compute and save grades for a set of students in the specified course.
The set of students will be determined by the order of enrollment date, and
limited to at most <batch_size> students, starting from the specified
......
......@@ -246,6 +246,20 @@ class TestCourseGradeFactory(GradeTestBase):
else:
self.assertIsNone(course_grade)
@ddt.data(True, False)
def test_iter_force_update(self, force_update):
base_string = 'lms.djangoapps.grades.new.subsection_grade_factory.SubsectionGradeFactory.{}'
desired_method_name = base_string.format('update' if force_update else 'create')
undesired_method_name = base_string.format('create' if force_update else 'update')
with patch(desired_method_name) as desired_call:
with patch(undesired_method_name) as undesired_call:
set(CourseGradeFactory().iter(
users=[self.request.user], course=self.course, force_update=force_update
))
self.assertTrue(desired_call.called)
self.assertFalse(undesired_call.called)
@ddt.ddt
class TestSubsectionGradeFactory(ProblemSubmissionTestMixin, GradeTestBase):
......
......@@ -4,22 +4,26 @@
// Please list the ticket number of the experiment
// --------------------
// LEARNER-1312 Track Selection V2
/* This css was added as part of the LEARNER-1312 experiment */
// LEARNER-1726 Track Selection V3
/* This css was added as part of the LEARNER-1726 experiment */
.v2.register-choice {
margin: 0 2% 20px 0 !important
}
.v2.register-choice-certificate .list-actions {
text-align: left !important;
}
.v2.register-choice-donate .list-actions {
margin-bottom: 0 !important;
}
.v2.register-choice-donate .action-select {
display: inline-block !important;
list-style-type: none !important;
width: 100% !important;
}
.v2.register-choice-donate .donation-link {
display: inline-block !important;
padding: 10px 15px !important;
......@@ -30,219 +34,361 @@
text-align: center !important;
color: #D7548E !important;
float: left !important;
font-size: 15px;
font-weight: 500 !important;
}
@media (min-width: 375px) {
.donation-link {
font-size: 16px;
}
}
.v2.register-choice-v2-audit {
height: 250px !important;
height: 300px;
background: none !important;
border-top-color: grey !important;
border-top-width: 1px !important;
}
@media screen and (min-width: 375px) {
.v2.register-choice-v2-audit {
height: 250px;
}
}
.v2.register-choice-v2-audit .list-actions {
margin-bottom: 0 !important;
}
.v2.register-choice-v2-audit .list-actions input {
background: transparent !important;
color: #0075b4 !important;
box-shadow: none !important;
text-decoration: underline !important;
border: none !important;
white-space: normal;
}
.v2.register-choice-v2-audit .wrapper-copy-inline {
height: 70px !important;
width: 100% !important;
display: flex !important;
}
.v2.register-choice-v2-audit .wrapper-copy {
width: 70% !important;
height: auto !important;
}
.v2.page-header {
padding-bottom: 0;
padding: 0;
}
.v2 img {
margin-top: 20px;
margin-left: 5px;
}
.v2 .donation-link {
font-weight: bold !important;
}
@media (min-width: 320px) {
.v2.register-choice-certificate,
.v2.register-choice-donate,
.v2.register-choice-view {
.v2.register-choice-certificate,
.v2.register-choice-donate,
.v2.register-choice-view {
width: 100%;
}
.v2 .wrapper-copy-inline {
}
.v2.register-choice-donate {
border-color: #D7548E !important;
}
.v2 .wrapper-copy-inline {
max-height: 115px;
}
.v2.register-choice-v2-audit .wrapper-copy-inline {
}
.v2.register-choice-v2-audit .wrapper-copy-inline {
display: block !important;
}
.v2.register-choice-v2-audit .copy-inline {
}
.v2.register-choice-v2-audit .copy-inline {
width: 100% !important;
}
.v2.register-choice-v2-audit .list-actions {
}
.v2.register-choice-v2-audit .list-actions {
width: 100% !important;
margin-top: 20px !important;
text-align: center !important;
}
.v2 .wrapper-copy-inline .wrapper-copy {
width: 100% !important;
}
.v2 .donation-link, .v2 input {
}
.v2 .wrapper-copy-inline .wrapper-copy {
width: 100% !important;
}
.v2 input{
font-size: 15px !important;
}
.v2 button {
background-color: rgb(0, 103, 0);
border-color: rgb(0, 103, 0);
border-radius: 2px;
box-shadow: rgb(0, 77, 0) 0px 2px 1px 0px;
cursor: pointer;
font-family: "Open Sans";
height: auto;
margin-right: 4px;
margin-top: 0px;
padding: 10px 15px;
width: initial;
background-image: none !important;
font-size: 14px !important;
font-weight: 500 !important;
&:hover, &:focus {
background-color: #009b00 !important;
border-color: #009b00;
box-shadow: #004d00 0px 2px 1px 0px;
}
.v2 img {
}
.savings-message {
margin-top: 10px;
font-size: 11px;
}
@media screen and (min-width: 375px) {
.savings-message {
font-size: 13px;
margin-left: 16px;
}
}
.v2 .donation-link, .v2 input, .v2 button {
width: 100%;
}
.v2 img {
display: none;
}
.v2 .deco-divider {
display: none;
}
.v2 .visual-reference {
width: 38%;
}
@media (min-width: 420px) {
.v2 button {
height: 45px;
font-size: 16px !important;
}
}
@media (min-width: 768px) {
.v2.register-choice-certificate,
.v2.register-choice-donate {
width: 48% !important;
width: 46.5% !important;
display: inline-block;
min-height: 250px;
min-height: 270px;
}
.v2.register-choice-v2-audit .wrapper-copy-inline {
display: flex !important;
}
.v2.register-choice-v2-audit .copy-inline {
width: 40% !important;
}
.v2.register-choice-v2-audit .list-actions {
margin-top: 0 !important;
text-align: right !important;
}
.v2 .wrapper-copy-inline .wrapper-copy {
width: 58% !important;
width: 100% !important;
}
.v2 .donation-link, .v2 input {
.v2 input {
font-size: 15px !important;
width: 55% !important;
}
.v2 .donation-link, .v2.register-choice-certificate button {
margin-top: 20px;
width: initial;
}
.v2.register-choice-v2-audit input {
width: 100% !important;
}
.v2.register-choice-view {
height: 250px;
}
.v2 img {
display: initial;
}
.v2.register-choice {
margin: 0 2% 20px 0;
}
.v2.deco-divider {
width: 3% !important;
box-sizing: border-box;
float: left;
display: inline-block;
height: 400px;
margin: 0px 0 40px 0 !important;
border-left: 4px solid #f5f5f5 !important;
border-top: none !important;
}
}
@media (min-width: 320px) {
.v2 .visual-reference {
width: 38%;
}
}
@media (min-width: 768px) {
@media (min-width: 320px) {
.v2.register-choice-donate .wrapper-copy-inline .wrapper-copy, .v2.register-choice-certificate .wrapper-copy-inline .wrapper-copy {
width: 60%;
}
}
@media (min-width: 768px) {
.v2.register-choice-donate .wrapper-copy-inline .wrapper-copy, .v2.register-choice-certificate .wrapper-copy-inline .wrapper-copy {
width: 60%;
}
}
@media (min-width: 320px) {
.v2.register-choice-view .wrapper-copy-inline .wrapper-copy {
width: 100%;
}
}
@media (min-width: 320px) {
.v2.register-choice {
padding: 15px !important;
}
.v2.register-choice-donate .wrapper-copy-inline .wrapper-copy, .v2.register-choice-certificate .wrapper-copy-inline .wrapper-copy {
width: 60%;
}
@media (min-width: 768px) {
.v2.register-choice {
padding: 20px !important;
}
.v2.register-choice.register-choice-view {
margin-right: 0;
}
}
@media screen and (min-width: 769px) {
.v2.register-choice .list-actions:last-child {
float: left;
width: 100%;
margin-top: 0px;
}
}
@media screen and (min-width: 769px) {
.v2.register-choice .action-select {
width: 100% !important;
}
}
.v2 .donation-link:hover,
.v2 .donation-link:focus {
background-color: #D7548E !important;
color: white !important;
text-decoration: none;
}
.v2 .donation-link:hover {
cursor: pointer;
}
.v2 .copy li {
margin-bottom: 5px;
}
.v2.register-choice .copy-inline {
width: 100%;
}
.v2.register-choice-donate {
border-color: #D7548E !important;
}
.v2 .register-choice-view {
border-color: #2991c3 !important;
}
.v2 .visual-reference {
vertical-align: top;
}
.v2 .wrapper-copy-inline .wrapper-copy ul {
margin-top: 0px;
padding-left: 30px;
}
.v2 .img-certificate {
border: 2px solid #009b00 !important;
float: right;
height: 120px;
width: auto;
margin-top: 0 !important;
display: none;
}
.v2 .img-donate {
margin-top: 0;
float: right;
border: 2px solid #D7548E !important;
display: none;
}
.v2 .img-view {
border: 2px solid #2991c3 !important;
}
.v2.register-choice .title {
width: 100%;
margin-bottom: 20px;
}
.v2.register-choice.register-choice-view .action-select {
border: 1px solid transparent !important;
border-radius: 3px;
}
.v2.register-choice.register-choice-view .action-select input {
.v2.register-choice.register-choice-view .action-select button {
border: 1px solid transparent !important;
}
.v2.register-choice.register-choice-view .action-select:hover {
border: 1px solid #0075b4 !important;
}
.v2.deco-divider {
display: none !important;
width: 3% !important;
box-sizing: border-box;
float: left;
display: inline-block;
height: 250px;
margin: 0px 0 40px 0 !important;
border-left: 4px solid #f5f5f5 !important; border-top:none !important;
.copy {
position: absolute;
top: 110px !important;
left: calc(50% - 40px) !important;
margin-left: 20px;
background: white;
text-align: center;
color: #474747;
width: 10px;
padding: 0 !important;
}
}
}
@media (min-width: 835px) {
.v2.register-choice-certificate,
.v2.register-choice-donate {
min-height: 250px;
}
}
@media (min-width: 1024px) {
.v2 .donation-link {
width: 55%;
}
.v2.deco-divider .copy {
margin-left: 15px;
}
}
@media (min-width: 1064px) {
.v2.register-choice-certificate,
.v2.register-choice-donate {
min-height: 260px;
}
.v2 .img-certificate, .v2 .img-donate {
display: initial;
}
.v2 .donation-link, .v2.register-choice-certificate button {
margin-top: -22px !important;
}
}
\ No newline at end of file
......@@ -50,6 +50,7 @@ class Command(BaseCommand):
site_config = getattr(site, 'configuration', None)
if site_config is None or not site_config.get_value('COURSE_CATALOG_API_URL'):
logger.info('Skipping site {domain}. No configuration.'.format(domain=site.domain))
cache.set(SITE_PROGRAM_UUIDS_CACHE_KEY_TPL.format(domain=site.domain), [], None)
continue
client = create_catalog_api_client(user, site=site)
......
......@@ -30,7 +30,7 @@ git+https://github.com/open-craft/problem-builder.git@v2.6.5#egg=xblock-problem-
-e git+https://github.com/edx/AnimationXBlock.git@d2b551bb8f49a138088e10298576102164145b87#egg=animation-xblock
# Peer instruction XBlock
ubcpi-xblock==0.6.2
ubcpi-xblock==0.6.3
# Vector Drawing and ActiveTable XBlocks (Davidson)
-e git+https://github.com/open-craft/xblock-vectordraw.git@v0.2.1#egg=xblock-vectordraw==0.2.1
......
......@@ -75,7 +75,7 @@ from openedx.core.djangolib.markup import HTML, Text
<h3 class="title v1">
${title_content}
</h3>
<!-- This div was added as part of the LEARNER-1312 experiment. The v2 class should be removed if the experiment is implemented-->
<!-- This div was added as part of the LEARNER-1726 experiment. The v2 class should be removed if the experiment is implemented-->
<h3 class="title v2 hidden">
Next, Select Your Learning Path
</h3>
......@@ -86,7 +86,7 @@ from openedx.core.djangolib.markup import HTML, Text
b_tag_kwargs = {'b_start': HTML('<b>'), 'b_end': HTML('</b>')}
%>
% if "verified" in modes:
<!-- This div was added as part of the LEARNER-1312 experiment. The v2 class should be removed if the experiment is implemented-->
<!-- This div was added as part of the LEARNER-1726 experiment. The v2 class should be removed if the experiment is implemented-->
<div class="register-choice register-choice-certificate v2 hidden">
<h4 class="title">Pursue a Verified Certificate</h4>
<div class="wrapper-copy-inline">
......@@ -102,7 +102,14 @@ from openedx.core.djangolib.markup import HTML, Text
<li class="action action-select">
<input type="hidden" name="contribution" value="${min_price}" />
<!-- The class verified_mode should be added to this selector if the experiment is implemented-->
<div class="upgradev1">
<input type="submit" name="verified_mode" value="Upgrade to a Certificate ($${min_price} USD)" />
</div>
<div class="upgradev2 hidden">
<button type="submit" name="verified_mode">Upgrade to a Certificate (<del>$${min_price} USD</del>)</button>
<br>
<div class="savings-message">Save 5% if you upgrade now! ($${int(min_price * .95)} USD)</div>
</div>
</li>
</ul>
</div>
......@@ -192,41 +199,41 @@ from openedx.core.djangolib.markup import HTML, Text
<span class="deco-divider v1">
<span class="copy">${_("or")}</span>
</span>
<!-- This div was added as part of the LEARNER-1312 experiment. The v2 class should be removed if the experiment is implemented-->
<!-- This div was added as part of the LEARNER-1726 experiment. The v2 class should be removed if the experiment is implemented-->
<span class="deco-divider v2 hidden">
<span class="copy">${_("or")}</span>
</span>
<!-- This div was added as part of the LEARNER-1312 experiment. The v2 class should be removed if the experiment is implemented-->
<!-- This div was added as part of the LEARNER-1726 experiment. The v2 class should be removed if the experiment is implemented-->
<div class="register-choice register-choice-donate v2 hidden">
<h4 class="title">
Donate to Support our Non-Profit Mission
I Don't Want to Upgrade or Donate Today
</h4>
<div class="wrapper-copy-inline">
<div class="wrapper-copy">
Any amount will support our mission to make the world's best education more accessible.
If you do not want to add a certificate or donate to edX's mission today, you can skip this step for now and continue to the course.
</div>
<img src="/static/images/edx-home-graphic.png" class="visual-reference img-donate" alt="Visual of two hands forming a heart shape" >
</div>
<div class="copy-inline">
<ul class="list-actions">
<li class="action action-select">
<a class="donation-link" href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&amp;hosted_button_id=MJUBGL54QTCTA">Donate and Continue to Course</a>
<a class="donation-link" href="/dashboard">Continue to Course</a>
</li>
</ul>
</div>
</div>
<!-- This div was added as part of the LEARNER-1312 experiment. The v2 class should be removed if the experiment is implemented-->
<!-- This div was added as part of the LEARNER-1726 experiment. The v2 class should be removed if the experiment is implemented-->
<div class="register-choice register-choice-v2-audit register-choice-view v2 hidden">
<h4 class="title">I Don't Want to Upgrade or Donate Today</h4>
<h4 class="title">Donate to Support our Non-Profit Mission</h4>
<div class="wrapper-copy-inline">
<div class="wrapper-copy">
If you do not want to buy a certificate or donate to edX's mission today, you can skip this step for now and continue to the course.
Even if you are not interested in pursuing a Verified Certificate, a donation helps edX continue to work towards its non-profit mission of making the world's best education more accessible to learners everywhere.
</div>
<div class="copy-inline">
<ul class="list-actions">
<input type="submit" name="audit_mode" value="Continue to Course">
<input type="submit" name="audit_mode" action="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=AG9VK2LC29L5Y" value="Donate and Continue to Course">
</ul>
</div>
</div>
......
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