-

${_("Welcome, {0}!".format(user.username))}

+

${_("Welcome, {0}!").format(user.username)}

%if len(courses) > 0:
diff --git a/lms/djangoapps/instructor/views/legacy.py b/lms/djangoapps/instructor/views/legacy.py index 0788b05cc8b7..5434e5f5efd2 100644 --- a/lms/djangoapps/instructor/views/legacy.py +++ b/lms/djangoapps/instructor/views/legacy.py @@ -172,10 +172,12 @@ def get_student_from_identifier(unique_student_identifier): student = User.objects.get(email=unique_student_identifier) else: student = User.objects.get(username=unique_student_identifier) - msg += "Found a single student. " + msg += _u("Found a single student. ") except User.DoesNotExist: student = None - msg += "Couldn't find student with that email or username. " + msg += "{text}".format( + text=_u("Couldn't find student with that email or username.") + ) return msg, student # process actions from form POST @@ -213,20 +215,20 @@ def get_student_from_identifier(unique_student_identifier): if action == 'Dump list of enrolled students' or action == 'List enrolled students': log.debug(action) datatable = get_student_grade_summary_data(request, course, course_id, get_grades=False, use_offline=use_offline) - datatable['title'] = 'List of students enrolled in {0}'.format(course_id) + datatable['title'] = _u('List of students enrolled in {0}').format(course_id) track.views.server_track(request, "list-students", {}, page="idashboard") elif 'Dump Grades' in action: log.debug(action) datatable = get_student_grade_summary_data(request, course, course_id, get_grades=True, use_offline=use_offline) - datatable['title'] = 'Summary Grades of students enrolled in {0}'.format(course_id) + datatable['title'] = _u('Summary Grades of students enrolled in {0}').format(course_id) track.views.server_track(request, "dump-grades", {}, page="idashboard") elif 'Dump all RAW grades' in action: log.debug(action) datatable = get_student_grade_summary_data(request, course, course_id, get_grades=True, get_raw_scores=True, use_offline=use_offline) - datatable['title'] = 'Raw Grades of students enrolled in {0}'.format(course_id) + datatable['title'] = _u('Raw Grades of students enrolled in {0}').format(course_id) track.views.server_track(request, "dump-grades-raw", {}, page="idashboard") elif 'Download CSV of all student grades' in action: @@ -254,14 +256,22 @@ def get_student_from_identifier(unique_student_identifier): try: instructor_task = submit_rescore_problem_for_all_students(request, course_id, problem_url) if instructor_task is None: - msg += 'Failed to create a background task for rescoring "{0}".'.format(problem_url) + msg += '{text}'.format( + text=_u('Failed to create a background task for rescoring "{0}".').format(problem_url) + ) else: track.views.server_track(request, "rescore-all-submissions", {"problem": problem_url, "course": course_id}, page="idashboard") except ItemNotFoundError as err: - msg += 'Failed to create a background task for rescoring "{0}": problem not found.'.format(problem_url) + msg += '{text}'.format( + text=_u('Failed to create a background task for rescoring "{0}": problem not found.').format(problem_url) + ) except Exception as err: log.error("Encountered exception from rescore: {0}".format(err)) - msg += 'Failed to create a background task for rescoring "{0}": {1}.'.format(problem_url, err.message) + msg += '{text}'.format( + text=_u('Failed to create a background task for rescoring "{url}": {message}.').format( + url=problem_url, message=err.message + ) + ) elif "Reset ALL students' attempts" in action: problem_urlname = request.POST.get('problem_for_all_students', '') @@ -269,15 +279,23 @@ def get_student_from_identifier(unique_student_identifier): try: instructor_task = submit_reset_problem_attempts_for_all_students(request, course_id, problem_url) if instructor_task is None: - msg += 'Failed to create a background task for resetting "{0}".'.format(problem_url) + msg += '{text}'.format( + text=_u('Failed to create a background task for resetting "{0}".').format(problem_url) + ) else: track.views.server_track(request, "reset-all-attempts", {"problem": problem_url, "course": course_id}, page="idashboard") except ItemNotFoundError as err: log.error('Failure to reset: unknown problem "{0}"'.format(err)) - msg += 'Failed to create a background task for resetting "{0}": problem not found.'.format(problem_url) + msg += '{text}'.format( + text=_u('Failed to create a background task for resetting "{0}": problem not found.').format(problem_url) + ) except Exception as err: log.error("Encountered exception from reset: {0}".format(err)) - msg += 'Failed to create a background task for resetting "{0}": {1}.'.format(problem_url, err.message) + msg += '{text}'.format( + text=_u('Failed to create a background task for resetting "{url}": {message}.').format( + url=problem_url, message=err.message + ) + ) elif "Show Background Task History for Student" in action: # put this before the non-student case, since the use of "in" will cause this to be missed @@ -318,12 +336,10 @@ def get_student_from_identifier(unique_student_identifier): course_id=course_id, module_state_key=module_state_key ) - msg += "Found module. " + msg += _u("Found module. ") except StudentModule.DoesNotExist as err: - error_msg = "Couldn't find module with that urlname: {0}. ".format( - problem_urlname - ) - msg += "" + error_msg + "({0}) ".format(err) + "" + error_msg = _u("Couldn't find module with that urlname: {url}. ").format(url=problem_urlname) + msg += "{err_msg} ({err})".format(err_msg=error_msg, err=err) log.debug(error_msg) if student_module is not None: @@ -331,7 +347,9 @@ def get_student_from_identifier(unique_student_identifier): # delete the state try: student_module.delete() - msg += "Deleted student module state for {0}!".format(module_state_key) + msg += "{text}".format( + text=_u("Deleted student module state for {state}!").format(state=module_state_key) + ) event = { "problem": module_state_key, "student": unique_student_identifier, @@ -344,10 +362,10 @@ def get_student_from_identifier(unique_student_identifier): page="idashboard" ) except Exception as err: - error_msg = "Failed to delete module state for {0}/{1}. ".format( - unique_student_identifier, problem_urlname + error_msg = _u("Failed to delete module state for {id}/{url}. ").format( + id=unique_student_identifier, url=problem_urlname ) - msg += "" + error_msg + "({0}) ".format(err) + "" + msg += "{err_msg} ({err})".format(err_msg=error_msg, err=err) log.exception(error_msg) elif "Reset student's attempts" in action: # modify the problem's state @@ -367,24 +385,32 @@ def get_student_from_identifier(unique_student_identifier): "course": course_id } track.views.server_track(request, "reset-student-attempts", event, page="idashboard") - msg += "Module state successfully reset!" + msg += "{text}".format( + text=_u("Module state successfully reset!") + ) except Exception as err: - error_msg = "Couldn't reset module state for {0}/{1}. ".format( - unique_student_identifier, problem_urlname + error_msg = _u("Couldn't reset module state for {id}/{url}. ").format( + id=unique_student_identifier, url=problem_urlname ) - msg += "" + error_msg + "({0}) ".format(err) + "" + msg += "{err_msg} ({err})".format(err_msg=error_msg, err=err) log.exception(error_msg) else: # "Rescore student's problem submission" case try: instructor_task = submit_rescore_problem_for_student(request, course_id, module_state_key, student) if instructor_task is None: - msg += 'Failed to create a background task for rescoring "{0}" for student {1}.'.format(module_state_key, unique_student_identifier) + msg += '{text}'.format( + text=_u('Failed to create a background task for rescoring "{key}" for student {id}.').format( + key=module_state_key, id=unique_student_identifier + ) + ) else: track.views.server_track(request, "rescore-student-submission", {"problem": module_state_key, "student": unique_student_identifier, "course": course_id}, page="idashboard") except Exception as err: - msg += 'Failed to create a background task for rescoring "{0}": {1}.'.format( - module_state_key, err.message + msg += '{text}'.format( + text=_u('Failed to create a background task for rescoring "{key}": {id}.').format( + key=module_state_key, id=err.message + ) ) log.exception("Encountered exception from rescore: student '{0}' problem '{1}'".format( unique_student_identifier, module_state_key @@ -399,7 +425,12 @@ def get_student_from_identifier(unique_student_identifier): if student is not None: progress_url = reverse('student_progress', kwargs={'course_id': course_id, 'student_id': student.id}) track.views.server_track(request, "get-student-progress-page", {"student": unicode(student), "instructor": unicode(request.user), "course": course_id}, page="idashboard") - msg += " Progress page for username: {1} with email address: {2}.".format(progress_url, student.username, student.email) + msg += "{text}.".format( + url=progress_url, + text=_u("Progress page for username: {username} with email address: {email}").format( + username=student.username, email=student.email + ) + ) #---------------------------------------- # export grades to remote gradebook @@ -413,7 +444,7 @@ def get_student_from_identifier(unique_student_identifier): allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True, use_offline=use_offline) assignments = [[x] for x in allgrades['assignments']] - datatable = {'header': ['Assignment Name']} + datatable = {'header': [_u('Assignment Name')]} datatable['data'] = assignments datatable['title'] = action @@ -437,27 +468,31 @@ def domatch(x): datatable = {} aname = request.POST.get('assignment_name', '') if not aname: - msg += "Please enter an assignment name" + msg += "{text}".format(text=_u("Please enter an assignment name")) else: allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True, use_offline=use_offline) if aname not in allgrades['assignments']: - msg += "Invalid assignment name '%s'" % aname + msg += "{text}".format( + text=_u("Invalid assignment name '{name}'").format(name=aname) + ) else: aidx = allgrades['assignments'].index(aname) - datatable = {'header': ['External email', aname]} + datatable = {'header': [_u('External email'), aname]} ddata = [] for x in allgrades['students']: # do one by one in case there is a student who has only partial grades try: ddata.append([x.email, x.grades[aidx]]) except IndexError: - log.debug('No grade for assignment %s (%s) for student %s', aidx, aname, x.email) + log.debug('No grade for assignment {idx} ({name}) for student {email}'.format( + idx=aidx, name=aname, email=x.email) + ) datatable['data'] = ddata - datatable['title'] = 'Grades for assignment "%s"' % aname + datatable['title'] = _u('Grades for assignment "{name}"').format(name=aname) if 'Export CSV' in action: # generate and return CSV file - return return_csv('grades %s.csv' % aname, datatable) + return return_csv('grades {name}.csv'.format(name=aname), datatable) elif 'remote gradebook' in action: file_pointer = StringIO() @@ -472,12 +507,12 @@ def domatch(x): elif 'List course staff' in action: role = CourseStaffRole(course.location) - datatable = _role_members_table(role, "List of Staff", course_id) + datatable = _role_members_table(role, _("List of Staff"), course_id) track.views.server_track(request, "list-staff", {}, page="idashboard") elif 'List course instructors' in action and GlobalStaff().has_user(request.user): role = CourseInstructorRole(course.location) - datatable = _role_members_table(role, "List of Instructors", course_id) + datatable = _role_members_table(role, _("List of Instructors"), course_id) track.views.server_track(request, "list-instructors", {}, page="idashboard") elif action == 'Add course staff': @@ -517,8 +552,8 @@ def getdat(u): return [u.username, u.email] + [getattr(p, x, '') for x in profkeys] datatable['data'] = [getdat(u) for u in enrolled_students] - datatable['title'] = 'Student profile data for course %s' % course_id - return return_csv('profiledata_%s.csv' % course_id, datatable) + datatable['title'] = _u('Student profile data for course {couse_id}').format(course_id = course_id) + return return_csv('profiledata_{course_id}.csv'.format(course_id = course_id), datatable) elif 'Download CSV of all responses to problem' in action: problem_to_dump = request.POST.get('problem_to_dump', '') @@ -531,17 +566,19 @@ def getdat(u): smdat = StudentModule.objects.filter(course_id=course_id, module_state_key=module_state_key) smdat = smdat.order_by('student') - msg += "Found %d records to dump " % len(smdat) + msg += _u("Found {num} records to dump.").format(num=smdat) except Exception as err: - msg += "Couldn't find module with that urlname. " - msg += "
%s
" % escape(err) + msg += "{text}
{err}
".format( + text=_u("Couldn't find module with that urlname."), + err=escape(err) + ) smdat = [] if smdat: datatable = {'header': ['username', 'state']} datatable['data'] = [[x.student.username, x.state] for x in smdat] - datatable['title'] = 'Student state for problem %s' % problem_to_dump - return return_csv('student_state_from_%s.csv' % problem_to_dump, datatable) + datatable['title'] = _u('Student state for problem {problem}').format(problem = problem_to_dump) + return return_csv('student_state_from_{problem}.csv'.format(problem = problem_to_dump), datatable) elif 'Download CSV of all student anonymized IDs' in action: students = User.objects.filter( @@ -557,7 +594,7 @@ def getdat(u): elif 'List beta testers' in action: role = CourseBetaTesterRole(course.location) - datatable = _role_members_table(role, "List of Beta Testers", course_id) + datatable = _role_members_table(role, _("List of Beta Testers"), course_id) track.views.server_track(request, "list-beta-testers", {}, page="idashboard") elif action == 'Add beta testers': @@ -696,9 +733,15 @@ def getdat(u): else: # If sending the task succeeds, deliver a success message to the user. if email_to_option == "all": - email_msg = '

