From 562456ade7f1d26bf8e2509c84a006b9017ee1b5 Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Fri, 2 Aug 2013 15:36:15 -0400 Subject: [PATCH 001/147] Make descriptorsystem inherit from Runtime --- common/lib/xmodule/xmodule/x_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index d399001a6a..7e4802d208 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -808,7 +808,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): return Fragment(self.get_html()) -class DescriptorSystem(object): +class DescriptorSystem(Runtime): def __init__(self, load_item, resources_fs, error_tracker, **kwargs): """ load_item: Takes a Location and returns an XModuleDescriptor From 7bfb0804f8034dcad9381f600356705c7c064b2c Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Fri, 2 Aug 2013 16:09:29 -0400 Subject: [PATCH 002/147] Switch to studio_view --- cms/djangoapps/contentstore/views/preview.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index f2a07abe32..2a0d71d569 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -75,9 +75,13 @@ def preview_component(request, location): component = modulestore().get_item(location) + # wrap_xmodule expects a function, so make a constant function + def get_render(): + return component.runtime.render(component, None, "studio_view").content + return render_to_response('component.html', { 'preview': get_module_previews(request, component)[0], - 'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(), + 'editor': wrap_xmodule(get_render, component, 'xmodule_edit.html')(), }) From b38750e15d41e8830f484f14ba5e5f577cea1075 Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Mon, 5 Aug 2013 11:09:43 -0400 Subject: [PATCH 003/147] Refactor wrap_xmodule call --- cms/djangoapps/contentstore/views/preview.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index 2a0d71d569..e801faa4f1 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -75,13 +75,15 @@ def preview_component(request, location): component = modulestore().get_item(location) - # wrap_xmodule expects a function, so make a constant function - def get_render(): - return component.runtime.render(component, None, "studio_view").content + component.get_html = wrap_xmodule( + component.get_html, + component, + 'xmodule_edit.html' + ) return render_to_response('component.html', { 'preview': get_module_previews(request, component)[0], - 'editor': wrap_xmodule(get_render, component, 'xmodule_edit.html')(), + 'editor': component.runtime.render(component, None, 'studio_view').content, }) From 1abea0b406a08f808f1850871a62fcb6504dcccc Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 5 Aug 2013 14:16:26 -0400 Subject: [PATCH 004/147] Check that content-type starts with application/json When Chrome sends the AJAX request to add a user to the course team, it sets the Content-type to "application/json". However, when Firefox sends the same request, it sets the Content-type to "application/json; charset=UTF-8". This commit only checks that the Content-type begins with "application/json", not is identical to it; that way, Firefox can play, too. --- cms/djangoapps/contentstore/views/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/views/user.py b/cms/djangoapps/contentstore/views/user.py index e1c75bad0f..5b38d47452 100644 --- a/cms/djangoapps/contentstore/views/user.py +++ b/cms/djangoapps/contentstore/views/user.py @@ -179,7 +179,7 @@ def course_team_user(request, org, course, name, email): return JsonResponse() # all other operations require the requesting user to specify a role - if request.META.get("CONTENT_TYPE", "") == "application/json" and request.body: + if request.META.get("CONTENT_TYPE", "").startswith("application/json") and request.body: try: payload = json.loads(request.body) except: From 73b9e261e424c22a856790cae764b94421dc76bb Mon Sep 17 00:00:00 2001 From: Adam Palay Date: Mon, 5 Aug 2013 13:23:45 -0400 Subject: [PATCH 005/147] redirects lms landing page to student.views.index if there is no marketing site resets the cms edge redirect to '/' --- cms/djangoapps/contentstore/views/requests.py | 2 +- lms/djangoapps/branding/views.py | 13 ++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/cms/djangoapps/contentstore/views/requests.py b/cms/djangoapps/contentstore/views/requests.py index dc1b7871ab..abbf84755e 100644 --- a/cms/djangoapps/contentstore/views/requests.py +++ b/cms/djangoapps/contentstore/views/requests.py @@ -12,7 +12,7 @@ def landing(request, org, course, coursename): # points to the temporary edge page def edge(request): - return redirect('/dashboard') + return redirect('/') def event(request): diff --git a/lms/djangoapps/branding/views.py b/lms/djangoapps/branding/views.py index 985dfa52d0..531b61f3ff 100644 --- a/lms/djangoapps/branding/views.py +++ b/lms/djangoapps/branding/views.py @@ -4,7 +4,6 @@ from django.shortcuts import redirect from django_future.csrf import ensure_csrf_cookie import student.views -import branding import courseware.views from mitxmako.shortcuts import marketing_link from util.cache import cache_if_anonymous @@ -26,11 +25,7 @@ def index(request): if settings.MITX_FEATURES.get('ENABLE_MKTG_SITE'): return redirect(settings.MKTG_URLS.get('ROOT')) - university = branding.get_university(request.META.get('HTTP_HOST')) - if university is None: - return student.views.index(request, user=request.user) - - return redirect('/') + return student.views.index(request, user=request.user) @ensure_csrf_cookie @@ -44,8 +39,4 @@ def courses(request): if settings.MITX_FEATURES.get('ENABLE_MKTG_SITE', False): return redirect(marketing_link('COURSES'), permanent=True) - university = branding.get_university(request.META.get('HTTP_HOST')) - if university is None: - return courseware.views.courses(request) - - return redirect('/') + return courseware.views.courses(request) From bf5af6c8cfb67e698ce14ba5217808d96df3b75c Mon Sep 17 00:00:00 2001 From: Adam Palay Date: Mon, 5 Aug 2013 17:11:33 -0400 Subject: [PATCH 006/147] add university_profile/edge.html for edge landing page --- lms/djangoapps/branding/views.py | 14 ++++- lms/templates/university_profile/edge.html | 65 ++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 lms/templates/university_profile/edge.html diff --git a/lms/djangoapps/branding/views.py b/lms/djangoapps/branding/views.py index 531b61f3ff..42c57e3090 100644 --- a/lms/djangoapps/branding/views.py +++ b/lms/djangoapps/branding/views.py @@ -2,8 +2,10 @@ from django.conf import settings from django.core.urlresolvers import reverse from django.shortcuts import redirect from django_future.csrf import ensure_csrf_cookie +from mitxmako.shortcuts import render_to_response import student.views +import branding import courseware.views from mitxmako.shortcuts import marketing_link from util.cache import cache_if_anonymous @@ -25,7 +27,11 @@ def index(request): if settings.MITX_FEATURES.get('ENABLE_MKTG_SITE'): return redirect(settings.MKTG_URLS.get('ROOT')) - return student.views.index(request, user=request.user) + university = branding.get_university(request.META.get('HTTP_HOST')) + if university is None: + return student.views.index(request, user=request.user) + + return render_to_response('university_profile/edge.html', {}) @ensure_csrf_cookie @@ -39,4 +45,8 @@ def courses(request): if settings.MITX_FEATURES.get('ENABLE_MKTG_SITE', False): return redirect(marketing_link('COURSES'), permanent=True) - return courseware.views.courses(request) + university = branding.get_university(request.META.get('HTTP_HOST')) + if university is None: + return courseware.views.courses(request) + + return render_to_response('university_profile/edge.html', {}) diff --git a/lms/templates/university_profile/edge.html b/lms/templates/university_profile/edge.html new file mode 100644 index 0000000000..a3e115ddd8 --- /dev/null +++ b/lms/templates/university_profile/edge.html @@ -0,0 +1,65 @@ +<%inherit file="../stripped-main.html" /> +<%! from django.core.urlresolvers import reverse %> +<%block name="title">edX edge +<%block name="bodyclass">no-header edge-landing + +<%block name="content"> +
+
edX edge
+
+ + +
+
+ + + +<%block name="js_extra"> + + + +<%include file="../signup_modal.html" /> +<%include file="../forgot_password_modal.html" /> \ No newline at end of file From 17b5ccf13a6c271532ca53431240c1d33fd194ec Mon Sep 17 00:00:00 2001 From: Adam Palay Date: Mon, 5 Aug 2013 18:08:32 -0400 Subject: [PATCH 007/147] creates "edge" case --- lms/djangoapps/branding/views.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lms/djangoapps/branding/views.py b/lms/djangoapps/branding/views.py index 42c57e3090..6a8ef8056b 100644 --- a/lms/djangoapps/branding/views.py +++ b/lms/djangoapps/branding/views.py @@ -28,10 +28,13 @@ def index(request): return redirect(settings.MKTG_URLS.get('ROOT')) university = branding.get_university(request.META.get('HTTP_HOST')) - if university is None: - return student.views.index(request, user=request.user) + if university == 'edge': + return render_to_response('university_profile/edge.html', {}) + + # we do not expect this case to be reached in cases where + # marketing and edge are enabled + return student.views.index(request, user=request.user) - return render_to_response('university_profile/edge.html', {}) @ensure_csrf_cookie @@ -46,7 +49,9 @@ def courses(request): return redirect(marketing_link('COURSES'), permanent=True) university = branding.get_university(request.META.get('HTTP_HOST')) - if university is None: - return courseware.views.courses(request) + if university == 'edge': + return render_to_response('university_profile/edge.html', {}) - return render_to_response('university_profile/edge.html', {}) + # we do not expect this case to be reached in cases where + # marketing and edge are enabled + return courseware.views.courses(request) From 5dd202e5926e84fa4ee54783447c0df72a8007b0 Mon Sep 17 00:00:00 2001 From: Adam Palay Date: Mon, 5 Aug 2013 18:45:54 -0400 Subject: [PATCH 008/147] update edge.html --- lms/templates/university_profile/edge.html | 23 +++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/lms/templates/university_profile/edge.html b/lms/templates/university_profile/edge.html index a3e115ddd8..166a95a106 100644 --- a/lms/templates/university_profile/edge.html +++ b/lms/templates/university_profile/edge.html @@ -1,33 +1,34 @@ +<%! from django.utils.translation import ugettext as _ %> <%inherit file="../stripped-main.html" /> <%! from django.core.urlresolvers import reverse %> -<%block name="title">edX edge +<%block name="title">${_("edX edge")} <%block name="bodyclass">no-header edge-landing <%block name="content">
-
edX edge
+
${_("edX edge")}
From 9728dceeb0da512f50e1e8efcac634bb6db82e2f Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Tue, 6 Aug 2013 11:29:51 +0300 Subject: [PATCH 009/147] Adds RawDescriptro for VideoAlpha --- common/lib/xmodule/xmodule/videoalpha_module.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index 084389411a..3ac3d5157c 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -21,6 +21,7 @@ from django.conf import settings from xmodule.x_module import XModule from xmodule.editing_module import TabsEditingDescriptor +from xmodule.raw_module import RawDescriptor from xmodule.modulestore.mongo import MongoModuleStore from xmodule.modulestore.django import modulestore from xmodule.contentstore.content import StaticContent @@ -187,7 +188,7 @@ class VideoAlphaModule(VideoAlphaFields, XModule): }) -class VideoAlphaDescriptor(VideoAlphaFields, TabsEditingDescriptor): +class VideoAlphaDescriptor(VideoAlphaFields, TabsEditingDescriptor, RawDescriptor): """Descriptor for `VideoAlphaModule`.""" module_class = VideoAlphaModule From cb54081d2eb4795b334d812edc76433d36d11935 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 6 Aug 2013 09:55:49 -0400 Subject: [PATCH 010/147] Fix notification problem --- .../open_ended_grading/open_ended_notifications.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/lms/djangoapps/open_ended_grading/open_ended_notifications.py b/lms/djangoapps/open_ended_grading/open_ended_notifications.py index 1d6fa22929..44ff41be22 100644 --- a/lms/djangoapps/open_ended_grading/open_ended_notifications.py +++ b/lms/djangoapps/open_ended_grading/open_ended_notifications.py @@ -146,19 +146,7 @@ def combined_notifications(course, user): #Get the time of the last login of the user last_login = user.last_login - - #Find the modules they have seen since they logged in - last_module_seen = StudentModule.objects.filter(student=user, course_id=course_id, - modified__gt=last_login).values('modified').order_by( - '-modified') - last_module_seen_count = last_module_seen.count() - - if last_module_seen_count > 0: - #The last time they viewed an updated notification (last module seen minus how long notifications are cached) - last_time_viewed = last_module_seen[0]['modified'] - datetime.timedelta(seconds=(NOTIFICATION_CACHE_TIME + 60)) - else: - #If they have not seen any modules since they logged in, then don't refresh - return {'pending_grading': False, 'img_path': img_path, 'response': notifications} + last_time_viewed = last_login - datetime.timedelta(seconds=(NOTIFICATION_CACHE_TIME + 60)) try: #Get the notifications from the grading controller From e8e09afa0a3b00fe88ea4978d7647cf14dc9b20b Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 6 Aug 2013 10:45:14 -0400 Subject: [PATCH 011/147] Make sure that we properly parse and save section release times Firefox wasn't saving section release times, due to issues with JS Date() parsing. I've modified the code to make it more explicit around what it should do and how it should work, which also makes it work better with both Firefox and Chrome. --- cms/static/js/base.js | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index de0fd955dc..7f82e67431 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -253,6 +253,12 @@ function syncReleaseDate(e) { $("#start_time").val(""); } +function pad2(number) { + // pad a number to two places: useful for formatting months, days, hours, etc + // when displaying a date/time + return (number < 10 ? '0' : '') + number; +} + function getEdxTimeFromDateTimeVals(date_val, time_val) { if (date_val != '') { if (time_val == '') time_val = '00:00'; @@ -772,21 +778,23 @@ function cancelSetSectionScheduleDate(e) { function saveSetSectionScheduleDate(e) { e.preventDefault(); - var input_date = $('.edit-subsection-publish-settings .start-date').val(); - var input_time = $('.edit-subsection-publish-settings .start-time').val(); - - var start = getEdxTimeFromDateTimeVals(input_date, input_time); + var date = $('.edit-subsection-publish-settings .start-date').datepicker("getDate"); + var time = $('.edit-subsection-publish-settings .start-time').timepicker("getTime"); + var datetime = new Date(Date.UTC( + date.getFullYear(), date.getMonth(), date.getDate(), + time.getHours(), time.getMinutes() + )); var id = $modal.attr('data-id'); analytics.track('Edited Section Release Date', { 'course': course_location_analytics, 'id': id, - 'start': start + 'start': datetime }); var saving = new CMS.Views.Notification.Mini({ - title: gettext("Saving") + "…", + title: gettext("Saving") + "…" }); saving.show(); // call into server to commit the new order @@ -798,7 +806,7 @@ function saveSetSectionScheduleDate(e) { data: JSON.stringify({ 'id': id, 'metadata': { - 'start': start + 'start': datetime } }) }).success(function() { @@ -806,12 +814,15 @@ function saveSetSectionScheduleDate(e) { var html = _.template( '' + '' + gettext("Will Release:") + ' ' + - gettext("<%= date %> at <%= time %> UTC") + + gettext("{month}/{day}/{year} at {hour}:{minute} UTC") + '' + - '' + + '' + gettext("Edit") + '', - {date: input_date, time: input_time, id: id}); + {year: datetime.getUTCFullYear(), month: pad2(datetime.getUTCMonth() + 1), day: pad2(datetime.getUTCDate()), + hour: pad2(datetime.getUTCHours()), minute: pad2(datetime.getUTCMinutes()), + id: id}, + {interpolate: /\{(.+?)\}/g}); $thisSection.find('.section-published-date').html(html); hideModal(); saving.hide(); From 87ce16c70e19c22c36177f8021a56e7b584e08ca Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 6 Aug 2013 11:28:35 -0400 Subject: [PATCH 012/147] Cleanup/reformatting --- cms/static/js/views/overview.js | 2 +- cms/templates/edit_subsection.html | 9 ++++----- cms/templates/overview.html | 8 ++++---- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/cms/static/js/views/overview.js b/cms/static/js/views/overview.js index e41a6971a6..4ce0d3688e 100644 --- a/cms/static/js/views/overview.js +++ b/cms/static/js/views/overview.js @@ -148,7 +148,7 @@ function generateCheckHoverState(selectorsToOpen, selectorsToShove) { } }); - } + }; } function removeHesitate(event, ui) { diff --git a/cms/templates/edit_subsection.html b/cms/templates/edit_subsection.html index bdcaf18015..5b03643f3b 100644 --- a/cms/templates/edit_subsection.html +++ b/cms/templates/edit_subsection.html @@ -1,11 +1,10 @@ -<%! from django.utils.translation import ugettext as _ %> <%inherit file="base.html" /> <%! - import logging - from xmodule.util.date_utils import get_default_time_display, almost_same_datetime + import logging + from xmodule.util.date_utils import get_default_time_display, almost_same_datetime + from django.utils.translation import ugettext as _ + from django.core.urlresolvers import reverse %> - -<%! from django.core.urlresolvers import reverse %> <%block name="title">${_("CMS Subsection")} <%block name="bodyclass">is-signedin course subsection diff --git a/cms/templates/overview.html b/cms/templates/overview.html index 3795e9d09b..2c42df187a 100644 --- a/cms/templates/overview.html +++ b/cms/templates/overview.html @@ -1,10 +1,10 @@ -<%! from django.utils.translation import ugettext as _ %> <%inherit file="base.html" /> <%! - import logging - from xmodule.util import date_utils + import logging + from xmodule.util import date_utils + from django.utils.translation import ugettext as _ + from django.core.urlresolvers import reverse %> -<%! from django.core.urlresolvers import reverse %> <%block name="title">${_("Course Outline")} <%block name="bodyclass">is-signedin course outline From 9c09323b0d1bd6215dd8b880f534aae245b64993 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 6 Aug 2013 11:28:57 -0400 Subject: [PATCH 013/147] Properly parse and save section release times --- cms/static/js/base.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 7f82e67431..b2fcec84ae 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -313,9 +313,18 @@ function saveSubsection() { metadata[$(el).data("metadata-name")] = el.value; } - // Piece back together the date/time UI elements into one date/time string - metadata['start'] = getEdxTimeFromDateTimeInputs('start_date', 'start_time'); - metadata['due'] = getEdxTimeFromDateTimeInputs('due_date', 'due_time'); + // get datetimes for start and due, stick into metadata + _(["start", "due"]).each(function(name) { + var date, time; + date = $("#"+name+"_date").datepicker("getDate"); + time = $("#"+name+"_time").timepicker("getTime"); + if (date && time) { + metadata[name] = new Date(Date.UTC( + date.getFullYear(), date.getMonth(), date.getDate(), + time.getHours(), time.getMinutes() + )); + } + }); $.ajax({ url: "/save_item", From d2c4ac2597d6c97d9f29629111699f5ec4fc27d2 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 6 Aug 2013 14:30:19 -0400 Subject: [PATCH 014/147] Fixed Jasmine tests --- .../coffee/spec/views/overview_spec.coffee | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/cms/static/coffee/spec/views/overview_spec.coffee b/cms/static/coffee/spec/views/overview_spec.coffee index d900e4bfb1..8bda96da66 100644 --- a/cms/static/coffee/spec/views/overview_spec.coffee +++ b/cms/static/coffee/spec/views/overview_spec.coffee @@ -1,22 +1,19 @@ describe "Course Overview", -> beforeEach -> - appendSetFixtures """ - - """ - - appendSetFixtures """ - - """ + _.each ["/static/js/vendor/date.js", "/static/js/vendor/timepicker/jquery.timepicker.js", "/jsi18n/"], (path) -> + appendSetFixtures """ + + """ appendSetFixtures """ - """#" + """ appendSetFixtures """
@@ -38,7 +35,7 @@ describe "Course Overview", -> SaveCancel
- """#" + """ appendSetFixtures """
@@ -46,12 +43,13 @@ describe "Course Overview", ->
- """#" + """ spyOn(window, 'saveSetSectionScheduleDate').andCallThrough() # Have to do this here, as it normally gets bound in document.ready() $('a.save-button').click(saveSetSectionScheduleDate) $('a.delete-section-button').click(deleteSection) + $(".edit-subsection-publish-settings .start-date").datepicker() @notificationSpy = spyOn(CMS.Views.Notification.Mini.prototype, 'show').andCallThrough() window.analytics = jasmine.createSpyObj('analytics', ['track']) From c0bd7db2936c89f3125e10ac562a6bcfcbf00645 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 6 Aug 2013 16:47:23 -0400 Subject: [PATCH 015/147] Removed unused JS functions --- cms/static/js/base.js | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index b2fcec84ae..7db66a525d 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -259,23 +259,6 @@ function pad2(number) { return (number < 10 ? '0' : '') + number; } -function getEdxTimeFromDateTimeVals(date_val, time_val) { - if (date_val != '') { - if (time_val == '') time_val = '00:00'; - - return new Date(date_val + " " + time_val + "Z"); - } - - else return null; -} - -function getEdxTimeFromDateTimeInputs(date_id, time_id) { - var input_date = $('#' + date_id).val(); - var input_time = $('#' + time_id).val(); - - return getEdxTimeFromDateTimeVals(input_date, input_time); -} - function autosaveInput(e) { var self = this; if (this.saveTimer) { From aab9661fe962012f0d7f983e94d19302162be6b6 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 6 Aug 2013 16:48:09 -0400 Subject: [PATCH 016/147] Scoped pad2 function to the one place that it's called --- cms/static/js/base.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 7db66a525d..4a5fc2b182 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -253,12 +253,6 @@ function syncReleaseDate(e) { $("#start_time").val(""); } -function pad2(number) { - // pad a number to two places: useful for formatting months, days, hours, etc - // when displaying a date/time - return (number < 10 ? '0' : '') + number; -} - function autosaveInput(e) { var self = this; if (this.saveTimer) { @@ -802,6 +796,12 @@ function saveSetSectionScheduleDate(e) { } }) }).success(function() { + var pad2 = function(number) { + // pad a number to two places: useful for formatting months, days, hours, etc + // when displaying a date/time + return (number < 10 ? '0' : '') + number; + }; + var $thisSection = $('.courseware-section[data-id="' + id + '"]'); var html = _.template( '' + From ae019ef8c9f4d0ea67574ee3edf4a2b5c7f3d393 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 6 Aug 2013 17:09:12 -0400 Subject: [PATCH 017/147] Abstracted functionality to get datetime into separate JS function --- cms/static/js/base.js | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 4a5fc2b182..bb772da02b 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -253,6 +253,22 @@ function syncReleaseDate(e) { $("#start_time").val(""); } +function getDatetime(datepickerInput, timepickerInput) { + // given a pair of inputs (datepicker and timepicker), return a JS Date + // object that corresponds to the datetime that they represent. Assume + // UTC timezone, NOT the timezone of the user's browser. + var date = $(datepickerInput).datepicker("getDate"); + var time = $(timepickerInput).timepicker("getTime"); + if(date && time) { + return new Date(Date.UTC( + date.getFullYear(), date.getMonth(), date.getDate(), + time.getHours(), time.getMinutes() + )); + } else { + return null; + } +} + function autosaveInput(e) { var self = this; if (this.saveTimer) { @@ -292,14 +308,13 @@ function saveSubsection() { // get datetimes for start and due, stick into metadata _(["start", "due"]).each(function(name) { - var date, time; - date = $("#"+name+"_date").datepicker("getDate"); - time = $("#"+name+"_time").timepicker("getTime"); - if (date && time) { - metadata[name] = new Date(Date.UTC( - date.getFullYear(), date.getMonth(), date.getDate(), - time.getHours(), time.getMinutes() - )); + + var datetime = getDatetime( + document.getElementById(name+"_date"), + document.getElementById(name+"_time") + ); + if (datetime) { + metadata[name] = datetime; } }); @@ -764,12 +779,10 @@ function cancelSetSectionScheduleDate(e) { function saveSetSectionScheduleDate(e) { e.preventDefault(); - var date = $('.edit-subsection-publish-settings .start-date').datepicker("getDate"); - var time = $('.edit-subsection-publish-settings .start-time').timepicker("getTime"); - var datetime = new Date(Date.UTC( - date.getFullYear(), date.getMonth(), date.getDate(), - time.getHours(), time.getMinutes() - )); + var datetime = getDatetime( + $('.edit-subsection-publish-settings .start-date'), + $('.edit-subsection-publish-settings .start-time') + ); var id = $modal.attr('data-id'); From ee3ce7b6c20861509b6ae72287ccd03eb20fb2e1 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Wed, 7 Aug 2013 09:45:44 -0400 Subject: [PATCH 018/147] Don't ignore null datetimes on subsection settings Clicking "Sync to " should send an AJAX request with the datetimes set to null, so that the server resets them. --- cms/static/js/base.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index bb772da02b..80b24776da 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -313,9 +313,9 @@ function saveSubsection() { document.getElementById(name+"_date"), document.getElementById(name+"_time") ); - if (datetime) { - metadata[name] = datetime; - } + // if datetime is null, we want to set that in metadata anyway; + // its an indication to the server to clear the datetime in the DB + metadata[name] = datetime; }); $.ajax({ From e30a9a6fe33877a79a6047e3ff5a6cf9e858133d Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Wed, 7 Aug 2013 11:03:19 -0400 Subject: [PATCH 019/147] Created a new lettuce test to catch bug, updated other lettuce tests --- .../contentstore/features/section.feature | 2 +- .../contentstore/features/section.py | 13 ++-- .../contentstore/features/subsection.feature | 24 ++++++-- .../contentstore/features/subsection.py | 60 ++++++++++++++----- 4 files changed, 75 insertions(+), 24 deletions(-) diff --git a/cms/djangoapps/contentstore/features/section.feature b/cms/djangoapps/contentstore/features/section.feature index a08b490c6d..d9dd6f9398 100644 --- a/cms/djangoapps/contentstore/features/section.feature +++ b/cms/djangoapps/contentstore/features/section.feature @@ -24,7 +24,7 @@ Feature: Create Section Given I have opened a new course in Studio And I have added a new section When I click the Edit link for the release date - And I save a new section release date + And I set the section release date to 12/25/2013 Then the section release date is updated And I see a "saving" notification diff --git a/cms/djangoapps/contentstore/features/section.py b/cms/djangoapps/contentstore/features/section.py index 955c6a8f4e..3ca8e1676d 100644 --- a/cms/djangoapps/contentstore/features/section.py +++ b/cms/djangoapps/contentstore/features/section.py @@ -35,10 +35,15 @@ def i_click_the_edit_link_for_the_release_date(_step): world.css_click(button_css) -@step('I save a new section release date$') -def i_save_a_new_section_release_date(_step): - set_date_and_time('input.start-date.date.hasDatepicker', '12/25/2013', - 'input.start-time.time.ui-timepicker-input', '00:00') +@step('I set the section release date to ([0-9/-]+)( [0-9:]+)?') +def set_section_release_date(_step, datestring, timestring): + if hasattr(timestring, "strip"): + timestring = timestring.strip() + if not timestring: + timestring = "00:00" + set_date_and_time( + 'input.start-date.date.hasDatepicker', datestring, + 'input.start-time.time.ui-timepicker-input', timestring) world.browser.click_link_by_text('Save') diff --git a/cms/djangoapps/contentstore/features/subsection.feature b/cms/djangoapps/contentstore/features/subsection.feature index 9f5793dbe7..84755b3644 100644 --- a/cms/djangoapps/contentstore/features/subsection.feature +++ b/cms/djangoapps/contentstore/features/subsection.feature @@ -14,7 +14,7 @@ Feature: Create Subsection When I click the New Subsection link And I enter a subsection name with a quote and click save Then I see my subsection name with a quote on the Courseware page - And I click to edit the subsection name + And I click on the subsection Then I see the complete subsection name with a quote in the editor Scenario: Assign grading type to a subsection and verify it is still shown after refresh (bug #258) @@ -27,10 +27,13 @@ Feature: Create Subsection Scenario: Set a due date in a different year (bug #256) Given I have opened a new subsection in Studio - And I have set a release date and due date in different years - Then I see the correct dates + And I set the subsection release date to 12/25/2011 03:00 + And I set the subsection due date to 01/02/2012 04:00 + Then I see the subsection release date is 12/25/2011 03:00 + And I see the subsection due date is 01/02/2012 04:00 And I reload the page - Then I see the correct dates + Then I see the subsection release date is 12/25/2011 03:00 + And I see the subsection due date is 01/02/2012 04:00 Scenario: Delete a subsection Given I have opened a new course section in Studio @@ -40,3 +43,16 @@ Feature: Create Subsection And I press the "subsection" delete icon And I confirm the prompt Then the subsection does not exist + + Scenario: Sync to Section + Given I have opened a new course section in Studio + And I click the Edit link for the release date + And I set the section release date to 01/02/2103 + And I have added a new subsection + And I click on the subsection + And I set the subsection release date to 01/20/2103 + And I reload the page + And I click the link to sync release date to section + And I wait for "1" second + And I reload the page + Then I see the subsection release date is 01/02/2103 diff --git a/cms/djangoapps/contentstore/features/subsection.py b/cms/djangoapps/contentstore/features/subsection.py index e280ec615d..60a325f550 100644 --- a/cms/djangoapps/contentstore/features/subsection.py +++ b/cms/djangoapps/contentstore/features/subsection.py @@ -41,8 +41,8 @@ def i_save_subsection_name_with_quote(step): save_subsection_name('Subsection With "Quote"') -@step('I click to edit the subsection name$') -def i_click_to_edit_subsection_name(step): +@step('I click on the subsection$') +def click_on_subsection(step): world.css_click('span.subsection-name-value') @@ -53,12 +53,28 @@ def i_see_complete_subsection_name_with_quote_in_editor(step): assert_equal(world.css_value(css), 'Subsection With "Quote"') -@step('I have set a release date and due date in different years$') -def test_have_set_dates_in_different_years(step): - set_date_and_time('input#start_date', '12/25/2011', 'input#start_time', '03:00') - world.css_click('.set-date') - # Use a year in the past so that current year will always be different. - set_date_and_time('input#due_date', '01/02/2012', 'input#due_time', '04:00') +@step('I set the subsection release date to ([0-9/-]+)( [0-9:]+)?') +def set_subsection_release_date(_step, datestring, timestring): + if hasattr(timestring, "strip"): + timestring = timestring.strip() + if not timestring: + timestring = "00:00" + set_date_and_time( + 'input#start_date', datestring, + 'input#start_time', timestring) + + +@step('I set the subsection due date to ([0-9/-]+)( [0-9:]+)?') +def set_subsection_due_date(_step, datestring, timestring): + if hasattr(timestring, "strip"): + timestring = timestring.strip() + if not timestring: + timestring = "00:00" + if not world.css_visible('input#due_date'): + world.css_click('.due-date-input .set-date') + set_date_and_time( + 'input#due_date', datestring, + 'input#due_time', timestring) @step('I mark it as Homework$') @@ -72,6 +88,11 @@ def i_see_it_marked__as_homework(step): assert_equal(world.css_value(".status-label"), 'Homework') +@step('I click the link to sync release date to section') +def click_sync_release_date(step): + world.css_click('.sync-date') + + ############ ASSERTIONS ################### @@ -91,16 +112,25 @@ def the_subsection_does_not_exist(step): assert world.browser.is_element_not_present_by_css(css) -@step('I see the correct dates$') -def i_see_the_correct_dates(step): - assert_equal('12/25/2011', get_date('input#start_date')) - assert_equal('03:00', get_date('input#start_time')) - assert_equal('01/02/2012', get_date('input#due_date')) - assert_equal('04:00', get_date('input#due_time')) +@step('I see the subsection release date is ([0-9/-]+)( [0-9:]+)?') +def i_see_subsection_release(_step, datestring, timestring): + if hasattr(timestring, "strip"): + timestring = timestring.strip() + assert_equal(datestring, get_date('input#start_date')) + if timestring: + assert_equal(timestring, get_date('input#start_time')) + + +@step('I see the subsection due date is ([0-9/-]+)( [0-9:]+)?') +def i_see_subsection_due(_step, datestring, timestring): + if hasattr(timestring, "strip"): + timestring = timestring.strip() + assert_equal(datestring, get_date('input#due_date')) + if timestring: + assert_equal(timestring, get_date('input#due_time')) ############ HELPER METHODS ################### - def get_date(css): return world.css_find(css).first.value.strip() From a19c1a3c202c6fcc006ea0b6c25df90c14b330fb Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Wed, 7 Aug 2013 15:03:23 -0400 Subject: [PATCH 020/147] Make the virtualenvs on jenkins use site-packages for numpy, scipy, etc. --- jenkins/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkins/test.sh b/jenkins/test.sh index 60dd59f7c0..0c12602bed 100755 --- a/jenkins/test.sh +++ b/jenkins/test.sh @@ -55,7 +55,7 @@ VIRTUALENV_DIR="/mnt/virtualenvs/${JOB_NAME}${WORKSPACE_SUFFIX}" if [ ! -d "$VIRTUALENV_DIR" ]; then mkdir -p "$VIRTUALENV_DIR" - virtualenv "$VIRTUALENV_DIR" + virtualenv --system-site-packages "$VIRTUALENV_DIR" fi export PIP_DOWNLOAD_CACHE=/mnt/pip-cache From df25770fa714d7349ff1d87e32bcaffb837071ba Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 6 Aug 2013 16:39:02 -0400 Subject: [PATCH 021/147] Assign isExternal JS function to window object When JS functions are defined with names, they are local variables, and inaccessible if defined inside a closure. Django-Pipeline concatenates all of our JS into one big closure. This function explicitly assings the function to a property of the `window` object, so that it is accessible to other JS functions. --- common/static/js/utility.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/common/static/js/utility.js b/common/static/js/utility.js index 6407faad48..a983535b46 100644 --- a/common/static/js/utility.js +++ b/common/static/js/utility.js @@ -1,20 +1,20 @@ // checks whether or not the url is external to the local site. // generously provided by StackOverflow: http://stackoverflow.com/questions/6238351/fastest-way-to-detect-external-urls -function isExternal(url) { +window.isExternal = function (url) { // parse the url into protocol, host, path, query, and fragment. More information can be found here: http://tools.ietf.org/html/rfc3986#appendix-B var match = url.match(/^([^:\/?#]+:)?(?:\/\/([^\/?#]*))?([^?#]+)?(\?[^#]*)?(#.*)?/); // match[1] matches a protocol if one exists in the url // if the protocol in the url does not match the protocol in the window's location, this url is considered external - if (typeof match[1] === "string" && - match[1].length > 0 - && match[1].toLowerCase() !== location.protocol) + if (typeof match[1] === "string" && + match[1].length > 0 && + match[1].toLowerCase() !== location.protocol) return true; // match[2] matches the host if one exists in the url // if the host in the url does not match the host of the window location, this url is considered external - if (typeof match[2] === "string" && - match[2].length > 0 && + if (typeof match[2] === "string" && + match[2].length > 0 && // this regex removes the port number if it patches the current location's protocol - match[2].replace(new RegExp(":("+{"http:":80,"https:":443}[location.protocol]+")?$"), "") !== location.host) + match[2].replace(new RegExp(":("+{"http:":80,"https:":443}[location.protocol]+")?$"), "") !== location.host) return true; return false; -} +}; From 1be6ce3ee387ead051b001112c0ef68a7a172b03 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 8 Aug 2013 08:34:08 -0400 Subject: [PATCH 022/147] Add in and route control options --- .../xmodule/combined_open_ended_module.py | 33 +++++++++++++++++-- .../combined_open_ended_modulev1.py | 11 +++++++ .../open_ended_module.py | 1 + .../openendedchild.py | 1 + 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index e01ae49149..f8ae7a3f13 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -14,7 +14,8 @@ import textwrap log = logging.getLogger("mitx.courseware") V1_SETTINGS_ATTRIBUTES = ["display_name", "max_attempts", "graded", "accept_file_upload", - "skip_spelling_checks", "due", "graceperiod", "weight"] + "skip_spelling_checks", "due", "graceperiod", "weight", "min_to_calibrate", + "max_to_calibrate", "peer_grader_count", "required_peer_grading"] V1_STUDENT_ATTRIBUTES = ["current_task_number", "task_states", "state", "student_attempts", "ready_to_reset"] @@ -37,7 +38,7 @@ DEFAULT_DATA = textwrap.dedent("""\

