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,7 +227,10 @@ 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.
return self._subsection_grade_factory.create(subsection, read_only=True)
if self.force_update_subsections:
return self._subsection_grade_factory.update(subsection)
else:
return self._subsection_grade_factory.create(subsection, read_only=True)
@staticmethod
def _compute_percent(grader_result):
......
......@@ -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):
......
......@@ -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-->
<input type="submit" name="verified_mode" value="Upgrade to a Certificate ($${min_price} USD)" />
<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