Your email was successfully queued for sending. Please note that for large classes, it may take up to an hour (or more, if other courses are simultaneously sending email) to send all emails.

' + text = _u( + "Your email was successfully queued for sending. " + "Please note that for large classes, it may take up to an hour " + "(or more, if other courses are simultaneously sending email) " + "to send all emails." + ) else: - email_msg = '

Your email was successfully queued for sending.

' + text = _u('Your email was successfully queued for sending.') + email_msg = '

{text}

'.format(text=text) elif "Show Background Email Task History" in action: message, datatable = get_background_task_table(course_id, task_type='bulk_course_email') @@ -765,7 +808,11 @@ def get_analytics_result(analytics_name): # offline grades? if use_offline: - msg += "
Grades from %s" % offline_grades_available(course_id) + msg += "
{text}".format( + text=_u("Grades from {course_id}").format( + course_id=offline_grades_available(course_id) + ) + ) # generate list of pending background tasks if settings.FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'): @@ -855,17 +902,17 @@ def _do_remote_gradebook(user, course, action, args=None, files=None): ''' rg = course.remote_gradebook if not rg: - msg = "No remote gradebook defined in course metadata" + msg = _u("No remote gradebook defined in course metadata") return msg, {} rgurl = settings.FEATURES.get('REMOTE_GRADEBOOK_URL', '') if not rgurl: - msg = "No remote gradebook url defined in settings.FEATURES" + msg = _u("No remote gradebook url defined in settings.FEATURES") return msg, {} rgname = rg.get('name', '') if not rgname: - msg = "No gradebook name defined in course remote_gradebook metadata" + msg = _u("No gradebook name defined in course remote_gradebook metadata") return msg, {} if args is None: @@ -877,19 +924,19 @@ def _do_remote_gradebook(user, course, action, args=None, files=None): resp = requests.post(rgurl, data=data, verify=False, files=files) retdict = json.loads(resp.content) except Exception as err: - msg = "Failed to communicate with gradebook server at %s
" % rgurl - msg += "Error: %s" % err - msg += "
resp=%s" % resp.content - msg += "
data=%s" % data + msg = _u("Failed to communicate with gradebook server at {url}").format(url = rgurl) + "
" + msg += _u("Error: {err}").format(err = err) + msg += "
resp={resp}".format(resp = resp.content) + msg += "
data={data}".format(data = data) return msg, {} - msg = '
%s
' % retdict['msg'].replace('\n', '
') + msg = '
{msg}
'.format(msg = retdict['msg'].replace('\n', '
')) retdata = retdict['data'] # a list of dicts if retdata: datatable = {'header': retdata[0].keys()} datatable['data'] = [x.values() for x in retdata] - datatable['title'] = 'Remote gradebook response for %s' % action + datatable['title'] = _u('Remote gradebook response for {action}').format(action = action) datatable['retdata'] = retdata else: datatable = {} @@ -908,13 +955,13 @@ def _list_course_forum_members(course_id, rolename, datatable): Returns message status string to append to displayed message, if role is unknown. """ # make sure datatable is set up properly for display first, before checking for errors - datatable['header'] = ['Username', 'Full name', 'Roles'] - datatable['title'] = 'List of Forum {0}s in course {1}'.format(rolename, course_id) + datatable['header'] = [_u('Username'), _u('Full name'), _u('Roles')] + datatable['title'] = _u('List of Forum {name}s in course {id}').format(name = rolename, id = course_id) datatable['data'] = [] try: role = Role.objects.get(name=rolename, course_id=course_id) except Role.DoesNotExist: - return 'Error: unknown rolename "{0}"'.format(rolename) + return '' + _u('Error: unknown rolename "{0}"').format(rolename) + '' uset = role.users.all().order_by('username') msg = 'Role = {0}'.format(rolename) log.debug('role={0}'.format(rolename)) @@ -938,11 +985,11 @@ def _update_forum_role_membership(uname, course, rolename, add_or_remove): try: user = User.objects.get(username=uname) except User.DoesNotExist: - return 'Error: unknown username "{0}"'.format(uname) + return '' + _u('Error: unknown username "{0}"').format(uname) + '' try: role = Role.objects.get(name=rolename, course_id=course.id) except Role.DoesNotExist: - return 'Error: unknown rolename "{0}"'.format(rolename) + return '' + _u('Error: unknown rolename "{0}"').format(rolename) + '' # check whether role already has the specified user: alreadyexists = role.users.filter(username=uname).exists() @@ -950,19 +997,19 @@ def _update_forum_role_membership(uname, course, rolename, add_or_remove): log.debug('rolename={0}'.format(rolename)) if add_or_remove == FORUM_ROLE_REMOVE: if not alreadyexists: - msg = 'Error: user "{0}" does not have rolename "{1}", cannot remove'.format(uname, rolename) + msg = '' + _u('Error: user "{0}" does not have rolename "{1}", cannot remove').format(uname, rolename) + '' else: user.roles.remove(role) - msg = 'Removed "{0}" from "{1}" forum role = "{2}"'.format(user, course.id, rolename) + msg = '' + _u('Removed "{0}" from "{1}" forum role = "{2}"').format(user, course.id, rolename) + '' else: if alreadyexists: - msg = 'Error: user "{0}" already has rolename "{1}", cannot add'.format(uname, rolename) + msg = '' + _u('Error: user "{0}" already has rolename "{1}", cannot add').format(uname, rolename) + '' else: if (rolename == FORUM_ROLE_ADMINISTRATOR and not has_access(user, course, 'staff')): - msg = 'Error: user "{0}" should first be added as staff before adding as a forum administrator, cannot add'.format(uname) + msg = '' + _u('Error: user "{0}" should first be added as staff before adding as a forum administrator, cannot add').format(uname) + '' else: user.roles.add(role) - msg = 'Added "{0}" to "{1}" forum role = "{2}"'.format(user, course.id, rolename) + msg = '' + _u('Added "{0}" to "{1}" forum role = "{2}"').format(user, course.id, rolename) + '' return msg @@ -982,9 +1029,9 @@ def _role_members_table(role, title, course_id): 'title': "{title} in course {course}" """ uset = role.users_with_role() - datatable = {'header': ['Username', 'Full name']} + datatable = {'header': [_u('Username'), _u('Full name')]} datatable['data'] = [[x.username, x.profile.name] for x in uset] - datatable['title'] = '{0} in course {1}'.format(title, course_id) + datatable['title'] = _u('{0} in course {1}').format(title, course_id) return datatable @@ -1107,7 +1154,7 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, courseenrollment__is_active=1, ).prefetch_related("groups").order_by('username') - header = ['ID', 'Username', 'Full Name', 'edX email', 'External email'] + header = [_u('ID'), _u('Username'), _u('Full Name'), _u('edX email'), _u('External email')] assignments = [] if get_grades and enrolled_students.count() > 0: # just to construct the header @@ -1292,7 +1339,7 @@ def _do_enroll_students(course, course_id, students, overload=False, auto_enroll datatable = {'header': ['StudentEmail', 'action']} datatable['data'] = [[x, status[x]] for x in sorted(status)] - datatable['title'] = 'Enrollment of students' + datatable['title'] = _u('Enrollment of students') def sf(stat): return [x for x in status if status[x] == stat] @@ -1365,7 +1412,7 @@ def _do_unenroll_students(course_id, students, email_students=False): datatable = {'header': ['StudentEmail', 'action']} datatable['data'] = [[x, status[x]] for x in sorted(status)] - datatable['title'] = 'Un-enrollment of students' + datatable['title'] = _u('Un-enrollment of students') data = dict(datatable=datatable) return data @@ -1548,10 +1595,10 @@ def get_background_task_table(course_id, problem_url=None, student=None, task_ty if problem_url is None: msg += 'Failed to find any background tasks for course "{course}".'.format(course=course_id) elif student is not None: - template = 'Failed to find any background tasks for course "{course}", module "{problem}" and student "{student}".' + template = '' + _u('Failed to find any background tasks for course "{course}", module "{problem}" and student "{student}".') + '' msg += template.format(course=course_id, problem=problem_url, student=student.username) else: - msg += 'Failed to find any background tasks for course "{course}" and module "{problem}".'.format(course=course_id, problem=problem_url) + msg += '' + _u('Failed to find any background tasks for course "{course}" and module "{problem}".').format(course=course_id, problem=problem_url) + '' else: datatable['header'] = ["Task Type", "Task Id", diff --git a/lms/static/coffee/src/staff_grading/staff_grading.coffee b/lms/static/coffee/src/staff_grading/staff_grading.coffee index 80487f24ee34..1035eadb3d1d 100644 --- a/lms/static/coffee/src/staff_grading/staff_grading.coffee +++ b/lms/static/coffee/src/staff_grading/staff_grading.coffee @@ -47,36 +47,36 @@ The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for t ''' rubric: ''' - + - + - + - + - + - + - + - + @@ -98,9 +98,9 @@ The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for t max_score: 2 + @mock_cnt % 3 ml_error_info : 'ML accuracy info: ' + @mock_cnt else - response = + response = success: false - + else if cmd == 'save_grade' response = @@ -109,7 +109,7 @@ The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for t # should get back a list of problem_ids, problem_names, num_graded, min_for_ml else if cmd == 'get_problem_list' @mock_cnt = 1 - response = + response = success: true problem_list: [ {location: 'i4x://MITx/3.091x/problem/open_ended_demo1', \ @@ -123,7 +123,7 @@ The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for t error: 'Unknown command ' + cmd if @mock_cnt % 5 == 0 - response = + response = success: true message: 'No more submissions' @@ -132,7 +132,7 @@ The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for t response = success: false error: 'An error for testing' - + return response @@ -190,7 +190,7 @@ class @StaffGrading @question_header = $('.question-header') @question_header.click @collapse_question @collapse_question() - + # model state @state = state_no_data @submission_id = null @@ -266,7 +266,7 @@ class @StaffGrading @render_view() @scroll_to_top() - + get_next_submission: (location) -> @location = location @list_view = false @@ -296,7 +296,7 @@ class @StaffGrading submission_id: @submission_id location: @location submission_flagged: @flag_submission_checkbox.is(':checked') - @gentle_alert "Grades saved. Fetching the next submission to grade." + @gentle_alert gettext("Grades saved. Fetching the next submission to grade.") @backend.post('save_grade', data, @ajax_callback) gentle_alert: (msg) => @@ -344,13 +344,13 @@ class @StaffGrading # clear the problem list and breadcrumbs @problem_list.html(''' - - - - - + + + + + - ''') + ''') @breadcrumbs.html('') @problem_list_container.toggle(@list_view) if @backend.mock_backend @@ -364,14 +364,14 @@ class @StaffGrading # only show the grading elements when we are not in list view or the state # is invalid - show_grading_elements = !(@list_view || @state == state_error || + show_grading_elements = !(@list_view || @state == state_error || @state == state_no_data) @prompt_wrapper.toggle(show_grading_elements) @submission_wrapper.toggle(show_grading_elements) @grading_wrapper.toggle(show_grading_elements) @meta_info_wrapper.toggle(show_grading_elements) @action_button.hide() - + if @list_view @render_list() else @@ -410,23 +410,27 @@ class @StaffGrading show_action_button = true problem_list_link = $('').attr('href', 'javascript:void(0);') - .append("< Back to problem list") + .append("< " + gettext("Back to problem list")) .click => @get_problem_list() # set up the breadcrumbing @breadcrumbs.append(problem_list_link) - + if @state == state_error - @set_button_text('Try loading again') + @set_button_text(gettext('Try loading again')) show_action_button = true else if @state == state_grading @ml_error_info_container.html(@ml_error_info) + available = _.template(gettext("<%= num %> available"), {num: @num_pending}) + graded = _.template(gettext("<%= num %> graded"), {num: @num_graded}) + needed = _.template(gettext("<%= num %> more needed to start ML"), + {num: Math.max(@min_for_ml - @num_graded, 0)}) meta_list = $("
") - meta_list.append("
#{@num_pending} available |
") - meta_list.append("
#{@num_graded} graded |
") - meta_list.append("
#{Math.max(@min_for_ml - @num_graded, 0)} more needed to start ML