- Write a persuasive essay to a newspaper reflecting your vies on censorship in libraries. Do you believe that certain materials, such as books, music, movies, magazines, etc., should be removed from the shelves if they are found offensive? Support your position with convincing arguments from your own experience, observations, and/or reading. + Write a persuasive essay to a newspaper reflecting your views on censorship in libraries. Do you believe that certain materials, such as books, music, movies, magazines, etc., should be removed from the shelves if they are found offensive? Support your position with convincing arguments from your own experience, observations, and/or reading.

@@ -244,6 +245,34 @@ class CombinedOpenEndedFields(object): values={"min" : 0 , "step": ".1"}, default=1 ) + min_to_calibrate = Integer( + display_name="Minimum Peer Grading Calibrations", + help="The minimum number of calibration essays each student will need to complete for peer grading.", + default=1, + scope=Scope.settings, + values={"min" : 1, "step" : "1"} + ) + max_to_calibrate = Integer( + display_name="Maximum Peer Grading Calibrations", + help="The maximum number of calibration essays each student will need to complete for peer grading.", + default=1, + scope=Scope.settings, + values={"max" : 20, "step" : "1"} + ) + peer_grader_count = Integer( + display_name="Peer Graders per Response", + help="The number of peers who will grade each submission.", + default=1, + scope=Scope.settings, + values={"min" : 1, "step" : "1", "max" : 5} + ) + required_peer_grading = Integer( + display_name="Required Peer Grading", + help="The number of other students each student making a submission will have to grade.", + default=1, + scope=Scope.settings, + values={"min" : 1, "step" : "1", "max" : 5} + ) markdown = String( help="Markdown source of this module", default=textwrap.dedent("""\ diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py index 933eb0b5bb..c65d30968d 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py @@ -106,6 +106,11 @@ class CombinedOpenEndedV1Module(): self.accept_file_upload = instance_state.get('accept_file_upload', ACCEPT_FILE_UPLOAD) in TRUE_DICT self.skip_basic_checks = instance_state.get('skip_spelling_checks', SKIP_BASIC_CHECKS) in TRUE_DICT + self.required_peer_grading = instance_state.get('required_peer_grading', 3) + self.peer_grader_count = instance_state.get('peer_grader_count', 3) + self.min_to_calibrate = instance_state.get('min_to_calibrate', 3) + self.max_to_calibrate = instance_state.get('max_to_calibrate', 6) + due_date = instance_state.get('due', None) grace_period_string = instance_state.get('graceperiod', None) @@ -131,6 +136,12 @@ class CombinedOpenEndedV1Module(): 'close_date': self.timeinfo.close_date, 's3_interface': self.system.s3_interface, 'skip_basic_checks': self.skip_basic_checks, + 'control': { + 'required_peer_grading': self.required_peer_grading, + 'peer_grader_count': self.peer_grader_count, + 'min_to_calibrate': self.min_to_calibrate, + 'max_to_calibrate': self.max_to_calibrate, + } } self.task_xml = definition['task_xml'] diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py index 2e7a3eaf89..924ca2c23d 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py @@ -118,6 +118,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): 'answer': self.answer, 'problem_id': self.display_name, 'skip_basic_checks': self.skip_basic_checks, + 'control': json.dumps(self.control), }) updated_grader_payload = json.dumps(parsed_grader_payload) diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py index 10f939b270..7138dcc723 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py @@ -92,6 +92,7 @@ class OpenEndedChild(object): self.s3_interface = static_data['s3_interface'] self.skip_basic_checks = static_data['skip_basic_checks'] self._max_score = static_data['max_score'] + self.control = static_data['control'] # Used for progress / grading. Currently get credit just for # completion (doesn't matter if you self-assessed correct/incorrect). From 4896444d10ec650f1c44e4c9a4c01178b61114c7 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Fri, 2 Aug 2013 09:29:55 -0400 Subject: [PATCH 023/147] Clean up item views, use JsonResponse class --- cms/djangoapps/contentstore/views/item.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index efebded9b9..ff347a2878 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -1,15 +1,13 @@ -import json from uuid import uuid4 from django.core.exceptions import PermissionDenied -from django.http import HttpResponse from django.contrib.auth.decorators import login_required from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore from xmodule.modulestore.inheritance import own_metadata -from util.json_request import expect_json +from util.json_request import expect_json, JsonResponse from ..utils import get_modulestore from .access import has_access from .requests import _xmodule_recurse @@ -20,6 +18,7 @@ __all__ = ['save_item', 'create_item', 'delete_item'] # cdodge: these are categories which should not be parented, they are detached from the hierarchy DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info'] + @login_required @expect_json def save_item(request): @@ -80,7 +79,7 @@ def save_item(request): # commit to datastore store.update_metadata(item_location, own_metadata(existing_item)) - return HttpResponse() + return JsonResponse() # [DHM] A hack until we implement a permanent soln. Proposed perm solution is to make namespace fields also top level @@ -139,13 +138,17 @@ def create_item(request): if display_name is not None: metadata['display_name'] = display_name - get_modulestore(category).create_and_save_xmodule(dest_location, definition_data=data, - metadata=metadata, system=parent.system) + get_modulestore(category).create_and_save_xmodule( + dest_location, + definition_data=data, + metadata=metadata, + system=parent.system, + ) if category not in DETACHED_CATEGORIES: get_modulestore(parent.location).update_children(parent_location, parent.children + [dest_location.url()]) - return HttpResponse(json.dumps({'id': dest_location.url()})) + return JsonResponse({'id': dest_location.url()}) @login_required @@ -184,4 +187,4 @@ def delete_item(request): parent.children = children modulestore('direct').update_children(parent.location, parent.children) - return HttpResponse() + return JsonResponse() From be103dfa0d3e758cec8c25eb08181d0570e315a9 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Wed, 31 Jul 2013 12:50:22 -0400 Subject: [PATCH 024/147] improving code style --- common/lib/xmodule/xmodule/x_module.py | 70 +++++++++++--------------- 1 file changed, 29 insertions(+), 41 deletions(-) diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 3556f3f0f3..310a871b72 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -711,20 +711,20 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): # =============================== BUILTIN METHODS ========================== def __eq__(self, other): - eq = (self.__class__ == other.__class__ and + return (self.__class__ == other.__class__ and all(getattr(self, attr, None) == getattr(other, attr, None) for attr in self.equality_attributes)) - return eq - def __repr__(self): - return ("{class_}({system!r}, location={location!r}," - " model_data={model_data!r})".format( - class_=self.__class__.__name__, - system=self.system, - location=self.location, - model_data=self._model_data, - )) + return ( + "{class_}({system!r}, location={location!r}," + " model_data={model_data!r})".format( + class_=self.__class__.__name__, + system=self.system, + location=self.location, + model_data=self._model_data, + ) + ) @property def non_editable_metadata_fields(self): @@ -785,15 +785,17 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): editor_type = "Float" elif isinstance(field, List): editor_type = "List" - metadata_fields[field.name] = {'field_name': field.name, - 'type': editor_type, - 'display_name': field.display_name, - 'value': field.to_json(value), - 'options': [] if values is None else values, - 'default_value': field.to_json(default_value), - 'inheritable': inheritable, - 'explicitly_set': explicitly_set, - 'help': field.help} + metadata_fields[field.name] = { + 'field_name': field.name, + 'type': editor_type, + 'display_name': field.display_name, + 'value': field.to_json(value), + 'options': [] if values is None else values, + 'default_value': field.to_json(default_value), + 'inheritable': inheritable, + 'explicitly_set': explicitly_set, + 'help': field.help, + } return metadata_fields @@ -885,28 +887,14 @@ class ModuleSystem(Runtime): Note that these functions can be closures over e.g. a django request and user, or other environment-specific info. ''' - def __init__(self, - ajax_url, - track_function, - get_module, - render_template, - replace_urls, - xblock_model_data, - user=None, - filestore=None, - debug=False, - xqueue=None, - publish=None, - node_path="", - anonymous_student_id='', - course_id=None, - open_ended_grading_interface=None, - s3_interface=None, - cache=None, - can_execute_unsafe_code=None, - replace_course_urls=None, - replace_jump_to_id_urls=None - ): + def __init__( + self, ajax_url, track_function, get_module, render_template, + replace_urls, xblock_model_data, user=None, filestore=None, + debug=False, xqueue=None, publish=None, node_path="", + anonymous_student_id='', course_id=None, + open_ended_grading_interface=None, s3_interface=None, + cache=None, can_execute_unsafe_code=None, replace_course_urls=None, + replace_jump_to_id_urls=None): ''' Create a closure around the system environment. From 9634e222bed244d3310f449faa137a029493977b Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Wed, 31 Jul 2013 12:51:01 -0400 Subject: [PATCH 025/147] Refactored get_module_previews function --- cms/djangoapps/contentstore/views/preview.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index f2a07abe32..a9c9757d1d 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -163,6 +163,11 @@ def load_preview_module(request, preview_id, descriptor): return module +def get_preview_html(request, descriptor, idx): + module = load_preview_module(request, str(idx), descriptor) + return module.get_html() + + def get_module_previews(request, descriptor): """ Returns a list of preview XModule html contents. One preview is returned for each @@ -170,8 +175,5 @@ def get_module_previews(request, descriptor): descriptor: An XModuleDescriptor """ - preview_html = [] - for idx, (_instance_state, _shared_state) in enumerate(descriptor.get_sample_state()): - module = load_preview_module(request, str(idx), descriptor) - preview_html.append(module.get_html()) - return preview_html + return tuple(get_preview_html(request, descriptor, idx) + for idx in range(len(descriptor.get_sample_state()))) From 8a95d7e6f0c72fe9836f01a209e6eb1d4ca4bab4 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Fri, 2 Aug 2013 13:51:46 -0400 Subject: [PATCH 026/147] XBlock integration: replaced `get_html` with `runtime.render()` Currently calls the same machinery, but re-routes the logic in preparation of deeper integration with XBlock --- cms/djangoapps/contentstore/views/preview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index a9c9757d1d..fa55cb2c24 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -165,7 +165,7 @@ def load_preview_module(request, preview_id, descriptor): def get_preview_html(request, descriptor, idx): module = load_preview_module(request, str(idx), descriptor) - return module.get_html() + return module.runtime.render(module, None, "student_view") def get_module_previews(request, descriptor): From 3fa990ea60dc3c704c82eea3539d1a71a5eafbd3 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 5 Aug 2013 09:38:51 -0400 Subject: [PATCH 027/147] Made some tests more general, less fragile --- .../contentstore/tests/test_contentstore.py | 30 +++++++++---------- .../contentstore/tests/test_item.py | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 838af2cafa..64fa53433e 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -1237,7 +1237,7 @@ class ContentStoreTest(ModuleStoreTestCase): 'course': loc.course, 'name': loc.name})) - self.assertEqual(200, resp.status_code) + self.assert2XX(resp.status_code) self.assertContains(resp, 'Chapter 2') # go to various pages @@ -1247,92 +1247,92 @@ class ContentStoreTest(ModuleStoreTestCase): kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) - self.assertEqual(200, resp.status_code) + self.assert2XX(resp.status_code) # export page resp = self.client.get(reverse('export_course', kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) - self.assertEqual(200, resp.status_code) + self.assert2XX(resp.status_code) # manage users resp = self.client.get(reverse('manage_users', kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) - self.assertEqual(200, resp.status_code) + self.assert2XX(resp.status_code) # course info resp = self.client.get(reverse('course_info', kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) - self.assertEqual(200, resp.status_code) + self.assert2XX(resp.status_code) # settings_details resp = self.client.get(reverse('settings_details', kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) - self.assertEqual(200, resp.status_code) + self.assert2XX(resp.status_code) # settings_details resp = self.client.get(reverse('settings_grading', kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) - self.assertEqual(200, resp.status_code) + self.assert2XX(resp.status_code) # static_pages resp = self.client.get(reverse('static_pages', kwargs={'org': loc.org, 'course': loc.course, 'coursename': loc.name})) - self.assertEqual(200, resp.status_code) + self.assert2XX(resp.status_code) # static_pages resp = self.client.get(reverse('asset_index', kwargs={'org': loc.org, 'course': loc.course, 'name': loc.name})) - self.assertEqual(200, resp.status_code) + self.assert2XX(resp.status_code) # go look at a subsection page subsection_location = loc.replace(category='sequential', name='test_sequence') resp = self.client.get(reverse('edit_subsection', kwargs={'location': subsection_location.url()})) - self.assertEqual(200, resp.status_code) + self.assert2XX(resp.status_code) # go look at the Edit page unit_location = loc.replace(category='vertical', name='test_vertical') resp = self.client.get(reverse('edit_unit', kwargs={'location': unit_location.url()})) - self.assertEqual(200, resp.status_code) + self.assert2XX(resp.status_code) # delete a component del_loc = loc.replace(category='html', name='test_html') resp = self.client.post(reverse('delete_item'), json.dumps({'id': del_loc.url()}), "application/json") - self.assertEqual(200, resp.status_code) + self.assert2XX(resp.status_code) # delete a unit del_loc = loc.replace(category='vertical', name='test_vertical') resp = self.client.post(reverse('delete_item'), json.dumps({'id': del_loc.url()}), "application/json") - self.assertEqual(200, resp.status_code) + self.assert2XX(resp.status_code) # delete a unit del_loc = loc.replace(category='sequential', name='test_sequence') resp = self.client.post(reverse('delete_item'), json.dumps({'id': del_loc.url()}), "application/json") - self.assertEqual(200, resp.status_code) + self.assert2XX(resp.status_code) # delete a chapter del_loc = loc.replace(category='chapter', name='chapter_2') resp = self.client.post(reverse('delete_item'), json.dumps({'id': del_loc.url()}), "application/json") - self.assertEqual(200, resp.status_code) + self.assert2XX(resp.status_code) def test_import_metadata_with_attempts_empty_string(self): module_store = modulestore('direct') diff --git a/cms/djangoapps/contentstore/tests/test_item.py b/cms/djangoapps/contentstore/tests/test_item.py index 827dd1b054..260444a8f7 100644 --- a/cms/djangoapps/contentstore/tests/test_item.py +++ b/cms/djangoapps/contentstore/tests/test_item.py @@ -34,7 +34,7 @@ class DeleteItem(CourseTestCase): resp.content, "application/json" ) - self.assertEqual(resp.status_code, 200) + self.assert2XX(resp.status_code) class TestCreateItem(CourseTestCase): From baa9bd5bdca69c358f2f4e81e4632febe2f6019f Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 5 Aug 2013 11:04:23 -0400 Subject: [PATCH 028/147] Make sure to return the content, not the fragment --- cms/djangoapps/contentstore/views/preview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index fa55cb2c24..202927bdfb 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -165,7 +165,7 @@ def load_preview_module(request, preview_id, descriptor): def get_preview_html(request, descriptor, idx): module = load_preview_module(request, str(idx), descriptor) - return module.runtime.render(module, None, "student_view") + return module.runtime.render(module, None, "student_view").content def get_module_previews(request, descriptor): From a87a1bfcdab0fa42d169f630e9eab85137e50e29 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Wed, 7 Aug 2013 16:14:07 -0400 Subject: [PATCH 029/147] Docstrings --- cms/djangoapps/contentstore/views/preview.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index 202927bdfb..1fef30dd99 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -164,13 +164,16 @@ def load_preview_module(request, preview_id, descriptor): def get_preview_html(request, descriptor, idx): + """ + Returns the HTML for the XModule specified by the descriptor and idx. + """ module = load_preview_module(request, str(idx), descriptor) return module.runtime.render(module, None, "student_view").content def get_module_previews(request, descriptor): """ - Returns a list of preview XModule html contents. One preview is returned for each + Returns a tuple of preview XModule html contents. One preview is returned for each pair of states returned by get_sample_state() for the supplied descriptor. descriptor: An XModuleDescriptor From 4b5aba29ca3fc0b24ea4195bf5cf5f50065ff7db Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 8 Aug 2013 09:52:13 -0400 Subject: [PATCH 030/147] Fix defaults --- common/lib/xmodule/xmodule/combined_open_ended_module.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index f8ae7a3f13..faf22d1926 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -248,28 +248,28 @@ class CombinedOpenEndedFields(object): min_to_calibrate = Integer( display_name="Minimum Peer Grading Calibrations", help="The minimum number of calibration essays each student will need to complete for peer grading.", - default=1, + default=3, scope=Scope.settings, values={"min" : 1, "step" : "1"} ) max_to_calibrate = Integer( display_name="Maximum Peer Grading Calibrations", help="The maximum number of calibration essays each student will need to complete for peer grading.", - default=1, + default=6, scope=Scope.settings, values={"max" : 20, "step" : "1"} ) peer_grader_count = Integer( display_name="Peer Graders per Response", help="The number of peers who will grade each submission.", - default=1, + default=3, scope=Scope.settings, values={"min" : 1, "step" : "1", "max" : 5} ) required_peer_grading = Integer( display_name="Required Peer Grading", help="The number of other students each student making a submission will have to grade.", - default=1, + default=3, scope=Scope.settings, values={"min" : 1, "step" : "1", "max" : 5} ) From 7aec95c3100132149e9f84f6d8e58927f78655e7 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 8 Aug 2013 09:52:39 -0400 Subject: [PATCH 031/147] Removed get_module_previews function According to @cpennington, no modules return anything for `get_sample_state`, so this function is extraneous. --- cms/djangoapps/contentstore/views/preview.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index 1fef30dd99..a325dd3b34 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -76,7 +76,7 @@ def preview_component(request, location): component = modulestore().get_item(location) return render_to_response('component.html', { - 'preview': get_module_previews(request, component)[0], + 'preview': get_preview_html(request, component, 0), 'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(), }) @@ -169,14 +169,3 @@ def get_preview_html(request, descriptor, idx): """ module = load_preview_module(request, str(idx), descriptor) return module.runtime.render(module, None, "student_view").content - - -def get_module_previews(request, descriptor): - """ - Returns a tuple of preview XModule html contents. One preview is returned for each - pair of states returned by get_sample_state() for the supplied descriptor. - - descriptor: An XModuleDescriptor - """ - return tuple(get_preview_html(request, descriptor, idx) - for idx in range(len(descriptor.get_sample_state()))) From 32f76988c6bb3128da5b3c804bc305f8bc7281d0 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 8 Aug 2013 09:53:19 -0400 Subject: [PATCH 032/147] Update docstring --- cms/djangoapps/contentstore/views/preview.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index a325dd3b34..121bf98393 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -165,7 +165,8 @@ def load_preview_module(request, preview_id, descriptor): def get_preview_html(request, descriptor, idx): """ - Returns the HTML for the XModule specified by the descriptor and idx. + Returns the HTML returned by the XModule's student_view, + specified by the descriptor and idx. """ module = load_preview_module(request, str(idx), descriptor) return module.runtime.render(module, None, "student_view").content From aaceb288a70de89f118e545c5220eaae9c809b04 Mon Sep 17 00:00:00 2001 From: Miles Steele Date: Wed, 31 Jul 2013 15:59:35 -0400 Subject: [PATCH 033/147] fix mysql indexing validity in migrations --- .../student/migrations/0023_add_test_center_registration.py | 4 ++-- .../student/migrations/0024_add_allow_certificate.py | 2 +- ...025_auto__add_field_courseenrollmentallowed_auto_enroll.py | 2 +- common/djangoapps/student/models.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/common/djangoapps/student/migrations/0023_add_test_center_registration.py b/common/djangoapps/student/migrations/0023_add_test_center_registration.py index 4c7de6dcd9..6186f5deef 100644 --- a/common/djangoapps/student/migrations/0023_add_test_center_registration.py +++ b/common/djangoapps/student/migrations/0023_add_test_center_registration.py @@ -21,7 +21,7 @@ class Migration(SchemaMigration): ('eligibility_appointment_date_first', self.gf('django.db.models.fields.DateField')(db_index=True)), ('eligibility_appointment_date_last', self.gf('django.db.models.fields.DateField')(db_index=True)), ('accommodation_code', self.gf('django.db.models.fields.CharField')(max_length=64, blank=True)), - ('accommodation_request', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=1024, blank=True)), + ('accommodation_request', self.gf('django.db.models.fields.CharField')(db_index=False, max_length=1024, blank=True)), ('uploaded_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)), ('processed_at', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)), ('upload_status', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=20, blank=True)), @@ -163,7 +163,7 @@ class Migration(SchemaMigration): 'student.testcenterregistration': { 'Meta': {'object_name': 'TestCenterRegistration'}, 'accommodation_code': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), - 'accommodation_request': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '1024', 'blank': 'True'}), + 'accommodation_request': ('django.db.models.fields.CharField', [], {'db_index': 'False', 'max_length': '1024', 'blank': 'True'}), 'authorization_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}), 'client_authorization_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20', 'db_index': 'True'}), 'confirmed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}), diff --git a/common/djangoapps/student/migrations/0024_add_allow_certificate.py b/common/djangoapps/student/migrations/0024_add_allow_certificate.py index 56eccf8d70..5753f0176e 100644 --- a/common/djangoapps/student/migrations/0024_add_allow_certificate.py +++ b/common/djangoapps/student/migrations/0024_add_allow_certificate.py @@ -93,7 +93,7 @@ class Migration(SchemaMigration): 'student.testcenterregistration': { 'Meta': {'object_name': 'TestCenterRegistration'}, 'accommodation_code': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), - 'accommodation_request': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '1024', 'blank': 'True'}), + 'accommodation_request': ('django.db.models.fields.CharField', [], {'db_index': 'False', 'max_length': '1024', 'blank': 'True'}), 'authorization_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}), 'client_authorization_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20', 'db_index': 'True'}), 'confirmed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}), diff --git a/common/djangoapps/student/migrations/0025_auto__add_field_courseenrollmentallowed_auto_enroll.py b/common/djangoapps/student/migrations/0025_auto__add_field_courseenrollmentallowed_auto_enroll.py index 8ce1d0cda1..1cb21e9b33 100644 --- a/common/djangoapps/student/migrations/0025_auto__add_field_courseenrollmentallowed_auto_enroll.py +++ b/common/djangoapps/student/migrations/0025_auto__add_field_courseenrollmentallowed_auto_enroll.py @@ -94,7 +94,7 @@ class Migration(SchemaMigration): 'student.testcenterregistration': { 'Meta': {'object_name': 'TestCenterRegistration'}, 'accommodation_code': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), - 'accommodation_request': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '1024', 'blank': 'True'}), + 'accommodation_request': ('django.db.models.fields.CharField', [], {'db_index': 'False', 'max_length': '1024', 'blank': 'True'}), 'authorization_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}), 'client_authorization_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20', 'db_index': 'True'}), 'confirmed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}), diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 4c41427ca6..34278c5581 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -370,7 +370,7 @@ class TestCenterRegistration(models.Model): accommodation_code = models.CharField(max_length=64, blank=True) # store the original text of the accommodation request. - accommodation_request = models.CharField(max_length=1024, blank=True, db_index=True) + accommodation_request = models.CharField(max_length=1024, blank=True, db_index=False) # time at which edX sent the registration to the test center uploaded_at = models.DateTimeField(null=True, db_index=True) From 090f8d812f75690f31c031d36d7af99262801255 Mon Sep 17 00:00:00 2001 From: Miles Steele Date: Wed, 7 Aug 2013 15:24:09 -0400 Subject: [PATCH 034/147] add migration to remove index --- ...enterregistration_accommodation_request.py | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 common/djangoapps/student/migrations/0026_auto__remove_index_student_testcenterregistration_accommodation_request.py diff --git a/common/djangoapps/student/migrations/0026_auto__remove_index_student_testcenterregistration_accommodation_request.py b/common/djangoapps/student/migrations/0026_auto__remove_index_student_testcenterregistration_accommodation_request.py new file mode 100644 index 0000000000..23fc476348 --- /dev/null +++ b/common/djangoapps/student/migrations/0026_auto__remove_index_student_testcenterregistration_accommodation_request.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models +from django.db.utils import DatabaseError + + +class Migration(SchemaMigration): + """ + Remove an unwanted index from environments that have it. + This is a one-way migration in that backwards is a no-op and will not undo the removal. + This migration is only relevant to dev environments that existed before a migration rewrite + which removed the creation of this index. + """ + + def forwards(self, orm): + try: + # Removing index on 'TestCenterRegistration', fields ['accommodation_request'] + db.delete_index('student_testcenterregistration', ['accommodation_request']) + except DatabaseError: + print "-- skipping delete_index of student_testcenterregistration.accommodation_request (index does not exist)" + + + def backwards(self, orm): + pass + + + 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'}) + }, + 'student.courseenrollment': { + 'Meta': {'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'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'student.courseenrollmentallowed': { + 'Meta': {'unique_together': "(('email', 'course_id'),)", 'object_name': 'CourseEnrollmentAllowed'}, + 'auto_enroll': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + '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'}), + 'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'student.pendingemailchange': { + 'Meta': {'object_name': 'PendingEmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.pendingnamechange': { + 'Meta': {'object_name': 'PendingNameChange'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.registration': { + 'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.testcenterregistration': { + 'Meta': {'object_name': 'TestCenterRegistration'}, + 'accommodation_code': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'accommodation_request': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'authorization_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}), + 'client_authorization_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20', 'db_index': 'True'}), + 'confirmed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}), + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'eligibility_appointment_date_first': ('django.db.models.fields.DateField', [], {'db_index': 'True'}), + 'eligibility_appointment_date_last': ('django.db.models.fields.DateField', [], {'db_index': 'True'}), + 'exam_series_code': ('django.db.models.fields.CharField', [], {'max_length': '15', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'processed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}), + 'testcenter_user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['student.TestCenterUser']"}), + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'upload_error_message': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}), + 'upload_status': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}), + 'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}), + 'user_updated_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}) + }, + 'student.testcenteruser': { + 'Meta': {'object_name': 'TestCenterUser'}, + 'address_1': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'address_2': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), + 'address_3': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), + 'candidate_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}), + 'city': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + 'client_candidate_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50', 'db_index': 'True'}), + 'company_name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '50', 'blank': 'True'}), + 'confirmed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}), + 'country': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'extension': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '8', 'blank': 'True'}), + 'fax': ('django.db.models.fields.CharField', [], {'max_length': '35', 'blank': 'True'}), + 'fax_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'middle_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'phone': ('django.db.models.fields.CharField', [], {'max_length': '35'}), + 'phone_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}), + 'postal_code': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '16', 'blank': 'True'}), + 'processed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}), + 'salutation': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'state': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}), + 'suffix': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'upload_error_message': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}), + 'upload_status': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}), + 'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'unique': 'True'}), + 'user_updated_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}) + }, + 'student.userprofile': { + 'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"}, + 'allow_certificate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}), + 'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}), + 'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}), + 'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'student.usertestgroup': { + 'Meta': {'object_name': 'UserTestGroup'}, + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'}) + } + } + + complete_apps = ['student'] From 35ffb1b34721809431601ebcd7bb17bf3579be46 Mon Sep 17 00:00:00 2001 From: Miles Steele Date: Fri, 2 Aug 2013 15:56:01 -0400 Subject: [PATCH 035/147] add spacing to student admin section --- lms/static/sass/course/instructor/_instructor_2.scss | 4 ++++ .../instructor_dashboard_2/student_admin.html | 11 +++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lms/static/sass/course/instructor/_instructor_2.scss b/lms/static/sass/course/instructor/_instructor_2.scss index 58ac22c7b9..b70b2c781b 100644 --- a/lms/static/sass/course/instructor/_instructor_2.scss +++ b/lms/static/sass/course/instructor/_instructor_2.scss @@ -269,6 +269,10 @@ section.instructor-dashboard-content-2 { .instructor-dashboard-wrapper-2 section.idash-section#student_admin > { + .action-type-container{ + margin-bottom: $baseline * 2; + } + .progress-link-wrapper { margin-top: 0.7em; } diff --git a/lms/templates/instructor/instructor_dashboard_2/student_admin.html b/lms/templates/instructor/instructor_dashboard_2/student_admin.html index a24288f4de..bf99fcea57 100644 --- a/lms/templates/instructor/instructor_dashboard_2/student_admin.html +++ b/lms/templates/instructor/instructor_dashboard_2/student_admin.html @@ -1,6 +1,6 @@ <%page args="section_data"/> -
+

Student-specific grade adjustment

@@ -47,12 +47,11 @@
%endif +
%if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS') and section_data['access']['instructor']: -
- -
+

Course-specific grade adjustment

@@ -81,9 +80,9 @@

-
-
+
+

Pending Instructor Tasks

From fb8c84a5162758ee578a4b85706cde8effd4a317 Mon Sep 17 00:00:00 2001 From: Miles Steele Date: Thu, 1 Aug 2013 10:56:47 -0400 Subject: [PATCH 036/147] add analytics proxy endpoint --- lms/djangoapps/instructor/tests/test_api.py | 92 +++++++++++++++++++++ lms/djangoapps/instructor/views/api.py | 57 ++++++++++++- lms/djangoapps/instructor/views/api_urls.py | 2 + 3 files changed, 150 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index cc2e23e8fe..32682d2b61 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -5,6 +5,7 @@ Unit tests for instructor.api methods. import unittest import json from urllib import quote +from django.conf import settings from django.test import TestCase from nose.tools import raises from mock import Mock @@ -23,6 +24,7 @@ from student.models import CourseEnrollment from courseware.models import StudentModule from instructor.access import allow_access +import instructor.views.api from instructor.views.api import ( _split_input_list, _msk_from_problem_urlname, common_exceptions_400) from instructor_task.api_helper import AlreadyRunningError @@ -118,6 +120,7 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase): 'list_instructor_tasks', 'list_forum_members', 'update_forum_role_membership', + 'proxy_legacy_analytics', ] for endpoint in staff_level_endpoints: url = reverse(endpoint, kwargs={'course_id': self.course.id}) @@ -753,6 +756,95 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase): self.assertEqual(json.loads(response.content), expected_res) +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(ANALYTICS_SERVER_URL="http://robotanalyticsserver.netbot:900/") +@override_settings(ANALYTICS_API_KEY="robot_api_key") +class TestInstructorAPIAnalyticsProxy(ModuleStoreTestCase, LoginEnrollmentTestCase): + """ + Test instructor analytics proxy endpoint. + """ + + class FakeProxyResponse(object): + """ Fake successful requests response object. """ + def __init__(self): + self.status_code = instructor.views.api.codes.OK + self.content = '{"test_content": "robot test content"}' + + class FakeBadProxyResponse(object): + """ Fake strange-failed requests response object. """ + def __init__(self): + self.status_code = 'notok.' + self.content = '{"test_content": "robot test content"}' + + def setUp(self): + self.instructor = AdminFactory.create() + self.course = CourseFactory.create() + self.client.login(username=self.instructor.username, password='test') + + def test_analytics_proxy_url(self): + """ Test legacy analytics proxy url generation. """ + act = Mock(return_value=self.FakeProxyResponse()) + instructor.views.api.requests.get = act + + url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id}) + response = self.client.get(url, { + 'aname': 'ProblemGradeDistribution' + }) + print response.content + self.assertEqual(response.status_code, 200) + + # check request url + expected_url = "{url}get?aname={aname}&course_id={course_id}&apikey={api_key}".format( + url="http://robotanalyticsserver.netbot:900/", + aname="ProblemGradeDistribution", + course_id=self.course.id, + api_key="robot_api_key", + ) + act.assert_called_once_with(expected_url) + + def test_analytics_proxy(self): + """ + Test legacy analytics content proxying. + """ + act = Mock(return_value=self.FakeProxyResponse()) + instructor.views.api.requests.get = act + + url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id}) + response = self.client.get(url, { + 'aname': 'ProblemGradeDistribution' + }) + print response.content + self.assertEqual(response.status_code, 200) + + # check response + self.assertTrue(act.called) + expected_res = {'test_content': "robot test content"} + self.assertEqual(json.loads(response.content), expected_res) + + def test_analytics_proxy_reqfailed(self): + """ Test proxy when server reponds with failure. """ + act = Mock(return_value=self.FakeBadProxyResponse()) + instructor.views.api.requests.get = act + + url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id}) + response = self.client.get(url, { + 'aname': 'ProblemGradeDistribution' + }) + print response.content + self.assertEqual(response.status_code, 500) + + def test_analytics_proxy_missing_param(self): + """ Test proxy when missing the aname query parameter. """ + act = Mock(return_value=self.FakeProxyResponse()) + instructor.views.api.requests.get = act + + url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id}) + response = self.client.get(url, {}) + print response.content + self.assertEqual(response.status_code, 400) + self.assertFalse(act.called) + + class TestInstructorAPIHelpers(TestCase): """ Test helpers for instructor.api """ def test_split_input_list(self): diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 7655fd5b13..e3d060e57a 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -8,11 +8,15 @@ Many of these GETs may become PUTs in the future. import re import logging +import requests +from requests.status_codes import codes +from collections import OrderedDict +from django.conf import settings from django_future.csrf import ensure_csrf_cookie from django.views.decorators.cache import cache_control from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ -from django.http import HttpResponseBadRequest, HttpResponseForbidden +from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden from util.json_request import JsonResponse from courseware.access import has_access @@ -725,6 +729,57 @@ def update_forum_role_membership(request, course_id): return JsonResponse(response_payload) +@ensure_csrf_cookie +@cache_control(no_cache=True, no_store=True, must_revalidate=True) +@require_level('staff') +@require_query_params( + aname="name of analytic to query", +) +@common_exceptions_400 +def proxy_legacy_analytics(request, course_id): + """ + Proxies to the analytics cron job server. + + `aname` is a query parameter specifying which analytic to query. + """ + analytics_name = request.GET.get('aname') + + # abort if misconfigured + if not (hasattr(settings, 'ANALYTICS_SERVER_URL') and hasattr(settings, 'ANALYTICS_API_KEY')): + return HttpResponse("Analytics service not configured.", status=501) + + url = "{}get?aname={}&course_id={}&apikey={}".format( + settings.ANALYTICS_SERVER_URL, + analytics_name, + course_id, + settings.ANALYTICS_API_KEY, + ) + + try: + res = requests.get(url) + except Exception: + log.exception("Error requesting from analytics server at %s", url) + return HttpResponse("Error requesting from analytics server.", status=500) + + if res.status_code is 200: + # return the successful request content + return HttpResponse(res.content, content_type="application/json") + elif res.status_code is 404: + # forward the 404 and content + return HttpResponse(res.content, content_type="application/json", status=404) + else: + # 500 on all other unexpected status codes. + log.error( + "Error fetching {}, code: {}, msg: {}".format( + url, res.status_code, res.content + ) + ) + return HttpResponse( + "Error from analytics server ({}).".format(res.status_code), + status=500 + ) + + def _split_input_list(str_list): """ Separate out individual student email from the comma, or space separated string. diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py index 8515b60524..8c67c24a77 100644 --- a/lms/djangoapps/instructor/views/api_urls.py +++ b/lms/djangoapps/instructor/views/api_urls.py @@ -30,4 +30,6 @@ urlpatterns = patterns('', # nopep8 'instructor.views.api.list_forum_members', name="list_forum_members"), url(r'^update_forum_role_membership$', 'instructor.views.api.update_forum_role_membership', name="update_forum_role_membership"), + url(r'^proxy_legacy_analytics$', + 'instructor.views.api.proxy_legacy_analytics', name="proxy_legacy_analytics"), ) From 4afde4dd79d6db951bbdad839c793211e1b5a33c Mon Sep 17 00:00:00 2001 From: Miles Steele Date: Thu, 1 Aug 2013 16:49:51 -0400 Subject: [PATCH 037/147] add proxied analytics graphs, refactor analytics --- .../instructor/views/instructor_dashboard.py | 1 + .../src/instructor_dashboard/analytics.coffee | 221 +++++++++++++----- .../instructor_dashboard.coffee | 73 ++++-- .../sass/course/instructor/_instructor_2.scss | 46 ++-- .../instructor_dashboard_2/analytics.html | 62 ++++- 5 files changed, 304 insertions(+), 99 deletions(-) diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index ca89545ec0..c37fb1bc9f 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -142,5 +142,6 @@ def _section_analytics(course_id): 'section_key': 'analytics', 'section_display_name': 'Analytics', 'get_distribution_url': reverse('get_distribution', kwargs={'course_id': course_id}), + 'proxy_legacy_analytics_url': reverse('proxy_legacy_analytics', kwargs={'course_id': course_id}), } return section_data diff --git a/lms/static/coffee/src/instructor_dashboard/analytics.coffee b/lms/static/coffee/src/instructor_dashboard/analytics.coffee index d6e1ffdd3e..3229f51899 100644 --- a/lms/static/coffee/src/instructor_dashboard/analytics.coffee +++ b/lms/static/coffee/src/instructor_dashboard/analytics.coffee @@ -6,60 +6,32 @@ plantTimeout = -> window.InstructorDashboard.util.plantTimeout.apply this, arguments std_ajax_err = -> window.InstructorDashboard.util.std_ajax_err.apply this, arguments -# Analytics Section -class Analytics - constructor: (@$section) -> - @$section.data 'wrapper', @ - # gather elements - @$display = @$section.find '.distribution-display' - @$display_text = @$display.find '.distribution-display-text' - @$display_graph = @$display.find '.distribution-display-graph' - @$display_table = @$display.find '.distribution-display-table' - @$distribution_select = @$section.find 'select#distributions' - @$request_response_error = @$display.find '.request-response-error' - - @populate_selector => @$distribution_select.change => @on_selector_change() +class ProfileDistributionWidget + constructor: ({@$container, @feature, title, @endpoint}) -> + # render template + template_params = + title: title + feature: @feature + endpoint: @endpoint + template_html = $("#profile-distribution-widget-template").html() + @$container.html Mustache.render template_html, template_params reset_display: -> - @$display_text.empty() - @$display_graph.empty() - @$display_table.empty() - @$request_response_error.empty() + @$container.find('.display-errors').empty() + @$container.find('.display-text').empty() + @$container.find('.display-graph').empty() + @$container.find('.display-table').empty() - # fetch and list available distributions - # `cb` is a callback to be run after - populate_selector: (cb) -> - # ask for no particular distribution to get list of available distribuitions. - @get_profile_distributions undefined, - # on error, print to console and dom. - error: std_ajax_err => @$request_response_error.text "Error getting available distributions." - success: (data) => - # replace loading text in drop-down with "-- Select Distribution --" - @$distribution_select.find('option').eq(0).text "-- Select Distribution --" - - # add all fetched available features to drop-down - for feature in data.available_features - opt = $ '