") + .append("
#{available}
") + .append("
#{graded}
") + .append("
#{needed}
") @problem_meta_info.html(meta_list) @prompt_container.html(@prompt) @@ -437,24 +441,24 @@ class @StaffGrading show_action_button = false @setup_score_selection() - + else if @state == state_graded - @set_button_text('Submit') + @set_button_text(gettext('Submit')) show_action_button = false else if @state == state_no_data @message_container.html(@message) - @set_button_text('Re-check for submissions') + @set_button_text(gettext('Re-check for submissions')) else - @error('System got into invalid state ' + @state) + @error(_.template(gettext('System got into invalid state: <%= state %>'), {state: @state})) @submit_button.toggle(show_submit_button) @action_button.toggle(show_action_button) submit: (event) => event.preventDefault() - + if @state == state_error @get_next_submission(@location) else if @state == state_graded @@ -462,17 +466,17 @@ class @StaffGrading else if @state == state_no_data @get_next_submission(@location) else - @error('System got into invalid state for submission: ' + @state) + @error(gettext('System got into invalid state for submission: ') + @state) collapse_question: () => @prompt_container.slideToggle() @prompt_container.toggleClass('open') - if @question_header.text() == "(Hide)" + if @question_header.text() == gettext("(Hide)") Logger.log 'staff_grading_hide_question', {location: @location} - new_text = "(Show)" + new_text = gettext("(Show)") else Logger.log 'staff_grading_show_question', {location: @location} - new_text = "(Hide)" + new_text = gettext("(Hide)") @question_header.text(new_text) scroll_to_top: () => diff --git a/lms/templates/combinedopenended/combined_open_ended.html b/lms/templates/combinedopenended/combined_open_ended.html index c8f4c029dc7f..5b0f94f6bb09 100644 --- a/lms/templates/combinedopenended/combined_open_ended.html +++ b/lms/templates/combinedopenended/combined_open_ended.html @@ -58,7 +58,7 @@

${display_name}

% if is_staff:
- Staff Warning: Please note that if you submit a duplicate of text that has already been submitted for grading, it will not show up in the staff grading view. It will be given the same grade that the original received automatically, and will be returned within 30 minutes if the original is already graded, or when the original is graded if not. + ${_("Staff Warning: Please note that if you submit a duplicate of text that has already been submitted for grading, it will not show up in the staff grading view. It will be given the same grade that the original received automatically, and will be returned within 30 minutes if the original is already graded, or when the original is graded if not.")}
% endif diff --git a/lms/templates/combinedopenended/openended/open_ended_rubric.html b/lms/templates/combinedopenended/openended/open_ended_rubric.html index 1d73647ec761..199f19924526 100644 --- a/lms/templates/combinedopenended/openended/open_ended_rubric.html +++ b/lms/templates/combinedopenended/openended/open_ended_rubric.html @@ -1,4 +1,5 @@ <%! from django.utils.translation import ugettext as _ %> +<%! from django.utils.translation import ungettext%> <% from random import randint %>
@@ -6,7 +7,7 @@
${_("Rubric")} -

Select the criteria you feel best represents this submission in each category.

+

${_("Select the criteria you feel best represents this submission in each category.")}

% for i in range(len(categories)): <% category = categories[i] %> @@ -22,7 +23,14 @@ % endif % endfor diff --git a/lms/templates/courseware/course_navigation.html b/lms/templates/courseware/course_navigation.html index 0be247ed899c..05189edfd15a 100644 --- a/lms/templates/courseware/course_navigation.html +++ b/lms/templates/courseware/course_navigation.html @@ -1,4 +1,5 @@ ## mako +<%! from django.utils.translation import ugettext as _ %> <%page args="active_page=None" /> <% diff --git a/lms/templates/courseware/gradebook.html b/lms/templates/courseware/gradebook.html index 8828721555de..4b1175590b90 100644 --- a/lms/templates/courseware/gradebook.html +++ b/lms/templates/courseware/gradebook.html @@ -45,7 +45,7 @@

${_("Gradebook")}

diff --git a/lms/templates/courseware/progress.html b/lms/templates/courseware/progress.html index 1c099119c0cd..6cb80e43e228 100644 --- a/lms/templates/courseware/progress.html +++ b/lms/templates/courseware/progress.html @@ -59,7 +59,7 @@

- ${"{0:.3n} of {1:.3n} possible points".format( float(earned), float(total) )} + ${_("{earned:.3n} of {total:.3n} possible points").format( earned = float(earned), total = float(total) )} %endif %if total > 0 or earned > 0: @@ -72,7 +72,7 @@

%if len(section['scores']) > 0: -

${ "Problem Scores: " if section['graded'] else "Practice Scores: "}

+

${ _("Problem Scores: ") if section['graded'] else _("Practice Scores: ")}

    %for score in section['scores']:
  1. ${"{0:.3n}/{1:.3n}".format(float(score.earned),float(score.possible))}
  2. From e8196d97617050abf036dbc70610cb70f690b2e6 Mon Sep 17 00:00:00 2001 From: Julia Hansbrough Date: Tue, 19 Nov 2013 19:13:47 +0000 Subject: [PATCH 038/142] Events for entering verified flow & buying --- common/djangoapps/course_modes/views.py | 6 +- common/djangoapps/student/models.py | 8 +- common/djangoapps/student/tests/tests.py | 9 +- common/djangoapps/track/contexts.py | 10 ++ .../courseware/features/certificates.feature | 2 + ...auto__add_field_certificateitem_upgrade.py | 115 ++++++++++++++++++ lms/djangoapps/shoppingcart/models.py | 20 ++- .../shoppingcart/tests/test_models.py | 26 +++- .../shoppingcart/tests/test_views.py | 1 + 9 files changed, 187 insertions(+), 10 deletions(-) create mode 100644 lms/djangoapps/shoppingcart/migrations/0005_auto__add_field_certificateitem_upgrade.py diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index f3dca5d08f8a..75e211b6c2ae 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -17,10 +17,12 @@ from course_modes.models import CourseMode from courseware.access import has_access -from student.models import CourseEnrollment +from student.models import CourseEnrollment, UserMethods from student.views import course_from_id from verify_student.models import SoftwareSecurePhotoVerification +EVENT_NAME_USER_CLICKED_UPGRADE = 'edx.user.upgrade.clicked' + class ChooseModeView(View): """ @@ -37,6 +39,8 @@ def get(self, request, course_id, error=None): enrollment_mode = CourseEnrollment.enrollment_mode_for_user(request.user, course_id) upgrade = request.GET.get('upgrade', False) + if upgrade == "True": + UserMethods.emit_event(request.user, course_id, EVENT_NAME_USER_CLICKED_UPGRADE) # verified users do not need to register or upgrade if enrollment_mode == 'verified': diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 734b3a1ec460..6503531a663d 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -16,6 +16,12 @@ import logging import uuid +import crum + +from track import contexts +from track.views import server_track +from eventtracking import tracker + from django.conf import settings from django.contrib.auth.models import User from django.contrib.auth.signals import user_logged_in, user_logged_out @@ -35,9 +41,7 @@ from track.views import server_track from eventtracking import tracker - unenroll_done = Signal(providing_args=["course_enrollment"]) - log = logging.getLogger(__name__) AUDIT_LOG = logging.getLogger("audit") diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index f2a9593123ff..d5f2da24ccde 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -340,7 +340,14 @@ def test_enrollment(self): user=user, course_id=course_id ) - self.assertFalse(enrollment_record.is_active) + + def test_user_emitted_events(self): + user = User.objects.create_user("joe", "joe@joe.com", "password") + course_id = "edX/Test101/2013" + course_id_partial = "edX/Test101" + with patch('eventtracking.tracker.get_tracker', side_effect=Exception): + UserMethods.emit_event(user, course_id, "fake") + self.assertTrue(True) # Make sure mode is updated properly if user unenrolls & re-enrolls enrollment = CourseEnrollment.enroll(user, course_id, "verified") diff --git a/common/djangoapps/track/contexts.py b/common/djangoapps/track/contexts.py index 0fb06fb1b1ac..e17bd3e61a92 100644 --- a/common/djangoapps/track/contexts.py +++ b/common/djangoapps/track/contexts.py @@ -57,3 +57,13 @@ def course_context_from_course_id(course_id): ) return context + + +def user_context(user): + """ + Creates a user context from `user` + """ + context = { + 'user': user, + } + return context diff --git a/lms/djangoapps/courseware/features/certificates.feature b/lms/djangoapps/courseware/features/certificates.feature index 4a4ceb44b103..64e4ae2b73d8 100644 --- a/lms/djangoapps/courseware/features/certificates.feature +++ b/lms/djangoapps/courseware/features/certificates.feature @@ -94,5 +94,7 @@ Feature: LMS.Verified certificates And I navigate to my dashboard Then I see the course on my dashboard And I see that I am on the verified track + And a "edx.user.upgrade.clicked" server event is emitted + And a "edx.user.upgrade.purchased" sever event is emitted And a "edx.course.enrollment.activated" server event is emitted diff --git a/lms/djangoapps/shoppingcart/migrations/0005_auto__add_field_certificateitem_upgrade.py b/lms/djangoapps/shoppingcart/migrations/0005_auto__add_field_certificateitem_upgrade.py new file mode 100644 index 000000000000..2089ba9add3d --- /dev/null +++ b/lms/djangoapps/shoppingcart/migrations/0005_auto__add_field_certificateitem_upgrade.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'CertificateItem.upgrade' + db.add_column('shoppingcart_certificateitem', 'upgrade', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'CertificateItem.upgrade' + db.delete_column('shoppingcart_certificateitem', 'upgrade') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'shoppingcart.certificateitem': { + 'Meta': {'object_name': 'CertificateItem', '_ormbases': ['shoppingcart.OrderItem']}, + 'course_enrollment': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['student.CourseEnrollment']"}), + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'mode': ('django.db.models.fields.SlugField', [], {'max_length': '50'}), + 'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'}), + 'upgrade': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'shoppingcart.order': { + 'Meta': {'object_name': 'Order'}, + 'bill_to_cardtype': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'bill_to_ccnum': ('django.db.models.fields.CharField', [], {'max_length': '8', 'blank': 'True'}), + 'bill_to_city': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'bill_to_country': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'bill_to_first': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'bill_to_last': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'bill_to_postalcode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'bill_to_state': ('django.db.models.fields.CharField', [], {'max_length': '8', 'blank': 'True'}), + 'bill_to_street1': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'bill_to_street2': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'processor_reply_dump': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'purchase_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'cart'", 'max_length': '32'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'shoppingcart.orderitem': { + 'Meta': {'object_name': 'OrderItem'}, + 'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}), + 'fulfilled_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'line_desc': ('django.db.models.fields.CharField', [], {'default': "'Misc. Item'", 'max_length': '1024'}), + 'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']"}), + 'qty': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'cart'", 'max_length': '32'}), + 'unit_cost': ('django.db.models.fields.DecimalField', [], {'default': '0.0', 'max_digits': '30', 'decimal_places': '2'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'shoppingcart.paidcourseregistration': { + 'Meta': {'object_name': 'PaidCourseRegistration', '_ormbases': ['shoppingcart.OrderItem']}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'mode': ('django.db.models.fields.SlugField', [], {'default': "'honor'", 'max_length': '50'}), + 'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'student.courseenrollment': { + 'Meta': {'ordering': "('user', 'course_id')", 'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '100'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + } + } + + complete_apps = ['shoppingcart'] \ No newline at end of file diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index 93f35ebc4688..ee44af950b66 100644 --- a/lms/djangoapps/shoppingcart/models.py +++ b/lms/djangoapps/shoppingcart/models.py @@ -25,7 +25,7 @@ from course_modes.models import CourseMode from edxmako.shortcuts import render_to_string from student.views import course_from_id -from student.models import CourseEnrollment, unenroll_done +from student.models import CourseEnrollment, unenroll_done, UserMethods from verify_student.models import SoftwareSecurePhotoVerification @@ -43,6 +43,8 @@ # we need a tuple to represent the primary key of various OrderItem subclasses OrderItemSubclassPK = namedtuple('OrderItemSubclassPK', ['cls', 'pk']) # pylint: disable=C0103 +EVENT_NAME_USER_UPGRADED = 'edx.user.upgrade.purchased' + class Order(models.Model): """ @@ -489,6 +491,7 @@ class CertificateItem(OrderItem): course_id = models.CharField(max_length=128, db_index=True) course_enrollment = models.ForeignKey(CourseEnrollment) mode = models.SlugField() + upgrade = models.BooleanField() @receiver(unenroll_done) def refund_cert_callback(sender, course_enrollment=None, **kwargs): @@ -557,7 +560,14 @@ def add_to_order(cls, order, course_id, cost, mode, currency='usd'): """ super(CertificateItem, cls).add_to_order(order, course_id, cost, currency=currency) - course_enrollment = CourseEnrollment.get_or_create_enrollment(order.user, course_id) + + try: + # If a course_enrollment already exists, this is an "upgrade" order + course_enrollment = CourseEnrollment.objects.get(user=order.user, course_id=course_id) + upgrade = True + except ObjectDoesNotExist: + course_enrollment = CourseEnrollment.get_or_create_enrollment(order.user, course_id) + upgrade = False # do some validation on the enrollment mode valid_modes = CourseMode.modes_for_course_dict(course_id) @@ -570,7 +580,8 @@ def add_to_order(cls, order, course_id, cost, mode, currency='usd'): user=order.user, course_id=course_id, course_enrollment=course_enrollment, - mode=mode + mode=mode, + upgrade=upgrade, ) item.status = order.status item.qty = 1 @@ -595,7 +606,8 @@ def purchased_callback(self): log.exception( "Could not submit verification attempt for enrollment {}".format(self.course_enrollment) ) - + if self.upgrade is True: + UserMethods.emit_event(self.user, self.course_enrollment.course_id, EVENT_NAME_USER_UPGRADED) self.course_enrollment.change_mode(self.mode) self.course_enrollment.activate() diff --git a/lms/djangoapps/shoppingcart/tests/test_models.py b/lms/djangoapps/shoppingcart/tests/test_models.py index cf01204942d9..a3b113dcbc3a 100644 --- a/lms/djangoapps/shoppingcart/tests/test_models.py +++ b/lms/djangoapps/shoppingcart/tests/test_models.py @@ -6,7 +6,7 @@ from textwrap import dedent from boto.exception import BotoServerError # this is a super-class of SESError and catches connection errors -from mock import patch, MagicMock +from mock import patch, MagicMock, sentinel from django.core import mail from django.conf import settings from django.db import DatabaseError @@ -425,15 +425,37 @@ def setUp(self): min_price=self.cost) course_mode.save() + patcher = patch('student.models.server_track') + self.mock_server_track = patcher.start() + self.addCleanup(patcher.stop) + crum_patcher = patch('student.models.crum.get_current_request') + self.mock_get_current_request = crum_patcher.start() + self.addCleanup(crum_patcher.stop) + self.mock_get_current_request.return_value = sentinel.request + def test_existing_enrollment(self): - CourseEnrollment.enroll(self.user, self.course_id) + enrollment = CourseEnrollment.enroll(self.user, self.course_id) cart = Order.get_cart_for_user(user=self.user) CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified') # verify that we are still enrolled self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_id)) + self.mock_server_track.reset_mock() cart.purchase() enrollment = CourseEnrollment.objects.get(user=self.user, course_id=self.course_id) self.assertEquals(enrollment.mode, u'verified') + self.assert_upgrade_event_was_emitted(self.user, self.course_id) + + def assert_upgrade_event_was_emitted(self, user, course_id): + """ Helper function; checks that a particular was called only once """ + self.mock_server_track.assert_called_once_with( + sentinel.request, + 'edx.user.upgrade.purchased', + { + 'user': user, + 'course_id': course_id, + } + ) + self.mock_server_track.reset_mock() def test_single_item_template(self): cart = Order.get_cart_for_user(user=self.user) diff --git a/lms/djangoapps/shoppingcart/tests/test_views.py b/lms/djangoapps/shoppingcart/tests/test_views.py index 0d23dc041904..816353ed907e 100644 --- a/lms/djangoapps/shoppingcart/tests/test_views.py +++ b/lms/djangoapps/shoppingcart/tests/test_views.py @@ -83,6 +83,7 @@ def test_add_nonexistent_course_to_cart(self): def test_add_course_to_cart_success(self): self.login_user() + reverse('shoppingcart.views.add_course_to_cart', args=[self.course_id]) resp = self.client.post(reverse('shoppingcart.views.add_course_to_cart', args=[self.course_id])) self.assertEqual(resp.status_code, 200) self.assertTrue(PaidCourseRegistration.contained_in_order(self.cart, self.course_id)) From cc187a318316cc904b8fb1f1c8743e7695f6233a Mon Sep 17 00:00:00 2001 From: Julia Hansbrough Date: Mon, 25 Nov 2013 21:52:12 +0000 Subject: [PATCH 039/142] Added refund datetime info to Orders, OrderItems Finance would like to have info about the date any given refund is requested. This commit adds a refund_requested_time field to OrderItem and a refund_time to Order. When a user *requests* a refund, the refund_requested_time field on OrderItem is updated; when a user actually *receives* their refund, the refund_time on Order is updated. Note that only the refund_requested_teim field is actually used right now; later on, when we're able to actually automate the process of actually giving users refunds, we will start usings Order's refund_time field. --- ...time__add_field_orderitem_refund_reques.py | 124 ++++++++++++++++++ lms/djangoapps/shoppingcart/models.py | 1 + 2 files changed, 125 insertions(+) create mode 100644 lms/djangoapps/shoppingcart/migrations/0005_auto__add_field_order_refunded_time__add_field_orderitem_refund_reques.py diff --git a/lms/djangoapps/shoppingcart/migrations/0005_auto__add_field_order_refunded_time__add_field_orderitem_refund_reques.py b/lms/djangoapps/shoppingcart/migrations/0005_auto__add_field_order_refunded_time__add_field_orderitem_refund_reques.py new file mode 100644 index 000000000000..9a96cbd2ece0 --- /dev/null +++ b/lms/djangoapps/shoppingcart/migrations/0005_auto__add_field_order_refunded_time__add_field_orderitem_refund_reques.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Order.refunded_time' + db.add_column('shoppingcart_order', 'refunded_time', + self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True), + keep_default=False) + + # Adding field 'OrderItem.refund_requested_time' + db.add_column('shoppingcart_orderitem', 'refund_requested_time', + self.gf('django.db.models.fields.DateTimeField')(null=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Order.refunded_time' + db.delete_column('shoppingcart_order', 'refunded_time') + + # Deleting field 'OrderItem.refund_requested_time' + db.delete_column('shoppingcart_orderitem', 'refund_requested_time') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'shoppingcart.certificateitem': { + 'Meta': {'object_name': 'CertificateItem', '_ormbases': ['shoppingcart.OrderItem']}, + 'course_enrollment': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['student.CourseEnrollment']"}), + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'mode': ('django.db.models.fields.SlugField', [], {'max_length': '50'}), + 'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'shoppingcart.order': { + 'Meta': {'object_name': 'Order'}, + 'bill_to_cardtype': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'bill_to_ccnum': ('django.db.models.fields.CharField', [], {'max_length': '8', 'blank': 'True'}), + 'bill_to_city': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'bill_to_country': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'bill_to_first': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'bill_to_last': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'bill_to_postalcode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'bill_to_state': ('django.db.models.fields.CharField', [], {'max_length': '8', 'blank': 'True'}), + 'bill_to_street1': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'bill_to_street2': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'processor_reply_dump': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'purchase_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'refunded_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'cart'", 'max_length': '32'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'shoppingcart.orderitem': { + 'Meta': {'object_name': 'OrderItem'}, + 'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}), + 'fulfilled_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'line_desc': ('django.db.models.fields.CharField', [], {'default': "'Misc. Item'", 'max_length': '1024'}), + 'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']"}), + 'qty': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'refund_requested_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'cart'", 'max_length': '32'}), + 'unit_cost': ('django.db.models.fields.DecimalField', [], {'default': '0.0', 'max_digits': '30', 'decimal_places': '2'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'shoppingcart.paidcourseregistration': { + 'Meta': {'object_name': 'PaidCourseRegistration', '_ormbases': ['shoppingcart.OrderItem']}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'mode': ('django.db.models.fields.SlugField', [], {'default': "'honor'", 'max_length': '50'}), + 'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'student.courseenrollment': { + 'Meta': {'ordering': "('user', 'course_id')", 'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '100'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + } + } + + complete_apps = ['shoppingcart'] \ No newline at end of file diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index ee44af950b66..0e9946613039 100644 --- a/lms/djangoapps/shoppingcart/models.py +++ b/lms/djangoapps/shoppingcart/models.py @@ -214,6 +214,7 @@ class OrderItem(models.Model): refund_requested_time = models.DateTimeField(null=True) # general purpose field, not user-visible. Used for reporting report_comments = models.TextField(default="") + refund_requested_time = models.DateTimeField(null=True) @property def line_cost(self): From f880bcb652605fd4ffd7e860345fc3fa20264263 Mon Sep 17 00:00:00 2001 From: Julia Hansbrough Date: Tue, 26 Nov 2013 20:23:56 +0000 Subject: [PATCH 040/142] Response to CR --- common/djangoapps/course_modes/views.py | 6 +----- common/djangoapps/student/models.py | 6 ------ common/djangoapps/student/tests/tests.py | 9 +-------- common/djangoapps/track/contexts.py | 10 ---------- lms/djangoapps/shoppingcart/models.py | 4 ++-- lms/djangoapps/shoppingcart/tests/test_models.py | 3 ++- lms/templates/dashboard.html | 6 ++++++ lms/templates/dashboard/_dashboard_course_listing.html | 2 +- 8 files changed, 13 insertions(+), 33 deletions(-) diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index 75e211b6c2ae..f3dca5d08f8a 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -17,12 +17,10 @@ from course_modes.models import CourseMode from courseware.access import has_access -from student.models import CourseEnrollment, UserMethods +from student.models import CourseEnrollment from student.views import course_from_id from verify_student.models import SoftwareSecurePhotoVerification -EVENT_NAME_USER_CLICKED_UPGRADE = 'edx.user.upgrade.clicked' - class ChooseModeView(View): """ @@ -39,8 +37,6 @@ def get(self, request, course_id, error=None): enrollment_mode = CourseEnrollment.enrollment_mode_for_user(request.user, course_id) upgrade = request.GET.get('upgrade', False) - if upgrade == "True": - UserMethods.emit_event(request.user, course_id, EVENT_NAME_USER_CLICKED_UPGRADE) # verified users do not need to register or upgrade if enrollment_mode == 'verified': diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 6503531a663d..8ff7fca8a10f 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -16,12 +16,6 @@ import logging import uuid -import crum - -from track import contexts -from track.views import server_track -from eventtracking import tracker - from django.conf import settings from django.contrib.auth.models import User from django.contrib.auth.signals import user_logged_in, user_logged_out diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index d5f2da24ccde..f2a9593123ff 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -340,14 +340,7 @@ def test_enrollment(self): user=user, course_id=course_id ) - - def test_user_emitted_events(self): - user = User.objects.create_user("joe", "joe@joe.com", "password") - course_id = "edX/Test101/2013" - course_id_partial = "edX/Test101" - with patch('eventtracking.tracker.get_tracker', side_effect=Exception): - UserMethods.emit_event(user, course_id, "fake") - self.assertTrue(True) + self.assertFalse(enrollment_record.is_active) # Make sure mode is updated properly if user unenrolls & re-enrolls enrollment = CourseEnrollment.enroll(user, course_id, "verified") diff --git a/common/djangoapps/track/contexts.py b/common/djangoapps/track/contexts.py index e17bd3e61a92..0fb06fb1b1ac 100644 --- a/common/djangoapps/track/contexts.py +++ b/common/djangoapps/track/contexts.py @@ -57,13 +57,3 @@ def course_context_from_course_id(course_id): ) return context - - -def user_context(user): - """ - Creates a user context from `user` - """ - context = { - 'user': user, - } - return context diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index 0e9946613039..d66e175776f5 100644 --- a/lms/djangoapps/shoppingcart/models.py +++ b/lms/djangoapps/shoppingcart/models.py @@ -25,7 +25,7 @@ from course_modes.models import CourseMode from edxmako.shortcuts import render_to_string from student.views import course_from_id -from student.models import CourseEnrollment, unenroll_done, UserMethods +from student.models import CourseEnrollment, unenroll_done from verify_student.models import SoftwareSecurePhotoVerification @@ -608,7 +608,7 @@ def purchased_callback(self): "Could not submit verification attempt for enrollment {}".format(self.course_enrollment) ) if self.upgrade is True: - UserMethods.emit_event(self.user, self.course_enrollment.course_id, EVENT_NAME_USER_UPGRADED) + self.course_enrollment.emit_event(EVENT_NAME_USER_UPGRADED) self.course_enrollment.change_mode(self.mode) self.course_enrollment.activate() diff --git a/lms/djangoapps/shoppingcart/tests/test_models.py b/lms/djangoapps/shoppingcart/tests/test_models.py index a3b113dcbc3a..f8b565155bf0 100644 --- a/lms/djangoapps/shoppingcart/tests/test_models.py +++ b/lms/djangoapps/shoppingcart/tests/test_models.py @@ -451,8 +451,9 @@ def assert_upgrade_event_was_emitted(self, user, course_id): sentinel.request, 'edx.user.upgrade.purchased', { - 'user': user, 'course_id': course_id, + 'user_id': user.pk, + 'mode': 'honor' } ) self.mock_server_track.reset_mock() diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index 306248b40165..7f0561033340 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -22,6 +22,12 @@ $(this).closest('.message.is-expandable').toggleClass('is-expanded'); } + $("#upgrade-to-verified").click(function(event) { + $user = $(event.target).data("user"); + $course = $(event.target).data("course-id"); + Logger.log('EVENT-NAME-USER-CLICKED-UPGRADE', [$user, $course], null); + }); + $(".email-settings").click(function(event) { $("#email_settings_course_id").val( $(event.target).data("course-id") ); $("#email_settings_course_number").text( $(event.target).data("course-number") ); diff --git a/lms/templates/dashboard/_dashboard_course_listing.html b/lms/templates/dashboard/_dashboard_course_listing.html index aa7b1b84b05e..c4da21824cba 100644 --- a/lms/templates/dashboard/_dashboard_course_listing.html +++ b/lms/templates/dashboard/_dashboard_course_listing.html @@ -80,7 +80,7 @@

    ID Verified Ribbon/Badge - ${_("Upgrade to Verified Track")} + ${_("Upgrade to Verified Track")} From 8c612ba975c5605c16517cb21028880c51a7b397 Mon Sep 17 00:00:00 2001 From: Julia Hansbrough Date: Tue, 26 Nov 2013 21:44:44 +0000 Subject: [PATCH 041/142] Quick fix --- lms/djangoapps/courseware/features/certificates.feature | 1 - 1 file changed, 1 deletion(-) diff --git a/lms/djangoapps/courseware/features/certificates.feature b/lms/djangoapps/courseware/features/certificates.feature index 64e4ae2b73d8..63e84fb13df0 100644 --- a/lms/djangoapps/courseware/features/certificates.feature +++ b/lms/djangoapps/courseware/features/certificates.feature @@ -94,7 +94,6 @@ Feature: LMS.Verified certificates And I navigate to my dashboard Then I see the course on my dashboard And I see that I am on the verified track - And a "edx.user.upgrade.clicked" server event is emitted And a "edx.user.upgrade.purchased" sever event is emitted And a "edx.course.enrollment.activated" server event is emitted From 83f42f5691e6df821c1ddabe89b86bdf1e101cc1 Mon Sep 17 00:00:00 2001 From: Julia Hansbrough Date: Tue, 26 Nov 2013 22:13:49 +0000 Subject: [PATCH 042/142] Response to CR --- common/djangoapps/course_modes/views.py | 1 + .../courseware/features/certificates.feature | 3 +- ...auto__add_field_certificateitem_upgrade.py | 115 ---------------- ...time__add_field_orderitem_refund_reques.py | 124 ------------------ lms/djangoapps/shoppingcart/models.py | 14 +- .../shoppingcart/tests/test_models.py | 3 +- lms/djangoapps/shoppingcart/views.py | 10 ++ lms/templates/dashboard.html | 6 +- 8 files changed, 17 insertions(+), 259 deletions(-) delete mode 100644 lms/djangoapps/shoppingcart/migrations/0005_auto__add_field_certificateitem_upgrade.py delete mode 100644 lms/djangoapps/shoppingcart/migrations/0005_auto__add_field_order_refunded_time__add_field_orderitem_refund_reques.py diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index f3dca5d08f8a..8ca43a17bb51 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -37,6 +37,7 @@ def get(self, request, course_id, error=None): enrollment_mode = CourseEnrollment.enrollment_mode_for_user(request.user, course_id) upgrade = request.GET.get('upgrade', False) + request.session['attempting_upgrade'] = upgrade # verified users do not need to register or upgrade if enrollment_mode == 'verified': diff --git a/lms/djangoapps/courseware/features/certificates.feature b/lms/djangoapps/courseware/features/certificates.feature index 63e84fb13df0..ced2a1c2a26a 100644 --- a/lms/djangoapps/courseware/features/certificates.feature +++ b/lms/djangoapps/courseware/features/certificates.feature @@ -94,6 +94,5 @@ Feature: LMS.Verified certificates And I navigate to my dashboard Then I see the course on my dashboard And I see that I am on the verified track - And a "edx.user.upgrade.purchased" sever event is emitted And a "edx.course.enrollment.activated" server event is emitted - + And a "edx.course.enrollment.upgrade.succeeded" server event is emitted diff --git a/lms/djangoapps/shoppingcart/migrations/0005_auto__add_field_certificateitem_upgrade.py b/lms/djangoapps/shoppingcart/migrations/0005_auto__add_field_certificateitem_upgrade.py deleted file mode 100644 index 2089ba9add3d..000000000000 --- a/lms/djangoapps/shoppingcart/migrations/0005_auto__add_field_certificateitem_upgrade.py +++ /dev/null @@ -1,115 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding field 'CertificateItem.upgrade' - db.add_column('shoppingcart_certificateitem', 'upgrade', - self.gf('django.db.models.fields.BooleanField')(default=False), - keep_default=False) - - - def backwards(self, orm): - # Deleting field 'CertificateItem.upgrade' - db.delete_column('shoppingcart_certificateitem', 'upgrade') - - - models = { - 'auth.group': { - 'Meta': {'object_name': 'Group'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - 'auth.permission': { - 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - 'auth.user': { - 'Meta': {'object_name': 'User'}, - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) - }, - 'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - 'shoppingcart.certificateitem': { - 'Meta': {'object_name': 'CertificateItem', '_ormbases': ['shoppingcart.OrderItem']}, - 'course_enrollment': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['student.CourseEnrollment']"}), - 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), - 'mode': ('django.db.models.fields.SlugField', [], {'max_length': '50'}), - 'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'}), - 'upgrade': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) - }, - 'shoppingcart.order': { - 'Meta': {'object_name': 'Order'}, - 'bill_to_cardtype': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), - 'bill_to_ccnum': ('django.db.models.fields.CharField', [], {'max_length': '8', 'blank': 'True'}), - 'bill_to_city': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), - 'bill_to_country': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), - 'bill_to_first': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), - 'bill_to_last': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), - 'bill_to_postalcode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), - 'bill_to_state': ('django.db.models.fields.CharField', [], {'max_length': '8', 'blank': 'True'}), - 'bill_to_street1': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), - 'bill_to_street2': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), - 'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'processor_reply_dump': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'purchase_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), - 'status': ('django.db.models.fields.CharField', [], {'default': "'cart'", 'max_length': '32'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) - }, - 'shoppingcart.orderitem': { - 'Meta': {'object_name': 'OrderItem'}, - 'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}), - 'fulfilled_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'line_desc': ('django.db.models.fields.CharField', [], {'default': "'Misc. Item'", 'max_length': '1024'}), - 'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']"}), - 'qty': ('django.db.models.fields.IntegerField', [], {'default': '1'}), - 'status': ('django.db.models.fields.CharField', [], {'default': "'cart'", 'max_length': '32'}), - 'unit_cost': ('django.db.models.fields.DecimalField', [], {'default': '0.0', 'max_digits': '30', 'decimal_places': '2'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) - }, - 'shoppingcart.paidcourseregistration': { - 'Meta': {'object_name': 'PaidCourseRegistration', '_ormbases': ['shoppingcart.OrderItem']}, - 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), - 'mode': ('django.db.models.fields.SlugField', [], {'default': "'honor'", 'max_length': '50'}), - 'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'}) - }, - 'student.courseenrollment': { - 'Meta': {'ordering': "('user', 'course_id')", 'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'}, - 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), - 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '100'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) - } - } - - complete_apps = ['shoppingcart'] \ No newline at end of file diff --git a/lms/djangoapps/shoppingcart/migrations/0005_auto__add_field_order_refunded_time__add_field_orderitem_refund_reques.py b/lms/djangoapps/shoppingcart/migrations/0005_auto__add_field_order_refunded_time__add_field_orderitem_refund_reques.py deleted file mode 100644 index 9a96cbd2ece0..000000000000 --- a/lms/djangoapps/shoppingcart/migrations/0005_auto__add_field_order_refunded_time__add_field_orderitem_refund_reques.py +++ /dev/null @@ -1,124 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding field 'Order.refunded_time' - db.add_column('shoppingcart_order', 'refunded_time', - self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True), - keep_default=False) - - # Adding field 'OrderItem.refund_requested_time' - db.add_column('shoppingcart_orderitem', 'refund_requested_time', - self.gf('django.db.models.fields.DateTimeField')(null=True), - keep_default=False) - - - def backwards(self, orm): - # Deleting field 'Order.refunded_time' - db.delete_column('shoppingcart_order', 'refunded_time') - - # Deleting field 'OrderItem.refund_requested_time' - db.delete_column('shoppingcart_orderitem', 'refund_requested_time') - - - models = { - 'auth.group': { - 'Meta': {'object_name': 'Group'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - 'auth.permission': { - 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - 'auth.user': { - 'Meta': {'object_name': 'User'}, - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) - }, - 'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - 'shoppingcart.certificateitem': { - 'Meta': {'object_name': 'CertificateItem', '_ormbases': ['shoppingcart.OrderItem']}, - 'course_enrollment': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['student.CourseEnrollment']"}), - 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), - 'mode': ('django.db.models.fields.SlugField', [], {'max_length': '50'}), - 'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'}) - }, - 'shoppingcart.order': { - 'Meta': {'object_name': 'Order'}, - 'bill_to_cardtype': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), - 'bill_to_ccnum': ('django.db.models.fields.CharField', [], {'max_length': '8', 'blank': 'True'}), - 'bill_to_city': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), - 'bill_to_country': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), - 'bill_to_first': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), - 'bill_to_last': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), - 'bill_to_postalcode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), - 'bill_to_state': ('django.db.models.fields.CharField', [], {'max_length': '8', 'blank': 'True'}), - 'bill_to_street1': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), - 'bill_to_street2': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), - 'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'processor_reply_dump': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'purchase_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), - 'refunded_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), - 'status': ('django.db.models.fields.CharField', [], {'default': "'cart'", 'max_length': '32'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) - }, - 'shoppingcart.orderitem': { - 'Meta': {'object_name': 'OrderItem'}, - 'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}), - 'fulfilled_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'line_desc': ('django.db.models.fields.CharField', [], {'default': "'Misc. Item'", 'max_length': '1024'}), - 'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']"}), - 'qty': ('django.db.models.fields.IntegerField', [], {'default': '1'}), - 'refund_requested_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), - 'status': ('django.db.models.fields.CharField', [], {'default': "'cart'", 'max_length': '32'}), - 'unit_cost': ('django.db.models.fields.DecimalField', [], {'default': '0.0', 'max_digits': '30', 'decimal_places': '2'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) - }, - 'shoppingcart.paidcourseregistration': { - 'Meta': {'object_name': 'PaidCourseRegistration', '_ormbases': ['shoppingcart.OrderItem']}, - 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), - 'mode': ('django.db.models.fields.SlugField', [], {'default': "'honor'", 'max_length': '50'}), - 'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'}) - }, - 'student.courseenrollment': { - 'Meta': {'ordering': "('user', 'course_id')", 'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'}, - 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), - 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '100'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) - } - } - - complete_apps = ['shoppingcart'] \ No newline at end of file diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index d66e175776f5..164debe0b8e3 100644 --- a/lms/djangoapps/shoppingcart/models.py +++ b/lms/djangoapps/shoppingcart/models.py @@ -43,8 +43,6 @@ # we need a tuple to represent the primary key of various OrderItem subclasses OrderItemSubclassPK = namedtuple('OrderItemSubclassPK', ['cls', 'pk']) # pylint: disable=C0103 -EVENT_NAME_USER_UPGRADED = 'edx.user.upgrade.purchased' - class Order(models.Model): """ @@ -492,7 +490,6 @@ class CertificateItem(OrderItem): course_id = models.CharField(max_length=128, db_index=True) course_enrollment = models.ForeignKey(CourseEnrollment) mode = models.SlugField() - upgrade = models.BooleanField() @receiver(unenroll_done) def refund_cert_callback(sender, course_enrollment=None, **kwargs): @@ -562,13 +559,7 @@ def add_to_order(cls, order, course_id, cost, mode, currency='usd'): """ super(CertificateItem, cls).add_to_order(order, course_id, cost, currency=currency) - try: - # If a course_enrollment already exists, this is an "upgrade" order - course_enrollment = CourseEnrollment.objects.get(user=order.user, course_id=course_id) - upgrade = True - except ObjectDoesNotExist: - course_enrollment = CourseEnrollment.get_or_create_enrollment(order.user, course_id) - upgrade = False + course_enrollment = CourseEnrollment.get_or_create_enrollment(order.user, course_id) # do some validation on the enrollment mode valid_modes = CourseMode.modes_for_course_dict(course_id) @@ -582,7 +573,6 @@ def add_to_order(cls, order, course_id, cost, mode, currency='usd'): course_id=course_id, course_enrollment=course_enrollment, mode=mode, - upgrade=upgrade, ) item.status = order.status item.qty = 1 @@ -607,8 +597,6 @@ def purchased_callback(self): log.exception( "Could not submit verification attempt for enrollment {}".format(self.course_enrollment) ) - if self.upgrade is True: - self.course_enrollment.emit_event(EVENT_NAME_USER_UPGRADED) self.course_enrollment.change_mode(self.mode) self.course_enrollment.activate() diff --git a/lms/djangoapps/shoppingcart/tests/test_models.py b/lms/djangoapps/shoppingcart/tests/test_models.py index f8b565155bf0..f4ec42246393 100644 --- a/lms/djangoapps/shoppingcart/tests/test_models.py +++ b/lms/djangoapps/shoppingcart/tests/test_models.py @@ -443,13 +443,12 @@ def test_existing_enrollment(self): cart.purchase() enrollment = CourseEnrollment.objects.get(user=self.user, course_id=self.course_id) self.assertEquals(enrollment.mode, u'verified') - self.assert_upgrade_event_was_emitted(self.user, self.course_id) def assert_upgrade_event_was_emitted(self, user, course_id): """ Helper function; checks that a particular was called only once """ self.mock_server_track.assert_called_once_with( sentinel.request, - 'edx.user.upgrade.purchased', + 'edx.course.enrollment.upgrade.succeeded', { 'course_id': course_id, 'user_id': user.pk, diff --git a/lms/djangoapps/shoppingcart/views.py b/lms/djangoapps/shoppingcart/views.py index 26cf3e5a51d3..5145525085ed 100644 --- a/lms/djangoapps/shoppingcart/views.py +++ b/lms/djangoapps/shoppingcart/views.py @@ -12,11 +12,14 @@ from django.contrib.auth.decorators import login_required from edxmako.shortcuts import render_to_response from .models import Order, PaidCourseRegistration, OrderItem +from student.models import CourseEnrollment from .processors import process_postpay_callback, render_purchase_form_html from .exceptions import ItemAlreadyInCartException, AlreadyEnrolledInCourseException, CourseDoesNotExistException log = logging.getLogger("shoppingcart") +EVENT_NAME_USER_UPGRADED = 'edx.course.enrollment.upgrade.succeeded' + @require_POST def add_course_to_cart(request, course_id): @@ -99,6 +102,7 @@ def show_receipt(request, ordernum): Displays a receipt for a particular order. 404 if order is not yet purchased or request.user != order.user """ + try: order = Order.objects.get(id=ordernum) except Order.DoesNotExist: @@ -124,6 +128,12 @@ def show_receipt(request, ordernum): receipt_template = order_items[0].single_item_receipt_template context.update(order_items[0].single_item_receipt_context) + attempting_upgrade = request.session.get('attempting_upgrade', False) + if attempting_upgrade: + course_enrollment = CourseEnrollment.get_or_create_enrollment(request.user, context['course_id']) + course_enrollment.emit_event(EVENT_NAME_USER_UPGRADED) + request.session['attempting_upgrade'] = False + return render_to_response(receipt_template, context) diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index 7f0561033340..343dfad06229 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -23,9 +23,9 @@ } $("#upgrade-to-verified").click(function(event) { - $user = $(event.target).data("user"); - $course = $(event.target).data("course-id"); - Logger.log('EVENT-NAME-USER-CLICKED-UPGRADE', [$user, $course], null); + user = $(event.target).data("user"); + course = $(event.target).data("course-id"); + Logger.log('edx.course.enrollment.upgrade.clicked', [user, course], null); }); $(".email-settings").click(function(event) { From 2b7587d6a88a119471cd1a66768b23269d1322d1 Mon Sep 17 00:00:00 2001 From: Julia Hansbrough Date: Wed, 4 Dec 2013 21:22:04 +0000 Subject: [PATCH 043/142] Added alternate upsell copy flag For waffle / split testing purposes --- lms/templates/dashboard/_dashboard_course_listing.html | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lms/templates/dashboard/_dashboard_course_listing.html b/lms/templates/dashboard/_dashboard_course_listing.html index c4da21824cba..3569e365ee94 100644 --- a/lms/templates/dashboard/_dashboard_course_listing.html +++ b/lms/templates/dashboard/_dashboard_course_listing.html @@ -61,13 +61,17 @@

    <%include file='_dashboard_certificate_information.html' args='cert_status=cert_status,course=course, enrollment=enrollment'/> % endif - %if course_mode_info['show_upsell']: + % if course_mode_info['show_upsell']:

Purpose
Organization
Problem NameGradedAvailable to GradeRequiredProgress''' + gettext("Problem Name") + '''''' + gettext("Graded") + '''''' + gettext("Available to Grade") + '''''' + gettext("Required") + '''''' + gettext("Progress") + '''
- +
+ + %for hname in datatable['header']: + + %endfor + + %for row in datatable['data']: + + %for value in row: + + %endfor + + %endfor +
${hname}
${value}
+

+%endif + +%if plots: + + %for plot in plots: +
+

${plot['title']}

+
+

${plot['info']}

+
+
+ +
+
+ %endfor + +%endif + +